vigiles 33.3.0 → 35.0.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/action.yml +45 -8
- package/dist/adapters/codex/driver.js +2 -1
- package/dist/adapters/codex/mock-model.d.ts +3 -2
- package/dist/adapters/codex/mock-model.js +15 -3
- package/dist/cli-flag-check.js +1 -0
- package/dist/cli-main.js +253 -56
- package/dist/core/config-schema.d.ts +53 -34
- package/dist/core/config-schema.js +42 -0
- package/dist/core/harness-driver.d.ts +5 -2
- package/dist/core/presets.d.ts +44 -0
- package/dist/core/presets.js +44 -0
- package/dist/core/script-overrun.d.ts +32 -0
- package/dist/core/script-overrun.js +42 -0
- package/dist/core/validate.js +1 -1
- package/dist/doc-test-script-coverage.d.ts +1 -1
- package/dist/doc-test-script-coverage.js +1 -1
- package/dist/eval-lock.d.ts +5 -6
- package/dist/eval-lock.js +5 -6
- package/dist/harness-test.d.ts +35 -1
- package/dist/harness-test.js +100 -10
- package/dist/mock-model.d.ts +4 -1
- package/dist/mock-model.js +42 -11
- package/dist/setup-plan.d.ts +93 -25
- package/dist/setup-plan.js +133 -67
- package/dist/vigilesrc.schema.json +6 -0
- package/package.json +1 -2
package/action.yml
CHANGED
|
@@ -19,9 +19,15 @@ inputs:
|
|
|
19
19
|
required: false
|
|
20
20
|
default: ""
|
|
21
21
|
version:
|
|
22
|
-
description:
|
|
22
|
+
description: >
|
|
23
|
+
Version of the vigiles npm package to run (e.g. '34', '34.1.2', 'latest').
|
|
24
|
+
Empty (the default) runs the vigiles THIS repo declares in package.json:
|
|
25
|
+
the installed copy when node_modules has it, else the version in
|
|
26
|
+
package-lock.json, else the declared range — and 'latest' only when the
|
|
27
|
+
repo does not declare vigiles. Use 'local' to run the action's own
|
|
28
|
+
checkout — for dogfooding via `uses: ./` in this repo's CI.
|
|
23
29
|
required: false
|
|
24
|
-
default: "
|
|
30
|
+
default: ""
|
|
25
31
|
max-rules:
|
|
26
32
|
description: "Maximum number of rules per spec file (maps to the CLI's --max-rules)."
|
|
27
33
|
required: false
|
|
@@ -109,17 +115,48 @@ runs:
|
|
|
109
115
|
fi
|
|
110
116
|
|
|
111
117
|
# Resolve the runner. 'local' runs the action's own build (CI
|
|
112
|
-
# dogfood via `uses: ./`);
|
|
113
|
-
# package at that version
|
|
114
|
-
#
|
|
115
|
-
|
|
118
|
+
# dogfood via `uses: ./`); an explicit version runs the published npm
|
|
119
|
+
# package at that version. EMPTY (the default) runs the vigiles this
|
|
120
|
+
# repo declares, so CI and a local `npx vigiles` are the same version
|
|
121
|
+
# rather than two pins that drift: the installed copy, else the
|
|
122
|
+
# lockfile's version, else the declared range; `latest` only when the
|
|
123
|
+
# repo does not declare vigiles at all.
|
|
124
|
+
version="${VIGILES_VERSION:-}"
|
|
116
125
|
if [[ "$version" == "local" ]]; then
|
|
117
126
|
runner=(node "${VIGILES_ACTION_PATH}/dist/cli.js")
|
|
118
|
-
|
|
127
|
+
from="the action's own checkout"
|
|
128
|
+
elif [[ -n "$version" ]]; then
|
|
119
129
|
runner=(npx --yes "vigiles@${version}")
|
|
130
|
+
from="the version input"
|
|
131
|
+
else
|
|
132
|
+
declared="$(node -e '
|
|
133
|
+
const fs = require("fs");
|
|
134
|
+
const read = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
|
|
135
|
+
const pkg = read("package.json") ?? {};
|
|
136
|
+
const spec = pkg.devDependencies?.vigiles ?? pkg.dependencies?.vigiles;
|
|
137
|
+
if (typeof spec !== "string") { console.log("none"); process.exit(0); }
|
|
138
|
+
if (fs.existsSync("node_modules/.bin/vigiles")) { console.log("installed"); process.exit(0); }
|
|
139
|
+
const lock = read("package-lock.json") ?? {};
|
|
140
|
+
const locked = lock.packages?.["node_modules/vigiles"]?.version ?? lock.dependencies?.vigiles?.version;
|
|
141
|
+
console.log(typeof locked === "string" ? "lock " + locked : "range " + spec);
|
|
142
|
+
' 2>/dev/null || echo none)"
|
|
143
|
+
case "$declared" in
|
|
144
|
+
installed)
|
|
145
|
+
runner=(npx --no-install vigiles)
|
|
146
|
+
from="this repo's installed vigiles" ;;
|
|
147
|
+
"lock "*)
|
|
148
|
+
runner=(npx --yes "vigiles@${declared#lock }")
|
|
149
|
+
from="package-lock.json" ;;
|
|
150
|
+
"range "*)
|
|
151
|
+
runner=(npx --yes "vigiles@${declared#range }")
|
|
152
|
+
from="the range package.json declares" ;;
|
|
153
|
+
*)
|
|
154
|
+
runner=(npx --yes "vigiles@latest")
|
|
155
|
+
from="latest (this repo does not declare vigiles)" ;;
|
|
156
|
+
esac
|
|
120
157
|
fi
|
|
121
158
|
|
|
122
|
-
echo "vigiles: ${runner[*]} ${args[*]}"
|
|
159
|
+
echo "vigiles: ${runner[*]} ${args[*]} (version from ${from})"
|
|
123
160
|
|
|
124
161
|
# Run, streaming to the log AND capturing for the summary/comment.
|
|
125
162
|
out="$(mktemp)"
|
|
@@ -75,7 +75,8 @@ async function startCodexMockHandle(script) {
|
|
|
75
75
|
}));
|
|
76
76
|
},
|
|
77
77
|
get count() {
|
|
78
|
-
|
|
78
|
+
// Turns SERVED: a request past the end of the script gets a 400.
|
|
79
|
+
return Math.min(mock.requests.length, script.length);
|
|
79
80
|
},
|
|
80
81
|
close: () => mock.close(),
|
|
81
82
|
};
|
|
@@ -42,8 +42,9 @@ export declare function parseResponsesRequest(body: string): {
|
|
|
42
42
|
};
|
|
43
43
|
/**
|
|
44
44
|
* Start the scripted Codex Responses mock on a free port. Each
|
|
45
|
-
* `POST /v1/responses` consumes the next scripted turn
|
|
46
|
-
*
|
|
45
|
+
* `POST /v1/responses` consumes the next scripted turn; one past the end of the
|
|
46
|
+
* script is a 400, never a copy of the last turn (#340); HEAD and any other path
|
|
47
|
+
* get `{}`. Resolves to a handle
|
|
47
48
|
* with the base `url`, the `port`, the recorded `requests`, and `close()`.
|
|
48
49
|
*/
|
|
49
50
|
export declare function startCodexMock(script: readonly CodexTurn[], opts?: {
|
|
@@ -22,6 +22,7 @@ exports.startCodexMock = startCodexMock;
|
|
|
22
22
|
* `src/adapters/codex/runtime.ts` (`codexMockArgs`/`codexMockEnv`).
|
|
23
23
|
*/
|
|
24
24
|
const node_http_1 = __importDefault(require("node:http"));
|
|
25
|
+
const script_overrun_js_1 = require("../../core/script-overrun.js");
|
|
25
26
|
const MSG_ID = "msg_mock_1";
|
|
26
27
|
const RESP_ID = "resp_mock_1";
|
|
27
28
|
/**
|
|
@@ -183,8 +184,9 @@ function parseResponsesRequest(body) {
|
|
|
183
184
|
}
|
|
184
185
|
/**
|
|
185
186
|
* Start the scripted Codex Responses mock on a free port. Each
|
|
186
|
-
* `POST /v1/responses` consumes the next scripted turn
|
|
187
|
-
*
|
|
187
|
+
* `POST /v1/responses` consumes the next scripted turn; one past the end of the
|
|
188
|
+
* script is a 400, never a copy of the last turn (#340); HEAD and any other path
|
|
189
|
+
* get `{}`. Resolves to a handle
|
|
188
190
|
* with the base `url`, the `port`, the recorded `requests`, and `close()`.
|
|
189
191
|
*/
|
|
190
192
|
function startCodexMock(script, opts = {}) {
|
|
@@ -204,7 +206,17 @@ function startCodexMock(script, opts = {}) {
|
|
|
204
206
|
const parsed = parseResponsesRequest(body);
|
|
205
207
|
requests.push(parsed);
|
|
206
208
|
opts.onRequest?.(parsed);
|
|
207
|
-
const turn = script[
|
|
209
|
+
const turn = script[i];
|
|
210
|
+
if (turn === undefined) {
|
|
211
|
+
const message = (0, script_overrun_js_1.scriptOverrunMessage)({
|
|
212
|
+
scripted: script.length,
|
|
213
|
+
request: script.length + 1,
|
|
214
|
+
lastMessage: parsed.prompt,
|
|
215
|
+
});
|
|
216
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
217
|
+
res.end(JSON.stringify({ error: { type: "invalid_request_error", message } }));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
208
220
|
i++;
|
|
209
221
|
const model = parsed.model || "gpt-5-codex";
|
|
210
222
|
res.writeHead(200, {
|
package/dist/cli-flag-check.js
CHANGED
package/dist/cli-main.js
CHANGED
|
@@ -27,6 +27,7 @@ const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
|
27
27
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
28
28
|
const validate_js_1 = require("./core/validate.js");
|
|
29
29
|
const eval_lock_js_1 = require("./eval-lock.js");
|
|
30
|
+
const zod_1 = require("zod");
|
|
30
31
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
31
32
|
const cli_commands_js_1 = require("./cli-commands.js");
|
|
32
33
|
const cli_flag_check_js_1 = require("./cli-flag-check.js");
|
|
@@ -2636,6 +2637,9 @@ function vigilesWorkflow(plan, harnesses, hasPackageJson) {
|
|
|
2636
2637
|
- uses: actions/setup-node@v4
|
|
2637
2638
|
with:
|
|
2638
2639
|
node-version: "20"
|
|
2640
|
+
# No \`version:\` on purpose: the Action runs the vigiles your
|
|
2641
|
+
# package.json declares, so CI and a local \`npx vigiles\` cannot
|
|
2642
|
+
# drift onto two different pins.
|
|
2639
2643
|
- uses: zernie/vigiles@v1
|
|
2640
2644
|
`
|
|
2641
2645
|
: "";
|
|
@@ -2667,17 +2671,21 @@ function vigilesWorkflow(plan, harnesses, hasPackageJson) {
|
|
|
2667
2671
|
eval-check:
|
|
2668
2672
|
# Eval staleness gate — real-model evals run LOCALLY on your subscription
|
|
2669
2673
|
# (\`npx vigiles eval --update\`, which commits a lock); this job VERIFIES those
|
|
2670
|
-
# committed results against the current inputs with NO model call.
|
|
2671
|
-
#
|
|
2674
|
+
# committed results against the current inputs with NO model call.
|
|
2675
|
+
# \`--min=0\` is deliberate: a repo with no eval file yet matches nothing, and
|
|
2676
|
+
# \`vigiles eval --check\` fails on an empty match by default (a gate that
|
|
2677
|
+
# verified nothing must not read as a pass). With \`--min=0\` this job is green
|
|
2678
|
+
# until the first \`*.eval.*\` file exists; from then on it is red until that
|
|
2679
|
+
# eval's lock is committed. Raise the floor (\`--min=N\`) once you want
|
|
2680
|
+
# a vanished eval file to fail it. See docs/harness-testing.md.
|
|
2672
2681
|
runs-on: ubuntu-latest
|
|
2673
2682
|
steps:
|
|
2674
2683
|
- uses: actions/checkout@v4
|
|
2675
2684
|
- uses: actions/setup-node@v4
|
|
2676
2685
|
with:
|
|
2677
2686
|
node-version: "20"
|
|
2678
|
-
-
|
|
2679
|
-
|
|
2680
|
-
command: eval-check
|
|
2687
|
+
- run: npm install
|
|
2688
|
+
- run: npx vigiles eval --check --min=0
|
|
2681
2689
|
`
|
|
2682
2690
|
: "";
|
|
2683
2691
|
// No selectable jobs (e.g. `--no-lint --no-test`, or `--test` on a repo with no
|
|
@@ -3036,6 +3044,9 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
3036
3044
|
`staling the integrity hash.`);
|
|
3037
3045
|
return out;
|
|
3038
3046
|
}
|
|
3047
|
+
/** The two files `init`'s lint layer generates under `.vigiles/`. */
|
|
3048
|
+
const GENERATED_TYPES = ".vigiles/generated.d.ts";
|
|
3049
|
+
const GENERATED_SCHEMA = ".vigiles/schema.json";
|
|
3039
3050
|
/** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
|
|
3040
3051
|
* instruction file (so `--lint` always delivers a spec), but never compiles
|
|
3041
3052
|
* OVER a hand-written file — that is left to the adopt-spec skill. */
|
|
@@ -3081,7 +3092,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3081
3092
|
const outDir = (0, node_path_1.resolve)(cwd, ".vigiles");
|
|
3082
3093
|
if (!(0, node_fs_1.existsSync)(outDir))
|
|
3083
3094
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
3084
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd,
|
|
3095
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, GENERATED_TYPES), typesResult.dts);
|
|
3085
3096
|
for (const l of typesResult.linters) {
|
|
3086
3097
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
3087
3098
|
}
|
|
@@ -3089,14 +3100,14 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3089
3100
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
3090
3101
|
}
|
|
3091
3102
|
console.log("✓ Generated .vigiles/generated.d.ts");
|
|
3092
|
-
written.push(
|
|
3103
|
+
written.push(GENERATED_TYPES);
|
|
3093
3104
|
const schemaResult = (0, generate_schema_js_1.generateSchema)({
|
|
3094
3105
|
basePath: cwd,
|
|
3095
3106
|
linters: (0, validate_js_1.loadConfig)().linters,
|
|
3096
3107
|
});
|
|
3097
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd,
|
|
3108
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, GENERATED_SCHEMA), schemaResult.json);
|
|
3098
3109
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
3099
|
-
written.push(
|
|
3110
|
+
written.push(GENERATED_SCHEMA);
|
|
3100
3111
|
// Compile — but NEVER overwrite an existing hand-written file during `init`:
|
|
3101
3112
|
// we compile only GREENFIELD targets (the file doesn't exist yet) and targets
|
|
3102
3113
|
// we already manage (carry our hash). An ADOPTED file is left untouched — the
|
|
@@ -3609,8 +3620,24 @@ function printSetupSummary(opts) {
|
|
|
3609
3620
|
console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
|
|
3610
3621
|
}
|
|
3611
3622
|
}
|
|
3623
|
+
/** The project config file `init` reads and writes. */
|
|
3624
|
+
const VIGILESRC = ".vigilesrc.json";
|
|
3612
3625
|
async function setup(args) {
|
|
3613
3626
|
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
3627
|
+
// An already-adopted repo gets the MINIMAL re-run unless a flag asks for more
|
|
3628
|
+
// (#338): re-running `init` to pick up one new piece must not also wire CI,
|
|
3629
|
+
// widen the rules, scaffold files or rewrite a pin. Decided from disk, before
|
|
3630
|
+
// any prompt — the minimal run asks nothing.
|
|
3631
|
+
const state = (0, setup_plan_js_1.adoptionState)({
|
|
3632
|
+
hasConfig: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), VIGILESRC)),
|
|
3633
|
+
specs: detectProject()
|
|
3634
|
+
.instructionFiles.filter((f) => f.hasSpec)
|
|
3635
|
+
.map((f) => `${f.path}.spec.ts`),
|
|
3636
|
+
});
|
|
3637
|
+
if ((0, setup_plan_js_1.rerunScope)(parsed, state) === "minimal" && state.kind === "adopted") {
|
|
3638
|
+
minimalRerun(parsed, state);
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3614
3641
|
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
3615
3642
|
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
3616
3643
|
const prompted = (0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false);
|
|
@@ -3707,6 +3734,152 @@ async function setup(args) {
|
|
|
3707
3734
|
console.log("\nℹ Ran the standard setup. Already have a harness, or not a JS/Python repo, and want only the CI integrity gate (nothing installed)? Re-run `npx vigiles init --ci-only`.");
|
|
3708
3735
|
}
|
|
3709
3736
|
}
|
|
3737
|
+
/**
|
|
3738
|
+
* The minimal re-run on an adopted repo (#338): add what is missing AND safe and
|
|
3739
|
+
* local — the skill links and a missing devDependency — then say what a full
|
|
3740
|
+
* setup would add and the flag that does it. It writes NOTHING else: no CI
|
|
3741
|
+
* workflow, no `.vigilesrc.json` edit, no harness/schema scaffold, no spec for a
|
|
3742
|
+
* hand-written file, no edit to an existing version pin, no global install.
|
|
3743
|
+
* The decisions are pure (`setup-plan.ts`); this is the IO.
|
|
3744
|
+
*/
|
|
3745
|
+
function minimalRerun(parsed, state) {
|
|
3746
|
+
const plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
3747
|
+
const detected = detectProject();
|
|
3748
|
+
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)((0, validate_js_1.loadConfig)().harnesses));
|
|
3749
|
+
console.log(`vigiles is already set up here (found ${state.evidence}) — adding only what is missing.\n`);
|
|
3750
|
+
printDetection(detected, harnesses);
|
|
3751
|
+
const links = plan.plugin ? linkSkillsOnly(harnesses) : [];
|
|
3752
|
+
const devDep = plan.lint || plan.test ? addMissingVigilesDevDep() : [];
|
|
3753
|
+
const written = [...links, ...devDep];
|
|
3754
|
+
if (written.length === 0)
|
|
3755
|
+
console.log("\nNothing was missing.");
|
|
3756
|
+
const followUps = (0, setup_plan_js_1.fullSetupFollowUps)(fullSetupGap(plan, detected, harnesses));
|
|
3757
|
+
if (followUps.length > 0)
|
|
3758
|
+
console.log(`\n${followUps.join("\n")}`);
|
|
3759
|
+
if (written.length > 0)
|
|
3760
|
+
console.log(`\n Commit:\n git add ${written.join(" ")} && git commit -m "Update vigiles"`);
|
|
3761
|
+
}
|
|
3762
|
+
/**
|
|
3763
|
+
* Link the shipped skills for each harness that links them — the safe, local
|
|
3764
|
+
* half of `installPlugins`, without the global install or config wiring.
|
|
3765
|
+
* Returns the links created or rewritten.
|
|
3766
|
+
*/
|
|
3767
|
+
function linkSkillsOnly(harnesses) {
|
|
3768
|
+
return (0, setup_plan_js_1.planPluginInstall)(harnesses, { hasClaude: false }).flatMap((plan) => {
|
|
3769
|
+
const home = plan.linkSkills ? harnessSkillsHome(plan.harness) : null;
|
|
3770
|
+
if (home === null)
|
|
3771
|
+
return [];
|
|
3772
|
+
const outcome = linkVigilesSkills(home, process.cwd());
|
|
3773
|
+
console.log("");
|
|
3774
|
+
(0, skill_links_js_1.formatSkillLinks)(outcome).forEach((l) => {
|
|
3775
|
+
console.log(l);
|
|
3776
|
+
});
|
|
3777
|
+
return changedLinks(outcome);
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
/** Add vigiles to `devDependencies` only when nothing declares it. */
|
|
3781
|
+
function addMissingVigilesDevDep() {
|
|
3782
|
+
const pkgPath = (0, node_path_1.resolve)(process.cwd(), PACKAGE_JSON);
|
|
3783
|
+
if (!(0, node_fs_1.existsSync)(pkgPath))
|
|
3784
|
+
return [];
|
|
3785
|
+
const pkg = readPackageJson(pkgPath);
|
|
3786
|
+
if (pkg === null || pkg.name === "vigiles")
|
|
3787
|
+
return [];
|
|
3788
|
+
const spec = vigilesDepSpec();
|
|
3789
|
+
const devDependencies = (0, setup_plan_js_1.missingVigilesDevDep)(pkg, spec);
|
|
3790
|
+
if (devDependencies === null)
|
|
3791
|
+
return [];
|
|
3792
|
+
(0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify({ ...pkg, devDependencies }, null, 2) + "\n");
|
|
3793
|
+
console.log(`\n✓ Added vigiles@${spec} to devDependencies — run \`npm install\` to fetch it`);
|
|
3794
|
+
return [PACKAGE_JSON];
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* The fields of package.json the dependency decisions read. LOOSE on purpose:
|
|
3798
|
+
* every other key is carried through untouched, because the result is written
|
|
3799
|
+
* back and a re-run must not drop a field it never looked at.
|
|
3800
|
+
*/
|
|
3801
|
+
const packageDepsSchema = zod_1.z.looseObject({
|
|
3802
|
+
name: zod_1.z.string().optional(),
|
|
3803
|
+
dependencies: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
3804
|
+
devDependencies: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
3805
|
+
});
|
|
3806
|
+
/** package.json parsed for its dependency fields; null when unreadable. */
|
|
3807
|
+
function readPackageJson(path) {
|
|
3808
|
+
try {
|
|
3809
|
+
const parsed = packageDepsSchema.safeParse(JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8")));
|
|
3810
|
+
return parsed.success ? parsed.data : null;
|
|
3811
|
+
}
|
|
3812
|
+
catch {
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* What a full setup would add here, read from disk — the input to the
|
|
3818
|
+
* follow-up lines. It mirrors the full path's own conditions (the plan's
|
|
3819
|
+
* layers, the existence checks those steps make), so the list names what
|
|
3820
|
+
* `--full` would actually do rather than everything it could.
|
|
3821
|
+
*/
|
|
3822
|
+
function fullSetupGap(plan, detected, harnesses) {
|
|
3823
|
+
const cwd = process.cwd();
|
|
3824
|
+
const has = (rel) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, rel));
|
|
3825
|
+
const config = readProjectConfig((0, node_path_1.resolve)(cwd, VIGILESRC));
|
|
3826
|
+
const pillar1 = plan.lint && plan.scaffoldSpecs;
|
|
3827
|
+
const unspecced = pillar1
|
|
3828
|
+
? [
|
|
3829
|
+
...determineTargets(detected, undefined, [...harnesses]),
|
|
3830
|
+
...discoverAdoptableSurfaces(cwd),
|
|
3831
|
+
].filter((t, i, all) => all.indexOf(t) === i && !has(`${t}.spec.ts`))
|
|
3832
|
+
: [];
|
|
3833
|
+
const pkg = has(PACKAGE_JSON)
|
|
3834
|
+
? readPackageJson((0, node_path_1.resolve)(cwd, PACKAGE_JSON))
|
|
3835
|
+
: null;
|
|
3836
|
+
const declared = pkg?.devDependencies?.vigiles ?? pkg?.dependencies?.vigiles ?? null;
|
|
3837
|
+
const spec = vigilesDepSpec();
|
|
3838
|
+
return {
|
|
3839
|
+
workflow: plan.gha && !has(".github/workflows/vigiles.yml"),
|
|
3840
|
+
config: configGap(config, plan.lint),
|
|
3841
|
+
harnessTest: plan.test && !has("vigiles.harness.mjs"),
|
|
3842
|
+
unspecced,
|
|
3843
|
+
generated: pillar1 && !(has(GENERATED_TYPES) && has(GENERATED_SCHEMA)),
|
|
3844
|
+
installs: plan.plugin ? fullInstallLines(harnesses) : [],
|
|
3845
|
+
devDep: declared !== null && declared !== spec && pkg?.name !== "vigiles"
|
|
3846
|
+
? { from: declared, to: spec }
|
|
3847
|
+
: null,
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
/** Where `.vigilesrc.json` stands against the preset a full setup writes. */
|
|
3851
|
+
function configGap(config, lint) {
|
|
3852
|
+
if (config === null)
|
|
3853
|
+
return "missing";
|
|
3854
|
+
return config.extends === undefined && lint ? "no-preset" : "has-preset";
|
|
3855
|
+
}
|
|
3856
|
+
/** `.vigilesrc.json` as a plain object, or null when absent or unreadable. */
|
|
3857
|
+
function readProjectConfig(path) {
|
|
3858
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
3859
|
+
return null;
|
|
3860
|
+
try {
|
|
3861
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
3862
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
3863
|
+
? { ...raw }
|
|
3864
|
+
: null;
|
|
3865
|
+
}
|
|
3866
|
+
catch {
|
|
3867
|
+
return null;
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
/** The per-machine installs and config wiring the full plugin step would run. */
|
|
3871
|
+
function fullInstallLines(harnesses) {
|
|
3872
|
+
return (0, setup_plan_js_1.planPluginInstall)(harnesses, { hasClaude: true }).flatMap((plan) => [
|
|
3873
|
+
...(plan.globalInstallCarriesHooks && plan.commands.length > 0
|
|
3874
|
+
? [
|
|
3875
|
+
`install the ${plan.harness} plugin for this machine (${plan.commands.join(" && ")}) and declare it in the project settings`,
|
|
3876
|
+
]
|
|
3877
|
+
: []),
|
|
3878
|
+
...(plan.vendors
|
|
3879
|
+
? [`wire vigiles's nudge hooks into the ${plan.harness} config`]
|
|
3880
|
+
: []),
|
|
3881
|
+
]);
|
|
3882
|
+
}
|
|
3710
3883
|
/** Canonical, de-duplicated harness names — the keys `harnesses` gets. */
|
|
3711
3884
|
function harnessConfigKeys(harnesses) {
|
|
3712
3885
|
return [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
|
|
@@ -5026,10 +5199,11 @@ async function promptYesNo(question) {
|
|
|
5026
5199
|
* Resolve the eval LOCK env from the `eval` flags. `--update` records each named
|
|
5027
5200
|
* eval's report to a committed `.vigiles/eval-locks/<name>.lock.json` (run locally
|
|
5028
5201
|
* on your subscription); `--check` (CI) verifies the committed result against the
|
|
5029
|
-
* current inputs WITHOUT a model call. `--check`
|
|
5030
|
-
*
|
|
5031
|
-
*
|
|
5032
|
-
*
|
|
5202
|
+
* current inputs WITHOUT a model call. `--check` with no lock committed at all is
|
|
5203
|
+
* a FAILURE, not a green no-op ({@link refuseEvalCheckWithoutLocks}, #197): a gate
|
|
5204
|
+
* that has verified nothing must not read as one that passed.
|
|
5205
|
+
* `--check`+`--update` together is a usage error (exit 2). The behavior epoch
|
|
5206
|
+
* comes from `.vigilesrc.json` `eval.apiVersion` (committed).
|
|
5033
5207
|
*/
|
|
5034
5208
|
function resolveEvalLockEnv(args) {
|
|
5035
5209
|
const wantCheck = args.includes("--check");
|
|
@@ -5038,13 +5212,6 @@ function resolveEvalLockEnv(args) {
|
|
|
5038
5212
|
console.error("vigiles eval: --check and --update are mutually exclusive (one verifies, one records).");
|
|
5039
5213
|
process.exit(2);
|
|
5040
5214
|
}
|
|
5041
|
-
if (wantCheck &&
|
|
5042
|
-
!(0, eval_lock_js_1.anyLocksCommitted)((0, node_path_1.resolve)(process.cwd(), eval_lock_js_1.DEFAULT_LOCK_DIR))) {
|
|
5043
|
-
console.log("ℹ vigiles eval --check: no committed eval locks found — nothing to verify.\n" +
|
|
5044
|
-
" Run `vigiles eval --update` locally (on your subscription) and commit the\n" +
|
|
5045
|
-
" lock to enable the CI staleness gate.");
|
|
5046
|
-
return "skip";
|
|
5047
|
-
}
|
|
5048
5215
|
const env = {};
|
|
5049
5216
|
if (wantCheck)
|
|
5050
5217
|
env.VIGILES_EVAL_LOCK = "check";
|
|
@@ -5057,6 +5224,22 @@ function resolveEvalLockEnv(args) {
|
|
|
5057
5224
|
}
|
|
5058
5225
|
return env;
|
|
5059
5226
|
}
|
|
5227
|
+
/**
|
|
5228
|
+
* `eval --check` over eval files when no lock is committed anywhere: every one of
|
|
5229
|
+
* them has no recorded result to compare the current inputs against, so the gate
|
|
5230
|
+
* would verify nothing and pass. It used to return green BEFORE discovery, which
|
|
5231
|
+
* also bypassed the empty-match failure. Once any lock exists, a file without one
|
|
5232
|
+
* already fails as "lock missing" in the eval itself. Exits 1; returns otherwise.
|
|
5233
|
+
*/
|
|
5234
|
+
function refuseEvalCheckWithoutLocks(args, evalCount) {
|
|
5235
|
+
if (!args.includes("--check"))
|
|
5236
|
+
return;
|
|
5237
|
+
if ((0, eval_lock_js_1.anyLocksCommitted)((0, node_path_1.resolve)(process.cwd(), eval_lock_js_1.DEFAULT_LOCK_DIR)))
|
|
5238
|
+
return;
|
|
5239
|
+
console.error(`✗ vigiles eval --check: ${String(evalCount)} eval file(s) have no recorded result — no eval lock is committed, so there is nothing to verify the current inputs against.\n` +
|
|
5240
|
+
" Run `vigiles eval --update` locally (on your subscription) and commit the lock(s).");
|
|
5241
|
+
process.exit(1);
|
|
5242
|
+
}
|
|
5060
5243
|
/**
|
|
5061
5244
|
* Turn a completed run into `.vigiles/coverage.json` — the composition root for
|
|
5062
5245
|
* the execution tier of coverage (`coverage-artifact.ts`).
|
|
@@ -5231,6 +5414,31 @@ function gitHead(cwd) {
|
|
|
5231
5414
|
return "";
|
|
5232
5415
|
}
|
|
5233
5416
|
}
|
|
5417
|
+
/**
|
|
5418
|
+
* Why a `vigiles test` / `vigiles eval` run matched no file — the message names
|
|
5419
|
+
* what was looked for, so a stale path, a wrong glob and an empty default
|
|
5420
|
+
* discovery each say so. A DIRECTORY is the one stale-looking target whose cause
|
|
5421
|
+
* we actually know, so it is said instead of the three guesses: `vigiles test .`
|
|
5422
|
+
* used to reach `spawn("node", ["."])` and surface Node's module-resolution stack.
|
|
5423
|
+
*/
|
|
5424
|
+
function emptyMatchMessage(kind, targets, defaultGlob) {
|
|
5425
|
+
const dirs = targets.filter((a) => (0, node_fs_1.lstatSync)(a, { throwIfNoEntry: false })?.isDirectory() === true);
|
|
5426
|
+
if (dirs.length > 0) {
|
|
5427
|
+
return (`✗ vigiles ${kind}: ${dirs.join(", ")} ${dirs.length === 1 ? "is a directory" : "are directories"} — ` +
|
|
5428
|
+
`pass a file, or a glob like "${defaultGlob}".\n` +
|
|
5429
|
+
` A directory is not a script; nothing ran.`);
|
|
5430
|
+
}
|
|
5431
|
+
const expected = ` If an empty match is expected here, say so with --min=0.`;
|
|
5432
|
+
if (targets.length === 0) {
|
|
5433
|
+
return (`✗ vigiles ${kind}: NOTHING matched the default glob ${defaultGlob}\n` +
|
|
5434
|
+
` Nothing ran. An empty run is not a pass: a renamed or moved file looks like this.\n` +
|
|
5435
|
+
expected);
|
|
5436
|
+
}
|
|
5437
|
+
return (`✗ vigiles ${kind}: ${String(targets.length)} target(s) given and NOTHING matched — ` +
|
|
5438
|
+
`${targets.join(", ")}\n` +
|
|
5439
|
+
` Nothing ran. A stale path, a wrong glob, or a moved file all look like this.\n` +
|
|
5440
|
+
expected);
|
|
5441
|
+
}
|
|
5234
5442
|
async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
5235
5443
|
const cwd = process.cwd();
|
|
5236
5444
|
// The ignore file keeps NEW copies of `.vigiles/` local files out of git, but
|
|
@@ -5251,14 +5459,10 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5251
5459
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
5252
5460
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
5253
5461
|
// The eval LOCK flags (`--check`/`--update`) are resolved BEFORE file discovery
|
|
5254
|
-
// so mutual-exclusion
|
|
5255
|
-
// count. Returns the env to thread to scripts, or `"skip"` to exit green now.
|
|
5462
|
+
// so mutual-exclusion (exit 2) is honored regardless of file count.
|
|
5256
5463
|
let lockEnv = {};
|
|
5257
5464
|
if (kind === "eval") {
|
|
5258
|
-
|
|
5259
|
-
if (r === "skip")
|
|
5260
|
-
return;
|
|
5261
|
-
lockEnv = r;
|
|
5465
|
+
lockEnv = resolveEvalLockEnv(args);
|
|
5262
5466
|
}
|
|
5263
5467
|
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
5264
5468
|
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
@@ -5280,44 +5484,33 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5280
5484
|
process.exit(1);
|
|
5281
5485
|
}
|
|
5282
5486
|
if (files.length === 0) {
|
|
5283
|
-
// 🔴
|
|
5284
|
-
//
|
|
5285
|
-
//
|
|
5487
|
+
// 🔴 A RUN THAT MATCHES NO FILE IS A FAILURE, NAMED OR NOT.
|
|
5488
|
+
// An empty match used to be one silent exit 0, and it cost a real repository three
|
|
5489
|
+
// days of green CI verifying zero files: a named step ran
|
|
5286
5490
|
// `vigiles test .claude/pipeline/skills.harness.mjs` after that file had been split
|
|
5287
5491
|
// into one-per-skill, printed "No **/*.harness.* files found" and passed, right next
|
|
5288
|
-
// to a step that was red for the same root cause.
|
|
5289
|
-
//
|
|
5290
|
-
//
|
|
5291
|
-
//
|
|
5292
|
-
// reached its target, and every one of those is a defect. Bare discovery finding
|
|
5293
|
-
// nothing is just an empty repository, which is a legitimate place to stand and must
|
|
5294
|
-
// stay quiet.
|
|
5492
|
+
// to a step that was red for the same root cause. The first repair failed only the
|
|
5493
|
+
// NAMED case and kept bare discovery quiet ("an empty repository is a legitimate
|
|
5494
|
+
// place to stand") — and bare `vigiles test` is the line `init` writes into CI, so
|
|
5495
|
+
// the same green no-op was one rename away from the default setup (#197).
|
|
5295
5496
|
//
|
|
5296
|
-
// This is the default the field settled on: Jest and Vitest FAIL on no tests found
|
|
5297
|
-
// make you opt in with `--passWithNoTests`; pytest exits 5.
|
|
5298
|
-
//
|
|
5299
|
-
// `
|
|
5300
|
-
//
|
|
5301
|
-
// the
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
// the generic message above would now be true but unhelpful.
|
|
5307
|
-
const dirs = restArgs.filter((a) => (0, node_fs_1.lstatSync)(a, { throwIfNoEntry: false })?.isDirectory() === true);
|
|
5308
|
-
console.error(dirs.length > 0
|
|
5309
|
-
? `✗ vigiles ${kind}: ${dirs.join(", ")} ${dirs.length === 1 ? "is a directory" : "are directories"} — ` +
|
|
5310
|
-
`pass a file, or a glob like "${defaultGlob}".\n` +
|
|
5311
|
-
` A directory is not a script; nothing ran.`
|
|
5312
|
-
: `✗ vigiles ${kind}: ${String(restArgs.length)} target(s) given and NOTHING matched — ` +
|
|
5313
|
-
`${restArgs.join(", ")}\n` +
|
|
5314
|
-
` Nothing ran. A stale path, a wrong glob, or a moved file all look like this.\n` +
|
|
5315
|
-
` If an empty match is expected here, say so with --min=0.`);
|
|
5497
|
+
// This is the default the field settled on: Jest and Vitest FAIL on no tests found
|
|
5498
|
+
// and make you opt in with `--passWithNoTests`; pytest exits 5. Here there is no
|
|
5499
|
+
// such flag (a person who wants an empty run does not call the command), and
|
|
5500
|
+
// `--min=0` — the existing floor, spelled as zero — is the only way to say an empty
|
|
5501
|
+
// match is expected. `minFlag`, not `minRequired`: 0 is both the DEFAULT and that
|
|
5502
|
+
// explicit opt-out, so the VALUE cannot tell them apart — only the flag's presence
|
|
5503
|
+
// can. (Caught by a control: the first version read `minRequired === 0` and made
|
|
5504
|
+
// `--min=0` do nothing.)
|
|
5505
|
+
if (minFlag === undefined) {
|
|
5506
|
+
console.error(emptyMatchMessage(kind, restArgs, defaultGlob));
|
|
5316
5507
|
process.exit(1);
|
|
5317
5508
|
}
|
|
5318
5509
|
console.log(`No ${defaultGlob} files found.`);
|
|
5319
5510
|
return;
|
|
5320
5511
|
}
|
|
5512
|
+
if (kind === "eval")
|
|
5513
|
+
refuseEvalCheckWithoutLocks(args, files.length);
|
|
5321
5514
|
// Consent gate for a bare `vigiles eval`: it runs the REAL model on your
|
|
5322
5515
|
// subscription, and a no-target run discovered the whole tree — so never fan out
|
|
5323
5516
|
// over an unbounded glob without explicit intent. Mirrors audit's read-vs-run
|
|
@@ -5446,6 +5639,10 @@ function capabilitiesOfReport(report, dialect) {
|
|
|
5446
5639
|
const COMMAND_HELP = {
|
|
5447
5640
|
init: {
|
|
5448
5641
|
usage: " vigiles init [flags] Set up this repo — specs, plugin, and CI.",
|
|
5642
|
+
detail: [
|
|
5643
|
+
" Already set up? A re-run only adds what is missing (skill links, the devDependency);",
|
|
5644
|
+
" --full run the whole setup again",
|
|
5645
|
+
],
|
|
5449
5646
|
},
|
|
5450
5647
|
compile: { usage: " vigiles compile [files...] Compile .spec.ts → .md" },
|
|
5451
5648
|
eject: {
|