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.
@@ -10,7 +10,7 @@
10
10
  * on a prompt. See docs/agent-setup.md.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.SHIPPED_SKILLS = exports.NUDGE_RULES = exports.WORKFLOW_RULES = exports.STRUCTURAL_RULES = void 0;
13
+ exports.SHIPPED_SKILLS = exports.NUDGE_RULES = exports.WORKFLOW_RULES = void 0;
14
14
  exports.parseSetupArgs = parseSetupArgs;
15
15
  exports.defaultPlan = defaultPlan;
16
16
  exports.mergeProjectConfig = mergeProjectConfig;
@@ -21,7 +21,12 @@ exports.planPluginInstall = planPluginInstall;
21
21
  exports.shouldRunGlobalInstall = shouldRunGlobalInstall;
22
22
  exports.codexPluginHooks = codexPluginHooks;
23
23
  exports.applyCodexPluginHooks = applyCodexPluginHooks;
24
+ exports.adoptionState = adoptionState;
25
+ exports.rerunScope = rerunScope;
26
+ exports.missingVigilesDevDep = missingVigilesDevDep;
27
+ exports.fullSetupFollowUps = fullSetupFollowUps;
24
28
  exports.resolvePlan = resolvePlan;
29
+ const presets_js_1 = require("./core/presets.js");
25
30
  function flagValue(args, prefix) {
26
31
  return args.find((a) => a.startsWith(prefix))?.slice(prefix.length);
27
32
  }
@@ -48,6 +53,7 @@ function parseSetupArgs(args) {
48
53
  gha: args.includes("--no-gha") ? false : undefined,
49
54
  plugin: args.includes("--no-plugin") ? false : undefined,
50
55
  ciOnly: args.includes("--ci-only"),
56
+ full: args.includes("--full"),
51
57
  };
52
58
  }
