vigiles 4.0.2 → 4.1.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.
@@ -31,12 +31,56 @@ export interface DetectResult {
31
31
  export declare function detectAdapterResult(root: string): DetectResult;
32
32
  /** The detected adapter (highest specificity), else the default (Claude Code). */
33
33
  export declare function detectAdapter(root: string): HarnessAdapter;
34
- /** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
34
+ /** Lower-case, trim, and map a short alias to its canonical adapter name. */
35
+ export declare function normalizeHarnessName(name: string): string;
36
+ /** Look up a registered adapter by `name` (alias-aware, e.g. `claude`). */
35
37
  export declare function getAdapter(name: string): HarnessAdapter | undefined;
38
+ /**
39
+ * The adapter whose instruction file is `filename` (e.g. `AGENTS.md` → codex,
40
+ * `CLAUDE.md` → claude-code), if any. The per-spec disambiguation signal: a
41
+ * `<file>.spec.ts` compiles a `<file>` instruction file, so the filename names
42
+ * the harness more specifically than config/detect for THAT spec.
43
+ */
44
+ export declare function adapterForInstructionFile(filename: string): HarnessAdapter | undefined;
36
45
  /**
37
46
  * Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
38
47
  * if unknown); otherwise auto-detect from `root`. The single entry point the CLI
39
48
  * uses so detection + override live in one place.
40
49
  */
41
50
  export declare function resolveAdapter(root: string, harness?: string): HarnessAdapter;
51
+ /** Normalize a config `harness` value (string | string[]) to a canonical list. */
52
+ export declare function normalizeHarnessList(harness?: string | readonly string[]): string[];
53
+ /**
54
+ * The adapter chosen for a single-dialect operation. A discriminated union so an
55
+ * invalid state — a "notice" with no message, or a clean pick carrying a stray
56
+ * string — is unrepresentable: `kind: "ok"` has no `notice`, `kind: "notice"`
57
+ * always carries a non-empty one. Both variants carry the `adapter`.
58
+ */
59
+ export type HarnessSelection = {
60
+ readonly kind: "ok";
61
+ readonly adapter: HarnessAdapter;
62
+ } | {
63
+ readonly kind: "notice";
64
+ readonly adapter: HarnessAdapter;
65
+ readonly notice: string;
66
+ };
67
+ /**
68
+ * Resolve the single harness a compile/lint operation should use, with explicit
69
+ * precedence — the deterministic replacement for sniffing the cwd:
70
+ *
71
+ * 1. `--harness=` flag (wins; throws if unknown).
72
+ * 2. config `harness` resolving to a single entry → use it.
73
+ * 3. config `harness` with multiple entries → use the first, with a loud notice.
74
+ * 4. no config → auto-detect, with a loud notice when the repo is ambiguous.
75
+ *
76
+ * `configHarness` is parsed once (alias-normalized) at the call site and passed
77
+ * in; this function re-normalizes idempotently so it's safe either way. Pure
78
+ * (besides reading `root`'s layout for detection) so the precedence is
79
+ * unit-testable without a real compile. See research/multi-harness-compile.md.
80
+ */
81
+ export declare function resolveHarnessSelection(opts: {
82
+ root: string;
83
+ flag?: string;
84
+ configHarness?: string | readonly string[];
85
+ }): HarnessSelection;
42
86
  //# sourceMappingURL=adapter-registry.d.ts.map
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ADAPTERS = exports.defaultAdapter = void 0;
4
4
  exports.detectAdapterResult = detectAdapterResult;
5
5
  exports.detectAdapter = detectAdapter;
6
+ exports.normalizeHarnessName = normalizeHarnessName;
6
7
  exports.getAdapter = getAdapter;
8
+ exports.adapterForInstructionFile = adapterForInstructionFile;
7
9
  exports.resolveAdapter = resolveAdapter;
10
+ exports.normalizeHarnessList = normalizeHarnessList;
11
+ exports.resolveHarnessSelection = resolveHarnessSelection;
8
12
  const adapter_js_1 = require("./adapters/claude-code/adapter.js");
9
13
  const adapter_js_2 = require("./adapters/codex/adapter.js");
10
14
  /** The default adapter when detection finds no harness markers. */
@@ -32,9 +36,32 @@ function detectAdapterResult(root) {
32
36
  function detectAdapter(root) {
33
37
  return detectAdapterResult(root).adapter;
34
38
  }
35
- /** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
39
+ /**
40
+ * Short-name aliases accepted anywhere a harness name is supplied (config,
41
+ * `--harness=`). `init` historically uses `"claude"`; the canonical adapter name
42
+ * is `"claude-code"`. Normalizing here keeps selection and the registry in sync.
43
+ */
44
+ const HARNESS_ALIASES = {
45
+ claude: "claude-code",
46
+ };
47
+ /** Lower-case, trim, and map a short alias to its canonical adapter name. */
48
+ function normalizeHarnessName(name) {
49
+ const n = name.trim().toLowerCase();
50
+ return HARNESS_ALIASES[n] ?? n;
51
+ }
52
+ /** Look up a registered adapter by `name` (alias-aware, e.g. `claude`). */
36
53
  function getAdapter(name) {
37
- return exports.ADAPTERS.find((a) => a.name === name);
54
+ const canonical = normalizeHarnessName(name);
55
+ return exports.ADAPTERS.find((a) => a.name === canonical);
56
+ }
57
+ /**
58
+ * The adapter whose instruction file is `filename` (e.g. `AGENTS.md` → codex,
59
+ * `CLAUDE.md` → claude-code), if any. The per-spec disambiguation signal: a
60
+ * `<file>.spec.ts` compiles a `<file>` instruction file, so the filename names
61
+ * the harness more specifically than config/detect for THAT spec.
62
+ */
63
+ function adapterForInstructionFile(filename) {
64
+ return exports.ADAPTERS.find((a) => a.layout.instructionFile === filename);
38
65
  }
39
66
  /**
40
67
  * Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
@@ -42,7 +69,7 @@ function getAdapter(name) {
42
69
  * uses so detection + override live in one place.
43
70
  */
44
71
  function resolveAdapter(root, harness) {
45
- if (harness !== undefined) {
72
+ if (harness !== undefined && harness !== "") {
46
73
  const a = getAdapter(harness);
47
74
  if (!a) {
48
75
  const known = exports.ADAPTERS.map((x) => x.name).join(", ");
@@ -52,4 +79,52 @@ function resolveAdapter(root, harness) {
52
79
  }
53
80
  return detectAdapter(root);
54
81
  }
82
+ /** Normalize a config `harness` value (string | string[]) to a canonical list. */
83
+ function normalizeHarnessList(harness) {
84
+ if (harness === undefined)
85
+ return [];
86
+ const arr = Array.isArray(harness) ? harness : [harness];
87
+ return arr.map(normalizeHarnessName).filter(Boolean);
88
+ }
89
+ /**
90
+ * Resolve the single harness a compile/lint operation should use, with explicit
91
+ * precedence — the deterministic replacement for sniffing the cwd:
92
+ *
93
+ * 1. `--harness=` flag (wins; throws if unknown).
94
+ * 2. config `harness` resolving to a single entry → use it.
95
+ * 3. config `harness` with multiple entries → use the first, with a loud notice.
96
+ * 4. no config → auto-detect, with a loud notice when the repo is ambiguous.
97
+ *
98
+ * `configHarness` is parsed once (alias-normalized) at the call site and passed
99
+ * in; this function re-normalizes idempotently so it's safe either way. Pure
100
+ * (besides reading `root`'s layout for detection) so the precedence is
101
+ * unit-testable without a real compile. See research/multi-harness-compile.md.
102
+ */
103
+ function resolveHarnessSelection(opts) {
104
+ const { root, flag, configHarness } = opts;
105
+ if (flag !== undefined && flag !== "") {
106
+ return { kind: "ok", adapter: resolveAdapter(root, flag) };
107
+ }
108
+ const list = normalizeHarnessList(configHarness);
109
+ if (list.length === 1) {
110
+ return { kind: "ok", adapter: resolveAdapter(root, list[0]) };
111
+ }
112
+ if (list.length > 1) {
113
+ const adapter = resolveAdapter(root, list[0]);
114
+ return {
115
+ kind: "notice",
116
+ adapter,
117
+ notice: `repo targets ${list.join(", ")} — compiling for ${adapter.name}; override with --harness=`,
118
+ };
119
+ }
120
+ const det = detectAdapterResult(root);
121
+ if (det.ambiguousWith.length > 0) {
122
+ return {
123
+ kind: "notice",
124
+ adapter: det.adapter,
125
+ notice: `repo matches ${[det.adapter.name, ...det.ambiguousWith].join(", ")} — set "harness" in .vigilesrc.json or use --harness=`,
126
+ };
127
+ }
128
+ return { kind: "ok", adapter: det.adapter };
129
+ }
55
130
  //# sourceMappingURL=adapter-registry.js.map
package/dist/cli.js CHANGED
@@ -21,9 +21,9 @@ const types_js_1 = require("./core/types.js");
21
21
  const test_coverage_js_1 = require("./test-coverage.js");
22
22
  const scan_js_1 = require("./scan.js");
23
23
  const adapter_registry_js_1 = require("./adapter-registry.js");
24
+ const skill_harness_js_1 = require("./skill-harness.js");
24
25
  const leaderboard_js_1 = require("./leaderboard.js");
25
26
  const compile_js_1 = require("./core/compile.js");
26
- const dialect_js_1 = require("./adapters/claude-code/dialect.js");
27
27
  const proofs_js_1 = require("./core/proofs.js");
28
28
  const inline_js_1 = require("./core/inline.js");
29
29
  const frontmatter_js_1 = require("./core/frontmatter.js");
@@ -140,12 +140,12 @@ function compileGeneratorSkillToFile(specPath, source) {
140
140
  return false;
141
141
  }
142
142
  /** Compile a ClaudeSpec → its primary + any additional targets. */
143
- function compileClaudeToFile(spec, specPath, config) {
143
+ function compileClaudeToFile(spec, specPath, config, dialect) {
144
144
  const basePath = process.cwd();
145
145
  const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
146
146
  basePath,
147
147
  specFile: specPath,
148
- dialect: dialect_js_1.claudeCodeDialect,
148
+ dialect,
149
149
  maxRules: config.maxRules,
150
150
  maxTokens: config.maxTokens,
151
151
  maxSectionLines: config.maxSectionLines,
@@ -175,15 +175,54 @@ function compileClaudeToFile(spec, specPath, config) {
175
175
  console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
176
176
  return true;
177
177
  }
178
+ /**
179
+ * Branch 3 of the mirror story (research/multi-harness-compile.md): when a repo
180
+ * declares ≥2 harnesses and nothing else fans out the instruction file, write a
181
+ * byte-identical copy to each other harness's instruction file (e.g. CLAUDE.md →
182
+ * AGENTS.md). A copy — not a symlink — because it works everywhere and carries
183
+ * the source's embedded integrity hash by construction, so a hand-edit of the
184
+ * mirror trips the existing `integrity` check. Never fights a sync tool or
185
+ * clobbers a target that owns its own spec.
186
+ */
187
+ function writeInstructionMirrors(primaryOutput, harnesses) {
188
+ if (harnesses.length < 2)
189
+ return;
190
+ const cwd = process.cwd();
191
+ // A sync tool (Ruler/rulesync) owns fan-out — don't fight it.
192
+ if ((0, compose_js_1.detectSyncTools)(cwd).length > 0)
193
+ return;
194
+ const primaryName = (0, node_path_1.basename)(primaryOutput);
195
+ const primaryAbs = (0, node_path_1.resolve)(cwd, primaryOutput);
196
+ if (!(0, node_fs_1.existsSync)(primaryAbs))
197
+ return;
198
+ const content = (0, node_fs_1.readFileSync)(primaryAbs, "utf-8");
199
+ for (const name of harnesses) {
200
+ const adapter = (0, adapter_registry_js_1.getAdapter)(name);
201
+ if (!adapter)
202
+ continue;
203
+ const target = adapter.layout.instructionFile;
204
+ if (target === primaryName)
205
+ continue; // the file we just compiled
206
+ // Never clobber a target that has its own spec (a genuinely separate file).
207
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, `${target}.spec.ts`)))
208
+ continue;
209
+ const targetAbs = (0, node_path_1.resolve)(cwd, target);
210
+ if ((0, node_fs_1.existsSync)(targetAbs) && (0, node_fs_1.readFileSync)(targetAbs, "utf-8") === content) {
211
+ continue; // already byte-identical
212
+ }
213
+ (0, node_fs_1.writeFileSync)(targetAbs, content);
214
+ console.log(` ↳ mirrored ${primaryName} → ${target} (byte-identical)`);
215
+ }
216
+ }
178
217
  /** Compile a declarative SkillSpec → SKILL.md. */
179
- function compileSkillToFile(spec, specPath) {
218
+ function compileSkillToFile(spec, specPath, dialect) {
180
219
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
181
220
  const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
182
221
  basePath: process.cwd(),
183
222
  specFile: specPath,
184
- // Pick the SKILL.md frontmatter profile from the detected harness — a Codex
223
+ // The SKILL.md frontmatter profile comes from the resolved harness — a Codex
185
224
  // repo gets a minimal (name + description) SKILL.md; CC gets the full set.
186
- dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
225
+ dialect,
187
226
  });
188
227
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
189
228
  if (errors.length === 0) {
@@ -195,12 +234,12 @@ function compileSkillToFile(spec, specPath) {
195
234
  return false;
196
235
  }
197
236
  /** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
198
- function compileAgentToFile(spec, specPath) {
237
+ function compileAgentToFile(spec, specPath, dialect) {
199
238
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
200
239
  const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
201
240
  basePath: process.cwd(),
202
241
  specFile: specPath,
203
- dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
242
+ dialect,
204
243
  });
205
244
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
206
245
  if (errors.length === 0) {
@@ -241,8 +280,21 @@ async function collectAgentNames() {
241
280
  }
242
281
  return names;
243
282
  }
244
- async function compile(specPaths, config) {
283
+ async function compile(specPaths, config, opts = {}) {
245
284
  let allValid = true;
285
+ // Parse the declared harness set ONCE (alias-normalized) and feed both the
286
+ // dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
287
+ // helpers. A loud notice (never a silent guess) on a multi-harness or
288
+ // ambiguous-detection pick.
289
+ const declaredHarnesses = (0, adapter_registry_js_1.normalizeHarnessList)(config.harness);
290
+ const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
291
+ root: process.cwd(),
292
+ flag: opts.harnessFlag,
293
+ configHarness: declaredHarnesses,
294
+ });
295
+ if (selection.kind === "notice")
296
+ console.log(`⚠ ${selection.notice}`);
297
+ const dialect = selection.adapter.dialect;
246
298
  // Resolved lazily on the first railway spec — every delegate() target is
247
299
  // checked against the agents defined anywhere in the project.
248
300
  let knownAgents = null;
@@ -262,15 +314,36 @@ async function compile(specPaths, config) {
262
314
  continue;
263
315
  }
264
316
  if (spec._specType === "claude") {
265
- if (!compileClaudeToFile(spec, specPath, config))
317
+ // Spec-target disambiguation: a CLAUDE.md.spec.ts is a claude-code file, an
318
+ // AGENTS.md.spec.ts a codex one — the strongest dialect signal for THIS
319
+ // spec. The flag still overrides; absent one, the spec's own target wins
320
+ // over config/detect. (Skill/agent targets don't name a harness, so they
321
+ // keep the run-level dialect.)
322
+ const targetFile = (0, node_path_1.basename)(specPath).replace(/\.spec\.ts$/, "");
323
+ const specDialect = opts.harnessFlag === undefined
324
+ ? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
325
+ : dialect;
326
+ if (compileClaudeToFile(spec, specPath, config, specDialect)) {
327
+ writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
328
+ }
329
+ else {
266
330
  allValid = false;
331
+ }
267
332
  }
268
333
  else if (spec._specType === "skill") {
269
- if (!compileSkillToFile(spec, specPath))
334
+ // Cross-harness verify: flag CC-only frontmatter a declared minimal-profile
335
+ // harness (Codex/OpenCode) would silently drop.
336
+ const forHarnesses = declaredHarnesses.length > 0
337
+ ? declaredHarnesses
338
+ : [selection.adapter.name];
339
+ for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
340
+ console.log(`⚠ ${w}`);
341
+ }
342
+ if (!compileSkillToFile(spec, specPath, dialect))
270
343
  allValid = false;
271
344
  }
272
345
  else if (spec._specType === "agent") {
273
- if (!compileAgentToFile(spec, specPath))
346
+ if (!compileAgentToFile(spec, specPath, dialect))
274
347
  allValid = false;
275
348
  }
276
349
  else if (spec._specType === "railway") {
@@ -1735,16 +1808,44 @@ async function setup(args) {
1735
1808
  console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
1736
1809
  console.log(" npm install -D rule-porter");
1737
1810
  }
1738
- // Strict config.
1739
- if (strict) {
1740
- const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
1741
- if (!(0, node_fs_1.existsSync)(configPath)) {
1742
- (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({ rules: { "require-spec": "error", "require-skill-spec": "error" } }, null, 2) + "\n");
1743
- console.log("✓ Created .vigilesrc.json with strict rules");
1744
- written.push(".vigilesrc.json");
1811
+ // Project config — record the harness(es) so compile/lint select the dialect
1812
+ // deterministically (no cwd sniffing), plus strict rule severities on --strict.
1813
+ writeProjectConfig({ harnesses, strict, written });
1814
+ printSetupSummary({ plan, strict, targets, needsMigration, written });
1815
+ }
1816
+ /** Canonical, de-duplicated harness list → a config value (string when one). */
1817
+ function harnessConfigValue(harnesses) {
1818
+ const canon = [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
1819
+ return canon.length === 1 ? canon[0] : canon;
1820
+ }
1821
+ /**
1822
+ * Merge the resolved harness(es) (and strict rule severities) into
1823
+ * `.vigilesrc.json` without clobbering existing keys — an existing `harness`
1824
+ * stays, a missing one is added, a malformed file is left untouched.
1825
+ */
1826
+ function writeProjectConfig(opts) {
1827
+ const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
1828
+ const existed = (0, node_fs_1.existsSync)(configPath);
1829
+ let existing = {};
1830
+ if (existed) {
1831
+ try {
1832
+ existing = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
1833
+ }
1834
+ catch {
1835
+ return; // user-owned malformed config — never clobber it
1745
1836
  }
1746
1837
  }
1747
- printSetupSummary({ plan, strict, targets, needsMigration, written });
1838
+ const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
1839
+ harness: harnessConfigValue(opts.harnesses),
1840
+ strict: opts.strict,
1841
+ });
1842
+ if (!merged)
1843
+ return;
1844
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(merged, null, 2) + "\n");
1845
+ console.log(`✓ ${existed ? "Updated" : "Created"} .vigilesrc.json`);
1846
+ if (!opts.written.includes(".vigilesrc.json")) {
1847
+ opts.written.push(".vigilesrc.json");
1848
+ }
1748
1849
  }
1749
1850
  // ---------------------------------------------------------------------------
1750
1851
  // Strengthen: guidance() → enforce() suggestions
@@ -2367,7 +2468,10 @@ async function main() {
2367
2468
  console.log("Run `vigiles init` to create one.");
2368
2469
  process.exit(0);
2369
2470
  }
2370
- const valid = await compile(specs, config);
2471
+ const harnessFlag = args
2472
+ .find((a) => a.startsWith("--harness="))
2473
+ ?.slice("--harness=".length);
2474
+ const valid = await compile(specs, config, { harnessFlag });
2371
2475
  console.log("");
2372
2476
  if (valid) {
2373
2477
  console.log("Compilation complete.");
@@ -127,6 +127,16 @@ export interface VigilesConfig {
127
127
  }>;
128
128
  /** Orphan-docs check configuration. Include/exclude globs, tsconfig-style. */
129
129
  orphans?: OrphansConfig;
130
+ /**
131
+ * The harness(es) this repo targets — selects the compile dialect / skill
132
+ * frontmatter profile / instruction-file shape, instead of sniffing the cwd.
133
+ * A single name (`"codex"`) for the common single-harness repo, or an array
134
+ * (`["claude-code", "codex"]`) declaring the supported set. Written by
135
+ * `vigiles init`. Omitted → the CLI auto-detects (backwards-compatible).
136
+ * Canonical adapter names; `"claude"` is accepted as an alias for
137
+ * `"claude-code"`. See research/multi-harness-compile.md.
138
+ */
139
+ harness?: string | string[];
130
140
  }
131
141
  /** Valid marker types for rule detection. */
132
142
  export type MarkerType = "headings" | "checkboxes";
@@ -44,6 +44,17 @@ export interface ParsedSetupArgs {
44
44
  export declare function parseSetupArgs(args: readonly string[]): ParsedSetupArgs;
45
45
  /** The non-interactive defaults: both pillars, CI, and the plugin. */
46
46
  export declare function defaultPlan(strict?: boolean): SetupPlan;
47
+ /**
48
+ * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
49
+ * the `harness` if absent, add strict rule severities if `--strict`, NEVER
50
+ * clobber an existing key. Returns the merged config, or `null` when nothing
51
+ * changed (so the IO layer skips the write). The IO (read/parse/write + the
52
+ * malformed-file guard) stays in cli.ts.
53
+ */
54
+ export declare function mergeProjectConfig(existing: Record<string, unknown>, opts: {
55
+ harness: string | string[];
56
+ strict: boolean;
57
+ }): Record<string, unknown> | null;
47
58
  /**
48
59
  * Whether to drop into interactive prompts: a human at a TTY who passed neither
49
60
  * `--yes` nor an explicit `--target`, and who hasn't already pinned every choice
@@ -12,6 +12,7 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.parseSetupArgs = parseSetupArgs;
14
14
  exports.defaultPlan = defaultPlan;
15
+ exports.mergeProjectConfig = mergeProjectConfig;
15
16
  exports.shouldPrompt = shouldPrompt;
16
17
  exports.planPluginInstall = planPluginInstall;
17
18
  exports.resolvePlan = resolvePlan;
@@ -52,6 +53,32 @@ function defaultPlan(strict = false) {
52
53
  force: false,
53
54
  };
54
55
  }
56
+ /**
57
+ * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
58
+ * the `harness` if absent, add strict rule severities if `--strict`, NEVER
59
+ * clobber an existing key. Returns the merged config, or `null` when nothing
60
+ * changed (so the IO layer skips the write). The IO (read/parse/write + the
61
+ * malformed-file guard) stays in cli.ts.
62
+ */
63
+ function mergeProjectConfig(existing, opts) {
64
+ const config = { ...existing };
65
+ let changed = false;
66
+ if (config.harness === undefined) {
67
+ config.harness = opts.harness;
68
+ changed = true;
69
+ }
70
+ if (opts.strict) {
71
+ const rules = { ...config.rules };
72
+ for (const r of ["require-spec", "require-skill-spec"]) {
73
+ if (rules[r] === undefined) {
74
+ rules[r] = "error";
75
+ changed = true;
76
+ }
77
+ }
78
+ config.rules = rules;
79
+ }
80
+ return changed ? config : null;
81
+ }
55
82
  /**
56
83
  * Whether to drop into interactive prompts: a human at a TTY who passed neither
57
84
  * `--yes` nor an explicit `--target`, and who hasn't already pinned every choice
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Cross-harness skill-frontmatter verification (slice 3 of
3
+ * research/multi-harness-compile.md, the *verify* half).
4
+ *
5
+ * A skill's `SKILL.md` references are harness-agnostic; the one harness-specific
6
+ * surface is the frontmatter PROFILE. The `claude-code` profile emits CC-only
7
+ * keys (`disable-model-invocation`, `argument-hint`); the `minimal` profile
8
+ * (Codex, OpenCode) omits them. So a skill that sets those keys, in a repo that
9
+ * also targets a minimal-profile harness, has a silent semantic gap: the
10
+ * constraint the author expressed won't take effect there.
11
+ *
12
+ * This reports that gap. It is ASSUMPTION-FREE — the minimal profile *drops* the
13
+ * keys, so the warning states a fact about vigiles's own output, not a guess
14
+ * about another tool's parser tolerance.
15
+ */
16
+ import type { SkillSpec } from "./core/spec.js";
17
+ /** The Claude-Code-only frontmatter keys a skill spec would emit. */
18
+ export declare function claudeOnlyFrontmatterKeys(spec: SkillSpec): string[];
19
+ /**
20
+ * Warn for each declared harness whose `minimal` SKILL.md profile would DROP a
21
+ * skill's Claude-Code-only frontmatter. Empty when the skill uses no such keys or
22
+ * no declared harness is minimal-profile.
23
+ */
24
+ export declare function skillFrontmatterDropWarnings(spec: SkillSpec, harnessNames: readonly string[]): string[];
25
+ //# sourceMappingURL=skill-harness.d.ts.map
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeOnlyFrontmatterKeys = claudeOnlyFrontmatterKeys;
4
+ exports.skillFrontmatterDropWarnings = skillFrontmatterDropWarnings;
5
+ const adapter_registry_js_1 = require("./adapter-registry.js");
6
+ /** The Claude-Code-only frontmatter keys a skill spec would emit. */
7
+ function claudeOnlyFrontmatterKeys(spec) {
8
+ const keys = [];
9
+ if (spec.disableModelInvocation !== undefined) {
10
+ keys.push("disable-model-invocation");
11
+ }
12
+ if (spec.argumentHint || (spec.inputs && spec.inputs.length > 0)) {
13
+ keys.push("argument-hint");
14
+ }
15
+ return keys;
16
+ }
17
+ /**
18
+ * Warn for each declared harness whose `minimal` SKILL.md profile would DROP a
19
+ * skill's Claude-Code-only frontmatter. Empty when the skill uses no such keys or
20
+ * no declared harness is minimal-profile.
21
+ */
22
+ function skillFrontmatterDropWarnings(spec, harnessNames) {
23
+ const ccKeys = claudeOnlyFrontmatterKeys(spec);
24
+ if (ccKeys.length === 0)
25
+ return [];
26
+ const warnings = [];
27
+ const seen = new Set();
28
+ for (const name of harnessNames) {
29
+ const adapter = (0, adapter_registry_js_1.getAdapter)(name);
30
+ if (!adapter || seen.has(adapter.name))
31
+ continue;
32
+ seen.add(adapter.name);
33
+ if (adapter.dialect.skillFrontmatter === "minimal") {
34
+ const one = ccKeys.length === 1;
35
+ warnings.push(`skill "${spec.name}": ${ccKeys.join(", ")} ${one ? "is" : "are"} Claude-Code-only — declared harness "${adapter.name}" drops ${one ? "it" : "them"}.`);
36
+ }
37
+ }
38
+ return warnings;
39
+ }
40
+ //# sourceMappingURL=skill-harness.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "Compile .spec.ts files to instruction files (CLAUDE.md, AGENTS.md) with linter cross-referencing",
5
5
  "bin": {
6
6
  "vigiles": "dist/cli.js"