canary-test-cli 7.1.0 → 8.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.
Files changed (128) hide show
  1. package/agents/skills/README.md +327 -0
  2. package/agents/skills/canary:generate.md +49 -0
  3. package/agents/skills/canary:init.md +37 -0
  4. package/agents/skills/canary:migrate.md +66 -0
  5. package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
  6. package/agents/skills/claude-code/canary-batwoman/SKILL.md +119 -0
  7. package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
  8. package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
  9. package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
  10. package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
  11. package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
  12. package/agents/skills/claude-code/canary-cassandra/SKILL.md +187 -0
  13. package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +270 -0
  14. package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
  15. package/agents/skills/claude-code/canary-ci-ready/SKILL.md +178 -0
  16. package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
  17. package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
  18. package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
  19. package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
  20. package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
  21. package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
  22. package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
  23. package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
  24. package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
  25. package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
  26. package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
  27. package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
  28. package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
  29. package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
  30. package/agents/skills/claude-code/canary-fleet-health/SKILL.md +197 -0
  31. package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
  32. package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
  33. package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
  34. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
  35. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
  36. package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
  37. package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
  38. package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
  39. package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
  40. package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
  41. package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
  42. package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
  43. package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +144 -0
  44. package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
  45. package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
  46. package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
  47. package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
  48. package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
  49. package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
  50. package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
  51. package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
  52. package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
  53. package/agents/skills/claude-code/canary-screech/SKILL.md +109 -0
  54. package/agents/skills/claude-code/canary-screech/scripts/blast.mjs +125 -0
  55. package/agents/skills/claude-code/canary-screech/scripts/cli.mjs +128 -0
  56. package/agents/skills/claude-code/canary-screech/scripts/cluster.mjs +97 -0
  57. package/agents/skills/claude-code/canary-screech/scripts/history.mjs +73 -0
  58. package/agents/skills/claude-code/canary-screech/scripts/redness.mjs +94 -0
  59. package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
  60. package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
  61. package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
  62. package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
  63. package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
  64. package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
  65. package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
  66. package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
  67. package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
  68. package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
  69. package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
  70. package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
  71. package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
  72. package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
  73. package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
  74. package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
  75. package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
  76. package/agents/skills/lib/parse-args.mjs +275 -0
  77. package/dist/engine/analysis/batwoman/audit.js +39 -0
  78. package/dist/engine/analysis/batwoman/closure.js +159 -0
  79. package/dist/engine/analysis/batwoman/gh-history.js +119 -0
  80. package/dist/engine/analysis/batwoman/probes.js +195 -0
  81. package/dist/engine/analysis/batwoman/registry.js +142 -0
  82. package/dist/engine/analysis/batwoman/render.js +194 -0
  83. package/dist/engine/analysis/batwoman/run-window.js +122 -0
  84. package/dist/engine/analysis/batwoman/text.js +84 -0
  85. package/dist/engine/analysis/batwoman/triggers.js +122 -0
  86. package/dist/engine/analysis/batwoman/verdict.js +64 -0
  87. package/dist/engine/analysis/cli.js +47 -14
  88. package/dist/engine/analysis/gh-flaky/gh-run-attempts.js +206 -0
  89. package/dist/engine/batwoman-cli.js +119 -0
  90. package/dist/engine/ci-ready-cli.js +71 -0
  91. package/dist/engine/cli-commands.js +49 -72
  92. package/dist/engine/cli.core.js +16 -0
  93. package/dist/engine/company-knowledge-cli.js +10 -2
  94. package/dist/engine/core/ci-ready.js +112 -0
  95. package/dist/engine/core/company-knowledge.js +8 -0
  96. package/dist/engine/core/migrator.js +147 -20
  97. package/dist/engine/core/permission-matrix.js +219 -0
  98. package/dist/engine/core/quality-scorer.js +27 -19
  99. package/dist/engine/core/scaling-curve.js +143 -0
  100. package/dist/engine/core/skill-dispatch.js +115 -0
  101. package/dist/engine/core/skill-examples.js +103 -3
  102. package/dist/engine/core/skill-registry.js +59 -4
  103. package/dist/engine/core/string-literals.js +3 -1
  104. package/dist/engine/core/test-files.js +77 -0
  105. package/dist/engine/core/vacuity-scanner.js +330 -15
  106. package/dist/engine/core/workflow-discovery.js +41 -23
  107. package/dist/engine/guardian/adjudication-github.js +136 -0
  108. package/dist/engine/guardian/adjudication.js +119 -340
  109. package/dist/engine/guardian/analysis-emit.js +7 -2
  110. package/dist/engine/guardian/cli.js +277 -249
  111. package/dist/engine/guardian/coverage.js +2 -1
  112. package/dist/engine/guardian/diff-coverage/coverage-delta.js +162 -0
  113. package/dist/engine/guardian/diff-coverage/formats/cobertura.js +45 -1
  114. package/dist/engine/guardian/diff-coverage/orchestrator.js +25 -21
  115. package/dist/engine/guardian/diff-coverage/paths.js +5 -9
  116. package/dist/engine/guardian/diff-coverage/report-tier.js +88 -12
  117. package/dist/engine/guardian/diff-extractor.js +31 -32
  118. package/dist/engine/guardian/pr-check.js +354 -223
  119. package/dist/engine/guardian/pr-comment.js +35 -58
  120. package/dist/engine/guardian/weak-test.js +236 -0
  121. package/dist/engine/mcp-server.js +67 -4
  122. package/dist/engine/permission-matrix-cli.js +51 -0
  123. package/dist/engine/scaling-curve-cli.js +147 -0
  124. package/dist/engine/skills-cli.js +171 -51
  125. package/dist/engine/workflow-cli.js +85 -65
  126. package/dist/reporters/testtracker.d.ts +1 -1
  127. package/dist/reporters/testtracker.js +1 -1
  128. package/package.json +3 -2
