@rhize/skill-forge 0.13.0 → 0.16.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/LICENSE +7 -0
- package/README.md +63 -930
- package/dist/cli.js +3159 -333
- package/dist/cli.js.map +1 -1
- package/dist/workflows/source-insight-planning/SKILL.md +130 -0
- package/dist/workflows/source-insight-planning/agents/openai.yaml +6 -0
- package/dist/workflows/source-insight-planning/references/capability-mapping.md +65 -0
- package/dist/workflows/source-insight-planning/references/jira-and-measurement.md +70 -0
- package/dist/workflows/source-insight-planning/references/source-and-evidence.md +87 -0
- package/dist/workflows/source-insight-planning/scripts/claude-source-insight-hook.sh +29 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createRequire } from "module";
|
|
|
8
8
|
import { join as join21 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
12
12
|
|
|
13
13
|
// src/errors.ts
|
|
14
14
|
var UserError = class extends Error {
|
|
@@ -218,10 +218,10 @@ function validatePreference(key, value) {
|
|
|
218
218
|
if (/[\u0000-\u001F\u007F-\u009F]/.test(value)) {
|
|
219
219
|
throw new UserError(`preference "${key}" contains control characters \u2014 refusing to store terminal escape sequences`);
|
|
220
220
|
}
|
|
221
|
-
for (const { pattern, label } of SECRET_VALUE_PATTERNS) {
|
|
221
|
+
for (const { pattern, label: label2 } of SECRET_VALUE_PATTERNS) {
|
|
222
222
|
if (pattern.test(value)) {
|
|
223
223
|
throw new UserError(
|
|
224
|
-
`refusing to store "${key}": the value looks like ${
|
|
224
|
+
`refusing to store "${key}": the value looks like ${label2}, and config.json is plain text. Keep secrets in your OS keychain or an env var.`
|
|
225
225
|
);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
@@ -418,14 +418,30 @@ function saveConfig(config) {
|
|
|
418
418
|
if (!existsSync(home)) {
|
|
419
419
|
mkdirSync(home, { recursive: true });
|
|
420
420
|
}
|
|
421
|
-
|
|
421
|
+
const path = getConfigPath();
|
|
422
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
423
|
+
chmodSync(path, 384);
|
|
422
424
|
}
|
|
423
425
|
|
|
424
426
|
// src/install/resolve.ts
|
|
425
427
|
import { existsSync as existsSync2 } from "fs";
|
|
426
428
|
|
|
427
429
|
// src/util.ts
|
|
428
|
-
import {
|
|
430
|
+
import {
|
|
431
|
+
accessSync,
|
|
432
|
+
closeSync,
|
|
433
|
+
constants,
|
|
434
|
+
fchmodSync,
|
|
435
|
+
fstatSync,
|
|
436
|
+
lstatSync,
|
|
437
|
+
mkdirSync as mkdirSync2,
|
|
438
|
+
openSync,
|
|
439
|
+
renameSync,
|
|
440
|
+
rmSync,
|
|
441
|
+
statSync as statSync2,
|
|
442
|
+
writeSync,
|
|
443
|
+
writeFileSync as writeFileSync2
|
|
444
|
+
} from "fs";
|
|
429
445
|
import { homedir as homedir2 } from "os";
|
|
430
446
|
import { basename, delimiter, dirname, join as join3 } from "path";
|
|
431
447
|
import { randomUUID } from "crypto";
|
|
@@ -484,6 +500,48 @@ function writeNewFile(path, contents, hint) {
|
|
|
484
500
|
throw err;
|
|
485
501
|
}
|
|
486
502
|
}
|
|
503
|
+
function appendPrivateJsonLine(path, value) {
|
|
504
|
+
const line = Buffer.from(`${JSON.stringify(value)}
|
|
505
|
+
`, "utf-8");
|
|
506
|
+
let priorStat;
|
|
507
|
+
try {
|
|
508
|
+
priorStat = lstatSync(path);
|
|
509
|
+
if (priorStat.isSymbolicLink()) throw new UserError(`refusing to append through a symlink: ${path}`);
|
|
510
|
+
if (!priorStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (error.code !== "ENOENT") throw error;
|
|
513
|
+
}
|
|
514
|
+
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
515
|
+
let descriptor;
|
|
516
|
+
try {
|
|
517
|
+
descriptor = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | noFollow, 384);
|
|
518
|
+
const descriptorStat = fstatSync(descriptor);
|
|
519
|
+
if (!descriptorStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
|
|
520
|
+
const openedPathStat = lstatSync(path);
|
|
521
|
+
if (openedPathStat.isSymbolicLink()) throw new UserError(`refusing to append through a symlink: ${path}`);
|
|
522
|
+
if (!openedPathStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
|
|
523
|
+
if (openedPathStat.dev !== descriptorStat.dev || openedPathStat.ino !== descriptorStat.ino) {
|
|
524
|
+
throw new UserError(`append destination changed while opening: ${path}`);
|
|
525
|
+
}
|
|
526
|
+
if (priorStat && (priorStat.dev !== descriptorStat.dev || priorStat.ino !== descriptorStat.ino)) {
|
|
527
|
+
throw new UserError(`append destination changed while opening: ${path}`);
|
|
528
|
+
}
|
|
529
|
+
fchmodSync(descriptor, 384);
|
|
530
|
+
let offset = 0;
|
|
531
|
+
while (offset < line.byteLength) {
|
|
532
|
+
const written = writeSync(descriptor, line, offset, line.byteLength - offset);
|
|
533
|
+
if (written === 0) throw new Error(`unable to append JSONL record to ${path}`);
|
|
534
|
+
offset += written;
|
|
535
|
+
}
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error.code === "ELOOP") {
|
|
538
|
+
throw new UserError(`refusing to append through a symlink: ${path}`);
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
} finally {
|
|
542
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
487
545
|
|
|
488
546
|
// src/install/resolve.ts
|
|
489
547
|
var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
|
|
@@ -659,7 +717,7 @@ import { basename as basename3, join as join6 } from "path";
|
|
|
659
717
|
|
|
660
718
|
// src/install/fingerprint.ts
|
|
661
719
|
import { createHash } from "crypto";
|
|
662
|
-
import { lstatSync, readFileSync as readFileSync3, readdirSync as readdirSync3, readlinkSync, realpathSync } from "fs";
|
|
720
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync3, readdirSync as readdirSync3, readlinkSync, realpathSync } from "fs";
|
|
663
721
|
import { isAbsolute, join as join5, relative, sep } from "path";
|
|
664
722
|
var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
665
723
|
function isInside(child, root) {
|
|
@@ -730,7 +788,7 @@ function fingerprintDirs(dirs) {
|
|
|
730
788
|
}
|
|
731
789
|
function isRealDirectory(path) {
|
|
732
790
|
try {
|
|
733
|
-
return
|
|
791
|
+
return lstatSync2(path).isDirectory();
|
|
734
792
|
} catch {
|
|
735
793
|
return false;
|
|
736
794
|
}
|
|
@@ -971,8 +1029,8 @@ function appendProvenance(entry, skillsRoot) {
|
|
|
971
1029
|
appendFileSync(ledgerPath, block, "utf-8");
|
|
972
1030
|
return ledgerPath;
|
|
973
1031
|
}
|
|
974
|
-
function extractField(block,
|
|
975
|
-
const escaped =
|
|
1032
|
+
function extractField(block, label2) {
|
|
1033
|
+
const escaped = label2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
976
1034
|
const match = block.match(new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.*)`));
|
|
977
1035
|
return match ? match[1].trim() : void 0;
|
|
978
1036
|
}
|
|
@@ -1031,7 +1089,7 @@ import { basename as basename7, dirname as dirname5, join as join13 } from "path
|
|
|
1031
1089
|
// src/audit.ts
|
|
1032
1090
|
import {
|
|
1033
1091
|
existsSync as existsSync9,
|
|
1034
|
-
lstatSync as
|
|
1092
|
+
lstatSync as lstatSync3,
|
|
1035
1093
|
mkdtempSync,
|
|
1036
1094
|
readFileSync as readFileSync9,
|
|
1037
1095
|
readdirSync as readdirSync5,
|
|
@@ -1465,14 +1523,14 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1465
1523
|
`.split(/\s+/).filter(Boolean)
|
|
1466
1524
|
);
|
|
1467
1525
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1468
|
-
function splitLimited(text,
|
|
1526
|
+
function splitLimited(text, sep7, maxSplit) {
|
|
1469
1527
|
const parts = [];
|
|
1470
1528
|
let rest = text;
|
|
1471
1529
|
for (let i = 0; i < maxSplit; i++) {
|
|
1472
|
-
const idx = rest.indexOf(
|
|
1530
|
+
const idx = rest.indexOf(sep7);
|
|
1473
1531
|
if (idx === -1) break;
|
|
1474
1532
|
parts.push(rest.slice(0, idx));
|
|
1475
|
-
rest = rest.slice(idx +
|
|
1533
|
+
rest = rest.slice(idx + sep7.length);
|
|
1476
1534
|
}
|
|
1477
1535
|
parts.push(rest);
|
|
1478
1536
|
return parts;
|
|
@@ -2570,7 +2628,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
|
|
|
2570
2628
|
const destDir = join12(resolvedRoot, "business-foundation");
|
|
2571
2629
|
let destLstat;
|
|
2572
2630
|
try {
|
|
2573
|
-
destLstat =
|
|
2631
|
+
destLstat = lstatSync3(destDir);
|
|
2574
2632
|
} catch {
|
|
2575
2633
|
destLstat = void 0;
|
|
2576
2634
|
}
|
|
@@ -2742,10 +2800,10 @@ import { join as join16 } from "path";
|
|
|
2742
2800
|
// src/gate/profile.ts
|
|
2743
2801
|
import { execFileSync } from "child_process";
|
|
2744
2802
|
import {
|
|
2745
|
-
closeSync,
|
|
2803
|
+
closeSync as closeSync2,
|
|
2746
2804
|
existsSync as existsSync12,
|
|
2747
2805
|
mkdtempSync as mkdtempSync2,
|
|
2748
|
-
openSync,
|
|
2806
|
+
openSync as openSync2,
|
|
2749
2807
|
readFileSync as readFileSync12,
|
|
2750
2808
|
readdirSync as readdirSync6,
|
|
2751
2809
|
readSync,
|
|
@@ -3073,10 +3131,10 @@ function findSkillMdOneLevelDown(dir) {
|
|
|
3073
3131
|
}
|
|
3074
3132
|
function isZipFile(p) {
|
|
3075
3133
|
try {
|
|
3076
|
-
const fd =
|
|
3134
|
+
const fd = openSync2(p, "r");
|
|
3077
3135
|
const buf = Buffer.alloc(4);
|
|
3078
3136
|
const bytesRead = readSync(fd, buf, 0, 4, 0);
|
|
3079
|
-
|
|
3137
|
+
closeSync2(fd);
|
|
3080
3138
|
if (bytesRead < 4) return false;
|
|
3081
3139
|
return buf[0] === 80 && buf[1] === 75 && (buf[2] === 3 || buf[2] === 5 || buf[2] === 7);
|
|
3082
3140
|
} catch {
|
|
@@ -3179,8 +3237,8 @@ function detectLicense(root, fm) {
|
|
|
3179
3237
|
const p = join15(root, name);
|
|
3180
3238
|
if (existsSync12(p) && statSync7(p).isFile()) {
|
|
3181
3239
|
const head = readFileSync12(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
3182
|
-
for (const [hint,
|
|
3183
|
-
if (head.includes(hint)) return
|
|
3240
|
+
for (const [hint, label2] of Object.entries(SPDX_HINTS)) {
|
|
3241
|
+
if (head.includes(hint)) return label2;
|
|
3184
3242
|
}
|
|
3185
3243
|
return "present but unrecognized";
|
|
3186
3244
|
}
|
|
@@ -3458,8 +3516,8 @@ function tagsForNode(map, node) {
|
|
|
3458
3516
|
if (edge.from !== node.id) continue;
|
|
3459
3517
|
if (edge.type !== "topic-tag" && edge.type !== "stack-tag") continue;
|
|
3460
3518
|
const tagNode = nodeById(map, edge.to);
|
|
3461
|
-
const
|
|
3462
|
-
if (
|
|
3519
|
+
const label2 = tagNode?.name ?? tagNode?.id;
|
|
3520
|
+
if (label2) tags.add(label2);
|
|
3463
3521
|
}
|
|
3464
3522
|
return Array.from(tags);
|
|
3465
3523
|
}
|
|
@@ -3505,18 +3563,23 @@ function nodeText(map, node) {
|
|
|
3505
3563
|
}
|
|
3506
3564
|
function scanMapOverlap(candidate, map) {
|
|
3507
3565
|
const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
|
|
3508
|
-
if (candidateTokens.size === 0) return { matches: [] };
|
|
3566
|
+
if (candidateTokens.size === 0) return { matches: [], compared: 0 };
|
|
3509
3567
|
const matches = [];
|
|
3568
|
+
let compared = 0;
|
|
3569
|
+
let topMatch;
|
|
3510
3570
|
for (const node of map.nodes) {
|
|
3511
3571
|
if (node.type !== "skill") continue;
|
|
3572
|
+
compared += 1;
|
|
3512
3573
|
const otherTokens = tokenize2(nodeText(map, node));
|
|
3513
3574
|
const s = score3(candidateTokens, otherTokens);
|
|
3575
|
+
const match = { skillId: node.id, name: node.name ?? node.id, score: s };
|
|
3576
|
+
if (!topMatch || s > topMatch.score) topMatch = match;
|
|
3514
3577
|
if (s >= REPORT_THRESHOLD) {
|
|
3515
|
-
matches.push(
|
|
3578
|
+
matches.push(match);
|
|
3516
3579
|
}
|
|
3517
3580
|
}
|
|
3518
3581
|
matches.sort((a, b) => b.score - a.score);
|
|
3519
|
-
return { matches };
|
|
3582
|
+
return { matches, compared, topMatch };
|
|
3520
3583
|
}
|
|
3521
3584
|
function mapOverlapFinding(result) {
|
|
3522
3585
|
const top = result.matches[0];
|
|
@@ -3569,9 +3632,9 @@ function renderListField(labelText, items, placeholder) {
|
|
|
3569
3632
|
if (!items || items.length === 0) {
|
|
3570
3633
|
return `${head}${placeholder}`;
|
|
3571
3634
|
}
|
|
3572
|
-
const
|
|
3635
|
+
const bullets2 = items.map((item) => ` - ${item}`).join("\n");
|
|
3573
3636
|
return `${head.trimEnd()}
|
|
3574
|
-
${
|
|
3637
|
+
${bullets2}`;
|
|
3575
3638
|
}
|
|
3576
3639
|
function renderSafetyLine(safety) {
|
|
3577
3640
|
const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
|
|
@@ -3608,6 +3671,17 @@ function renderCapabilitiesLine(profile) {
|
|
|
3608
3671
|
);
|
|
3609
3672
|
return `${padLabel("Capabilities")}: ${counts} (declaredConfidence: ${declaredConfidence})`;
|
|
3610
3673
|
}
|
|
3674
|
+
function renderMapOverlapLines(report) {
|
|
3675
|
+
if (!report || report.status === "not-requested") return [];
|
|
3676
|
+
if (report.status === "skipped") {
|
|
3677
|
+
return [`${padLabel("Marketplace overlap")}: skipped \u2014 ${report.reason ?? "map unavailable"}`];
|
|
3678
|
+
}
|
|
3679
|
+
const top = report.topMatch;
|
|
3680
|
+
const flagged = report.matches?.length ?? 0;
|
|
3681
|
+
const closest = top ? `closest "${top.name}" (${top.score})` : "no comparable skills";
|
|
3682
|
+
const verdict = flagged > 0 ? `${flagged} at/over threshold` : "none over threshold";
|
|
3683
|
+
return [`${padLabel("Marketplace overlap")}: checked ${report.compared ?? 0} skills \u2014 ${closest}, ${verdict}`];
|
|
3684
|
+
}
|
|
3611
3685
|
function renderReport(result) {
|
|
3612
3686
|
const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
|
|
3613
3687
|
const { name, version, license } = profileHeaderFields(profile);
|
|
@@ -3622,6 +3696,7 @@ function renderReport(result) {
|
|
|
3622
3696
|
`${padLabel("Artifact type")}: ${profile.artifactType}`,
|
|
3623
3697
|
...profile.kind === "mcp" ? [renderCapabilitiesLine(profile)] : [],
|
|
3624
3698
|
...overlapLines,
|
|
3699
|
+
...renderMapOverlapLines(result.mapOverlap),
|
|
3625
3700
|
renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
|
|
3626
3701
|
renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
|
|
3627
3702
|
`${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
|
|
@@ -3676,13 +3751,23 @@ function tryMcpOverlap(profile, mcpTargets) {
|
|
|
3676
3751
|
}
|
|
3677
3752
|
}
|
|
3678
3753
|
function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
|
|
3679
|
-
if (!skillMapPath) return
|
|
3754
|
+
if (!skillMapPath) return { report: { status: "not-requested" } };
|
|
3680
3755
|
try {
|
|
3681
3756
|
const map = loadSkillMap(skillMapPath);
|
|
3682
|
-
|
|
3757
|
+
const result = scanMapOverlap(candidate, map);
|
|
3758
|
+
return {
|
|
3759
|
+
decision: mapOverlapDecision(result, extendsDeclarations),
|
|
3760
|
+
report: {
|
|
3761
|
+
status: "checked",
|
|
3762
|
+
compared: result.compared,
|
|
3763
|
+
topMatch: result.topMatch,
|
|
3764
|
+
matches: result.matches
|
|
3765
|
+
}
|
|
3766
|
+
};
|
|
3683
3767
|
} catch (err) {
|
|
3684
|
-
|
|
3685
|
-
|
|
3768
|
+
const reason = err.message;
|
|
3769
|
+
console.error(`[skill-forge] skill-map overlap check skipped: ${reason}`);
|
|
3770
|
+
return { report: { status: "skipped", reason } };
|
|
3686
3771
|
}
|
|
3687
3772
|
}
|
|
3688
3773
|
function resolveMcpCandidatePath(entry) {
|
|
@@ -3705,7 +3790,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3705
3790
|
const proStatus = proFeatureStatus(proFeatureLabel);
|
|
3706
3791
|
const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
|
|
3707
3792
|
const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
|
|
3708
|
-
const
|
|
3793
|
+
const mapOutcome = artifact === "mcp" ? { decision: void 0, report: { status: "not-requested" } } : tryMapOverlapDecision(
|
|
3709
3794
|
skillMapPath,
|
|
3710
3795
|
{
|
|
3711
3796
|
name: "name" in profile ? profile.name : void 0,
|
|
@@ -3713,6 +3798,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3713
3798
|
},
|
|
3714
3799
|
profile.kind === "skill" ? profile.extends : []
|
|
3715
3800
|
);
|
|
3801
|
+
const mapDecision = mapOutcome.decision;
|
|
3716
3802
|
const mapFinding = mapDecision?.finding;
|
|
3717
3803
|
const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
|
|
3718
3804
|
const advisory = formatAuditAdvisory(readAuditState(), {
|
|
@@ -3728,6 +3814,7 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3728
3814
|
overlap,
|
|
3729
3815
|
overlapLocked: !proStatus.enabled,
|
|
3730
3816
|
notices: notices.length > 0 ? notices : void 0,
|
|
3817
|
+
mapOverlap: mapOutcome.report,
|
|
3731
3818
|
safety: {
|
|
3732
3819
|
verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
|
|
3733
3820
|
findings: mapSafetyFindings(combinedSafety.findings)
|
|
@@ -3807,11 +3894,14 @@ function generateId(existingIds) {
|
|
|
3807
3894
|
// src/handoff.ts
|
|
3808
3895
|
import { spawnSync } from "child_process";
|
|
3809
3896
|
import { existsSync as existsSync15 } from "fs";
|
|
3810
|
-
import { dirname as dirname7, join as join18 } from "path";
|
|
3897
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join18, win32 } from "path";
|
|
3811
3898
|
import { fileURLToPath } from "url";
|
|
3812
3899
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
3813
3900
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
3814
3901
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
3902
|
+
if (assetName.length === 0 || assetName.includes("\0") || isAbsolute2(assetName) || win32.isAbsolute(assetName) || assetName.split(/[\\/]/).includes("..")) {
|
|
3903
|
+
throw new UserError(`Invalid handoff prompt asset path: ${assetName}`);
|
|
3904
|
+
}
|
|
3815
3905
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
3816
3906
|
const builtCandidate = join18(here, assetName);
|
|
3817
3907
|
if (existsSync15(builtCandidate)) return builtCandidate;
|
|
@@ -3828,29 +3918,35 @@ function substitute(template, vars) {
|
|
|
3828
3918
|
function buildHandoffCommand(config, installedPath, deps = {}) {
|
|
3829
3919
|
const isOnPath = deps.isOnPath ?? defaultIsOnPath;
|
|
3830
3920
|
const promptPath = resolvePromptPath(deps.promptAsset);
|
|
3831
|
-
const
|
|
3921
|
+
const baseVars = { path: installedPath, prompt: promptPath };
|
|
3922
|
+
const instruction = substitute([deps.instruction ?? HANDOFF_INSTRUCTION], baseVars)[0];
|
|
3923
|
+
const vars = { ...baseVars, instruction };
|
|
3832
3924
|
if (config.handoffCommand && config.handoffCommand.length > 0) {
|
|
3833
|
-
|
|
3925
|
+
const template = config.handoffCommand.map(
|
|
3926
|
+
(token) => deps.instruction !== void 0 && token === HANDOFF_INSTRUCTION ? "{instruction}" : token
|
|
3927
|
+
);
|
|
3928
|
+
return { argv: substitute(template, vars), source: "template" };
|
|
3834
3929
|
}
|
|
3835
3930
|
for (const bin of KNOWN_AGENT_BINARIES) {
|
|
3836
3931
|
if (isOnPath(bin)) {
|
|
3837
|
-
|
|
3932
|
+
const template = bin === "codex" ? [bin, "exec", "{instruction}"] : [bin, "-p", "{instruction}"];
|
|
3933
|
+
return { argv: substitute(template, vars), source: "detected" };
|
|
3838
3934
|
}
|
|
3839
3935
|
}
|
|
3840
3936
|
return { argv: [], source: "none" };
|
|
3841
3937
|
}
|
|
3842
|
-
function printOnlyMessage(installedPath, promptAsset) {
|
|
3938
|
+
function printOnlyMessage(installedPath, promptAsset, targetLabel = "Skill path") {
|
|
3843
3939
|
const promptPath = resolvePromptPath(promptAsset);
|
|
3844
3940
|
return `[skill-forge] no agent handoff available \u2014 run one yourself:
|
|
3845
3941
|
Prompt: ${promptPath}
|
|
3846
|
-
|
|
3942
|
+
${targetLabel}: ${installedPath}`;
|
|
3847
3943
|
}
|
|
3848
3944
|
function runHandoff(config, installedPath, options = {}) {
|
|
3849
3945
|
const command = buildHandoffCommand(config, installedPath, options);
|
|
3850
3946
|
const spawn2 = options.spawn ?? spawnSync;
|
|
3851
3947
|
const log = options.log ?? console.log;
|
|
3852
3948
|
if (command.source === "none" || command.argv.length === 0) {
|
|
3853
|
-
const printed = printOnlyMessage(installedPath, options.promptAsset);
|
|
3949
|
+
const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
|
|
3854
3950
|
log(printed);
|
|
3855
3951
|
return { ran: false, command, printed };
|
|
3856
3952
|
}
|
|
@@ -3858,7 +3954,7 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
3858
3954
|
const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
|
|
3859
3955
|
const error = result.error;
|
|
3860
3956
|
if (error?.code === "ENOENT") {
|
|
3861
|
-
const printed = printOnlyMessage(installedPath, options.promptAsset);
|
|
3957
|
+
const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
|
|
3862
3958
|
log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
|
|
3863
3959
|
log(printed);
|
|
3864
3960
|
return { ran: false, command, printed, error: error.message };
|
|
@@ -4121,6 +4217,136 @@ var AGENTS = [
|
|
|
4121
4217
|
{ id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
|
|
4122
4218
|
].map(makeAgent);
|
|
4123
4219
|
|
|
4220
|
+
// src/gitPreflight.ts
|
|
4221
|
+
import { relative as relative2, sep as sep2 } from "path";
|
|
4222
|
+
function splitNulTerminated(output) {
|
|
4223
|
+
return output.split("\0").filter((s) => s.length > 0);
|
|
4224
|
+
}
|
|
4225
|
+
function toPosixPath(p) {
|
|
4226
|
+
return p.split(sep2).join("/");
|
|
4227
|
+
}
|
|
4228
|
+
var missingResult = (root) => ({
|
|
4229
|
+
root,
|
|
4230
|
+
state: "MISSING",
|
|
4231
|
+
baseline: "NO_HEAD",
|
|
4232
|
+
dirty: false,
|
|
4233
|
+
otherStaged: false,
|
|
4234
|
+
rollbackReady: false
|
|
4235
|
+
});
|
|
4236
|
+
var notInRepoResult = (root) => ({
|
|
4237
|
+
root,
|
|
4238
|
+
state: "NOT_IN_REPO",
|
|
4239
|
+
baseline: "NO_HEAD",
|
|
4240
|
+
dirty: false,
|
|
4241
|
+
otherStaged: false,
|
|
4242
|
+
rollbackReady: false
|
|
4243
|
+
});
|
|
4244
|
+
async function evaluateGitPreflight(root, deps = {}) {
|
|
4245
|
+
const exec = deps.exec ?? defaultExec;
|
|
4246
|
+
if (!isDir(root)) return missingResult(root);
|
|
4247
|
+
let toplevel;
|
|
4248
|
+
try {
|
|
4249
|
+
toplevel = await exec("git", ["-C", root, "rev-parse", "--show-toplevel"], { cwd: root });
|
|
4250
|
+
} catch {
|
|
4251
|
+
return notInRepoResult(root);
|
|
4252
|
+
}
|
|
4253
|
+
if (toplevel.code !== 0) return notInRepoResult(root);
|
|
4254
|
+
const repoRoot = toplevel.stdout.trim();
|
|
4255
|
+
if (!repoRoot) return notInRepoResult(root);
|
|
4256
|
+
const rel = toPosixPath(relative2(repoRoot, root));
|
|
4257
|
+
const pathspec = rel === "" ? "." : rel;
|
|
4258
|
+
const tracked = await exec("git", ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--", pathspec], { cwd: repoRoot });
|
|
4259
|
+
const untracked = await exec(
|
|
4260
|
+
"git",
|
|
4261
|
+
["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--others", "--exclude-standard", "--", pathspec],
|
|
4262
|
+
{ cwd: repoRoot }
|
|
4263
|
+
);
|
|
4264
|
+
const ignored = await exec(
|
|
4265
|
+
"git",
|
|
4266
|
+
["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", pathspec],
|
|
4267
|
+
{ cwd: repoRoot }
|
|
4268
|
+
);
|
|
4269
|
+
let rootIgnored = false;
|
|
4270
|
+
if (rel !== "") {
|
|
4271
|
+
const checkIgnore = await exec("git", ["-C", repoRoot, "check-ignore", "--no-index", "-q", "--", pathspec], { cwd: repoRoot });
|
|
4272
|
+
rootIgnored = checkIgnore.code === 0;
|
|
4273
|
+
}
|
|
4274
|
+
const hasTracked = splitNulTerminated(tracked.stdout).length > 0;
|
|
4275
|
+
const hasUntracked = splitNulTerminated(untracked.stdout).length > 0;
|
|
4276
|
+
const hasIgnored = splitNulTerminated(ignored.stdout).length > 0 || rootIgnored;
|
|
4277
|
+
let state;
|
|
4278
|
+
if (hasTracked && hasUntracked) state = "MIXED";
|
|
4279
|
+
else if (hasTracked) state = "TRACKED";
|
|
4280
|
+
else if (hasUntracked) state = "UNTRACKED";
|
|
4281
|
+
else if (hasIgnored) state = "IGNORED";
|
|
4282
|
+
else state = "UNTRACKED";
|
|
4283
|
+
const headCheck = await exec("git", ["-C", repoRoot, "rev-parse", "--verify", "HEAD"], { cwd: repoRoot });
|
|
4284
|
+
const hasHead = headCheck.code === 0;
|
|
4285
|
+
const diffCached = await exec("git", ["-C", repoRoot, "diff", "--cached", "--name-only", "-z"], { cwd: repoRoot });
|
|
4286
|
+
const stagedFiles = splitNulTerminated(diffCached.stdout);
|
|
4287
|
+
const otherStaged = rel !== "" && stagedFiles.some((f) => f !== pathspec && !f.startsWith(`${pathspec}/`));
|
|
4288
|
+
if (!hasHead) {
|
|
4289
|
+
return { root, repoRoot, state, baseline: "NO_HEAD", dirty: hasTracked || hasUntracked, otherStaged, rollbackReady: false };
|
|
4290
|
+
}
|
|
4291
|
+
const committed = await exec(
|
|
4292
|
+
"git",
|
|
4293
|
+
["--literal-pathspecs", "-C", repoRoot, "ls-tree", "-rz", "--name-only", "HEAD", "--", pathspec],
|
|
4294
|
+
{ cwd: repoRoot }
|
|
4295
|
+
);
|
|
4296
|
+
const hasCommitted = splitNulTerminated(committed.stdout).length > 0;
|
|
4297
|
+
const statusRes = await exec(
|
|
4298
|
+
"git",
|
|
4299
|
+
["--literal-pathspecs", "-C", repoRoot, "status", "--porcelain", "-z", "--", pathspec],
|
|
4300
|
+
{ cwd: repoRoot }
|
|
4301
|
+
);
|
|
4302
|
+
const dirty = splitNulTerminated(statusRes.stdout).length > 0;
|
|
4303
|
+
const baseline = !hasCommitted ? "UNCOMMITTED" : dirty ? "PARTIAL" : "COMMITTED";
|
|
4304
|
+
return { root, repoRoot, state, baseline, dirty, otherStaged, rollbackReady: hasCommitted };
|
|
4305
|
+
}
|
|
4306
|
+
function quoteForDisplay(arg) {
|
|
4307
|
+
return /\s/.test(arg) ? `"${arg}"` : arg;
|
|
4308
|
+
}
|
|
4309
|
+
function formatGitCommand(cmd) {
|
|
4310
|
+
return `git ${cmd.args.map(quoteForDisplay).join(" ")}`;
|
|
4311
|
+
}
|
|
4312
|
+
var BASELINE_COMMIT_MESSAGE = "skill-forge init: baseline";
|
|
4313
|
+
function notInRepoInitCommands(root) {
|
|
4314
|
+
return [
|
|
4315
|
+
{ args: ["init", "--", root], cwd: root },
|
|
4316
|
+
{ args: ["--literal-pathspecs", "-C", root, "add", "--", "."], cwd: root },
|
|
4317
|
+
{ args: ["--literal-pathspecs", "-C", root, "commit", "-m", BASELINE_COMMIT_MESSAGE, "--", "."], cwd: root }
|
|
4318
|
+
];
|
|
4319
|
+
}
|
|
4320
|
+
function untrackedAddCommitCommands(root, repoRoot) {
|
|
4321
|
+
return [
|
|
4322
|
+
{ args: ["--literal-pathspecs", "-C", repoRoot, "add", "--", root], cwd: repoRoot },
|
|
4323
|
+
{ args: ["--literal-pathspecs", "-C", repoRoot, "commit", "-m", BASELINE_COMMIT_MESSAGE, "--", root], cwd: repoRoot }
|
|
4324
|
+
];
|
|
4325
|
+
}
|
|
4326
|
+
function ignoredUnignoreSuggestions(root, repoRoot) {
|
|
4327
|
+
const rel = toPosixPath(relative2(repoRoot, root));
|
|
4328
|
+
return [`!${rel}/`, `!${rel}/**`];
|
|
4329
|
+
}
|
|
4330
|
+
async function runGitCommands(commands, deps = {}) {
|
|
4331
|
+
const exec = deps.exec ?? defaultExec;
|
|
4332
|
+
const results = [];
|
|
4333
|
+
for (const cmd of commands) {
|
|
4334
|
+
const result = await exec("git", cmd.args, { cwd: cmd.cwd });
|
|
4335
|
+
results.push(result);
|
|
4336
|
+
if (result.code !== 0) break;
|
|
4337
|
+
}
|
|
4338
|
+
return results;
|
|
4339
|
+
}
|
|
4340
|
+
function describeGitPreflight(result) {
|
|
4341
|
+
if (result.state === "MISSING") return "root does not exist";
|
|
4342
|
+
if (result.state === "NOT_IN_REPO") return "not in a Git repository";
|
|
4343
|
+
const parts = [result.state.toLowerCase()];
|
|
4344
|
+
if (result.state !== "IGNORED") parts.push(result.baseline.toLowerCase());
|
|
4345
|
+
if (result.dirty) parts.push("dirty");
|
|
4346
|
+
if (result.otherStaged) parts.push("other staged changes present");
|
|
4347
|
+
return parts.join(", ");
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4124
4350
|
// src/commands/audit.ts
|
|
4125
4351
|
import { existsSync as existsSync16 } from "fs";
|
|
4126
4352
|
import { join as join20, resolve as resolvePath3 } from "path";
|
|
@@ -4364,6 +4590,15 @@ function presenceNote(presence) {
|
|
|
4364
4590
|
if (presence === "absent") return " [CLI not on PATH]";
|
|
4365
4591
|
return "";
|
|
4366
4592
|
}
|
|
4593
|
+
async function printGitDetectionColumn(detected, deps = {}) {
|
|
4594
|
+
const groups = groupByRoot(detected);
|
|
4595
|
+
if (groups.length === 0) return;
|
|
4596
|
+
console.log("\n[skill-forge] init \u2014 version control");
|
|
4597
|
+
for (const group of groups) {
|
|
4598
|
+
const result = await evaluateGitPreflight(group.root, deps);
|
|
4599
|
+
console.log(` ${group.root} \u2014 ${describeGitPreflight(result)}`);
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4367
4602
|
function printMcpDetectionResults(detectedMcp) {
|
|
4368
4603
|
if (detectedMcp.length === 0) return;
|
|
4369
4604
|
console.log("\n[skill-forge] init \u2014 MCP config detection");
|
|
@@ -4393,13 +4628,17 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
|
|
|
4393
4628
|
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
|
|
4394
4629
|
};
|
|
4395
4630
|
}
|
|
4396
|
-
function printSummary(config) {
|
|
4631
|
+
async function printSummary(config, deps = {}) {
|
|
4397
4632
|
console.log(`
|
|
4398
4633
|
[skill-forge] wrote config to ${getConfigPath()}`);
|
|
4399
4634
|
console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
|
|
4400
4635
|
if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
|
|
4401
4636
|
if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
|
|
4402
4637
|
if (config.mcpTargets?.length) console.log(` mcp targets: ${config.mcpTargets.join(", ")}`);
|
|
4638
|
+
for (const root of config.skillsRoots) {
|
|
4639
|
+
const result = await evaluateGitPreflight(root, deps);
|
|
4640
|
+
console.log(` version control: ${root} \u2014 ${describeGitPreflight(result)}`);
|
|
4641
|
+
}
|
|
4403
4642
|
}
|
|
4404
4643
|
function isPromptAbort(err) {
|
|
4405
4644
|
return err instanceof Error && (err.name === "AbortError" || err.code === "ABORT_ERR");
|
|
@@ -4515,8 +4754,41 @@ async function promptHandoffAgent(rl, detected, narrow) {
|
|
|
4515
4754
|
const idx = await promptSingleChoice(rl, `Choose [1-${ordered.length}] or Enter to skip: `, ordered.length, "skip");
|
|
4516
4755
|
return idx === void 0 ? void 0 : ordered[idx].agent;
|
|
4517
4756
|
}
|
|
4757
|
+
async function promptGitPreflight(rl, selected, deps = {}) {
|
|
4758
|
+
for (const group of selected) {
|
|
4759
|
+
const result = await evaluateGitPreflight(group.root, deps);
|
|
4760
|
+
if (result.state === "IGNORED") {
|
|
4761
|
+
console.log(`
|
|
4762
|
+
${group.root} is excluded by a .gitignore pattern.`);
|
|
4763
|
+
console.log(" To track it, add these lines to the relevant .gitignore:");
|
|
4764
|
+
for (const line of ignoredUnignoreSuggestions(group.root, result.repoRoot ?? group.root)) {
|
|
4765
|
+
console.log(` ${line}`);
|
|
4766
|
+
}
|
|
4767
|
+
continue;
|
|
4768
|
+
}
|
|
4769
|
+
const commands = result.state === "NOT_IN_REPO" ? notInRepoInitCommands(group.root) : result.state === "UNTRACKED" && result.repoRoot ? untrackedAddCommitCommands(group.root, result.repoRoot) : void 0;
|
|
4770
|
+
if (!commands) continue;
|
|
4771
|
+
console.log(
|
|
4772
|
+
result.state === "NOT_IN_REPO" ? `
|
|
4773
|
+
${group.root} is not inside a Git repository yet.` : `
|
|
4774
|
+
${group.root} is inside a Git repository but isn't tracked yet.`
|
|
4775
|
+
);
|
|
4776
|
+
if (result.otherStaged) {
|
|
4777
|
+
console.log(" Note: this repository has OTHER staged changes outside this root \u2014 they are left untouched below.");
|
|
4778
|
+
}
|
|
4779
|
+
console.log(" Would run:");
|
|
4780
|
+
for (const cmd of commands) console.log(` ${formatGitCommand(cmd)}`);
|
|
4781
|
+
const answer = (await rl.question(" Commit a Git baseline for this root now? [y/N] ")).trim().toLowerCase();
|
|
4782
|
+
if (answer !== "y" && answer !== "yes") continue;
|
|
4783
|
+
const results = await runGitCommands(commands, deps);
|
|
4784
|
+
if (results.some((r) => r.code !== 0)) {
|
|
4785
|
+
console.log(" git baseline failed \u2014 see output above; the root is left as-is.");
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4518
4789
|
async function init(options = {}, deps = {}) {
|
|
4519
4790
|
const runAudit = deps.runAudit ?? auditCommand;
|
|
4791
|
+
const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
|
|
4520
4792
|
const cwd = options.cwd ?? process.cwd();
|
|
4521
4793
|
const home = options.home ?? homedir4();
|
|
4522
4794
|
const detected = detectAgents(cwd, home, options.pathEnv);
|
|
@@ -4524,6 +4796,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4524
4796
|
if (options.list) {
|
|
4525
4797
|
printDetectionResults(detected);
|
|
4526
4798
|
printMcpDetectionResults(detectedMcp);
|
|
4799
|
+
await printGitDetectionColumn(detected, gitDeps);
|
|
4527
4800
|
return;
|
|
4528
4801
|
}
|
|
4529
4802
|
if (detected.length === 0 && detectedMcp.length === 0) {
|
|
@@ -4545,7 +4818,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4545
4818
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
4546
4819
|
const config = buildConfig(selectedRoots, selectedRoots[0], void 0, selectedMcpTargets);
|
|
4547
4820
|
saveConfig(config);
|
|
4548
|
-
printSummary(config);
|
|
4821
|
+
await printSummary(config, gitDeps);
|
|
4549
4822
|
await runAudit({ yes: true });
|
|
4550
4823
|
return;
|
|
4551
4824
|
}
|
|
@@ -4566,9 +4839,10 @@ async function init(options = {}, deps = {}) {
|
|
|
4566
4839
|
}
|
|
4567
4840
|
const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
|
|
4568
4841
|
const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
|
|
4842
|
+
await promptGitPreflight(rl, selected, gitDeps);
|
|
4569
4843
|
const config = buildConfig(selected, target, handoffAgent, selectedMcp);
|
|
4570
4844
|
saveConfig(config);
|
|
4571
|
-
printSummary(config);
|
|
4845
|
+
await printSummary(config, gitDeps);
|
|
4572
4846
|
if (options.offerAudit !== false) {
|
|
4573
4847
|
const answer = (await rl.question("\nRun the skills & MCP audit now? [Y/n] ")).trim().toLowerCase();
|
|
4574
4848
|
runAuditNow = answer === "" || answer === "y" || answer === "yes";
|
|
@@ -4867,7 +5141,7 @@ import { existsSync as existsSync19 } from "fs";
|
|
|
4867
5141
|
|
|
4868
5142
|
// src/commands/held.ts
|
|
4869
5143
|
import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
|
|
4870
|
-
import { basename as basename9, join as join23, resolve as resolvePath4, sep as
|
|
5144
|
+
import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep3 } from "path";
|
|
4871
5145
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
4872
5146
|
function resolveHeldPath(quarantineDir, id) {
|
|
4873
5147
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
@@ -4879,7 +5153,7 @@ function resolveHeldPath(quarantineDir, id) {
|
|
|
4879
5153
|
}
|
|
4880
5154
|
const realRoot = realpathSync3(resolvePath4(quarantineDir));
|
|
4881
5155
|
const realCandidate = realpathSync3(candidate);
|
|
4882
|
-
if (!realCandidate.startsWith(realRoot +
|
|
5156
|
+
if (!realCandidate.startsWith(realRoot + sep3)) {
|
|
4883
5157
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
4884
5158
|
}
|
|
4885
5159
|
return realCandidate;
|
|
@@ -5407,6 +5681,10 @@ async function status() {
|
|
|
5407
5681
|
if (config.mcpTargets?.length) console.log(` mcpTargets: ${config.mcpTargets.join(", ")}`);
|
|
5408
5682
|
console.log(` quarantineDir: ${config.quarantineDir}`);
|
|
5409
5683
|
console.log(` strictness: ${config.strictness}`);
|
|
5684
|
+
for (const root of config.skillsRoots) {
|
|
5685
|
+
const git = await evaluateGitPreflight(root);
|
|
5686
|
+
console.log(` version control: ${root} \u2014 ${describeGitPreflight(git)}`);
|
|
5687
|
+
}
|
|
5410
5688
|
console.log(` last audit: ${formatAuditFreshness(readAuditState())}`);
|
|
5411
5689
|
console.log(
|
|
5412
5690
|
` held in quarantine: ${held.readable ? held.count : "UNREADABLE (quarantine dir cannot be listed)"}` + (held.count > 0 ? " (`skill-forge promote <id>` / `reject <id>`)" : "")
|
|
@@ -5419,9 +5697,9 @@ async function status() {
|
|
|
5419
5697
|
// src/evolve.ts
|
|
5420
5698
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
5421
5699
|
import { createHash as createHash3 } from "crypto";
|
|
5422
|
-
import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as
|
|
5700
|
+
import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
|
|
5423
5701
|
import { tmpdir as tmpdir2 } from "os";
|
|
5424
|
-
import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as
|
|
5702
|
+
import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep4 } from "path";
|
|
5425
5703
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
5426
5704
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
5427
5705
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
@@ -5489,7 +5767,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5489
5767
|
}
|
|
5490
5768
|
const stagingRoot = join24(project, ".skillopt-sleep", "staging");
|
|
5491
5769
|
const stagingDir = resolvePath5(parsed.staging_dir);
|
|
5492
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
5770
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep4)) {
|
|
5493
5771
|
throw new UserError(
|
|
5494
5772
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
5495
5773
|
);
|
|
@@ -5708,15 +5986,15 @@ function isAlreadyAdopted(stagingDir, manifest) {
|
|
|
5708
5986
|
}
|
|
5709
5987
|
return sawComparison;
|
|
5710
5988
|
}
|
|
5711
|
-
function assertNotSymlink(path,
|
|
5989
|
+
function assertNotSymlink(path, label2) {
|
|
5712
5990
|
let stat;
|
|
5713
5991
|
try {
|
|
5714
|
-
stat =
|
|
5992
|
+
stat = lstatSync4(path);
|
|
5715
5993
|
} catch {
|
|
5716
5994
|
return;
|
|
5717
5995
|
}
|
|
5718
5996
|
if (stat.isSymbolicLink()) {
|
|
5719
|
-
throw new UserError(`refusing to re-gate ${
|
|
5997
|
+
throw new UserError(`refusing to re-gate ${label2}: ${path} is a symlink (a staged proposal must be a plain file)`);
|
|
5720
5998
|
}
|
|
5721
5999
|
}
|
|
5722
6000
|
function sha256File(path) {
|
|
@@ -6467,7 +6745,7 @@ import { join as join26, resolve as resolvePath8 } from "path";
|
|
|
6467
6745
|
// src/gate/skillMapDrift.ts
|
|
6468
6746
|
import { createHash as createHash4 } from "crypto";
|
|
6469
6747
|
import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
|
|
6470
|
-
import { dirname as dirname10, isAbsolute as
|
|
6748
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
|
|
6471
6749
|
function sha256(text) {
|
|
6472
6750
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6473
6751
|
}
|
|
@@ -6500,7 +6778,7 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
6500
6778
|
}
|
|
6501
6779
|
function resolveLocalPath(path, mapDir) {
|
|
6502
6780
|
const expanded = expandHome(path);
|
|
6503
|
-
if (
|
|
6781
|
+
if (isAbsolute3(expanded)) return resolve7(expanded);
|
|
6504
6782
|
const candidates = [resolve7(expanded)];
|
|
6505
6783
|
if (mapDir) {
|
|
6506
6784
|
candidates.push(resolve7(mapDir, expanded));
|
|
@@ -6913,9 +7191,9 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
6913
7191
|
if (result.housekeeping.length > 0) {
|
|
6914
7192
|
lines.push("", "## Housekeeping", "");
|
|
6915
7193
|
for (const action of result.housekeeping) {
|
|
6916
|
-
const
|
|
7194
|
+
const label2 = action.outcome === "applied" ? "APPLIED" : action.outcome === "failed" ? "FAILED" : "WOULD APPLY";
|
|
6917
7195
|
lines.push(
|
|
6918
|
-
`- ${
|
|
7196
|
+
`- ${label2} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
|
|
6919
7197
|
);
|
|
6920
7198
|
}
|
|
6921
7199
|
}
|
|
@@ -7031,8 +7309,8 @@ async function routineCommand(options = {}) {
|
|
|
7031
7309
|
if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
|
|
7032
7310
|
console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
|
|
7033
7311
|
for (const action of housekeeping) {
|
|
7034
|
-
const
|
|
7035
|
-
console.log(` ${
|
|
7312
|
+
const label2 = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
|
|
7313
|
+
console.log(` ${label2}: ${action.kind} ${action.target}${action.outcome === "failed" ? ` \u2014 ${action.reason}` : ""}`);
|
|
7036
7314
|
}
|
|
7037
7315
|
for (const item of result.degraded) console.log(` INCOMPLETE: ${item}`);
|
|
7038
7316
|
console.log(` report -> ${result.reportPath}`);
|
|
@@ -7051,7 +7329,7 @@ async function routineCommand(options = {}) {
|
|
|
7051
7329
|
|
|
7052
7330
|
// src/commands/ingest.ts
|
|
7053
7331
|
import { realpathSync as realpathSync6 } from "fs";
|
|
7054
|
-
import { dirname as dirname12, isAbsolute as
|
|
7332
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, relative as relative3, resolve as resolvePath9 } from "path";
|
|
7055
7333
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
7056
7334
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
7057
7335
|
function requireProFeature() {
|
|
@@ -7098,8 +7376,8 @@ function realpathOrNull(p) {
|
|
|
7098
7376
|
}
|
|
7099
7377
|
}
|
|
7100
7378
|
function isUnderRoot(candidate, root) {
|
|
7101
|
-
const rel =
|
|
7102
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7379
|
+
const rel = relative3(root, candidate);
|
|
7380
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
7103
7381
|
}
|
|
7104
7382
|
function validatePendingEntries(entries, config) {
|
|
7105
7383
|
const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
|
|
@@ -7175,192 +7453,2711 @@ async function queueClose(id, options = {}) {
|
|
|
7175
7453
|
console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
|
|
7176
7454
|
}
|
|
7177
7455
|
|
|
7178
|
-
// src/commands/
|
|
7179
|
-
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
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";
|
|
7181
|
-
import { dirname as dirname14, isAbsolute as isAbsolute5, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
|
|
7456
|
+
// src/commands/insight.ts
|
|
7182
7457
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
7458
|
+
import {
|
|
7459
|
+
chmodSync as chmodSync3,
|
|
7460
|
+
lstatSync as lstatSync6,
|
|
7461
|
+
mkdirSync as mkdirSync9,
|
|
7462
|
+
readFileSync as readFileSync22,
|
|
7463
|
+
readSync as readSync3,
|
|
7464
|
+
writeFileSync as writeFileSync8
|
|
7465
|
+
} from "fs";
|
|
7466
|
+
import { dirname as dirname13, isAbsolute as isAbsolute6, join as join32, relative as relative5, resolve as resolvePath10 } from "path";
|
|
7183
7467
|
|
|
7184
|
-
// src/
|
|
7185
|
-
import {
|
|
7186
|
-
import {
|
|
7468
|
+
// src/insights/inventory.ts
|
|
7469
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
7470
|
+
import { join as join28 } from "path";
|
|
7187
7471
|
|
|
7188
|
-
// src/
|
|
7189
|
-
import {
|
|
7190
|
-
var
|
|
7191
|
-
var
|
|
7192
|
-
var
|
|
7193
|
-
var
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7472
|
+
// src/insights/models.ts
|
|
7473
|
+
import { createHash as createHash5 } from "crypto";
|
|
7474
|
+
var INSIGHT_SCHEMA_VERSION = 1;
|
|
7475
|
+
var MAX_EVIDENCE_EXCERPT_CHARS = 500;
|
|
7476
|
+
var SOURCE_TYPES = ["url", "file", "directory", "connector", "stdin"];
|
|
7477
|
+
var SOURCE_ACCESS_CLASSES = [
|
|
7478
|
+
"public",
|
|
7479
|
+
"user-owned",
|
|
7480
|
+
"client-confidential",
|
|
7481
|
+
"restricted",
|
|
7482
|
+
"unknown"
|
|
7483
|
+
];
|
|
7484
|
+
var SOURCE_LICENSE_CLASSES = [
|
|
7485
|
+
"public-domain",
|
|
7486
|
+
"permissive",
|
|
7487
|
+
"attribution-required",
|
|
7488
|
+
"copyleft",
|
|
7489
|
+
"proprietary",
|
|
7490
|
+
"unknown"
|
|
7491
|
+
];
|
|
7492
|
+
var SOURCE_RETENTIONS = ["not-retained", "retained", "deleted"];
|
|
7493
|
+
var ANALYSIS_MODES = ["multi-agent", "separate-context", "single-context"];
|
|
7494
|
+
var INSIGHT_VALIDATIONS = [
|
|
7495
|
+
"corroborated",
|
|
7496
|
+
"contested",
|
|
7497
|
+
"single-source",
|
|
7498
|
+
"rejected",
|
|
7499
|
+
"unverifiable"
|
|
7500
|
+
];
|
|
7501
|
+
var FORGE_VERBS2 = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
|
|
7502
|
+
var INVENTORY_COVERAGE_VALUES = ["complete", "partial", "unavailable"];
|
|
7503
|
+
var STUDY_STATUSES = [
|
|
7504
|
+
"collecting",
|
|
7505
|
+
"analysis-submitted",
|
|
7506
|
+
"review-ready",
|
|
7507
|
+
"approved",
|
|
7508
|
+
"jira-manifested",
|
|
7509
|
+
"dismissed",
|
|
7510
|
+
"archived"
|
|
7511
|
+
];
|
|
7512
|
+
function canonicalJson(value) {
|
|
7513
|
+
const normalize = (input) => {
|
|
7514
|
+
if (Array.isArray(input)) return input.map(normalize);
|
|
7515
|
+
if (input !== null && typeof input === "object") {
|
|
7516
|
+
return Object.fromEntries(
|
|
7517
|
+
Object.entries(input).filter(([, child]) => child !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, normalize(child)])
|
|
7518
|
+
);
|
|
7519
|
+
}
|
|
7520
|
+
return input;
|
|
7521
|
+
};
|
|
7522
|
+
return JSON.stringify(normalize(value));
|
|
7523
|
+
}
|
|
7524
|
+
function stableHash(value) {
|
|
7525
|
+
return createHash5("sha256").update(canonicalJson(value)).digest("hex");
|
|
7526
|
+
}
|
|
7527
|
+
function stableId(prefix, value, length = 16) {
|
|
7528
|
+
if (!/^[A-Z][A-Z0-9-]*$/.test(prefix)) {
|
|
7529
|
+
throw new Error(`invalid stable id prefix: ${prefix}`);
|
|
7530
|
+
}
|
|
7531
|
+
if (!Number.isInteger(length) || length < 8 || length > 64) {
|
|
7532
|
+
throw new Error(`stable id length must be an integer from 8 to 64`);
|
|
7533
|
+
}
|
|
7534
|
+
return `${prefix}-${stableHash(value).slice(0, length)}`;
|
|
7535
|
+
}
|
|
7536
|
+
function sourceIdFor(fingerprint) {
|
|
7537
|
+
return stableId("SRC", fingerprint);
|
|
7538
|
+
}
|
|
7539
|
+
function sourceInventoryHashFor(sources) {
|
|
7540
|
+
return stableHash(
|
|
7541
|
+
sources.map((source) => ({
|
|
7542
|
+
sourceId: source.sourceId,
|
|
7543
|
+
type: source.type,
|
|
7544
|
+
title: source.title,
|
|
7545
|
+
privateLocator: source.privateLocator,
|
|
7546
|
+
publicLocator: source.publicLocator,
|
|
7547
|
+
author: source.author,
|
|
7548
|
+
publisher: source.publisher,
|
|
7549
|
+
publicationDate: source.publicationDate,
|
|
7550
|
+
revisionDate: source.revisionDate,
|
|
7551
|
+
revision: source.revision,
|
|
7552
|
+
etag: source.etag,
|
|
7553
|
+
contentSha256: source.contentSha256,
|
|
7554
|
+
hashUnavailableReason: source.hashUnavailableReason,
|
|
7555
|
+
fingerprint: source.fingerprint,
|
|
7556
|
+
accessClass: source.accessClass,
|
|
7557
|
+
licenseName: source.licenseName,
|
|
7558
|
+
licenseClass: source.licenseClass,
|
|
7559
|
+
byteLength: source.byteLength,
|
|
7560
|
+
mediaType: source.mediaType,
|
|
7561
|
+
includeHidden: source.includeHidden,
|
|
7562
|
+
skippedContent: source.skippedContent,
|
|
7563
|
+
suspiciousInstructionFlags: source.suspiciousInstructionFlags
|
|
7564
|
+
})).sort((a, b) => a.sourceId < b.sourceId ? -1 : a.sourceId > b.sourceId ? 1 : 0)
|
|
7565
|
+
);
|
|
7199
7566
|
}
|
|
7200
|
-
function
|
|
7201
|
-
return
|
|
7567
|
+
function insightIdFor(domain, claim, sourceIds) {
|
|
7568
|
+
return stableId("INS", { domain: domain.trim(), claim: claim.trim(), sourceIds: [...sourceIds].sort() });
|
|
7202
7569
|
}
|
|
7203
|
-
function
|
|
7204
|
-
return
|
|
7570
|
+
function domainPlanIdFor(domain, insightIds) {
|
|
7571
|
+
return stableId("DOM", { domain: domain.trim(), insightIds: [...insightIds].sort() });
|
|
7205
7572
|
}
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7573
|
+
|
|
7574
|
+
// src/insights/inventory.ts
|
|
7575
|
+
function compareStrings(a, b) {
|
|
7576
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
7209
7577
|
}
|
|
7210
|
-
function
|
|
7211
|
-
|
|
7578
|
+
function declaredMapCoverage(path, claim) {
|
|
7579
|
+
if (!path) return "not-requested";
|
|
7580
|
+
return claim ?? "unknown";
|
|
7212
7581
|
}
|
|
7213
|
-
function
|
|
7214
|
-
|
|
7582
|
+
function fingerprintInstalledSkill(path) {
|
|
7583
|
+
try {
|
|
7584
|
+
return { fingerprint: stableHash(readFileSync19(join28(path, "SKILL.md"), "utf-8")) };
|
|
7585
|
+
} catch (error) {
|
|
7586
|
+
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
7587
|
+
}
|
|
7215
7588
|
}
|
|
7216
|
-
function
|
|
7217
|
-
const
|
|
7218
|
-
const
|
|
7219
|
-
const
|
|
7220
|
-
|
|
7589
|
+
function installedEntries(config) {
|
|
7590
|
+
const report = buildOrganizeReport(config);
|
|
7591
|
+
const warnings = [...report.registry.warnings, ...report.notices];
|
|
7592
|
+
const entries = report.registry.skills.map((skill) => {
|
|
7593
|
+
const fingerprint = fingerprintInstalledSkill(skill.path);
|
|
7594
|
+
if (fingerprint.reason) warnings.push(`${skill.name}: ${fingerprint.reason}`);
|
|
7595
|
+
return {
|
|
7596
|
+
capabilityId: skill.id,
|
|
7597
|
+
name: skill.name,
|
|
7598
|
+
source: "installed",
|
|
7599
|
+
fingerprint: fingerprint.fingerprint,
|
|
7600
|
+
fingerprintUnavailableReason: fingerprint.reason,
|
|
7601
|
+
tier: skill.tier,
|
|
7602
|
+
domain: skill.domain,
|
|
7603
|
+
consumes: [...skill.consumes],
|
|
7604
|
+
provenance: skill.provenance,
|
|
7605
|
+
maturity: skill.maturity
|
|
7606
|
+
};
|
|
7607
|
+
});
|
|
7608
|
+
return { entries, warnings, rootCount: report.registry.skillsRoots.length };
|
|
7221
7609
|
}
|
|
7222
|
-
function
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7610
|
+
function skillMapEntries(map) {
|
|
7611
|
+
return map.nodes.filter((node) => node.type === "skill").map((node) => {
|
|
7612
|
+
const validFingerprint = typeof node.contentHash === "string" && /^[0-9a-f]{64}$/.test(node.contentHash);
|
|
7613
|
+
return {
|
|
7614
|
+
capabilityId: node.id,
|
|
7615
|
+
name: node.name ?? node.id,
|
|
7616
|
+
source: "skill-map",
|
|
7617
|
+
fingerprint: validFingerprint ? node.contentHash : void 0,
|
|
7618
|
+
fingerprintUnavailableReason: validFingerprint ? void 0 : "skill-map node has no valid contentHash",
|
|
7619
|
+
consumes: [],
|
|
7620
|
+
description: node.description
|
|
7621
|
+
};
|
|
7622
|
+
});
|
|
7229
7623
|
}
|
|
7230
|
-
function
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7624
|
+
function candidateFor(match, proposal, byId) {
|
|
7625
|
+
const entry = byId.get(match.skillId);
|
|
7626
|
+
const extended = (proposal.extendsCapabilityIds ?? []).some((id) => id === match.skillId || id === match.name);
|
|
7627
|
+
return {
|
|
7628
|
+
capabilityId: match.skillId,
|
|
7629
|
+
label: match.name,
|
|
7630
|
+
fingerprint: entry?.fingerprint,
|
|
7631
|
+
lexicalScore: match.score,
|
|
7632
|
+
relationship: extended ? "extension" : "ambiguous",
|
|
7633
|
+
semanticReviewRequired: true
|
|
7634
|
+
};
|
|
7236
7635
|
}
|
|
7237
|
-
function
|
|
7238
|
-
const
|
|
7239
|
-
|
|
7636
|
+
function mapProposals(proposals, capabilities, map, coverage) {
|
|
7637
|
+
const byId = new Map(capabilities.map((entry) => [entry.capabilityId, entry]));
|
|
7638
|
+
const installed = capabilities.filter((entry) => entry.source === "installed");
|
|
7639
|
+
return proposals.map((proposal) => {
|
|
7640
|
+
const exact = installed.filter((entry) => entry.name === proposal.label);
|
|
7641
|
+
const overlap = map ? scanMapOverlap({ name: proposal.label, description: proposal.description }, map) : { matches: [], compared: 0, topMatch: void 0 };
|
|
7642
|
+
const candidates = exact.map((entry) => ({
|
|
7643
|
+
capabilityId: entry.capabilityId,
|
|
7644
|
+
label: entry.name,
|
|
7645
|
+
fingerprint: entry.fingerprint,
|
|
7646
|
+
relationship: (proposal.extendsCapabilityIds ?? []).includes(entry.capabilityId) ? "extension" : "existing",
|
|
7647
|
+
semanticReviewRequired: true
|
|
7648
|
+
}));
|
|
7649
|
+
const seen = new Set(candidates.map((candidate) => candidate.capabilityId));
|
|
7650
|
+
for (const match of overlap.matches) {
|
|
7651
|
+
if (!seen.has(match.skillId)) candidates.push(candidateFor(match, proposal, byId));
|
|
7652
|
+
}
|
|
7653
|
+
if (candidates.length === 0) {
|
|
7654
|
+
candidates.push({
|
|
7655
|
+
label: proposal.label,
|
|
7656
|
+
relationship: coverage === "complete" ? "gap" : "ambiguous",
|
|
7657
|
+
semanticReviewRequired: true
|
|
7658
|
+
});
|
|
7659
|
+
}
|
|
7660
|
+
return {
|
|
7661
|
+
proposalId: proposal.proposalId,
|
|
7662
|
+
candidates,
|
|
7663
|
+
installedExactMatches: exact.map((entry) => entry.capabilityId),
|
|
7664
|
+
mapCompared: overlap.compared,
|
|
7665
|
+
mapTopMatch: overlap.topMatch,
|
|
7666
|
+
duplicateCapabilityBlocked: candidates.some(
|
|
7667
|
+
(candidate) => candidate.relationship === "existing" || candidate.relationship === "ambiguous"
|
|
7668
|
+
),
|
|
7669
|
+
semanticReviewRequired: true
|
|
7670
|
+
};
|
|
7671
|
+
});
|
|
7240
7672
|
}
|
|
7241
|
-
function
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
const tokensB = tokenize3(b);
|
|
7245
|
-
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
7246
|
-
let intersectionSize = 0;
|
|
7247
|
-
for (const token of tokensA) {
|
|
7248
|
-
if (tokensB.has(token)) intersectionSize++;
|
|
7673
|
+
function buildInsightInventory(config, options = {}) {
|
|
7674
|
+
if (options.skillMapCoverage !== void 0 && options.skillMapPath === void 0) {
|
|
7675
|
+
throw new UserError(`skill-map coverage requires a skill-map path`);
|
|
7249
7676
|
}
|
|
7250
|
-
const
|
|
7251
|
-
|
|
7677
|
+
const installed = installedEntries(config);
|
|
7678
|
+
let map;
|
|
7679
|
+
let mapEntries = [];
|
|
7680
|
+
const warnings = [...installed.warnings];
|
|
7681
|
+
if (options.skillMapPath) {
|
|
7682
|
+
map = loadSkillMap(options.skillMapPath);
|
|
7683
|
+
mapEntries = skillMapEntries(map);
|
|
7684
|
+
const installedIds = new Set(installed.entries.map((entry) => entry.capabilityId));
|
|
7685
|
+
if (mapEntries.some((entry) => installedIds.has(entry.capabilityId))) {
|
|
7686
|
+
throw new UserError(`skill-map capability id collides with installed capability identity`);
|
|
7687
|
+
}
|
|
7688
|
+
for (const entry of mapEntries) {
|
|
7689
|
+
if (entry.fingerprintUnavailableReason) warnings.push(`${entry.name}: ${entry.fingerprintUnavailableReason}`);
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
const capabilities = [...installed.entries, ...mapEntries].sort(
|
|
7693
|
+
(a, b) => compareStrings(`${a.source}:${a.capabilityId}`, `${b.source}:${b.capabilityId}`)
|
|
7694
|
+
);
|
|
7695
|
+
const missingFingerprints = capabilities.filter((entry) => !entry.fingerprint).length;
|
|
7696
|
+
const mapCoverage = declaredMapCoverage(options.skillMapPath, options.skillMapCoverage);
|
|
7697
|
+
const coverage = mapCoverage === "resolved" && missingFingerprints === 0 ? "complete" : "partial";
|
|
7698
|
+
const coverageReason = mapCoverage === "not-requested" ? "no resolved skill map was supplied; installed roots were inventoried only" : mapCoverage !== "resolved" ? `${mapCoverage} skill-map coverage cannot prove ecosystem-wide deduplication` : missingFingerprints > 0 ? `${missingFingerprints} capability fingerprint(s) were unavailable` : void 0;
|
|
7699
|
+
const mappings = mapProposals(options.proposals ?? [], capabilities, map, coverage);
|
|
7700
|
+
const capabilityInventoryHash = stableHash({
|
|
7701
|
+
coverage,
|
|
7702
|
+
coverageReason,
|
|
7703
|
+
installedRootCount: installed.rootCount,
|
|
7704
|
+
mapCoverage,
|
|
7705
|
+
capabilities: capabilities.map((entry) => ({
|
|
7706
|
+
capabilityId: entry.capabilityId,
|
|
7707
|
+
name: entry.name,
|
|
7708
|
+
source: entry.source,
|
|
7709
|
+
fingerprint: entry.fingerprint,
|
|
7710
|
+
tier: entry.tier,
|
|
7711
|
+
domain: entry.domain,
|
|
7712
|
+
consumes: entry.consumes,
|
|
7713
|
+
provenance: entry.provenance,
|
|
7714
|
+
maturity: entry.maturity,
|
|
7715
|
+
description: entry.description
|
|
7716
|
+
}))
|
|
7717
|
+
});
|
|
7718
|
+
return {
|
|
7719
|
+
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
7720
|
+
coverage,
|
|
7721
|
+
coverageReason,
|
|
7722
|
+
installedRootCount: installed.rootCount,
|
|
7723
|
+
installedCapabilityCount: installed.entries.length,
|
|
7724
|
+
mapCoverage,
|
|
7725
|
+
mapCapabilityCount: mapEntries.length,
|
|
7726
|
+
capabilities,
|
|
7727
|
+
mappings,
|
|
7728
|
+
warnings,
|
|
7729
|
+
capabilityInventoryHash
|
|
7730
|
+
};
|
|
7252
7731
|
}
|
|
7253
7732
|
|
|
7254
|
-
// src/
|
|
7255
|
-
import {
|
|
7256
|
-
import {
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7733
|
+
// src/insights/jira.ts
|
|
7734
|
+
import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
|
|
7735
|
+
import { join as join31 } from "path";
|
|
7736
|
+
|
|
7737
|
+
// src/insights/store.ts
|
|
7738
|
+
import { chmodSync as chmodSync2, existsSync as existsSync25, mkdirSync as mkdirSync8, readFileSync as readFileSync20 } from "fs";
|
|
7739
|
+
import { join as join30 } from "path";
|
|
7740
|
+
|
|
7741
|
+
// src/insights/sources.ts
|
|
7742
|
+
import { createHash as createHash6 } from "crypto";
|
|
7743
|
+
import {
|
|
7744
|
+
closeSync as closeSync3,
|
|
7745
|
+
constants as constants2,
|
|
7746
|
+
fstatSync as fstatSync2,
|
|
7747
|
+
lstatSync as lstatSync5,
|
|
7748
|
+
openSync as openSync3,
|
|
7749
|
+
readSync as readSync2,
|
|
7750
|
+
readdirSync as readdirSync9,
|
|
7751
|
+
realpathSync as realpathSync7,
|
|
7752
|
+
statSync as statSync11
|
|
7753
|
+
} from "fs";
|
|
7754
|
+
import { basename as basename12, extname as extname6, isAbsolute as isAbsolute5, join as join29, relative as relative4, resolve as resolve8, sep as sep5 } from "path";
|
|
7755
|
+
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
7756
|
+
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
7757
|
+
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
7758
|
+
var CREDENTIAL_NAME = /(?:^|[._-])(\.env|credentials?|secrets?|tokens?|api[._-]?keys?|id_rsa|id_ed25519)(?:$|[._-])/i;
|
|
7759
|
+
var CREDENTIAL_QUERY_KEY = /(?:^|[._-])(?:auth(?:orization)?|token|secret|password|credential|api[._-]?key|sig(?:nature)?)(?:$|[._-])/i;
|
|
7760
|
+
var CLOUD_SIGNED_QUERY_KEY = /^(?:(?:x-amz|x-goog)-(?:algorithm|credential|date|expires|security-token|signature|signedheaders)|awsaccesskeyid|googleaccessid|key[-_]pair[-_]id|signed(?:[-_]url)?)$/i;
|
|
7761
|
+
var COMPACT_CREDENTIAL_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
7762
|
+
"apikey",
|
|
7763
|
+
"auth",
|
|
7764
|
+
"authorization",
|
|
7765
|
+
"authtoken",
|
|
7766
|
+
"bearertoken",
|
|
7767
|
+
"clientsecret",
|
|
7768
|
+
"credential",
|
|
7769
|
+
"credentials",
|
|
7770
|
+
"idtoken",
|
|
7771
|
+
"password",
|
|
7772
|
+
"refreshtoken",
|
|
7773
|
+
"secret",
|
|
7774
|
+
"securitytoken",
|
|
7775
|
+
"sig",
|
|
7776
|
+
"signature",
|
|
7777
|
+
"signedurl",
|
|
7778
|
+
"token",
|
|
7779
|
+
"accesstoken"
|
|
7780
|
+
]);
|
|
7781
|
+
var SUPPORTED_MEDIA_BY_EXTENSION = {
|
|
7782
|
+
".md": "text/markdown",
|
|
7783
|
+
".markdown": "text/markdown",
|
|
7784
|
+
".txt": "text/plain",
|
|
7785
|
+
".html": "text/html",
|
|
7786
|
+
".htm": "text/html",
|
|
7787
|
+
".pdf": "application/pdf"
|
|
7788
|
+
};
|
|
7789
|
+
function ensurePositiveLimit(value, label2) {
|
|
7790
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
7791
|
+
throw new UserError(`${label2} must be a positive integer number of bytes`);
|
|
7792
|
+
}
|
|
7793
|
+
return value;
|
|
7265
7794
|
}
|
|
7266
|
-
function
|
|
7267
|
-
|
|
7795
|
+
function validateObservedMetadata(sha2562, byteLength) {
|
|
7796
|
+
if (sha2562 !== void 0 && !/^[0-9a-f]{64}$/.test(sha2562)) {
|
|
7797
|
+
throw new UserError(`observedSha256 must be a lowercase SHA-256 digest`);
|
|
7798
|
+
}
|
|
7799
|
+
if (byteLength !== void 0 && (!Number.isSafeInteger(byteLength) || byteLength < 0)) {
|
|
7800
|
+
throw new UserError(`observedByteLength must be a non-negative integer number of bytes`);
|
|
7801
|
+
}
|
|
7268
7802
|
}
|
|
7269
|
-
function
|
|
7270
|
-
return
|
|
7803
|
+
function compareStrings2(a, b) {
|
|
7804
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
7271
7805
|
}
|
|
7272
|
-
function
|
|
7273
|
-
let
|
|
7806
|
+
function canonicalPublicUrl(raw, label2) {
|
|
7807
|
+
let url;
|
|
7274
7808
|
try {
|
|
7275
|
-
|
|
7276
|
-
} catch
|
|
7277
|
-
throw new UserError(`
|
|
7809
|
+
url = new URL(raw);
|
|
7810
|
+
} catch {
|
|
7811
|
+
throw new UserError(`invalid ${label2}: ${raw}`);
|
|
7812
|
+
}
|
|
7813
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
7814
|
+
throw new UserError(`${label2} must use http or https: ${raw}`);
|
|
7815
|
+
}
|
|
7816
|
+
if (url.username || url.password || [...url.searchParams.keys()].some(
|
|
7817
|
+
(key) => CREDENTIAL_QUERY_KEY.test(key) || CLOUD_SIGNED_QUERY_KEY.test(key) || COMPACT_CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())
|
|
7818
|
+
)) {
|
|
7819
|
+
throw new UserError(`${label2} must not contain credentials: ${raw}`);
|
|
7820
|
+
}
|
|
7821
|
+
url.hash = "";
|
|
7822
|
+
return url.toString();
|
|
7823
|
+
}
|
|
7824
|
+
function isInside2(child, root) {
|
|
7825
|
+
const rel = relative4(root, child);
|
|
7826
|
+
return rel === "" || !rel.startsWith(`..${sep5}`) && rel !== ".." && !isAbsolute5(rel);
|
|
7827
|
+
}
|
|
7828
|
+
function isHiddenName(name) {
|
|
7829
|
+
return name.startsWith(".") && name !== "." && name !== "..";
|
|
7830
|
+
}
|
|
7831
|
+
function hasCredentialShapedName(name) {
|
|
7832
|
+
return name === ".env" || name.startsWith(".env.") || CREDENTIAL_NAME.test(name);
|
|
7833
|
+
}
|
|
7834
|
+
function mediaTypeFor(path, declared) {
|
|
7835
|
+
return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[extname6(path).toLowerCase()];
|
|
7836
|
+
}
|
|
7837
|
+
function suspiciousInstructionFlags(bytes, mediaType) {
|
|
7838
|
+
if (!mediaType.startsWith("text/")) return [];
|
|
7839
|
+
const sample = bytes.subarray(0, 64 * 1024).toString("utf-8");
|
|
7840
|
+
const flags = [];
|
|
7841
|
+
if (/ignore\s+(?:all\s+)?previous\s+instructions/i.test(sample)) flags.push("instruction-override-language");
|
|
7842
|
+
if (/(?:reveal|print|exfiltrate).{0,40}(?:system prompt|credential|secret|token)/i.test(sample)) flags.push("data-exfiltration-language");
|
|
7843
|
+
if (/(?:run|execute)\s+(?:this\s+)?(?:command|script)/i.test(sample)) flags.push("execution-request-language");
|
|
7844
|
+
return flags;
|
|
7845
|
+
}
|
|
7846
|
+
function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remainingBytes) {
|
|
7847
|
+
const name = basename12(path);
|
|
7848
|
+
if (hasCredentialShapedName(name)) {
|
|
7849
|
+
return { path: relativePath, reason: "credential-shaped-name" };
|
|
7850
|
+
}
|
|
7851
|
+
const mediaType = mediaTypeFor(path, declaredMediaType);
|
|
7852
|
+
if (!mediaType) return { path: relativePath, reason: "unsupported-media-type" };
|
|
7853
|
+
const noFollow = typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
|
|
7854
|
+
let descriptor;
|
|
7855
|
+
let bytes;
|
|
7856
|
+
let descriptorStat;
|
|
7857
|
+
try {
|
|
7858
|
+
descriptor = openSync3(path, constants2.O_RDONLY | noFollow);
|
|
7859
|
+
descriptorStat = fstatSync2(descriptor);
|
|
7860
|
+
if (!descriptorStat.isFile()) return { path: relativePath, reason: "unsupported-media-type" };
|
|
7861
|
+
if (descriptorStat.size > maxFileBytes) {
|
|
7862
|
+
return { path: relativePath, reason: "file-too-large", byteLength: descriptorStat.size };
|
|
7863
|
+
}
|
|
7864
|
+
if (descriptorStat.size > remainingBytes) {
|
|
7865
|
+
return { path: relativePath, reason: "total-byte-limit", byteLength: descriptorStat.size };
|
|
7866
|
+
}
|
|
7867
|
+
const limit = Math.min(maxFileBytes, remainingBytes);
|
|
7868
|
+
const chunks = [];
|
|
7869
|
+
let total = 0;
|
|
7870
|
+
while (true) {
|
|
7871
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, limit - total + 1));
|
|
7872
|
+
const count = readSync2(descriptor, chunk, 0, chunk.byteLength, null);
|
|
7873
|
+
if (count === 0) break;
|
|
7874
|
+
total += count;
|
|
7875
|
+
if (total > limit) {
|
|
7876
|
+
const reason = maxFileBytes <= remainingBytes ? "file-too-large" : "total-byte-limit";
|
|
7877
|
+
return { path: relativePath, reason, byteLength: total };
|
|
7878
|
+
}
|
|
7879
|
+
chunks.push(chunk.subarray(0, count));
|
|
7880
|
+
}
|
|
7881
|
+
bytes = Buffer.concat(chunks, total);
|
|
7882
|
+
} catch (error) {
|
|
7883
|
+
throw new UserError(`unable to read source file ${relativePath}: ${error.message}`);
|
|
7884
|
+
} finally {
|
|
7885
|
+
if (descriptor !== void 0) closeSync3(descriptor);
|
|
7278
7886
|
}
|
|
7887
|
+
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
7888
|
+
let realPath;
|
|
7279
7889
|
try {
|
|
7280
|
-
|
|
7890
|
+
realPath = realpathSync7(path);
|
|
7891
|
+
const pathStat = statSync11(realPath);
|
|
7892
|
+
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
7893
|
+
throw new Error("path identity changed");
|
|
7894
|
+
}
|
|
7281
7895
|
} catch {
|
|
7282
|
-
throw new UserError(`
|
|
7283
|
-
}
|
|
7284
|
-
}
|
|
7285
|
-
function readStore(path, label, validateRecord) {
|
|
7286
|
-
if (!existsSync25(path)) {
|
|
7287
|
-
return { schemaVersion: 1, records: [] };
|
|
7896
|
+
throw new UserError(`source file changed while it was being observed: ${relativePath}`);
|
|
7288
7897
|
}
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
|
|
7292
|
-
}
|
|
7293
|
-
const records = parsed.records;
|
|
7294
|
-
if (validateRecord) {
|
|
7295
|
-
records.forEach((record, index) => validateRecord(record, index));
|
|
7898
|
+
if (mediaType !== "application/pdf" && bytes.subarray(0, 512).includes(0)) {
|
|
7899
|
+
return { path: relativePath, reason: "binary", byteLength: bytes.byteLength };
|
|
7296
7900
|
}
|
|
7297
|
-
return {
|
|
7901
|
+
return {
|
|
7902
|
+
realPath,
|
|
7903
|
+
relativePath,
|
|
7904
|
+
byteLength: bytes.byteLength,
|
|
7905
|
+
sha256: createHash6("sha256").update(bytes).digest("hex"),
|
|
7906
|
+
mediaType,
|
|
7907
|
+
suspiciousInstructionFlags: suspiciousInstructionFlags(bytes, mediaType),
|
|
7908
|
+
bytes
|
|
7909
|
+
};
|
|
7298
7910
|
}
|
|
7299
|
-
function
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
}
|
|
7313
|
-
if (typeof r.skill !== "string" || r.skill.length === 0) {
|
|
7314
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
|
|
7315
|
-
}
|
|
7316
|
-
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
7317
|
-
if (r.overrideType === "patch") {
|
|
7318
|
-
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
7319
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
7320
|
-
}
|
|
7321
|
-
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
7322
|
-
throw new UserError(
|
|
7323
|
-
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
7324
|
-
);
|
|
7325
|
-
}
|
|
7326
|
-
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
7327
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
7328
|
-
}
|
|
7329
|
-
} else if (r.patchAction !== void 0) {
|
|
7330
|
-
throw new UserError(
|
|
7331
|
-
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
7332
|
-
);
|
|
7333
|
-
}
|
|
7911
|
+
function normalizedCommon(input, retrievedAt) {
|
|
7912
|
+
return {
|
|
7913
|
+
title: input.title?.trim() ?? "",
|
|
7914
|
+
accessClass: input.accessClass ?? "unknown",
|
|
7915
|
+
licenseName: input.licenseName,
|
|
7916
|
+
licenseClass: input.licenseClass ?? "unknown",
|
|
7917
|
+
author: input.author,
|
|
7918
|
+
publisher: input.publisher,
|
|
7919
|
+
publicationDate: input.publicationDate,
|
|
7920
|
+
revisionDate: input.revisionDate,
|
|
7921
|
+
retrievedAt,
|
|
7922
|
+
revision: input.revision,
|
|
7923
|
+
etag: input.etag
|
|
7924
|
+
};
|
|
7334
7925
|
}
|
|
7335
|
-
function
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7926
|
+
function normalizeUrl(input, retrievedAt) {
|
|
7927
|
+
validateObservedMetadata(input.observedSha256, input.observedByteLength);
|
|
7928
|
+
const canonicalUrl = canonicalPublicUrl(input.url, "source URL");
|
|
7929
|
+
const fingerprint = stableHash({
|
|
7930
|
+
type: input.type,
|
|
7931
|
+
canonicalUrl,
|
|
7932
|
+
revision: input.revision,
|
|
7933
|
+
etag: input.etag,
|
|
7934
|
+
observedSha256: input.observedSha256
|
|
7935
|
+
});
|
|
7936
|
+
const common = normalizedCommon(input, retrievedAt);
|
|
7937
|
+
return {
|
|
7938
|
+
...common,
|
|
7939
|
+
title: common.title || canonicalUrl,
|
|
7940
|
+
sourceId: sourceIdFor(fingerprint),
|
|
7941
|
+
type: "url",
|
|
7942
|
+
privateLocator: canonicalUrl,
|
|
7943
|
+
publicLocator: common.accessClass === "public" ? canonicalUrl : void 0,
|
|
7944
|
+
contentSha256: input.observedSha256,
|
|
7945
|
+
hashUnavailableReason: input.observedSha256 ? void 0 : "not fetched by skill-forge",
|
|
7946
|
+
fingerprint,
|
|
7947
|
+
retention: "not-retained",
|
|
7948
|
+
byteLength: input.observedByteLength,
|
|
7949
|
+
mediaType: input.mediaType,
|
|
7950
|
+
skippedContent: [],
|
|
7951
|
+
suspiciousInstructionFlags: []
|
|
7952
|
+
};
|
|
7360
7953
|
}
|
|
7361
|
-
function
|
|
7362
|
-
|
|
7363
|
-
|
|
7954
|
+
function normalizeConnector(input, retrievedAt) {
|
|
7955
|
+
validateObservedMetadata(input.observedSha256, input.observedByteLength);
|
|
7956
|
+
const provider = input.provider.trim();
|
|
7957
|
+
const opaqueId = input.opaqueId.trim();
|
|
7958
|
+
if (!provider || !opaqueId) throw new UserError(`connector sources require a provider and opaque id`);
|
|
7959
|
+
const fingerprint = stableHash({
|
|
7960
|
+
type: input.type,
|
|
7961
|
+
provider,
|
|
7962
|
+
opaqueId,
|
|
7963
|
+
revision: input.revision,
|
|
7964
|
+
observedSha256: input.observedSha256
|
|
7965
|
+
});
|
|
7966
|
+
const common = normalizedCommon(input, retrievedAt);
|
|
7967
|
+
const publicLocator = input.publicLocator ? canonicalPublicUrl(input.publicLocator, "connector public locator") : void 0;
|
|
7968
|
+
return {
|
|
7969
|
+
...common,
|
|
7970
|
+
title: common.title || `${provider} source`,
|
|
7971
|
+
sourceId: sourceIdFor(fingerprint),
|
|
7972
|
+
type: "connector",
|
|
7973
|
+
privateLocator: `${provider}:${opaqueId}`,
|
|
7974
|
+
publicLocator: common.accessClass === "public" ? publicLocator : void 0,
|
|
7975
|
+
contentSha256: input.observedSha256,
|
|
7976
|
+
hashUnavailableReason: input.observedSha256 ? void 0 : "content is available only through the configured connector",
|
|
7977
|
+
fingerprint,
|
|
7978
|
+
retention: "not-retained",
|
|
7979
|
+
byteLength: input.observedByteLength,
|
|
7980
|
+
mediaType: input.mediaType,
|
|
7981
|
+
skippedContent: [],
|
|
7982
|
+
suspiciousInstructionFlags: []
|
|
7983
|
+
};
|
|
7984
|
+
}
|
|
7985
|
+
function normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
7986
|
+
const bytes = Buffer.from(input.content);
|
|
7987
|
+
if (bytes.byteLength > maxFileBytes) throw new UserError(`stdin source exceeds the per-source byte limit (${maxFileBytes})`);
|
|
7988
|
+
if (bytes.byteLength > remainingBytes) throw new UserError(`stdin source exceeds the total source byte limit`);
|
|
7989
|
+
const contentSha256 = createHash6("sha256").update(bytes).digest("hex");
|
|
7990
|
+
const mediaType = input.mediaType ?? "text/plain";
|
|
7991
|
+
const fingerprint = stableHash({ type: input.type, contentSha256, mediaType });
|
|
7992
|
+
const common = normalizedCommon(input, retrievedAt);
|
|
7993
|
+
return {
|
|
7994
|
+
receipt: {
|
|
7995
|
+
...common,
|
|
7996
|
+
title: common.title || "stdin source",
|
|
7997
|
+
sourceId: sourceIdFor(fingerprint),
|
|
7998
|
+
type: "stdin",
|
|
7999
|
+
privateLocator: "stdin",
|
|
8000
|
+
contentSha256,
|
|
8001
|
+
fingerprint,
|
|
8002
|
+
retention: "not-retained",
|
|
8003
|
+
byteLength: bytes.byteLength,
|
|
8004
|
+
mediaType,
|
|
8005
|
+
skippedContent: [],
|
|
8006
|
+
suspiciousInstructionFlags: suspiciousInstructionFlags(bytes, mediaType)
|
|
8007
|
+
},
|
|
8008
|
+
consumedBytes: bytes.byteLength
|
|
8009
|
+
};
|
|
8010
|
+
}
|
|
8011
|
+
function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8012
|
+
const resolved = resolve8(input.path);
|
|
8013
|
+
let lexicalStat;
|
|
8014
|
+
try {
|
|
8015
|
+
lexicalStat = lstatSync5(resolved);
|
|
8016
|
+
} catch (err) {
|
|
8017
|
+
throw new UserError(`unable to read source file ${resolved}: ${err.message}`);
|
|
8018
|
+
}
|
|
8019
|
+
if (lexicalStat.isSymbolicLink()) throw new UserError(`source file must not be a symlink: ${resolved}`);
|
|
8020
|
+
if (!lexicalStat.isFile()) throw new UserError(`source file is not a regular file: ${resolved}`);
|
|
8021
|
+
if (hasCredentialShapedName(basename12(resolved))) throw new UserError(`refusing credential-shaped source file: ${resolved}`);
|
|
8022
|
+
const observed = observeFile(resolved, basename12(resolved), input.mediaType, maxFileBytes, remainingBytes);
|
|
8023
|
+
if (!("realPath" in observed)) {
|
|
8024
|
+
throw new UserError(`source file ${resolved} was rejected: ${observed.reason}`);
|
|
8025
|
+
}
|
|
8026
|
+
const fingerprint = stableHash({
|
|
8027
|
+
type: input.type,
|
|
8028
|
+
path: observed.realPath,
|
|
8029
|
+
sha256: observed.sha256,
|
|
8030
|
+
mediaType: observed.mediaType
|
|
8031
|
+
});
|
|
8032
|
+
const sourceId = sourceIdFor(fingerprint);
|
|
8033
|
+
const common = normalizedCommon(input, retrievedAt);
|
|
8034
|
+
const receipt = {
|
|
8035
|
+
...common,
|
|
8036
|
+
title: common.title || basename12(observed.realPath),
|
|
8037
|
+
sourceId,
|
|
8038
|
+
type: "file",
|
|
8039
|
+
privateLocator: observed.realPath,
|
|
8040
|
+
contentSha256: observed.sha256,
|
|
8041
|
+
fingerprint,
|
|
8042
|
+
retention: "not-retained",
|
|
8043
|
+
byteLength: observed.byteLength,
|
|
8044
|
+
mediaType: observed.mediaType,
|
|
8045
|
+
skippedContent: [],
|
|
8046
|
+
suspiciousInstructionFlags: observed.suspiciousInstructionFlags
|
|
8047
|
+
};
|
|
8048
|
+
return {
|
|
8049
|
+
receipt,
|
|
8050
|
+
observedFiles: [{
|
|
8051
|
+
sourceId,
|
|
8052
|
+
path: observed.realPath,
|
|
8053
|
+
relativePath: observed.relativePath,
|
|
8054
|
+
byteLength: observed.byteLength,
|
|
8055
|
+
sha256: observed.sha256,
|
|
8056
|
+
mediaType: observed.mediaType,
|
|
8057
|
+
bytes: observed.bytes
|
|
8058
|
+
}],
|
|
8059
|
+
consumedBytes: observed.byteLength
|
|
8060
|
+
};
|
|
8061
|
+
}
|
|
8062
|
+
function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
8063
|
+
const resolved = resolve8(input.path);
|
|
8064
|
+
let lexicalStat;
|
|
8065
|
+
try {
|
|
8066
|
+
lexicalStat = lstatSync5(resolved);
|
|
8067
|
+
} catch (err) {
|
|
8068
|
+
throw new UserError(`unable to read source directory ${resolved}: ${err.message}`);
|
|
8069
|
+
}
|
|
8070
|
+
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8071
|
+
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8072
|
+
const realRoot = realpathSync7(resolved);
|
|
8073
|
+
const observations = [];
|
|
8074
|
+
const skipped = [];
|
|
8075
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
8076
|
+
const seenDirs = /* @__PURE__ */ new Set([realRoot]);
|
|
8077
|
+
let consumedBytes = 0;
|
|
8078
|
+
const walk2 = (dir, logicalDir) => {
|
|
8079
|
+
let entries;
|
|
8080
|
+
try {
|
|
8081
|
+
entries = readdirSync9(dir, { withFileTypes: true }).sort((a, b) => compareStrings2(a.name, b.name));
|
|
8082
|
+
} catch (err) {
|
|
8083
|
+
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8084
|
+
}
|
|
8085
|
+
for (const entry of entries) {
|
|
8086
|
+
const full = join29(dir, entry.name);
|
|
8087
|
+
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8088
|
+
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8089
|
+
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
8090
|
+
continue;
|
|
8091
|
+
}
|
|
8092
|
+
if (!input.includeHidden && isHiddenName(entry.name)) {
|
|
8093
|
+
skipped.push({ path: logicalPath, reason: "hidden" });
|
|
8094
|
+
continue;
|
|
8095
|
+
}
|
|
8096
|
+
if (hasCredentialShapedName(entry.name)) {
|
|
8097
|
+
skipped.push({ path: logicalPath, reason: "credential-shaped-name" });
|
|
8098
|
+
continue;
|
|
8099
|
+
}
|
|
8100
|
+
if (entry.isSymbolicLink()) {
|
|
8101
|
+
let realTarget;
|
|
8102
|
+
try {
|
|
8103
|
+
realTarget = realpathSync7(full);
|
|
8104
|
+
} catch {
|
|
8105
|
+
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8106
|
+
}
|
|
8107
|
+
if (!isInside2(realTarget, realRoot)) {
|
|
8108
|
+
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8109
|
+
}
|
|
8110
|
+
const targetStat = statSync11(realTarget);
|
|
8111
|
+
if (targetStat.isDirectory()) {
|
|
8112
|
+
if (seenDirs.has(realTarget)) continue;
|
|
8113
|
+
seenDirs.add(realTarget);
|
|
8114
|
+
walk2(realTarget, logicalPath);
|
|
8115
|
+
continue;
|
|
8116
|
+
}
|
|
8117
|
+
if (!targetStat.isFile()) {
|
|
8118
|
+
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8119
|
+
continue;
|
|
8120
|
+
}
|
|
8121
|
+
} else if (entry.isDirectory()) {
|
|
8122
|
+
const realDir = realpathSync7(full);
|
|
8123
|
+
if (seenDirs.has(realDir)) continue;
|
|
8124
|
+
seenDirs.add(realDir);
|
|
8125
|
+
walk2(full, logicalPath);
|
|
8126
|
+
continue;
|
|
8127
|
+
} else if (!entry.isFile()) {
|
|
8128
|
+
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8129
|
+
continue;
|
|
8130
|
+
}
|
|
8131
|
+
const realFile = realpathSync7(full);
|
|
8132
|
+
if (!isInside2(realFile, realRoot)) {
|
|
8133
|
+
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8134
|
+
}
|
|
8135
|
+
if (seenFiles.has(realFile)) {
|
|
8136
|
+
skipped.push({ path: logicalPath, reason: "duplicate-realpath" });
|
|
8137
|
+
continue;
|
|
8138
|
+
}
|
|
8139
|
+
seenFiles.add(realFile);
|
|
8140
|
+
const observed = observeFile(realFile, logicalPath, void 0, maxFileBytes, remainingBytes - consumedBytes);
|
|
8141
|
+
if (!("realPath" in observed)) {
|
|
8142
|
+
skipped.push(observed);
|
|
8143
|
+
continue;
|
|
8144
|
+
}
|
|
8145
|
+
observations.push(observed);
|
|
8146
|
+
consumedBytes += observed.byteLength;
|
|
8147
|
+
}
|
|
8148
|
+
};
|
|
8149
|
+
walk2(realRoot, "");
|
|
8150
|
+
observations.sort((a, b) => compareStrings2(a.relativePath, b.relativePath));
|
|
8151
|
+
skipped.sort((a, b) => compareStrings2(a.path, b.path) || compareStrings2(a.reason, b.reason));
|
|
8152
|
+
const contentSha256 = createHash6("sha256");
|
|
8153
|
+
for (const file of observations) contentSha256.update(`${file.relativePath}\0${file.sha256}
|
|
8154
|
+
`);
|
|
8155
|
+
const aggregateHash = contentSha256.digest("hex");
|
|
8156
|
+
const fingerprint = stableHash({
|
|
8157
|
+
type: input.type,
|
|
8158
|
+
path: realRoot,
|
|
8159
|
+
files: observations.map(({ relativePath, sha256: sha2562, byteLength, mediaType }) => ({ relativePath, sha256: sha2562, byteLength, mediaType }))
|
|
8160
|
+
});
|
|
8161
|
+
const sourceId = sourceIdFor(fingerprint);
|
|
8162
|
+
const common = normalizedCommon(input, retrievedAt);
|
|
8163
|
+
const receipt = {
|
|
8164
|
+
...common,
|
|
8165
|
+
title: common.title || basename12(realRoot),
|
|
8166
|
+
sourceId,
|
|
8167
|
+
type: "directory",
|
|
8168
|
+
privateLocator: realRoot,
|
|
8169
|
+
contentSha256: aggregateHash,
|
|
8170
|
+
fingerprint,
|
|
8171
|
+
retention: "not-retained",
|
|
8172
|
+
byteLength: consumedBytes,
|
|
8173
|
+
mediaType: input.mediaType ?? "application/x-directory",
|
|
8174
|
+
includeHidden: input.includeHidden === true,
|
|
8175
|
+
skippedContent: skipped,
|
|
8176
|
+
suspiciousInstructionFlags: [...new Set(observations.flatMap((file) => file.suspiciousInstructionFlags))].sort()
|
|
8177
|
+
};
|
|
8178
|
+
return {
|
|
8179
|
+
receipt,
|
|
8180
|
+
observedFiles: observations.map((file) => ({
|
|
8181
|
+
sourceId,
|
|
8182
|
+
path: file.realPath,
|
|
8183
|
+
relativePath: file.relativePath,
|
|
8184
|
+
byteLength: file.byteLength,
|
|
8185
|
+
sha256: file.sha256,
|
|
8186
|
+
mediaType: file.mediaType,
|
|
8187
|
+
bytes: file.bytes
|
|
8188
|
+
})),
|
|
8189
|
+
consumedBytes
|
|
8190
|
+
};
|
|
8191
|
+
}
|
|
8192
|
+
function normalizeSources(inputs, options = {}) {
|
|
8193
|
+
if (inputs.length === 0) throw new UserError(`at least one source is required`);
|
|
8194
|
+
const maxFileBytes = ensurePositiveLimit(options.maxFileBytes ?? DEFAULT_MAX_SOURCE_FILE_BYTES, "maxFileBytes");
|
|
8195
|
+
const maxTotalBytes = ensurePositiveLimit(options.maxTotalBytes ?? DEFAULT_MAX_SOURCE_TOTAL_BYTES, "maxTotalBytes");
|
|
8196
|
+
const retrievedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8197
|
+
const sources = [];
|
|
8198
|
+
const observedFiles = [];
|
|
8199
|
+
let totalBytes = 0;
|
|
8200
|
+
for (const input of inputs) {
|
|
8201
|
+
const remainingBytes = maxTotalBytes - totalBytes;
|
|
8202
|
+
if (remainingBytes <= 0) throw new UserError(`total source byte limit exceeded (${maxTotalBytes})`);
|
|
8203
|
+
if (input.type === "url") {
|
|
8204
|
+
sources.push(normalizeUrl(input, retrievedAt));
|
|
8205
|
+
} else if (input.type === "connector") {
|
|
8206
|
+
sources.push(normalizeConnector(input, retrievedAt));
|
|
8207
|
+
} else if (input.type === "stdin") {
|
|
8208
|
+
const normalized = normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes);
|
|
8209
|
+
sources.push(normalized.receipt);
|
|
8210
|
+
totalBytes += normalized.consumedBytes;
|
|
8211
|
+
} else if (input.type === "file") {
|
|
8212
|
+
const normalized = normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes);
|
|
8213
|
+
sources.push(normalized.receipt);
|
|
8214
|
+
observedFiles.push(...normalized.observedFiles);
|
|
8215
|
+
totalBytes += normalized.consumedBytes;
|
|
8216
|
+
} else {
|
|
8217
|
+
const normalized = normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes);
|
|
8218
|
+
sources.push(normalized.receipt);
|
|
8219
|
+
observedFiles.push(...normalized.observedFiles);
|
|
8220
|
+
totalBytes += normalized.consumedBytes;
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
const distinctSources = [...new Map(sources.map((source) => [source.sourceId, source])).values()];
|
|
8224
|
+
const distinctObservedFiles = [
|
|
8225
|
+
...new Map(observedFiles.map((file) => [`${file.sourceId}\0${file.path}`, file])).values()
|
|
8226
|
+
];
|
|
8227
|
+
return {
|
|
8228
|
+
sources: distinctSources,
|
|
8229
|
+
sourceInventoryHash: sourceInventoryHashFor(distinctSources),
|
|
8230
|
+
observedFiles: distinctObservedFiles,
|
|
8231
|
+
totalBytes
|
|
8232
|
+
};
|
|
8233
|
+
}
|
|
8234
|
+
|
|
8235
|
+
// src/insights/store.ts
|
|
8236
|
+
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8237
|
+
function getInsightsDir() {
|
|
8238
|
+
return join30(getHomeDir(), "insights");
|
|
8239
|
+
}
|
|
8240
|
+
function getStudyDir(studyId) {
|
|
8241
|
+
assertStudyId(studyId);
|
|
8242
|
+
return join30(getInsightsDir(), studyId);
|
|
8243
|
+
}
|
|
8244
|
+
function getStudyPath(studyId) {
|
|
8245
|
+
return join30(getStudyDir(studyId), "study.json");
|
|
8246
|
+
}
|
|
8247
|
+
function getStudyEventsPath(studyId) {
|
|
8248
|
+
return join30(getStudyDir(studyId), "events.jsonl");
|
|
8249
|
+
}
|
|
8250
|
+
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8251
|
+
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
8252
|
+
}
|
|
8253
|
+
function assertStudyId(studyId) {
|
|
8254
|
+
if (!STUDY_ID.test(studyId)) throw new UserError(`invalid insight study id: ${studyId}`);
|
|
8255
|
+
}
|
|
8256
|
+
function ensurePrivateDir(path) {
|
|
8257
|
+
mkdirSync8(path, { recursive: true, mode: 448 });
|
|
8258
|
+
chmodSync2(path, 448);
|
|
8259
|
+
}
|
|
8260
|
+
function isRecord2(value) {
|
|
8261
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8262
|
+
}
|
|
8263
|
+
function isNonEmptyString(value) {
|
|
8264
|
+
return typeof value === "string" && value.length > 0;
|
|
8265
|
+
}
|
|
8266
|
+
function isStringArray(value) {
|
|
8267
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8268
|
+
}
|
|
8269
|
+
function assertIsoDate(value, label2, path) {
|
|
8270
|
+
if (!isNonEmptyString(value) || Number.isNaN(Date.parse(value))) {
|
|
8271
|
+
throw new UserError(`malformed insight study at ${path}: ${label2} must be an ISO-8601 timestamp`);
|
|
8272
|
+
}
|
|
8273
|
+
}
|
|
8274
|
+
function validateSourceReceipt(value, index, path) {
|
|
8275
|
+
if (!isRecord2(value)) throw new UserError(`malformed insight study at ${path}: sources[${index}] is not an object`);
|
|
8276
|
+
if (!isNonEmptyString(value.sourceId) || !value.sourceId.startsWith("SRC-")) {
|
|
8277
|
+
throw new UserError(`malformed insight study at ${path}: sources[${index}] has an invalid sourceId`);
|
|
8278
|
+
}
|
|
8279
|
+
if (!SOURCE_TYPES.includes(value.type)) {
|
|
8280
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid type`);
|
|
8281
|
+
}
|
|
8282
|
+
if (!isNonEmptyString(value.title) || !isNonEmptyString(value.privateLocator) || !isNonEmptyString(value.fingerprint)) {
|
|
8283
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} is missing required identity fields`);
|
|
8284
|
+
}
|
|
8285
|
+
if (value.publicLocator !== void 0) {
|
|
8286
|
+
if (!isNonEmptyString(value.publicLocator)) {
|
|
8287
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid publicLocator`);
|
|
8288
|
+
}
|
|
8289
|
+
try {
|
|
8290
|
+
if (canonicalPublicUrl(value.publicLocator, "source public locator") !== value.publicLocator) {
|
|
8291
|
+
throw new Error("not canonical");
|
|
8292
|
+
}
|
|
8293
|
+
} catch {
|
|
8294
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid publicLocator`);
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
if (value.type === "directory") {
|
|
8298
|
+
if (typeof value.includeHidden !== "boolean") {
|
|
8299
|
+
throw new UserError(`malformed insight study at ${path}: directory source ${value.sourceId} is missing includeHidden`);
|
|
8300
|
+
}
|
|
8301
|
+
} else if (value.includeHidden !== void 0) {
|
|
8302
|
+
throw new UserError(`malformed insight study at ${path}: non-directory source ${value.sourceId} has includeHidden`);
|
|
8303
|
+
}
|
|
8304
|
+
assertIsoDate(value.retrievedAt, `source ${value.sourceId}.retrievedAt`, path);
|
|
8305
|
+
if (!SOURCE_ACCESS_CLASSES.includes(value.accessClass)) {
|
|
8306
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid accessClass`);
|
|
8307
|
+
}
|
|
8308
|
+
if (!SOURCE_LICENSE_CLASSES.includes(value.licenseClass)) {
|
|
8309
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid licenseClass`);
|
|
8310
|
+
}
|
|
8311
|
+
if (!SOURCE_RETENTIONS.includes(value.retention)) {
|
|
8312
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid retention value`);
|
|
8313
|
+
}
|
|
8314
|
+
const hasHash = isNonEmptyString(value.contentSha256);
|
|
8315
|
+
const hasUnavailableReason = isNonEmptyString(value.hashUnavailableReason);
|
|
8316
|
+
if (hasHash === hasUnavailableReason) {
|
|
8317
|
+
throw new UserError(
|
|
8318
|
+
`malformed insight study at ${path}: source ${value.sourceId} must have exactly one of contentSha256 or hashUnavailableReason`
|
|
8319
|
+
);
|
|
8320
|
+
}
|
|
8321
|
+
if (!Array.isArray(value.skippedContent) || !isStringArray(value.suspiciousInstructionFlags)) {
|
|
8322
|
+
throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has invalid evidence arrays`);
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
function validateStudyShape(value, path) {
|
|
8326
|
+
if (!isRecord2(value)) throw new UserError(`malformed insight study at ${path}: expected an object`);
|
|
8327
|
+
if (value.schemaVersion !== INSIGHT_SCHEMA_VERSION) {
|
|
8328
|
+
throw new UserError(`malformed insight study at ${path}: unsupported schemaVersion ${String(value.schemaVersion)}`);
|
|
8329
|
+
}
|
|
8330
|
+
if (!isNonEmptyString(value.studyId) || !STUDY_ID.test(value.studyId)) {
|
|
8331
|
+
throw new UserError(`malformed insight study at ${path}: invalid studyId`);
|
|
8332
|
+
}
|
|
8333
|
+
if (!STUDY_STATUSES.includes(value.status)) {
|
|
8334
|
+
throw new UserError(`malformed insight study at ${path}: invalid status ${String(value.status)}`);
|
|
8335
|
+
}
|
|
8336
|
+
assertIsoDate(value.createdAt, "createdAt", path);
|
|
8337
|
+
assertIsoDate(value.updatedAt, "updatedAt", path);
|
|
8338
|
+
if (value.requestedAnalysisMode !== "independent" && value.requestedAnalysisMode !== "single") {
|
|
8339
|
+
throw new UserError(`malformed insight study at ${path}: invalid requestedAnalysisMode`);
|
|
8340
|
+
}
|
|
8341
|
+
if (!Array.isArray(value.sources)) throw new UserError(`malformed insight study at ${path}: sources must be an array`);
|
|
8342
|
+
value.sources.forEach((source, index) => validateSourceReceipt(source, index, path));
|
|
8343
|
+
if (!isNonEmptyString(value.sourceInventoryHash)) {
|
|
8344
|
+
throw new UserError(`malformed insight study at ${path}: missing sourceInventoryHash`);
|
|
8345
|
+
}
|
|
8346
|
+
if (sourceInventoryHashFor(value.sources) !== value.sourceInventoryHash) {
|
|
8347
|
+
throw new UserError(`malformed insight study at ${path}: sourceInventoryHash does not match sources`);
|
|
8348
|
+
}
|
|
8349
|
+
if (!Array.isArray(value.insights) || !Array.isArray(value.domainPlans)) {
|
|
8350
|
+
throw new UserError(`malformed insight study at ${path}: insights and domainPlans must be arrays`);
|
|
8351
|
+
}
|
|
8352
|
+
if (value.status === "approved" || value.status === "jira-manifested") {
|
|
8353
|
+
if (!isRecord2(value.approval) || !isNonEmptyString(value.planHash)) {
|
|
8354
|
+
throw new UserError(`malformed insight study at ${path}: status ${value.status} requires planHash and approval`);
|
|
8355
|
+
}
|
|
8356
|
+
if (value.approval.planHash !== value.planHash) {
|
|
8357
|
+
throw new UserError(`malformed insight study at ${path}: approval planHash does not match study planHash`);
|
|
8358
|
+
}
|
|
8359
|
+
if (value.approval.sourceInventoryHash !== value.sourceInventoryHash) {
|
|
8360
|
+
throw new UserError(`malformed insight study at ${path}: approval sourceInventoryHash does not match study`);
|
|
8361
|
+
}
|
|
8362
|
+
if (value.approval.capabilityInventoryHash !== value.capabilityInventoryHash) {
|
|
8363
|
+
throw new UserError(`malformed insight study at ${path}: approval capabilityInventoryHash does not match study`);
|
|
8364
|
+
}
|
|
8365
|
+
assertIsoDate(value.approval.approvedAt, "approval.approvedAt", path);
|
|
8366
|
+
} else if (value.approval !== void 0 || value.jira !== void 0) {
|
|
8367
|
+
throw new UserError(`malformed insight study at ${path}: status ${value.status} must not carry approval or Jira state`);
|
|
8368
|
+
}
|
|
8369
|
+
if (value.status === "jira-manifested" && !isRecord2(value.jira)) {
|
|
8370
|
+
throw new UserError(`malformed insight study at ${path}: jira-manifested status requires Jira receipt metadata`);
|
|
8371
|
+
}
|
|
8372
|
+
if (value.status === "dismissed" || value.status === "archived") {
|
|
8373
|
+
assertIsoDate(value.closedAt, "closedAt", path);
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
function parseStudy(path) {
|
|
8377
|
+
let raw;
|
|
8378
|
+
try {
|
|
8379
|
+
raw = readFileSync20(path, "utf-8");
|
|
8380
|
+
} catch (err) {
|
|
8381
|
+
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
8382
|
+
}
|
|
8383
|
+
let parsed;
|
|
8384
|
+
try {
|
|
8385
|
+
parsed = JSON.parse(raw);
|
|
8386
|
+
} catch {
|
|
8387
|
+
throw new UserError(`malformed insight study at ${path}: not valid JSON`);
|
|
8388
|
+
}
|
|
8389
|
+
validateStudyShape(parsed, path);
|
|
8390
|
+
return parsed;
|
|
8391
|
+
}
|
|
8392
|
+
function approvalBasisHash(study) {
|
|
8393
|
+
return stableHash({
|
|
8394
|
+
sourceInventoryHash: study.sourceInventoryHash,
|
|
8395
|
+
requestedAnalysisMode: study.requestedAnalysisMode,
|
|
8396
|
+
actualAnalysisMode: study.actualAnalysisMode,
|
|
8397
|
+
capabilityInventoryHash: study.capabilityInventoryHash,
|
|
8398
|
+
insights: study.insights,
|
|
8399
|
+
domainPlans: study.domainPlans,
|
|
8400
|
+
planHash: study.planHash,
|
|
8401
|
+
approvalSelection: study.approval ? {
|
|
8402
|
+
approvedDomainIds: study.approval.approvedDomainIds,
|
|
8403
|
+
acceptedExceptions: study.approval.acceptedExceptions
|
|
8404
|
+
} : void 0
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
8407
|
+
function createInsightStudy(input, options = {}) {
|
|
8408
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
8409
|
+
const createdAt = now.toISOString();
|
|
8410
|
+
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
8411
|
+
assertStudyId(studyId);
|
|
8412
|
+
const path = getStudyPath(studyId);
|
|
8413
|
+
if (existsSync25(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
8414
|
+
const study = {
|
|
8415
|
+
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
8416
|
+
studyId,
|
|
8417
|
+
status: "collecting",
|
|
8418
|
+
createdAt,
|
|
8419
|
+
updatedAt: createdAt,
|
|
8420
|
+
requestedAnalysisMode: input.requestedAnalysisMode,
|
|
8421
|
+
sources: input.sources,
|
|
8422
|
+
sourceInventoryHash: input.sourceInventoryHash,
|
|
8423
|
+
insights: [],
|
|
8424
|
+
domainPlans: []
|
|
8425
|
+
};
|
|
8426
|
+
validateStudyShape(study, path);
|
|
8427
|
+
ensurePrivateDir(getInsightsDir());
|
|
8428
|
+
ensurePrivateDir(getStudyDir(studyId));
|
|
8429
|
+
writeJsonAtomic(path, study);
|
|
8430
|
+
return study;
|
|
8431
|
+
}
|
|
8432
|
+
function readInsightStudy(studyId) {
|
|
8433
|
+
const path = getStudyPath(studyId);
|
|
8434
|
+
if (!existsSync25(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
8435
|
+
const study = parseStudy(path);
|
|
8436
|
+
if (study.studyId !== studyId) {
|
|
8437
|
+
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
8438
|
+
}
|
|
8439
|
+
return study;
|
|
8440
|
+
}
|
|
8441
|
+
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
8442
|
+
assertStudyId(study.studyId);
|
|
8443
|
+
const path = getStudyPath(study.studyId);
|
|
8444
|
+
if (!existsSync25(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
8445
|
+
const existing = parseStudy(path);
|
|
8446
|
+
let next = { ...study, updatedAt: now.toISOString() };
|
|
8447
|
+
if (next.status === "dismissed" || next.status === "archived") {
|
|
8448
|
+
next = { ...next, approval: void 0, jira: void 0 };
|
|
8449
|
+
} else if (existing.approval && approvalBasisHash(existing) !== approvalBasisHash(next)) {
|
|
8450
|
+
next = {
|
|
8451
|
+
...next,
|
|
8452
|
+
status: "review-ready",
|
|
8453
|
+
approval: void 0,
|
|
8454
|
+
jira: void 0
|
|
8455
|
+
};
|
|
8456
|
+
}
|
|
8457
|
+
validateStudyShape(next, path);
|
|
8458
|
+
ensurePrivateDir(getInsightsDir());
|
|
8459
|
+
ensurePrivateDir(getStudyDir(next.studyId));
|
|
8460
|
+
writeJsonAtomic(path, next);
|
|
8461
|
+
return next;
|
|
8462
|
+
}
|
|
8463
|
+
function appendStudyEvent(studyId, input) {
|
|
8464
|
+
readInsightStudy(studyId);
|
|
8465
|
+
const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8466
|
+
if (!isNonEmptyString(input.type) || /[\u0000-\u001f\u007f-\u009f]/u.test(input.type)) {
|
|
8467
|
+
throw new UserError(`study event type must be non-empty and contain no control characters`);
|
|
8468
|
+
}
|
|
8469
|
+
assertIsoDate(occurredAt, "event occurredAt", getStudyEventsPath(studyId));
|
|
8470
|
+
const event = {
|
|
8471
|
+
eventId: stableId("EVT", { studyId, type: input.type, occurredAt, detail: input.detail }),
|
|
8472
|
+
studyId,
|
|
8473
|
+
type: input.type,
|
|
8474
|
+
occurredAt,
|
|
8475
|
+
detail: input.detail
|
|
8476
|
+
};
|
|
8477
|
+
const path = getStudyEventsPath(studyId);
|
|
8478
|
+
appendPrivateJsonLine(path, event);
|
|
8479
|
+
return event;
|
|
8480
|
+
}
|
|
8481
|
+
|
|
8482
|
+
// src/insights/validate.ts
|
|
8483
|
+
var CONTROL_OR_ANSI = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]|\u001b\[[0-?]*[ -/]*[@-~]/u;
|
|
8484
|
+
var PRIVATE_PATH = /(?:^|[\s("'])(?:~\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes|root)\/|[A-Za-z]:\\)/u;
|
|
8485
|
+
var SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_]{16,}|\bAKIA[0-9A-Z]{16}\b|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b|\b(?:password|passwd|api[_-]?key|access[_-]?token)\s*[:=]\s*\S+)/iu;
|
|
8486
|
+
var STUDY_ID2 = /^STUDY-[0-9a-f]{16}$/;
|
|
8487
|
+
var INSIGHT_ID = /^INS-[0-9a-f]{16}$/;
|
|
8488
|
+
var DOMAIN_ID = /^DOM-[0-9a-f]{16}$/;
|
|
8489
|
+
var SAFE_WORK_ITEM_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8490
|
+
var MAX_EXPORT_TEXT_CHARS = 1e4;
|
|
8491
|
+
function isRecord3(value) {
|
|
8492
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8493
|
+
}
|
|
8494
|
+
function nonEmpty(value) {
|
|
8495
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
8496
|
+
}
|
|
8497
|
+
function stringArray(value) {
|
|
8498
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8499
|
+
}
|
|
8500
|
+
function assertKeys(value, allowed, label2) {
|
|
8501
|
+
const allowedSet = new Set(allowed);
|
|
8502
|
+
const extra = Object.keys(value).filter((key) => value[key] !== void 0 && !allowedSet.has(key));
|
|
8503
|
+
if (extra.length > 0) throw new UserError(`${label2} contains unsupported field(s): ${extra.join(", ")}`);
|
|
8504
|
+
}
|
|
8505
|
+
function assertRequired(value, required, label2) {
|
|
8506
|
+
const missing = required.filter((key) => value[key] === void 0);
|
|
8507
|
+
if (missing.length > 0) throw new UserError(`${label2} is missing required field(s): ${missing.join(", ")}`);
|
|
8508
|
+
}
|
|
8509
|
+
function assertStringArray(value, label2, minItems = 0) {
|
|
8510
|
+
if (!stringArray(value) || value.length < minItems || value.some((entry) => entry.trim().length === 0)) {
|
|
8511
|
+
throw new UserError(`${label2} must be an array of ${minItems > 0 ? "non-empty " : ""}strings${minItems > 0 ? ` with at least ${minItems} item(s)` : ""}`);
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
function assertExportSafeText(value, label2, privateLocators) {
|
|
8515
|
+
if (value.length > MAX_EXPORT_TEXT_CHARS) throw new UserError(`${label2} exceeds ${MAX_EXPORT_TEXT_CHARS} characters`);
|
|
8516
|
+
if (CONTROL_OR_ANSI.test(value)) throw new UserError(`${label2} contains control or terminal escape characters`);
|
|
8517
|
+
if (PRIVATE_PATH.test(value)) throw new UserError(`${label2} contains an absolute private path`);
|
|
8518
|
+
if (SECRET_VALUE.test(value)) throw new UserError(`${label2} contains credential-shaped content`);
|
|
8519
|
+
const disclosedLocator = privateLocators.find((locator) => locator.length >= 4 && value.includes(locator));
|
|
8520
|
+
if (disclosedLocator) throw new UserError(`${label2} contains a private source locator`);
|
|
8521
|
+
}
|
|
8522
|
+
function exportStrings(value, prefix = "$") {
|
|
8523
|
+
if (typeof value === "string") return [{ path: prefix, value }];
|
|
8524
|
+
if (Array.isArray(value)) return value.flatMap((child, index) => exportStrings(child, `${prefix}[${index}]`));
|
|
8525
|
+
if (!isRecord3(value)) return [];
|
|
8526
|
+
return Object.entries(value).flatMap(([key, child]) => exportStrings(child, `${prefix}.${key}`));
|
|
8527
|
+
}
|
|
8528
|
+
function privateLocatorsFor(study) {
|
|
8529
|
+
return study.sources.filter((source) => source.privateLocator !== source.publicLocator).map((source) => source.privateLocator).filter((locator) => locator !== "stdin");
|
|
8530
|
+
}
|
|
8531
|
+
function assertExportSafe(value, study, label2 = "export") {
|
|
8532
|
+
const privateLocators = privateLocatorsFor(study);
|
|
8533
|
+
for (const entry of exportStrings(value)) assertExportSafeText(entry.value, `${label2}${entry.path}`, privateLocators);
|
|
8534
|
+
}
|
|
8535
|
+
function parseAnchor(value, label2, sourceIds) {
|
|
8536
|
+
if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
|
|
8537
|
+
assertKeys(value, ["sourceId", "locator", "excerpt"], label2);
|
|
8538
|
+
assertRequired(value, ["sourceId", "locator"], label2);
|
|
8539
|
+
if (!nonEmpty(value.sourceId) || !sourceIds.has(value.sourceId)) throw new UserError(`${label2}.sourceId is unknown`);
|
|
8540
|
+
if (!nonEmpty(value.locator)) throw new UserError(`${label2}.locator must be a non-empty string`);
|
|
8541
|
+
if (value.excerpt !== void 0) {
|
|
8542
|
+
if (typeof value.excerpt !== "string" || value.excerpt.length > MAX_EVIDENCE_EXCERPT_CHARS) {
|
|
8543
|
+
throw new UserError(`${label2}.excerpt must not exceed ${MAX_EVIDENCE_EXCERPT_CHARS} characters`);
|
|
8544
|
+
}
|
|
8545
|
+
if (CONTROL_OR_ANSI.test(value.excerpt)) throw new UserError(`${label2}.excerpt contains control characters`);
|
|
8546
|
+
}
|
|
8547
|
+
return value;
|
|
8548
|
+
}
|
|
8549
|
+
function parseCapabilityCandidate(value, label2, inventoryById) {
|
|
8550
|
+
if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
|
|
8551
|
+
assertKeys(value, ["capabilityId", "label", "fingerprint", "lexicalScore", "relationship", "semanticReviewRequired"], label2);
|
|
8552
|
+
assertRequired(value, ["label", "relationship", "semanticReviewRequired"], label2);
|
|
8553
|
+
if (!nonEmpty(value.label)) throw new UserError(`${label2}.label must be a non-empty string`);
|
|
8554
|
+
if (!["existing", "extension", "ambiguous", "gap"].includes(String(value.relationship))) {
|
|
8555
|
+
throw new UserError(`${label2}.relationship is unsupported`);
|
|
8556
|
+
}
|
|
8557
|
+
if (value.semanticReviewRequired !== true) throw new UserError(`${label2}.semanticReviewRequired must be true`);
|
|
8558
|
+
if (value.lexicalScore !== void 0 && (typeof value.lexicalScore !== "number" || value.lexicalScore < 0 || value.lexicalScore > 1)) {
|
|
8559
|
+
throw new UserError(`${label2}.lexicalScore must be a number from 0 to 1`);
|
|
8560
|
+
}
|
|
8561
|
+
if (value.capabilityId !== void 0) {
|
|
8562
|
+
if (!nonEmpty(value.capabilityId) || !inventoryById.has(value.capabilityId)) {
|
|
8563
|
+
throw new UserError(`${label2}.capabilityId is not present in the current inventory`);
|
|
8564
|
+
}
|
|
8565
|
+
const current = inventoryById.get(value.capabilityId);
|
|
8566
|
+
if (value.fingerprint !== void 0 && value.fingerprint !== current.fingerprint) {
|
|
8567
|
+
throw new UserError(`${label2}.fingerprint does not match the current capability inventory`);
|
|
8568
|
+
}
|
|
8569
|
+
} else if (value.fingerprint !== void 0) {
|
|
8570
|
+
throw new UserError(`${label2}.fingerprint requires a capabilityId`);
|
|
8571
|
+
}
|
|
8572
|
+
if (value.relationship === "existing" || value.relationship === "extension") {
|
|
8573
|
+
const currentFingerprint = nonEmpty(value.capabilityId) ? inventoryById.get(value.capabilityId)?.fingerprint : void 0;
|
|
8574
|
+
if (!currentFingerprint || value.fingerprint !== currentFingerprint) {
|
|
8575
|
+
throw new UserError(`${label2}.${String(value.relationship)} requires a capabilityId and exact current fingerprint`);
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
if (value.relationship === "gap" && (value.capabilityId !== void 0 || value.fingerprint !== void 0)) {
|
|
8579
|
+
throw new UserError(`${label2}.gap must not claim an existing capability identity`);
|
|
8580
|
+
}
|
|
8581
|
+
return value;
|
|
8582
|
+
}
|
|
8583
|
+
function parseInsight(value, index, study, inventory) {
|
|
8584
|
+
const label2 = `insights[${index}]`;
|
|
8585
|
+
if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
|
|
8586
|
+
const allowed = [
|
|
8587
|
+
"insightId",
|
|
8588
|
+
"claim",
|
|
8589
|
+
"domain",
|
|
8590
|
+
"sourceIds",
|
|
8591
|
+
"evidence",
|
|
8592
|
+
"extractionPassIds",
|
|
8593
|
+
"independenceMode",
|
|
8594
|
+
"validation",
|
|
8595
|
+
"contradictingEvidence",
|
|
8596
|
+
"unresolvedQuestions",
|
|
8597
|
+
"relevance",
|
|
8598
|
+
"relevanceBasis",
|
|
8599
|
+
"confidence",
|
|
8600
|
+
"confidenceBasis",
|
|
8601
|
+
"provenanceConstraints",
|
|
8602
|
+
"licenseConstraints",
|
|
8603
|
+
"privacyConstraints",
|
|
8604
|
+
"securityConstraints",
|
|
8605
|
+
"proposedVerb",
|
|
8606
|
+
"capabilityCandidates",
|
|
8607
|
+
"inventoryCoverage",
|
|
8608
|
+
"inventoryCoverageReason"
|
|
8609
|
+
];
|
|
8610
|
+
assertKeys(value, allowed, label2);
|
|
8611
|
+
assertRequired(value, allowed.filter((key) => key !== "inventoryCoverageReason"), label2);
|
|
8612
|
+
if (!nonEmpty(value.insightId) || !INSIGHT_ID.test(value.insightId)) throw new UserError(`${label2}.insightId is invalid`);
|
|
8613
|
+
if (!nonEmpty(value.claim) || !nonEmpty(value.domain)) throw new UserError(`${label2} requires a claim and domain`);
|
|
8614
|
+
assertStringArray(value.sourceIds, `${label2}.sourceIds`, 1);
|
|
8615
|
+
const knownSources = new Set(study.sources.map((source) => source.sourceId));
|
|
8616
|
+
if (value.sourceIds.some((id) => !knownSources.has(id))) throw new UserError(`${label2}.sourceIds contains an unknown source`);
|
|
8617
|
+
if (new Set(value.sourceIds).size !== value.sourceIds.length) throw new UserError(`${label2}.sourceIds contains duplicates`);
|
|
8618
|
+
const expectedInsightId = insightIdFor(value.domain, value.claim, value.sourceIds);
|
|
8619
|
+
if (value.insightId !== expectedInsightId) throw new UserError(`${label2}.insightId must equal the stable id ${expectedInsightId}`);
|
|
8620
|
+
if (!Array.isArray(value.evidence) || value.evidence.length === 0) throw new UserError(`${label2}.evidence must not be empty`);
|
|
8621
|
+
const sourceIds = new Set(value.sourceIds);
|
|
8622
|
+
const evidence = value.evidence.map((anchor, anchorIndex) => parseAnchor(anchor, `${label2}.evidence[${anchorIndex}]`, sourceIds));
|
|
8623
|
+
assertStringArray(value.extractionPassIds, `${label2}.extractionPassIds`, 1);
|
|
8624
|
+
if (new Set(value.extractionPassIds).size !== value.extractionPassIds.length) {
|
|
8625
|
+
throw new UserError(`${label2}.extractionPassIds contains duplicates`);
|
|
8626
|
+
}
|
|
8627
|
+
if (!ANALYSIS_MODES.includes(value.independenceMode)) throw new UserError(`${label2}.independenceMode is unsupported`);
|
|
8628
|
+
if (!INSIGHT_VALIDATIONS.includes(value.validation)) throw new UserError(`${label2}.validation is unsupported`);
|
|
8629
|
+
if (!Array.isArray(value.contradictingEvidence)) throw new UserError(`${label2}.contradictingEvidence must be an array`);
|
|
8630
|
+
const contradictingEvidence = value.contradictingEvidence.map((item, contradictionIndex) => {
|
|
8631
|
+
const itemLabel = `${label2}.contradictingEvidence[${contradictionIndex}]`;
|
|
8632
|
+
if (!isRecord3(item)) throw new UserError(`${itemLabel} must be an object`);
|
|
8633
|
+
assertKeys(item, ["claim", "anchors"], itemLabel);
|
|
8634
|
+
assertRequired(item, ["claim", "anchors"], itemLabel);
|
|
8635
|
+
if (!nonEmpty(item.claim) || !Array.isArray(item.anchors) || item.anchors.length === 0) {
|
|
8636
|
+
throw new UserError(`${itemLabel} requires a claim and at least one anchor`);
|
|
8637
|
+
}
|
|
8638
|
+
return {
|
|
8639
|
+
claim: item.claim,
|
|
8640
|
+
anchors: item.anchors.map((anchor, anchorIndex) => parseAnchor(anchor, `${itemLabel}.anchors[${anchorIndex}]`, sourceIds))
|
|
8641
|
+
};
|
|
8642
|
+
});
|
|
8643
|
+
const anchoredSourceIds = /* @__PURE__ */ new Set([
|
|
8644
|
+
...evidence.map((anchor) => anchor.sourceId),
|
|
8645
|
+
...contradictingEvidence.flatMap((item) => item.anchors.map((anchor) => anchor.sourceId))
|
|
8646
|
+
]);
|
|
8647
|
+
if (value.sourceIds.some((sourceId) => !anchoredSourceIds.has(sourceId))) {
|
|
8648
|
+
throw new UserError(`${label2}.evidence must anchor every declared sourceId`);
|
|
8649
|
+
}
|
|
8650
|
+
if (value.validation === "contested" && contradictingEvidence.length === 0) {
|
|
8651
|
+
throw new UserError(`${label2} is contested but has no contradicting evidence`);
|
|
8652
|
+
}
|
|
8653
|
+
for (const key of ["unresolvedQuestions", "provenanceConstraints", "licenseConstraints", "privacyConstraints", "securityConstraints"]) {
|
|
8654
|
+
assertStringArray(value[key], `${label2}.${key}`);
|
|
8655
|
+
}
|
|
8656
|
+
if (!["high", "medium", "low"].includes(String(value.relevance)) || !nonEmpty(value.relevanceBasis)) {
|
|
8657
|
+
throw new UserError(`${label2} requires a supported relevance and written basis`);
|
|
8658
|
+
}
|
|
8659
|
+
if (!["high", "medium", "low"].includes(String(value.confidence)) || !nonEmpty(value.confidenceBasis)) {
|
|
8660
|
+
throw new UserError(`${label2} requires a supported confidence and written basis`);
|
|
8661
|
+
}
|
|
8662
|
+
if (!FORGE_VERBS2.includes(value.proposedVerb)) throw new UserError(`${label2}.proposedVerb is unsupported`);
|
|
8663
|
+
if (!INVENTORY_COVERAGE_VALUES.includes(value.inventoryCoverage)) throw new UserError(`${label2}.inventoryCoverage is unsupported`);
|
|
8664
|
+
if (value.inventoryCoverage !== "complete" && !nonEmpty(value.inventoryCoverageReason)) {
|
|
8665
|
+
throw new UserError(`${label2}.inventoryCoverageReason is required when coverage is not complete`);
|
|
8666
|
+
}
|
|
8667
|
+
if (value.inventoryCoverage === "complete" && inventory.coverage !== "complete") {
|
|
8668
|
+
throw new UserError(`${label2} cannot claim complete inventory coverage when the current inventory is ${inventory.coverage}`);
|
|
8669
|
+
}
|
|
8670
|
+
if (!Array.isArray(value.capabilityCandidates)) throw new UserError(`${label2}.capabilityCandidates must be an array`);
|
|
8671
|
+
const inventoryById = new Map(inventory.capabilities.map((entry) => [entry.capabilityId, entry]));
|
|
8672
|
+
const capabilityCandidates = value.capabilityCandidates.map(
|
|
8673
|
+
(candidate, candidateIndex) => parseCapabilityCandidate(candidate, `${label2}.capabilityCandidates[${candidateIndex}]`, inventoryById)
|
|
8674
|
+
);
|
|
8675
|
+
if (value.proposedVerb === "ABSORB" && !capabilityCandidates.some((candidate) => candidate.relationship === "existing" || candidate.relationship === "extension")) {
|
|
8676
|
+
throw new UserError(`${label2}: ABSORB requires an exact existing/extension capability candidate`);
|
|
8677
|
+
}
|
|
8678
|
+
if (value.proposedVerb === "FORK") {
|
|
8679
|
+
if (value.inventoryCoverage !== "complete" || inventory.coverage !== "complete") {
|
|
8680
|
+
throw new UserError(`${label2}: FORK requires complete installed and skill-map inventory coverage`);
|
|
8681
|
+
}
|
|
8682
|
+
if (!capabilityCandidates.some((candidate) => candidate.relationship === "gap")) {
|
|
8683
|
+
throw new UserError(`${label2}: FORK requires an explicitly reviewed capability gap`);
|
|
8684
|
+
}
|
|
8685
|
+
if (capabilityCandidates.some((candidate) => candidate.relationship === "existing" || candidate.relationship === "ambiguous")) {
|
|
8686
|
+
throw new UserError(`${label2}: FORK is blocked while existing or ambiguous capability candidates remain`);
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
if (value.validation === "corroborated" && (new Set(value.sourceIds).size < 2 || new Set(value.extractionPassIds).size < 2)) {
|
|
8690
|
+
throw new UserError(`${label2}: corroborated evidence requires at least two sources and two extraction passes`);
|
|
8691
|
+
}
|
|
8692
|
+
const sources = study.sources.filter((source) => sourceIds.has(source.sourceId));
|
|
8693
|
+
const restricted = sources.some(
|
|
8694
|
+
(source) => ["copyleft", "proprietary", "unknown"].includes(source.licenseClass) || ["client-confidential", "restricted"].includes(source.accessClass)
|
|
8695
|
+
);
|
|
8696
|
+
if (restricted && (value.proposedVerb === "ABSORB" || value.proposedVerb === "FORK")) {
|
|
8697
|
+
throw new UserError(`${label2}: restrictive/private evidence cannot authorize ABSORB or FORK without a separately modeled legal exception`);
|
|
8698
|
+
}
|
|
8699
|
+
if (sources.some((source) => source.licenseClass === "attribution-required") && value.licenseConstraints.length === 0) {
|
|
8700
|
+
throw new UserError(`${label2}: attribution-required evidence must carry a license obligation`);
|
|
8701
|
+
}
|
|
8702
|
+
if (sources.some((source) => ["client-confidential", "restricted"].includes(source.accessClass)) && value.privacyConstraints.length === 0) {
|
|
8703
|
+
throw new UserError(`${label2}: private/restricted evidence must carry a privacy obligation`);
|
|
8704
|
+
}
|
|
8705
|
+
if (sources.some((source) => source.suspiciousInstructionFlags.length > 0) && value.securityConstraints.length === 0) {
|
|
8706
|
+
throw new UserError(`${label2}: suspicious source instructions must remain visible as a security constraint`);
|
|
8707
|
+
}
|
|
8708
|
+
const parsed = {
|
|
8709
|
+
...value,
|
|
8710
|
+
sourceIds: [...value.sourceIds],
|
|
8711
|
+
evidence,
|
|
8712
|
+
contradictingEvidence,
|
|
8713
|
+
capabilityCandidates
|
|
8714
|
+
};
|
|
8715
|
+
assertExportSafe(
|
|
8716
|
+
{
|
|
8717
|
+
claim: parsed.claim,
|
|
8718
|
+
domain: parsed.domain,
|
|
8719
|
+
unresolvedQuestions: parsed.unresolvedQuestions,
|
|
8720
|
+
relevanceBasis: parsed.relevanceBasis,
|
|
8721
|
+
confidenceBasis: parsed.confidenceBasis,
|
|
8722
|
+
provenanceConstraints: parsed.provenanceConstraints,
|
|
8723
|
+
licenseConstraints: parsed.licenseConstraints,
|
|
8724
|
+
privacyConstraints: parsed.privacyConstraints,
|
|
8725
|
+
securityConstraints: parsed.securityConstraints,
|
|
8726
|
+
candidateLabels: parsed.capabilityCandidates.map((candidate) => candidate.label)
|
|
8727
|
+
},
|
|
8728
|
+
study,
|
|
8729
|
+
label2
|
|
8730
|
+
);
|
|
8731
|
+
return parsed;
|
|
8732
|
+
}
|
|
8733
|
+
function parseDomainPlan(value, index, insightsById, inventory, study) {
|
|
8734
|
+
const label2 = `domainPlans[${index}]`;
|
|
8735
|
+
if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
|
|
8736
|
+
const required = [
|
|
8737
|
+
"domainId",
|
|
8738
|
+
"domain",
|
|
8739
|
+
"outcome",
|
|
8740
|
+
"acceptedInsightIds",
|
|
8741
|
+
"contestedInsightIds",
|
|
8742
|
+
"rejectedInsightIds",
|
|
8743
|
+
"targetCapabilityIds",
|
|
8744
|
+
"targetFingerprints",
|
|
8745
|
+
"deduplicationEvidence",
|
|
8746
|
+
"selectedVerb",
|
|
8747
|
+
"proposedChanges",
|
|
8748
|
+
"provenanceObligations",
|
|
8749
|
+
"licenseObligations",
|
|
8750
|
+
"privacyObligations",
|
|
8751
|
+
"securityObligations",
|
|
8752
|
+
"dependencies",
|
|
8753
|
+
"acceptanceTests",
|
|
8754
|
+
"measurement",
|
|
8755
|
+
"rollbackMechanism",
|
|
8756
|
+
"rollbackTrigger",
|
|
8757
|
+
"workItems",
|
|
8758
|
+
"nonGoals"
|
|
8759
|
+
];
|
|
8760
|
+
assertKeys(value, required, label2);
|
|
8761
|
+
assertRequired(value, required, label2);
|
|
8762
|
+
if (!nonEmpty(value.domainId) || !DOMAIN_ID.test(value.domainId) || !nonEmpty(value.domain) || !nonEmpty(value.outcome)) {
|
|
8763
|
+
throw new UserError(`${label2} requires a valid domainId, domain, and outcome`);
|
|
8764
|
+
}
|
|
8765
|
+
const groups = ["acceptedInsightIds", "contestedInsightIds", "rejectedInsightIds"];
|
|
8766
|
+
for (const key of groups) assertStringArray(value[key], `${label2}.${key}`);
|
|
8767
|
+
const allInsightIds = groups.flatMap((key) => value[key]);
|
|
8768
|
+
if (new Set(allInsightIds).size !== allInsightIds.length) throw new UserError(`${label2} classifies an insight more than once`);
|
|
8769
|
+
if (allInsightIds.some((id) => !insightsById.has(id))) throw new UserError(`${label2} references an unknown insight`);
|
|
8770
|
+
const expectedId = domainPlanIdFor(value.domain, allInsightIds);
|
|
8771
|
+
if (value.domainId !== expectedId) throw new UserError(`${label2}.domainId must equal the stable id ${expectedId}`);
|
|
8772
|
+
for (const insightId of allInsightIds) {
|
|
8773
|
+
if (insightsById.get(insightId).domain !== value.domain) throw new UserError(`${label2} contains an insight from another domain`);
|
|
8774
|
+
}
|
|
8775
|
+
for (const insightId of value.acceptedInsightIds) {
|
|
8776
|
+
const insight2 = insightsById.get(insightId);
|
|
8777
|
+
if (["contested", "rejected", "unverifiable"].includes(insight2.validation)) {
|
|
8778
|
+
throw new UserError(`${label2} cannot accept ${insight2.validation} insight ${insightId}`);
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
for (const insightId of value.contestedInsightIds) {
|
|
8782
|
+
if (insightsById.get(insightId).validation !== "contested") throw new UserError(`${label2} misclassifies non-contested insight ${insightId}`);
|
|
8783
|
+
}
|
|
8784
|
+
if (!FORGE_VERBS2.includes(value.selectedVerb)) throw new UserError(`${label2}.selectedVerb is unsupported`);
|
|
8785
|
+
const selectedVerb = value.selectedVerb;
|
|
8786
|
+
if (value.acceptedInsightIds.some((id) => insightsById.get(id).proposedVerb !== selectedVerb)) {
|
|
8787
|
+
throw new UserError(`${label2}.selectedVerb must match each accepted insight`);
|
|
8788
|
+
}
|
|
8789
|
+
assertStringArray(value.targetCapabilityIds, `${label2}.targetCapabilityIds`);
|
|
8790
|
+
if (!isRecord3(value.targetFingerprints)) throw new UserError(`${label2}.targetFingerprints must be an object`);
|
|
8791
|
+
const targetFingerprints = value.targetFingerprints;
|
|
8792
|
+
const inventoryById = new Map(inventory.capabilities.map((entry) => [entry.capabilityId, entry]));
|
|
8793
|
+
for (const capabilityId of value.targetCapabilityIds) {
|
|
8794
|
+
const current = inventoryById.get(capabilityId);
|
|
8795
|
+
if (!current) throw new UserError(`${label2} targets an unknown capability ${capabilityId}`);
|
|
8796
|
+
if (!current.fingerprint || targetFingerprints[capabilityId] !== current.fingerprint) {
|
|
8797
|
+
throw new UserError(`${label2} has a stale or unavailable fingerprint for ${capabilityId}`);
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
if (Object.keys(targetFingerprints).some((id) => !value.targetCapabilityIds.includes(id))) {
|
|
8801
|
+
throw new UserError(`${label2}.targetFingerprints contains an untargeted capability`);
|
|
8802
|
+
}
|
|
8803
|
+
if (selectedVerb === "ABSORB") {
|
|
8804
|
+
const exactCandidateIds = new Set(
|
|
8805
|
+
value.acceptedInsightIds.flatMap((insightId) => insightsById.get(insightId).capabilityCandidates).flatMap(
|
|
8806
|
+
(candidate) => (candidate.relationship === "existing" || candidate.relationship === "extension") && candidate.capabilityId ? [candidate.capabilityId] : []
|
|
8807
|
+
)
|
|
8808
|
+
);
|
|
8809
|
+
const hasExactTarget = value.targetCapabilityIds.some(
|
|
8810
|
+
(capabilityId) => exactCandidateIds.has(capabilityId)
|
|
8811
|
+
);
|
|
8812
|
+
if (!hasExactTarget) {
|
|
8813
|
+
throw new UserError(`${label2}: ABSORB must bind an exact existing/extension capability candidate and current fingerprint`);
|
|
8814
|
+
}
|
|
8815
|
+
}
|
|
8816
|
+
for (const key of [
|
|
8817
|
+
"deduplicationEvidence",
|
|
8818
|
+
"proposedChanges",
|
|
8819
|
+
"provenanceObligations",
|
|
8820
|
+
"licenseObligations",
|
|
8821
|
+
"privacyObligations",
|
|
8822
|
+
"securityObligations",
|
|
8823
|
+
"dependencies",
|
|
8824
|
+
"acceptanceTests",
|
|
8825
|
+
"nonGoals"
|
|
8826
|
+
]) assertStringArray(value[key], `${label2}.${key}`);
|
|
8827
|
+
if (value.acceptanceTests.length === 0) throw new UserError(`${label2}.acceptanceTests must not be empty`);
|
|
8828
|
+
if (!nonEmpty(value.rollbackMechanism) || !nonEmpty(value.rollbackTrigger)) throw new UserError(`${label2} requires rollback mechanism and trigger`);
|
|
8829
|
+
if (!isRecord3(value.measurement)) throw new UserError(`${label2}.measurement must be an object`);
|
|
8830
|
+
assertKeys(value.measurement, ["evidenceClass", "baseline", "method", "observationWindow", "stoppingRule", "guardrails"], `${label2}.measurement`);
|
|
8831
|
+
assertRequired(value.measurement, ["evidenceClass", "baseline", "method", "observationWindow", "stoppingRule", "guardrails"], `${label2}.measurement`);
|
|
8832
|
+
if (!["controlled", "natural", "observational"].includes(String(value.measurement.evidenceClass))) {
|
|
8833
|
+
throw new UserError(`${label2}.measurement.evidenceClass is unsupported`);
|
|
8834
|
+
}
|
|
8835
|
+
for (const key of ["baseline", "method", "observationWindow", "stoppingRule"]) {
|
|
8836
|
+
if (!nonEmpty(value.measurement[key])) throw new UserError(`${label2}.measurement.${key} must be non-empty`);
|
|
8837
|
+
}
|
|
8838
|
+
assertStringArray(value.measurement.guardrails, `${label2}.measurement.guardrails`);
|
|
8839
|
+
if (!Array.isArray(value.workItems) || value.workItems.length === 0) throw new UserError(`${label2}.workItems must not be empty`);
|
|
8840
|
+
const workItems = value.workItems.map((item, itemIndex) => {
|
|
8841
|
+
const itemLabel = `${label2}.workItems[${itemIndex}]`;
|
|
8842
|
+
if (!isRecord3(item)) throw new UserError(`${itemLabel} must be an object`);
|
|
8843
|
+
assertKeys(item, ["workItemId", "intent", "summary", "acceptanceCriteria", "dependsOn"], itemLabel);
|
|
8844
|
+
assertRequired(item, ["workItemId", "intent", "summary", "acceptanceCriteria", "dependsOn"], itemLabel);
|
|
8845
|
+
if (!nonEmpty(item.workItemId) || !SAFE_WORK_ITEM_ID.test(item.workItemId)) throw new UserError(`${itemLabel}.workItemId is invalid`);
|
|
8846
|
+
if (!["implementation", "measurement", "research", "prerequisite"].includes(String(item.intent)) || !nonEmpty(item.summary)) {
|
|
8847
|
+
throw new UserError(`${itemLabel} has an invalid intent or summary`);
|
|
8848
|
+
}
|
|
8849
|
+
assertStringArray(item.acceptanceCriteria, `${itemLabel}.acceptanceCriteria`, 1);
|
|
8850
|
+
assertStringArray(item.dependsOn, `${itemLabel}.dependsOn`);
|
|
8851
|
+
return item;
|
|
8852
|
+
});
|
|
8853
|
+
const workIds = new Set(workItems.map((item) => item.workItemId));
|
|
8854
|
+
if (workIds.size !== workItems.length) throw new UserError(`${label2}.workItems contains duplicate ids`);
|
|
8855
|
+
const primaryIntent = selectedVerb === "WATCH" ? "research" : "implementation";
|
|
8856
|
+
const primaryIds = workItems.filter((item) => item.intent === primaryIntent).map((item) => item.workItemId);
|
|
8857
|
+
const measurements = workItems.filter((item) => item.intent === "measurement");
|
|
8858
|
+
if (primaryIds.length === 0 || measurements.length === 0 || measurements.some((item) => !item.dependsOn.some((id) => primaryIds.includes(id)))) {
|
|
8859
|
+
throw new UserError(`${label2} requires ${primaryIntent} work plus dependent measurement work`);
|
|
8860
|
+
}
|
|
8861
|
+
const parsed = value;
|
|
8862
|
+
assertExportSafe({
|
|
8863
|
+
domain: parsed.domain,
|
|
8864
|
+
outcome: parsed.outcome,
|
|
8865
|
+
deduplicationEvidence: parsed.deduplicationEvidence,
|
|
8866
|
+
proposedChanges: parsed.proposedChanges,
|
|
8867
|
+
provenanceObligations: parsed.provenanceObligations,
|
|
8868
|
+
licenseObligations: parsed.licenseObligations,
|
|
8869
|
+
privacyObligations: parsed.privacyObligations,
|
|
8870
|
+
securityObligations: parsed.securityObligations,
|
|
8871
|
+
dependencies: parsed.dependencies,
|
|
8872
|
+
acceptanceTests: parsed.acceptanceTests,
|
|
8873
|
+
measurement: parsed.measurement,
|
|
8874
|
+
rollbackMechanism: parsed.rollbackMechanism,
|
|
8875
|
+
rollbackTrigger: parsed.rollbackTrigger,
|
|
8876
|
+
workItems: parsed.workItems,
|
|
8877
|
+
nonGoals: parsed.nonGoals
|
|
8878
|
+
}, study, label2);
|
|
8879
|
+
return parsed;
|
|
8880
|
+
}
|
|
8881
|
+
function parseSubmission(value, study, inventory) {
|
|
8882
|
+
if (!isRecord3(value)) throw new UserError(`insight submission must be an object`);
|
|
8883
|
+
const keys = ["schemaVersion", "studyId", "sourceInventoryHash", "capabilityInventoryHash", "actualAnalysisMode", "insights", "domainPlans"];
|
|
8884
|
+
assertKeys(value, keys, "insight submission");
|
|
8885
|
+
assertRequired(value, keys, "insight submission");
|
|
8886
|
+
if (value.schemaVersion !== INSIGHT_SCHEMA_VERSION) throw new UserError(`insight submission has an unsupported schemaVersion`);
|
|
8887
|
+
if (!nonEmpty(value.studyId) || !STUDY_ID2.test(value.studyId) || value.studyId !== study.studyId) {
|
|
8888
|
+
throw new UserError(`insight submission studyId does not match the target study`);
|
|
8889
|
+
}
|
|
8890
|
+
if (value.sourceInventoryHash !== study.sourceInventoryHash) throw new UserError(`source inventory changed; re-run analysis`);
|
|
8891
|
+
if (value.capabilityInventoryHash !== inventory.capabilityInventoryHash) throw new UserError(`capability inventory changed; re-run mapping`);
|
|
8892
|
+
if (!ANALYSIS_MODES.includes(value.actualAnalysisMode)) throw new UserError(`actualAnalysisMode is unsupported`);
|
|
8893
|
+
if (!Array.isArray(value.insights) || !Array.isArray(value.domainPlans)) throw new UserError(`insights and domainPlans must be arrays`);
|
|
8894
|
+
const insights = value.insights.map((insight2, index) => parseInsight(insight2, index, study, inventory));
|
|
8895
|
+
const insightIds = new Set(insights.map((insight2) => insight2.insightId));
|
|
8896
|
+
if (insightIds.size !== insights.length) throw new UserError(`insight submission contains duplicate insight ids`);
|
|
8897
|
+
if (insights.some((insight2) => insight2.independenceMode !== value.actualAnalysisMode)) {
|
|
8898
|
+
throw new UserError(`each insight independenceMode must match actualAnalysisMode`);
|
|
8899
|
+
}
|
|
8900
|
+
const domainPlans = value.domainPlans.map(
|
|
8901
|
+
(plan, index) => parseDomainPlan(plan, index, new Map(insights.map((insight2) => [insight2.insightId, insight2])), inventory, study)
|
|
8902
|
+
);
|
|
8903
|
+
if (new Set(domainPlans.map((plan) => plan.domainId)).size !== domainPlans.length) throw new UserError(`domainPlans contains duplicate ids`);
|
|
8904
|
+
const allWorkItems = domainPlans.flatMap((plan) => plan.workItems);
|
|
8905
|
+
const allWorkItemIds = new Set(allWorkItems.map((item) => item.workItemId));
|
|
8906
|
+
if (allWorkItemIds.size !== allWorkItems.length) throw new UserError(`domainPlans contains duplicate work-item ids`);
|
|
8907
|
+
if (allWorkItems.some((item) => item.dependsOn.some((id) => !allWorkItemIds.has(id)))) {
|
|
8908
|
+
throw new UserError(`domainPlans contains an unknown work-item dependency`);
|
|
8909
|
+
}
|
|
8910
|
+
const classified = domainPlans.flatMap((plan) => [
|
|
8911
|
+
...plan.acceptedInsightIds,
|
|
8912
|
+
...plan.contestedInsightIds,
|
|
8913
|
+
...plan.rejectedInsightIds
|
|
8914
|
+
]);
|
|
8915
|
+
if (new Set(classified).size !== classified.length || classified.length !== insights.length) {
|
|
8916
|
+
throw new UserError(`every insight must be classified exactly once across domain plans`);
|
|
8917
|
+
}
|
|
8918
|
+
return {
|
|
8919
|
+
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
8920
|
+
studyId: study.studyId,
|
|
8921
|
+
sourceInventoryHash: study.sourceInventoryHash,
|
|
8922
|
+
capabilityInventoryHash: inventory.capabilityInventoryHash,
|
|
8923
|
+
actualAnalysisMode: value.actualAnalysisMode,
|
|
8924
|
+
insights,
|
|
8925
|
+
domainPlans
|
|
8926
|
+
};
|
|
8927
|
+
}
|
|
8928
|
+
function insightPlanHash(value) {
|
|
8929
|
+
return stableHash({
|
|
8930
|
+
sourceInventoryHash: value.sourceInventoryHash,
|
|
8931
|
+
capabilityInventoryHash: value.capabilityInventoryHash,
|
|
8932
|
+
actualAnalysisMode: value.actualAnalysisMode,
|
|
8933
|
+
insights: value.insights,
|
|
8934
|
+
domainPlans: value.domainPlans
|
|
8935
|
+
});
|
|
8936
|
+
}
|
|
8937
|
+
function validateCurrentSourceFingerprints(study) {
|
|
8938
|
+
const issues = [];
|
|
8939
|
+
for (const source of study.sources) {
|
|
8940
|
+
if (source.type !== "file" && source.type !== "directory") continue;
|
|
8941
|
+
try {
|
|
8942
|
+
const normalized = normalizeSources([
|
|
8943
|
+
source.type === "file" ? { type: "file", path: source.privateLocator, mediaType: source.mediaType } : { type: "directory", path: source.privateLocator, includeHidden: source.includeHidden }
|
|
8944
|
+
]);
|
|
8945
|
+
if (normalized.sources[0]?.fingerprint !== source.fingerprint) {
|
|
8946
|
+
issues.push({
|
|
8947
|
+
severity: "blocker",
|
|
8948
|
+
code: "source-drift",
|
|
8949
|
+
message: `Local source ${source.sourceId} changed after analysis.`
|
|
8950
|
+
});
|
|
8951
|
+
}
|
|
8952
|
+
} catch (error) {
|
|
8953
|
+
issues.push({
|
|
8954
|
+
severity: "blocker",
|
|
8955
|
+
code: "source-unavailable",
|
|
8956
|
+
message: `Local source ${source.sourceId} cannot be revalidated: ${error.message}`
|
|
8957
|
+
});
|
|
8958
|
+
}
|
|
8959
|
+
}
|
|
8960
|
+
return issues;
|
|
8961
|
+
}
|
|
8962
|
+
function validateInsightSubmission(study, value, inventory) {
|
|
8963
|
+
if (["approved", "jira-manifested", "dismissed", "archived"].includes(study.status)) {
|
|
8964
|
+
throw new UserError(`study ${study.studyId} is ${study.status} and cannot accept an analysis submission`);
|
|
8965
|
+
}
|
|
8966
|
+
const submission = parseSubmission(value, study, inventory);
|
|
8967
|
+
const next = {
|
|
8968
|
+
...study,
|
|
8969
|
+
status: "review-ready",
|
|
8970
|
+
actualAnalysisMode: submission.actualAnalysisMode,
|
|
8971
|
+
capabilityInventoryHash: submission.capabilityInventoryHash,
|
|
8972
|
+
insights: submission.insights,
|
|
8973
|
+
domainPlans: submission.domainPlans,
|
|
8974
|
+
approval: void 0,
|
|
8975
|
+
jira: void 0
|
|
8976
|
+
};
|
|
8977
|
+
next.planHash = insightPlanHash(next);
|
|
8978
|
+
return next;
|
|
8979
|
+
}
|
|
8980
|
+
function validateStudyForReview(study, inventory) {
|
|
8981
|
+
const blockers = [];
|
|
8982
|
+
const warnings = [];
|
|
8983
|
+
const planHash = study.capabilityInventoryHash && study.actualAnalysisMode ? insightPlanHash(study) : stableHash({ studyId: study.studyId, domainPlans: study.domainPlans });
|
|
8984
|
+
if (!study.actualAnalysisMode) blockers.push({ severity: "blocker", code: "analysis-mode-missing", message: "No actual analysis mode is recorded." });
|
|
8985
|
+
if (!study.capabilityInventoryHash) blockers.push({ severity: "blocker", code: "inventory-missing", message: "No capability inventory hash is recorded." });
|
|
8986
|
+
if (study.planHash !== planHash) blockers.push({ severity: "blocker", code: "plan-hash-stale", message: "The stored plan hash does not match current analysis and plans." });
|
|
8987
|
+
if (study.insights.length === 0) blockers.push({ severity: "blocker", code: "insights-empty", message: "No validated insights were submitted." });
|
|
8988
|
+
if (study.domainPlans.length === 0) blockers.push({ severity: "blocker", code: "plans-empty", message: "No domain plans were submitted." });
|
|
8989
|
+
blockers.push(...validateCurrentSourceFingerprints(study));
|
|
8990
|
+
if (inventory && study.capabilityInventoryHash !== inventory.capabilityInventoryHash) {
|
|
8991
|
+
blockers.push({ severity: "blocker", code: "inventory-drift", message: "The capability inventory changed after analysis." });
|
|
8992
|
+
}
|
|
8993
|
+
if (inventory && inventory.coverage !== "complete") {
|
|
8994
|
+
warnings.push({ severity: "warning", code: "inventory-incomplete", message: inventory.coverageReason ?? "Capability inventory coverage is incomplete." });
|
|
8995
|
+
}
|
|
8996
|
+
if (inventory && study.actualAnalysisMode && study.capabilityInventoryHash) {
|
|
8997
|
+
try {
|
|
8998
|
+
parseSubmission({
|
|
8999
|
+
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
9000
|
+
studyId: study.studyId,
|
|
9001
|
+
sourceInventoryHash: study.sourceInventoryHash,
|
|
9002
|
+
capabilityInventoryHash: study.capabilityInventoryHash,
|
|
9003
|
+
actualAnalysisMode: study.actualAnalysisMode,
|
|
9004
|
+
insights: study.insights,
|
|
9005
|
+
domainPlans: study.domainPlans
|
|
9006
|
+
}, study, inventory);
|
|
9007
|
+
} catch (error) {
|
|
9008
|
+
blockers.push({
|
|
9009
|
+
severity: "blocker",
|
|
9010
|
+
code: "submission-invalid",
|
|
9011
|
+
message: error.message
|
|
9012
|
+
});
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
9015
|
+
if (study.requestedAnalysisMode === "independent" && study.actualAnalysisMode === "single-context") {
|
|
9016
|
+
warnings.push({ severity: "warning", code: "independence-degraded", message: "Independent analysis was requested but only a single context was recorded." });
|
|
9017
|
+
}
|
|
9018
|
+
for (const insight2 of study.insights) {
|
|
9019
|
+
if (insight2.validation === "contested") warnings.push({ severity: "warning", code: "contested", message: "Contradicting evidence remains visible.", insightId: insight2.insightId });
|
|
9020
|
+
if (insight2.validation === "single-source") warnings.push({ severity: "warning", code: "single-source", message: "This insight has only single-source support.", insightId: insight2.insightId });
|
|
9021
|
+
if (insight2.inventoryCoverage !== "complete") warnings.push({ severity: "warning", code: "insight-coverage-incomplete", message: insight2.inventoryCoverageReason ?? "Insight mapping coverage is incomplete.", insightId: insight2.insightId });
|
|
9022
|
+
}
|
|
9023
|
+
return { valid: blockers.length === 0, blockers, warnings, planHash };
|
|
9024
|
+
}
|
|
9025
|
+
function assertCurrentApproval(study) {
|
|
9026
|
+
if (study.status !== "approved" && study.status !== "jira-manifested" || !study.approval || !study.planHash) {
|
|
9027
|
+
throw new UserError(`study ${study.studyId} does not have a current human approval`);
|
|
9028
|
+
}
|
|
9029
|
+
const expectedPlanHash = insightPlanHash(study);
|
|
9030
|
+
if (study.planHash !== expectedPlanHash || study.approval.planHash !== expectedPlanHash) {
|
|
9031
|
+
throw new UserError(`study ${study.studyId} approval is stale because the plan hash changed`);
|
|
9032
|
+
}
|
|
9033
|
+
if (sourceInventoryHashFor(study.sources) !== study.sourceInventoryHash) {
|
|
9034
|
+
throw new UserError(`study ${study.studyId} approval is stale because the source policy changed`);
|
|
9035
|
+
}
|
|
9036
|
+
if (study.approval.sourceInventoryHash !== study.sourceInventoryHash) {
|
|
9037
|
+
throw new UserError(`study ${study.studyId} approval is stale because the source inventory changed`);
|
|
9038
|
+
}
|
|
9039
|
+
if (!study.capabilityInventoryHash || study.approval.capabilityInventoryHash !== study.capabilityInventoryHash) {
|
|
9040
|
+
throw new UserError(`study ${study.studyId} approval is stale because the capability inventory changed`);
|
|
9041
|
+
}
|
|
9042
|
+
const domainIds = new Set(study.domainPlans.map((plan) => plan.domainId));
|
|
9043
|
+
if (study.approval.approvedDomainIds.length === 0 || study.approval.approvedDomainIds.some((id) => !domainIds.has(id))) {
|
|
9044
|
+
throw new UserError(`study ${study.studyId} approval names an unknown domain plan`);
|
|
9045
|
+
}
|
|
9046
|
+
const sourceIssues = validateCurrentSourceFingerprints(study);
|
|
9047
|
+
if (sourceIssues.length > 0) throw new UserError(sourceIssues[0].message);
|
|
9048
|
+
return study.approval;
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
// src/insights/jira.ts
|
|
9052
|
+
var PROJECT_KEY = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
9053
|
+
var RECEIPT_EVENT_ID = /^JIRA-EVT-[0-9a-f]{16}$/;
|
|
9054
|
+
var RECEIPT_KINDS = ["attempted", "found", "created", "failed"];
|
|
9055
|
+
var SAFE_MARKER_PART = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9056
|
+
var ISSUE_KEY = /^[A-Za-z][A-Za-z0-9_-]*-[1-9][0-9]*$/;
|
|
9057
|
+
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
9058
|
+
function nonEmpty2(value) {
|
|
9059
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
9060
|
+
}
|
|
9061
|
+
function isRecord4(value) {
|
|
9062
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9063
|
+
}
|
|
9064
|
+
function safeText(value, max = 2e3) {
|
|
9065
|
+
const normalized = value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "").replace(/\s+/gu, " ").trim();
|
|
9066
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max)}\u2026`;
|
|
9067
|
+
}
|
|
9068
|
+
function label(value) {
|
|
9069
|
+
return value.toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 64) || "unclassified";
|
|
9070
|
+
}
|
|
9071
|
+
function markerFor(studyId, workItemId) {
|
|
9072
|
+
if (!SAFE_MARKER_PART.test(workItemId)) throw new UserError(`invalid Jira work-item id for marker: ${workItemId}`);
|
|
9073
|
+
return `skill-forge-study:${studyId}:${workItemId}`;
|
|
9074
|
+
}
|
|
9075
|
+
function approvedPlans(study) {
|
|
9076
|
+
const approval = assertCurrentApproval(study);
|
|
9077
|
+
const approvedIds = new Set(approval.approvedDomainIds);
|
|
9078
|
+
return study.domainPlans.filter((plan) => approvedIds.has(plan.domainId));
|
|
9079
|
+
}
|
|
9080
|
+
function sourceIdsFor(plan, study) {
|
|
9081
|
+
const insightIds = /* @__PURE__ */ new Set([
|
|
9082
|
+
...plan.acceptedInsightIds,
|
|
9083
|
+
...plan.contestedInsightIds,
|
|
9084
|
+
...plan.rejectedInsightIds
|
|
9085
|
+
]);
|
|
9086
|
+
return [...new Set(
|
|
9087
|
+
study.insights.filter((insight2) => insightIds.has(insight2.insightId)).flatMap((insight2) => insight2.sourceIds)
|
|
9088
|
+
)].sort();
|
|
9089
|
+
}
|
|
9090
|
+
function approvedPublicUrl(value) {
|
|
9091
|
+
if (!value) return void 0;
|
|
9092
|
+
try {
|
|
9093
|
+
return canonicalPublicUrl(value, "source public locator");
|
|
9094
|
+
} catch {
|
|
9095
|
+
return void 0;
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
function implementationRoute(plan) {
|
|
9099
|
+
switch (plan.selectedVerb) {
|
|
9100
|
+
case "ABSORB":
|
|
9101
|
+
return "Use the existing refinement or targeted plugin-change path; preview first and preserve rollback.";
|
|
9102
|
+
case "DEFER":
|
|
9103
|
+
return "Keep the maintained source/capability authoritative; add only a thin wrapper or reference when approved.";
|
|
9104
|
+
case "FORK":
|
|
9105
|
+
return "Gate and deduplicate the proposed custom capability before any promotion or repository implementation.";
|
|
9106
|
+
case "REJECT":
|
|
9107
|
+
return "No capability mutation; document why the existing behavior is sufficient.";
|
|
9108
|
+
case "WATCH":
|
|
9109
|
+
return "Research and measure only; do not implement a capability until evidence and fit are ready.";
|
|
9110
|
+
}
|
|
9111
|
+
}
|
|
9112
|
+
function measurementText(plan) {
|
|
9113
|
+
const measurement = plan.measurement;
|
|
9114
|
+
return safeText(
|
|
9115
|
+
`${measurement.evidenceClass}: baseline ${measurement.baseline}; method ${measurement.method}; window ${measurement.observationWindow}; stopping rule ${measurement.stoppingRule}`
|
|
9116
|
+
);
|
|
9117
|
+
}
|
|
9118
|
+
function workItemFor(plan, workItem, study) {
|
|
9119
|
+
const insightIds = [.../* @__PURE__ */ new Set([
|
|
9120
|
+
...plan.acceptedInsightIds,
|
|
9121
|
+
...plan.contestedInsightIds,
|
|
9122
|
+
...plan.rejectedInsightIds
|
|
9123
|
+
])].sort();
|
|
9124
|
+
return {
|
|
9125
|
+
workItemId: workItem.workItemId,
|
|
9126
|
+
marker: markerFor(study.studyId, workItem.workItemId),
|
|
9127
|
+
issueTypeIntent: workItem.intent,
|
|
9128
|
+
summary: safeText(workItem.summary, 255),
|
|
9129
|
+
context: safeText(`${plan.outcome} Domain plan: ${plan.domainId}. Evidence is referenced by stable source/insight ids only.`),
|
|
9130
|
+
requirements: plan.proposedChanges.map((entry) => safeText(entry)),
|
|
9131
|
+
acceptanceCriteria: workItem.acceptanceCriteria.map((entry) => safeText(entry)),
|
|
9132
|
+
dependencies: workItem.dependsOn.map((entry) => markerFor(study.studyId, entry)),
|
|
9133
|
+
implementationRoute: implementationRoute(plan),
|
|
9134
|
+
measurement: measurementText(plan),
|
|
9135
|
+
rollback: safeText(`${plan.rollbackMechanism} Trigger: ${plan.rollbackTrigger}`),
|
|
9136
|
+
labels: ["skill-forge", "source-insight", `domain-${label(plan.domain)}`, `intent-${label(workItem.intent)}`],
|
|
9137
|
+
domainIds: [plan.domainId],
|
|
9138
|
+
insightIds,
|
|
9139
|
+
sourceIds: sourceIdsFor(plan, study)
|
|
9140
|
+
};
|
|
9141
|
+
}
|
|
9142
|
+
function buildJiraManifest(study, projectKey, options = {}) {
|
|
9143
|
+
if (!PROJECT_KEY.test(projectKey)) throw new UserError(`invalid Jira project key: ${projectKey}`);
|
|
9144
|
+
const plans = approvedPlans(study);
|
|
9145
|
+
if (plans.length === 0) throw new UserError(`the current approval contains no domain plans`);
|
|
9146
|
+
const workItems = plans.flatMap((plan) => plan.workItems.map((item) => workItemFor(plan, item, study)));
|
|
9147
|
+
const ids = new Set(workItems.map((item) => item.workItemId));
|
|
9148
|
+
if (ids.size !== workItems.length) throw new UserError(`approved plans contain duplicate Jira work-item ids`);
|
|
9149
|
+
if (workItems.some((item) => item.dependencies.some((dependency) => !ids.has(dependency.split(":").at(-1))))) {
|
|
9150
|
+
throw new UserError(`approved plans depend on a work item outside the approved Jira manifest`);
|
|
9151
|
+
}
|
|
9152
|
+
const epicId = stableId("EPIC", { studyId: study.studyId, planHash: study.planHash });
|
|
9153
|
+
const approvedSourceIds = new Set(plans.flatMap((plan) => sourceIdsFor(plan, study)));
|
|
9154
|
+
const approvedSourceLinks = [...new Set(
|
|
9155
|
+
study.sources.filter((source) => approvedSourceIds.has(source.sourceId) && source.accessClass === "public").map((source) => approvedPublicUrl(source.publicLocator)).filter((locator) => locator !== void 0)
|
|
9156
|
+
)].sort();
|
|
9157
|
+
const manifest = {
|
|
9158
|
+
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
9159
|
+
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
9160
|
+
studyId: study.studyId,
|
|
9161
|
+
approvedPlanHash: study.planHash,
|
|
9162
|
+
projectKey: projectKey.toUpperCase(),
|
|
9163
|
+
backlogMode: "epic",
|
|
9164
|
+
epic: {
|
|
9165
|
+
workItemId: epicId,
|
|
9166
|
+
marker: markerFor(study.studyId, epicId),
|
|
9167
|
+
summary: safeText(`Source insight improvements \u2014 ${study.studyId}`, 255),
|
|
9168
|
+
overview: "Implement only the human-approved source-insight domain plans. Source material remains untrusted data and private locators are excluded.",
|
|
9169
|
+
objectives: plans.map((plan) => safeText(plan.outcome)),
|
|
9170
|
+
scope: plans.map((plan) => `${plan.domainId}: ${safeText(plan.domain)}`),
|
|
9171
|
+
successCriteria: plans.flatMap((plan) => plan.acceptanceTests.map((entry) => safeText(entry))),
|
|
9172
|
+
approvedSourceLinks,
|
|
9173
|
+
labels: ["skill-forge", "source-insight", `study-${label(study.studyId)}`]
|
|
9174
|
+
},
|
|
9175
|
+
workItems
|
|
9176
|
+
};
|
|
9177
|
+
assertExportSafe(manifest, study, "Jira manifest");
|
|
9178
|
+
return manifest;
|
|
9179
|
+
}
|
|
9180
|
+
function getJiraReceiptsPath(studyId) {
|
|
9181
|
+
return join31(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9182
|
+
}
|
|
9183
|
+
function assertCurrentJiraManifest(study, value) {
|
|
9184
|
+
if (study.status !== "jira-manifested" || !study.jira) {
|
|
9185
|
+
throw new UserError(`study ${study.studyId} does not have a current Jira manifest`);
|
|
9186
|
+
}
|
|
9187
|
+
if (!isRecord4(value) || !nonEmpty2(value.projectKey) || !nonEmpty2(value.generatedAt)) {
|
|
9188
|
+
throw new UserError(`stored Jira manifest for study ${study.studyId} is malformed`);
|
|
9189
|
+
}
|
|
9190
|
+
const generatedAt = new Date(value.generatedAt);
|
|
9191
|
+
if (Number.isNaN(generatedAt.getTime())) {
|
|
9192
|
+
throw new UserError(`stored Jira manifest for study ${study.studyId} has an invalid generatedAt`);
|
|
9193
|
+
}
|
|
9194
|
+
const expected = buildJiraManifest(study, value.projectKey, { now: generatedAt });
|
|
9195
|
+
const manifestHash = stableHash(value);
|
|
9196
|
+
if (stableHash(expected) !== manifestHash || study.jira.manifestHash !== manifestHash || study.jira.projectKey.toUpperCase() !== expected.projectKey || study.jira.generatedAt !== expected.generatedAt) {
|
|
9197
|
+
throw new UserError(`stored Jira manifest for study ${study.studyId} does not match the current approved plan`);
|
|
9198
|
+
}
|
|
9199
|
+
return expected;
|
|
9200
|
+
}
|
|
9201
|
+
function manifestMarkers(manifest) {
|
|
9202
|
+
return /* @__PURE__ */ new Set([manifest.epic.marker, ...manifest.workItems.map((item) => item.marker)]);
|
|
9203
|
+
}
|
|
9204
|
+
function assertManifestMarker(marker, manifest, label2) {
|
|
9205
|
+
if (!manifestMarkers(manifest).has(marker)) {
|
|
9206
|
+
throw new UserError(`${label2}: marker is not present in the current approved Jira manifest`);
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
9209
|
+
function assertResultProjectKey(event, manifest, label2) {
|
|
9210
|
+
if (event.kind !== "created" && event.kind !== "found") return;
|
|
9211
|
+
const expectedPrefix = `${manifest.projectKey}-`;
|
|
9212
|
+
const issueKey = event.issueKey;
|
|
9213
|
+
if (!nonEmpty2(issueKey) || !issueKey.startsWith(expectedPrefix) || !/^[1-9][0-9]*$/.test(issueKey.slice(expectedPrefix.length))) {
|
|
9214
|
+
throw new UserError(`${label2}: issueKey does not match Jira project ${manifest.projectKey}`);
|
|
9215
|
+
}
|
|
9216
|
+
}
|
|
9217
|
+
function parseJiraReceiptInput(value) {
|
|
9218
|
+
if (!isRecord4(value)) throw new UserError(`Jira receipt import must be an object`);
|
|
9219
|
+
const allowed = /* @__PURE__ */ new Set(["kind", "marker", "occurredAt", "issueKey", "issueUrl", "error"]);
|
|
9220
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
9221
|
+
if (extra.length > 0) throw new UserError(`Jira receipt import contains unsupported field(s): ${extra.join(", ")}`);
|
|
9222
|
+
if (!RECEIPT_KINDS.includes(value.kind) || !nonEmpty2(value.marker)) {
|
|
9223
|
+
throw new UserError(`Jira receipt import requires a supported kind and non-empty marker`);
|
|
9224
|
+
}
|
|
9225
|
+
if (value.occurredAt !== void 0 && (!nonEmpty2(value.occurredAt) || Number.isNaN(Date.parse(value.occurredAt)))) {
|
|
9226
|
+
throw new UserError(`Jira receipt import occurredAt must be an ISO-8601 timestamp`);
|
|
9227
|
+
}
|
|
9228
|
+
for (const key of ["issueKey", "issueUrl", "error"]) {
|
|
9229
|
+
if (value[key] !== void 0 && typeof value[key] !== "string") {
|
|
9230
|
+
throw new UserError(`Jira receipt import ${key} must be a string`);
|
|
9231
|
+
}
|
|
9232
|
+
}
|
|
9233
|
+
return value;
|
|
9234
|
+
}
|
|
9235
|
+
function assertReceiptShape(value, studyId, manifest, line) {
|
|
9236
|
+
const where = line ? ` line ${line}` : "";
|
|
9237
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new UserError(`malformed Jira receipts${where}: expected an object`);
|
|
9238
|
+
const event = value;
|
|
9239
|
+
const allowed = /* @__PURE__ */ new Set(["eventId", "studyId", "kind", "marker", "occurredAt", "issueKey", "issueUrl", "error"]);
|
|
9240
|
+
const extra = Object.keys(event).filter((key) => !allowed.has(key));
|
|
9241
|
+
if (extra.length > 0) throw new UserError(`malformed Jira receipts${where}: unsupported fields ${extra.join(", ")}`);
|
|
9242
|
+
if (!nonEmpty2(event.eventId) || !RECEIPT_EVENT_ID.test(event.eventId) || event.studyId !== studyId) {
|
|
9243
|
+
throw new UserError(`malformed Jira receipts${where}: invalid event identity`);
|
|
9244
|
+
}
|
|
9245
|
+
if (!RECEIPT_KINDS.includes(event.kind) || !nonEmpty2(event.marker)) {
|
|
9246
|
+
throw new UserError(`malformed Jira receipts${where}: invalid kind or marker`);
|
|
9247
|
+
}
|
|
9248
|
+
const markerPrefix = `skill-forge-study:${studyId}:`;
|
|
9249
|
+
if (!event.marker.startsWith(markerPrefix) || !SAFE_MARKER_PART.test(event.marker.slice(markerPrefix.length)) || CONTROL.test(event.marker)) {
|
|
9250
|
+
throw new UserError(`malformed Jira receipts${where}: marker does not belong to the study`);
|
|
9251
|
+
}
|
|
9252
|
+
assertManifestMarker(event.marker, manifest, `malformed Jira receipts${where}`);
|
|
9253
|
+
if (!nonEmpty2(event.occurredAt) || Number.isNaN(Date.parse(event.occurredAt))) {
|
|
9254
|
+
throw new UserError(`malformed Jira receipts${where}: occurredAt is invalid`);
|
|
9255
|
+
}
|
|
9256
|
+
if ((event.kind === "created" || event.kind === "found") && (!nonEmpty2(event.issueKey) || !nonEmpty2(event.issueUrl))) {
|
|
9257
|
+
throw new UserError(`malformed Jira receipts${where}: ${event.kind} requires issueKey and issueUrl`);
|
|
9258
|
+
}
|
|
9259
|
+
if (event.issueKey !== void 0 && !ISSUE_KEY.test(String(event.issueKey))) {
|
|
9260
|
+
throw new UserError(`malformed Jira receipts${where}: issueKey is invalid`);
|
|
9261
|
+
}
|
|
9262
|
+
assertResultProjectKey(event, manifest, `malformed Jira receipts${where}`);
|
|
9263
|
+
if (event.issueUrl !== void 0) {
|
|
9264
|
+
try {
|
|
9265
|
+
if (canonicalPublicUrl(String(event.issueUrl), "Jira issue URL") !== event.issueUrl) {
|
|
9266
|
+
throw new Error("non-canonical URL");
|
|
9267
|
+
}
|
|
9268
|
+
} catch {
|
|
9269
|
+
throw new UserError(`malformed Jira receipts${where}: issueUrl violates the public URL policy`);
|
|
9270
|
+
}
|
|
9271
|
+
}
|
|
9272
|
+
if (event.kind === "failed" && !nonEmpty2(event.error)) throw new UserError(`malformed Jira receipts${where}: failed requires error`);
|
|
9273
|
+
for (const key of ["issueKey", "issueUrl", "error"]) {
|
|
9274
|
+
if (event[key] !== void 0 && (typeof event[key] !== "string" || CONTROL.test(event[key]))) {
|
|
9275
|
+
throw new UserError(`malformed Jira receipts${where}: ${key} is invalid`);
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
const expectedEventId = stableId("JIRA-EVT", {
|
|
9279
|
+
studyId,
|
|
9280
|
+
kind: event.kind,
|
|
9281
|
+
marker: event.marker,
|
|
9282
|
+
occurredAt: event.occurredAt,
|
|
9283
|
+
issueKey: event.issueKey,
|
|
9284
|
+
issueUrl: event.issueUrl,
|
|
9285
|
+
error: event.error
|
|
9286
|
+
});
|
|
9287
|
+
if (event.eventId !== expectedEventId) throw new UserError(`malformed Jira receipts${where}: eventId does not match event contents`);
|
|
9288
|
+
}
|
|
9289
|
+
function appendJiraReceipt(studyId, manifest, input) {
|
|
9290
|
+
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
9291
|
+
const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9292
|
+
const identity = {
|
|
9293
|
+
studyId,
|
|
9294
|
+
kind: input.kind,
|
|
9295
|
+
marker: input.marker,
|
|
9296
|
+
occurredAt,
|
|
9297
|
+
issueKey: input.issueKey,
|
|
9298
|
+
issueUrl: input.issueUrl,
|
|
9299
|
+
error: input.error
|
|
9300
|
+
};
|
|
9301
|
+
const event = {
|
|
9302
|
+
eventId: stableId("JIRA-EVT", identity),
|
|
9303
|
+
...identity
|
|
9304
|
+
};
|
|
9305
|
+
assertReceiptShape(event, studyId, currentManifest);
|
|
9306
|
+
const path = getJiraReceiptsPath(studyId);
|
|
9307
|
+
appendPrivateJsonLine(path, event);
|
|
9308
|
+
return event;
|
|
9309
|
+
}
|
|
9310
|
+
function readJiraReceipts(studyId, manifest) {
|
|
9311
|
+
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
9312
|
+
const path = getJiraReceiptsPath(studyId);
|
|
9313
|
+
if (!existsSync26(path)) return [];
|
|
9314
|
+
let text;
|
|
9315
|
+
try {
|
|
9316
|
+
text = readFileSync21(path, "utf-8");
|
|
9317
|
+
} catch (error) {
|
|
9318
|
+
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
9319
|
+
}
|
|
9320
|
+
return text.split("\n").filter(Boolean).map((line, index) => {
|
|
9321
|
+
let value;
|
|
9322
|
+
try {
|
|
9323
|
+
value = JSON.parse(line);
|
|
9324
|
+
} catch {
|
|
9325
|
+
throw new UserError(`malformed Jira receipts line ${index + 1}: not valid JSON`);
|
|
9326
|
+
}
|
|
9327
|
+
assertReceiptShape(value, studyId, currentManifest, index + 1);
|
|
9328
|
+
return value;
|
|
9329
|
+
});
|
|
9330
|
+
}
|
|
9331
|
+
function resumeItem(marker, workItemId, kind, receipts) {
|
|
9332
|
+
const events = receipts.filter((event) => event.marker === marker);
|
|
9333
|
+
const completed = [...events].reverse().find((event) => event.kind === "created" || event.kind === "found");
|
|
9334
|
+
if (completed) {
|
|
9335
|
+
return { marker, workItemId, kind, state: "created", issueKey: completed.issueKey, issueUrl: completed.issueUrl };
|
|
9336
|
+
}
|
|
9337
|
+
const last = events.at(-1);
|
|
9338
|
+
if (last) return { marker, workItemId, kind, state: "search-required", lastError: last.error };
|
|
9339
|
+
return { marker, workItemId, kind, state: "create" };
|
|
9340
|
+
}
|
|
9341
|
+
function buildJiraResumeState(manifest, receipts) {
|
|
9342
|
+
for (const receipt of receipts) {
|
|
9343
|
+
if (receipt.studyId !== manifest.studyId) {
|
|
9344
|
+
throw new UserError(`Jira receipt ${receipt.eventId} belongs to another study`);
|
|
9345
|
+
}
|
|
9346
|
+
assertManifestMarker(receipt.marker, manifest, `Jira receipt ${receipt.eventId}`);
|
|
9347
|
+
assertResultProjectKey(receipt, manifest, `Jira receipt ${receipt.eventId}`);
|
|
9348
|
+
}
|
|
9349
|
+
const epic = resumeItem(manifest.epic.marker, manifest.epic.workItemId, "epic", receipts);
|
|
9350
|
+
const workItems = manifest.workItems.map((item) => resumeItem(item.marker, item.workItemId, "work-item", receipts));
|
|
9351
|
+
const all = [epic, ...workItems];
|
|
9352
|
+
const complete = all.every((item) => item.state === "created");
|
|
9353
|
+
const nextAction = complete ? "complete" : all.some((item) => item.state === "search-required") ? "search-by-marker" : epic.state !== "created" ? "create-epic" : "create-work-item";
|
|
9354
|
+
return { epic, workItems, complete, nextAction };
|
|
9355
|
+
}
|
|
9356
|
+
|
|
9357
|
+
// src/insights/render.ts
|
|
9358
|
+
var ANSI_ESCAPE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/gu;
|
|
9359
|
+
var UNSAFE_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
9360
|
+
var MAX_RENDERED_TEXT = 4e3;
|
|
9361
|
+
function sanitizeRenderedText(value) {
|
|
9362
|
+
const text = String(value ?? "").replace(ANSI_ESCAPE, "").replace(UNSAFE_CONTROL, "").replace(/\r\n?|\u2028|\u2029/gu, "\n");
|
|
9363
|
+
return text.length <= MAX_RENDERED_TEXT ? text : `${text.slice(0, MAX_RENDERED_TEXT)}\u2026`;
|
|
9364
|
+
}
|
|
9365
|
+
function oneLine(value) {
|
|
9366
|
+
return sanitizeRenderedText(value).replace(/\s+/gu, " ").trim();
|
|
9367
|
+
}
|
|
9368
|
+
function bullets(values, empty = "none") {
|
|
9369
|
+
return values.length > 0 ? values.map((value) => ` - ${oneLine(value)}`) : [` - ${empty}`];
|
|
9370
|
+
}
|
|
9371
|
+
function renderDomainPlan(plan) {
|
|
9372
|
+
const lines = [
|
|
9373
|
+
`${oneLine(plan.domain)} (${oneLine(plan.domainId)})`,
|
|
9374
|
+
` Outcome: ${oneLine(plan.outcome)}`,
|
|
9375
|
+
` Forge verb: ${oneLine(plan.selectedVerb)}`,
|
|
9376
|
+
` Accepted insights: ${plan.acceptedInsightIds.map(oneLine).join(", ") || "none"}`,
|
|
9377
|
+
` Contested insights: ${plan.contestedInsightIds.map(oneLine).join(", ") || "none"}`,
|
|
9378
|
+
` Rejected insights: ${plan.rejectedInsightIds.map(oneLine).join(", ") || "none"}`,
|
|
9379
|
+
` Targets: ${plan.targetCapabilityIds.length} private capability id(s)`,
|
|
9380
|
+
" Proposed changes:",
|
|
9381
|
+
...bullets(plan.proposedChanges),
|
|
9382
|
+
" Acceptance tests:",
|
|
9383
|
+
...bullets(plan.acceptanceTests),
|
|
9384
|
+
` Measurement: ${oneLine(plan.measurement.evidenceClass)} \u2014 ${oneLine(plan.measurement.method)}`,
|
|
9385
|
+
` Stopping rule: ${oneLine(plan.measurement.stoppingRule)}`,
|
|
9386
|
+
` Rollback: ${oneLine(plan.rollbackMechanism)} (trigger: ${oneLine(plan.rollbackTrigger)})`,
|
|
9387
|
+
" Non-goals:",
|
|
9388
|
+
...bullets(plan.nonGoals)
|
|
9389
|
+
];
|
|
9390
|
+
return lines.join("\n");
|
|
9391
|
+
}
|
|
9392
|
+
function renderInsightReview(study, inventory, validation = validateStudyForReview(study, inventory)) {
|
|
9393
|
+
const lines = [
|
|
9394
|
+
`Insight study ${oneLine(study.studyId)}`,
|
|
9395
|
+
`Status: ${oneLine(study.status)}`,
|
|
9396
|
+
`Plan hash: ${oneLine(validation.planHash)}`,
|
|
9397
|
+
`Analysis: requested ${oneLine(study.requestedAnalysisMode)}; actual ${oneLine(study.actualAnalysisMode ?? "not submitted")}`,
|
|
9398
|
+
`Inventory: ${inventory ? `${oneLine(inventory.coverage)} (${inventory.installedCapabilityCount} installed, ${inventory.mapCapabilityCount} mapped)` : "not refreshed"}`,
|
|
9399
|
+
`Review gate: ${validation.valid ? "READY FOR HUMAN APPROVAL" : "BLOCKED"}`
|
|
9400
|
+
];
|
|
9401
|
+
if (validation.blockers.length > 0) {
|
|
9402
|
+
lines.push("Blockers:", ...validation.blockers.map((issue) => ` - [${oneLine(issue.code)}] ${oneLine(issue.message)}`));
|
|
9403
|
+
}
|
|
9404
|
+
if (validation.warnings.length > 0) {
|
|
9405
|
+
lines.push("Warnings:", ...validation.warnings.map((issue) => ` - [${oneLine(issue.code)}] ${oneLine(issue.message)}`));
|
|
9406
|
+
}
|
|
9407
|
+
lines.push("Sources:");
|
|
9408
|
+
for (const source of study.sources) {
|
|
9409
|
+
const publicLink = source.accessClass === "public" && source.publicLocator ? ` \u2014 ${oneLine(source.publicLocator)}` : "";
|
|
9410
|
+
lines.push(` - ${oneLine(source.sourceId)}: ${oneLine(source.title)}${publicLink}`);
|
|
9411
|
+
if (source.suspiciousInstructionFlags.length > 0) {
|
|
9412
|
+
lines.push(` untrusted-instruction flags: ${source.suspiciousInstructionFlags.map(oneLine).join(", ")}`);
|
|
9413
|
+
}
|
|
9414
|
+
}
|
|
9415
|
+
lines.push("Insights:");
|
|
9416
|
+
for (const insight2 of study.insights) {
|
|
9417
|
+
lines.push(
|
|
9418
|
+
` - ${oneLine(insight2.insightId)} [${oneLine(insight2.validation)}/${oneLine(insight2.confidence)}] ${oneLine(insight2.claim)}`
|
|
9419
|
+
);
|
|
9420
|
+
}
|
|
9421
|
+
lines.push("Domain plans:");
|
|
9422
|
+
for (const plan of study.domainPlans) lines.push(renderDomainPlan(plan));
|
|
9423
|
+
lines.push(
|
|
9424
|
+
"",
|
|
9425
|
+
"Approval is human-only. An agent must stop here and ask the user to run `skill-forge insight approve` in an interactive terminal."
|
|
9426
|
+
);
|
|
9427
|
+
return lines.join("\n");
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
// src/commands/insight.ts
|
|
9431
|
+
var PRO_FEATURE_LABEL8 = "source insight planning";
|
|
9432
|
+
var WORKFLOW_ASSET = "workflows/source-insight-planning/SKILL.md";
|
|
9433
|
+
var CONTEXT_FILENAME = "command-context.json";
|
|
9434
|
+
var INVENTORY_FILENAME = "inventory.json";
|
|
9435
|
+
var JIRA_MANIFEST_FILENAME = "jira-manifest.json";
|
|
9436
|
+
function requireProFeature2() {
|
|
9437
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL8);
|
|
9438
|
+
if (!status2.enabled) {
|
|
9439
|
+
throw new UserError(
|
|
9440
|
+
status2.notice ?? "insight is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock."
|
|
9441
|
+
);
|
|
9442
|
+
}
|
|
9443
|
+
if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
|
|
9444
|
+
}
|
|
9445
|
+
function isRecord5(value) {
|
|
9446
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9447
|
+
}
|
|
9448
|
+
function nonEmptyString(value, label2) {
|
|
9449
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
9450
|
+
throw new UserError(`${label2} must be a non-empty string`);
|
|
9451
|
+
}
|
|
9452
|
+
return value;
|
|
9453
|
+
}
|
|
9454
|
+
function parseJsonFile(path, label2) {
|
|
9455
|
+
const resolved = resolvePath10(path);
|
|
9456
|
+
let raw;
|
|
9457
|
+
try {
|
|
9458
|
+
raw = readFileSync22(resolved, "utf-8");
|
|
9459
|
+
} catch (error) {
|
|
9460
|
+
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
9461
|
+
}
|
|
9462
|
+
try {
|
|
9463
|
+
return JSON.parse(raw);
|
|
9464
|
+
} catch {
|
|
9465
|
+
throw new UserError(`${label2} at ${resolved} is not valid JSON`);
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
var COMMON_DESCRIPTOR_FIELDS = /* @__PURE__ */ new Set([
|
|
9469
|
+
"type",
|
|
9470
|
+
"title",
|
|
9471
|
+
"accessClass",
|
|
9472
|
+
"licenseName",
|
|
9473
|
+
"licenseClass",
|
|
9474
|
+
"author",
|
|
9475
|
+
"publisher",
|
|
9476
|
+
"publicationDate",
|
|
9477
|
+
"revisionDate",
|
|
9478
|
+
"revision",
|
|
9479
|
+
"etag",
|
|
9480
|
+
"mediaType"
|
|
9481
|
+
]);
|
|
9482
|
+
var COMMON_STRING_DESCRIPTOR_FIELDS = [
|
|
9483
|
+
"title",
|
|
9484
|
+
"licenseName",
|
|
9485
|
+
"author",
|
|
9486
|
+
"publisher",
|
|
9487
|
+
"publicationDate",
|
|
9488
|
+
"revisionDate",
|
|
9489
|
+
"revision",
|
|
9490
|
+
"etag",
|
|
9491
|
+
"mediaType"
|
|
9492
|
+
];
|
|
9493
|
+
function assertDescriptorKeys(value, specific, label2) {
|
|
9494
|
+
const allowed = /* @__PURE__ */ new Set([...COMMON_DESCRIPTOR_FIELDS, ...specific]);
|
|
9495
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
9496
|
+
if (extra.length > 0) throw new UserError(`${label2} contains unsupported field(s): ${extra.join(", ")}`);
|
|
9497
|
+
}
|
|
9498
|
+
function assertCommonDescriptorFields(value, label2) {
|
|
9499
|
+
for (const field of COMMON_STRING_DESCRIPTOR_FIELDS) {
|
|
9500
|
+
if (value[field] !== void 0 && typeof value[field] !== "string") {
|
|
9501
|
+
throw new UserError(`${label2}.${field} must be a string`);
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
if (value.accessClass !== void 0 && !SOURCE_ACCESS_CLASSES.includes(value.accessClass)) {
|
|
9505
|
+
throw new UserError(`${label2}.accessClass is unsupported`);
|
|
9506
|
+
}
|
|
9507
|
+
if (value.licenseClass !== void 0 && !SOURCE_LICENSE_CLASSES.includes(value.licenseClass)) {
|
|
9508
|
+
throw new UserError(`${label2}.licenseClass is unsupported`);
|
|
9509
|
+
}
|
|
9510
|
+
}
|
|
9511
|
+
function pathRelativeTo(baseDir, raw, label2) {
|
|
9512
|
+
const path = nonEmptyString(raw, label2);
|
|
9513
|
+
return isAbsolute6(path) ? path : resolvePath10(baseDir, path);
|
|
9514
|
+
}
|
|
9515
|
+
function parseManifestDescriptor(value, index, baseDir) {
|
|
9516
|
+
const label2 = `source manifest entry ${index + 1}`;
|
|
9517
|
+
if (!isRecord5(value)) throw new UserError(`${label2} must be an object`);
|
|
9518
|
+
const type = nonEmptyString(value.type, `${label2}.type`);
|
|
9519
|
+
assertCommonDescriptorFields(value, label2);
|
|
9520
|
+
if (type === "url") {
|
|
9521
|
+
assertDescriptorKeys(value, ["url", "observedSha256", "observedByteLength"], label2);
|
|
9522
|
+
return { ...value, type, url: nonEmptyString(value.url, `${label2}.url`) };
|
|
9523
|
+
}
|
|
9524
|
+
if (type === "file" || type === "directory") {
|
|
9525
|
+
assertDescriptorKeys(value, type === "directory" ? ["path", "includeHidden"] : ["path"], label2);
|
|
9526
|
+
if (value.includeHidden !== void 0 && typeof value.includeHidden !== "boolean") {
|
|
9527
|
+
throw new UserError(`${label2}.includeHidden must be a boolean`);
|
|
9528
|
+
}
|
|
9529
|
+
return { ...value, type, path: pathRelativeTo(baseDir, value.path, `${label2}.path`) };
|
|
9530
|
+
}
|
|
9531
|
+
if (type === "connector") {
|
|
9532
|
+
assertDescriptorKeys(value, ["provider", "opaqueId", "publicLocator", "observedSha256", "observedByteLength"], label2);
|
|
9533
|
+
if (value.publicLocator !== void 0 && typeof value.publicLocator !== "string") {
|
|
9534
|
+
throw new UserError(`${label2}.publicLocator must be a string`);
|
|
9535
|
+
}
|
|
9536
|
+
return {
|
|
9537
|
+
...value,
|
|
9538
|
+
type,
|
|
9539
|
+
provider: nonEmptyString(value.provider, `${label2}.provider`),
|
|
9540
|
+
opaqueId: nonEmptyString(value.opaqueId, `${label2}.opaqueId`)
|
|
9541
|
+
};
|
|
9542
|
+
}
|
|
9543
|
+
if (type === "stdin") {
|
|
9544
|
+
assertDescriptorKeys(value, ["content"], label2);
|
|
9545
|
+
if (typeof value.content !== "string") throw new UserError(`${label2}.content must be a string`);
|
|
9546
|
+
return { ...value, type, content: value.content };
|
|
9547
|
+
}
|
|
9548
|
+
throw new UserError(`${label2}.type must be url|file|directory|connector|stdin`);
|
|
9549
|
+
}
|
|
9550
|
+
function readManifest2(path) {
|
|
9551
|
+
const resolved = resolvePath10(path);
|
|
9552
|
+
const parsed = parseJsonFile(resolved, "source manifest");
|
|
9553
|
+
let values;
|
|
9554
|
+
if (Array.isArray(parsed)) {
|
|
9555
|
+
values = parsed;
|
|
9556
|
+
} else if (isRecord5(parsed)) {
|
|
9557
|
+
const extra = Object.keys(parsed).filter((key) => key !== "sources");
|
|
9558
|
+
if (extra.length > 0) throw new UserError(`source manifest contains unsupported field(s): ${extra.join(", ")}`);
|
|
9559
|
+
values = parsed.sources;
|
|
9560
|
+
}
|
|
9561
|
+
if (!Array.isArray(values)) throw new UserError(`source manifest must be an array or an object with a sources array`);
|
|
9562
|
+
return values.map((value, index) => parseManifestDescriptor(value, index, dirname13(resolved)));
|
|
9563
|
+
}
|
|
9564
|
+
function readBoundedStdin(descriptor = 0, maxFileBytes = DEFAULT_MAX_SOURCE_FILE_BYTES) {
|
|
9565
|
+
const chunks = [];
|
|
9566
|
+
let total = 0;
|
|
9567
|
+
while (true) {
|
|
9568
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxFileBytes + 1 - total));
|
|
9569
|
+
const count = readSync3(descriptor, chunk, 0, chunk.byteLength, null);
|
|
9570
|
+
if (count === 0) return Buffer.concat(chunks, total);
|
|
9571
|
+
total += count;
|
|
9572
|
+
if (total > maxFileBytes) {
|
|
9573
|
+
throw new UserError(`stdin source exceeds the per-source byte limit (${maxFileBytes})`);
|
|
9574
|
+
}
|
|
9575
|
+
chunks.push(chunk.subarray(0, count));
|
|
9576
|
+
}
|
|
9577
|
+
}
|
|
9578
|
+
function positionalSource(value) {
|
|
9579
|
+
if (/^https?:\/\//iu.test(value)) return { type: "url", url: value };
|
|
9580
|
+
if (value === "-") {
|
|
9581
|
+
if (process.stdin.isTTY) throw new UserError(`stdin source "-" requires piped input`);
|
|
9582
|
+
return { type: "stdin", content: readBoundedStdin() };
|
|
9583
|
+
}
|
|
9584
|
+
const resolved = resolvePath10(value);
|
|
9585
|
+
let stat;
|
|
9586
|
+
try {
|
|
9587
|
+
stat = lstatSync6(resolved);
|
|
9588
|
+
} catch (error) {
|
|
9589
|
+
throw new UserError(`unable to read source ${resolved}: ${error.message}`);
|
|
9590
|
+
}
|
|
9591
|
+
if (stat.isSymbolicLink()) throw new UserError(`source path must not be a symlink: ${resolved}`);
|
|
9592
|
+
if (stat.isFile()) return { type: "file", path: resolved };
|
|
9593
|
+
if (stat.isDirectory()) return { type: "directory", path: resolved };
|
|
9594
|
+
throw new UserError(`source must be a regular file or directory: ${resolved}`);
|
|
9595
|
+
}
|
|
9596
|
+
function collectSourceInputs(sourceArgs, options) {
|
|
9597
|
+
const inputs = sourceArgs.map(positionalSource);
|
|
9598
|
+
if (options.manifest) inputs.push(...readManifest2(options.manifest));
|
|
9599
|
+
if (inputs.length === 0) throw new UserError(`insight analyze requires at least one source or --manifest`);
|
|
9600
|
+
return inputs;
|
|
9601
|
+
}
|
|
9602
|
+
function contextPath(studyId) {
|
|
9603
|
+
return join32(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
9604
|
+
}
|
|
9605
|
+
function inventoryPath(studyId) {
|
|
9606
|
+
return join32(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
9607
|
+
}
|
|
9608
|
+
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
9609
|
+
const context = {
|
|
9610
|
+
schemaVersion: 1,
|
|
9611
|
+
skillMapPath: skillMapPath ? resolvePath10(skillMapPath) : void 0,
|
|
9612
|
+
skillMapCoverage
|
|
9613
|
+
};
|
|
9614
|
+
writeJsonAtomic(contextPath(studyId), context);
|
|
9615
|
+
writeJsonAtomic(inventoryPath(studyId), inventory);
|
|
9616
|
+
}
|
|
9617
|
+
function readCommandContext(studyId) {
|
|
9618
|
+
const path = contextPath(studyId);
|
|
9619
|
+
const parsed = parseJsonFile(path, "insight command context");
|
|
9620
|
+
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !isAbsolute6(parsed.skillMapPath)) || parsed.skillMapCoverage !== void 0 && parsed.skillMapCoverage !== "static" && parsed.skillMapCoverage !== "resolved" || parsed.skillMapCoverage !== void 0 && parsed.skillMapPath === void 0 || Object.keys(parsed).some((key) => !["schemaVersion", "skillMapPath", "skillMapCoverage"].includes(key))) {
|
|
9621
|
+
throw new UserError(`malformed insight command context at ${path}`);
|
|
9622
|
+
}
|
|
9623
|
+
return parsed;
|
|
9624
|
+
}
|
|
9625
|
+
function currentInventory(study, config) {
|
|
9626
|
+
const storedPath = inventoryPath(study.studyId);
|
|
9627
|
+
const stored = parseJsonFile(storedPath, "insight inventory snapshot");
|
|
9628
|
+
if (!isRecord5(stored) || typeof stored.capabilityInventoryHash !== "string" || stored.capabilityInventoryHash !== study.capabilityInventoryHash) {
|
|
9629
|
+
throw new UserError(`insight inventory snapshot at ${storedPath} does not match study ${study.studyId}`);
|
|
9630
|
+
}
|
|
9631
|
+
const context = readCommandContext(study.studyId);
|
|
9632
|
+
return buildInsightInventory(config, {
|
|
9633
|
+
skillMapPath: context.skillMapPath,
|
|
9634
|
+
skillMapCoverage: context.skillMapCoverage
|
|
9635
|
+
});
|
|
9636
|
+
}
|
|
9637
|
+
function readCurrentStudy(studyId) {
|
|
9638
|
+
const study = readInsightStudy(studyId);
|
|
9639
|
+
const config = loadConfig();
|
|
9640
|
+
return { study, inventory: currentInventory(study, config), config };
|
|
9641
|
+
}
|
|
9642
|
+
function retainedPath(root, relativePath) {
|
|
9643
|
+
const candidate = resolvePath10(root, relativePath);
|
|
9644
|
+
const rel = relative5(root, candidate);
|
|
9645
|
+
if (rel.startsWith("..") || isAbsolute6(rel)) throw new UserError(`refusing retained source path outside the study`);
|
|
9646
|
+
return candidate;
|
|
9647
|
+
}
|
|
9648
|
+
function retainObservedSources(study, observedFiles) {
|
|
9649
|
+
if (observedFiles.length === 0) return study;
|
|
9650
|
+
const root = join32(getStudyDir(study.studyId), "retained-sources");
|
|
9651
|
+
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
9652
|
+
chmodSync3(root, 448);
|
|
9653
|
+
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
9654
|
+
for (const file of observedFiles) {
|
|
9655
|
+
const sourceRoot = join32(root, file.sourceId);
|
|
9656
|
+
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
9657
|
+
chmodSync3(sourceRoot, 448);
|
|
9658
|
+
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
9659
|
+
mkdirSync9(dirname13(destination), { recursive: true, mode: 448 });
|
|
9660
|
+
writeFileSync8(destination, file.bytes, { flag: "wx", mode: 384 });
|
|
9661
|
+
chmodSync3(destination, 384);
|
|
9662
|
+
retainedSourceIds.add(file.sourceId);
|
|
9663
|
+
}
|
|
9664
|
+
const sources = study.sources.map((source) => {
|
|
9665
|
+
if (!retainedSourceIds.has(source.sourceId)) return source;
|
|
9666
|
+
return {
|
|
9667
|
+
...source,
|
|
9668
|
+
retention: "retained"
|
|
9669
|
+
};
|
|
9670
|
+
});
|
|
9671
|
+
return writeInsightStudy({ ...study, sources });
|
|
9672
|
+
}
|
|
9673
|
+
function studySummary(study) {
|
|
9674
|
+
const validationCounts = Object.fromEntries(
|
|
9675
|
+
["corroborated", "contested", "single-source", "rejected", "unverifiable"].map((value) => [
|
|
9676
|
+
value,
|
|
9677
|
+
study.insights.filter((insight2) => insight2.validation === value).length
|
|
9678
|
+
])
|
|
9679
|
+
);
|
|
9680
|
+
return {
|
|
9681
|
+
studyId: study.studyId,
|
|
9682
|
+
status: study.status,
|
|
9683
|
+
requestedAnalysisMode: study.requestedAnalysisMode,
|
|
9684
|
+
actualAnalysisMode: study.actualAnalysisMode,
|
|
9685
|
+
sourceCount: study.sources.length,
|
|
9686
|
+
insightCount: study.insights.length,
|
|
9687
|
+
validationCounts,
|
|
9688
|
+
domainCount: study.domainPlans.length,
|
|
9689
|
+
planHash: study.planHash,
|
|
9690
|
+
approval: study.approval ? { approvedDomainIds: study.approval.approvedDomainIds, planHash: study.approval.planHash, approvedAt: study.approval.approvedAt } : void 0,
|
|
9691
|
+
jira: study.jira,
|
|
9692
|
+
closedAt: study.closedAt,
|
|
9693
|
+
closeReason: study.closeReason
|
|
9694
|
+
};
|
|
9695
|
+
}
|
|
9696
|
+
function emit(json, payload, text) {
|
|
9697
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
9698
|
+
else text();
|
|
9699
|
+
}
|
|
9700
|
+
function reviewDomainPlan(plan) {
|
|
9701
|
+
return {
|
|
9702
|
+
domainId: plan.domainId,
|
|
9703
|
+
domain: plan.domain,
|
|
9704
|
+
outcome: plan.outcome,
|
|
9705
|
+
acceptedInsightIds: plan.acceptedInsightIds,
|
|
9706
|
+
contestedInsightIds: plan.contestedInsightIds,
|
|
9707
|
+
rejectedInsightIds: plan.rejectedInsightIds,
|
|
9708
|
+
deduplicationEvidence: plan.deduplicationEvidence,
|
|
9709
|
+
selectedVerb: plan.selectedVerb,
|
|
9710
|
+
proposedChanges: plan.proposedChanges,
|
|
9711
|
+
provenanceObligations: plan.provenanceObligations,
|
|
9712
|
+
licenseObligations: plan.licenseObligations,
|
|
9713
|
+
privacyObligations: plan.privacyObligations,
|
|
9714
|
+
securityObligations: plan.securityObligations,
|
|
9715
|
+
dependencies: plan.dependencies,
|
|
9716
|
+
acceptanceTests: plan.acceptanceTests,
|
|
9717
|
+
measurement: plan.measurement,
|
|
9718
|
+
rollbackMechanism: plan.rollbackMechanism,
|
|
9719
|
+
rollbackTrigger: plan.rollbackTrigger,
|
|
9720
|
+
workItems: plan.workItems,
|
|
9721
|
+
nonGoals: plan.nonGoals
|
|
9722
|
+
};
|
|
9723
|
+
}
|
|
9724
|
+
function runStudyHandoff(config, studyId, instruction, json) {
|
|
9725
|
+
runHandoff(config, getStudyDir(studyId), {
|
|
9726
|
+
promptAsset: WORKFLOW_ASSET,
|
|
9727
|
+
instruction,
|
|
9728
|
+
targetLabel: "Study path",
|
|
9729
|
+
log: json ? console.error : console.log
|
|
9730
|
+
});
|
|
9731
|
+
}
|
|
9732
|
+
async function insightAnalyze(sourceArgs = [], options = {}) {
|
|
9733
|
+
requireProFeature2();
|
|
9734
|
+
const config = loadConfig();
|
|
9735
|
+
const inputs = collectSourceInputs(sourceArgs, options);
|
|
9736
|
+
const skillMapPath = options.skillMap ? resolvePath10(options.skillMap) : void 0;
|
|
9737
|
+
if (options.skillMapCoverage !== void 0 && skillMapPath === void 0) {
|
|
9738
|
+
throw new UserError("--skill-map-coverage requires --skill-map");
|
|
9739
|
+
}
|
|
9740
|
+
const inventory = buildInsightInventory(config, {
|
|
9741
|
+
skillMapPath,
|
|
9742
|
+
skillMapCoverage: options.skillMapCoverage
|
|
9743
|
+
});
|
|
9744
|
+
const normalized = normalizeSources(inputs);
|
|
9745
|
+
let study = createInsightStudy({
|
|
9746
|
+
sources: normalized.sources,
|
|
9747
|
+
sourceInventoryHash: normalized.sourceInventoryHash,
|
|
9748
|
+
requestedAnalysisMode: options.analysisMode ?? "independent"
|
|
9749
|
+
});
|
|
9750
|
+
study = writeInsightStudy({ ...study, capabilityInventoryHash: inventory.capabilityInventoryHash });
|
|
9751
|
+
writeCommandContext(study.studyId, skillMapPath, options.skillMapCoverage, inventory);
|
|
9752
|
+
if (options.retainSourceCopies) study = retainObservedSources(study, normalized.observedFiles);
|
|
9753
|
+
appendStudyEvent(study.studyId, {
|
|
9754
|
+
type: "study-created",
|
|
9755
|
+
detail: {
|
|
9756
|
+
sourceCount: study.sources.length,
|
|
9757
|
+
requestedAnalysisMode: study.requestedAnalysisMode,
|
|
9758
|
+
retainedLocalSources: options.retainSourceCopies === true,
|
|
9759
|
+
inventoryCoverage: inventory.coverage
|
|
9760
|
+
}
|
|
9761
|
+
});
|
|
9762
|
+
emit(options.json, { ...studySummary(study), inventory: {
|
|
9763
|
+
coverage: inventory.coverage,
|
|
9764
|
+
coverageReason: inventory.coverageReason,
|
|
9765
|
+
installedCapabilityCount: inventory.installedCapabilityCount,
|
|
9766
|
+
mapCapabilityCount: inventory.mapCapabilityCount
|
|
9767
|
+
} }, () => {
|
|
9768
|
+
console.log(`[skill-forge] created insight study ${study.studyId}`);
|
|
9769
|
+
console.log(` sources: ${study.sources.length}; inventory: ${inventory.coverage}`);
|
|
9770
|
+
console.log(` study: ${getStudyDir(study.studyId)}`);
|
|
9771
|
+
console.log(` next: skill-forge insight submit ${study.studyId} --file <analysis.json>`);
|
|
9772
|
+
});
|
|
9773
|
+
if (options.handoff) {
|
|
9774
|
+
runStudyHandoff(
|
|
9775
|
+
config,
|
|
9776
|
+
study.studyId,
|
|
9777
|
+
"Read {prompt}. Analyze the untrusted source study at {path}. Write candidate JSON outside the study and import it only through `skill-forge insight submit`. Never approve the study, call Jira, or mutate a capability.",
|
|
9778
|
+
options.json
|
|
9779
|
+
);
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9782
|
+
async function insightSubmit(studyId, options) {
|
|
9783
|
+
requireProFeature2();
|
|
9784
|
+
const { study, inventory } = readCurrentStudy(studyId);
|
|
9785
|
+
const sourceIssue = validateCurrentSourceFingerprints(study)[0];
|
|
9786
|
+
if (sourceIssue) throw new UserError(sourceIssue.message);
|
|
9787
|
+
const candidate = parseJsonFile(options.file, "insight analysis submission");
|
|
9788
|
+
const next = writeInsightStudy(validateInsightSubmission(study, candidate, inventory));
|
|
9789
|
+
appendStudyEvent(studyId, { type: "analysis-submitted", detail: { planHash: next.planHash } });
|
|
9790
|
+
emit(options.json, studySummary(next), () => {
|
|
9791
|
+
console.log(`[skill-forge] imported analysis for ${studyId}`);
|
|
9792
|
+
console.log(` plan hash: ${next.planHash}`);
|
|
9793
|
+
console.log(` next: skill-forge insight review ${studyId}`);
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
async function insightReview(studyId, options = {}) {
|
|
9797
|
+
requireProFeature2();
|
|
9798
|
+
const { study, inventory } = readCurrentStudy(studyId);
|
|
9799
|
+
const validation = validateStudyForReview(study, inventory);
|
|
9800
|
+
if (options.json) {
|
|
9801
|
+
const payload = {
|
|
9802
|
+
...studySummary(study),
|
|
9803
|
+
validation,
|
|
9804
|
+
inventory: {
|
|
9805
|
+
coverage: inventory.coverage,
|
|
9806
|
+
coverageReason: inventory.coverageReason,
|
|
9807
|
+
installedCapabilityCount: inventory.installedCapabilityCount,
|
|
9808
|
+
mapCapabilityCount: inventory.mapCapabilityCount
|
|
9809
|
+
},
|
|
9810
|
+
domainPlans: study.domainPlans.map(reviewDomainPlan)
|
|
9811
|
+
};
|
|
9812
|
+
assertExportSafe(payload, study, "insight review JSON");
|
|
9813
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
9814
|
+
return;
|
|
9815
|
+
}
|
|
9816
|
+
console.log(renderInsightReview(study, inventory, validation));
|
|
9817
|
+
}
|
|
9818
|
+
async function insightApprove(studyId, options = {}) {
|
|
9819
|
+
requireProFeature2();
|
|
9820
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
9821
|
+
throw new UserError(
|
|
9822
|
+
"insight approve is human-only and requires an interactive terminal; there is deliberately no --yes option"
|
|
9823
|
+
);
|
|
9824
|
+
}
|
|
9825
|
+
const { study, inventory } = readCurrentStudy(studyId);
|
|
9826
|
+
if (study.status !== "review-ready") {
|
|
9827
|
+
throw new UserError(`study ${studyId} is ${study.status} and cannot be approved`);
|
|
9828
|
+
}
|
|
9829
|
+
const validation = validateStudyForReview(study, inventory);
|
|
9830
|
+
if (!validation.valid) {
|
|
9831
|
+
throw new UserError(`study ${studyId} is not ready for approval: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
9832
|
+
}
|
|
9833
|
+
if (!study.capabilityInventoryHash) throw new UserError(`study ${studyId} has no capability inventory hash`);
|
|
9834
|
+
const knownDomains = new Set(study.domainPlans.map((plan) => plan.domainId));
|
|
9835
|
+
const requested = options.domain ?? [];
|
|
9836
|
+
const approvedDomainIds = requested.length > 0 ? [...new Set(requested)] : [...knownDomains];
|
|
9837
|
+
const unknown = approvedDomainIds.filter((id) => !knownDomains.has(id));
|
|
9838
|
+
if (unknown.length > 0) throw new UserError(`unknown domain id(s): ${unknown.join(", ")}`);
|
|
9839
|
+
if (approvedDomainIds.length === 0) throw new UserError(`study ${studyId} has no domain plans to approve`);
|
|
9840
|
+
console.log(renderInsightReview(study, inventory, validation));
|
|
9841
|
+
console.log("\nSelected domain plans:");
|
|
9842
|
+
for (const plan of study.domainPlans.filter((candidate) => approvedDomainIds.includes(candidate.domainId))) {
|
|
9843
|
+
console.log(renderDomainPlan(plan));
|
|
9844
|
+
}
|
|
9845
|
+
console.log(`
|
|
9846
|
+
Exact plan hash: ${validation.planHash}`);
|
|
9847
|
+
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
9848
|
+
let confirmed = false;
|
|
9849
|
+
try {
|
|
9850
|
+
const answer = (await rl.question("Approve this exact plan hash and selected domains? [y/N] ")).trim().toLowerCase();
|
|
9851
|
+
confirmed = answer === "y" || answer === "yes";
|
|
9852
|
+
} finally {
|
|
9853
|
+
rl.close();
|
|
9854
|
+
}
|
|
9855
|
+
if (!confirmed) {
|
|
9856
|
+
console.log(`[skill-forge] study ${studyId} was not approved.`);
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9859
|
+
const approvedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9860
|
+
const approval = {
|
|
9861
|
+
approvedDomainIds,
|
|
9862
|
+
planHash: validation.planHash,
|
|
9863
|
+
approvedAt,
|
|
9864
|
+
sourceInventoryHash: study.sourceInventoryHash,
|
|
9865
|
+
capabilityInventoryHash: study.capabilityInventoryHash,
|
|
9866
|
+
acceptedExceptions: validation.warnings.map((issue) => `${issue.code}: ${issue.message}`)
|
|
9867
|
+
};
|
|
9868
|
+
const next = writeInsightStudy({ ...study, status: "approved", planHash: validation.planHash, approval, jira: void 0 });
|
|
9869
|
+
appendStudyEvent(studyId, {
|
|
9870
|
+
type: "plan-approved",
|
|
9871
|
+
occurredAt: approvedAt,
|
|
9872
|
+
detail: { planHash: validation.planHash, approvedDomainIds }
|
|
9873
|
+
});
|
|
9874
|
+
console.log(`[skill-forge] approved ${approvedDomainIds.length} domain plan(s) for ${next.studyId}`);
|
|
9875
|
+
console.log(` plan hash: ${validation.planHash}`);
|
|
9876
|
+
}
|
|
9877
|
+
async function insightJira(studyId, options) {
|
|
9878
|
+
requireProFeature2();
|
|
9879
|
+
const localMode = options.receipt !== void 0 || options.resume === true;
|
|
9880
|
+
if (options.receipt !== void 0 && options.resume === true) {
|
|
9881
|
+
throw new UserError("--receipt and --resume are mutually exclusive");
|
|
9882
|
+
}
|
|
9883
|
+
if (localMode && (options.project !== void 0 || options.out !== void 0 || options.handoff === true)) {
|
|
9884
|
+
throw new UserError("--receipt and --resume cannot be combined with --project, --out, or --handoff");
|
|
9885
|
+
}
|
|
9886
|
+
const projectKey = localMode ? void 0 : nonEmptyString(options.project, "--project").trim();
|
|
9887
|
+
const { study, inventory, config } = readCurrentStudy(studyId);
|
|
9888
|
+
const validation = validateStudyForReview(study, inventory);
|
|
9889
|
+
if (!validation.valid) {
|
|
9890
|
+
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
9891
|
+
}
|
|
9892
|
+
if (localMode) {
|
|
9893
|
+
const privatePath2 = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9894
|
+
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
9895
|
+
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
9896
|
+
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
9897
|
+
emit(options.json, receipt ? { receipt, resume } : resume, () => {
|
|
9898
|
+
if (receipt) console.log(`[skill-forge] recorded Jira receipt ${receipt.eventId} for ${studyId}`);
|
|
9899
|
+
console.log(`[skill-forge] Jira resume state for ${studyId}: ${resume.nextAction}`);
|
|
9900
|
+
console.log(` complete: ${resume.complete ? "yes" : "no"}`);
|
|
9901
|
+
});
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
const manifest = buildJiraManifest(study, projectKey);
|
|
9905
|
+
const privatePath = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9906
|
+
writeJsonAtomic(privatePath, manifest);
|
|
9907
|
+
if (options.out) {
|
|
9908
|
+
writeNewFile(resolvePath10(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
9909
|
+
`, "--out");
|
|
9910
|
+
}
|
|
9911
|
+
const generatedAt = manifest.generatedAt;
|
|
9912
|
+
const manifestHash = stableHash(manifest);
|
|
9913
|
+
const next = writeInsightStudy({
|
|
9914
|
+
...study,
|
|
9915
|
+
status: "jira-manifested",
|
|
9916
|
+
jira: { projectKey: manifest.projectKey, manifestHash, generatedAt }
|
|
9917
|
+
});
|
|
9918
|
+
appendStudyEvent(studyId, {
|
|
9919
|
+
type: "jira-manifest-generated",
|
|
9920
|
+
occurredAt: generatedAt,
|
|
9921
|
+
detail: { projectKey: manifest.projectKey, manifestHash, workItemCount: manifest.workItems.length }
|
|
9922
|
+
});
|
|
9923
|
+
emit(options.json, manifest, () => {
|
|
9924
|
+
console.log(`[skill-forge] wrote Jira manifest for ${next.studyId}`);
|
|
9925
|
+
console.log(` manifest: ${privatePath}`);
|
|
9926
|
+
if (options.out) console.log(` export: ${resolvePath10(options.out)}`);
|
|
9927
|
+
console.log(" no Jira issues were created.");
|
|
9928
|
+
});
|
|
9929
|
+
if (options.handoff) {
|
|
9930
|
+
runStudyHandoff(
|
|
9931
|
+
config,
|
|
9932
|
+
studyId,
|
|
9933
|
+
"Read {prompt}. Present the approved Jira manifest in the private study at {path}, resolve live project metadata and every idempotency marker, then obtain literal human confirmation before any Jira write. Never approve the plan or mutate a capability.",
|
|
9934
|
+
options.json
|
|
9935
|
+
);
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
async function insightStatus(studyId, options = {}) {
|
|
9939
|
+
requireProFeature2();
|
|
9940
|
+
const study = readInsightStudy(studyId);
|
|
9941
|
+
const summary = studySummary(study);
|
|
9942
|
+
emit(options.json, summary, () => {
|
|
9943
|
+
console.log(`[skill-forge] insight study ${sanitizeRenderedText(studyId)}`);
|
|
9944
|
+
console.log(` status: ${study.status}`);
|
|
9945
|
+
console.log(` sources: ${study.sources.length}`);
|
|
9946
|
+
console.log(` insights: ${study.insights.length}`);
|
|
9947
|
+
console.log(` domains: ${study.domainPlans.length}`);
|
|
9948
|
+
console.log(` plan: ${study.planHash ?? "not submitted"}`);
|
|
9949
|
+
console.log(` approval: ${study.approval ? `${study.approval.approvedAt} (${study.approval.approvedDomainIds.length} domain(s))` : "none"}`);
|
|
9950
|
+
console.log(` Jira: ${study.jira ? `manifest for ${sanitizeRenderedText(study.jira.projectKey)}; issues not asserted` : "no manifest"}`);
|
|
9951
|
+
});
|
|
9952
|
+
}
|
|
9953
|
+
async function insightClose(studyId, options) {
|
|
9954
|
+
requireProFeature2();
|
|
9955
|
+
if (options.status !== "dismissed" && options.status !== "archived") {
|
|
9956
|
+
throw new UserError(`--status must be dismissed or archived`);
|
|
9957
|
+
}
|
|
9958
|
+
const study = readInsightStudy(studyId);
|
|
9959
|
+
if (study.status === "dismissed" || study.status === "archived") {
|
|
9960
|
+
throw new UserError(`study ${studyId} is already ${study.status}`);
|
|
9961
|
+
}
|
|
9962
|
+
const closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9963
|
+
const next = writeInsightStudy({
|
|
9964
|
+
...study,
|
|
9965
|
+
status: options.status,
|
|
9966
|
+
approval: void 0,
|
|
9967
|
+
jira: void 0,
|
|
9968
|
+
closedAt,
|
|
9969
|
+
closeReason: `closed by insight close --status ${options.status}`
|
|
9970
|
+
});
|
|
9971
|
+
appendStudyEvent(studyId, { type: "study-closed", occurredAt: closedAt, detail: { status: options.status } });
|
|
9972
|
+
console.log(`[skill-forge] ${next.studyId} closed as ${next.status}; append-only receipts were preserved.`);
|
|
9973
|
+
}
|
|
9974
|
+
|
|
9975
|
+
// src/commands/refine.ts
|
|
9976
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
9977
|
+
import { chmodSync as chmodSync4, existsSync as existsSync29, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync24, realpathSync as realpathSync10, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
9978
|
+
import { dirname as dirname15, isAbsolute as isAbsolute8, join as join35, relative as relative7, resolve as resolvePath13, sep as sep6 } from "path";
|
|
9979
|
+
import { createInterface as createInterface7 } from "readline/promises";
|
|
9980
|
+
|
|
9981
|
+
// src/refine/context.ts
|
|
9982
|
+
import { existsSync as existsSync28, realpathSync as realpathSync9, statSync as statSync13 } from "fs";
|
|
9983
|
+
import { isAbsolute as isAbsolute7, join as join34, relative as relative6, resolve as resolvePath12 } from "path";
|
|
9984
|
+
|
|
9985
|
+
// src/refine/models.ts
|
|
9986
|
+
import { randomBytes } from "crypto";
|
|
9987
|
+
var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
|
|
9988
|
+
var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
|
|
9989
|
+
var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
|
|
9990
|
+
var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
|
|
9991
|
+
function isRefinementCategory(value) {
|
|
9992
|
+
return REFINEMENT_CATEGORIES.includes(value);
|
|
9993
|
+
}
|
|
9994
|
+
function isOverrideType(value) {
|
|
9995
|
+
return OVERRIDE_TYPES.includes(value);
|
|
9996
|
+
}
|
|
9997
|
+
function isPatchAction(value) {
|
|
9998
|
+
return PATCH_ACTIONS.includes(value);
|
|
9999
|
+
}
|
|
10000
|
+
function isPatternStatus(value) {
|
|
10001
|
+
return PATTERN_STATUSES.includes(value);
|
|
10002
|
+
}
|
|
10003
|
+
var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
|
|
10004
|
+
function isRefinementStatus(value) {
|
|
10005
|
+
return REFINEMENT_STATUSES.includes(value);
|
|
10006
|
+
}
|
|
10007
|
+
function patternCount(pattern) {
|
|
10008
|
+
return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
|
|
10009
|
+
}
|
|
10010
|
+
function defaultRandomHex() {
|
|
10011
|
+
return randomBytes(2).toString("hex");
|
|
10012
|
+
}
|
|
10013
|
+
function formatYYYYMMDD(date) {
|
|
10014
|
+
const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
|
|
10015
|
+
const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
10016
|
+
const dd = date.getUTCDate().toString().padStart(2, "0");
|
|
10017
|
+
return `${yyyy}${mm}${dd}`;
|
|
10018
|
+
}
|
|
10019
|
+
function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
|
|
10020
|
+
const datePart = formatYYYYMMDD(now);
|
|
10021
|
+
let id;
|
|
10022
|
+
do {
|
|
10023
|
+
id = `REF-${datePart}-${randomHex()}`;
|
|
10024
|
+
} while (existingIds.has(id));
|
|
10025
|
+
return id;
|
|
10026
|
+
}
|
|
10027
|
+
function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
|
|
10028
|
+
let id;
|
|
10029
|
+
do {
|
|
10030
|
+
id = `PAT-${randomHex()}`;
|
|
10031
|
+
} while (existingIds.has(id));
|
|
10032
|
+
return id;
|
|
10033
|
+
}
|
|
10034
|
+
function fingerprintKey(skill, target, overrideType, action) {
|
|
10035
|
+
const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
10036
|
+
return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
|
|
10037
|
+
}
|
|
10038
|
+
function contentSimilarity(a, b) {
|
|
10039
|
+
const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
|
|
10040
|
+
const tokensA = tokenize3(a);
|
|
10041
|
+
const tokensB = tokenize3(b);
|
|
10042
|
+
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
10043
|
+
let intersectionSize = 0;
|
|
10044
|
+
for (const token of tokensA) {
|
|
10045
|
+
if (tokensB.has(token)) intersectionSize++;
|
|
10046
|
+
}
|
|
10047
|
+
const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
|
|
10048
|
+
return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
|
|
10049
|
+
}
|
|
10050
|
+
|
|
10051
|
+
// src/refine/store.ts
|
|
10052
|
+
import { createHash as createHash7 } from "crypto";
|
|
10053
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23, statSync as statSync12, realpathSync as realpathSync8 } from "fs";
|
|
10054
|
+
import { join as join33, resolve as resolvePath11 } from "path";
|
|
10055
|
+
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
10056
|
+
var READY_THRESHOLD = 2;
|
|
10057
|
+
function getRefinementsDir() {
|
|
10058
|
+
return join33(getHomeDir(), "refinements");
|
|
10059
|
+
}
|
|
10060
|
+
function getRefinementsPath() {
|
|
10061
|
+
return join33(getRefinementsDir(), "refinements.json");
|
|
10062
|
+
}
|
|
10063
|
+
function getPatternsPath() {
|
|
10064
|
+
return join33(getRefinementsDir(), "patterns.json");
|
|
10065
|
+
}
|
|
10066
|
+
function getBackupsDir() {
|
|
10067
|
+
return join33(getRefinementsDir(), "backups");
|
|
10068
|
+
}
|
|
10069
|
+
function readJsonFile(path, label2) {
|
|
10070
|
+
let raw;
|
|
10071
|
+
try {
|
|
10072
|
+
raw = readFileSync23(path, "utf-8");
|
|
10073
|
+
} catch (err) {
|
|
10074
|
+
throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
|
|
10075
|
+
}
|
|
10076
|
+
try {
|
|
10077
|
+
return JSON.parse(raw);
|
|
10078
|
+
} catch {
|
|
10079
|
+
throw new UserError(`malformed ${label2} at ${path}: not valid JSON`);
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
function readStore(path, label2, validateRecord) {
|
|
10083
|
+
if (!existsSync27(path)) {
|
|
10084
|
+
return { schemaVersion: 1, records: [] };
|
|
10085
|
+
}
|
|
10086
|
+
const parsed = readJsonFile(path, label2);
|
|
10087
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
10088
|
+
throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
|
|
10089
|
+
}
|
|
10090
|
+
const records = parsed.records;
|
|
10091
|
+
if (validateRecord) {
|
|
10092
|
+
records.forEach((record, index) => validateRecord(record, index));
|
|
10093
|
+
}
|
|
10094
|
+
return { schemaVersion: 1, records };
|
|
10095
|
+
}
|
|
10096
|
+
function validateRefinementRecordShape(record, index, path) {
|
|
10097
|
+
if (typeof record !== "object" || record === null) {
|
|
10098
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
10099
|
+
}
|
|
10100
|
+
const r = record;
|
|
10101
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
10102
|
+
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
10103
|
+
}
|
|
10104
|
+
if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
|
|
10105
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
|
|
10106
|
+
}
|
|
10107
|
+
if (typeof r.status !== "string" || r.status.length === 0) {
|
|
10108
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
|
|
10109
|
+
}
|
|
10110
|
+
if (typeof r.skill !== "string" || r.skill.length === 0) {
|
|
10111
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
|
|
10112
|
+
}
|
|
10113
|
+
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
10114
|
+
if (r.overrideType === "patch") {
|
|
10115
|
+
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
10116
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
10117
|
+
}
|
|
10118
|
+
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
10119
|
+
throw new UserError(
|
|
10120
|
+
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
10121
|
+
);
|
|
10122
|
+
}
|
|
10123
|
+
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
10124
|
+
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
10125
|
+
}
|
|
10126
|
+
} else if (r.patchAction !== void 0) {
|
|
10127
|
+
throw new UserError(
|
|
10128
|
+
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
10129
|
+
);
|
|
10130
|
+
}
|
|
10131
|
+
}
|
|
10132
|
+
function validatePatternRecordShape(record, index, path) {
|
|
10133
|
+
if (typeof record !== "object" || record === null) {
|
|
10134
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
|
|
10135
|
+
}
|
|
10136
|
+
const p = record;
|
|
10137
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
10138
|
+
throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
10139
|
+
}
|
|
10140
|
+
if (typeof p.status !== "string" || p.status.length === 0) {
|
|
10141
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
|
|
10142
|
+
}
|
|
10143
|
+
if (!Array.isArray(p.occurrences)) {
|
|
10144
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
|
|
10145
|
+
}
|
|
10146
|
+
if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
|
|
10147
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
|
|
10148
|
+
}
|
|
10149
|
+
if (p.status === "generalized") {
|
|
10150
|
+
if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
|
|
10151
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
|
|
10152
|
+
}
|
|
10153
|
+
if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
|
|
10154
|
+
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
|
|
10155
|
+
}
|
|
10156
|
+
}
|
|
10157
|
+
}
|
|
10158
|
+
function readRefinementsStore() {
|
|
10159
|
+
const path = getRefinementsPath();
|
|
10160
|
+
return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
|
|
7364
10161
|
}
|
|
7365
10162
|
function writeRefinementsStore(store) {
|
|
7366
10163
|
writeJsonAtomic(getRefinementsPath(), store);
|
|
@@ -7373,9 +10170,9 @@ function writePatternsStore(store) {
|
|
|
7373
10170
|
writeJsonAtomic(getPatternsPath(), store);
|
|
7374
10171
|
}
|
|
7375
10172
|
function resolveProjectRealpath(projectRoot) {
|
|
7376
|
-
const resolved =
|
|
10173
|
+
const resolved = resolvePath11(projectRoot);
|
|
7377
10174
|
try {
|
|
7378
|
-
return
|
|
10175
|
+
return realpathSync8(resolved);
|
|
7379
10176
|
} catch {
|
|
7380
10177
|
return resolved;
|
|
7381
10178
|
}
|
|
@@ -7507,16 +10304,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
7507
10304
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
7508
10305
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
7509
10306
|
const files = filePaths.map((path) => {
|
|
7510
|
-
if (!
|
|
10307
|
+
if (!existsSync27(path)) {
|
|
7511
10308
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
7512
10309
|
}
|
|
7513
|
-
const bytes =
|
|
7514
|
-
const mode =
|
|
7515
|
-
const sha2562 =
|
|
10310
|
+
const bytes = readFileSync23(path);
|
|
10311
|
+
const mode = statSync12(path).mode & 511;
|
|
10312
|
+
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
7516
10313
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
7517
10314
|
});
|
|
7518
10315
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
7519
|
-
writeJsonAtomic(
|
|
10316
|
+
writeJsonAtomic(join33(getBackupsDir(), id, "manifest.json"), manifest);
|
|
7520
10317
|
return manifest;
|
|
7521
10318
|
}
|
|
7522
10319
|
function validateBackupManifestShape(value, path) {
|
|
@@ -7565,8 +10362,8 @@ function readBackupManifest(id) {
|
|
|
7565
10362
|
if (!isValidBackupId(id)) {
|
|
7566
10363
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
7567
10364
|
}
|
|
7568
|
-
const manifestPath =
|
|
7569
|
-
if (!
|
|
10365
|
+
const manifestPath = join33(getBackupsDir(), id, "manifest.json");
|
|
10366
|
+
if (!existsSync27(manifestPath)) {
|
|
7570
10367
|
throw new UserError(`no backup with id: ${id}`);
|
|
7571
10368
|
}
|
|
7572
10369
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -7576,7 +10373,7 @@ function readBackupManifest(id) {
|
|
|
7576
10373
|
|
|
7577
10374
|
// src/refine/context.ts
|
|
7578
10375
|
function resolveConfiguredRoot2(root) {
|
|
7579
|
-
return
|
|
10376
|
+
return resolvePath12(expandHome(root));
|
|
7580
10377
|
}
|
|
7581
10378
|
function assertPlainSkillName(name) {
|
|
7582
10379
|
if (!name || name.trim().length === 0) {
|
|
@@ -7588,32 +10385,32 @@ function assertPlainSkillName(name) {
|
|
|
7588
10385
|
}
|
|
7589
10386
|
function realpathOrNull2(p) {
|
|
7590
10387
|
try {
|
|
7591
|
-
return
|
|
10388
|
+
return realpathSync9(p);
|
|
7592
10389
|
} catch {
|
|
7593
10390
|
return null;
|
|
7594
10391
|
}
|
|
7595
10392
|
}
|
|
7596
10393
|
function isUnderRoot2(candidate, root) {
|
|
7597
|
-
const rel =
|
|
7598
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
10394
|
+
const rel = relative6(root, candidate);
|
|
10395
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
7599
10396
|
}
|
|
7600
10397
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
7601
10398
|
assertPlainSkillName(skill);
|
|
7602
|
-
const resolvedCwd =
|
|
7603
|
-
const localDir =
|
|
7604
|
-
const sharedDir =
|
|
7605
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
7606
|
-
const shared = { scope: "shared", dir: sharedDir, exists:
|
|
10399
|
+
const resolvedCwd = resolvePath12(cwd);
|
|
10400
|
+
const localDir = join34(resolvedCwd, ".claude", "skills", skill);
|
|
10401
|
+
const sharedDir = join34(resolvedCwd, "skills", skill);
|
|
10402
|
+
const local = { scope: "local", dir: localDir, exists: existsSync28(localDir) };
|
|
10403
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync28(sharedDir) };
|
|
7607
10404
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
7608
10405
|
return { local, shared, user };
|
|
7609
10406
|
}
|
|
7610
10407
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
7611
10408
|
if (userRootFlag) {
|
|
7612
|
-
const root =
|
|
7613
|
-
return { resolved: true, dir:
|
|
10409
|
+
const root = resolvePath12(userRootFlag);
|
|
10410
|
+
return { resolved: true, dir: join34(root, skill), root: userRootFlag, reason: "flag" };
|
|
7614
10411
|
}
|
|
7615
10412
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
7616
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
10413
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join34(resolveConfiguredRoot2(root), skill) }));
|
|
7617
10414
|
for (const candidate of candidates) {
|
|
7618
10415
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
7619
10416
|
if (skillReal) {
|
|
@@ -7631,14 +10428,14 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
|
7631
10428
|
}
|
|
7632
10429
|
function isFile2(p) {
|
|
7633
10430
|
try {
|
|
7634
|
-
return
|
|
10431
|
+
return statSync13(p).isFile();
|
|
7635
10432
|
} catch {
|
|
7636
10433
|
return false;
|
|
7637
10434
|
}
|
|
7638
10435
|
}
|
|
7639
10436
|
function isDirectory(p) {
|
|
7640
10437
|
try {
|
|
7641
|
-
return
|
|
10438
|
+
return statSync13(p).isDirectory();
|
|
7642
10439
|
} catch {
|
|
7643
10440
|
return false;
|
|
7644
10441
|
}
|
|
@@ -7648,12 +10445,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
7648
10445
|
scope,
|
|
7649
10446
|
dir,
|
|
7650
10447
|
dirExists: isDirectory(dir),
|
|
7651
|
-
skillMd: isFile2(
|
|
7652
|
-
patchMd: isFile2(
|
|
7653
|
-
extendMd: isFile2(
|
|
7654
|
-
configJson: isFile2(
|
|
7655
|
-
hooksDir: isDirectory(
|
|
7656
|
-
scriptsDir: isDirectory(
|
|
10448
|
+
skillMd: isFile2(join34(dir, "SKILL.md")),
|
|
10449
|
+
patchMd: isFile2(join34(dir, "SKILL.patch.md")),
|
|
10450
|
+
extendMd: isFile2(join34(dir, "SKILL.extend.md")),
|
|
10451
|
+
configJson: isFile2(join34(dir, "skill-config.json")),
|
|
10452
|
+
hooksDir: isDirectory(join34(dir, "hooks")),
|
|
10453
|
+
scriptsDir: isDirectory(join34(dir, "scripts"))
|
|
7657
10454
|
};
|
|
7658
10455
|
}
|
|
7659
10456
|
function inventoryAllScopes(resolution) {
|
|
@@ -7886,10 +10683,10 @@ function previewDiff(before, after, path) {
|
|
|
7886
10683
|
}
|
|
7887
10684
|
|
|
7888
10685
|
// src/commands/refine.ts
|
|
7889
|
-
var
|
|
10686
|
+
var PRO_FEATURE_LABEL9 = "skill refinement";
|
|
7890
10687
|
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
7891
|
-
function
|
|
7892
|
-
const status2 = proFeatureStatus(
|
|
10688
|
+
function requireProFeature3() {
|
|
10689
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
|
|
7893
10690
|
if (!status2.enabled) {
|
|
7894
10691
|
throw new UserError(status2.notice ?? "refine is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.");
|
|
7895
10692
|
}
|
|
@@ -7898,7 +10695,7 @@ function requireProFeature2() {
|
|
|
7898
10695
|
function isInteractive(json) {
|
|
7899
10696
|
return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
7900
10697
|
}
|
|
7901
|
-
function
|
|
10698
|
+
function emit2(json, payload, renderText) {
|
|
7902
10699
|
if (json) {
|
|
7903
10700
|
console.log(JSON.stringify(payload, null, 2));
|
|
7904
10701
|
} else {
|
|
@@ -7914,7 +10711,7 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
7914
10711
|
if (!isInteractive(json)) {
|
|
7915
10712
|
throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
|
|
7916
10713
|
}
|
|
7917
|
-
const rl =
|
|
10714
|
+
const rl = createInterface7({ input: process.stdin, output: process.stdout });
|
|
7918
10715
|
try {
|
|
7919
10716
|
return await confirmYesNo(rl, prompt);
|
|
7920
10717
|
} finally {
|
|
@@ -7922,18 +10719,18 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
7922
10719
|
}
|
|
7923
10720
|
}
|
|
7924
10721
|
function resolveConfiguredRoot3(root) {
|
|
7925
|
-
return
|
|
10722
|
+
return resolvePath13(expandHome(root));
|
|
7926
10723
|
}
|
|
7927
10724
|
function isUnderRoot3(candidate, root) {
|
|
7928
|
-
const rel =
|
|
7929
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
10725
|
+
const rel = relative7(root, candidate);
|
|
10726
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
|
|
7930
10727
|
}
|
|
7931
10728
|
function sanitizeDiffForConsole(diff) {
|
|
7932
10729
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
7933
10730
|
}
|
|
7934
10731
|
function resolveContentOption(options) {
|
|
7935
10732
|
if (options.content !== void 0) return options.content;
|
|
7936
|
-
if (options.contentFile) return
|
|
10733
|
+
if (options.contentFile) return readFileSync24(resolvePath13(options.contentFile), "utf-8");
|
|
7937
10734
|
return void 0;
|
|
7938
10735
|
}
|
|
7939
10736
|
function validateCaptureOptions(options) {
|
|
@@ -8059,19 +10856,19 @@ ${rendered}` : rendered;
|
|
|
8059
10856
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
8060
10857
|
let lst;
|
|
8061
10858
|
try {
|
|
8062
|
-
lst =
|
|
10859
|
+
lst = lstatSync7(scopeDir);
|
|
8063
10860
|
} catch {
|
|
8064
10861
|
lst = void 0;
|
|
8065
10862
|
}
|
|
8066
10863
|
if (lst?.isSymbolicLink()) {
|
|
8067
10864
|
throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
|
|
8068
10865
|
}
|
|
8069
|
-
|
|
10866
|
+
mkdirSync11(scopeDir, { recursive: true });
|
|
8070
10867
|
const real = resolveConfiguredRoot3(scopeDir);
|
|
8071
|
-
const lexical =
|
|
10868
|
+
const lexical = resolvePath13(scopeDir);
|
|
8072
10869
|
let realResolved;
|
|
8073
10870
|
try {
|
|
8074
|
-
realResolved =
|
|
10871
|
+
realResolved = realpathSync10(scopeDir);
|
|
8075
10872
|
} catch {
|
|
8076
10873
|
realResolved = real;
|
|
8077
10874
|
}
|
|
@@ -8085,28 +10882,28 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
8085
10882
|
}
|
|
8086
10883
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
8087
10884
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
8088
|
-
const destPath =
|
|
8089
|
-
const destDir =
|
|
8090
|
-
|
|
8091
|
-
const realDestDir =
|
|
10885
|
+
const destPath = join35(scopeDir, relPath);
|
|
10886
|
+
const destDir = dirname15(destPath);
|
|
10887
|
+
mkdirSync11(destDir, { recursive: true });
|
|
10888
|
+
const realDestDir = realpathSync10(destDir);
|
|
8092
10889
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
8093
10890
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
8094
10891
|
}
|
|
8095
10892
|
let destLst;
|
|
8096
10893
|
try {
|
|
8097
|
-
destLst =
|
|
10894
|
+
destLst = lstatSync7(destPath);
|
|
8098
10895
|
} catch {
|
|
8099
10896
|
destLst = void 0;
|
|
8100
10897
|
}
|
|
8101
10898
|
if (destLst?.isSymbolicLink()) {
|
|
8102
10899
|
throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
|
|
8103
10900
|
}
|
|
8104
|
-
|
|
8105
|
-
|
|
10901
|
+
writeFileSync10(destPath, content, { mode: 384 });
|
|
10902
|
+
chmodSync4(destPath, 384);
|
|
8106
10903
|
return destPath;
|
|
8107
10904
|
}
|
|
8108
10905
|
async function refineCaptureCommand(rawOptions = {}) {
|
|
8109
|
-
|
|
10906
|
+
requireProFeature3();
|
|
8110
10907
|
const opts = validateCaptureOptions(rawOptions);
|
|
8111
10908
|
const config = loadConfig();
|
|
8112
10909
|
const cwd = process.cwd();
|
|
@@ -8118,7 +10915,7 @@ async function refineCaptureCommand(rawOptions = {}) {
|
|
|
8118
10915
|
const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
|
|
8119
10916
|
const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
|
|
8120
10917
|
if (opts.dryRun) {
|
|
8121
|
-
|
|
10918
|
+
emit2(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
|
|
8122
10919
|
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
8123
10920
|
console.log(sanitizeDiffForConsole(preview.diff));
|
|
8124
10921
|
});
|
|
@@ -8131,7 +10928,7 @@ async function refineCaptureCommand(rawOptions = {}) {
|
|
|
8131
10928
|
const proceed = await confirmWrite(opts.yes, opts.json, `
|
|
8132
10929
|
Write ${preview.destPath}? [y/N] `);
|
|
8133
10930
|
if (!proceed) {
|
|
8134
|
-
|
|
10931
|
+
emit2(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
|
|
8135
10932
|
console.log("[skill-forge] capture cancelled \u2014 nothing written.");
|
|
8136
10933
|
});
|
|
8137
10934
|
return;
|
|
@@ -8164,7 +10961,7 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
8164
10961
|
} catch (err) {
|
|
8165
10962
|
throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
|
|
8166
10963
|
}
|
|
8167
|
-
|
|
10964
|
+
emit2(
|
|
8168
10965
|
opts.json,
|
|
8169
10966
|
{ written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
|
|
8170
10967
|
() => {
|
|
@@ -8181,21 +10978,21 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
8181
10978
|
}
|
|
8182
10979
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
8183
10980
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
8184
|
-
const probePath = probeRelPath ?
|
|
8185
|
-
const existingContent = probePath &&
|
|
10981
|
+
const probePath = probeRelPath ? join35(scopeDir, probeRelPath) : void 0;
|
|
10982
|
+
const existingContent = probePath && existsSync29(probePath) ? readFileSync24(probePath, "utf-8") : "";
|
|
8186
10983
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
8187
10984
|
const relPath = rendered.relPath;
|
|
8188
|
-
const destPath =
|
|
8189
|
-
const before =
|
|
10985
|
+
const destPath = join35(scopeDir, relPath);
|
|
10986
|
+
const before = existsSync29(destPath) ? readFileSync24(destPath, "utf-8") : "";
|
|
8190
10987
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
8191
10988
|
return { relPath, content: rendered.content, destPath, diff };
|
|
8192
10989
|
}
|
|
8193
10990
|
async function refineListCommand(options = {}) {
|
|
8194
|
-
|
|
10991
|
+
requireProFeature3();
|
|
8195
10992
|
const { records } = readRefinementsStore();
|
|
8196
10993
|
let rows = records;
|
|
8197
10994
|
if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
|
|
8198
|
-
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot ===
|
|
10995
|
+
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath13(options.project));
|
|
8199
10996
|
if (options.status) {
|
|
8200
10997
|
if (!isRefinementStatus(options.status)) {
|
|
8201
10998
|
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
@@ -8203,7 +11000,7 @@ async function refineListCommand(options = {}) {
|
|
|
8203
11000
|
rows = rows.filter((r) => r.status === options.status);
|
|
8204
11001
|
}
|
|
8205
11002
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
8206
|
-
|
|
11003
|
+
emit2(options.json, { records: rows }, () => {
|
|
8207
11004
|
if (rows.length === 0) {
|
|
8208
11005
|
if (filtered && records.length > 0) {
|
|
8209
11006
|
console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
|
|
@@ -8221,7 +11018,7 @@ async function refineListCommand(options = {}) {
|
|
|
8221
11018
|
});
|
|
8222
11019
|
}
|
|
8223
11020
|
async function refinePatternsCommand(options = {}) {
|
|
8224
|
-
|
|
11021
|
+
requireProFeature3();
|
|
8225
11022
|
const { records } = readPatternsStore();
|
|
8226
11023
|
let rows = records;
|
|
8227
11024
|
if (options.status) {
|
|
@@ -8232,11 +11029,11 @@ async function refinePatternsCommand(options = {}) {
|
|
|
8232
11029
|
}
|
|
8233
11030
|
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
8234
11031
|
if (options.project) {
|
|
8235
|
-
const resolvedProject =
|
|
11032
|
+
const resolvedProject = resolvePath13(options.project);
|
|
8236
11033
|
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
8237
11034
|
}
|
|
8238
11035
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
8239
|
-
|
|
11036
|
+
emit2(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
|
|
8240
11037
|
if (rows.length === 0) {
|
|
8241
11038
|
if (filtered && records.length > 0) {
|
|
8242
11039
|
console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
|
|
@@ -8265,13 +11062,13 @@ function renderInventoryLine(row) {
|
|
|
8265
11062
|
return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
|
|
8266
11063
|
}
|
|
8267
11064
|
async function refineWhichCommand(skill, options = {}) {
|
|
8268
|
-
|
|
11065
|
+
requireProFeature3();
|
|
8269
11066
|
if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
|
|
8270
11067
|
const config = loadConfig();
|
|
8271
11068
|
const cwd = process.cwd();
|
|
8272
11069
|
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
8273
11070
|
const inventory = inventoryAllScopes(scopes);
|
|
8274
|
-
|
|
11071
|
+
emit2(options.json, { scopes, inventory }, () => {
|
|
8275
11072
|
console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
|
|
8276
11073
|
for (const row of inventory) console.log(renderInventoryLine(row));
|
|
8277
11074
|
if (!scopes.user.resolved) {
|
|
@@ -8283,16 +11080,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
8283
11080
|
}
|
|
8284
11081
|
function writePostWriteSidecar(backupId, files) {
|
|
8285
11082
|
const sidecar = { schemaVersion: 1, files };
|
|
8286
|
-
const dir =
|
|
8287
|
-
|
|
8288
|
-
|
|
11083
|
+
const dir = join35(getBackupsDir(), backupId);
|
|
11084
|
+
mkdirSync11(dir, { recursive: true });
|
|
11085
|
+
writeFileSync10(join35(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
8289
11086
|
}
|
|
8290
11087
|
function readPostWriteSidecar(backupId) {
|
|
8291
|
-
const path =
|
|
8292
|
-
if (!
|
|
11088
|
+
const path = join35(getBackupsDir(), backupId, "post.json");
|
|
11089
|
+
if (!existsSync29(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
8293
11090
|
let raw;
|
|
8294
11091
|
try {
|
|
8295
|
-
raw =
|
|
11092
|
+
raw = readFileSync24(path, "utf-8");
|
|
8296
11093
|
} catch {
|
|
8297
11094
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
8298
11095
|
}
|
|
@@ -8311,7 +11108,7 @@ function readPostWriteSidecar(backupId) {
|
|
|
8311
11108
|
return { hashes, status: "ok" };
|
|
8312
11109
|
}
|
|
8313
11110
|
async function refinePromoteCommand(patternId, options = {}) {
|
|
8314
|
-
|
|
11111
|
+
requireProFeature3();
|
|
8315
11112
|
if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
|
|
8316
11113
|
const patternsStore = readPatternsStore();
|
|
8317
11114
|
const pattern = patternsStore.records.find((p) => p.id === patternId);
|
|
@@ -8337,21 +11134,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8337
11134
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
8338
11135
|
let realUserRoot;
|
|
8339
11136
|
try {
|
|
8340
|
-
realUserRoot =
|
|
11137
|
+
realUserRoot = realpathSync10(resolvedUserRoot);
|
|
8341
11138
|
} catch {
|
|
8342
11139
|
realUserRoot = resolvedUserRoot;
|
|
8343
11140
|
}
|
|
8344
11141
|
let realSkillDir;
|
|
8345
11142
|
try {
|
|
8346
|
-
realSkillDir =
|
|
11143
|
+
realSkillDir = realpathSync10(skillDir);
|
|
8347
11144
|
} catch {
|
|
8348
|
-
realSkillDir =
|
|
11145
|
+
realSkillDir = resolvePath13(skillDir);
|
|
8349
11146
|
}
|
|
8350
11147
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
8351
11148
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
8352
11149
|
}
|
|
8353
|
-
const skillMdPath =
|
|
8354
|
-
if (!
|
|
11150
|
+
const skillMdPath = join35(skillDir, "SKILL.md");
|
|
11151
|
+
if (!existsSync29(skillMdPath)) {
|
|
8355
11152
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
8356
11153
|
}
|
|
8357
11154
|
const refinementsStore = readRefinementsStore();
|
|
@@ -8368,11 +11165,11 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8368
11165
|
`pattern ${patternId}'s representative refinement is a '${representative.overrideType}' override \u2014 promote currently only materializes patch/extend/new-type patterns against the user-scope SKILL.md; other override types must be merged manually.`
|
|
8369
11166
|
);
|
|
8370
11167
|
}
|
|
8371
|
-
const before =
|
|
11168
|
+
const before = readFileSync24(skillMdPath, "utf-8");
|
|
8372
11169
|
const after = applyPatchBlocks(before, [block]);
|
|
8373
11170
|
const diff = previewDiff(before, after, skillMdPath);
|
|
8374
11171
|
if (options.dryRun) {
|
|
8375
|
-
|
|
11172
|
+
emit2(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
|
|
8376
11173
|
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
8377
11174
|
console.log(sanitizeDiffForConsole(diff));
|
|
8378
11175
|
});
|
|
@@ -8385,7 +11182,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8385
11182
|
const proceed = await confirmWrite(options.yes, options.json, `
|
|
8386
11183
|
Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
8387
11184
|
if (!proceed) {
|
|
8388
|
-
|
|
11185
|
+
emit2(options.json, { promoted: false, cancelled: true }, () => {
|
|
8389
11186
|
console.log("[skill-forge] promote cancelled \u2014 nothing written.");
|
|
8390
11187
|
});
|
|
8391
11188
|
return;
|
|
@@ -8393,14 +11190,14 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
8393
11190
|
const backup = createBackup([skillMdPath]);
|
|
8394
11191
|
const tempPath = `${skillMdPath}.tmp-${randomUUID5()}`;
|
|
8395
11192
|
try {
|
|
8396
|
-
|
|
11193
|
+
writeFileSync10(tempPath, after, "utf-8");
|
|
8397
11194
|
renameSync6(tempPath, skillMdPath);
|
|
8398
11195
|
} catch (err) {
|
|
8399
11196
|
rmSync9(tempPath, { force: true });
|
|
8400
11197
|
throw err;
|
|
8401
11198
|
}
|
|
8402
11199
|
try {
|
|
8403
|
-
const postSha256 =
|
|
11200
|
+
const postSha256 = createHash8("sha256").update(readFileSync24(skillMdPath)).digest("hex");
|
|
8404
11201
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
8405
11202
|
markGeneralized(patternId, backup.id);
|
|
8406
11203
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -8420,31 +11217,31 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
8420
11217
|
`the user-scope skill was updated at ${skillMdPath} (backup ${backup.id}), but recording post-write verification and/or pattern/provenance state failed: ${err.message}`
|
|
8421
11218
|
);
|
|
8422
11219
|
}
|
|
8423
|
-
|
|
11220
|
+
emit2(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
|
|
8424
11221
|
console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
|
|
8425
11222
|
});
|
|
8426
11223
|
}
|
|
8427
11224
|
function assertBackupDirContained(backupId) {
|
|
8428
11225
|
const backupsDir = getBackupsDir();
|
|
8429
|
-
if (!
|
|
8430
|
-
const realBackupsDir =
|
|
8431
|
-
const backupDir =
|
|
8432
|
-
if (!
|
|
8433
|
-
const realBackupDir =
|
|
11226
|
+
if (!existsSync29(backupsDir)) return;
|
|
11227
|
+
const realBackupsDir = realpathSync10(backupsDir);
|
|
11228
|
+
const backupDir = join35(backupsDir, backupId);
|
|
11229
|
+
if (!existsSync29(backupDir)) return;
|
|
11230
|
+
const realBackupDir = realpathSync10(backupDir);
|
|
8434
11231
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
8435
11232
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
8436
11233
|
}
|
|
8437
11234
|
}
|
|
8438
11235
|
function resolveTargetRealPath(path) {
|
|
8439
|
-
let lexicalAncestor =
|
|
11236
|
+
let lexicalAncestor = resolvePath13(path);
|
|
8440
11237
|
while (true) {
|
|
8441
11238
|
try {
|
|
8442
|
-
const real =
|
|
8443
|
-
const remainder =
|
|
8444
|
-
return remainder === "" ? real :
|
|
11239
|
+
const real = realpathSync10(lexicalAncestor);
|
|
11240
|
+
const remainder = relative7(lexicalAncestor, resolvePath13(path));
|
|
11241
|
+
return remainder === "" ? real : join35(real, remainder);
|
|
8445
11242
|
} catch {
|
|
8446
|
-
const parent =
|
|
8447
|
-
if (parent === lexicalAncestor) return
|
|
11243
|
+
const parent = dirname15(lexicalAncestor);
|
|
11244
|
+
if (parent === lexicalAncestor) return resolvePath13(path);
|
|
8448
11245
|
lexicalAncestor = parent;
|
|
8449
11246
|
}
|
|
8450
11247
|
}
|
|
@@ -8456,7 +11253,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
8456
11253
|
const roots = config.skillsRoots.map((root) => {
|
|
8457
11254
|
const resolved = resolveConfiguredRoot3(root);
|
|
8458
11255
|
try {
|
|
8459
|
-
return
|
|
11256
|
+
return realpathSync10(resolved);
|
|
8460
11257
|
} catch {
|
|
8461
11258
|
return resolved;
|
|
8462
11259
|
}
|
|
@@ -8473,7 +11270,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
8473
11270
|
for (const file of manifest.files) {
|
|
8474
11271
|
let lst;
|
|
8475
11272
|
try {
|
|
8476
|
-
lst =
|
|
11273
|
+
lst = lstatSync7(file.path);
|
|
8477
11274
|
} catch {
|
|
8478
11275
|
lst = void 0;
|
|
8479
11276
|
}
|
|
@@ -8483,14 +11280,14 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
8483
11280
|
}
|
|
8484
11281
|
}
|
|
8485
11282
|
function findLexicalRoot(filePath, config) {
|
|
8486
|
-
const lexicalTarget =
|
|
11283
|
+
const lexicalTarget = resolvePath13(filePath);
|
|
8487
11284
|
for (const root of config.skillsRoots) {
|
|
8488
11285
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
8489
|
-
const rel =
|
|
8490
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
11286
|
+
const rel = relative7(rootLexical, lexicalTarget);
|
|
11287
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute8(rel))) continue;
|
|
8491
11288
|
let rootReal;
|
|
8492
11289
|
try {
|
|
8493
|
-
rootReal =
|
|
11290
|
+
rootReal = realpathSync10(rootLexical);
|
|
8494
11291
|
} catch {
|
|
8495
11292
|
rootReal = rootLexical;
|
|
8496
11293
|
}
|
|
@@ -8500,8 +11297,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
8500
11297
|
}
|
|
8501
11298
|
function walkRealComponents(filePath, config, createMissing) {
|
|
8502
11299
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
8503
|
-
const relPath =
|
|
8504
|
-
const components = relPath.split(
|
|
11300
|
+
const relPath = relative7(rootLexical, resolvePath13(filePath));
|
|
11301
|
+
const components = relPath.split(sep6).filter((c) => c.length > 0);
|
|
8505
11302
|
if (components.length === 0) {
|
|
8506
11303
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
8507
11304
|
}
|
|
@@ -8510,10 +11307,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
8510
11307
|
let current = rootReal;
|
|
8511
11308
|
for (const component of dirComponents) {
|
|
8512
11309
|
if (current === void 0) break;
|
|
8513
|
-
const next =
|
|
11310
|
+
const next = join35(current, component);
|
|
8514
11311
|
let lst;
|
|
8515
11312
|
try {
|
|
8516
|
-
lst =
|
|
11313
|
+
lst = lstatSync7(next);
|
|
8517
11314
|
} catch {
|
|
8518
11315
|
lst = void 0;
|
|
8519
11316
|
}
|
|
@@ -8522,7 +11319,7 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
8522
11319
|
current = void 0;
|
|
8523
11320
|
continue;
|
|
8524
11321
|
}
|
|
8525
|
-
|
|
11322
|
+
mkdirSync11(next);
|
|
8526
11323
|
current = next;
|
|
8527
11324
|
continue;
|
|
8528
11325
|
}
|
|
@@ -8541,13 +11338,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8541
11338
|
if (parentReal === void 0) {
|
|
8542
11339
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
8543
11340
|
}
|
|
8544
|
-
const finalPath =
|
|
11341
|
+
const finalPath = join35(parentReal, leafComponent);
|
|
8545
11342
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
8546
11343
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
8547
11344
|
}
|
|
8548
11345
|
let leafLst;
|
|
8549
11346
|
try {
|
|
8550
|
-
leafLst =
|
|
11347
|
+
leafLst = lstatSync7(finalPath);
|
|
8551
11348
|
} catch {
|
|
8552
11349
|
leafLst = void 0;
|
|
8553
11350
|
}
|
|
@@ -8556,8 +11353,8 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8556
11353
|
}
|
|
8557
11354
|
const tempPath = `${finalPath}.tmp-${randomUUID5()}`;
|
|
8558
11355
|
try {
|
|
8559
|
-
|
|
8560
|
-
|
|
11356
|
+
writeFileSync10(tempPath, contentBuffer);
|
|
11357
|
+
chmodSync4(tempPath, mode);
|
|
8561
11358
|
renameSync6(tempPath, finalPath);
|
|
8562
11359
|
} catch (err) {
|
|
8563
11360
|
rmSync9(tempPath, { force: true });
|
|
@@ -8567,13 +11364,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8567
11364
|
function safeDeleteTombstone(filePath, config) {
|
|
8568
11365
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
8569
11366
|
if (parentReal === void 0) return;
|
|
8570
|
-
const finalPath =
|
|
11367
|
+
const finalPath = join35(parentReal, leafComponent);
|
|
8571
11368
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
8572
11369
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
8573
11370
|
}
|
|
8574
11371
|
let leafLst;
|
|
8575
11372
|
try {
|
|
8576
|
-
leafLst =
|
|
11373
|
+
leafLst = lstatSync7(finalPath);
|
|
8577
11374
|
} catch {
|
|
8578
11375
|
leafLst = void 0;
|
|
8579
11376
|
}
|
|
@@ -8584,7 +11381,7 @@ function safeDeleteTombstone(filePath, config) {
|
|
|
8584
11381
|
rmSync9(finalPath, { force: true });
|
|
8585
11382
|
}
|
|
8586
11383
|
async function refineRollbackCommand(backupId, options = {}) {
|
|
8587
|
-
|
|
11384
|
+
requireProFeature3();
|
|
8588
11385
|
if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
|
|
8589
11386
|
if (!isValidBackupId(backupId)) {
|
|
8590
11387
|
throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
@@ -8609,7 +11406,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
8609
11406
|
for (const file of manifest.files) {
|
|
8610
11407
|
const expected = sidecar.hashes.get(file.path);
|
|
8611
11408
|
if (expected === void 0) continue;
|
|
8612
|
-
const currentSha256 =
|
|
11409
|
+
const currentSha256 = existsSync29(file.path) ? createHash8("sha256").update(readFileSync24(file.path)).digest("hex") : null;
|
|
8613
11410
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
8614
11411
|
}
|
|
8615
11412
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -8628,7 +11425,7 @@ Roll back backup ${backupId}? Affected files:
|
|
|
8628
11425
|
${fileList}
|
|
8629
11426
|
[y/N] `);
|
|
8630
11427
|
if (!proceed) {
|
|
8631
|
-
|
|
11428
|
+
emit2(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
|
|
8632
11429
|
console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
|
|
8633
11430
|
});
|
|
8634
11431
|
return;
|
|
@@ -8657,7 +11454,7 @@ ${fileList}
|
|
|
8657
11454
|
pattern.generalizedBackupId = void 0;
|
|
8658
11455
|
writePatternsStore(patternsStore);
|
|
8659
11456
|
}
|
|
8660
|
-
|
|
11457
|
+
emit2(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
|
|
8661
11458
|
console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
|
|
8662
11459
|
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
8663
11460
|
});
|
|
@@ -8700,6 +11497,27 @@ function parsePruneDays(value) {
|
|
|
8700
11497
|
}
|
|
8701
11498
|
return n;
|
|
8702
11499
|
}
|
|
11500
|
+
function parseInsightAnalysisMode(value) {
|
|
11501
|
+
if (value !== "independent" && value !== "single") {
|
|
11502
|
+
throw new UserError(`--analysis-mode must be "independent" or "single" (got "${value}")`);
|
|
11503
|
+
}
|
|
11504
|
+
return value;
|
|
11505
|
+
}
|
|
11506
|
+
function parseSkillMapCoverage(value) {
|
|
11507
|
+
if (value !== "static" && value !== "resolved") {
|
|
11508
|
+
throw new UserError(`--skill-map-coverage must be "static" or "resolved" (got "${value}")`);
|
|
11509
|
+
}
|
|
11510
|
+
return value;
|
|
11511
|
+
}
|
|
11512
|
+
function parseInsightCloseStatus(value) {
|
|
11513
|
+
if (value !== "dismissed" && value !== "archived") {
|
|
11514
|
+
throw new UserError(`--status must be "dismissed" or "archived" (got "${value}")`);
|
|
11515
|
+
}
|
|
11516
|
+
return value;
|
|
11517
|
+
}
|
|
11518
|
+
function collectValue(value, previous) {
|
|
11519
|
+
return [...previous, value];
|
|
11520
|
+
}
|
|
8703
11521
|
var program = new Command();
|
|
8704
11522
|
program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
|
|
8705
11523
|
program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
|
|
@@ -8743,6 +11561,14 @@ program.command("ingest").description(
|
|
|
8743
11561
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
8744
11562
|
var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
|
|
8745
11563
|
queue.command("close <id>").description("Close a queue entry after the decide pass \u2014 agents use this instead of hand-editing queue.json").requiredOption("--status <status>", 'final status: "ingested" or "dismissed"').action(queueClose);
|
|
11564
|
+
var insight = program.command("insight").description("Turn untrusted sources into evidence-backed capability plans and Jira manifests (Pro)");
|
|
11565
|
+
insight.command("analyze [source...]").description("Register and fingerprint sources in a private study; never fetches URLs or mutates capabilities").option("--manifest <file>", "JSON source manifest (an array or { sources: [...] })").option("--skill-map <file>", "optional resolved/static skill map used for capability coverage").option("--skill-map-coverage <coverage>", "declare supplied map provenance: static|resolved", parseSkillMapCoverage).option("--analysis-mode <mode>", "requested evidence mode: independent|single (default: independent)", parseInsightAnalysisMode).option("--retain-source-copies", "copy observed local source files into the private study workspace").option("--handoff", "launch the configured agent with the bundled source-insight workflow").option("--json", "print the created study summary as one JSON document").action(insightAnalyze);
|
|
11566
|
+
insight.command("submit <study-id>").description("Validate and atomically import agent-produced analysis (cannot approve the study)").requiredOption("--file <analysis.json>", "candidate analysis JSON file").option("--json", "print the updated study summary as one JSON document").action(insightSubmit);
|
|
11567
|
+
insight.command("review <study-id>").description("Validate and render the current evidence and domain plans; read-only").option("--json", "print the validation/report payload as one JSON document").action(insightReview);
|
|
11568
|
+
insight.command("approve <study-id>").description("Human-only TTY approval of the exact plan hash; agents must never invoke this command").option("--domain <id>", "approve one domain ID (repeatable; default: every domain)", collectValue, []).action(insightApprove);
|
|
11569
|
+
insight.command("jira <study-id>").description("Generate a Jira manifest or manage its private local receipts; never calls Jira").option("--project <key>", "destination Jira project key recorded in a new manifest").option("--out <file>", "write an additional export-safe manifest here (refuses an existing file)").option("--handoff", "launch the configured agent to present and confirm the backlog before Jira writes").option("--receipt <file>", "append one validated local receipt for the current manifest").option("--resume", "show local resume state for the current manifest").option("--json", "print the selected mode result as one JSON document").action(insightJira);
|
|
11570
|
+
insight.command("status <study-id>").description("Show private study lifecycle and evidence counts; read-only").option("--json", "print the status payload as one JSON document").action(insightStatus);
|
|
11571
|
+
insight.command("close <study-id>").description("Close a study while preserving its append-only receipts").requiredOption("--status <status>", "dismissed|archived", parseInsightCloseStatus).action(insightClose);
|
|
8746
11572
|
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
8747
11573
|
refine.command("capture", { isDefault: true }).description("Record a refinement and write a project-scope override artifact (never touches the base skill)").option("--skill <name>", "target skill name").option("--category <cat>", "trigger|content|hook|tool|pattern|config|new").option("--target <section>", 'heading path or file the refinement targets, e.g. "Hooks > duplicate-check"').option("--override-type <type>", "patch|extend|config|full|hook|script|new").option("--action <action>", "patch action: append|prepend|replace-section|insert-after|insert-before|delete-section").option("--marker <text>", "marker text for insert-* / replace-section / delete-section (filename for full|hook|script)").option("--content <text>", "override content inline").option("--content-file <file>", "read override content from a file").option("--expected <text>", "expected behavior").option("--actual <text>", "actual observed behavior").option("--example <text>", "concrete example of the gap").option("--outcome <text>", "desired outcome").option("--root-cause <text>", "root cause per the gap analysis").option("--pattern-id <id>", "join an existing pattern (PAT-xxxx) instead of fingerprint matching").option("--scope <scope>", 'where to write the artifact: "local" (.claude/skills, default) or "shared" (./skills)').option("--dry-run", "preview the artifact diff; write nothing").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm; never implies --handoff").option("--handoff", "authorizes launching the configured agent with the bundled refine prompt").action(refineCaptureCommand);
|
|
8748
11574
|
refine.command("list").description("Show refinement history from the user-scope store").option("--status <status>", "filter by refinement status").option("--skill <name>", "filter by skill").option("--project <name>", "filter by project").option("--json", "print records as JSON").action(refineListCommand);
|