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/dist/harness-test.js
CHANGED
|
@@ -6,10 +6,14 @@ exports.parseSubagents = parseSubagents;
|
|
|
6
6
|
exports.parseResultEvent = parseResultEvent;
|
|
7
7
|
exports.parseOutput = parseOutput;
|
|
8
8
|
exports.parseHooks = parseHooks;
|
|
9
|
+
exports.toolAvailabilityList = toolAvailabilityList;
|
|
10
|
+
exports.unofferedScriptedTool = unofferedScriptedTool;
|
|
11
|
+
exports.unsupportedToolsFlag = unsupportedToolsFlag;
|
|
9
12
|
exports.buildClaudeArgs = buildClaudeArgs;
|
|
10
13
|
exports.parseClaudeRun = parseClaudeRun;
|
|
11
14
|
exports.harnessSpawnEnv = harnessSpawnEnv;
|
|
12
15
|
exports.claudeAvailable = claudeAvailable;
|
|
16
|
+
exports.warnUnconsumed = warnUnconsumed;
|
|
13
17
|
exports.runHarnessTest = runHarnessTest;
|
|
14
18
|
exports.runHarnessTestIn = runHarnessTestIn;
|
|
15
19
|
exports.runHarness = runHarness;
|
|
@@ -49,6 +53,7 @@ const node_fs_1 = require("node:fs");
|
|
|
49
53
|
const node_path_1 = require("node:path");
|
|
50
54
|
const adapter_conformance_js_1 = require("./adapter-conformance.js");
|
|
51
55
|
const check_count_js_1 = require("./check-count.js");
|
|
56
|
+
const script_overrun_js_1 = require("./core/script-overrun.js");
|
|
52
57
|
const coverage_probe_js_1 = require("./coverage-probe.js");
|
|
53
58
|
const runtime_js_1 = require("./adapters/claude-code/runtime.js");
|
|
54
59
|
const mock_model_js_1 = require("./mock-model.js");
|
|
@@ -274,6 +279,51 @@ function parseHooks(stdout) {
|
|
|
274
279
|
}
|
|
275
280
|
return hooks;
|
|
276
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* The first `claude` that has `--tools` (print mode). Found by diffing the
|
|
284
|
+
* published `@anthropic-ai/claude-code` tarballs: 2.0.30 has no such option,
|
|
285
|
+
* 2.0.31 does (the changelog does not list it; its 2.1.0 entry only extends the
|
|
286
|
+
* flag to interactive mode).
|
|
287
|
+
*/
|
|
288
|
+
const TOOLS_FLAG_MIN_CLAUDE = "2.0.31";
|
|
289
|
+
/**
|
|
290
|
+
* The names `--tools` should offer for an `allowedTools` list. `--allowedTools`
|
|
291
|
+
* takes permission rules (`Bash(git *)`); `--tools` takes tool NAMES, and a rule
|
|
292
|
+
* with a specifier there makes the CLI offer no tools at all. So the specifier is
|
|
293
|
+
* dropped (`Bash(git *)` → `Bash`) and repeats collapse. Pure.
|
|
294
|
+
*/
|
|
295
|
+
function toolAvailabilityList(allowed) {
|
|
296
|
+
const names = allowed.map((rule) => rule.replace(/\(.*$/, "").trim());
|
|
297
|
+
return [...new Set(names.filter((n) => n !== ""))];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* A scripted call to a tool an EXPLICIT `allowedTools` does not offer. The CLI
|
|
301
|
+
* answers it with "No such tool available" and the run goes on, so a test that
|
|
302
|
+
* scripts the call and then asserts on something else passes over a step that
|
|
303
|
+
* never happened. Returns the message to throw, or undefined. MCP tools are not
|
|
304
|
+
* built-ins, so `--tools` does not withhold them and they are skipped. Pure.
|
|
305
|
+
*/
|
|
306
|
+
function unofferedScriptedTool(model, allowed) {
|
|
307
|
+
if (allowed === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
const offered = toolAvailabilityList(allowed);
|
|
310
|
+
const at = model.findIndex((t) => t.tool !== undefined &&
|
|
311
|
+
!t.tool.startsWith("mcp__") &&
|
|
312
|
+
!offered.includes(t.tool));
|
|
313
|
+
return at < 0
|
|
314
|
+
? undefined
|
|
315
|
+
: `scripted call to "${model[at]?.tool ?? ""}" on turn ${String(at + 1)}, but this run offers only: ${offered.join(", ")} — add it to allowedTools`;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A run on a `claude` that predates `--tools` exits at argument parsing, and
|
|
319
|
+
* commander's "unknown option" says nothing about WHY vigiles passed it. Returns
|
|
320
|
+
* the message to throw, or undefined when the run did not die that way. Pure.
|
|
321
|
+
*/
|
|
322
|
+
function unsupportedToolsFlag(stderr) {
|
|
323
|
+
return /unknown option '--tools'/.test(stderr)
|
|
324
|
+
? `this \`claude\` does not know \`--tools\`, which runHarnessTest uses to withhold every tool not in \`allowedTools\` (#252). Update Claude Code to ${TOOLS_FLAG_MIN_CLAUDE} or newer. Running without it would hand the agent every tool while the test reads as if it were fenced.`
|
|
325
|
+
: undefined;
|
|
326
|
+
}
|
|
277
327
|
/**
|
|
278
328
|
* The `claude` CLI argv for a harness run (shared by the direct and sandboxed
|
|
279
329
|
* paths). `ANTHROPIC_BASE_URL` is set by the caller's environment / wrapper, not
|
|
@@ -293,8 +343,17 @@ function buildClaudeArgs(spec, hasSettings) {
|
|
|
293
343
|
? ["--plugin-dir", (0, node_path_1.resolve)(spec.pluginDir)]
|
|
294
344
|
: []),
|
|
295
345
|
...(hasSettings ? ["--settings", "settings.json"] : []),
|
|
296
|
-
//
|
|
297
|
-
//
|
|
346
|
+
// An EXPLICIT list restricts: `--tools` is the AVAILABILITY list, a tool left
|
|
347
|
+
// out is not offered at all. `--allowedTools` only PRE-APPROVES (it never
|
|
348
|
+
// restricts), so on its own it left every unnamed tool runnable (#252). An
|
|
349
|
+
// empty list is `--tools ""` = no tools. With no list, nothing is withheld:
|
|
350
|
+
// the default four are pre-approved and every other tool stays available.
|
|
351
|
+
...(spec.allowedTools === undefined
|
|
352
|
+
? []
|
|
353
|
+
: ["--tools", toolAvailabilityList(spec.allowedTools).join(",")]),
|
|
354
|
+
// Pre-approval for the offered tools, so none stops on a permission prompt
|
|
355
|
+
// (headless). An empty one approves nothing, so the flag is left out (a bare
|
|
356
|
+
// `--allowedTools` exits before any model turn).
|
|
298
357
|
...(tools.length === 0 ? [] : ["--allowedTools", ...tools]),
|
|
299
358
|
];
|
|
300
359
|
}
|
|
@@ -415,6 +474,26 @@ exports.claudeCodeDriver = {
|
|
|
415
474
|
parseRun: parseClaudeRun,
|
|
416
475
|
available: claudeAvailable,
|
|
417
476
|
};
|
|
477
|
+
/** Say so on stderr when side-channel calls arrived and no script turn was served. */
|
|
478
|
+
function warnUnconsumed(count, sideChannelCount) {
|
|
479
|
+
const unconsumed = (0, mock_model_js_1.scriptUnconsumedWarning)(count, sideChannelCount);
|
|
480
|
+
if (unconsumed !== undefined)
|
|
481
|
+
console.error(unconsumed);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* A run that proves nothing about the script fails the test instead of coming
|
|
485
|
+
* back as a result: the `claude` on PATH predates `--tools` (#252), or the agent
|
|
486
|
+
* asked for a model turn the script did not have and the mock answered with an
|
|
487
|
+
* error rather than an invented turn (#340). Removes the run's directory, since
|
|
488
|
+
* no result is handed back to clean it up.
|
|
489
|
+
*/
|
|
490
|
+
function assertRunSound(cwd, stderr, scripted, modelRequests) {
|
|
491
|
+
const problem = unsupportedToolsFlag(stderr) ?? (0, script_overrun_js_1.overrunMessageFor)(scripted, modelRequests);
|
|
492
|
+
if (problem === undefined)
|
|
493
|
+
return;
|
|
494
|
+
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
495
|
+
throw new Error(problem);
|
|
496
|
+
}
|
|
418
497
|
/* v8 ignore start -- spawns the real agent CLI + filesystem; exercised by the
|
|
419
498
|
claude-backed + gated codex suites, excluded from the deterministic coverage
|
|
420
499
|
gate (the parse helpers above carry the testable logic). */
|
|
@@ -457,8 +536,22 @@ function makeResult(cwd, out, parsed, turns, modelRequests) {
|
|
|
457
536
|
* only — requesting confinement for another harness throws.
|
|
458
537
|
*/
|
|
459
538
|
async function runHarnessTest(spec, opts = {}) {
|
|
539
|
+
refuseUnrunnableSpec(spec);
|
|
460
540
|
return runHarnessTestIn(spec, opts, "inherit");
|
|
461
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* The scripted model tells an agent turn from the CLI's own bookkeeping calls by
|
|
544
|
+
* the tools the request declares (`isMainLoopRequest`). An agent with none is
|
|
545
|
+
* never served a script turn, and the run would decide on nothing.
|
|
546
|
+
*/
|
|
547
|
+
function refuseUnrunnableSpec(spec) {
|
|
548
|
+
if (spec.allowedTools?.length === 0) {
|
|
549
|
+
throw new Error("allowedTools: [] leaves the agent with no tools, and the scripted model only serves a turn to a request that declares tools — no script turn would be consumed. Name the tools the agent has.");
|
|
550
|
+
}
|
|
551
|
+
const unoffered = unofferedScriptedTool(spec.model, spec.allowedTools);
|
|
552
|
+
if (unoffered !== undefined)
|
|
553
|
+
throw new Error(unoffered);
|
|
554
|
+
}
|
|
462
555
|
/**
|
|
463
556
|
* {@link runHarnessTest} with the run's HOME chosen by the caller — see
|
|
464
557
|
* {@link HarnessRunHome}. Not on a public entry point: the one caller that needs
|
|
@@ -491,13 +584,12 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
491
584
|
const { files, settings, check } = fixtureFor(spec, opts.adapter);
|
|
492
585
|
const cwd = (0, tmp_root_js_1.makeTmpDir)("harness");
|
|
493
586
|
writeFixture(cwd, files, settings);
|
|
494
|
-
const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
|
|
495
587
|
const timeoutMs = spec.timeoutMs ?? 60000;
|
|
496
588
|
const buildArgs = (mockArgs) => driver.buildArgs({
|
|
497
589
|
prompt: spec.prompt ?? "go",
|
|
498
590
|
cwd,
|
|
499
591
|
hasSettings: settings !== undefined,
|
|
500
|
-
tools,
|
|
592
|
+
tools: spec.allowedTools,
|
|
501
593
|
transcript: spec.transcript ?? false,
|
|
502
594
|
pluginDir: spec.pluginDir,
|
|
503
595
|
mockArgs,
|
|
@@ -519,9 +611,8 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
519
611
|
// the direct one stays right. Same recovery feeds the unconsumed-script
|
|
520
612
|
// warning, which the sandbox path could not emit at all before.
|
|
521
613
|
const { count, sideChannelCount } = (0, mock_model_js_1.splitRequestCounts)(out.requests);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
console.error(unconsumed);
|
|
614
|
+
warnUnconsumed(count, sideChannelCount);
|
|
615
|
+
assertRunSound(cwd, "", spec.model.length, out.requests);
|
|
525
616
|
return check(makeResult(cwd, out, parseClaudeRun(out.stdout), count, out.requests));
|
|
526
617
|
}
|
|
527
618
|
// Direct path: mock runs in this process; the agent reaches it over localhost.
|
|
@@ -539,9 +630,8 @@ async function runHarnessTestIn(spec, opts, home) {
|
|
|
539
630
|
// looks empty. `scriptUnconsumedWarning` names the one shape that produces
|
|
540
631
|
// it (every request arriving without tool declarations, so nothing looked
|
|
541
632
|
// like an agent turn) instead of leaving it to be rediscovered.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
console.error(unconsumed);
|
|
633
|
+
warnUnconsumed(mock.count, mock.sideChannelCount ?? 0);
|
|
634
|
+
assertRunSound(cwd, out.stderr, spec.model.length, mock.requests);
|
|
545
635
|
return check(makeResult(cwd, out, driver.parseRun(out.stdout), mock.count, [
|
|
546
636
|
...mock.requests,
|
|
547
637
|
]));
|
package/dist/mock-model.d.ts
CHANGED
|
@@ -78,7 +78,10 @@ export declare function extractRequest(body: {
|
|
|
78
78
|
}): ModelRequest;
|
|
79
79
|
/**
|
|
80
80
|
* Start the scripted mock on a free port. Each MAIN-LOOP `/v1/messages` POST
|
|
81
|
-
* consumes the next turn
|
|
81
|
+
* consumes the next turn; one past the end of the script is answered with a
|
|
82
|
+
* non-retryable API error, never a copy of the last turn (#340 — a looping
|
|
83
|
+
* agent would pass on turns nobody scripted), and `runHarnessTest` then fails
|
|
84
|
+
* the test from the recorded requests ({@link findScriptOverrun});
|
|
82
85
|
* a SIDE-CHANNEL POST — the CLI's own bookkeeping calls, see
|
|
83
86
|
* {@link isMainLoopRequest} — is answered from outside the script and consumes
|
|
84
87
|
* nothing. Resolves to a handle with the base `url` and a `close()`.
|
package/dist/mock-model.js
CHANGED
|
@@ -28,6 +28,7 @@ exports.startMock = startMock;
|
|
|
28
28
|
* and echoing the requested model.
|
|
29
29
|
*/
|
|
30
30
|
const node_http_1 = __importDefault(require("node:http"));
|
|
31
|
+
const script_overrun_js_1 = require("./core/script-overrun.js");
|
|
31
32
|
/** Build a scripted model from an ordered list of turns. */
|
|
32
33
|
function scriptModel(turns) {
|
|
33
34
|
return [...turns];
|
|
@@ -388,9 +389,41 @@ function extractRequest(body) {
|
|
|
388
389
|
: [];
|
|
389
390
|
return { system: flattenContent(body.system), messages };
|
|
390
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* The 400 body for an agent request past the end of the script: an API error,
|
|
394
|
+
* because the CLI retries 429/5xx and an invented turn would let the agent act on
|
|
395
|
+
* a step nobody scripted (#340). Pure.
|
|
396
|
+
*/
|
|
397
|
+
function pastScriptBody(scripted, request) {
|
|
398
|
+
const message = (0, script_overrun_js_1.scriptOverrunMessage)({
|
|
399
|
+
scripted,
|
|
400
|
+
request: scripted + 1,
|
|
401
|
+
lastMessage: request.messages.at(-1)?.text ?? "",
|
|
402
|
+
});
|
|
403
|
+
return JSON.stringify({
|
|
404
|
+
type: "error",
|
|
405
|
+
error: { type: "invalid_request_error", message },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** Does a message's content carry a tool result (the agent reacting to a tool)? */
|
|
409
|
+
function endsWithToolResult(content) {
|
|
410
|
+
return JSON.stringify(content ?? "").includes('"tool_result"');
|
|
411
|
+
}
|
|
412
|
+
/** The JSON body of a request, `{}` for the HEAD / health checks that have none. */
|
|
413
|
+
function parseReqBody(body) {
|
|
414
|
+
try {
|
|
415
|
+
return JSON.parse(body);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return {};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
391
421
|
/**
|
|
392
422
|
* Start the scripted mock on a free port. Each MAIN-LOOP `/v1/messages` POST
|
|
393
|
-
* consumes the next turn
|
|
423
|
+
* consumes the next turn; one past the end of the script is answered with a
|
|
424
|
+
* non-retryable API error, never a copy of the last turn (#340 — a looping
|
|
425
|
+
* agent would pass on turns nobody scripted), and `runHarnessTest` then fails
|
|
426
|
+
* the test from the recorded requests ({@link findScriptOverrun});
|
|
394
427
|
* a SIDE-CHANNEL POST — the CLI's own bookkeeping calls, see
|
|
395
428
|
* {@link isMainLoopRequest} — is answered from outside the script and consumes
|
|
396
429
|
* nothing. Resolves to a handle with the base `url` and a `close()`.
|
|
@@ -406,13 +439,7 @@ function startMock(script, opts = {}) {
|
|
|
406
439
|
const url = req.url ?? "";
|
|
407
440
|
const isCount = url.includes("count_tokens");
|
|
408
441
|
const isMessages = url.includes("/v1/messages") && !isCount;
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
reqBody = JSON.parse(body);
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
/* HEAD / health checks have no JSON body */
|
|
415
|
-
}
|
|
442
|
+
const reqBody = parseReqBody(body);
|
|
416
443
|
if (req.method === "HEAD" || (!isMessages && !isCount)) {
|
|
417
444
|
res.writeHead(200, { "content-type": "application/json" });
|
|
418
445
|
res.end("{}");
|
|
@@ -445,13 +472,17 @@ function startMock(script, opts = {}) {
|
|
|
445
472
|
jsonTurn(res, reply, model);
|
|
446
473
|
return;
|
|
447
474
|
}
|
|
448
|
-
const
|
|
475
|
+
const turn = script[i];
|
|
476
|
+
if (turn === undefined) {
|
|
477
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
478
|
+
res.end(pastScriptBody(script.length, request));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
449
481
|
opts.onTurn?.({
|
|
450
482
|
n: i,
|
|
451
483
|
stream: reqBody.stream === true,
|
|
452
|
-
hasToolResult:
|
|
484
|
+
hasToolResult: endsWithToolResult(reqBody.messages?.at(-1)?.content),
|
|
453
485
|
});
|
|
454
|
-
const turn = script[Math.min(i, script.length - 1)] ?? { text: "" };
|
|
455
486
|
i++;
|
|
456
487
|
if (reqBody.stream === true)
|
|
457
488
|
streamTurn(res, turn, model);
|
package/dist/setup-plan.d.ts
CHANGED
|
@@ -77,6 +77,11 @@ export interface ParsedSetupArgs {
|
|
|
77
77
|
* DISCOVERY row of `cohesive-feature-delivery`.
|
|
78
78
|
*/
|
|
79
79
|
ciOnly: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* `--full` — run the whole setup even on a repo that already uses vigiles.
|
|
82
|
+
* Without it, a re-run there is the minimal one (see {@link rerunScope}).
|
|
83
|
+
*/
|
|
84
|
+
full: boolean;
|
|
80
85
|
}
|
|
81
86
|
/** Parse `init` args into the choices the user pinned. The two pillars are
|
|
82
87
|
* selected with `--lint` / `--test`. */
|
|
@@ -87,29 +92,6 @@ export declare function parseSetupArgs(args: readonly string[]): ParsedSetupArgs
|
|
|
87
92
|
* raw files WITHOUT scaffolding a spec (gate-first-adoption). Non-interactive
|
|
88
93
|
* behaviour is unchanged: bare `init` (lint on) scaffolds as before. */
|
|
89
94
|
export declare function defaultPlan(strict?: boolean): SetupPlan;
|
|
90
|
-
/**
|
|
91
|
-
* Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
|
|
92
|
-
* the `harness` if absent, add strict rule severities if `--strict`, NEVER
|
|
93
|
-
* clobber an existing key. Returns the merged config, or `null` when nothing
|
|
94
|
-
* changed (so the IO layer skips the write). The IO (read/parse/write + the
|
|
95
|
-
* malformed-file guard) stays in cli.ts.
|
|
96
|
-
*/
|
|
97
|
-
/**
|
|
98
|
-
* The structural rules `init` gates BY DEFAULT (severity `error`, so a broken
|
|
99
|
-
* surface fails `vigiles lint`). Every one is HIGH-PRECISION / FP-safe — it fires
|
|
100
|
-
* only on a genuine defect (a never-available/typo'd tool, a subagent missing
|
|
101
|
-
* `name`/`description`, a typo'd hook event, a dead hook script, a broken MCP
|
|
102
|
-
* ref, two skills that collide in the selector) — so a well-formed plugin stays
|
|
103
|
-
* green and catching real breakage out of the box never cries wolf.
|
|
104
|
-
*
|
|
105
|
-
* Deliberately EXCLUDES `require-instructions-spec` and the workflow-forcing rules:
|
|
106
|
-
* those make a CLEAN repo fail (you simply haven't written the spec/test yet), so
|
|
107
|
-
* they stay opt-in under `--strict` (progressive adoption — see
|
|
108
|
-
* `STRICT_EXTRA_RULES`).
|
|
109
|
-
*
|
|
110
|
-
* This is the **`structural`** rule group (see research/install-enforcement-dx.md).
|
|
111
|
-
*/
|
|
112
|
-
export declare const STRUCTURAL_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap"];
|
|
113
95
|
/**
|
|
114
96
|
* The **`workflow`** group — the WORKFLOW-FORCING / opinionated tier `--strict`
|
|
115
97
|
* gates, which a clean repo can still fail because you haven't done the work yet:
|
|
@@ -135,7 +117,24 @@ export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "unt
|
|
|
135
117
|
* (research/install-enforcement-dx.md).
|
|
136
118
|
*/
|
|
137
119
|
export declare const NUDGE_RULES: readonly ["skill-description-budget", "frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs", "lethal-trifecta", "skill-resource-resolves", "skill-missing-fence", "plugin-dir-layout", "delegation-trifecta", "hook-block-ineffective", "hook-matcher", "doc-refs"];
|
|
138
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
|
|
122
|
+
* the harnesses if absent, gate the structural rules by EXTENDING the
|
|
123
|
+
* `vigiles:recommended` preset (never by writing each rule), add the workflow
|
|
124
|
+
* rules on `--strict`, and NEVER clobber an existing key or severity. Returns
|
|
125
|
+
* the merged config, or `null` when nothing changed (so the IO layer skips the
|
|
126
|
+
* write). The IO (read/parse/write + the malformed-file guard) stays in cli.ts.
|
|
127
|
+
*
|
|
128
|
+
* 🔴 THE PRESET, NOT NINE LINES (#338). Written lines are a snapshot of the
|
|
129
|
+
* structural group on the day `init` ran, so a repo adopted earlier never got a
|
|
130
|
+
* rule added to the group later; the preset is resolved by the loader on every
|
|
131
|
+
* run (`src/core/presets.ts`).
|
|
132
|
+
*
|
|
133
|
+
* `--report-only` is the exception and stays EXPLICIT: it promises that nothing
|
|
134
|
+
* fails CI, and extending a preset that later gains a rule at `error` would
|
|
135
|
+
* break that promise on an upgrade. So it writes the same rules at `warn`.
|
|
136
|
+
*/
|
|
137
|
+
export declare function mergeProjectConfig(existing: Readonly<Record<string, unknown>>, opts: Readonly<{
|
|
139
138
|
/** Canonical harness names this repo targets, in the order to declare them. */
|
|
140
139
|
harnesses: readonly string[];
|
|
141
140
|
strict: boolean;
|
|
@@ -144,7 +143,7 @@ export declare function mergeProjectConfig(existing: Record<string, unknown>, op
|
|
|
144
143
|
* concern, so a test-only setup (`init --test` / `--no-lint`) records the
|
|
145
144
|
* harness but writes NO lint rules. */
|
|
146
145
|
lint?: boolean;
|
|
147
|
-
}): Record<string, unknown> | null;
|
|
146
|
+
}>): Record<string, unknown> | null;
|
|
148
147
|
/**
|
|
149
148
|
* Whether to drop into interactive prompts: a human at a TTY who passed neither
|
|
150
149
|
* `--yes` nor an explicit `--target`, and who hasn't already pinned every choice
|
|
@@ -292,6 +291,75 @@ export declare function codexPluginHooks(): CodexPluginHook[];
|
|
|
292
291
|
* (and every other config key) untouched. Returns a new object.
|
|
293
292
|
*/
|
|
294
293
|
export declare function applyCodexPluginHooks(existing: Record<string, unknown>): Record<string, unknown>;
|
|
294
|
+
/**
|
|
295
|
+
* Whether this repo already uses vigiles, decided from what is ON DISK.
|
|
296
|
+
*
|
|
297
|
+
* 🔴 THE SIGNAL IS `.vigilesrc.json` OR A SPEC NEXT TO AN INSTRUCTION FILE, and
|
|
298
|
+
* deliberately NOT a `vigiles` devDependency: the documented first step on a new
|
|
299
|
+
* repo can be `npm i -D vigiles` followed by `npx vigiles init`, and that run
|
|
300
|
+
* must still be the full setup. Every `init` writes `.vigilesrc.json` (it records
|
|
301
|
+
* the harnesses even on a test-only run), and a hand-written one means the owner
|
|
302
|
+
* has already configured vigiles. A spec beside `CLAUDE.md`/`AGENTS.md` covers a
|
|
303
|
+
* repo adopted before the config file existed.
|
|
304
|
+
*/
|
|
305
|
+
export type AdoptionState = {
|
|
306
|
+
readonly kind: "fresh";
|
|
307
|
+
} | {
|
|
308
|
+
readonly kind: "adopted";
|
|
309
|
+
readonly evidence: string;
|
|
310
|
+
};
|
|
311
|
+
/** {@link AdoptionState} from the two disk facts that decide it. */
|
|
312
|
+
export declare function adoptionState(facts: Readonly<{
|
|
313
|
+
hasConfig: boolean;
|
|
314
|
+
specs: readonly string[];
|
|
315
|
+
}>): AdoptionState;
|
|
316
|
+
/**
|
|
317
|
+
* Which setup a run performs: the full one, or the minimal re-run (#338).
|
|
318
|
+
*
|
|
319
|
+
* MINIMAL only when the repo is adopted AND the run named nothing that asks for
|
|
320
|
+
* more. A flag that selects or widens the setup (`--full`, `--strict`,
|
|
321
|
+
* `--ci-only`, `--report-only`, `--force`, `--target=`, a positive `--lint` /
|
|
322
|
+
* `--test`) is an explicit request and keeps today's behaviour; the narrowing
|
|
323
|
+
* flags (`--no-*`, `--harness=`, `--yes`) do not, because they can only make a
|
|
324
|
+
* run do less.
|
|
325
|
+
*/
|
|
326
|
+
export declare function rerunScope(parsed: Readonly<ParsedSetupArgs>, state: AdoptionState): "full" | "minimal";
|
|
327
|
+
/**
|
|
328
|
+
* The `devDependencies` a minimal re-run writes, or null to leave package.json
|
|
329
|
+
* alone. It ADDS vigiles when no dependency field declares it and never edits an
|
|
330
|
+
* existing declaration: rewriting `^33.3.0` to `^33` loosens a pin the owner
|
|
331
|
+
* chose, and moving it between fields is a decision, not a missing piece.
|
|
332
|
+
*/
|
|
333
|
+
export declare function missingVigilesDevDep(pkg: Readonly<{
|
|
334
|
+
dependencies?: Readonly<Record<string, string>>;
|
|
335
|
+
devDependencies?: Readonly<Record<string, string>>;
|
|
336
|
+
}>, spec: string): Readonly<Record<string, string>> | null;
|
|
337
|
+
/** What a full setup would add on an adopted repo — the facts that decide it. */
|
|
338
|
+
export interface FullSetupGap {
|
|
339
|
+
/** `.github/workflows/vigiles.yml` is absent (and CI wiring is not opted out). */
|
|
340
|
+
readonly workflow: boolean;
|
|
341
|
+
/** `.vigilesrc.json`: absent, present without the preset, or already extending it. */
|
|
342
|
+
readonly config: "missing" | "no-preset" | "has-preset";
|
|
343
|
+
/** `vigiles.harness.mjs` is absent (and the test layer is not opted out). */
|
|
344
|
+
readonly harnessTest: boolean;
|
|
345
|
+
/** Instruction files and surfaces a full setup would adopt into a spec. */
|
|
346
|
+
readonly unspecced: readonly string[];
|
|
347
|
+
/** `.vigiles/generated.d.ts` / `.vigiles/schema.json` would be created. */
|
|
348
|
+
readonly generated: boolean;
|
|
349
|
+
/** The global plugin install / hook wiring a full setup would run, one line each. */
|
|
350
|
+
readonly installs: readonly string[];
|
|
351
|
+
/** The devDependency rewrite a full setup would make, if any. */
|
|
352
|
+
readonly devDep: Readonly<{
|
|
353
|
+
from: string;
|
|
354
|
+
to: string;
|
|
355
|
+
}> | null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The lines a minimal re-run prints about what it did NOT do: each thing a full
|
|
359
|
+
* setup would add here, then the flag that does it. Empty when nothing is
|
|
360
|
+
* missing, so a fully set-up repo is not told to run anything.
|
|
361
|
+
*/
|
|
362
|
+
export declare function fullSetupFollowUps(gap: FullSetupGap): readonly string[];
|
|
295
363
|
/**
|
|
296
364
|
* Resolve the final plan: defaults, then flags, then interactive answers (each
|
|
297
365
|
* layer overrides the previous only where it has an opinion). `--target` pins a
|