vigiles 7.0.0 → 9.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/README.md +207 -88
- package/dist/adoptability.d.ts +55 -0
- package/dist/adoptability.js +196 -0
- package/dist/audit-html.d.ts +20 -0
- package/dist/audit-html.js +61 -0
- package/dist/audit-prompts.d.ts +46 -0
- package/dist/audit-prompts.js +90 -0
- package/dist/audit-report.d.ts +70 -0
- package/dist/audit-report.js +51 -0
- package/dist/audit-report.template.html +110 -0
- package/dist/audit-score.d.ts +44 -0
- package/dist/audit-score.js +221 -0
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +3 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +749 -180
- package/dist/core/adopt.d.ts +65 -0
- package/dist/core/adopt.js +199 -0
- package/dist/core/compose.d.ts +1 -1
- package/dist/core/compose.js +1 -1
- package/dist/core/evolve.d.ts +4 -0
- package/dist/core/evolve.js +4 -0
- package/dist/core/frontmatter.d.ts +8 -7
- package/dist/core/frontmatter.js +8 -7
- package/dist/core/generate-harness.d.ts +1 -1
- package/dist/core/generate-harness.js +3 -3
- package/dist/core/generate-schema.js +1 -1
- package/dist/core/inline.d.ts +6 -6
- package/dist/core/inline.js +17 -7
- package/dist/core/integrity.d.ts +31 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/orphans.js +1 -1
- package/dist/core/spec.d.ts +40 -2
- package/dist/core/spec.js +16 -1
- package/dist/core/types.d.ts +42 -6
- package/dist/core/validate.js +26 -26
- package/dist/dialect-drift.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/guardrail-check.d.ts +1 -1
- package/dist/guardrail-check.js +1 -1
- package/dist/optimize.d.ts +12 -5
- package/dist/optimize.js +27 -5
- package/dist/scan-behavioral.d.ts +8 -2
- package/dist/scan-behavioral.js +6 -4
- package/dist/scan-trigger-suggest.d.ts +91 -0
- package/dist/scan-trigger-suggest.js +103 -0
- package/dist/scan.d.ts +53 -12
- package/dist/scan.js +92 -16
- package/dist/score-explainer.d.ts +1 -1
- package/dist/setup-plan.d.ts +59 -1
- package/dist/setup-plan.js +103 -5
- package/hooks/post-edit.sh +1 -1
- package/package.json +4 -2
- package/skills/adopt-spec/SKILL.md +7 -7
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/strengthen/SKILL.md +1 -1
- package/skills/test-harness/SKILL.md +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit → the ONE read-vs-run decision. `audit` is a Lighthouse-style LOCAL
|
|
3
|
+
* report: a deterministic READ by default — safe + identical on every OS, nothing
|
|
4
|
+
* executes — NOT a CI step (CI uses `vigiles lint`, the deterministic gate). The
|
|
5
|
+
* executing checks (safety battery, live MCP resolution, skill-firing trigger-rate)
|
|
6
|
+
* run ONLY when there's a human to consent: at a TTY `audit` ASKS once (and
|
|
7
|
+
* remembers in `.vigilesrc.json` `audit.measure`); headless (an agent / `--json` /
|
|
8
|
+
* `--no-interactive` / a pipe) it stays a read + a one-line nudge — never hangs,
|
|
9
|
+
* never silently executes. There is deliberately NO execution flag: automation
|
|
10
|
+
* tests the harness through the `vigiles/testing` API + skills (the layered tiers),
|
|
11
|
+
* not through the report verb. The IO (prompt / run / remember) lives in the CLI;
|
|
12
|
+
* this is the pure decision + helpers.
|
|
13
|
+
*/
|
|
14
|
+
/** Only the env vars that signal a reachable model (parse, don't validate). */
|
|
15
|
+
export interface ModelEnv {
|
|
16
|
+
readonly ANTHROPIC_API_KEY?: string;
|
|
17
|
+
readonly CLAUDECODE?: string;
|
|
18
|
+
readonly CLAUDE_CODE_ENTRYPOINT?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Is a real model reachable for the trigger tier? Either a metered API key
|
|
22
|
+
* (`ANTHROPIC_API_KEY`), OR an authenticated Claude Code session (`CLAUDECODE=1`
|
|
23
|
+
* / `CLAUDE_CODE_ENTRYPOINT`, web/desktop/CLI) — the latter drives the `claude`
|
|
24
|
+
* CLI on the user's subscription, no key needed and $0 metered. A tiny env-only
|
|
25
|
+
* predicate (not a live probe), so it never spends a token just to decide.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hasModelAccess(env: ModelEnv): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Is the reachable model METERED (a paid API key) rather than a subscription?
|
|
30
|
+
* Only affects the consent DISCLOSURE wording (a metered key bills per token; a
|
|
31
|
+
* subscription is $0 metered) — the run/skip decision itself is consent-driven,
|
|
32
|
+
* not metered-driven.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isMeteredAccess(env: ModelEnv): boolean;
|
|
35
|
+
/** Why the executing checks were skipped (drives the "not run" nudge). */
|
|
36
|
+
export type ExecuteSkipReason = "nothing" | "headless" | "remembered-no";
|
|
37
|
+
/**
|
|
38
|
+
* What `audit` should do about the EXECUTING checks (battery + live MCP +
|
|
39
|
+
* trigger-rate), as ONE bundle:
|
|
40
|
+
* - `run` — run them now (a remembered yes).
|
|
41
|
+
* - `ask` — interactive human + something to run + no sticky choice: ask once,
|
|
42
|
+
* then remember.
|
|
43
|
+
* - `skip` — stay a deterministic read; the `reason` drives a one-line nudge.
|
|
44
|
+
*/
|
|
45
|
+
export type ExecuteDecision = {
|
|
46
|
+
readonly kind: "run";
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "ask";
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "skip";
|
|
51
|
+
readonly reason: ExecuteSkipReason;
|
|
52
|
+
};
|
|
53
|
+
export interface ExecuteEnv {
|
|
54
|
+
/** Is there ANY executable surface — runnable hooks, an own-repo MCP server, or
|
|
55
|
+
* a model-invocable skill? Nothing to run → never ask, never nudge. */
|
|
56
|
+
readonly hasExecutable: boolean;
|
|
57
|
+
/** Both stdin AND stdout are a terminal (a human who can answer + wait). */
|
|
58
|
+
readonly isTTY: boolean;
|
|
59
|
+
/** `--json` — machine output; stays a read even at a TTY (never prompt). */
|
|
60
|
+
readonly json: boolean;
|
|
61
|
+
/** `--no-interactive` / `--yes` — explicit agent/CI mode (never prompt). */
|
|
62
|
+
readonly noInteractive: boolean;
|
|
63
|
+
/** Sticky remembered choice from `.vigilesrc.json` (`audit.measure`), or undefined. */
|
|
64
|
+
readonly remembered?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Decide what `audit` does with the executing checks. Total + pure; the first
|
|
68
|
+
* matching rule wins. There is NO execution flag — `audit` is a local report, so
|
|
69
|
+
* the executing checks need a human to consent:
|
|
70
|
+
* 1. nothing executable → skip "nothing" (a clean read; no nudge)
|
|
71
|
+
* 2. headless (`--json` / `--no-interactive` / non-TTY — an agent, a pipe, CI) →
|
|
72
|
+
* skip "headless" (no one to ask; automation uses the `vigiles/testing` API)
|
|
73
|
+
* 3. sticky no → skip "remembered-no"
|
|
74
|
+
* 4. sticky yes → run
|
|
75
|
+
* 5. interactive human, no sticky choice → ask (then remember)
|
|
76
|
+
*/
|
|
77
|
+
export declare function decideExecute(o: ExecuteEnv): ExecuteDecision;
|
|
78
|
+
/**
|
|
79
|
+
* The one-line "executing checks not run" nudge for a skipped read (the
|
|
80
|
+
* no-silent-skips corollary). Returns null for `nothing` (nothing to run — not a
|
|
81
|
+
* gap). There is no flag to point at — `audit` runs them only interactively, and
|
|
82
|
+
* automation uses the `vigiles/testing` API.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatExecuteSkip(reason: ExecuteSkipReason): string | null;
|
|
85
|
+
/**
|
|
86
|
+
* A starter `--prompts` file (the real `TriggerPromptSet` shape: bare skill name
|
|
87
|
+
* → `{ prompts, irrelevant }`). One entry per triggerable skill, with TODO
|
|
88
|
+
* placeholders the user replaces with real requests.
|
|
89
|
+
*/
|
|
90
|
+
export declare function scaffoldTriggerPrompts(skillNames: readonly string[]): string;
|
|
91
|
+
//# sourceMappingURL=scan-trigger-suggest.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* audit → the ONE read-vs-run decision. `audit` is a Lighthouse-style LOCAL
|
|
4
|
+
* report: a deterministic READ by default — safe + identical on every OS, nothing
|
|
5
|
+
* executes — NOT a CI step (CI uses `vigiles lint`, the deterministic gate). The
|
|
6
|
+
* executing checks (safety battery, live MCP resolution, skill-firing trigger-rate)
|
|
7
|
+
* run ONLY when there's a human to consent: at a TTY `audit` ASKS once (and
|
|
8
|
+
* remembers in `.vigilesrc.json` `audit.measure`); headless (an agent / `--json` /
|
|
9
|
+
* `--no-interactive` / a pipe) it stays a read + a one-line nudge — never hangs,
|
|
10
|
+
* never silently executes. There is deliberately NO execution flag: automation
|
|
11
|
+
* tests the harness through the `vigiles/testing` API + skills (the layered tiers),
|
|
12
|
+
* not through the report verb. The IO (prompt / run / remember) lives in the CLI;
|
|
13
|
+
* this is the pure decision + helpers.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.hasModelAccess = hasModelAccess;
|
|
17
|
+
exports.isMeteredAccess = isMeteredAccess;
|
|
18
|
+
exports.decideExecute = decideExecute;
|
|
19
|
+
exports.formatExecuteSkip = formatExecuteSkip;
|
|
20
|
+
exports.scaffoldTriggerPrompts = scaffoldTriggerPrompts;
|
|
21
|
+
/**
|
|
22
|
+
* Is a real model reachable for the trigger tier? Either a metered API key
|
|
23
|
+
* (`ANTHROPIC_API_KEY`), OR an authenticated Claude Code session (`CLAUDECODE=1`
|
|
24
|
+
* / `CLAUDE_CODE_ENTRYPOINT`, web/desktop/CLI) — the latter drives the `claude`
|
|
25
|
+
* CLI on the user's subscription, no key needed and $0 metered. A tiny env-only
|
|
26
|
+
* predicate (not a live probe), so it never spends a token just to decide.
|
|
27
|
+
*/
|
|
28
|
+
function hasModelAccess(env) {
|
|
29
|
+
return Boolean(env.ANTHROPIC_API_KEY ||
|
|
30
|
+
env.CLAUDECODE === "1" ||
|
|
31
|
+
env.CLAUDE_CODE_ENTRYPOINT);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Is the reachable model METERED (a paid API key) rather than a subscription?
|
|
35
|
+
* Only affects the consent DISCLOSURE wording (a metered key bills per token; a
|
|
36
|
+
* subscription is $0 metered) — the run/skip decision itself is consent-driven,
|
|
37
|
+
* not metered-driven.
|
|
38
|
+
*/
|
|
39
|
+
function isMeteredAccess(env) {
|
|
40
|
+
return Boolean(env.ANTHROPIC_API_KEY);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide what `audit` does with the executing checks. Total + pure; the first
|
|
44
|
+
* matching rule wins. There is NO execution flag — `audit` is a local report, so
|
|
45
|
+
* the executing checks need a human to consent:
|
|
46
|
+
* 1. nothing executable → skip "nothing" (a clean read; no nudge)
|
|
47
|
+
* 2. headless (`--json` / `--no-interactive` / non-TTY — an agent, a pipe, CI) →
|
|
48
|
+
* skip "headless" (no one to ask; automation uses the `vigiles/testing` API)
|
|
49
|
+
* 3. sticky no → skip "remembered-no"
|
|
50
|
+
* 4. sticky yes → run
|
|
51
|
+
* 5. interactive human, no sticky choice → ask (then remember)
|
|
52
|
+
*/
|
|
53
|
+
function decideExecute(o) {
|
|
54
|
+
if (!o.hasExecutable)
|
|
55
|
+
return { kind: "skip", reason: "nothing" };
|
|
56
|
+
if (o.json || o.noInteractive || !o.isTTY)
|
|
57
|
+
return { kind: "skip", reason: "headless" };
|
|
58
|
+
if (o.remembered === false)
|
|
59
|
+
return { kind: "skip", reason: "remembered-no" };
|
|
60
|
+
if (o.remembered === true)
|
|
61
|
+
return { kind: "run" };
|
|
62
|
+
return { kind: "ask" };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The one-line "executing checks not run" nudge for a skipped read (the
|
|
66
|
+
* no-silent-skips corollary). Returns null for `nothing` (nothing to run — not a
|
|
67
|
+
* gap). There is no flag to point at — `audit` runs them only interactively, and
|
|
68
|
+
* automation uses the `vigiles/testing` API.
|
|
69
|
+
*/
|
|
70
|
+
function formatExecuteSkip(reason) {
|
|
71
|
+
switch (reason) {
|
|
72
|
+
case "nothing":
|
|
73
|
+
return null;
|
|
74
|
+
case "headless":
|
|
75
|
+
return ("\nℹ Executing checks (safety battery · live MCP · skill firing) skipped — " +
|
|
76
|
+
"`audit` runs them only interactively (a terminal). For automation, test the " +
|
|
77
|
+
"harness with the `vigiles/testing` API.");
|
|
78
|
+
case "remembered-no":
|
|
79
|
+
return ("\nℹ Executing checks not run (you disabled them — edit .vigilesrc.json " +
|
|
80
|
+
"`audit.measure` to re-enable).");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A starter `--prompts` file (the real `TriggerPromptSet` shape: bare skill name
|
|
85
|
+
* → `{ prompts, irrelevant }`). One entry per triggerable skill, with TODO
|
|
86
|
+
* placeholders the user replaces with real requests.
|
|
87
|
+
*/
|
|
88
|
+
function scaffoldTriggerPrompts(skillNames) {
|
|
89
|
+
const obj = {};
|
|
90
|
+
for (const name of skillNames) {
|
|
91
|
+
obj[name] = {
|
|
92
|
+
prompts: [
|
|
93
|
+
`TODO: a request that SHOULD trigger "${name}"`,
|
|
94
|
+
`TODO: a differently-phrased request that should also trigger it`,
|
|
95
|
+
],
|
|
96
|
+
irrelevant: [
|
|
97
|
+
`TODO: an unrelated request that should NOT trigger "${name}"`,
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify(obj, null, 2) + "\n";
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=scan-trigger-suggest.js.map
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `vigiles
|
|
2
|
+
* `vigiles audit <dir>` — point vigiles at any plugin/repo and see what it ships
|
|
3
3
|
* and what's broken, with **no model and no API key**.
|
|
4
4
|
*
|
|
5
5
|
* This is the deterministic substrate under the plugin/skill leaderboard
|
|
@@ -27,14 +27,20 @@ export interface ScanSkill {
|
|
|
27
27
|
readonly name: string;
|
|
28
28
|
readonly path: string;
|
|
29
29
|
readonly hasDescription: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The skill's effective description (frontmatter `description`, else the first
|
|
32
|
+
* body paragraph — the same text the selector keys on), trimmed; `undefined`
|
|
33
|
+
* when neither exists. Feeds the model trigger tier's auto-generated probes.
|
|
34
|
+
*/
|
|
35
|
+
readonly description?: string;
|
|
30
36
|
readonly userInvoked: boolean;
|
|
31
37
|
/**
|
|
32
38
|
* The description's dominant script when it DIFFERS from the expected one
|
|
33
39
|
* (default `"Latin"`), else null. The model's skill-selection context is
|
|
34
40
|
* English-centric, so a description in another script carries a cross-language
|
|
35
41
|
* trigger risk — it may under-fire on English prompts. A RISK flag, not a
|
|
36
|
-
* defect (a language-matched audience is fine); measure the real gap with
|
|
37
|
-
* `
|
|
42
|
+
* defect (a language-matched audience is fine); measure the real gap with the
|
|
43
|
+
* `audit` trigger tier / `measureTriggerRate`.
|
|
38
44
|
*/
|
|
39
45
|
readonly descriptionScript: Script | null;
|
|
40
46
|
}
|
|
@@ -86,15 +92,32 @@ export interface FrontmatterValueIssue {
|
|
|
86
92
|
/** ok = file present; missing = referenced but absent; unresolved = path still has an unexpanded var, can't check. */
|
|
87
93
|
export type HookStatus = "ok" | "missing" | "unresolved";
|
|
88
94
|
export interface ScanHook {
|
|
95
|
+
/**
|
|
96
|
+
* The full hook command as it would be run (plugin-root token expanded, shell
|
|
97
|
+
* quotes stripped). Present on script-based hooks; empty string on hooks whose
|
|
98
|
+
* command is entirely inline (no script file) — but inline hooks never appear
|
|
99
|
+
* in `hooks[]`, they are counted by `inlineHooks`, so in practice `command` is
|
|
100
|
+
* always non-empty when a `ScanHook` is in the list.
|
|
101
|
+
*/
|
|
102
|
+
readonly command: string;
|
|
89
103
|
readonly script: string;
|
|
90
104
|
readonly status: HookStatus;
|
|
105
|
+
/**
|
|
106
|
+
* The hook EVENT this script is registered under (`PreToolUse`, `PostToolUse`,
|
|
107
|
+
* `SessionStart`, …), when it can be determined from the canonical
|
|
108
|
+
* object-keyed-by-event settings shape; `undefined` for a non-object/array
|
|
109
|
+
* config. The safety battery uses it to test only the blocking-capable
|
|
110
|
+
* `PreToolUse` guards — so a `SessionStart`/`PostToolUse` hook isn't unfairly
|
|
111
|
+
* scored against "does it block rm -rf".
|
|
112
|
+
*/
|
|
113
|
+
readonly event?: string;
|
|
91
114
|
}
|
|
92
115
|
/**
|
|
93
116
|
* The repo's top-level instruction file (`CLAUDE.md` / `AGENTS.md`), if present.
|
|
94
117
|
* Every cc/codex repo has one even when it ships no plugin surface, so `scan`
|
|
95
118
|
* reports it — otherwise a plain instruction-only repo looks empty. `hasSpec` is
|
|
96
119
|
* the deterministic fact that a `<file>.spec.ts` sits beside it (spec-managed vs
|
|
97
|
-
* hand-written); it is informational, NOT the `require-spec` gate (that's lint).
|
|
120
|
+
* hand-written); it is informational, NOT the `require-instructions-spec` gate (that's lint).
|
|
98
121
|
*/
|
|
99
122
|
export interface ScanInstructions {
|
|
100
123
|
readonly file: string;
|
|
@@ -109,6 +132,13 @@ export interface ScanReport {
|
|
|
109
132
|
readonly hooks: readonly ScanHook[];
|
|
110
133
|
/** Hook entries with no script file (inline shell one-liners) — can't be path-checked. */
|
|
111
134
|
readonly inlineHooks: number;
|
|
135
|
+
/**
|
|
136
|
+
* Hand-written hook commands that are NOT compiled `vigiles/hook` artifacts (a
|
|
137
|
+
* compiled hook's command invokes `vigiles hook-runtime run-program`). The basis
|
|
138
|
+
* for the `prefer-compiled-hooks` recommendation — a single nudge regardless of
|
|
139
|
+
* count. Zero when there are no hooks or every hook is vigiles-managed.
|
|
140
|
+
*/
|
|
141
|
+
readonly manualHookCount: number;
|
|
112
142
|
readonly commands: number;
|
|
113
143
|
readonly mcp: boolean;
|
|
114
144
|
/**
|
|
@@ -175,17 +205,28 @@ export interface SurfaceClassifier {
|
|
|
175
205
|
* no drift). The ≥20% guard avoids a near-empty string tripping on one letter.
|
|
176
206
|
*/
|
|
177
207
|
export declare function unexpectedScript(text: string, expected?: Script): Script | null;
|
|
208
|
+
/**
|
|
209
|
+
* A compiled `vigiles/hook` artifact runs through the `hook-runtime run-program`
|
|
210
|
+
* runtime entrypoint; any other hook command is hand-written (a shell script or
|
|
211
|
+
* an inline one-liner) the author maintains directly. The basis for the
|
|
212
|
+
* `prefer-compiled-hooks` nudge.
|
|
213
|
+
*/
|
|
214
|
+
export declare function isManagedHookCommand(command: string): boolean;
|
|
215
|
+
/** The `prefer-compiled-hooks` recommendation message (shared by `lint` + `scan`). */
|
|
216
|
+
export declare function preferCompiledHooksMessage(count: number): string;
|
|
178
217
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
179
218
|
export declare function scanPlugin(dir: string, layout?: PluginLayout, dialect?: HarnessDialect): ScanReport;
|
|
180
219
|
/**
|
|
181
|
-
* LIVE MCP tool resolution for a scanned plugin — the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
220
|
+
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
|
|
221
|
+
* linter can do: it STARTS each declared MCP server and checks every
|
|
222
|
+
* `mcp__server__tool` the plugin's agents reference actually exists on it
|
|
223
|
+
* (catching rename/removal rot, e.g. `create_issue`→`issue_write`). Reuses the
|
|
185
224
|
* already-computed `report` (its agents' tool lists) + the declared server configs;
|
|
186
225
|
* returns `[]` when the plugin declares no MCP servers (nothing to start). Async +
|
|
187
|
-
* side-effecting (spawns servers) —
|
|
188
|
-
*
|
|
226
|
+
* side-effecting (spawns servers) — so `audit` runs it by default only for the
|
|
227
|
+
* user's OWN repo (own-repo, like running your own tools); a FOREIGN plugin's
|
|
228
|
+
* servers are never spawned, and `--fast` opts out. See `verifyMcpContractTools`
|
|
229
|
+
* (core/mcp.ts).
|
|
189
230
|
*/
|
|
190
231
|
export declare function verifyLiveMcpTools(report: ScanReport, layout: PluginLayout, dialect: HarnessDialect, timeoutMs?: number): Promise<McpContractToolError[]>;
|
|
191
232
|
/** Render the live MCP tool-check result (human-readable). */
|
|
@@ -211,7 +252,7 @@ export interface MarketplaceInfo {
|
|
|
211
252
|
* Read a `marketplace.json` beside the layout's plugin manifest and classify its
|
|
212
253
|
* members into on-disk vs external. Returns `null` when `dir` is not a
|
|
213
254
|
* marketplace. The source of truth behind {@link expandMarketplace} and the
|
|
214
|
-
* curated-marketplace report in `vigiles
|
|
255
|
+
* curated-marketplace report in `vigiles audit`.
|
|
215
256
|
*/
|
|
216
257
|
export declare function inspectMarketplace(dir: string, layout?: PluginLayout): MarketplaceInfo | null;
|
|
217
258
|
/**
|
|
@@ -219,7 +260,7 @@ export declare function inspectMarketplace(dir: string, layout?: PluginLayout):
|
|
|
219
260
|
* plugin manifest, e.g. `.claude-plugin/marketplace.json`), expand it into the
|
|
220
261
|
* absolute dirs of its member plugins. Returns `null` when there's no
|
|
221
262
|
* marketplace, `[]` when it's a marketplace whose members are all external (not
|
|
222
|
-
* on disk). Used by `vigiles
|
|
263
|
+
* on disk). Used by `vigiles audit` to rank a whole marketplace — wshobson/agents
|
|
223
264
|
* alone ships 80+ plugins under one `marketplace.json`. See {@link inspectMarketplace}.
|
|
224
265
|
*/
|
|
225
266
|
export declare function expandMarketplace(dir: string, layout?: PluginLayout): string[] | null;
|
package/dist/scan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `vigiles
|
|
3
|
+
* `vigiles audit <dir>` — point vigiles at any plugin/repo and see what it ships
|
|
4
4
|
* and what's broken, with **no model and no API key**.
|
|
5
5
|
*
|
|
6
6
|
* This is the deterministic substrate under the plugin/skill leaderboard
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.unexpectedScript = unexpectedScript;
|
|
17
|
+
exports.isManagedHookCommand = isManagedHookCommand;
|
|
18
|
+
exports.preferCompiledHooksMessage = preferCompiledHooksMessage;
|
|
17
19
|
exports.scanPlugin = scanPlugin;
|
|
18
20
|
exports.verifyLiveMcpTools = verifyLiveMcpTools;
|
|
19
21
|
exports.formatMcpContractReport = formatMcpContractReport;
|
|
@@ -166,6 +168,7 @@ function scanSkills(files, cls) {
|
|
|
166
168
|
name: fm.name ?? skillName(path),
|
|
167
169
|
path,
|
|
168
170
|
hasDescription: Boolean(effectiveDesc && effectiveDesc.length >= 20),
|
|
171
|
+
description: effectiveDesc?.trim(),
|
|
169
172
|
userInvoked: /^\s*disable-model-invocation:\s*true\s*$/m.test(md),
|
|
170
173
|
descriptionScript: effectiveDesc ? unexpectedScript(effectiveDesc) : null,
|
|
171
174
|
});
|
|
@@ -242,7 +245,7 @@ function scanAgents(files, dialect, declaredServers, cls) {
|
|
|
242
245
|
* quotes. A token that still carries any `$VAR` after that is genuinely
|
|
243
246
|
* uncheckable.
|
|
244
247
|
*/
|
|
245
|
-
function resolveScript(token, root, pluginRootToken) {
|
|
248
|
+
function resolveScript(token, root, pluginRootToken, fullCommand) {
|
|
246
249
|
// "${CLAUDE_PLUGIN_ROOT}" → unbraced "$CLAUDE_PLUGIN_ROOT".
|
|
247
250
|
const unbraced = pluginRootToken.replace(/^\$\{(.+)\}$/, "$$$1");
|
|
248
251
|
const cleaned = token
|
|
@@ -250,14 +253,23 @@ function resolveScript(token, root, pluginRootToken) {
|
|
|
250
253
|
.replaceAll(pluginRootToken, root)
|
|
251
254
|
.replaceAll(unbraced, root);
|
|
252
255
|
if (cleaned.includes("$"))
|
|
253
|
-
return { script: token, status: "unresolved" };
|
|
256
|
+
return { command: fullCommand, script: token, status: "unresolved" };
|
|
254
257
|
// A relative hook path (`./hooks/x.sh`, `scripts/x.py`) is the plugin's own —
|
|
255
258
|
// resolve it against the PLUGIN ROOT, not the scanner's cwd. Without this, a
|
|
256
259
|
// plugin that references `./hooks/x.sh` (the file IS present) was reported
|
|
257
260
|
// MISSING because existsSync() checked cwd-relative (a false positive caught on
|
|
258
261
|
// ananddtyagi/cc-marketplace). The displayed `script` stays as the author wrote it.
|
|
259
262
|
const abs = (0, node_path_1.isAbsolute)(cleaned) ? cleaned : (0, node_path_1.resolve)(root, cleaned);
|
|
260
|
-
|
|
263
|
+
// Resolve the full command the same way we resolve the script token (expand
|
|
264
|
+
// plugin-root, strip outer quotes) so the CLI can pass it to verifyGuardrail.
|
|
265
|
+
const resolvedCommand = fullCommand
|
|
266
|
+
.replaceAll(pluginRootToken, root)
|
|
267
|
+
.replaceAll(unbraced, root);
|
|
268
|
+
return {
|
|
269
|
+
command: resolvedCommand,
|
|
270
|
+
script: cleaned,
|
|
271
|
+
status: (0, node_fs_1.existsSync)(abs) ? "ok" : "missing",
|
|
272
|
+
};
|
|
261
273
|
}
|
|
262
274
|
// A shell existence guard around a command — `[ ! -f x ] || x`, `[ -f x ] && x`,
|
|
263
275
|
// `test -f x && …`. Authors use it to make a hook OPTIONAL (run the script only
|
|
@@ -265,10 +277,65 @@ function resolveScript(token, root, pluginRootToken) {
|
|
|
265
277
|
// target is INTENTIONAL, not a broken reference. Don't flag scripts in such a
|
|
266
278
|
// command as MISSING (a false positive caught on gmickel/flow-next's ralph-guard).
|
|
267
279
|
const EXISTENCE_GUARD = /(?:\[\[?\s*!?\s*-[efsx]\s)|(?:\btest\s+!?\s*-[efsx]\s)/;
|
|
280
|
+
/**
|
|
281
|
+
* A compiled `vigiles/hook` artifact runs through the `hook-runtime run-program`
|
|
282
|
+
* runtime entrypoint; any other hook command is hand-written (a shell script or
|
|
283
|
+
* an inline one-liner) the author maintains directly. The basis for the
|
|
284
|
+
* `prefer-compiled-hooks` nudge.
|
|
285
|
+
*/
|
|
286
|
+
function isManagedHookCommand(command) {
|
|
287
|
+
return /\bhook-runtime\b/.test(command);
|
|
288
|
+
}
|
|
289
|
+
/** The `prefer-compiled-hooks` recommendation message (shared by `lint` + `scan`). */
|
|
290
|
+
function preferCompiledHooksMessage(count) {
|
|
291
|
+
return (`${String(count)} hand-written hook command(s) — if any gate the agent ` +
|
|
292
|
+
`(a block/deny decision), compiled hooks (\`vigiles/hook\`) make whole hook ` +
|
|
293
|
+
`bug classes unrepresentable at authoring time, and \`guardrail-check\` proves ` +
|
|
294
|
+
`an existing one blocks. See docs/compiled-hooks.md.`);
|
|
295
|
+
}
|
|
268
296
|
/** Pull script-file hook commands out of the resolved settings; count inline ones. */
|
|
297
|
+
/**
|
|
298
|
+
* Best-effort map of each script token → the hook EVENT it's registered under,
|
|
299
|
+
* by walking the canonical object-keyed-by-event settings shape
|
|
300
|
+
* (`{ PreToolUse: [{ hooks: [{ command }] }], … }`). Lets the safety battery
|
|
301
|
+
* scope itself to `PreToolUse` (the only event that can block a tool call), so a
|
|
302
|
+
* `SessionStart`/`PostToolUse`/`Stop` hook isn't tested against the disaster
|
|
303
|
+
* catalog. Returns an empty map for a non-object/array config (event → unknown).
|
|
304
|
+
*/
|
|
305
|
+
function eventsByScript(hooks) {
|
|
306
|
+
const map = new Map();
|
|
307
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
|
|
308
|
+
return map;
|
|
309
|
+
for (const [event, arr] of Object.entries(hooks)) {
|
|
310
|
+
if (!Array.isArray(arr))
|
|
311
|
+
continue;
|
|
312
|
+
for (const entry of arr) {
|
|
313
|
+
const hookList = entry.hooks;
|
|
314
|
+
if (!Array.isArray(hookList))
|
|
315
|
+
continue;
|
|
316
|
+
for (const h of hookList) {
|
|
317
|
+
const cmd = h.command;
|
|
318
|
+
if (typeof cmd !== "string")
|
|
319
|
+
continue;
|
|
320
|
+
for (const tok of cmd.match(SCRIPT_RE) ?? []) {
|
|
321
|
+
if (!map.has(tok))
|
|
322
|
+
map.set(tok, event);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return map;
|
|
328
|
+
}
|
|
269
329
|
function scanHooks(settings, root, pluginRootToken) {
|
|
270
330
|
const text = JSON.stringify(settings.hooks ?? {});
|
|
271
331
|
const commands = [...text.matchAll(/"command":\s*"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
332
|
+
const evMap = eventsByScript(settings.hooks);
|
|
333
|
+
// A hand-written hook is any non-empty command that isn't a vigiles-managed
|
|
334
|
+
// (compiled) hook-runtime invocation — the basis for the prefer-compiled-hooks nudge.
|
|
335
|
+
const manual = commands.filter((c) => {
|
|
336
|
+
const u = c.replace(/\\(.)/g, "$1").trim();
|
|
337
|
+
return u !== "" && !isManagedHookCommand(u);
|
|
338
|
+
}).length;
|
|
272
339
|
const byScript = new Map();
|
|
273
340
|
let inline = 0;
|
|
274
341
|
for (const cmd of commands) {
|
|
@@ -285,12 +352,13 @@ function scanHooks(settings, root, pluginRootToken) {
|
|
|
285
352
|
continue;
|
|
286
353
|
}
|
|
287
354
|
for (const tok of found) {
|
|
288
|
-
const hook = resolveScript(tok, root, pluginRootToken);
|
|
289
|
-
|
|
355
|
+
const hook = resolveScript(tok, root, pluginRootToken, unescaped);
|
|
356
|
+
const event = evMap.get(tok);
|
|
357
|
+
byScript.set(hook.script, event ? { ...hook, event } : hook);
|
|
290
358
|
}
|
|
291
359
|
}
|
|
292
360
|
const hooks = [...byScript.values()].sort((a, b) => a.script.localeCompare(b.script));
|
|
293
|
-
return { hooks, inline };
|
|
361
|
+
return { hooks, inline, manual };
|
|
294
362
|
}
|
|
295
363
|
// ---------------------------------------------------------------------------
|
|
296
364
|
// Public API
|
|
@@ -487,7 +555,7 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
|
487
555
|
const lay = layout ?? layout_js_1.claudeCodeLayout;
|
|
488
556
|
const cls = makeClassifier(lay);
|
|
489
557
|
const loaded = (0, plugin_loader_js_1.loadPlugin)(dir, lay);
|
|
490
|
-
const { hooks, inline } = scanHooks(loaded.settings, (0, node_path_1.resolve)(dir), lay.pluginRootToken);
|
|
558
|
+
const { hooks, inline, manual } = scanHooks(loaded.settings, (0, node_path_1.resolve)(dir), lay.pluginRootToken);
|
|
491
559
|
// Hook-event keys are a CLOSED platform set — an unrecognized one is a dead
|
|
492
560
|
// registration (the hook never fires), so flag every unknown (not just typos).
|
|
493
561
|
// ONLY for the canonical object-keyed-by-event shape: a plugin shipping a
|
|
@@ -521,6 +589,7 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
|
521
589
|
agents,
|
|
522
590
|
hooks,
|
|
523
591
|
inlineHooks: inline,
|
|
592
|
+
manualHookCount: manual,
|
|
524
593
|
commands: Object.keys(loaded.files).filter(cls.isCommand).length,
|
|
525
594
|
mcp: loaded.warnings.some((w) => w.includes("MCP server")),
|
|
526
595
|
danglingRefs: (0, plugin_loader_js_2.danglingRefs)((0, node_path_1.resolve)(dir), lay),
|
|
@@ -539,14 +608,16 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
|
539
608
|
};
|
|
540
609
|
}
|
|
541
610
|
/**
|
|
542
|
-
* LIVE MCP tool resolution for a scanned plugin — the
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
611
|
+
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
|
|
612
|
+
* linter can do: it STARTS each declared MCP server and checks every
|
|
613
|
+
* `mcp__server__tool` the plugin's agents reference actually exists on it
|
|
614
|
+
* (catching rename/removal rot, e.g. `create_issue`→`issue_write`). Reuses the
|
|
546
615
|
* already-computed `report` (its agents' tool lists) + the declared server configs;
|
|
547
616
|
* returns `[]` when the plugin declares no MCP servers (nothing to start). Async +
|
|
548
|
-
* side-effecting (spawns servers) —
|
|
549
|
-
*
|
|
617
|
+
* side-effecting (spawns servers) — so `audit` runs it by default only for the
|
|
618
|
+
* user's OWN repo (own-repo, like running your own tools); a FOREIGN plugin's
|
|
619
|
+
* servers are never spawned, and `--fast` opts out. See `verifyMcpContractTools`
|
|
620
|
+
* (core/mcp.ts).
|
|
550
621
|
*/
|
|
551
622
|
async function verifyLiveMcpTools(report, layout, dialect, timeoutMs = 10000) {
|
|
552
623
|
// collectMcpServers yields the raw JSON server entries; a malformed one (no
|
|
@@ -571,7 +642,7 @@ function formatMcpContractReport(errors) {
|
|
|
571
642
|
* Read a `marketplace.json` beside the layout's plugin manifest and classify its
|
|
572
643
|
* members into on-disk vs external. Returns `null` when `dir` is not a
|
|
573
644
|
* marketplace. The source of truth behind {@link expandMarketplace} and the
|
|
574
|
-
* curated-marketplace report in `vigiles
|
|
645
|
+
* curated-marketplace report in `vigiles audit`.
|
|
575
646
|
*/
|
|
576
647
|
function inspectMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
577
648
|
const mpPath = (0, node_path_1.join)(dir, (0, node_path_1.dirname)(layout.manifestPath), "marketplace.json");
|
|
@@ -623,7 +694,7 @@ function inspectMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
|
623
694
|
* plugin manifest, e.g. `.claude-plugin/marketplace.json`), expand it into the
|
|
624
695
|
* absolute dirs of its member plugins. Returns `null` when there's no
|
|
625
696
|
* marketplace, `[]` when it's a marketplace whose members are all external (not
|
|
626
|
-
* on disk). Used by `vigiles
|
|
697
|
+
* on disk). Used by `vigiles audit` to rank a whole marketplace — wshobson/agents
|
|
627
698
|
* alone ships 80+ plugins under one `marketplace.json`. See {@link inspectMarketplace}.
|
|
628
699
|
*/
|
|
629
700
|
function expandMarketplace(dir, layout = layout_js_1.claudeCodeLayout) {
|
|
@@ -749,6 +820,11 @@ function formatScanReport(r) {
|
|
|
749
820
|
if (r.skillMetaIssues.length > 0) {
|
|
750
821
|
out.push(`ℹ ${String(r.skillMetaIssues.length)} skill(s) lack an explicit frontmatter name/description (recommended for a reliable trigger surface) — they still load via fallback`, "");
|
|
751
822
|
}
|
|
823
|
+
// One discovery nudge toward compiled hooks (never per-hook); the hand-written
|
|
824
|
+
// shell lane stays first-class, so this is a recommendation, not a defect.
|
|
825
|
+
if (r.manualHookCount > 0) {
|
|
826
|
+
out.push(`ℹ ${preferCompiledHooksMessage(r.manualHookCount)}`, "");
|
|
827
|
+
}
|
|
752
828
|
// Malformed-YAML frontmatter is INFORMATIONAL, not a structural defect: js-yaml
|
|
753
829
|
// is stricter than some loaders (a colon/quote/<example> in a one-line
|
|
754
830
|
// description trips it though the file may still load), and the other fields are
|
|
@@ -35,7 +35,7 @@ export type BehavioralSymptom = "wrong-skill-fires" | "skill-never-fires" | "age
|
|
|
35
35
|
* never-available tool can't be called); the cause is near-certain.
|
|
36
36
|
* - `"possible"` — a high-precision PROXY for a behavioral risk (a description
|
|
37
37
|
* overlap / a foreign-script description); deterministic to detect, but whether
|
|
38
|
-
* it actually moved behaviour is confirmed by `
|
|
38
|
+
* it actually moved behaviour is confirmed by the `audit` trigger tier.
|
|
39
39
|
*/
|
|
40
40
|
export type ExplanationConfidence = "likely" | "possible";
|
|
41
41
|
export interface ScoreExplanation {
|
package/dist/setup-plan.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface SetupPlan {
|
|
|
27
27
|
export interface ParsedSetupArgs {
|
|
28
28
|
target?: string;
|
|
29
29
|
strict: boolean;
|
|
30
|
+
/** `--report-only` — write the gating rules at "warn" (nothing fails CI). The
|
|
31
|
+
* orthogonal severity dial; composes with `--strict` (which rules) by setting
|
|
32
|
+
* their severity. */
|
|
33
|
+
reportOnly: boolean;
|
|
30
34
|
yes: boolean;
|
|
31
35
|
/** `--force` — rewrite a stale CI workflow in place. */
|
|
32
36
|
force: boolean;
|
|
@@ -51,9 +55,52 @@ export declare function defaultPlan(strict?: boolean): SetupPlan;
|
|
|
51
55
|
* changed (so the IO layer skips the write). The IO (read/parse/write + the
|
|
52
56
|
* malformed-file guard) stays in cli.ts.
|
|
53
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* The structural rules `init` gates BY DEFAULT (severity `error`, so a broken
|
|
60
|
+
* surface fails `vigiles lint`). Every one is HIGH-PRECISION / FP-safe — it fires
|
|
61
|
+
* only on a genuine defect (a never-available/typo'd tool, a subagent missing
|
|
62
|
+
* `name`/`description`, a typo'd hook event, a dead hook script, a broken MCP
|
|
63
|
+
* ref, two skills that collide in the selector) — so a well-formed plugin stays
|
|
64
|
+
* green and catching real breakage out of the box never cries wolf.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately EXCLUDES `require-instructions-spec` and the workflow-forcing rules:
|
|
67
|
+
* those make a CLEAN repo fail (you simply haven't written the spec/test yet), so
|
|
68
|
+
* they stay opt-in under `--strict` (progressive adoption — see
|
|
69
|
+
* `STRICT_EXTRA_RULES`).
|
|
70
|
+
*
|
|
71
|
+
* This is the **`structural`** rule group (see research/install-enforcement-dx.md).
|
|
72
|
+
*/
|
|
73
|
+
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"];
|
|
74
|
+
/**
|
|
75
|
+
* The **`workflow`** group — the WORKFLOW-FORCING / opinionated tier `--strict`
|
|
76
|
+
* gates, which a clean repo can still fail because you haven't done the work yet:
|
|
77
|
+
* a spec per instruction file (`require-instructions-spec`), a test/eval per
|
|
78
|
+
* surface (`untested-*`). Opt-in by design (the smooth-adoption on-ramp). The
|
|
79
|
+
* Clippy-`pedantic` / TS-`strict` analog — ONE opinionated opt-in.
|
|
80
|
+
*
|
|
81
|
+
* NB `frontmatter-valid` / `skill-frontmatter` live in the `nudge` group, not
|
|
82
|
+
* here: they're acknowledged-noisy recommendations we never gate on (see
|
|
83
|
+
* research/install-enforcement-dx.md).
|
|
84
|
+
*/
|
|
85
|
+
export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "untested-skill", "untested-subagent", "untested-hook"];
|
|
86
|
+
/**
|
|
87
|
+
* The **`nudge`** group — recommendations / acknowledged-noisy checks that NEVER
|
|
88
|
+
* gate (not even under `--strict`): `frontmatter-valid` (js-yaml is stricter than
|
|
89
|
+
* CC's loader), `skill-frontmatter` (skills load without it) and `unmarked-refs`
|
|
90
|
+
* (the undecidable-plaintext nudge) sit at `warn`; `prefer-compiled-hooks` defaults
|
|
91
|
+
* OFF (a recommendation that shouldn't fire unasked — the shell lane stays
|
|
92
|
+
* first-class). `init` does not write these — they keep their own default
|
|
93
|
+
* severities. Named for the group taxonomy (research/install-enforcement-dx.md).
|
|
94
|
+
*/
|
|
95
|
+
export declare const NUDGE_RULES: readonly ["frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs"];
|
|
54
96
|
export declare function mergeProjectConfig(existing: Record<string, unknown>, opts: {
|
|
55
97
|
harness: string | string[];
|
|
56
98
|
strict: boolean;
|
|
99
|
+
reportOnly?: boolean;
|
|
100
|
+
/** Whether the LINT pillar is on (default true). The rule gate is a lint-layer
|
|
101
|
+
* concern, so a test-only setup (`init --test` / `--no-lint`) records the
|
|
102
|
+
* harness but writes NO lint rules. */
|
|
103
|
+
lint?: boolean;
|
|
57
104
|
}): Record<string, unknown> | null;
|
|
58
105
|
/**
|
|
59
106
|
* Whether to drop into interactive prompts: a human at a TTY who passed neither
|
|
@@ -62,7 +109,18 @@ export declare function mergeProjectConfig(existing: Record<string, unknown>, op
|
|
|
62
109
|
*/
|
|
63
110
|
export declare function shouldPrompt(parsed: ParsedSetupArgs, isTTY: boolean): boolean;
|
|
64
111
|
/** Interactive answers (only the fields the prompts cover). */
|
|
65
|
-
export type SetupAnswers = Partial<Pick<SetupPlan, "lint" | "test" | "gha" | "plugin">>;
|
|
112
|
+
export type SetupAnswers = Partial<Pick<SetupPlan, "lint" | "test" | "gha" | "plugin" | "strict">>;
|
|
113
|
+
/** Ask one question with a default — injected so the interactive Q&A is pure +
|
|
114
|
+
* unit-testable (a fake `ask` scripts answers; no TTY, no readline). */
|
|
115
|
+
export type AskFn = (question: string, def: string) => Promise<string>;
|
|
116
|
+
/**
|
|
117
|
+
* The interactive setup Q&A as PURE logic over an injected `ask` — the prompts,
|
|
118
|
+
* their defaults, and the answer→`SetupAnswers` mapping. The IO shell (readline)
|
|
119
|
+
* lives in `cli.ts`'s `promptSetup`, which just supplies a real `ask`. Keeping
|
|
120
|
+
* this here means the fragile interactive path is unit-tested deterministically
|
|
121
|
+
* (the questions can't silently break) without a terminal.
|
|
122
|
+
*/
|
|
123
|
+
export declare function collectSetupAnswers(ask: AskFn): Promise<SetupAnswers>;
|
|
66
124
|
/**
|
|
67
125
|
* How to install vigiles's skills/hooks for ONE harness — the deterministic
|
|
68
126
|
* decision behind the IO in cli.ts, so a CI test asserts WHICH commands an
|