@@ -16,9 +16,10 @@ import { Command } from 'commander';
16
16
  import pc from 'picocolors';
17
17
  import { CliExitError, jsonIndent2, normalizeUsageExit } from './cli-common.js';
18
18
  import { gateOutcome } from './core/gate-result.js';
19
- import { checkExamples, spawnRunner, } from './core/skill-examples.js';
19
+ import { checkExamples, countDeclaredIllustrative, spawnRunner, } from './core/skill-examples.js';
20
20
  import { SurfaceFindingKind, checkSurfaces, collectSurfaces, } from './core/skill-surfaces.js';
21
21
  import { isExecutableSkillAllowed, resolveCliPath, } from './core/skill-registry.js';
22
+ import { SkillDispatchError, dispatchProseSkill, } from './core/skill-dispatch.js';
22
23
  import { CROSS, EM_DASH } from './main-deps.js';
23
24
  function overlayName(skill) {
24
25
  // Clone layout: ~/.canary/overlays/<overlay>/.canary/skills/<name>/SKILL.md
@@ -42,51 +43,145 @@ function formatSkill(skill, verbose) {
42
43
  line += `\n ${pc.dim(skill.path)}`;
43
44
  return line;
44
45
  }
46
+ /**
47
+ * An empty discovery, reported as the abstention it is (#757).
48
+ *
49
+ * `No skills found.` was a claim about the world made by a check that had
50
+ * verified nothing. All discovery can attest is that four specific roots held
51
+ * nothing -- so it says which four, and whether each one even existed. The
52
+ * distinction is what separates "this project has no skills" from "the tree I
53
+ * was pointed at is not there", and an installed CLI that shipped no bundled
54
+ * skills at all printed the first while meaning the second.
55
+ */
56
+ function reportEmptyDiscovery(registry, deps) {
57
+ const roots = registry.searchRoots();
58
+ const outcome = gateOutcome({ checked: 0, findings: [] }, 'advisory', {
59
+ noun: 'skill(s)',
60
+ });
61
+ deps.out(pc.bold(pc.yellow(outcome.summaryLine)));
62
+ deps.out('No skill was discoverable. Searched:');
63
+ for (const root of roots) {
64
+ const state = root.exists ? 'present, no skill inside' : 'does not exist';
65
+ deps.out(` ${root.tier.padEnd(8)} ${root.path} ${pc.dim(`(${state})`)}`);
66
+ }
67
+ const bundled = roots.find((r) => r.tier === 'bundled');
68
+ if (bundled && !bundled.exists) {
69
+ deps.out(`${pc.yellow(CROSS)} The bundled-skill root is missing from this ` +
70
+ `install ${EM_DASH} the CLI cannot see its own skills, so no cwd will ` +
71
+ 'make them appear. Reinstall, or run from a Canary checkout.');
72
+ }
73
+ }
74
+ /**
75
+ * Print one source tier: a separating blank line only when an earlier tier
76
+ * printed, then the header, then its skills. Split out of `listCmd` to pay
77
+ * down its complexity; every helper here stays at or under the perf-ratchet
78
+ * warning threshold (10) so the paydown adds no new finding identity.
79
+ */
80
+ function printTier(deps, header, skills, verbose, printedAbove) {
81
+ if (printedAbove)
82
+ deps.out('');
83
+ deps.out(header);
84
+ for (const skill of skills)
85
+ deps.out(formatSkill(skill, verbose));
86
+ }
87
+ function byOverlayName(a, b) {
88
+ return overlayName(a).localeCompare(overlayName(b));
89
+ }
90
+ /** Overlay skills print one header per overlay, groups sorted by name. */
91
+ function printOverlayGroups(deps, overlay, verbose, printedAbove) {
92
+ let above = printedAbove;
93
+ let cur = null;
94
+ for (const skill of [...overlay].sort(byOverlayName)) {
95
+ const oname = overlayName(skill);
96
+ if (oname !== cur) {
97
+ if (above)
98
+ deps.out('');
99
+ deps.out(`${pc.bold('Overlay skills')} ${pc.dim(`(${oname} ${EM_DASH} override bundled):`)}`);
100
+ cur = oname;
101
+ above = true;
102
+ }
103
+ deps.out(formatSkill(skill, verbose));
104
+ }
105
+ }
45
106
  function listCmd(opts, deps) {
46
- const verbose = opts.verbose ?? false;
47
- const skills = deps.makeSkillRegistry().discover();
107
+ const verbose = opts.verbose === true;
108
+ const registry = deps.makeSkillRegistry();
109
+ const skills = registry.discover();
48
110
  if (skills.length === 0) {
49
- deps.out(pc.yellow('No skills found.'));
111
+ reportEmptyDiscovery(registry, deps);
50
112
  return;
51
113
  }
52
- const bundled = skills.filter((s) => s.source === 'bundled');
53
- const overlay = skills.filter((s) => s.source === 'overlay');
54
- const globalSkills = skills.filter((s) => s.source === 'global');
55
- const local = skills.filter((s) => s.source === 'local');
114
+ const bySource = (source) => skills.filter((s) => s.source === source);
115
+ const bundled = bySource('bundled');
116
+ const overlay = bySource('overlay');
117
+ const globalSkills = bySource('global');
118
+ const local = bySource('local');
119
+ let printed = false;
56
120
  if (bundled.length) {
57
- deps.out(pc.bold('Bundled skills:'));
58
- for (const skill of bundled)
59
- deps.out(formatSkill(skill, verbose));
121
+ printTier(deps, pc.bold('Bundled skills:'), bundled, verbose, printed);
122
+ printed = true;
60
123
  }
61
124
  if (overlay.length) {
62
- const sorted = [...overlay].sort((a, b) => overlayName(a).localeCompare(overlayName(b)));
63
- let idx = 0;
64
- let cur = null;
65
- for (const skill of sorted) {
66
- const oname = overlayName(skill);
67
- if (oname !== cur) {
68
- if (bundled.length || idx > 0)
69
- deps.out('');
70
- deps.out(`${pc.bold('Overlay skills')} ${pc.dim(`(${oname} ${EM_DASH} override bundled):`)}`);
71
- cur = oname;
72
- idx += 1;
73
- }
74
- deps.out(formatSkill(skill, verbose));
75
- }
125
+ printOverlayGroups(deps, overlay, verbose, printed);
126
+ printed = true;
76
127
  }
77
128
  if (globalSkills.length) {
78
- if (bundled.length || overlay.length)
79
- deps.out('');
80
- deps.out(`${pc.bold('Global skills')} ${pc.dim(`(~/.canary/skills/ ${EM_DASH} override overlay):`)}`);
81
- for (const skill of globalSkills)
82
- deps.out(formatSkill(skill, verbose));
129
+ const header = `${pc.bold('Global skills')} ${pc.dim(`(~/.canary/skills/ ${EM_DASH} override overlay):`)}`;
130
+ printTier(deps, header, globalSkills, verbose, printed);
131
+ printed = true;
83
132
  }
84
133
  if (local.length) {
85
- if (bundled.length || overlay.length || globalSkills.length)
86
- deps.out('');
87
- deps.out(`${pc.bold('Local overlay skills')} ${pc.dim('(override global):')}`);
88
- for (const skill of local)
89
- deps.out(formatSkill(skill, verbose));
134
+ const header = `${pc.bold('Local overlay skills')} ${pc.dim('(override global):')}`;
135
+ printTier(deps, header, local, verbose, printed);
136
+ }
137
+ }
138
+ /**
139
+ * Tier 2: render a dispatched prose skill (#756).
140
+ *
141
+ * The determinism label leads, in both modes. A consumer merging this with a
142
+ * `cli:` detector's findings has to be able to see that an agent produced it;
143
+ * a payload that only carried the workflow text would read exactly like a
144
+ * deterministic run's output.
145
+ */
146
+ function renderDispatch(dispatch, json, deps) {
147
+ if (json) {
148
+ deps.out(jsonIndent2(dispatch));
149
+ return;
150
+ }
151
+ deps.out(`${pc.bold(dispatch.skill)} ${EM_DASH} dispatched as a prose skill ` +
152
+ `${pc.dim('(no cli:/entry: target)')}`);
153
+ deps.out(`${pc.yellow('determinism:')} ${pc.bold('agent-applied')} ${EM_DASH} canary ` +
154
+ 'applied no judgment here. The workflow below must be executed by an ' +
155
+ 'agent runtime, and its results must not be merged with a ' +
156
+ "deterministic detector's findings without carrying this label.");
157
+ if (dispatch.requires.length) {
158
+ deps.out(`${pc.dim('requires:')} ${dispatch.requires.join(', ')}`);
159
+ }
160
+ if (dispatch.args.length) {
161
+ deps.out(`${pc.dim('args (forwarded, uninterpreted):')} ${dispatch.args.join(' ')}`);
162
+ }
163
+ deps.out(`${pc.dim(`source: ${dispatch.path}`)}\n`);
164
+ deps.out(dispatch.instructions);
165
+ }
166
+ /**
167
+ * Dispatch a skill with no `cli:`/`entry:` target instead of refusing it.
168
+ *
169
+ * Deliberately reached BEFORE the `--allow-executable-skills` gate: that flag
170
+ * guards spawning someone else's code, and dispatch spawns nothing. Gating it
171
+ * would leave the 14 CLI-less skills unreachable in precisely the CI contexts
172
+ * #756 is about. See `core/skill-dispatch.ts` for the full reasoning.
173
+ */
174
+ function runProseSkill(skill, args, opts, deps) {
175
+ try {
176
+ renderDispatch(dispatchProseSkill(skill, args), opts.json ?? false, deps);
177
+ }
178
+ catch (exc) {
179
+ if (!(exc instanceof SkillDispatchError))
180
+ throw exc;
181
+ // A dispatch that could not happen is reported as a failure, never as an
182
+ // empty run: exit 2 keeps the old "this skill is not runnable" contract.
183
+ deps.out(`${pc.red(CROSS)} Skill ${pc.bold(skill.name)}: ${exc.message}`);
184
+ throw new CliExitError(2);
90
185
  }
91
186
  }
92
187
  async function runCmd(name, args, opts, deps) {
@@ -99,9 +194,10 @@ async function runCmd(name, args, opts, deps) {
99
194
  deps.out(`${pc.red(CROSS)} Skill ${pc.bold(name)}: ${skill.error}`);
100
195
  throw new CliExitError(2);
101
196
  }
197
+ // Tier 2 (#756): a skill with no cli:/entry: is dispatched, not refused.
102
198
  if (!skill.isExecutable) {
103
- deps.out(pc.yellow(`Skill ${pc.bold(name)} is markdown-only ${EM_DASH} no cli: or entry: field to run.`));
104
- throw new CliExitError(2);
199
+ runProseSkill(skill, args, opts, deps);
200
+ return;
105
201
  }
106
202
  if (!isExecutableSkillAllowed(opts.allowExecutableSkills ?? false)) {
107
203
  deps.out(`${pc.red(CROSS)} Refusing to invoke executable skill in non-interactive context. Pass ${pc.bold('--allow-executable-skills')} to opt in (e.g. in trusted CI configurations).`);
@@ -215,6 +311,11 @@ function verifyJson(root, surfaceResult, exampleResult) {
215
311
  abstained: !(exampleResult.checked > 0),
216
312
  findings: exampleResult.findings,
217
313
  skipped: exampleResult.skipped ?? [],
314
+ // The skipped bucket, split (#707). Emitted rather than left for the
315
+ // consumer to derive: a CI step counting declared-illustrative examples
316
+ // by matching the reason string would be a second copy of a literal
317
+ // owned here, and it would drift to 0 silently.
318
+ illustrative: countDeclaredIllustrative(exampleResult.skipped ?? []),
218
319
  },
219
320
  });
220
321
  }
