@rhize/skill-forge 0.14.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 -932
- package/dist/cli.js +3125 -328
- 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 +1 -1
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
|
}
|
|
@@ -3574,9 +3632,9 @@ function renderListField(labelText, items, placeholder) {
|
|
|
3574
3632
|
if (!items || items.length === 0) {
|
|
3575
3633
|
return `${head}${placeholder}`;
|
|
3576
3634
|
}
|
|
3577
|
-
const
|
|
3635
|
+
const bullets2 = items.map((item) => ` - ${item}`).join("\n");
|
|
3578
3636
|
return `${head.trimEnd()}
|
|
3579
|
-
${
|
|
3637
|
+
${bullets2}`;
|
|
3580
3638
|
}
|
|
3581
3639
|
function renderSafetyLine(safety) {
|
|
3582
3640
|
const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
|
|
@@ -3836,11 +3894,14 @@ function generateId(existingIds) {
|
|
|
3836
3894
|
// src/handoff.ts
|
|
3837
3895
|
import { spawnSync } from "child_process";
|
|
3838
3896
|
import { existsSync as existsSync15 } from "fs";
|
|
3839
|
-
import { dirname as dirname7, join as join18 } from "path";
|
|
3897
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join18, win32 } from "path";
|
|
3840
3898
|
import { fileURLToPath } from "url";
|
|
3841
3899
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
3842
3900
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
3843
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
|
+
}
|
|
3844
3905
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
3845
3906
|
const builtCandidate = join18(here, assetName);
|
|
3846
3907
|
if (existsSync15(builtCandidate)) return builtCandidate;
|
|
@@ -3857,29 +3918,35 @@ function substitute(template, vars) {
|
|
|
3857
3918
|
function buildHandoffCommand(config, installedPath, deps = {}) {
|
|
3858
3919
|
const isOnPath = deps.isOnPath ?? defaultIsOnPath;
|
|
3859
3920
|
const promptPath = resolvePromptPath(deps.promptAsset);
|
|
3860
|
-
const
|
|
3921
|
+
const baseVars = { path: installedPath, prompt: promptPath };
|
|
3922
|
+
const instruction = substitute([deps.instruction ?? HANDOFF_INSTRUCTION], baseVars)[0];
|
|
3923
|
+
const vars = { ...baseVars, instruction };
|
|
3861
3924
|
if (config.handoffCommand && config.handoffCommand.length > 0) {
|
|
3862
|
-
|
|
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" };
|
|
3863
3929
|
}
|
|
3864
3930
|
for (const bin of KNOWN_AGENT_BINARIES) {
|
|
3865
3931
|
if (isOnPath(bin)) {
|
|
3866
|
-
|
|
3932
|
+
const template = bin === "codex" ? [bin, "exec", "{instruction}"] : [bin, "-p", "{instruction}"];
|
|
3933
|
+
return { argv: substitute(template, vars), source: "detected" };
|
|
3867
3934
|
}
|
|
3868
3935
|
}
|
|
3869
3936
|
return { argv: [], source: "none" };
|
|
3870
3937
|
}
|
|
3871
|
-
function printOnlyMessage(installedPath, promptAsset) {
|
|
3938
|
+
function printOnlyMessage(installedPath, promptAsset, targetLabel = "Skill path") {
|
|
3872
3939
|
const promptPath = resolvePromptPath(promptAsset);
|
|
3873
3940
|
return `[skill-forge] no agent handoff available \u2014 run one yourself:
|
|
3874
3941
|
Prompt: ${promptPath}
|
|
3875
|
-
|
|
3942
|
+
${targetLabel}: ${installedPath}`;
|
|
3876
3943
|
}
|
|
3877
3944
|
function runHandoff(config, installedPath, options = {}) {
|
|
3878
3945
|
const command = buildHandoffCommand(config, installedPath, options);
|
|
3879
3946
|
const spawn2 = options.spawn ?? spawnSync;
|
|
3880
3947
|
const log = options.log ?? console.log;
|
|
3881
3948
|
if (command.source === "none" || command.argv.length === 0) {
|
|
3882
|
-
const printed = printOnlyMessage(installedPath, options.promptAsset);
|
|
3949
|
+
const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
|
|
3883
3950
|
log(printed);
|
|
3884
3951
|
return { ran: false, command, printed };
|
|
3885
3952
|
}
|
|
@@ -3887,7 +3954,7 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
3887
3954
|
const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
|
|
3888
3955
|
const error = result.error;
|
|
3889
3956
|
if (error?.code === "ENOENT") {
|
|
3890
|
-
const printed = printOnlyMessage(installedPath, options.promptAsset);
|
|
3957
|
+
const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
|
|
3891
3958
|
log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
|
|
3892
3959
|
log(printed);
|
|
3893
3960
|
return { ran: false, command, printed, error: error.message };
|
|
@@ -4150,6 +4217,136 @@ var AGENTS = [
|
|
|
4150
4217
|
{ id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
|
|
4151
4218
|
].map(makeAgent);
|
|
4152
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
|
+
|
|
4153
4350
|
// src/commands/audit.ts
|
|
4154
4351
|
import { existsSync as existsSync16 } from "fs";
|
|
4155
4352
|
import { join as join20, resolve as resolvePath3 } from "path";
|
|
@@ -4393,6 +4590,15 @@ function presenceNote(presence) {
|
|
|
4393
4590
|
if (presence === "absent") return " [CLI not on PATH]";
|
|
4394
4591
|
return "";
|
|
4395
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
|
+
}
|
|
4396
4602
|
function printMcpDetectionResults(detectedMcp) {
|
|
4397
4603
|
if (detectedMcp.length === 0) return;
|
|
4398
4604
|
console.log("\n[skill-forge] init \u2014 MCP config detection");
|
|
@@ -4422,13 +4628,17 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
|
|
|
4422
4628
|
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
|
|
4423
4629
|
};
|
|
4424
4630
|
}
|
|
4425
|
-
function printSummary(config) {
|
|
4631
|
+
async function printSummary(config, deps = {}) {
|
|
4426
4632
|
console.log(`
|
|
4427
4633
|
[skill-forge] wrote config to ${getConfigPath()}`);
|
|
4428
4634
|
console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
|
|
4429
4635
|
if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
|
|
4430
4636
|
if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
|
|
4431
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
|
+
}
|
|
4432
4642
|
}
|
|
4433
4643
|
function isPromptAbort(err) {
|
|
4434
4644
|
return err instanceof Error && (err.name === "AbortError" || err.code === "ABORT_ERR");
|
|
@@ -4544,8 +4754,41 @@ async function promptHandoffAgent(rl, detected, narrow) {
|
|
|
4544
4754
|
const idx = await promptSingleChoice(rl, `Choose [1-${ordered.length}] or Enter to skip: `, ordered.length, "skip");
|
|
4545
4755
|
return idx === void 0 ? void 0 : ordered[idx].agent;
|
|
4546
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
|
+
}
|
|
4547
4789
|
async function init(options = {}, deps = {}) {
|
|
4548
4790
|
const runAudit = deps.runAudit ?? auditCommand;
|
|
4791
|
+
const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
|
|
4549
4792
|
const cwd = options.cwd ?? process.cwd();
|
|
4550
4793
|
const home = options.home ?? homedir4();
|
|
4551
4794
|
const detected = detectAgents(cwd, home, options.pathEnv);
|
|
@@ -4553,6 +4796,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4553
4796
|
if (options.list) {
|
|
4554
4797
|
printDetectionResults(detected);
|
|
4555
4798
|
printMcpDetectionResults(detectedMcp);
|
|
4799
|
+
await printGitDetectionColumn(detected, gitDeps);
|
|
4556
4800
|
return;
|
|
4557
4801
|
}
|
|
4558
4802
|
if (detected.length === 0 && detectedMcp.length === 0) {
|
|
@@ -4574,7 +4818,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4574
4818
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
4575
4819
|
const config = buildConfig(selectedRoots, selectedRoots[0], void 0, selectedMcpTargets);
|
|
4576
4820
|
saveConfig(config);
|
|
4577
|
-
printSummary(config);
|
|
4821
|
+
await printSummary(config, gitDeps);
|
|
4578
4822
|
await runAudit({ yes: true });
|
|
4579
4823
|
return;
|
|
4580
4824
|
}
|
|
@@ -4595,9 +4839,10 @@ async function init(options = {}, deps = {}) {
|
|
|
4595
4839
|
}
|
|
4596
4840
|
const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
|
|
4597
4841
|
const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
|
|
4842
|
+
await promptGitPreflight(rl, selected, gitDeps);
|
|
4598
4843
|
const config = buildConfig(selected, target, handoffAgent, selectedMcp);
|
|
4599
4844
|
saveConfig(config);
|
|
4600
|
-
printSummary(config);
|
|
4845
|
+
await printSummary(config, gitDeps);
|
|
4601
4846
|
if (options.offerAudit !== false) {
|
|
4602
4847
|
const answer = (await rl.question("\nRun the skills & MCP audit now? [Y/n] ")).trim().toLowerCase();
|
|
4603
4848
|
runAuditNow = answer === "" || answer === "y" || answer === "yes";
|
|
@@ -4896,7 +5141,7 @@ import { existsSync as existsSync19 } from "fs";
|
|
|
4896
5141
|
|
|
4897
5142
|
// src/commands/held.ts
|
|
4898
5143
|
import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
|
|
4899
|
-
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";
|
|
4900
5145
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
4901
5146
|
function resolveHeldPath(quarantineDir, id) {
|
|
4902
5147
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
@@ -4908,7 +5153,7 @@ function resolveHeldPath(quarantineDir, id) {
|
|
|
4908
5153
|
}
|
|
4909
5154
|
const realRoot = realpathSync3(resolvePath4(quarantineDir));
|
|
4910
5155
|
const realCandidate = realpathSync3(candidate);
|
|
4911
|
-
if (!realCandidate.startsWith(realRoot +
|
|
5156
|
+
if (!realCandidate.startsWith(realRoot + sep3)) {
|
|
4912
5157
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
4913
5158
|
}
|
|
4914
5159
|
return realCandidate;
|
|
@@ -5436,6 +5681,10 @@ async function status() {
|
|
|
5436
5681
|
if (config.mcpTargets?.length) console.log(` mcpTargets: ${config.mcpTargets.join(", ")}`);
|
|
5437
5682
|
console.log(` quarantineDir: ${config.quarantineDir}`);
|
|
5438
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
|
+
}
|
|
5439
5688
|
console.log(` last audit: ${formatAuditFreshness(readAuditState())}`);
|
|
5440
5689
|
console.log(
|
|
5441
5690
|
` held in quarantine: ${held.readable ? held.count : "UNREADABLE (quarantine dir cannot be listed)"}` + (held.count > 0 ? " (`skill-forge promote <id>` / `reject <id>`)" : "")
|
|
@@ -5448,9 +5697,9 @@ async function status() {
|
|
|
5448
5697
|
// src/evolve.ts
|
|
5449
5698
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
5450
5699
|
import { createHash as createHash3 } from "crypto";
|
|
5451
|
-
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";
|
|
5452
5701
|
import { tmpdir as tmpdir2 } from "os";
|
|
5453
|
-
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";
|
|
5454
5703
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
5455
5704
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
5456
5705
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
@@ -5518,7 +5767,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5518
5767
|
}
|
|
5519
5768
|
const stagingRoot = join24(project, ".skillopt-sleep", "staging");
|
|
5520
5769
|
const stagingDir = resolvePath5(parsed.staging_dir);
|
|
5521
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
5770
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep4)) {
|
|
5522
5771
|
throw new UserError(
|
|
5523
5772
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
5524
5773
|
);
|
|
@@ -5737,15 +5986,15 @@ function isAlreadyAdopted(stagingDir, manifest) {
|
|
|
5737
5986
|
}
|
|
5738
5987
|
return sawComparison;
|
|
5739
5988
|
}
|
|
5740
|
-
function assertNotSymlink(path,
|
|
5989
|
+
function assertNotSymlink(path, label2) {
|
|
5741
5990
|
let stat;
|
|
5742
5991
|
try {
|
|
5743
|
-
stat =
|
|
5992
|
+
stat = lstatSync4(path);
|
|
5744
5993
|
} catch {
|
|
5745
5994
|
return;
|
|
5746
5995
|
}
|
|
5747
5996
|
if (stat.isSymbolicLink()) {
|
|
5748
|
-
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)`);
|
|
5749
5998
|
}
|
|
5750
5999
|
}
|
|
5751
6000
|
function sha256File(path) {
|
|
@@ -6496,7 +6745,7 @@ import { join as join26, resolve as resolvePath8 } from "path";
|
|
|
6496
6745
|
// src/gate/skillMapDrift.ts
|
|
6497
6746
|
import { createHash as createHash4 } from "crypto";
|
|
6498
6747
|
import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
|
|
6499
|
-
import { dirname as dirname10, isAbsolute as
|
|
6748
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
|
|
6500
6749
|
function sha256(text) {
|
|
6501
6750
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6502
6751
|
}
|
|
@@ -6529,7 +6778,7 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
6529
6778
|
}
|
|
6530
6779
|
function resolveLocalPath(path, mapDir) {
|
|
6531
6780
|
const expanded = expandHome(path);
|
|
6532
|
-
if (
|
|
6781
|
+
if (isAbsolute3(expanded)) return resolve7(expanded);
|
|
6533
6782
|
const candidates = [resolve7(expanded)];
|
|
6534
6783
|
if (mapDir) {
|
|
6535
6784
|
candidates.push(resolve7(mapDir, expanded));
|
|
@@ -6942,9 +7191,9 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
6942
7191
|
if (result.housekeeping.length > 0) {
|
|
6943
7192
|
lines.push("", "## Housekeeping", "");
|
|
6944
7193
|
for (const action of result.housekeeping) {
|
|
6945
|
-
const
|
|
7194
|
+
const label2 = action.outcome === "applied" ? "APPLIED" : action.outcome === "failed" ? "FAILED" : "WOULD APPLY";
|
|
6946
7195
|
lines.push(
|
|
6947
|
-
`- ${
|
|
7196
|
+
`- ${label2} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
|
|
6948
7197
|
);
|
|
6949
7198
|
}
|
|
6950
7199
|
}
|
|
@@ -7060,8 +7309,8 @@ async function routineCommand(options = {}) {
|
|
|
7060
7309
|
if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
|
|
7061
7310
|
console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
|
|
7062
7311
|
for (const action of housekeeping) {
|
|
7063
|
-
const
|
|
7064
|
-
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}` : ""}`);
|
|
7065
7314
|
}
|
|
7066
7315
|
for (const item of result.degraded) console.log(` INCOMPLETE: ${item}`);
|
|
7067
7316
|
console.log(` report -> ${result.reportPath}`);
|
|
@@ -7080,7 +7329,7 @@ async function routineCommand(options = {}) {
|
|
|
7080
7329
|
|
|
7081
7330
|
// src/commands/ingest.ts
|
|
7082
7331
|
import { realpathSync as realpathSync6 } from "fs";
|
|
7083
|
-
import { dirname as dirname12, isAbsolute as
|
|
7332
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, relative as relative3, resolve as resolvePath9 } from "path";
|
|
7084
7333
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
7085
7334
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
7086
7335
|
function requireProFeature() {
|
|
@@ -7127,8 +7376,8 @@ function realpathOrNull(p) {
|
|
|
7127
7376
|
}
|
|
7128
7377
|
}
|
|
7129
7378
|
function isUnderRoot(candidate, root) {
|
|
7130
|
-
const rel =
|
|
7131
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7379
|
+
const rel = relative3(root, candidate);
|
|
7380
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
7132
7381
|
}
|
|
7133
7382
|
function validatePendingEntries(entries, config) {
|
|
7134
7383
|
const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
|
|
@@ -7204,195 +7453,2714 @@ async function queueClose(id, options = {}) {
|
|
|
7204
7453
|
console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
|
|
7205
7454
|
}
|
|
7206
7455
|
|
|
7207
|
-
// src/commands/
|
|
7208
|
-
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
7209
|
-
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";
|
|
7210
|
-
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
|
|
7211
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";
|
|
7212
7467
|
|
|
7213
|
-
// src/
|
|
7214
|
-
import {
|
|
7215
|
-
import {
|
|
7468
|
+
// src/insights/inventory.ts
|
|
7469
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
7470
|
+
import { join as join28 } from "path";
|
|
7216
7471
|
|
|
7217
|
-
// src/
|
|
7218
|
-
import {
|
|
7219
|
-
var
|
|
7220
|
-
var
|
|
7221
|
-
var
|
|
7222
|
-
var
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
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
|
+
);
|
|
7228
7566
|
}
|
|
7229
|
-
function
|
|
7230
|
-
return
|
|
7567
|
+
function insightIdFor(domain, claim, sourceIds) {
|
|
7568
|
+
return stableId("INS", { domain: domain.trim(), claim: claim.trim(), sourceIds: [...sourceIds].sort() });
|
|
7231
7569
|
}
|
|
7232
|
-
function
|
|
7233
|
-
return
|
|
7570
|
+
function domainPlanIdFor(domain, insightIds) {
|
|
7571
|
+
return stableId("DOM", { domain: domain.trim(), insightIds: [...insightIds].sort() });
|
|
7234
7572
|
}
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7573
|
+
|
|
7574
|
+
// src/insights/inventory.ts
|
|
7575
|
+
function compareStrings(a, b) {
|
|
7576
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
7238
7577
|
}
|
|
7239
|
-
function
|
|
7240
|
-
|
|
7578
|
+
function declaredMapCoverage(path, claim) {
|
|
7579
|
+
if (!path) return "not-requested";
|
|
7580
|
+
return claim ?? "unknown";
|
|
7241
7581
|
}
|
|
7242
|
-
function
|
|
7243
|
-
|
|
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
|
+
}
|
|
7244
7588
|
}
|
|
7245
|
-
function
|
|
7246
|
-
const
|
|
7247
|
-
const
|
|
7248
|
-
const
|
|
7249
|
-
|
|
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 };
|
|
7250
7609
|
}
|
|
7251
|
-
function
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
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
|
+
});
|
|
7258
7623
|
}
|
|
7259
|
-
function
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
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
|
+
};
|
|
7265
7635
|
}
|
|
7266
|
-
function
|
|
7267
|
-
const
|
|
7268
|
-
|
|
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
|
+
});
|
|
7269
7672
|
}
|
|
7270
|
-
function
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
const tokensB = tokenize3(b);
|
|
7274
|
-
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
7275
|
-
let intersectionSize = 0;
|
|
7276
|
-
for (const token of tokensA) {
|
|
7277
|
-
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`);
|
|
7278
7676
|
}
|
|
7279
|
-
const
|
|
7280
|
-
|
|
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
|
+
};
|
|
7281
7731
|
}
|
|
7282
7732
|
|
|
7283
|
-
// src/
|
|
7284
|
-
import {
|
|
7285
|
-
import {
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
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;
|
|
7294
7794
|
}
|
|
7295
|
-
function
|
|
7296
|
-
|
|
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
|
+
}
|
|
7297
7802
|
}
|
|
7298
|
-
function
|
|
7299
|
-
return
|
|
7803
|
+
function compareStrings2(a, b) {
|
|
7804
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
7300
7805
|
}
|
|
7301
|
-
function
|
|
7302
|
-
let
|
|
7806
|
+
function canonicalPublicUrl(raw, label2) {
|
|
7807
|
+
let url;
|
|
7303
7808
|
try {
|
|
7304
|
-
|
|
7305
|
-
} catch
|
|
7306
|
-
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);
|
|
7307
7886
|
}
|
|
7887
|
+
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
7888
|
+
let realPath;
|
|
7308
7889
|
try {
|
|
7309
|
-
|
|
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
|
+
}
|
|
7310
7895
|
} catch {
|
|
7311
|
-
throw new UserError(`
|
|
7312
|
-
}
|
|
7313
|
-
}
|
|
7314
|
-
function readStore(path, label, validateRecord) {
|
|
7315
|
-
if (!existsSync25(path)) {
|
|
7316
|
-
return { schemaVersion: 1, records: [] };
|
|
7317
|
-
}
|
|
7318
|
-
const parsed = readJsonFile(path, label);
|
|
7319
|
-
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
|
|
7320
|
-
throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
|
|
7321
|
-
}
|
|
7322
|
-
const records = parsed.records;
|
|
7323
|
-
if (validateRecord) {
|
|
7324
|
-
records.forEach((record, index) => validateRecord(record, index));
|
|
7325
|
-
}
|
|
7326
|
-
return { schemaVersion: 1, records };
|
|
7327
|
-
}
|
|
7328
|
-
function validateRefinementRecordShape(record, index, path) {
|
|
7329
|
-
if (typeof record !== "object" || record === null) {
|
|
7330
|
-
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
|
|
7331
|
-
}
|
|
7332
|
-
const r = record;
|
|
7333
|
-
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
7334
|
-
throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
|
|
7335
|
-
}
|
|
7336
|
-
if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
|
|
7337
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
|
|
7338
|
-
}
|
|
7339
|
-
if (typeof r.status !== "string" || r.status.length === 0) {
|
|
7340
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
|
|
7896
|
+
throw new UserError(`source file changed while it was being observed: ${relativePath}`);
|
|
7341
7897
|
}
|
|
7342
|
-
if (
|
|
7343
|
-
|
|
7344
|
-
}
|
|
7345
|
-
const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
|
|
7346
|
-
if (r.overrideType === "patch") {
|
|
7347
|
-
if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
|
|
7348
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
|
|
7349
|
-
}
|
|
7350
|
-
if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
|
|
7351
|
-
throw new UserError(
|
|
7352
|
-
`malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
|
|
7353
|
-
);
|
|
7354
|
-
}
|
|
7355
|
-
if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
|
|
7356
|
-
throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
|
|
7357
|
-
}
|
|
7358
|
-
} else if (r.patchAction !== void 0) {
|
|
7359
|
-
throw new UserError(
|
|
7360
|
-
`malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
|
|
7361
|
-
);
|
|
7898
|
+
if (mediaType !== "application/pdf" && bytes.subarray(0, 512).includes(0)) {
|
|
7899
|
+
return { path: relativePath, reason: "binary", byteLength: bytes.byteLength };
|
|
7362
7900
|
}
|
|
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
|
+
};
|
|
7363
7910
|
}
|
|
7364
|
-
function
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
}
|
|
7378
|
-
if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
|
|
7379
|
-
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
|
|
7380
|
-
}
|
|
7381
|
-
if (p.status === "generalized") {
|
|
7382
|
-
if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
|
|
7383
|
-
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
|
|
7384
|
-
}
|
|
7385
|
-
if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
|
|
7386
|
-
throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
|
|
7387
|
-
}
|
|
7388
|
-
}
|
|
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
|
+
};
|
|
7389
7925
|
}
|
|
7390
|
-
function
|
|
7391
|
-
|
|
7392
|
-
|
|
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
|
+
};
|
|
7393
7953
|
}
|
|
7394
|
-
function
|
|
7395
|
-
|
|
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));
|
|
10161
|
+
}
|
|
10162
|
+
function writeRefinementsStore(store) {
|
|
10163
|
+
writeJsonAtomic(getRefinementsPath(), store);
|
|
7396
10164
|
}
|
|
7397
10165
|
function readPatternsStore() {
|
|
7398
10166
|
const path = getPatternsPath();
|
|
@@ -7402,9 +10170,9 @@ function writePatternsStore(store) {
|
|
|
7402
10170
|
writeJsonAtomic(getPatternsPath(), store);
|
|
7403
10171
|
}
|
|
7404
10172
|
function resolveProjectRealpath(projectRoot) {
|
|
7405
|
-
const resolved =
|
|
10173
|
+
const resolved = resolvePath11(projectRoot);
|
|
7406
10174
|
try {
|
|
7407
|
-
return
|
|
10175
|
+
return realpathSync8(resolved);
|
|
7408
10176
|
} catch {
|
|
7409
10177
|
return resolved;
|
|
7410
10178
|
}
|
|
@@ -7536,16 +10304,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
7536
10304
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
7537
10305
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
7538
10306
|
const files = filePaths.map((path) => {
|
|
7539
|
-
if (!
|
|
10307
|
+
if (!existsSync27(path)) {
|
|
7540
10308
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
7541
10309
|
}
|
|
7542
|
-
const bytes =
|
|
7543
|
-
const mode =
|
|
7544
|
-
const sha2562 =
|
|
10310
|
+
const bytes = readFileSync23(path);
|
|
10311
|
+
const mode = statSync12(path).mode & 511;
|
|
10312
|
+
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
7545
10313
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
7546
10314
|
});
|
|
7547
10315
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
7548
|
-
writeJsonAtomic(
|
|
10316
|
+
writeJsonAtomic(join33(getBackupsDir(), id, "manifest.json"), manifest);
|
|
7549
10317
|
return manifest;
|
|
7550
10318
|
}
|
|
7551
10319
|
function validateBackupManifestShape(value, path) {
|
|
@@ -7594,8 +10362,8 @@ function readBackupManifest(id) {
|
|
|
7594
10362
|
if (!isValidBackupId(id)) {
|
|
7595
10363
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
7596
10364
|
}
|
|
7597
|
-
const manifestPath =
|
|
7598
|
-
if (!
|
|
10365
|
+
const manifestPath = join33(getBackupsDir(), id, "manifest.json");
|
|
10366
|
+
if (!existsSync27(manifestPath)) {
|
|
7599
10367
|
throw new UserError(`no backup with id: ${id}`);
|
|
7600
10368
|
}
|
|
7601
10369
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -7605,7 +10373,7 @@ function readBackupManifest(id) {
|
|
|
7605
10373
|
|
|
7606
10374
|
// src/refine/context.ts
|
|
7607
10375
|
function resolveConfiguredRoot2(root) {
|
|
7608
|
-
return
|
|
10376
|
+
return resolvePath12(expandHome(root));
|
|
7609
10377
|
}
|
|
7610
10378
|
function assertPlainSkillName(name) {
|
|
7611
10379
|
if (!name || name.trim().length === 0) {
|
|
@@ -7617,32 +10385,32 @@ function assertPlainSkillName(name) {
|
|
|
7617
10385
|
}
|
|
7618
10386
|
function realpathOrNull2(p) {
|
|
7619
10387
|
try {
|
|
7620
|
-
return
|
|
10388
|
+
return realpathSync9(p);
|
|
7621
10389
|
} catch {
|
|
7622
10390
|
return null;
|
|
7623
10391
|
}
|
|
7624
10392
|
}
|
|
7625
10393
|
function isUnderRoot2(candidate, root) {
|
|
7626
|
-
const rel =
|
|
7627
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
10394
|
+
const rel = relative6(root, candidate);
|
|
10395
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
7628
10396
|
}
|
|
7629
10397
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
7630
10398
|
assertPlainSkillName(skill);
|
|
7631
|
-
const resolvedCwd =
|
|
7632
|
-
const localDir =
|
|
7633
|
-
const sharedDir =
|
|
7634
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
7635
|
-
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) };
|
|
7636
10404
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
7637
10405
|
return { local, shared, user };
|
|
7638
10406
|
}
|
|
7639
10407
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
7640
10408
|
if (userRootFlag) {
|
|
7641
|
-
const root =
|
|
7642
|
-
return { resolved: true, dir:
|
|
10409
|
+
const root = resolvePath12(userRootFlag);
|
|
10410
|
+
return { resolved: true, dir: join34(root, skill), root: userRootFlag, reason: "flag" };
|
|
7643
10411
|
}
|
|
7644
10412
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
7645
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
10413
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join34(resolveConfiguredRoot2(root), skill) }));
|
|
7646
10414
|
for (const candidate of candidates) {
|
|
7647
10415
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
7648
10416
|
if (skillReal) {
|
|
@@ -7660,14 +10428,14 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
|
7660
10428
|
}
|
|
7661
10429
|
function isFile2(p) {
|
|
7662
10430
|
try {
|
|
7663
|
-
return
|
|
10431
|
+
return statSync13(p).isFile();
|
|
7664
10432
|
} catch {
|
|
7665
10433
|
return false;
|
|
7666
10434
|
}
|
|
7667
10435
|
}
|
|
7668
10436
|
function isDirectory(p) {
|
|
7669
10437
|
try {
|
|
7670
|
-
return
|
|
10438
|
+
return statSync13(p).isDirectory();
|
|
7671
10439
|
} catch {
|
|
7672
10440
|
return false;
|
|
7673
10441
|
}
|
|
@@ -7677,12 +10445,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
7677
10445
|
scope,
|
|
7678
10446
|
dir,
|
|
7679
10447
|
dirExists: isDirectory(dir),
|
|
7680
|
-
skillMd: isFile2(
|
|
7681
|
-
patchMd: isFile2(
|
|
7682
|
-
extendMd: isFile2(
|
|
7683
|
-
configJson: isFile2(
|
|
7684
|
-
hooksDir: isDirectory(
|
|
7685
|
-
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"))
|
|
7686
10454
|
};
|
|
7687
10455
|
}
|
|
7688
10456
|
function inventoryAllScopes(resolution) {
|
|
@@ -7915,10 +10683,10 @@ function previewDiff(before, after, path) {
|
|
|
7915
10683
|
}
|
|
7916
10684
|
|
|
7917
10685
|
// src/commands/refine.ts
|
|
7918
|
-
var
|
|
10686
|
+
var PRO_FEATURE_LABEL9 = "skill refinement";
|
|
7919
10687
|
var REFINE_PROMPT_ASSET = "refine-prompt.md";
|
|
7920
|
-
function
|
|
7921
|
-
const status2 = proFeatureStatus(
|
|
10688
|
+
function requireProFeature3() {
|
|
10689
|
+
const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
|
|
7922
10690
|
if (!status2.enabled) {
|
|
7923
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.");
|
|
7924
10692
|
}
|
|
@@ -7927,7 +10695,7 @@ function requireProFeature2() {
|
|
|
7927
10695
|
function isInteractive(json) {
|
|
7928
10696
|
return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
7929
10697
|
}
|
|
7930
|
-
function
|
|
10698
|
+
function emit2(json, payload, renderText) {
|
|
7931
10699
|
if (json) {
|
|
7932
10700
|
console.log(JSON.stringify(payload, null, 2));
|
|
7933
10701
|
} else {
|
|
@@ -7943,7 +10711,7 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
7943
10711
|
if (!isInteractive(json)) {
|
|
7944
10712
|
throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
|
|
7945
10713
|
}
|
|
7946
|
-
const rl =
|
|
10714
|
+
const rl = createInterface7({ input: process.stdin, output: process.stdout });
|
|
7947
10715
|
try {
|
|
7948
10716
|
return await confirmYesNo(rl, prompt);
|
|
7949
10717
|
} finally {
|
|
@@ -7951,18 +10719,18 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
7951
10719
|
}
|
|
7952
10720
|
}
|
|
7953
10721
|
function resolveConfiguredRoot3(root) {
|
|
7954
|
-
return
|
|
10722
|
+
return resolvePath13(expandHome(root));
|
|
7955
10723
|
}
|
|
7956
10724
|
function isUnderRoot3(candidate, root) {
|
|
7957
|
-
const rel =
|
|
7958
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
10725
|
+
const rel = relative7(root, candidate);
|
|
10726
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
|
|
7959
10727
|
}
|
|
7960
10728
|
function sanitizeDiffForConsole(diff) {
|
|
7961
10729
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
7962
10730
|
}
|
|
7963
10731
|
function resolveContentOption(options) {
|
|
7964
10732
|
if (options.content !== void 0) return options.content;
|
|
7965
|
-
if (options.contentFile) return
|
|
10733
|
+
if (options.contentFile) return readFileSync24(resolvePath13(options.contentFile), "utf-8");
|
|
7966
10734
|
return void 0;
|
|
7967
10735
|
}
|
|
7968
10736
|
function validateCaptureOptions(options) {
|
|
@@ -8088,19 +10856,19 @@ ${rendered}` : rendered;
|
|
|
8088
10856
|
function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
8089
10857
|
let lst;
|
|
8090
10858
|
try {
|
|
8091
|
-
lst =
|
|
10859
|
+
lst = lstatSync7(scopeDir);
|
|
8092
10860
|
} catch {
|
|
8093
10861
|
lst = void 0;
|
|
8094
10862
|
}
|
|
8095
10863
|
if (lst?.isSymbolicLink()) {
|
|
8096
10864
|
throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
|
|
8097
10865
|
}
|
|
8098
|
-
|
|
10866
|
+
mkdirSync11(scopeDir, { recursive: true });
|
|
8099
10867
|
const real = resolveConfiguredRoot3(scopeDir);
|
|
8100
|
-
const lexical =
|
|
10868
|
+
const lexical = resolvePath13(scopeDir);
|
|
8101
10869
|
let realResolved;
|
|
8102
10870
|
try {
|
|
8103
|
-
realResolved =
|
|
10871
|
+
realResolved = realpathSync10(scopeDir);
|
|
8104
10872
|
} catch {
|
|
8105
10873
|
realResolved = real;
|
|
8106
10874
|
}
|
|
@@ -8114,28 +10882,28 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
8114
10882
|
}
|
|
8115
10883
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
8116
10884
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
8117
|
-
const destPath =
|
|
8118
|
-
const destDir =
|
|
8119
|
-
|
|
8120
|
-
const realDestDir =
|
|
10885
|
+
const destPath = join35(scopeDir, relPath);
|
|
10886
|
+
const destDir = dirname15(destPath);
|
|
10887
|
+
mkdirSync11(destDir, { recursive: true });
|
|
10888
|
+
const realDestDir = realpathSync10(destDir);
|
|
8121
10889
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
8122
10890
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
8123
10891
|
}
|
|
8124
10892
|
let destLst;
|
|
8125
10893
|
try {
|
|
8126
|
-
destLst =
|
|
10894
|
+
destLst = lstatSync7(destPath);
|
|
8127
10895
|
} catch {
|
|
8128
10896
|
destLst = void 0;
|
|
8129
10897
|
}
|
|
8130
10898
|
if (destLst?.isSymbolicLink()) {
|
|
8131
10899
|
throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
|
|
8132
10900
|
}
|
|
8133
|
-
|
|
8134
|
-
|
|
10901
|
+
writeFileSync10(destPath, content, { mode: 384 });
|
|
10902
|
+
chmodSync4(destPath, 384);
|
|
8135
10903
|
return destPath;
|
|
8136
10904
|
}
|
|
8137
10905
|
async function refineCaptureCommand(rawOptions = {}) {
|
|
8138
|
-
|
|
10906
|
+
requireProFeature3();
|
|
8139
10907
|
const opts = validateCaptureOptions(rawOptions);
|
|
8140
10908
|
const config = loadConfig();
|
|
8141
10909
|
const cwd = process.cwd();
|
|
@@ -8147,7 +10915,7 @@ async function refineCaptureCommand(rawOptions = {}) {
|
|
|
8147
10915
|
const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
|
|
8148
10916
|
const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
|
|
8149
10917
|
if (opts.dryRun) {
|
|
8150
|
-
|
|
10918
|
+
emit2(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
|
|
8151
10919
|
console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
|
|
8152
10920
|
console.log(sanitizeDiffForConsole(preview.diff));
|
|
8153
10921
|
});
|
|
@@ -8160,7 +10928,7 @@ async function refineCaptureCommand(rawOptions = {}) {
|
|
|
8160
10928
|
const proceed = await confirmWrite(opts.yes, opts.json, `
|
|
8161
10929
|
Write ${preview.destPath}? [y/N] `);
|
|
8162
10930
|
if (!proceed) {
|
|
8163
|
-
|
|
10931
|
+
emit2(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
|
|
8164
10932
|
console.log("[skill-forge] capture cancelled \u2014 nothing written.");
|
|
8165
10933
|
});
|
|
8166
10934
|
return;
|
|
@@ -8193,7 +10961,7 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
8193
10961
|
} catch (err) {
|
|
8194
10962
|
throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
|
|
8195
10963
|
}
|
|
8196
|
-
|
|
10964
|
+
emit2(
|
|
8197
10965
|
opts.json,
|
|
8198
10966
|
{ written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
|
|
8199
10967
|
() => {
|
|
@@ -8210,21 +10978,21 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
8210
10978
|
}
|
|
8211
10979
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
8212
10980
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
8213
|
-
const probePath = probeRelPath ?
|
|
8214
|
-
const existingContent = probePath &&
|
|
10981
|
+
const probePath = probeRelPath ? join35(scopeDir, probeRelPath) : void 0;
|
|
10982
|
+
const existingContent = probePath && existsSync29(probePath) ? readFileSync24(probePath, "utf-8") : "";
|
|
8215
10983
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
8216
10984
|
const relPath = rendered.relPath;
|
|
8217
|
-
const destPath =
|
|
8218
|
-
const before =
|
|
10985
|
+
const destPath = join35(scopeDir, relPath);
|
|
10986
|
+
const before = existsSync29(destPath) ? readFileSync24(destPath, "utf-8") : "";
|
|
8219
10987
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
8220
10988
|
return { relPath, content: rendered.content, destPath, diff };
|
|
8221
10989
|
}
|
|
8222
10990
|
async function refineListCommand(options = {}) {
|
|
8223
|
-
|
|
10991
|
+
requireProFeature3();
|
|
8224
10992
|
const { records } = readRefinementsStore();
|
|
8225
10993
|
let rows = records;
|
|
8226
10994
|
if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
|
|
8227
|
-
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));
|
|
8228
10996
|
if (options.status) {
|
|
8229
10997
|
if (!isRefinementStatus(options.status)) {
|
|
8230
10998
|
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
@@ -8232,7 +11000,7 @@ async function refineListCommand(options = {}) {
|
|
|
8232
11000
|
rows = rows.filter((r) => r.status === options.status);
|
|
8233
11001
|
}
|
|
8234
11002
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
8235
|
-
|
|
11003
|
+
emit2(options.json, { records: rows }, () => {
|
|
8236
11004
|
if (rows.length === 0) {
|
|
8237
11005
|
if (filtered && records.length > 0) {
|
|
8238
11006
|
console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
|
|
@@ -8250,7 +11018,7 @@ async function refineListCommand(options = {}) {
|
|
|
8250
11018
|
});
|
|
8251
11019
|
}
|
|
8252
11020
|
async function refinePatternsCommand(options = {}) {
|
|
8253
|
-
|
|
11021
|
+
requireProFeature3();
|
|
8254
11022
|
const { records } = readPatternsStore();
|
|
8255
11023
|
let rows = records;
|
|
8256
11024
|
if (options.status) {
|
|
@@ -8261,11 +11029,11 @@ async function refinePatternsCommand(options = {}) {
|
|
|
8261
11029
|
}
|
|
8262
11030
|
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
8263
11031
|
if (options.project) {
|
|
8264
|
-
const resolvedProject =
|
|
11032
|
+
const resolvedProject = resolvePath13(options.project);
|
|
8265
11033
|
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
8266
11034
|
}
|
|
8267
11035
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
8268
|
-
|
|
11036
|
+
emit2(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
|
|
8269
11037
|
if (rows.length === 0) {
|
|
8270
11038
|
if (filtered && records.length > 0) {
|
|
8271
11039
|
console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
|
|
@@ -8294,13 +11062,13 @@ function renderInventoryLine(row) {
|
|
|
8294
11062
|
return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
|
|
8295
11063
|
}
|
|
8296
11064
|
async function refineWhichCommand(skill, options = {}) {
|
|
8297
|
-
|
|
11065
|
+
requireProFeature3();
|
|
8298
11066
|
if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
|
|
8299
11067
|
const config = loadConfig();
|
|
8300
11068
|
const cwd = process.cwd();
|
|
8301
11069
|
const scopes = resolveScopes(config, cwd, skill, options.userRoot);
|
|
8302
11070
|
const inventory = inventoryAllScopes(scopes);
|
|
8303
|
-
|
|
11071
|
+
emit2(options.json, { scopes, inventory }, () => {
|
|
8304
11072
|
console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
|
|
8305
11073
|
for (const row of inventory) console.log(renderInventoryLine(row));
|
|
8306
11074
|
if (!scopes.user.resolved) {
|
|
@@ -8312,16 +11080,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
8312
11080
|
}
|
|
8313
11081
|
function writePostWriteSidecar(backupId, files) {
|
|
8314
11082
|
const sidecar = { schemaVersion: 1, files };
|
|
8315
|
-
const dir =
|
|
8316
|
-
|
|
8317
|
-
|
|
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 });
|
|
8318
11086
|
}
|
|
8319
11087
|
function readPostWriteSidecar(backupId) {
|
|
8320
|
-
const path =
|
|
8321
|
-
if (!
|
|
11088
|
+
const path = join35(getBackupsDir(), backupId, "post.json");
|
|
11089
|
+
if (!existsSync29(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
8322
11090
|
let raw;
|
|
8323
11091
|
try {
|
|
8324
|
-
raw =
|
|
11092
|
+
raw = readFileSync24(path, "utf-8");
|
|
8325
11093
|
} catch {
|
|
8326
11094
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
8327
11095
|
}
|
|
@@ -8340,7 +11108,7 @@ function readPostWriteSidecar(backupId) {
|
|
|
8340
11108
|
return { hashes, status: "ok" };
|
|
8341
11109
|
}
|
|
8342
11110
|
async function refinePromoteCommand(patternId, options = {}) {
|
|
8343
|
-
|
|
11111
|
+
requireProFeature3();
|
|
8344
11112
|
if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
|
|
8345
11113
|
const patternsStore = readPatternsStore();
|
|
8346
11114
|
const pattern = patternsStore.records.find((p) => p.id === patternId);
|
|
@@ -8366,21 +11134,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8366
11134
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
8367
11135
|
let realUserRoot;
|
|
8368
11136
|
try {
|
|
8369
|
-
realUserRoot =
|
|
11137
|
+
realUserRoot = realpathSync10(resolvedUserRoot);
|
|
8370
11138
|
} catch {
|
|
8371
11139
|
realUserRoot = resolvedUserRoot;
|
|
8372
11140
|
}
|
|
8373
11141
|
let realSkillDir;
|
|
8374
11142
|
try {
|
|
8375
|
-
realSkillDir =
|
|
11143
|
+
realSkillDir = realpathSync10(skillDir);
|
|
8376
11144
|
} catch {
|
|
8377
|
-
realSkillDir =
|
|
11145
|
+
realSkillDir = resolvePath13(skillDir);
|
|
8378
11146
|
}
|
|
8379
11147
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
8380
11148
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
8381
11149
|
}
|
|
8382
|
-
const skillMdPath =
|
|
8383
|
-
if (!
|
|
11150
|
+
const skillMdPath = join35(skillDir, "SKILL.md");
|
|
11151
|
+
if (!existsSync29(skillMdPath)) {
|
|
8384
11152
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
8385
11153
|
}
|
|
8386
11154
|
const refinementsStore = readRefinementsStore();
|
|
@@ -8397,11 +11165,11 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8397
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.`
|
|
8398
11166
|
);
|
|
8399
11167
|
}
|
|
8400
|
-
const before =
|
|
11168
|
+
const before = readFileSync24(skillMdPath, "utf-8");
|
|
8401
11169
|
const after = applyPatchBlocks(before, [block]);
|
|
8402
11170
|
const diff = previewDiff(before, after, skillMdPath);
|
|
8403
11171
|
if (options.dryRun) {
|
|
8404
|
-
|
|
11172
|
+
emit2(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
|
|
8405
11173
|
console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
|
|
8406
11174
|
console.log(sanitizeDiffForConsole(diff));
|
|
8407
11175
|
});
|
|
@@ -8414,7 +11182,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
8414
11182
|
const proceed = await confirmWrite(options.yes, options.json, `
|
|
8415
11183
|
Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
8416
11184
|
if (!proceed) {
|
|
8417
|
-
|
|
11185
|
+
emit2(options.json, { promoted: false, cancelled: true }, () => {
|
|
8418
11186
|
console.log("[skill-forge] promote cancelled \u2014 nothing written.");
|
|
8419
11187
|
});
|
|
8420
11188
|
return;
|
|
@@ -8422,14 +11190,14 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
8422
11190
|
const backup = createBackup([skillMdPath]);
|
|
8423
11191
|
const tempPath = `${skillMdPath}.tmp-${randomUUID5()}`;
|
|
8424
11192
|
try {
|
|
8425
|
-
|
|
11193
|
+
writeFileSync10(tempPath, after, "utf-8");
|
|
8426
11194
|
renameSync6(tempPath, skillMdPath);
|
|
8427
11195
|
} catch (err) {
|
|
8428
11196
|
rmSync9(tempPath, { force: true });
|
|
8429
11197
|
throw err;
|
|
8430
11198
|
}
|
|
8431
11199
|
try {
|
|
8432
|
-
const postSha256 =
|
|
11200
|
+
const postSha256 = createHash8("sha256").update(readFileSync24(skillMdPath)).digest("hex");
|
|
8433
11201
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
8434
11202
|
markGeneralized(patternId, backup.id);
|
|
8435
11203
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -8449,31 +11217,31 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
8449
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}`
|
|
8450
11218
|
);
|
|
8451
11219
|
}
|
|
8452
|
-
|
|
11220
|
+
emit2(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
|
|
8453
11221
|
console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
|
|
8454
11222
|
});
|
|
8455
11223
|
}
|
|
8456
11224
|
function assertBackupDirContained(backupId) {
|
|
8457
11225
|
const backupsDir = getBackupsDir();
|
|
8458
|
-
if (!
|
|
8459
|
-
const realBackupsDir =
|
|
8460
|
-
const backupDir =
|
|
8461
|
-
if (!
|
|
8462
|
-
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);
|
|
8463
11231
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
8464
11232
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
8465
11233
|
}
|
|
8466
11234
|
}
|
|
8467
11235
|
function resolveTargetRealPath(path) {
|
|
8468
|
-
let lexicalAncestor =
|
|
11236
|
+
let lexicalAncestor = resolvePath13(path);
|
|
8469
11237
|
while (true) {
|
|
8470
11238
|
try {
|
|
8471
|
-
const real =
|
|
8472
|
-
const remainder =
|
|
8473
|
-
return remainder === "" ? real :
|
|
11239
|
+
const real = realpathSync10(lexicalAncestor);
|
|
11240
|
+
const remainder = relative7(lexicalAncestor, resolvePath13(path));
|
|
11241
|
+
return remainder === "" ? real : join35(real, remainder);
|
|
8474
11242
|
} catch {
|
|
8475
|
-
const parent =
|
|
8476
|
-
if (parent === lexicalAncestor) return
|
|
11243
|
+
const parent = dirname15(lexicalAncestor);
|
|
11244
|
+
if (parent === lexicalAncestor) return resolvePath13(path);
|
|
8477
11245
|
lexicalAncestor = parent;
|
|
8478
11246
|
}
|
|
8479
11247
|
}
|
|
@@ -8485,7 +11253,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
8485
11253
|
const roots = config.skillsRoots.map((root) => {
|
|
8486
11254
|
const resolved = resolveConfiguredRoot3(root);
|
|
8487
11255
|
try {
|
|
8488
|
-
return
|
|
11256
|
+
return realpathSync10(resolved);
|
|
8489
11257
|
} catch {
|
|
8490
11258
|
return resolved;
|
|
8491
11259
|
}
|
|
@@ -8502,7 +11270,7 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
8502
11270
|
for (const file of manifest.files) {
|
|
8503
11271
|
let lst;
|
|
8504
11272
|
try {
|
|
8505
|
-
lst =
|
|
11273
|
+
lst = lstatSync7(file.path);
|
|
8506
11274
|
} catch {
|
|
8507
11275
|
lst = void 0;
|
|
8508
11276
|
}
|
|
@@ -8512,14 +11280,14 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
8512
11280
|
}
|
|
8513
11281
|
}
|
|
8514
11282
|
function findLexicalRoot(filePath, config) {
|
|
8515
|
-
const lexicalTarget =
|
|
11283
|
+
const lexicalTarget = resolvePath13(filePath);
|
|
8516
11284
|
for (const root of config.skillsRoots) {
|
|
8517
11285
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
8518
|
-
const rel =
|
|
8519
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
11286
|
+
const rel = relative7(rootLexical, lexicalTarget);
|
|
11287
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute8(rel))) continue;
|
|
8520
11288
|
let rootReal;
|
|
8521
11289
|
try {
|
|
8522
|
-
rootReal =
|
|
11290
|
+
rootReal = realpathSync10(rootLexical);
|
|
8523
11291
|
} catch {
|
|
8524
11292
|
rootReal = rootLexical;
|
|
8525
11293
|
}
|
|
@@ -8529,8 +11297,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
8529
11297
|
}
|
|
8530
11298
|
function walkRealComponents(filePath, config, createMissing) {
|
|
8531
11299
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
8532
|
-
const relPath =
|
|
8533
|
-
const components = relPath.split(
|
|
11300
|
+
const relPath = relative7(rootLexical, resolvePath13(filePath));
|
|
11301
|
+
const components = relPath.split(sep6).filter((c) => c.length > 0);
|
|
8534
11302
|
if (components.length === 0) {
|
|
8535
11303
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
8536
11304
|
}
|
|
@@ -8539,10 +11307,10 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
8539
11307
|
let current = rootReal;
|
|
8540
11308
|
for (const component of dirComponents) {
|
|
8541
11309
|
if (current === void 0) break;
|
|
8542
|
-
const next =
|
|
11310
|
+
const next = join35(current, component);
|
|
8543
11311
|
let lst;
|
|
8544
11312
|
try {
|
|
8545
|
-
lst =
|
|
11313
|
+
lst = lstatSync7(next);
|
|
8546
11314
|
} catch {
|
|
8547
11315
|
lst = void 0;
|
|
8548
11316
|
}
|
|
@@ -8551,7 +11319,7 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
8551
11319
|
current = void 0;
|
|
8552
11320
|
continue;
|
|
8553
11321
|
}
|
|
8554
|
-
|
|
11322
|
+
mkdirSync11(next);
|
|
8555
11323
|
current = next;
|
|
8556
11324
|
continue;
|
|
8557
11325
|
}
|
|
@@ -8570,13 +11338,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8570
11338
|
if (parentReal === void 0) {
|
|
8571
11339
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
8572
11340
|
}
|
|
8573
|
-
const finalPath =
|
|
11341
|
+
const finalPath = join35(parentReal, leafComponent);
|
|
8574
11342
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
8575
11343
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
8576
11344
|
}
|
|
8577
11345
|
let leafLst;
|
|
8578
11346
|
try {
|
|
8579
|
-
leafLst =
|
|
11347
|
+
leafLst = lstatSync7(finalPath);
|
|
8580
11348
|
} catch {
|
|
8581
11349
|
leafLst = void 0;
|
|
8582
11350
|
}
|
|
@@ -8585,8 +11353,8 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8585
11353
|
}
|
|
8586
11354
|
const tempPath = `${finalPath}.tmp-${randomUUID5()}`;
|
|
8587
11355
|
try {
|
|
8588
|
-
|
|
8589
|
-
|
|
11356
|
+
writeFileSync10(tempPath, contentBuffer);
|
|
11357
|
+
chmodSync4(tempPath, mode);
|
|
8590
11358
|
renameSync6(tempPath, finalPath);
|
|
8591
11359
|
} catch (err) {
|
|
8592
11360
|
rmSync9(tempPath, { force: true });
|
|
@@ -8596,13 +11364,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
8596
11364
|
function safeDeleteTombstone(filePath, config) {
|
|
8597
11365
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
8598
11366
|
if (parentReal === void 0) return;
|
|
8599
|
-
const finalPath =
|
|
11367
|
+
const finalPath = join35(parentReal, leafComponent);
|
|
8600
11368
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
8601
11369
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
8602
11370
|
}
|
|
8603
11371
|
let leafLst;
|
|
8604
11372
|
try {
|
|
8605
|
-
leafLst =
|
|
11373
|
+
leafLst = lstatSync7(finalPath);
|
|
8606
11374
|
} catch {
|
|
8607
11375
|
leafLst = void 0;
|
|
8608
11376
|
}
|
|
@@ -8613,7 +11381,7 @@ function safeDeleteTombstone(filePath, config) {
|
|
|
8613
11381
|
rmSync9(finalPath, { force: true });
|
|
8614
11382
|
}
|
|
8615
11383
|
async function refineRollbackCommand(backupId, options = {}) {
|
|
8616
|
-
|
|
11384
|
+
requireProFeature3();
|
|
8617
11385
|
if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
|
|
8618
11386
|
if (!isValidBackupId(backupId)) {
|
|
8619
11387
|
throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
@@ -8638,7 +11406,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
8638
11406
|
for (const file of manifest.files) {
|
|
8639
11407
|
const expected = sidecar.hashes.get(file.path);
|
|
8640
11408
|
if (expected === void 0) continue;
|
|
8641
|
-
const currentSha256 =
|
|
11409
|
+
const currentSha256 = existsSync29(file.path) ? createHash8("sha256").update(readFileSync24(file.path)).digest("hex") : null;
|
|
8642
11410
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
8643
11411
|
}
|
|
8644
11412
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -8657,7 +11425,7 @@ Roll back backup ${backupId}? Affected files:
|
|
|
8657
11425
|
${fileList}
|
|
8658
11426
|
[y/N] `);
|
|
8659
11427
|
if (!proceed) {
|
|
8660
|
-
|
|
11428
|
+
emit2(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
|
|
8661
11429
|
console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
|
|
8662
11430
|
});
|
|
8663
11431
|
return;
|
|
@@ -8686,7 +11454,7 @@ ${fileList}
|
|
|
8686
11454
|
pattern.generalizedBackupId = void 0;
|
|
8687
11455
|
writePatternsStore(patternsStore);
|
|
8688
11456
|
}
|
|
8689
|
-
|
|
11457
|
+
emit2(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
|
|
8690
11458
|
console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
|
|
8691
11459
|
for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
|
|
8692
11460
|
});
|
|
@@ -8729,6 +11497,27 @@ function parsePruneDays(value) {
|
|
|
8729
11497
|
}
|
|
8730
11498
|
return n;
|
|
8731
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
|
+
}
|
|
8732
11521
|
var program = new Command();
|
|
8733
11522
|
program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
|
|
8734
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(
|
|
@@ -8772,6 +11561,14 @@ program.command("ingest").description(
|
|
|
8772
11561
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|
|
8773
11562
|
var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
|
|
8774
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);
|
|
8775
11572
|
var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
|
|
8776
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);
|
|
8777
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);
|