vigiles 33.2.0 → 33.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/adapter.d.ts +1 -1
- package/dist/adapters/claude-code/runtime.js +70 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +42 -21
- package/dist/adapters/claude-code/skill-reachability.js +91 -28
- package/dist/cli-main.js +224 -17
- package/dist/core/adapter.d.ts +7 -0
- package/dist/core/install-reader.js +15 -0
- package/dist/core/layout.d.ts +10 -0
- package/dist/core/layout.js +17 -0
- package/dist/core/run-env.d.ts +50 -0
- package/dist/core/run-env.js +51 -0
- package/dist/core/runtime.d.ts +44 -0
- package/dist/core/skill-link-target.d.ts +20 -0
- package/dist/core/skill-link-target.js +33 -0
- package/dist/eval.d.ts +39 -39
- package/dist/eval.js +62 -115
- package/dist/harness-test.d.ts +35 -0
- package/dist/harness-test.js +49 -7
- package/dist/output-style-arms.js +7 -2
- package/dist/plugin-declaration.d.ts +6 -6
- package/dist/plugin-declaration.js +6 -6
- package/dist/setup-plan.d.ts +49 -9
- package/dist/setup-plan.js +102 -68
- package/dist/skill-links.d.ts +183 -0
- package/dist/skill-links.js +255 -0
- package/dist/test-coverage.js +37 -1
- package/package.json +1 -1
package/dist/cli-main.js
CHANGED
|
@@ -39,11 +39,13 @@ const frame_js_1 = require("./core/frame.js");
|
|
|
39
39
|
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
40
40
|
const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
41
41
|
const install_reader_js_1 = require("./core/install-reader.js");
|
|
42
|
+
const skill_links_js_1 = require("./skill-links.js");
|
|
42
43
|
const plugin_declaration_js_1 = require("./plugin-declaration.js");
|
|
43
44
|
const local_files_tracked_js_1 = require("./local-files-tracked.js");
|
|
44
45
|
const local_files_js_1 = require("./local-files.js");
|
|
45
46
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
46
47
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
48
|
+
const layout_js_1 = require("./core/layout.js");
|
|
47
49
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
48
50
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
49
51
|
const optimize_js_1 = require("./optimize.js");
|
|
@@ -3177,29 +3179,226 @@ function reportInstall(plan, outcome) {
|
|
|
3177
3179
|
}
|
|
3178
3180
|
/**
|
|
3179
3181
|
* Install vigiles's skills/hooks for the chosen harness(es) via the per-harness
|
|
3180
|
-
* `planPluginInstall` decision
|
|
3181
|
-
*
|
|
3182
|
-
*
|
|
3182
|
+
* `planPluginInstall` decision. Two carriers: the SKILLS are linked into the
|
|
3183
|
+
* repo (`linkVigilesSkills`), so every clone and container has them after
|
|
3184
|
+
* `npm install`; the GLOBAL install runs where it carries something the links
|
|
3185
|
+
* cannot (`shouldRunGlobalInstall` — Claude Code's plugin carries the hooks).
|
|
3186
|
+
* The decisions are pure and unit-tested; this is the thin IO.
|
|
3187
|
+
*
|
|
3188
|
+
* Returns the links it created or relinked, for the commit hint.
|
|
3183
3189
|
*/
|
|
3184
3190
|
function installPlugins(harnesses) {
|
|
3185
3191
|
const { execSync: exec } = require("node:child_process");
|
|
3186
3192
|
const plans = (0, setup_plan_js_1.planPluginInstall)(harnesses, {
|
|
3187
3193
|
hasClaude: harnesses.includes("claude") && harnessBinaryPresent("claude"),
|
|
3188
3194
|
});
|
|
3189
|
-
|
|
3195
|
+
const written = plans.flatMap((plan) => {
|
|
3190
3196
|
console.log("");
|
|
3191
|
-
|
|
3192
|
-
|
|
3197
|
+
const home = plan.linkSkills ? harnessSkillsHome(plan.harness) : null;
|
|
3198
|
+
const outcome = home === null ? null : linkVigilesSkills(home, process.cwd());
|
|
3199
|
+
if (outcome !== null)
|
|
3200
|
+
(0, skill_links_js_1.formatSkillLinks)(outcome).forEach((l) => {
|
|
3201
|
+
console.log(l);
|
|
3202
|
+
});
|
|
3203
|
+
const linked = outcome !== null && (0, skill_links_js_1.skillLinksUsable)(outcome);
|
|
3204
|
+
if ((0, setup_plan_js_1.shouldRunGlobalInstall)(plan, linked))
|
|
3205
|
+
reportInstall(plan, runInstall(plan, exec));
|
|
3206
|
+
else
|
|
3207
|
+
console.log(` No global ${plan.harness} skills install — the linked skills replace it, in every clone.`);
|
|
3208
|
+
return outcome === null ? [] : changedLinks(outcome);
|
|
3209
|
+
});
|
|
3193
3210
|
// Claude Code gets its hooks from the global marketplace plugin; Codex has no
|
|
3194
3211
|
// global store, so wire vigiles's proactive nudge hooks into the repo's
|
|
3195
3212
|
// .codex/config.toml directly (the idiomatic, repo-committed place).
|
|
3196
3213
|
if (harnesses.includes("codex"))
|
|
3197
3214
|
wireCodexHooks();
|
|
3198
3215
|
// Claude Code additionally gets a committed DECLARATION, so a collaborator who
|
|
3199
|
-
// clones and never runs `init` is told the project wants this plugin
|
|
3200
|
-
//
|
|
3216
|
+
// clones and never runs `init` is told the project wants this plugin (its
|
|
3217
|
+
// hooks) instead of silence. It does not install anything.
|
|
3201
3218
|
if (harnesses.includes("claude"))
|
|
3202
3219
|
declareVigilesPlugin();
|
|
3220
|
+
return written;
|
|
3221
|
+
}
|
|
3222
|
+
/** The repo-relative skills home of the harness `init` calls `name`, or null. */
|
|
3223
|
+
function harnessSkillsHome(name) {
|
|
3224
|
+
const adapter = (0, adapter_registry_js_1.getAdapter)(name);
|
|
3225
|
+
return adapter === undefined ? null : (0, layout_js_1.skillsHome)(adapter.layout);
|
|
3226
|
+
}
|
|
3227
|
+
/** The repo-relative paths of the links this run created or rewrote — the ones git sees change. */
|
|
3228
|
+
function changedLinks(outcome) {
|
|
3229
|
+
return outcome.kind === "linked"
|
|
3230
|
+
? outcome.results
|
|
3231
|
+
.filter((r) => r.status === "created" || r.status === "relinked")
|
|
3232
|
+
.map((r) => `${outcome.physicalHome}/${r.name}`)
|
|
3233
|
+
: [];
|
|
3234
|
+
}
|
|
3235
|
+
/** The manifest file name the skill-linking code reads. */
|
|
3236
|
+
const PACKAGE_JSON = "package.json";
|
|
3237
|
+
/** One top-level field of `<dir>/package.json`, or undefined. Never throws. */
|
|
3238
|
+
function packageField(dir, field) {
|
|
3239
|
+
try {
|
|
3240
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, PACKAGE_JSON), "utf-8"));
|
|
3241
|
+
return typeof pkg === "object" && pkg !== null
|
|
3242
|
+
? Object.entries(pkg).find(([k]) => k === field)?.[1]
|
|
3243
|
+
: undefined;
|
|
3244
|
+
}
|
|
3245
|
+
catch {
|
|
3246
|
+
return undefined;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
/** `package.json`'s `name`, or null. Never throws. */
|
|
3250
|
+
function packageNameAt(dir) {
|
|
3251
|
+
const name = packageField(dir, "name");
|
|
3252
|
+
return typeof name === "string" ? name : null;
|
|
3253
|
+
}
|
|
3254
|
+
/** A thrown value's message. */
|
|
3255
|
+
function errorText(e) {
|
|
3256
|
+
return e instanceof Error ? e.message : String(e);
|
|
3257
|
+
}
|
|
3258
|
+
/** A path's real location, or null when it does not resolve. Never throws. */
|
|
3259
|
+
function realOrNull(path) {
|
|
3260
|
+
try {
|
|
3261
|
+
return (0, node_fs_1.realpathSync)(path);
|
|
3262
|
+
}
|
|
3263
|
+
catch {
|
|
3264
|
+
return null;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
/** What occupies `path`, for `planSkillLinks`. */
|
|
3268
|
+
function observeSkillEntry(path) {
|
|
3269
|
+
const st = (0, node_fs_1.lstatSync)(path, { throwIfNoEntry: false });
|
|
3270
|
+
if (st === undefined)
|
|
3271
|
+
return { kind: "missing" };
|
|
3272
|
+
if (st.isSymbolicLink())
|
|
3273
|
+
return {
|
|
3274
|
+
kind: "link",
|
|
3275
|
+
target: (0, node_fs_1.readlinkSync)(path),
|
|
3276
|
+
resolvesTo: realOrNull(path),
|
|
3277
|
+
};
|
|
3278
|
+
return st.isDirectory() ? { kind: "directory" } : { kind: "file" };
|
|
3279
|
+
}
|
|
3280
|
+
const LINK_IO = {
|
|
3281
|
+
// "dir" matters only on Windows, where a directory link must say so.
|
|
3282
|
+
symlink: (target, path) => {
|
|
3283
|
+
(0, node_fs_1.symlinkSync)(target, path, "dir");
|
|
3284
|
+
},
|
|
3285
|
+
rename: node_fs_1.renameSync,
|
|
3286
|
+
unlink: node_fs_1.unlinkSync,
|
|
3287
|
+
};
|
|
3288
|
+
/** Carry out one decision. A failed `symlink` is reported, never thrown. */
|
|
3289
|
+
function applySkillLink(decision, absHome) {
|
|
3290
|
+
switch (decision.action) {
|
|
3291
|
+
case "keep":
|
|
3292
|
+
return { name: decision.name, status: "present" };
|
|
3293
|
+
case "refuse":
|
|
3294
|
+
return {
|
|
3295
|
+
name: decision.name,
|
|
3296
|
+
status: "skipped",
|
|
3297
|
+
reason: decision.reason,
|
|
3298
|
+
};
|
|
3299
|
+
case "create":
|
|
3300
|
+
case "replace":
|
|
3301
|
+
try {
|
|
3302
|
+
// Only a link `decideSkillLink` proved ours is ever replaced.
|
|
3303
|
+
(0, skill_links_js_1.writeSkillLink)(LINK_IO, (0, node_path_1.join)(absHome, decision.name), decision.target, decision.action);
|
|
3304
|
+
return {
|
|
3305
|
+
name: decision.name,
|
|
3306
|
+
status: decision.action === "replace" ? "relinked" : "created",
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
catch (e) {
|
|
3310
|
+
const code = e instanceof Error && "code" in e && typeof e.code === "string"
|
|
3311
|
+
? e.code
|
|
3312
|
+
: "";
|
|
3313
|
+
return {
|
|
3314
|
+
name: decision.name,
|
|
3315
|
+
status: "skipped",
|
|
3316
|
+
reason: (0, skill_links_js_1.linkFailureReason)(code, errorText(e)),
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Why `init` must not write under `absHome`, or null when it may. `.claude` or
|
|
3323
|
+
* its `skills` can be a link to a dotfiles directory outside the repo; creating
|
|
3324
|
+
* the home would follow it and put links where no commit of this repo reaches.
|
|
3325
|
+
* What `mkdir -p` would write through is the nearest part of the path that
|
|
3326
|
+
* already exists, so that is the part judged.
|
|
3327
|
+
*/
|
|
3328
|
+
function homeEscape(project, home, absHome) {
|
|
3329
|
+
const nearest = (p) => (0, node_fs_1.lstatSync)(p, { throwIfNoEntry: false }) === undefined
|
|
3330
|
+
? nearest((0, node_path_1.dirname)(p))
|
|
3331
|
+
: p;
|
|
3332
|
+
const anchor = nearest(absHome);
|
|
3333
|
+
const real = realOrNull(anchor);
|
|
3334
|
+
if (real === null)
|
|
3335
|
+
return `${(0, node_path_1.relative)(project, anchor)} is a link that does not resolve, so ${home} cannot be created`;
|
|
3336
|
+
return (0, skill_links_js_1.isWithinProject)(project, real)
|
|
3337
|
+
? null
|
|
3338
|
+
: `${home} leads to ${real}, outside the repository, and a link written there would not travel with a clone`;
|
|
3339
|
+
}
|
|
3340
|
+
/** The outcome when `init` makes no links, with the reason it prints. */
|
|
3341
|
+
function notLinked(reason) {
|
|
3342
|
+
return { kind: "not-linked", reason };
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Link vigiles's shipped skills into `home` (repo-relative) — the IO half of
|
|
3346
|
+
* `src/skill-links.ts`, which decides everything: where the package is, what
|
|
3347
|
+
* each entry becomes, and what the report says.
|
|
3348
|
+
*/
|
|
3349
|
+
function linkVigilesSkills(home, cwd) {
|
|
3350
|
+
// Physical, so the relative target computed against the (physical) skills
|
|
3351
|
+
// home is right even when the checkout sits under a symlink (macOS /tmp).
|
|
3352
|
+
const project = (0, node_fs_1.realpathSync)(cwd);
|
|
3353
|
+
const site = (0, skill_links_js_1.locatePackage)(project, {
|
|
3354
|
+
isPackage: (dir) => packageNameAt(dir) === skill_links_js_1.VIGILES_PACKAGE,
|
|
3355
|
+
isRepoRoot: (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, ".git")),
|
|
3356
|
+
isWorkspaceRoot: (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, "pnpm-workspace.yaml")) ||
|
|
3357
|
+
packageField(dir, "workspaces") !== undefined,
|
|
3358
|
+
});
|
|
3359
|
+
const pkgText = (0, node_fs_1.existsSync)((0, node_path_1.join)(project, PACKAGE_JSON))
|
|
3360
|
+
? (0, node_fs_1.readFileSync)((0, node_path_1.join)(project, PACKAGE_JSON), "utf-8")
|
|
3361
|
+
: null;
|
|
3362
|
+
const blocked = (0, skill_links_js_1.linkPrecondition)({
|
|
3363
|
+
isVigilesItself: packageNameAt(project) === skill_links_js_1.VIGILES_PACKAGE,
|
|
3364
|
+
dependsOnVigiles: pkgText !== null && (0, install_reader_js_1.declaresVigilesDependency)(pkgText),
|
|
3365
|
+
installed: site.kind === "installed",
|
|
3366
|
+
});
|
|
3367
|
+
if (blocked !== null)
|
|
3368
|
+
return notLinked(blocked);
|
|
3369
|
+
if (site.kind === "unsure")
|
|
3370
|
+
return notLinked(site.reason);
|
|
3371
|
+
// Names from the installed package when there is one, else from this CLI's
|
|
3372
|
+
// own — the version `init` just declared as the devDependency.
|
|
3373
|
+
const source = site.kind === "installed" ? site.dir : selfRoot();
|
|
3374
|
+
const names = setup_plan_js_1.SHIPPED_SKILLS.filter((n) => (0, node_fs_1.existsSync)((0, node_path_1.join)(source, "skills", n, "SKILL.md")));
|
|
3375
|
+
if (names.length === 0)
|
|
3376
|
+
return notLinked(`no shipped skills found under ${(0, node_path_1.join)(source, "skills")}`);
|
|
3377
|
+
const absHome = (0, node_path_1.resolve)(project, home);
|
|
3378
|
+
const escape = homeEscape(project, home, absHome);
|
|
3379
|
+
if (escape !== null)
|
|
3380
|
+
return notLinked(escape);
|
|
3381
|
+
try {
|
|
3382
|
+
(0, node_fs_1.mkdirSync)(absHome, { recursive: true });
|
|
3383
|
+
}
|
|
3384
|
+
catch (e) {
|
|
3385
|
+
return notLinked(`cannot create ${home}: ${errorText(e)}`);
|
|
3386
|
+
}
|
|
3387
|
+
const physicalHome = (0, node_fs_1.realpathSync)(absHome);
|
|
3388
|
+
const plan = (0, skill_links_js_1.planSkillLinks)({
|
|
3389
|
+
names,
|
|
3390
|
+
site,
|
|
3391
|
+
physicalHome,
|
|
3392
|
+
entries: (n) => observeSkillEntry((0, node_path_1.join)(absHome, n)),
|
|
3393
|
+
realOf: (n) => realOrNull((0, node_path_1.join)(site.dir, "skills", n)),
|
|
3394
|
+
});
|
|
3395
|
+
return {
|
|
3396
|
+
kind: "linked",
|
|
3397
|
+
home,
|
|
3398
|
+
physicalHome: (0, skill_links_js_1.portableTarget)((0, node_path_1.relative)(project, physicalHome)),
|
|
3399
|
+
pending: plan.pending,
|
|
3400
|
+
results: plan.decisions.map((d) => applySkillLink(d, absHome)),
|
|
3401
|
+
};
|
|
3203
3402
|
}
|
|
3204
3403
|
/**
|
|
3205
3404
|
* Write the project-level plugin declaration into `.claude/settings.json`.
|
|
@@ -3208,8 +3407,9 @@ function installPlugins(harnesses) {
|
|
|
3208
3407
|
* collaborator: an external-source plugin declared project-level does not load
|
|
3209
3408
|
* until each person installs it on their own machine (the boundary is
|
|
3210
3409
|
* deliberate — plugins run arbitrary code with the user's privileges). What it
|
|
3211
|
-
* buys is that Claude Code then PROMPTS them with the install command
|
|
3212
|
-
*
|
|
3410
|
+
* buys is that Claude Code then PROMPTS them with the install command — for the
|
|
3411
|
+
* HOOKS, which only the plugin carries. The skills reach them through the
|
|
3412
|
+
* committed links (`linkVigilesSkills`), with or without the plugin.
|
|
3213
3413
|
*
|
|
3214
3414
|
* MERGES, never clobbers: this file holds the user's hooks, permissions and
|
|
3215
3415
|
* other plugins. The merge rules are the pure, unit-tested
|
|
@@ -3249,8 +3449,9 @@ function declareVigilesPlugin() {
|
|
|
3249
3449
|
(0, node_fs_1.writeFileSync)(path, JSON.stringify(edit.settings, null, 2) + "\n");
|
|
3250
3450
|
console.log("✓ Declared the vigiles plugin in .claude/settings.json (commit it)\n" +
|
|
3251
3451
|
" This does NOT install it for anyone else — Claude Code will PROMPT a\n" +
|
|
3252
|
-
" collaborator to run `claude plugin install vigiles@vigiles`,
|
|
3253
|
-
" the
|
|
3452
|
+
" collaborator to run `claude plugin install vigiles@vigiles`, which is how\n" +
|
|
3453
|
+
" they get the hooks. The skills don't wait for it: they come with the\n" +
|
|
3454
|
+
" linked .claude/skills/ entries.\n" +
|
|
3254
3455
|
" Nothing is vendored: the entry is a reference; the plugin still lives in\n" +
|
|
3255
3456
|
" the global cache, one copy shared across your repos.");
|
|
3256
3457
|
}
|
|
@@ -3460,10 +3661,9 @@ async function setup(args) {
|
|
|
3460
3661
|
console.log("");
|
|
3461
3662
|
written.push(...wireGha(plan, harnesses));
|
|
3462
3663
|
}
|
|
3463
|
-
//
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
}
|
|
3664
|
+
// Skills linked into the repo + the per-harness global install (Claude
|
|
3665
|
+
// marketplace for the hooks / Codex only when nothing could be linked).
|
|
3666
|
+
const linkedSkills = plan.plugin ? installPlugins(harnesses) : [];
|
|
3467
3667
|
// Agent-specific guidance.
|
|
3468
3668
|
if (targets.includes("AGENTS.md")) {
|
|
3469
3669
|
console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
|
|
@@ -3483,7 +3683,14 @@ async function setup(args) {
|
|
|
3483
3683
|
lint: plan.lint,
|
|
3484
3684
|
written,
|
|
3485
3685
|
});
|
|
3486
|
-
printSetupSummary({
|
|
3686
|
+
printSetupSummary({
|
|
3687
|
+
plan,
|
|
3688
|
+
strict,
|
|
3689
|
+
targets,
|
|
3690
|
+
adopted,
|
|
3691
|
+
// The skill links the plugin step created, so the commit hint names them.
|
|
3692
|
+
written: [...written, ...linkedSkills],
|
|
3693
|
+
});
|
|
3487
3694
|
// Surface the OTHER mode so both directions are discoverable — the two branches
|
|
3488
3695
|
// are mutually exclusive (a run is either gate-only or full):
|
|
3489
3696
|
// • a gate-only setup → a ONE-LINE nudge to graduate to the full layer;
|
package/dist/core/adapter.d.ts
CHANGED
|
@@ -182,6 +182,13 @@ export interface InstallReader {
|
|
|
182
182
|
* outside its own surface.
|
|
183
183
|
*/
|
|
184
184
|
readonly repo: (repoRelative: string) => string | null;
|
|
185
|
+
/**
|
|
186
|
+
* The stored target of a repo SYMLINK (what `readlink` returns), or null when
|
|
187
|
+
* the path is not a link, is missing, or is NOT a path this adapter claims.
|
|
188
|
+
* It answers the one question `repo` cannot: a committed link whose target is
|
|
189
|
+
* not installed yet reads as null through `repo`, exactly like no link at all.
|
|
190
|
+
*/
|
|
191
|
+
readonly repoLink: (repoRelative: string) => string | null;
|
|
185
192
|
/**
|
|
186
193
|
* A file under the user's HOME, or null. This is a read of the MACHINE, never
|
|
187
194
|
* of the repository — the plugin-install record lives there — and everything
|
|
@@ -30,6 +30,18 @@ function readOrNull(absolute) {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/** A symlink's stored target, or null when it is not a link. Never throws. */
|
|
34
|
+
function linkOrNull(absolute) {
|
|
35
|
+
try {
|
|
36
|
+
return (0, node_fs_1.lstatSync)(absolute, { throwIfNoEntry: false })?.isSymbolicLink() ===
|
|
37
|
+
true
|
|
38
|
+
? (0, node_fs_1.readlinkSync)(absolute)
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
33
45
|
/** Directory names directly under `dir`, or [] when it is not readable. */
|
|
34
46
|
function dirNames(dir) {
|
|
35
47
|
try {
|
|
@@ -80,6 +92,9 @@ function buildInstallReader(adapter, root, opts = {}) {
|
|
|
80
92
|
repo: (repoRelative) => adapter.claims(repoRelative)
|
|
81
93
|
? readOrNull((0, node_path_1.resolve)(root, repoRelative))
|
|
82
94
|
: null,
|
|
95
|
+
repoLink: (repoRelative) => adapter.claims(repoRelative)
|
|
96
|
+
? linkOrNull((0, node_path_1.resolve)(root, repoRelative))
|
|
97
|
+
: null,
|
|
83
98
|
home: (homeRelative) => readOrNull((0, node_path_1.join)(home, homeRelative)),
|
|
84
99
|
repoDependsOnVigiles: pkgJson !== null && declaresVigilesDependency(pkgJson),
|
|
85
100
|
vendoredSkillNames: dirNames((0, node_path_1.join)(root, "node_modules", "vigiles", "skills")),
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -241,6 +241,16 @@ export declare function executableSourceDirs(layout: PluginLayout): readonly str
|
|
|
241
241
|
* written a layout where they differed.
|
|
242
242
|
*/
|
|
243
243
|
export declare function materializePrefix(layout: PluginLayout): string;
|
|
244
|
+
/**
|
|
245
|
+
* Where a REPOSITORY keeps its skills for this harness, repo-relative — the
|
|
246
|
+
* skill surface under {@link materializePrefix}: `.claude/skills` on Claude
|
|
247
|
+
* Code, `.agents/skills` on Codex. `null` when the layout has no skill surface.
|
|
248
|
+
*
|
|
249
|
+
* `vigiles init` links the package's shipped skills here, so it must be the
|
|
250
|
+
* directory the harness actually scans for a project; deriving it from the two
|
|
251
|
+
* fields that already say so keeps a third spelling from appearing.
|
|
252
|
+
*/
|
|
253
|
+
export declare function skillsHome(layout: Pick<PluginLayout, "surfaces" | "userSurfaceRoot">): string | null;
|
|
244
254
|
/**
|
|
245
255
|
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
246
256
|
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
package/dist/core/layout.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.RULE_FILE_LEAF_RE = exports.AGENT_FILE_LEAF_RE = exports.SURFACE_KINDS =
|
|
|
4
4
|
exports.surfaceDirs = surfaceDirs;
|
|
5
5
|
exports.executableSourceDirs = executableSourceDirs;
|
|
6
6
|
exports.materializePrefix = materializePrefix;
|
|
7
|
+
exports.skillsHome = skillsHome;
|
|
7
8
|
exports.rulesHome = rulesHome;
|
|
8
9
|
exports.ruleFileRe = ruleFileRe;
|
|
9
10
|
exports.agentSurfaceName = agentSurfaceName;
|
|
@@ -46,6 +47,22 @@ function executableSourceDirs(layout) {
|
|
|
46
47
|
function materializePrefix(layout) {
|
|
47
48
|
return layout.userSurfaceRoot ?? "";
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Where a REPOSITORY keeps its skills for this harness, repo-relative — the
|
|
52
|
+
* skill surface under {@link materializePrefix}: `.claude/skills` on Claude
|
|
53
|
+
* Code, `.agents/skills` on Codex. `null` when the layout has no skill surface.
|
|
54
|
+
*
|
|
55
|
+
* `vigiles init` links the package's shipped skills here, so it must be the
|
|
56
|
+
* directory the harness actually scans for a project; deriving it from the two
|
|
57
|
+
* fields that already say so keeps a third spelling from appearing.
|
|
58
|
+
*/
|
|
59
|
+
function skillsHome(layout) {
|
|
60
|
+
const skill = layout.surfaces.skill;
|
|
61
|
+
if (skill === undefined || skill === "")
|
|
62
|
+
return null;
|
|
63
|
+
const root = layout.userSurfaceRoot;
|
|
64
|
+
return root === undefined || root === "" ? skill : `${root}/${skill}`;
|
|
65
|
+
}
|
|
49
66
|
/**
|
|
50
67
|
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
51
68
|
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a child agent run inherits — the one place the runners turn
|
|
3
|
+
* a caller's `process.env` into a spawn env.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes, both driven by the harness's declared {@link RunEnvPolicy}:
|
|
6
|
+
*
|
|
7
|
+
* - SCRUBBED (`scrubbedRunEnv`): a throwaway HOME/TMPDIR, the OS essentials, the
|
|
8
|
+
* harness's declared auth, and the caller's own named extras. Everything else
|
|
9
|
+
* is dropped. The eval tier's `ephemeralEnv` and the harness-tier preflight of
|
|
10
|
+
* `experimental_outputStyleArms` use it.
|
|
11
|
+
* - INHERITED (`withoutSessionIdentity`): the caller's env as-is, minus the
|
|
12
|
+
* harness's declared session identity. The default spawn of both tiers.
|
|
13
|
+
*
|
|
14
|
+
* In both, the parent session's identity never reaches the child. The core
|
|
15
|
+
* names no harness: which variables are auth and which are identity is the
|
|
16
|
+
* adapter's declaration.
|
|
17
|
+
*
|
|
18
|
+
* Pure — no fs, no spawn.
|
|
19
|
+
*/
|
|
20
|
+
import type { RunEnvPolicy } from "./runtime.js";
|
|
21
|
+
type EnvLike = Readonly<Record<string, string | undefined>>;
|
|
22
|
+
/**
|
|
23
|
+
* What ANY spawned process needs to function, whatever the harness: resolve
|
|
24
|
+
* binaries (`PATH`) and keep the locale and terminal (`LANG`, `LC_*`, `TERM`).
|
|
25
|
+
* Mirrors what `bwrapArgs` sets back after `--clearenv` in `src/sandbox.ts`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const OS_ESSENTIAL_ENV: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Build a SCRUBBED run environment: a NEW env with `HOME`/`TMPDIR` set to the
|
|
30
|
+
* throwaway `opts.home`, the {@link OS_ESSENTIAL_ENV}, the harness's
|
|
31
|
+
* `policy.keep`, and the caller's `opts.allow`, taken from `base`. Everything
|
|
32
|
+
* else is dropped, and `policy.sessionIdentity` is dropped even when listed in
|
|
33
|
+
* `keep` or `allow`.
|
|
34
|
+
*
|
|
35
|
+
* Without a policy only the essentials pass: right for a run against a scripted
|
|
36
|
+
* mock, which needs no auth.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scrubbedRunEnv(base: EnvLike, opts: {
|
|
39
|
+
readonly home: string;
|
|
40
|
+
readonly policy?: RunEnvPolicy;
|
|
41
|
+
readonly allow?: readonly string[];
|
|
42
|
+
}): Readonly<Record<string, string>>;
|
|
43
|
+
/**
|
|
44
|
+
* The caller's env with the harness's declared session identity removed — the
|
|
45
|
+
* INHERITED shape. Unset (`undefined`) values are dropped too, so the result is
|
|
46
|
+
* a plain spawn env. Without a policy nothing is removed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withoutSessionIdentity(base: EnvLike, policy?: RunEnvPolicy): Readonly<Record<string, string>>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=run-env.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OS_ESSENTIAL_ENV = void 0;
|
|
4
|
+
exports.scrubbedRunEnv = scrubbedRunEnv;
|
|
5
|
+
exports.withoutSessionIdentity = withoutSessionIdentity;
|
|
6
|
+
/**
|
|
7
|
+
* What ANY spawned process needs to function, whatever the harness: resolve
|
|
8
|
+
* binaries (`PATH`) and keep the locale and terminal (`LANG`, `LC_*`, `TERM`).
|
|
9
|
+
* Mirrors what `bwrapArgs` sets back after `--clearenv` in `src/sandbox.ts`.
|
|
10
|
+
*/
|
|
11
|
+
exports.OS_ESSENTIAL_ENV = ["PATH", "LANG", "TERM"];
|
|
12
|
+
const OS_ESSENTIAL_PREFIXES = ["LC_"];
|
|
13
|
+
/**
|
|
14
|
+
* Build a SCRUBBED run environment: a NEW env with `HOME`/`TMPDIR` set to the
|
|
15
|
+
* throwaway `opts.home`, the {@link OS_ESSENTIAL_ENV}, the harness's
|
|
16
|
+
* `policy.keep`, and the caller's `opts.allow`, taken from `base`. Everything
|
|
17
|
+
* else is dropped, and `policy.sessionIdentity` is dropped even when listed in
|
|
18
|
+
* `keep` or `allow`.
|
|
19
|
+
*
|
|
20
|
+
* Without a policy only the essentials pass: right for a run against a scripted
|
|
21
|
+
* mock, which needs no auth.
|
|
22
|
+
*/
|
|
23
|
+
function scrubbedRunEnv(base, opts) {
|
|
24
|
+
const identity = new Set(opts.policy?.sessionIdentity ?? []);
|
|
25
|
+
const exact = new Set([
|
|
26
|
+
...exports.OS_ESSENTIAL_ENV,
|
|
27
|
+
...(opts.policy?.keep ?? []),
|
|
28
|
+
...(opts.allow ?? []),
|
|
29
|
+
]);
|
|
30
|
+
const passes = (k) => !identity.has(k) &&
|
|
31
|
+
(exact.has(k) || OS_ESSENTIAL_PREFIXES.some((p) => k.startsWith(p)));
|
|
32
|
+
return {
|
|
33
|
+
...defined(base, passes),
|
|
34
|
+
// Last, so they win over anything passed through.
|
|
35
|
+
HOME: opts.home,
|
|
36
|
+
TMPDIR: opts.home,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The caller's env with the harness's declared session identity removed — the
|
|
41
|
+
* INHERITED shape. Unset (`undefined`) values are dropped too, so the result is
|
|
42
|
+
* a plain spawn env. Without a policy nothing is removed.
|
|
43
|
+
*/
|
|
44
|
+
function withoutSessionIdentity(base, policy) {
|
|
45
|
+
const identity = new Set(policy?.sessionIdentity ?? []);
|
|
46
|
+
return defined(base, (k) => !identity.has(k));
|
|
47
|
+
}
|
|
48
|
+
function defined(base, keep) {
|
|
49
|
+
return Object.fromEntries(Object.entries(base).filter((e) => e[1] !== undefined && keep(e[0])));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=run-env.js.map
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -54,5 +54,49 @@ export interface HarnessRuntime {
|
|
|
54
54
|
* caller resolves the raw string via `agentBinary --version`); unit-testable.
|
|
55
55
|
*/
|
|
56
56
|
versionKey(raw: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Which of the caller's environment a child run may inherit — see
|
|
59
|
+
* {@link RunEnvPolicy}. Absent: a scrubbed run inherits no harness variable,
|
|
60
|
+
* and nothing is removed from an inherited env.
|
|
61
|
+
*/
|
|
62
|
+
readonly runEnv?: RunEnvPolicy;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Which of the caller's environment a CHILD run of this harness may inherit.
|
|
66
|
+
*
|
|
67
|
+
* A run spawned from inside a live session of the same harness (an eval started
|
|
68
|
+
* by an agent, a harness test run from a hook) inherits that session's process
|
|
69
|
+
* environment, and a harness marks its own session there. Measured on Claude
|
|
70
|
+
* Code 2.1.292 (2026-10-07): a child `claude -p` started with the parent's
|
|
71
|
+
* session variables ran AS the parent session — a task it created landed in the
|
|
72
|
+
* parent's live task list. Which variables are identity and which are auth is a
|
|
73
|
+
* fact about one harness, so the adapter declares it and the runners apply it
|
|
74
|
+
* (`src/core/run-env.ts`) without spelling a single name.
|
|
75
|
+
*
|
|
76
|
+
* Optional, so an adapter written before this field still type-checks. An
|
|
77
|
+
* adapter without one gets the strict reading: a scrubbed run inherits no
|
|
78
|
+
* harness variable at all, and an inherited one has nothing removed.
|
|
79
|
+
*/
|
|
80
|
+
export interface RunEnvPolicy {
|
|
81
|
+
/**
|
|
82
|
+
* Exact names a SCRUBBED run (fresh HOME, cleared env) inherits: the
|
|
83
|
+
* harness's auth and backend selection, nothing else. An allowlist on purpose —
|
|
84
|
+
* a harness's session variables are open-ended (the Claude Code 2.1.292
|
|
85
|
+
* binary references several dozen `*SESSION*` / `*SOCKET*` / `*TOKEN*` names),
|
|
86
|
+
* so a prefix with a few exceptions would keep passing whichever ones nobody
|
|
87
|
+
* listed.
|
|
88
|
+
*/
|
|
89
|
+
readonly keep: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* Auth FILES under HOME (relative paths) that a scrubbed run's throwaway HOME
|
|
92
|
+
* is seeded with, by copy. The file half of `keep`.
|
|
93
|
+
*/
|
|
94
|
+
readonly keepHomeFiles: readonly string[];
|
|
95
|
+
/**
|
|
96
|
+
* The PARENT session's identity: names no child run ever inherits — not a
|
|
97
|
+
* scrubbed run, and not a run that otherwise inherits the whole caller env.
|
|
98
|
+
* Wins over `keep` and over any name a caller allows.
|
|
99
|
+
*/
|
|
100
|
+
readonly sessionIdentity: readonly string[];
|
|
57
101
|
}
|
|
58
102
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a symlink's stored target one that `vigiles init` writes — a path ending
|
|
3
|
+
* in `node_modules/vigiles/skills/<name>`, for the SAME skill name?
|
|
4
|
+
*
|
|
5
|
+
* Two readers ask, and must agree, so the answer lives here once:
|
|
6
|
+
*
|
|
7
|
+
* - `init` (`../skill-links.ts`): an existing link of this shape that points
|
|
8
|
+
* somewhere other than where `init` would link now was made by an earlier
|
|
9
|
+
* `init` (for example before a workspace install hoisted the package), so it
|
|
10
|
+
* is replaced instead of being refused as the user's;
|
|
11
|
+
* - `audit` (`../adapters/claude-code/skill-reachability.ts`): only a dangling
|
|
12
|
+
* link of this shape is "vigiles's link, run npm install". A dangling link
|
|
13
|
+
* that merely shares a skill's NAME is the user's, and the advice would send
|
|
14
|
+
* them to fix the wrong thing.
|
|
15
|
+
*
|
|
16
|
+
* The test is on path SEGMENTS (either separator, so a link written on Windows
|
|
17
|
+
* reads the same), not on a substring: `my-node_modules/vigiles/...` is not it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isVigilesSkillTarget(target: string, name: string): boolean;
|
|
20
|
+
//# sourceMappingURL=skill-link-target.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isVigilesSkillTarget = isVigilesSkillTarget;
|
|
4
|
+
/**
|
|
5
|
+
* Is a symlink's stored target one that `vigiles init` writes — a path ending
|
|
6
|
+
* in `node_modules/vigiles/skills/<name>`, for the SAME skill name?
|
|
7
|
+
*
|
|
8
|
+
* Two readers ask, and must agree, so the answer lives here once:
|
|
9
|
+
*
|
|
10
|
+
* - `init` (`../skill-links.ts`): an existing link of this shape that points
|
|
11
|
+
* somewhere other than where `init` would link now was made by an earlier
|
|
12
|
+
* `init` (for example before a workspace install hoisted the package), so it
|
|
13
|
+
* is replaced instead of being refused as the user's;
|
|
14
|
+
* - `audit` (`../adapters/claude-code/skill-reachability.ts`): only a dangling
|
|
15
|
+
* link of this shape is "vigiles's link, run npm install". A dangling link
|
|
16
|
+
* that merely shares a skill's NAME is the user's, and the advice would send
|
|
17
|
+
* them to fix the wrong thing.
|
|
18
|
+
*
|
|
19
|
+
* The test is on path SEGMENTS (either separator, so a link written on Windows
|
|
20
|
+
* reads the same), not on a substring: `my-node_modules/vigiles/...` is not it.
|
|
21
|
+
*/
|
|
22
|
+
function isVigilesSkillTarget(target, name) {
|
|
23
|
+
const tail = target
|
|
24
|
+
.split(/[\\/]+/)
|
|
25
|
+
.filter((s) => s !== "")
|
|
26
|
+
.slice(-4);
|
|
27
|
+
return (tail.length === 4 &&
|
|
28
|
+
tail[0] === "node_modules" &&
|
|
29
|
+
tail[1] === "vigiles" &&
|
|
30
|
+
tail[2] === "skills" &&
|
|
31
|
+
tail[3] === name);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=skill-link-target.js.map
|