vigiles 9.0.0 → 9.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.
@@ -4,9 +4,12 @@
4
4
  * A single structural-health number (the leaderboard's `scoreReport`) ranks
5
5
  * plugins, but it hides WHERE a harness is weak. This buckets the SAME
6
6
  * deterministic findings into four categories — Truthfulness, Triggering,
7
- * Structure, Tested — each a 0–100 ring, with a weighted overall. Same
8
- * detectors, no re-detection (one-detector-no-drift); all deterministic, no
9
- * execution. (Safety — "do your hooks actually block?" — is NOT an `audit` ring:
7
+ * Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown beneath one
8
+ * headline `overall` = `100 − Σ(all graded penalties)` (the SAME summed model as
9
+ * the leaderboard's single health number, via the shared `computeIntegrityScore`
10
+ * — so the two surfaces never disagree). Same detectors, no re-detection
11
+ * (one-detector-no-drift); all deterministic, no execution. (Safety — "do your
12
+ * hooks actually block?" — is NOT an `audit` ring:
10
13
  * it requires executing your hooks, which needs cross-platform confinement
11
14
  * that isn't shipped yet, so it lives in the `vigiles/testing` API via
12
15
  * `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.)
@@ -23,11 +26,25 @@ export interface CategoryScore {
23
26
  readonly score: number | null;
24
27
  /** Relative weight in the overall (equal by default — tune later). */
25
28
  readonly weight: number;
29
+ /**
30
+ * Advisory categories are shown but EXCLUDED from the overall grade. An untested
31
+ * surface (or any best-practice gap) is a HARDENING signal, not a broken harness
32
+ * — it must never drag the grade down, so `audit` doesn't read as F on a clean
33
+ * repo that simply hasn't written tests yet. The grade reflects what's BROKEN.
34
+ */
35
+ readonly advisory?: boolean;
26
36
  /** Human-readable deductions / notes, worst first; empty when clean. */
27
37
  readonly findings: readonly string[];
28
38
  }
29
39
  export interface AuditScore {
30
- /** Weighted average over the ASSESSABLE categories (n/a excluded). 0 when empty. */
40
+ /**
41
+ * The headline score — `100 − Σ(all graded penalties)`, clamped to [0,100]
42
+ * (the SAME summed model as the leaderboard's single health number, computed by
43
+ * the shared {@link computeIntegrityScore}, so the two surfaces never disagree).
44
+ * The per-category rings below are a DIAGNOSTIC breakdown, not the headline: a
45
+ * plugin whose only issue is Structure −30 shows Structure 70 in the breakdown
46
+ * AND overall 70 (averaging the rings would dilute that to ~90). 0 when empty.
47
+ */
31
48
  readonly overall: number;
32
49
  readonly grade: PluginScore["grade"];
33
50
  readonly categories: readonly CategoryScore[];
@@ -35,10 +52,13 @@ export interface AuditScore {
35
52
  readonly empty: boolean;
36
53
  }
37
54
  /**
38
- * Bucket a scan report into the four deterministic Lighthouse categories with a
39
- * weighted overall. n/a categories are excluded from the overall, never scored 0.
55
+ * Bucket a scan report into the four deterministic Lighthouse categories as a
56
+ * DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
57
+ * penalties)` (the shared summed model — NOT the average of the rings — so it
58
+ * equals the leaderboard's single health number). The advisory Tested ring and
59
+ * any n/a ring are shown but excluded from the headline.
40
60
  */
41
61
  export declare function auditScore(report: ScanReport): AuditScore;
42
- /** Render the category rings + the weighted overall for the terminal. */
62
+ /** Render the category rings (diagnostic) + the summed overall for the terminal. */
43
63
  export declare function formatAuditScore(s: AuditScore): string;
44
64
  //# sourceMappingURL=audit-score.d.ts.map
@@ -8,9 +8,12 @@ exports.formatAuditScore = formatAuditScore;
8
8
  * A single structural-health number (the leaderboard's `scoreReport`) ranks
9
9
  * plugins, but it hides WHERE a harness is weak. This buckets the SAME
10
10
  * deterministic findings into four categories — Truthfulness, Triggering,
11
- * Structure, Tested — each a 0–100 ring, with a weighted overall. Same
12
- * detectors, no re-detection (one-detector-no-drift); all deterministic, no
13
- * execution. (Safety — "do your hooks actually block?" — is NOT an `audit` ring:
11
+ * Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown beneath one
12
+ * headline `overall` = `100 − Σ(all graded penalties)` (the SAME summed model as
13
+ * the leaderboard's single health number, via the shared `computeIntegrityScore`
14
+ * — so the two surfaces never disagree). Same detectors, no re-detection
15
+ * (one-detector-no-drift); all deterministic, no execution. (Safety — "do your
16
+ * hooks actually block?" — is NOT an `audit` ring:
14
17
  * it requires executing your hooks, which needs cross-platform confinement
15
18
  * that isn't shipped yet, so it lives in the `vigiles/testing` API via
16
19
  * `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.)
@@ -19,13 +22,9 @@ exports.formatAuditScore = formatAuditScore;
19
22
  * overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
20
23
  */
21
24
  const leaderboard_js_1 = require("./leaderboard.js");
22
- // Per-item penalties — mirror the leaderboard's weights so the category view and
23
- // the single health number stay consistent (broken-at-runtime costs most).
24
- const W_MISSING_HOOK = 15;
25
- const W_NO_DESCRIPTION = 10;
26
- const W_DANGLING_REF = 8;
27
- const W_OVERLAP = 8; // a description collision → the wrong skill fires
28
- const W_NO_CONTRACT = 5;
25
+ // Per-item penalties are the SHARED leaderboard weights (imported above) so the
26
+ // category rings and the single health number can never drift. W_UNTESTED is
27
+ // audit-only — untested surfaces are advisory (shown, never scored into overall).
29
28
  const W_UNTESTED = 3;
30
29
  /** Apply deductions to a 100 base, clamped to [0,100], collecting non-zero labels. */
31
30
  function scoreFrom(deductions) {
@@ -48,12 +47,12 @@ function truthfulness(r) {
48
47
  const { score, findings } = scoreFrom([
49
48
  {
50
49
  n: r.danglingRefs.length,
51
- weight: W_DANGLING_REF,
50
+ weight: leaderboard_js_1.W_DANGLING_REF,
52
51
  label: "broken intra-plugin reference(s)",
53
52
  },
54
53
  {
55
54
  n: missingHooks,
56
- weight: W_MISSING_HOOK,
55
+ weight: leaderboard_js_1.W_MISSING_HOOK,
57
56
  label: "hook script(s) missing (never run)",
58
57
  },
59
58
  ]);
@@ -64,12 +63,12 @@ function triggering(r) {
64
63
  const { score, findings } = scoreFrom([
65
64
  {
66
65
  n: noDesc,
67
- weight: W_NO_DESCRIPTION,
66
+ weight: leaderboard_js_1.W_NO_DESCRIPTION,
68
67
  label: "skill(s) with no usable description (can't trigger)",
69
68
  },
70
69
  {
71
70
  n: r.descriptionOverlaps.length,
72
- weight: W_OVERLAP,
71
+ weight: leaderboard_js_1.W_OVERLAP,
73
72
  label: "near-identical skill description(s) (wrong one fires)",
74
73
  },
75
74
  ]);
@@ -83,47 +82,47 @@ function structure(r) {
83
82
  const { score, findings } = scoreFrom([
84
83
  {
85
84
  n: deadTools,
86
- weight: W_DANGLING_REF,
85
+ weight: leaderboard_js_1.W_DANGLING_REF,
87
86
  label: "agent tool(s) that don't exist (typo / never-available)",
88
87
  },
89
88
  {
90
89
  n: deadMcpTools,
91
- weight: W_DANGLING_REF,
90
+ weight: leaderboard_js_1.W_DANGLING_REF,
92
91
  label: "agent MCP tool(s) whose server isn't declared",
93
92
  },
94
93
  {
95
94
  n: r.hookEventIssues.length,
96
- weight: W_MISSING_HOOK,
95
+ weight: leaderboard_js_1.W_MISSING_HOOK,
97
96
  label: "hook(s) on an unknown event (never fire)",
98
97
  },
99
98
  {
100
99
  n: r.mcpIssues.length,
101
- weight: W_DANGLING_REF,
100
+ weight: leaderboard_js_1.W_DANGLING_REF,
102
101
  label: "MCP server(s) that can't start (no command/url)",
103
102
  },
104
103
  {
105
104
  n: r.mcpHookIssues.length,
106
- weight: W_DANGLING_REF,
105
+ weight: leaderboard_js_1.W_DANGLING_REF,
107
106
  label: "mcp_tool hook(s) incomplete / undeclared server",
108
107
  },
109
108
  {
110
109
  n: r.frontmatterIssues.length,
111
- weight: W_NO_DESCRIPTION,
110
+ weight: leaderboard_js_1.W_NO_DESCRIPTION,
112
111
  label: "surface(s) missing required frontmatter",
113
112
  },
114
113
  {
115
114
  n: r.frontmatterValueIssues.length,
116
- weight: W_NO_CONTRACT,
115
+ weight: leaderboard_js_1.W_NO_CONTRACT,
117
116
  label: "agent(s) with an invalid model/color (silent fallback)",
118
117
  },
119
118
  {
120
119
  n: deadDisallowed,
121
- weight: W_NO_CONTRACT,
120
+ weight: leaderboard_js_1.W_NO_CONTRACT,
122
121
  label: "disallowedTools typo(s) that block nothing",
123
122
  },
124
123
  {
125
124
  n: noContract,
126
- weight: W_NO_CONTRACT,
125
+ weight: leaderboard_js_1.W_NO_CONTRACT,
127
126
  label: "agent(s) inherit all tools (no contract)",
128
127
  },
129
128
  ]);
@@ -133,26 +132,29 @@ function tested(r) {
133
132
  const { score, findings } = scoreFrom([
134
133
  { n: r.untested, weight: W_UNTESTED, label: "untested surface(s)" },
135
134
  ]);
136
- return { key: "Tested", score, weight: 1, findings };
135
+ // ADVISORY: untested surfaces are a hardening gap, not breakage — shown, but
136
+ // excluded from the overall grade (so a clean-but-untested repo isn't graded F).
137
+ return { key: "Tested", score, weight: 1, advisory: true, findings };
137
138
  }
138
- function isEmptyMachine(r) {
139
- const surfaces = r.skills.length +
140
- r.agents.length +
141
- r.hooks.length +
142
- r.inlineHooks +
143
- r.commands;
144
- // An instruction-only repo (just a CLAUDE.md/AGENTS.md, no plugin surface) is
145
- // NOT empty — the scan records `instructions` precisely so it isn't graded
146
- // F/0 "no loadable surface". Only a dir with NO instruction file AND no
147
- // surface is the empty machine.
148
- return surfaces === 0 && !r.mcp && !r.instructions;
139
+ /**
140
+ * An instruction-only repo (just a CLAUDE.md/AGENTS.md, no plugin surface) is NOT
141
+ * empty — the scan records `instructions` precisely so it isn't graded F/0 "no
142
+ * loadable surface". Only a dir with NO instruction file AND no surface is empty.
143
+ * (The shared `isEmptyMachine` ignores `instructions`; audit additionally treats
144
+ * an instruction file as a surface.)
145
+ */
146
+ function isEmptyAudit(r) {
147
+ return (0, leaderboard_js_1.isEmptyMachine)(r) && !r.instructions;
149
148
  }
150
149
  /**
151
- * Bucket a scan report into the four deterministic Lighthouse categories with a
152
- * weighted overall. n/a categories are excluded from the overall, never scored 0.
150
+ * Bucket a scan report into the four deterministic Lighthouse categories as a
151
+ * DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
152
+ * penalties)` (the shared summed model — NOT the average of the rings — so it
153
+ * equals the leaderboard's single health number). The advisory Tested ring and
154
+ * any n/a ring are shown but excluded from the headline.
153
155
  */
154
156
  function auditScore(report) {
155
- if (isEmptyMachine(report)) {
157
+ if (isEmptyAudit(report)) {
156
158
  const categories = [
157
159
  "Truthfulness",
158
160
  "Triggering",
@@ -177,11 +179,11 @@ function auditScore(report) {
177
179
  structure(report),
178
180
  tested(report),
179
181
  ];
180
- const assessable = categories.filter((c) => c.score !== null);
181
- const totalWeight = assessable.reduce((s, c) => s + c.weight, 0);
182
- const overall = totalWeight === 0
183
- ? 0
184
- : Math.round(assessable.reduce((s, c) => s + c.score * c.weight, 0) / totalWeight);
182
+ // The headline is the SUMMED model (the shared integrity score), NOT the average
183
+ // of the rings — averaging would let a real problem in one category be diluted
184
+ // by clean siblings. The rings above stay a diagnostic breakdown; Tested
185
+ // (advisory) is never summed in (untested surfaces don't drag the grade).
186
+ const { score: overall } = (0, leaderboard_js_1.computeIntegrityScore)((0, leaderboard_js_1.reportDeductions)(report));
185
187
  return { overall, grade: (0, leaderboard_js_1.gradeFor)(overall), categories, empty: false };
186
188
  }
187
189
  // A 22-cell bar gauge ("ring" in the terminal; the real rings are the HTML).
@@ -202,14 +204,15 @@ function bar(score) {
202
204
  const filled = Math.round((score / 100) * BAR_CELLS);
203
205
  return "█".repeat(filled) + "░".repeat(BAR_CELLS - filled);
204
206
  }
205
- /** Render the category rings + the weighted overall for the terminal. */
207
+ /** Render the category rings (diagnostic) + the summed overall for the terminal. */
206
208
  function formatAuditScore(s) {
207
209
  const lines = ["Harness audit", ""];
208
210
  for (const c of s.categories) {
209
211
  const glyph = bandGlyph(c.score);
210
212
  const label = c.key.padEnd(13);
211
213
  const num = (c.score === null ? "n/a" : String(c.score)).padStart(4);
212
- lines.push(` ${glyph} ${label} ${num} ${bar(c.score)}`);
214
+ const tag = c.advisory ? " · advisory (not graded)" : "";
215
+ lines.push(` ${glyph} ${label} ${num} ${bar(c.score)}${tag}`);
213
216
  if (c.findings.length > 0) {
214
217
  lines.push(` └ ${c.findings.join("; ")}`);
215
218
  }
package/dist/cli.js CHANGED
@@ -1219,6 +1219,100 @@ function targetHasHash(absPath) {
1219
1219
  * target by `setupPillar1`. The full onboarding (both layers, deps, CI, plugin)
1220
1220
  * is `setup()`.
1221
1221
  */
1222
+ /** Classify an adoption target by its path: a `SKILL.md` is a skill, a file under
1223
+ * an `agents/` dir is a subagent, everything else is an instruction file. Used to
1224
+ * pick the right adopt function so `init --target=skills/x/SKILL.md` (the
1225
+ * per-surface path the audit report points at) makes a `skill()`/`agent()` spec. */
1226
+ function surfaceKind(target) {
1227
+ if (/^SKILL\.md$/i.test((0, node_path_1.basename)(target)))
1228
+ return "skill";
1229
+ if (/(^|[/\\])agents[/\\]/.test(target))
1230
+ return "agent";
1231
+ return "instruction";
1232
+ }
1233
+ function logAdoptedSurface(target, specPath, label, unmappedKeys) {
1234
+ const note = unmappedKeys.length > 0
1235
+ ? ` (review the // NOTE — unmapped frontmatter: ${unmappedKeys.join(", ")})`
1236
+ : "";
1237
+ console.log(`Adopted ${label} ${target} → ${specPath}${note}. ` +
1238
+ `Run \`vigiles compile\` and review the diff.`);
1239
+ }
1240
+ /** Discover existing skill (`skills/<x>/SKILL.md`) and subagent (`agents/<x>.md`)
1241
+ * surfaces — under the bare or `.claude/` roots — that don't yet have a spec, so
1242
+ * bare `vigiles init` creates a spec for EVERY surface it can, not just the
1243
+ * instruction file. Shallow (top-level only) so it never walks node_modules or a
1244
+ * vendored plugin. CC paths are intentional here — `init` is the one composition
1245
+ * point allowed to know them (see adapter-aware-lint-rules). */
1246
+ function discoverAdoptableSurfaces(cwd) {
1247
+ const out = [];
1248
+ const unspecced = (rel) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, rel)) &&
1249
+ !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, `${rel}.spec.ts`));
1250
+ for (const root of ["skills", ".claude/skills"]) {
1251
+ const abs = (0, node_path_1.resolve)(cwd, root);
1252
+ if (!(0, node_fs_1.existsSync)(abs))
1253
+ continue;
1254
+ for (const e of (0, node_fs_1.readdirSync)(abs, { withFileTypes: true })) {
1255
+ const rel = `${root}/${e.name}/SKILL.md`;
1256
+ if (e.isDirectory() && unspecced(rel))
1257
+ out.push(rel);
1258
+ }
1259
+ }
1260
+ for (const root of ["agents", ".claude/agents"]) {
1261
+ const abs = (0, node_path_1.resolve)(cwd, root);
1262
+ if (!(0, node_fs_1.existsSync)(abs))
1263
+ continue;
1264
+ for (const e of (0, node_fs_1.readdirSync)(abs, { withFileTypes: true })) {
1265
+ const rel = `${root}/${e.name}`;
1266
+ if (e.isFile() && e.name.endsWith(".md") && unspecced(rel))
1267
+ out.push(rel);
1268
+ }
1269
+ }
1270
+ return out;
1271
+ }
1272
+ /** The full adoptable-surface list `audit` reports: the instruction file (when it
1273
+ * exists hand-written, no spec) PLUS every skill/subagent surface without a spec.
1274
+ * Same notion `init` adopts; surfaced in the AuditReport + the terminal nudge so
1275
+ * the report's "Create spec" / "Create all specs" affordances have their paths.
1276
+ * Composition-root only — CC paths are intentional here (like discoverAdoptableSurfaces). */
1277
+ function discoverAdoptableForAudit(root, instructionFile) {
1278
+ const out = [];
1279
+ const instrAbs = (0, node_path_1.resolve)(root, instructionFile);
1280
+ if ((0, node_fs_1.existsSync)(instrAbs) &&
1281
+ !targetHasHash(instrAbs) &&
1282
+ !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, `${instructionFile}.spec.ts`))) {
1283
+ out.push(instructionFile);
1284
+ }
1285
+ out.push(...discoverAdoptableSurfaces(root));
1286
+ return out;
1287
+ }
1288
+ /** The terminal "adoptable surfaces" nudge — N un-spec'd surfaces + the create-all
1289
+ * command and up to ~5 per-surface commands (then "+K more"). "" when nothing to
1290
+ * adopt (a fully spec-managed repo says nothing). */
1291
+ function formatAdoptableNudge(surfaces) {
1292
+ if (surfaces.length === 0)
1293
+ return "";
1294
+ const n = surfaces.length;
1295
+ const lines = [
1296
+ `ℹ ${String(n)} surface${n === 1 ? "" : "s"} not yet spec-managed — create specs with \`npx vigiles init\``,
1297
+ ` (or one at a time: \`npx vigiles init --target=<path>\`)`,
1298
+ ];
1299
+ const shown = surfaces.slice(0, 5);
1300
+ for (const s of shown)
1301
+ lines.push(` • npx vigiles init --target=${s}`);
1302
+ const more = n - shown.length;
1303
+ if (more > 0)
1304
+ lines.push(` • +${String(more)} more`);
1305
+ return lines.join("\n");
1306
+ }
1307
+ /** A small, terse behavioral nudge — the deterministic read can't tell whether a
1308
+ * skill actually FIRES. "" when there are no model-invocable skills. */
1309
+ function formatTriggerNudge(triggerableSkills) {
1310
+ if (triggerableSkills <= 0)
1311
+ return "";
1312
+ const n = triggerableSkills;
1313
+ return (`ℹ Do your ${String(n)} skill${n === 1 ? "" : "s"} actually fire? The deterministic read can't tell — ` +
1314
+ `run \`audit\` interactively to measure, or test with \`measureTriggerRate\` (vigiles/testing).`);
1315
+ }
1222
1316
  function scaffoldSpec(args) {
1223
1317
  const targetFlag = args.find((a) => a.startsWith("--target="));
1224
1318
  const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
@@ -1237,11 +1331,24 @@ function scaffoldSpec(args) {
1237
1331
  const targetAbs = (0, node_path_1.resolve)(process.cwd(), target);
1238
1332
  if ((0, node_fs_1.existsSync)(targetAbs) && !targetHasHash(targetAbs)) {
1239
1333
  const md = (0, node_fs_1.readFileSync)(targetAbs, "utf-8");
1240
- const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target));
1241
1334
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
1242
- (0, node_fs_1.writeFileSync)(specAbs, source);
1243
- console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
1244
- `Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
1335
+ const kind = surfaceKind(target);
1336
+ if (kind === "skill") {
1337
+ const { source, unmappedKeys } = (0, adopt_js_1.adoptSkill)(md, (0, node_path_1.basename)((0, node_path_1.dirname)(target)));
1338
+ (0, node_fs_1.writeFileSync)(specAbs, source);
1339
+ logAdoptedSurface(target, specPath, "skill", unmappedKeys);
1340
+ }
1341
+ else if (kind === "agent") {
1342
+ const { source, unmappedKeys } = (0, adopt_js_1.adoptAgent)(md, (0, node_path_1.basename)(target, ".md"));
1343
+ (0, node_fs_1.writeFileSync)(specAbs, source);
1344
+ logAdoptedSurface(target, specPath, "subagent", unmappedKeys);
1345
+ }
1346
+ else {
1347
+ const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target));
1348
+ (0, node_fs_1.writeFileSync)(specAbs, source);
1349
+ console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
1350
+ `Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
1351
+ }
1245
1352
  return;
1246
1353
  }
1247
1354
  // The compiled output is derived from the spec FILE path; the spec's `target`
@@ -1768,9 +1875,15 @@ async function setupPillar1(detected, targetValue, harnesses) {
1768
1875
  // An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
1769
1876
  // AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
1770
1877
  // into a sync tool's source slot when one would own the output.
1771
- const targets = targetValue
1878
+ const instructionTargets = targetValue
1772
1879
  ? determineTargets(detected, targetValue, harnesses)
1773
1880
  : redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
1881
+ // Bare `init` (no explicit --target) also adopts every existing skill +
1882
+ // subagent surface — "create all the specs it can", not just the instruction
1883
+ // file. An explicit --target stays scoped to that one surface.
1884
+ const targets = targetValue
1885
+ ? instructionTargets
1886
+ : [...instructionTargets, ...discoverAdoptableSurfaces(cwd)];
1774
1887
  // Create specs. An existing hand-written target is faithfully ADOPTED into a
1775
1888
  // spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
1776
1889
  // compile below reproduces it (the user reviews the diff). A greenfield target
@@ -4383,9 +4496,15 @@ async function main() {
4383
4496
  // HTML renders, `--json` emits, and (later) a hosted dashboard ingests.
4384
4497
  // Built ONCE; the rings + fix list are read off it. Pure deterministic —
4385
4498
  // nothing executes to produce it.
4499
+ // Surfaces that exist but aren't spec-managed yet — the same notion
4500
+ // `init` adopts (layout-driven instruction file + skill/subagent sweep).
4501
+ // Surfaced in the AuditReport (the report's "Create spec" command-emit
4502
+ // buttons read it) and the terminal nudge below.
4503
+ const adoptableSurfaces = discoverAdoptableForAudit(root, adapter.layout.instructionFile);
4386
4504
  const auditReport = (0, audit_report_js_1.buildAuditReport)(report, {
4387
4505
  harness: adapter.name,
4388
4506
  vigilesVersion: getVersion(),
4507
+ adoptableSurfaces,
4389
4508
  });
4390
4509
  const sc = auditReport.score;
4391
4510
  const plan = (0, optimize_js_1.optimize)(report);
@@ -4404,6 +4523,18 @@ async function main() {
4404
4523
  const fixes = (0, optimize_js_1.formatRecommendations)(plan);
4405
4524
  if (fixes)
4406
4525
  console.log("\n" + fixes);
4526
+ // Adoption nudge: surfaces that exist but aren't spec-managed yet, with
4527
+ // the create-all + per-surface `init` commands (the JSON carries the
4528
+ // data in `adoptable` instead — the terminal stays human-readable).
4529
+ const adoptNudge = formatAdoptableNudge(adoptableSurfaces);
4530
+ if (adoptNudge)
4531
+ console.log("\n" + adoptNudge);
4532
+ // A small behavioral nudge — the deterministic read can't tell whether
4533
+ // skills actually FIRE; point at the interactive measure + the API.
4534
+ const fireNudge = formatTriggerNudge(report.skills.filter((s) => s.hasDescription && !s.userInvoked)
4535
+ .length);
4536
+ if (fireNudge)
4537
+ console.log("\n" + fireNudge);
4407
4538
  }
4408
4539
  // ONE read-vs-run decision for the EXECUTING checks (live MCP + skill
4409
4540
  // firing). A plain `audit` is a deterministic READ; these run only on
@@ -18,6 +18,7 @@
18
18
  * vs `enforce()` is deliberately NOT guessed here — that cross-referencing is
19
19
  * `strengthen`'s separate, later job; adoption is lossless transcription.
20
20
  */
21
+ import { type SkillSpec, type AgentSpec } from "./spec.js";
21
22
  export type AdoptTier = "structured" | "raw";
22
23
  export interface AdoptResult {
23
24
  /** Generated `.spec.ts` source (compiles back to ~the original file). */
@@ -62,4 +63,31 @@ export declare function adoptToSpec(markdown: string, target: string): AdoptedSp
62
63
  * (the deliverable `init` writes).
63
64
  */
64
65
  export declare function adoptMarkdown(markdown: string, target: string): AdoptResult;
66
+ export interface AdoptSurfaceResult {
67
+ /** Generated `.spec.ts` source. */
68
+ source: string;
69
+ kind: "skill" | "agent";
70
+ /**
71
+ * The parsed spec object the source builds — exposed so a round-trip test can
72
+ * feed it straight to `compileSkill`/`compileAgent` without evaluating the
73
+ * generated TS (the same split as `adoptToSpec`/`renderSpecSource`).
74
+ */
75
+ spec: SkillSpec | AgentSpec;
76
+ /**
77
+ * Frontmatter keys present in the source that the typed spec has no field for
78
+ * — emitted as a `// NOTE:` comment in the source so nothing is lost silently.
79
+ */
80
+ unmappedKeys: string[];
81
+ }
82
+ /**
83
+ * Adopt an existing SKILL.md into a `skill()` spec. The body is carried verbatim
84
+ * (skills are freeform markdown — `##` headings stay in the body), so a clean
85
+ * skill round-trips below the integrity header.
86
+ *
87
+ * @param markdown the SKILL.md content
88
+ * @param dirName the skill's directory name — the CC fallback for `name` when
89
+ * frontmatter omits it
90
+ */
91
+ export declare function adoptSkill(markdown: string, dirName: string): AdoptSurfaceResult;
92
+ export declare function adoptAgent(markdown: string, fileBase: string): AdoptSurfaceResult;
65
93
  //# sourceMappingURL=adopt.d.ts.map