@@ -239,6 +340,15 @@ function verifyReport(root, surfaceResult, exampleResult, deps) {
239
340
  noun: 'documented example(s)',
240
341
  });
241
342
  deps.out(`examples: ${exampleOutcome.summaryLine}`);
343
+ // #707: the denominator, split. "29 unverifiable" reads as one fact and is
344
+ // two — blocks nobody could run, and blocks the author declared as prose.
345
+ // Only the first is a gap, and printing the total alone buries it.
346
+ const skipped = exampleResult.skipped ?? [];
347
+ const declared = countDeclaredIllustrative(skipped);
348
+ deps.out(` denominator: executed=${exampleResult.checked} ` +
349
+ `illustrative=${declared} ` +
350
+ `unverifiable=${skipped.length - declared} ` +
351
+ `total=${exampleResult.checked + skipped.length}`);
242
352
  if (exampleOutcome.abstained) {
243
353
  deps.out(` zero documented commands were executed ${EM_DASH} nothing here is ` +
244
354
  'proven. Add a help-shaped example in a shell fence to a SKILL.md, or ' +
@@ -263,28 +373,22 @@ function partition(s, sep) {
263
373
  return [s, '', ''];
264
374
  return [s.slice(0, i), sep, s.slice(i + sep.length)];
265
375
  }
266
- /** Build the `skills` sub-app wired to `deps`. */
267
- export function buildSkillsCommand(deps) {
268
- const program = new Command('skills');
269
- program
270
- .description('List and invoke discoverable Canary skills.')
271
- .exitOverride(normalizeUsageExit);
272
- program
273
- .command('list')
274
- .description('List every skill discoverable from the current directory.')
275
- .option('-v, --verbose', 'Also print the SKILL.md path for each skill.')
276
- .action((opts) => {
277
- listCmd(opts, deps);
278
- });
376
+ /** Register `skills run` -- the tier-1 target path plus the tier-2 dispatcher. */
377
+ function registerRun(program, deps) {
279
378
  program
280
379
  .command('run')
281
- .description("Invoke a code-bearing skill's declared cli or entry target.")
380
+ .description("Invoke a skill: a code-bearing skill's cli/entry target, or a prose " +
381
+ "skill's workflow via the dispatcher.")
282
382
  .argument('<name>', 'Name of the skill to invoke.')
283
383
  .argument('[args...]', "Arguments forwarded to the skill's cli/entry.")
284
384
  .option('--allow-executable-skills', 'Opt-in to invoking cli:/entry: skills in non-interactive (CI) contexts.')
385
+ .option('--json', 'For a dispatched prose skill, emit the labelled payload as JSON.')
285
386
  .action(async (name, args, opts) => {
286
387
  await runCmd(name, args ?? [], opts, deps);
287
388
  });
389
+ }
390
+ /** Register `skills verify` -- the two-denominator cross-surface check. */
391
+ function registerVerify(program, deps) {
288
392
  program
289
393
  .command('verify')
290
394
  .description('Check that skill surfaces agree and that documented examples still run.')
@@ -295,6 +399,22 @@ export function buildSkillsCommand(deps) {
295
399
  .action((opts) => {
296
400
  verifyCmd(opts, deps);
297
401
  });
402
+ }
403
+ /** Build the `skills` sub-app wired to `deps`. */
404
+ export function buildSkillsCommand(deps) {
405
+ const program = new Command('skills');
406
+ program
407
+ .description('List and invoke discoverable Canary skills.')
408
+ .exitOverride(normalizeUsageExit);
409
+ program
410
+ .command('list')
411
+ .description('List every skill discoverable from the current directory.')
412
+ .option('-v, --verbose', 'Also print the SKILL.md path for each skill.')
413
+ .action((opts) => {
414
+ listCmd(opts, deps);
415
+ });
416
+ registerRun(program, deps);
417
+ registerVerify(program, deps);
298
418
  for (const sub of program.commands) {
299
419
  sub.exitOverride(normalizeUsageExit);
300
420
  }
@@ -79,29 +79,94 @@ async function discoverCmd(opts, deps) {
79
79
  throw new CliExitError(1);
80
80
  }
81
81
  }
82
- function showCmd(opts, deps) {
83
- const wd = deps.makeWorkflowDiscovery();
82
+ /**
83
+ * Which project keys `show` should report on.
84
+ *
85
+ * `--project` names one; otherwise every cached `.canary/workflow-*.json` is
86
+ * listed. An unreadable `.canary` yields no keys rather than throwing: a
87
+ * missing cache is the ordinary first-run state, not an error.
88
+ */
89
+ function resolveProjectKeys(opts, deps) {
90
+ if (opts.project)
91
+ return [opts.project];
84
92
  let keys = [];
85
- if (opts.project) {
86
- keys = [opts.project];
93
+ const canaryDir = join(deps.cwd(), '.canary');
94
+ if (existsSync(canaryDir)) {
95
+ try {
96
+ keys = readdirSync(canaryDir)
97
+ .filter((f) => f.startsWith('workflow-') && f.endsWith('.json'))
98
+ .map((f) => f.slice('workflow-'.length, -'.json'.length));
99
+ }
100
+ catch {
101
+ keys = [];
102
+ }
87
103
  }
88
- else {
89
- const canaryDir = join(deps.cwd(), '.canary');
90
- if (existsSync(canaryDir)) {
91
- try {
92
- keys = readdirSync(canaryDir)
93
- .filter((f) => f.startsWith('workflow-') && f.endsWith('.json'))
94
- .map((f) => f.slice('workflow-'.length, -'.json'.length));
95
- }
96
- catch {
97
- keys = [];
98
- }
104
+ if (keys.length === 0) {
105
+ deps.out(pc.yellow('No cached workflow mappings found.'));
106
+ throw new CliExitError(0);
107
+ }
108
+ return keys;
109
+ }
110
+ /** One mapping as JSON: the whole document, or just its semantic roles. */
111
+ function mappingAsJson(mapping, rolesOnly) {
112
+ if (!rolesOnly)
113
+ return mapping.toJson();
114
+ const rolesDict = {};
115
+ for (const [r, sr] of Object.entries(mapping.semantic_roles)) {
116
+ rolesDict[r] = { status_name: sr.status_name, issue_type: sr.issue_type };
117
+ }
118
+ return jsonIndent2(rolesDict);
119
+ }
120
+ /** `role -> status` lines, indented under a heading. Shared by both modes. */
121
+ function printSemanticRoles(roles, lead, deps) {
122
+ deps.out(`${lead}${pc.bold('Semantic roles:')}`);
123
+ for (const [role, sr] of Object.entries(roles)) {
124
+ deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
125
+ }
126
+ }
127
+ /** Each issue type, its statuses, and its transitions. */
128
+ function printIssueTypes(issueTypes, deps) {
129
+ for (const it of issueTypes) {
130
+ deps.out(`\n ${pc.bold(it.name)}`);
131
+ for (const s of it.statuses) {
132
+ deps.out(` [${s.category}] ${s.name}`);
99
133
  }
100
- if (keys.length === 0) {
101
- deps.out(pc.yellow('No cached workflow mappings found.'));
102
- throw new CliExitError(0);
134
+ if (it.transitions.length === 0)
135
+ continue;
136
+ deps.out(' Transitions:');
137
+ for (const t of it.transitions) {
138
+ deps.out(` ${t.from_status} ${'\u{2192}'} ${t.to_status} ${pc.dim(`(${t.name})`)}`);
103
139
  }
104
140
  }
141
+ }
142
+ /**
143
+ * One cached mapping, rendered for a human.
144
+ *
145
+ * Extracted from `showCmd`, which had grown to hold key resolution, JSON
146
+ * emission and this rendering at once -- and carried the semantic-roles block
147
+ * TWICE, once per mode, which is why the two had already drifted in their
148
+ * indentation.
149
+ */
150
+ function printMapping(key, mapping, rolesOnly, deps) {
151
+ const confirmedTag = mapping.role_annotations_confirmed
152
+ ? pc.green('confirmed')
153
+ : pc.yellow('unconfirmed');
154
+ deps.out(`\n${pc.bold(key)} ${pc.dim(`source=${mapping.source} discovered=${mapping.discovered_at} roles=${confirmedTag}`)}`);
155
+ const hasRoles = Object.keys(mapping.semantic_roles).length > 0;
156
+ if (rolesOnly) {
157
+ if (hasRoles)
158
+ printSemanticRoles(mapping.semantic_roles, ' ', deps);
159
+ else
160
+ deps.out(` ${pc.yellow('No semantic roles resolved yet.')}`);
161
+ return;
162
+ }
163
+ printIssueTypes(mapping.issue_types, deps);
164
+ if (hasRoles)
165
+ printSemanticRoles(mapping.semantic_roles, '\n ', deps);
166
+ }
167
+ function showCmd(opts, deps) {
168
+ const wd = deps.makeWorkflowDiscovery();
169
+ const keys = resolveProjectKeys(opts, deps);
105
170
  let anyFound = false;
106
171
  for (const key of keys) {
107
172
  const mapping = wd.show(key);
@@ -111,55 +176,10 @@ function showCmd(opts, deps) {
111
176
  }
112
177
  anyFound = true;
113
178
  if (opts.json) {
114
- if (opts.rolesOnly) {
115
- const rolesDict = {};
116
- for (const [r, sr] of Object.entries(mapping.semantic_roles)) {
117
- rolesDict[r] = {
118
- status_name: sr.status_name,
119
- issue_type: sr.issue_type,
120
- };
121
- }
122
- deps.out(jsonIndent2(rolesDict));
123
- }
124
- else {
125
- deps.out(mapping.toJson());
126
- }
127
- continue;
128
- }
129
- const confirmedTag = mapping.role_annotations_confirmed
130
- ? pc.green('confirmed')
131
- : pc.yellow('unconfirmed');
132
- deps.out(`\n${pc.bold(key)} ${pc.dim(`source=${mapping.source} discovered=${mapping.discovered_at} roles=${confirmedTag}`)}`);
133
- if (opts.rolesOnly) {
134
- if (Object.keys(mapping.semantic_roles).length) {
135
- deps.out(` ${pc.bold('Semantic roles:')}`);
136
- for (const [role, sr] of Object.entries(mapping.semantic_roles)) {
137
- deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
138
- }
139
- }
140
- else {
141
- deps.out(` ${pc.yellow('No semantic roles resolved yet.')}`);
142
- }
179
+ deps.out(mappingAsJson(mapping, opts.rolesOnly === true));
143
180
  continue;
144
181
  }
145
- for (const it of mapping.issue_types) {
146
- deps.out(`\n ${pc.bold(it.name)}`);
147
- for (const s of it.statuses) {
148
- deps.out(` [${s.category}] ${s.name}`);
149
- }
150
- if (it.transitions.length) {
151
- deps.out(' Transitions:');
152
- for (const t of it.transitions) {
153
- deps.out(` ${t.from_status} ${'\u{2192}'} ${t.to_status} ${pc.dim(`(${t.name})`)}`);
154
- }
155
- }
156
- }
157
- if (Object.keys(mapping.semantic_roles).length) {
158
- deps.out(`\n ${pc.bold('Semantic roles:')}`);
159
- for (const [role, sr] of Object.entries(mapping.semantic_roles)) {
160
- deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
161
- }
162
- }
182
+ printMapping(key, mapping, opts.rolesOnly === true, deps);
163
183
  }
164
184
  if (!anyFound) {
165
185
  throw new CliExitError(1);
@@ -82,7 +82,7 @@ export declare function runStatus(totals: {
82
82
  * `test.id`, which is genuinely unique, but a title is not: a Playwright
83
83
  * `dependencies:` setup project runs in full in EVERY shard (dependencies are
84
84
  * not sharded), so a `merge-reports` payload over a sharded matrix legitimately
85
- * carries the same setup title once per shard. Capwell's `capwell-web` suite
85
+ * carries the same setup title once per shard. One consumer’s sharded suite
86
86
  * had every nightly rejected this way and never ingested a single run.
87
87
  *
88
88
  * Collapse rule keeps the merged row honest: worst status wins, the first real
@@ -109,7 +109,7 @@ const STATUS_SEVERITY = {
109
109
  * `test.id`, which is genuinely unique, but a title is not: a Playwright
110
110
  * `dependencies:` setup project runs in full in EVERY shard (dependencies are
111
111
  * not sharded), so a `merge-reports` payload over a sharded matrix legitimately
112
- * carries the same setup title once per shard. Capwell's `capwell-web` suite
112
+ * carries the same setup title once per shard. One consumer’s sharded suite
113
113
  * had every nightly rejected this way and never ingested a single run.
114
114
  *
115
115
  * Collapse rule keeps the merged row honest: worst status wins, the first real
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-test-cli",
3
- "version": "7.1.0",
3
+ "version": "8.0.0",
4
4
  "description": "Canary — AI-powered test automation agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,7 +39,8 @@
39
39
  "files": [
40
40
  "bin/canary.js",
41
41
  "bin/canary-mcp.js",
42
- "dist/"
42
+ "dist/",
43
+ "agents/"
43
44
  ],
44
45
  "dependencies": {
45
46
  "@modelcontextprotocol/sdk": "^1.30.0",