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.
- package/agents/skills/README.md +327 -0
- package/agents/skills/canary:generate.md +49 -0
- package/agents/skills/canary:init.md +37 -0
- package/agents/skills/canary:migrate.md +66 -0
- package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
- package/agents/skills/claude-code/canary-batwoman/SKILL.md +119 -0
- package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-cassandra/SKILL.md +187 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +270 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
- package/agents/skills/claude-code/canary-ci-ready/SKILL.md +178 -0
- package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
- package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
- package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
- package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
- package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
- package/agents/skills/claude-code/canary-fleet-health/SKILL.md +197 -0
- package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
- package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
- package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
- package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
- package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
- package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
- package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
- package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
- package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
- package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
- package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +144 -0
- package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
- package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
- package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
- package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
- package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
- package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
- package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-screech/SKILL.md +109 -0
- package/agents/skills/claude-code/canary-screech/scripts/blast.mjs +125 -0
- package/agents/skills/claude-code/canary-screech/scripts/cli.mjs +128 -0
- package/agents/skills/claude-code/canary-screech/scripts/cluster.mjs +97 -0
- package/agents/skills/claude-code/canary-screech/scripts/history.mjs +73 -0
- package/agents/skills/claude-code/canary-screech/scripts/redness.mjs +94 -0
- package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
- package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
- package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
- package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
- package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
- package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
- package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
- package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
- package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
- package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
- package/agents/skills/lib/parse-args.mjs +275 -0
- package/dist/engine/analysis/batwoman/audit.js +39 -0
- package/dist/engine/analysis/batwoman/closure.js +159 -0
- package/dist/engine/analysis/batwoman/gh-history.js +119 -0
- package/dist/engine/analysis/batwoman/probes.js +195 -0
- package/dist/engine/analysis/batwoman/registry.js +142 -0
- package/dist/engine/analysis/batwoman/render.js +194 -0
- package/dist/engine/analysis/batwoman/run-window.js +122 -0
- package/dist/engine/analysis/batwoman/text.js +84 -0
- package/dist/engine/analysis/batwoman/triggers.js +122 -0
- package/dist/engine/analysis/batwoman/verdict.js +64 -0
- package/dist/engine/analysis/cli.js +47 -14
- package/dist/engine/analysis/gh-flaky/gh-run-attempts.js +206 -0
- package/dist/engine/batwoman-cli.js +119 -0
- package/dist/engine/ci-ready-cli.js +71 -0
- package/dist/engine/cli-commands.js +49 -72
- package/dist/engine/cli.core.js +16 -0
- package/dist/engine/company-knowledge-cli.js +10 -2
- package/dist/engine/core/ci-ready.js +112 -0
- package/dist/engine/core/company-knowledge.js +8 -0
- package/dist/engine/core/migrator.js +147 -20
- package/dist/engine/core/permission-matrix.js +219 -0
- package/dist/engine/core/quality-scorer.js +27 -19
- package/dist/engine/core/scaling-curve.js +143 -0
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +103 -3
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/string-literals.js +3 -1
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/vacuity-scanner.js +330 -15
- package/dist/engine/core/workflow-discovery.js +41 -23
- package/dist/engine/guardian/adjudication-github.js +136 -0
- package/dist/engine/guardian/adjudication.js +119 -340
- package/dist/engine/guardian/analysis-emit.js +7 -2
- package/dist/engine/guardian/cli.js +277 -249
- package/dist/engine/guardian/coverage.js +2 -1
- package/dist/engine/guardian/diff-coverage/coverage-delta.js +162 -0
- package/dist/engine/guardian/diff-coverage/formats/cobertura.js +45 -1
- package/dist/engine/guardian/diff-coverage/orchestrator.js +25 -21
- package/dist/engine/guardian/diff-coverage/paths.js +5 -9
- package/dist/engine/guardian/diff-coverage/report-tier.js +88 -12
- package/dist/engine/guardian/diff-extractor.js +31 -32
- package/dist/engine/guardian/pr-check.js +354 -223
- package/dist/engine/guardian/pr-comment.js +35 -58
- package/dist/engine/guardian/weak-test.js +236 -0
- package/dist/engine/mcp-server.js +67 -4
- package/dist/engine/permission-matrix-cli.js +51 -0
- package/dist/engine/scaling-curve-cli.js +147 -0
- package/dist/engine/skills-cli.js +171 -51
- package/dist/engine/workflow-cli.js +85 -65
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- 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
|
|
47
|
-
const
|
|
107
|
+
const verbose = opts.verbose === true;
|
|
108
|
+
const registry = deps.makeSkillRegistry();
|
|
109
|
+
const skills = registry.discover();
|
|
48
110
|
if (skills.length === 0) {
|
|
49
|
-
|
|
111
|
+
reportEmptyDiscovery(registry, deps);
|
|
50
112
|
return;
|
|
51
113
|
}
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
/**
|
|
267
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
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",
|