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.
@@ -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
- // A permission allowlist: an empty one approves nothing, so the flag is
297
- // left out (a bare `--allowedTools` exits before any model turn).
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
- const unconsumed = (0, mock_model_js_1.scriptUnconsumedWarning)(count, sideChannelCount);
523
- if (unconsumed !== undefined)
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
- const unconsumed = (0, mock_model_js_1.scriptUnconsumedWarning)(mock.count, mock.sideChannelCount ?? 0);
543
- if (unconsumed !== undefined)
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
  ]));
@@ -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 (the last turn repeats if the client asks for more);
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()`.
@@ -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 (the last turn repeats if the client asks for more);
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
- let reqBody = {};
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 last = JSON.stringify(reqBody.messages?.at(-1)?.content ?? "");
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: last.includes('"tool_result"'),
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);
@@ -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
- export declare function mergeProjectConfig(existing: Record<string, unknown>, opts: {
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