53
59
  /** The non-interactive defaults: both pillars, CI, and the plugin. `scaffoldSpecs`
@@ -66,39 +72,6 @@ function defaultPlan(strict = false) {
66
72
  force: false,
67
73
  };
68
74
  }
69
- /**
70
- * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
71
- * the `harness` if absent, add strict rule severities if `--strict`, NEVER
72
- * clobber an existing key. Returns the merged config, or `null` when nothing
73
- * changed (so the IO layer skips the write). The IO (read/parse/write + the
74
- * malformed-file guard) stays in cli.ts.
75
- */
76
- /**
77
- * The structural rules `init` gates BY DEFAULT (severity `error`, so a broken
78
- * surface fails `vigiles lint`). Every one is HIGH-PRECISION / FP-safe — it fires
79
- * only on a genuine defect (a never-available/typo'd tool, a subagent missing
80
- * `name`/`description`, a typo'd hook event, a dead hook script, a broken MCP
81
- * ref, two skills that collide in the selector) — so a well-formed plugin stays
82
- * green and catching real breakage out of the box never cries wolf.
83
- *
84
- * Deliberately EXCLUDES `require-instructions-spec` and the workflow-forcing rules:
85
- * those make a CLEAN repo fail (you simply haven't written the spec/test yet), so
86
- * they stay opt-in under `--strict` (progressive adoption — see
87
- * `STRICT_EXTRA_RULES`).
88
- *
89
- * This is the **`structural`** rule group (see research/install-enforcement-dx.md).
90
- */
91
- exports.STRUCTURAL_RULES = [
92
- "subagent-tool-contract",
93
- "subagent-frontmatter",
94
- "hook-events",
95
- "hook-script-exists",
96
- "mcp-config",
97
- "mcp-tool-resolves",
98
- "mcp-hook-target-resolves",
99
- "disallowed-tools-contract",
100
- "description-overlap",
101
- ];
102
75
  /**
103
76
  * The **`workflow`** group — the WORKFLOW-FORCING / opinionated tier `--strict`
104
77
  * gates, which a clean repo can still fail because you haven't done the work yet:
@@ -147,41 +120,53 @@ exports.NUDGE_RULES = [
147
120
  // than config. See docs/rules/doc-refs.md.
148
121
  "doc-refs",
149
122
  ];
123
+ /**
124
+ * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
125
+ * the harnesses if absent, gate the structural rules by EXTENDING the
126
+ * `vigiles:recommended` preset (never by writing each rule), add the workflow
127
+ * rules on `--strict`, and NEVER clobber an existing key or severity. Returns
128
+ * the merged config, or `null` when nothing changed (so the IO layer skips the
129
+ * write). The IO (read/parse/write + the malformed-file guard) stays in cli.ts.
130
+ *
131
+ * 🔴 THE PRESET, NOT NINE LINES (#338). Written lines are a snapshot of the
132
+ * structural group on the day `init` ran, so a repo adopted earlier never got a
133
+ * rule added to the group later; the preset is resolved by the loader on every
134
+ * run (`src/core/presets.ts`).
135
+ *
136
+ * `--report-only` is the exception and stays EXPLICIT: it promises that nothing
137
+ * fails CI, and extending a preset that later gains a rule at `error` would
138
+ * break that promise on an upgrade. So it writes the same rules at `warn`.
139
+ */
150
140
  function mergeProjectConfig(existing, opts) {
151
- const config = { ...existing };
152
- let changed = false;
153
141
  // The NESTED key (#240). `init` writes the declaration with no roots — a repo
154
142
  // whose surfaces sit where its harness reads them needs none, and a root is a
155
- // fact only the owner knows. Writing the flat `harness`/`surfaceRoots` pair
156
- // here would emit a config the loader now REFUSES, which is why this one line
157
- // moved with the shape even though the rest of `init` did not.
158
- if (config.harnesses === undefined) {
159
- config.harnesses = Object.fromEntries(opts.harnesses.map((h) => [h, {}]));
160
- changed = true;
161
- }
162
- // The rule gate belongs to the LINT layer — a test-only setup records the
163
- // harness but writes no rules (honoring the positive-flag contract that
164
- // `--test` selects only the test pillar).
165
- if (opts.lint !== false) {
166
- // Gate the FP-safe `structural` group by default; `--strict` adds the
167
- // `workflow` group on top. `--report-only` is the orthogonal severity dial —
168
- // it writes the SAME rule set at "warn" (nothing fails CI; the
169
- // migration/observe mode). Never clobber a severity the user already set —
170
- // only fill the undefined ones.
171
- const severity = opts.reportOnly ? "warn" : "error";
172
- const gate = opts.strict
173
- ? [...exports.STRUCTURAL_RULES, ...exports.WORKFLOW_RULES]
174
- : [...exports.STRUCTURAL_RULES];
175
- const rules = { ...config.rules };
176
- for (const r of gate) {
177
- if (rules[r] === undefined) {
178
- rules[r] = severity;
179
- changed = true;
180
- }
181
- }
182
- config.rules = rules;
183
- }
184
- return changed ? config : null;
143
+ // fact only the owner knows.
144
+ const harnesses = existing.harnesses === undefined
145
+ ? { harnesses: Object.fromEntries(opts.harnesses.map((h) => [h, {}])) }
146
+ : {};
147
+ const gate = opts.lint === false ? {} : lintGate(existing, opts);
148
+ const added = { ...harnesses, ...gate };
149
+ return Object.keys(added).length === 0 ? null : { ...existing, ...added };
150
+ }
151
+ /**
152
+ * The lint-layer keys `init` adds: `extends` and/or the `rules` it fills.
153
+ * Only the keys that CHANGE are returned, so an already-satisfied config
154
+ * yields `{}` and is not rewritten.
155
+ */
156
+ function lintGate(existing, opts) {
157
+ const workflow = opts.strict ? exports.WORKFLOW_RULES : [];
158
+ const explicit = opts.reportOnly
159
+ ? [...presets_js_1.RECOMMENDED_RULES, ...workflow]
160
+ : workflow;
161
+ const severity = opts.reportOnly ? "warn" : "error";
162
+ const before = (existing.rules ?? {});
163
+ // Never clobber a severity the user already set — only fill the undefined ones.
164
+ const filled = Object.fromEntries(explicit.filter((r) => before[r] === undefined).map((r) => [r, severity]));
165
+ const rules = Object.keys(filled).length === 0 ? {} : { rules: { ...before, ...filled } };
166
+ const preset = opts.reportOnly || existing.extends !== undefined
167
+ ? {}
168
+ : { extends: presets_js_1.RECOMMENDED_PRESET };
169
+ return { ...preset, ...rules };
185
170
  }
186
171
  /**
187
172
  * Whether to drop into interactive prompts: a human at a TTY who passed neither
@@ -288,7 +273,7 @@ function gateOnlyInvitation(plan) {
288
273
  const gateOnly = !plan.plugin && !plan.scaffoldSpecs;
289
274
  if (!gateOnly)
290
275
  return null;
291
- return "→ Want your agent to maintain this + measure whether your skills fire? Run `npx vigiles init` and choose 'full' (installs the skills). Optional — the gate above already works.";
276
+ return "→ Want your agent to maintain this + measure whether your skills fire? Run `npx vigiles init --full` and choose 'full' (installs the skills). Optional — the gate above already works.";
292
277
  }
293
278
  /**
294
279
  * The SHIPPED consumer skills (the `skills/` dir, published via `plugin.json`) —
@@ -470,6 +455,87 @@ function applyCodexPluginHooks(existing) {
470
455
  }
471
456
  return { ...existing, hooks };
472
457
  }
458
+ /** {@link AdoptionState} from the two disk facts that decide it. */
459
+ function adoptionState(facts) {
460
+ if (facts.hasConfig)
461
+ return { kind: "adopted", evidence: ".vigilesrc.json" };
462
+ const [spec] = facts.specs;
463
+ return spec === undefined
464
+ ? { kind: "fresh" }
465
+ : { kind: "adopted", evidence: spec };
466
+ }
467
+ /**
468
+ * Which setup a run performs: the full one, or the minimal re-run (#338).
469
+ *
470
+ * MINIMAL only when the repo is adopted AND the run named nothing that asks for
471
+ * more. A flag that selects or widens the setup (`--full`, `--strict`,
472
+ * `--ci-only`, `--report-only`, `--force`, `--target=`, a positive `--lint` /
473
+ * `--test`) is an explicit request and keeps today's behaviour; the narrowing
474
+ * flags (`--no-*`, `--harness=`, `--yes`) do not, because they can only make a
475
+ * run do less.
476
+ */
477
+ function rerunScope(parsed, state) {
478
+ if (state.kind === "fresh")
479
+ return "full";
480
+ const asksForMore = parsed.full ||
481
+ parsed.strict ||
482
+ parsed.ciOnly ||
483
+ parsed.reportOnly ||
484
+ parsed.force ||
485
+ parsed.target !== undefined ||
486
+ parsed.lint === true ||
487
+ parsed.test === true;
488
+ return asksForMore ? "full" : "minimal";
489
+ }
490
+ /**
491
+ * The `devDependencies` a minimal re-run writes, or null to leave package.json
492
+ * alone. It ADDS vigiles when no dependency field declares it and never edits an
493
+ * existing declaration: rewriting `^33.3.0` to `^33` loosens a pin the owner
494
+ * chose, and moving it between fields is a decision, not a missing piece.
495
+ */
496
+ function missingVigilesDevDep(pkg, spec) {
497
+ const declared = pkg.dependencies?.vigiles !== undefined ||
498
+ pkg.devDependencies?.vigiles !== undefined;
499
+ return declared ? null : { ...pkg.devDependencies, vigiles: spec };
500
+ }
501
+ /**
502
+ * The lines a minimal re-run prints about what it did NOT do: each thing a full
503
+ * setup would add here, then the flag that does it. Empty when nothing is
504
+ * missing, so a fully set-up repo is not told to run anything.
505
+ */
506
+ function fullSetupFollowUps(gap) {
507
+ const configLine = {
508
+ missing: `write .vigilesrc.json with your harnesses and "extends": "${presets_js_1.RECOMMENDED_PRESET}" (structural rules fail CI)`,
509
+ "no-preset": `add "extends": "${presets_js_1.RECOMMENDED_PRESET}" to .vigilesrc.json (structural rules fail CI)`,
510
+ "has-preset": null,
511
+ };
512
+ const items = [
513
+ gap.workflow
514
+ ? "create .github/workflows/vigiles.yml (a CI workflow — new CI minutes)"
515
+ : null,
516
+ configLine[gap.config],
517
+ gap.harnessTest
518
+ ? "scaffold vigiles.harness.mjs (a starter harness test)"
519
+ : null,
520
+ gap.unspecced.length > 0
521
+ ? `write specs for ${String(gap.unspecced.length)} hand-written file(s): ${gap.unspecced.join(", ")}`
522
+ : null,
523
+ gap.generated
524
+ ? "generate .vigiles/generated.d.ts and .vigiles/schema.json"
525
+ : null,
526
+ ...gap.installs,
527
+ gap.devDep === null
528
+ ? null
529
+ : `set the vigiles devDependency to ${gap.devDep.to} (yours: ${gap.devDep.from})`,
530
+ ].filter((l) => l !== null);
531
+ return items.length === 0
532
+ ? []
533
+ : [
534
+ "A full setup would also:",
535
+ ...items.map((l) => ` - ${l}`),
536
+ "Run `npx vigiles init --full` for that (flags such as --no-gha still apply).",
537
+ ];
538
+ }
473
539
  /**
474
540
  * Resolve the final plan: defaults, then flags, then interactive answers (each
475
541
  * layer overrides the previous only where it has an opinion). `--target` pins a
@@ -1851,6 +1851,12 @@
1851
1851
  "type": "string",
1852
1852
  "const": "dismissed"
1853
1853
  },
1854
+ "extends": {
1855
+ "type": "string",
1856
+ "enum": [
1857
+ "vigiles:recommended"
1858
+ ]
1859
+ },
1854
1860
  "$schema": {
1855
1861
  "type": "string"
1856
1862
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "33.3.0",
3
+ "version": "35.0.0",
4
4
  "description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -83,7 +83,6 @@
83
83
  "test:unit": "npm run build && vitest run --project unit",
84
84
  "test:integration": "npm run build && vitest run --project integration",
85
85
  "test:e2e": "npm run build && vitest run --project e2e",
86
- "test:cli-e2e": "bash test/e2e/run.sh",
87
86
  "test:harness": "npm run build && node dist/cli.js test",
88
87
  "test:eval": "npm run build && node dist/cli.js eval --all",
89
88
  "test:vitest": "npm run build && vitest run --project runners",