canary-test-cli 7.0.0 → 7.2.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 +308 -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-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 +180 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
- package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -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 +196 -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 +129 -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-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 +214 -0
- package/dist/engine/analysis/cli.js +116 -54
- package/dist/engine/analysis/engine.js +34 -16
- package/dist/engine/analysis/reports.js +5 -4
- package/dist/engine/cli-commands.js +252 -106
- package/dist/engine/cli-common.js +15 -24
- package/dist/engine/cli.core.js +37 -11
- package/dist/engine/cli.js +2 -2
- package/dist/engine/company-knowledge-cli.js +2 -2
- package/dist/engine/core/adoption.js +408 -0
- package/dist/engine/core/framework-probes.js +7 -7
- package/dist/engine/core/fs-glob.js +2 -2
- package/dist/engine/core/gate-result.js +17 -0
- package/dist/engine/core/migrator.js +9 -17
- package/dist/engine/core/pattern-matcher.js +23 -5
- package/dist/engine/core/persona.js +421 -0
- package/dist/engine/core/promotion-verdict.js +261 -0
- package/dist/engine/core/quality-scorer.js +15 -2
- package/dist/engine/core/reporter.js +1 -9
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +392 -0
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/skill-surfaces.js +307 -0
- package/dist/engine/core/static-linter.js +310 -38
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/ticket-updater.js +1 -7
- package/dist/engine/core/vacuity-scanner.js +726 -0
- package/dist/engine/core/workflow-discovery.js +2 -8
- package/dist/engine/core/workspace-detect.js +7 -6
- package/dist/engine/data/personas/registry.json +36 -0
- package/dist/engine/guardian/adjudication.js +5 -5
- package/dist/engine/guardian/analysis-emit.js +19 -28
- package/dist/engine/guardian/cli.js +150 -51
- package/dist/engine/guardian/coverage.js +1 -1
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
- package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
- package/dist/engine/guardian/pr-check.js +322 -33
- package/dist/engine/guardian/pr-comment.js +4 -3
- package/dist/engine/history/cli.js +210 -6
- package/dist/engine/history/ndjson-store.js +9 -5
- package/dist/engine/history/record.js +34 -5
- package/dist/engine/history/run-recorder.js +165 -0
- package/dist/engine/history/schema.js +25 -7
- package/dist/engine/history/store.js +9 -0
- package/dist/engine/mcp-server.js +35 -13
- package/dist/engine/skills-cli.js +249 -23
- package/dist/engine/util/ensure-ascii.js +37 -0
- package/dist/engine/workflow-cli.js +6 -6
- package/dist/gate-result.d.ts +11 -0
- package/dist/gate-result.js +18 -0
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- package/dist/uninstall.js +12 -5
- package/package.json +3 -2
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute the commands the docs promise (#487).
|
|
3
|
+
*
|
|
4
|
+
* #472 added `canary skills run canary-blackhawk -- --help` to a SKILL.md in
|
|
5
|
+
* the same PR that left the command broken: the CLI landed at mode 644, the
|
|
6
|
+
* spawn hit `EACCES`, and that became a bare exit 1 with no output. Nobody had
|
|
7
|
+
* ever run the command the doc documented. #480 answered part of it with a
|
|
8
|
+
* hand-written `spawnSync(cli, ['--help'])` per skill — real execution, which
|
|
9
|
+
* is why the exec-bit bug surfaced at all — but the block is duplicated across
|
|
10
|
+
* five test files, so a seventh skill is covered only when somebody remembers
|
|
11
|
+
* to add a sixth copy (#479).
|
|
12
|
+
*
|
|
13
|
+
* This module is the discovery-driven form. It consumes the surface inventory
|
|
14
|
+
* from {@link ./skill-surfaces.js} — deliberately, rather than walking the skill
|
|
15
|
+
* tree a second time: two walkers with two notions of what a skill is would
|
|
16
|
+
* disagree eventually, and that disagreement is the very bug class both checks
|
|
17
|
+
* exist to catch.
|
|
18
|
+
*
|
|
19
|
+
* ## Which examples are executable
|
|
20
|
+
*
|
|
21
|
+
* #487 left this as an open scope decision. The answer taken here is
|
|
22
|
+
* conservative and mechanical, because the alternative — a fenced-block
|
|
23
|
+
* annotation — asks every SKILL.md author to opt in, and an opt-in that is
|
|
24
|
+
* forgotten reads exactly like a skill with no examples:
|
|
25
|
+
*
|
|
26
|
+
* - The command must live in a **shell-info fenced block** (` ```bash `,
|
|
27
|
+
* `sh`, `shell`, `zsh`, `console`). Prose backticks are illustrative.
|
|
28
|
+
* - It must be a **`canary` invocation**. Running arbitrary `npm`/`git` lines
|
|
29
|
+
* out of a doc is a different and much larger blast radius.
|
|
30
|
+
* - It must carry **no placeholder or shell metacharacter** (`<path>`, `$VAR`,
|
|
31
|
+
* a pipe, a glob). A placeholder command was never meant to run verbatim.
|
|
32
|
+
* - It must be **help-shaped** (`--help` / `-h` / `--version`, or a pure
|
|
33
|
+
* listing command). This is what keeps a documented `katana scan` from
|
|
34
|
+
* writing a ledger into whatever directory CI happens to be sitting in.
|
|
35
|
+
*
|
|
36
|
+
* Everything else is {@link ExampleVerdict.Unverifiable}, **with its reason
|
|
37
|
+
* recorded**. That is the load-bearing half of the design, and it is the same
|
|
38
|
+
* distinction `reachability.ts` draws between a dead link and a slow one: an
|
|
39
|
+
* outcome the checker is not entitled to assert on gets its own status instead
|
|
40
|
+
* of being folded into either pass or fail.
|
|
41
|
+
*
|
|
42
|
+
* ## Denominator
|
|
43
|
+
*
|
|
44
|
+
* `checked` counts the examples **actually executed** — never the examples
|
|
45
|
+
* found. Unverifiable examples travel in `GateResult.skipped`, so `gateOutcome`
|
|
46
|
+
* renders them in every summary line (D7) and an all-illustrative corpus
|
|
47
|
+
* ABSTAINS rather than reporting "all 0 examples passed" (#508).
|
|
48
|
+
*/
|
|
49
|
+
import { spawnSync } from 'node:child_process';
|
|
50
|
+
import { SurfaceKind } from './skill-surfaces.js';
|
|
51
|
+
/** Fence info strings whose contents are shell commands. */
|
|
52
|
+
const SHELL_FENCES = new Set(['bash', 'sh', 'shell', 'zsh', 'console']);
|
|
53
|
+
/**
|
|
54
|
+
* Characters that make a command line unsafe to run verbatim: placeholder
|
|
55
|
+
* brackets, variable expansion, redirection, pipes, subshells, globs.
|
|
56
|
+
*/
|
|
57
|
+
const PLACEHOLDER = /[<>${}|`*\\]/;
|
|
58
|
+
/** Flags that make an invocation a pure read of the CLI's own surface. */
|
|
59
|
+
const HELP_FLAGS = new Set(['--help', '-h', '--version', '-V']);
|
|
60
|
+
/** Non-mutating subcommands worth executing even without a help flag. */
|
|
61
|
+
const READ_ONLY_COMMANDS = new Set(['canary skills list']);
|
|
62
|
+
/**
|
|
63
|
+
* The author's declaration that a block is illustrative (#707).
|
|
64
|
+
*
|
|
65
|
+
* Placed on its own line immediately above the fence it governs:
|
|
66
|
+
*
|
|
67
|
+
* <!-- canary:illustrative -->
|
|
68
|
+
* ```bash
|
|
69
|
+
* canary katana scan --since HEAD~1
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* Two facts land in the same "unverifiable" bucket and they are not the same
|
|
73
|
+
* fact: "nobody could run this" and "this was never meant to be run". The
|
|
74
|
+
* first is a gap in the corpus; the second is a deliberate authoring choice.
|
|
75
|
+
* Collapsing them is what let 88% of the corpus read as coverage debt when
|
|
76
|
+
* some of it was prose doing its job — and, worse, hid the real gaps inside
|
|
77
|
+
* the pile.
|
|
78
|
+
*
|
|
79
|
+
* Marking is NOT an escape hatch from the executable-example rule. It changes
|
|
80
|
+
* the reason on one block; a code-bearing skill still has to carry at least
|
|
81
|
+
* one example that actually runs (`no-executable-example`), so a skill cannot
|
|
82
|
+
* mark its way to green.
|
|
83
|
+
*/
|
|
84
|
+
const ILLUSTRATIVE_MARKER = /^\s*<!--\s*canary:illustrative\s*-->\s*$/;
|
|
85
|
+
/**
|
|
86
|
+
* The reason carried by a declared-illustrative example.
|
|
87
|
+
*
|
|
88
|
+
* Exported because the summary line splits the unverifiable bucket on it
|
|
89
|
+
* (see {@link countDeclaredIllustrative}). A string literal compared in two
|
|
90
|
+
* files is a drift waiting to happen, and the drift would be silent: the
|
|
91
|
+
* split would quietly read 0 declared and the distinction this issue exists
|
|
92
|
+
* to draw would be gone with nothing red.
|
|
93
|
+
*/
|
|
94
|
+
export const ILLUSTRATIVE_REASON = 'declared illustrative by the author, so it is not run';
|
|
95
|
+
/**
|
|
96
|
+
* How many of a gate's skipped examples were skipped BY DECLARATION.
|
|
97
|
+
*
|
|
98
|
+
* The rest are the honest gap: examples nobody could run and nobody said
|
|
99
|
+
* were prose.
|
|
100
|
+
*/
|
|
101
|
+
export function countDeclaredIllustrative(skipped) {
|
|
102
|
+
return skipped.filter((s) => s.reason === ILLUSTRATIVE_REASON).length;
|
|
103
|
+
}
|
|
104
|
+
/** How an example turned out. */
|
|
105
|
+
export var ExampleVerdict;
|
|
106
|
+
(function (ExampleVerdict) {
|
|
107
|
+
/** Ran and exited 0 — the doc is proven. */
|
|
108
|
+
ExampleVerdict["Executed"] = "executed";
|
|
109
|
+
/** Ran and did not exit 0 — the doc promises something broken. */
|
|
110
|
+
ExampleVerdict["Failed"] = "failed";
|
|
111
|
+
/** Could not be run at all; the reason travels with it. */
|
|
112
|
+
ExampleVerdict["Unverifiable"] = "unverifiable";
|
|
113
|
+
})(ExampleVerdict || (ExampleVerdict = {}));
|
|
114
|
+
export var ExampleFindingKind;
|
|
115
|
+
(function (ExampleFindingKind) {
|
|
116
|
+
/** A documented command was executed and failed. */
|
|
117
|
+
ExampleFindingKind["ExampleFailed"] = "example-failed";
|
|
118
|
+
/** A code-bearing skill's doc offers no command to execute at all. */
|
|
119
|
+
ExampleFindingKind["NoDocumentedExample"] = "no-documented-example";
|
|
120
|
+
/**
|
|
121
|
+
* A code-bearing skill documents commands, but not one of them can be run
|
|
122
|
+
* (#707). Distinct from {@link NoDocumentedExample}, and it was the larger
|
|
123
|
+
* hole: 5 of 9 `cli:` skills sat here while the corpus looked documented.
|
|
124
|
+
* A skill in this state can break in every documented way and CI stays
|
|
125
|
+
* green, which is the false-green shape the whole check exists to close.
|
|
126
|
+
*/
|
|
127
|
+
ExampleFindingKind["NoExecutableExample"] = "no-executable-example";
|
|
128
|
+
})(ExampleFindingKind || (ExampleFindingKind = {}));
|
|
129
|
+
/**
|
|
130
|
+
* Whether `line` closes the currently open fence.
|
|
131
|
+
*
|
|
132
|
+
* A fence closes only on a BARE delimiter run at least as long as the opener,
|
|
133
|
+
* so a ```` block may legitimately contain ``` -- the same fence rule
|
|
134
|
+
* `scripts/check_doc_links.mjs` had to get right for #686.
|
|
135
|
+
*/
|
|
136
|
+
function closesFence(line, delimiter, fence) {
|
|
137
|
+
if (delimiter === null)
|
|
138
|
+
return false;
|
|
139
|
+
const run = delimiter[1];
|
|
140
|
+
return (run[0] === fence[0] && run.length >= fence.length && line.trim() === run);
|
|
141
|
+
}
|
|
142
|
+
/** Shell-fenced lines with their 1-based source line numbers. */
|
|
143
|
+
function fencedShellLines(text) {
|
|
144
|
+
const out = [];
|
|
145
|
+
const lines = text.split('\n');
|
|
146
|
+
let fence = null;
|
|
147
|
+
let shell = false;
|
|
148
|
+
let illustrative = false;
|
|
149
|
+
// The marker governs the NEXT fence, so it survives the blank line authors
|
|
150
|
+
// naturally leave between a comment and a block, and is spent by the fence
|
|
151
|
+
// it opens — a marker cannot leak onto a later, unrelated example.
|
|
152
|
+
let pendingMarker = false;
|
|
153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
const delimiter = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/.exec(line);
|
|
156
|
+
if (fence === null) {
|
|
157
|
+
// A fence opens on a delimiter run; the info string decides whether the
|
|
158
|
+
// body is shell. Anything else (json, ts, text) is not a command.
|
|
159
|
+
if (delimiter) {
|
|
160
|
+
fence = delimiter[1];
|
|
161
|
+
shell = SHELL_FENCES.has((delimiter[2] ?? '').toLowerCase());
|
|
162
|
+
illustrative = pendingMarker;
|
|
163
|
+
pendingMarker = false;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ILLUSTRATIVE_MARKER.test(line))
|
|
167
|
+
pendingMarker = true;
|
|
168
|
+
else if (line.trim() !== '')
|
|
169
|
+
pendingMarker = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (closesFence(line, delimiter, fence)) {
|
|
173
|
+
fence = null;
|
|
174
|
+
shell = false;
|
|
175
|
+
illustrative = false;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (shell)
|
|
179
|
+
out.push({ line: i + 1, raw: line, illustrative });
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/** Classify one command line: executable, or unverifiable with a reason. */
|
|
184
|
+
function classify(command) {
|
|
185
|
+
if (PLACEHOLDER.test(command)) {
|
|
186
|
+
return {
|
|
187
|
+
executable: false,
|
|
188
|
+
reason: 'contains a placeholder or shell metacharacter, so it was never ' +
|
|
189
|
+
'meant to run verbatim',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (READ_ONLY_COMMANDS.has(command))
|
|
193
|
+
return { executable: true, reason: null };
|
|
194
|
+
const tokens = command.split(/\s+/);
|
|
195
|
+
if (tokens.some((t) => HELP_FLAGS.has(t))) {
|
|
196
|
+
return { executable: true, reason: null };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
executable: false,
|
|
200
|
+
reason: 'not help-shaped, so running it could write files, need credentials, ' +
|
|
201
|
+
'or reach the network',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract the documented `canary` commands from one document.
|
|
206
|
+
*
|
|
207
|
+
* `text` is the raw file body (the inventory already read it), `skill` and
|
|
208
|
+
* `path` are carried through onto each example so a finding is attributable.
|
|
209
|
+
*/
|
|
210
|
+
export function extractExamples(text, skill, path) {
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const { line, raw, illustrative } of fencedShellLines(text)) {
|
|
213
|
+
// Strip a `$ ` or `> ` shell prompt; a doc that shows a prompt is still
|
|
214
|
+
// documenting the command after it.
|
|
215
|
+
const command = raw
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/^[$>]\s+/, '')
|
|
218
|
+
.trim();
|
|
219
|
+
if (command === '' || command.startsWith('#'))
|
|
220
|
+
continue;
|
|
221
|
+
if (command !== 'canary' && !command.startsWith('canary '))
|
|
222
|
+
continue;
|
|
223
|
+
// A declaration beats an inference. The author saying "this is prose"
|
|
224
|
+
// is a better fact than the classifier guessing why it could not run,
|
|
225
|
+
// and it is the fact a reader of the skipped list needs.
|
|
226
|
+
if (illustrative) {
|
|
227
|
+
out.push({
|
|
228
|
+
skill,
|
|
229
|
+
path,
|
|
230
|
+
command,
|
|
231
|
+
line,
|
|
232
|
+
executable: false,
|
|
233
|
+
declaredIllustrative: true,
|
|
234
|
+
reason: ILLUSTRATIVE_REASON,
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const { executable, reason } = classify(command);
|
|
239
|
+
out.push({
|
|
240
|
+
skill,
|
|
241
|
+
path,
|
|
242
|
+
command,
|
|
243
|
+
line,
|
|
244
|
+
executable,
|
|
245
|
+
declaredIllustrative: false,
|
|
246
|
+
reason,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
/** Execute the executable examples; report a verdict for every example. */
|
|
252
|
+
export function runExamples(examples, run, cwd) {
|
|
253
|
+
return examples.map((example) => {
|
|
254
|
+
if (!example.executable) {
|
|
255
|
+
return {
|
|
256
|
+
example,
|
|
257
|
+
verdict: ExampleVerdict.Unverifiable,
|
|
258
|
+
detail: example.reason ?? 'unverifiable',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const { status, output } = run(example.command, cwd);
|
|
262
|
+
if (status === 0) {
|
|
263
|
+
return { example, verdict: ExampleVerdict.Executed, detail: 'exit 0' };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
example,
|
|
267
|
+
verdict: ExampleVerdict.Failed,
|
|
268
|
+
detail: status === null
|
|
269
|
+
? `the process never started: ${output.trim() || '(no output)'}`
|
|
270
|
+
: `exit ${status}: ${output.trim() || '(no output)'}`,
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** A skill declaration that ships code, and can therefore ship broken code. */
|
|
275
|
+
function codeBearing(decl) {
|
|
276
|
+
return ((decl.kind === SurfaceKind.Skill || decl.kind === SurfaceKind.FlatSkill) &&
|
|
277
|
+
decl.cli !== null);
|
|
278
|
+
}
|
|
279
|
+
/** Fold one skill declaration's results into `tally`. */
|
|
280
|
+
function tallyDeclaration(decl, results, tally) {
|
|
281
|
+
for (const result of results) {
|
|
282
|
+
// `<skill>:<line>` rather than the absolute path: a skill's SKILL.md is
|
|
283
|
+
// unambiguous from its name, and 29 absolute paths turned the D7 skip
|
|
284
|
+
// suffix into a summary line no reader would finish. Still fully
|
|
285
|
+
// attributable; `--json` carries the paths.
|
|
286
|
+
const where = `${decl.name}:${result.example.line}`;
|
|
287
|
+
if (result.verdict === ExampleVerdict.Unverifiable) {
|
|
288
|
+
// Never silently dropped: a skip renders in the summary line, so an
|
|
289
|
+
// example nobody can run stays visible instead of leaving the corpus.
|
|
290
|
+
tally.skipped.push({ name: where, reason: result.detail });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
tally.checked += 1;
|
|
294
|
+
if (result.verdict === ExampleVerdict.Failed) {
|
|
295
|
+
tally.findings.push({
|
|
296
|
+
kind: ExampleFindingKind.ExampleFailed,
|
|
297
|
+
skill: decl.name,
|
|
298
|
+
path: decl.path,
|
|
299
|
+
detail: `\`${result.example.command}\` (line ${result.example.line}) ${result.detail}`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
export function checkExamples(surfaces, run, cwd) {
|
|
305
|
+
const tally = { checked: 0, findings: [], skipped: [] };
|
|
306
|
+
for (const decl of surfaces) {
|
|
307
|
+
if (decl.kind !== SurfaceKind.Skill &&
|
|
308
|
+
decl.kind !== SurfaceKind.FlatSkill) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const examples = extractExamples(decl.text, decl.name, decl.path);
|
|
312
|
+
// #487 acceptance: a code-bearing skill with no runnable command in its
|
|
313
|
+
// doc is UNPROVEN, not clean. A markdown-only skill has no command that a
|
|
314
|
+
// mode bit could break, so it is not held to this.
|
|
315
|
+
if (examples.length === 0) {
|
|
316
|
+
if (codeBearing(decl)) {
|
|
317
|
+
tally.findings.push({
|
|
318
|
+
kind: ExampleFindingKind.NoDocumentedExample,
|
|
319
|
+
skill: decl.name,
|
|
320
|
+
path: decl.path,
|
|
321
|
+
detail: 'declares a `cli:` but its SKILL.md documents no command in a ' +
|
|
322
|
+
'shell fence, so nothing about it has ever been executed from ' +
|
|
323
|
+
'the doc',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// #707: documenting commands is not the same as documenting a RUNNABLE
|
|
329
|
+
// one. The cheapest fix is the skill's own `--help`, which needs no
|
|
330
|
+
// fixtures, credentials or network — and marking blocks illustrative
|
|
331
|
+
// cannot satisfy this, so the declaration stays honest.
|
|
332
|
+
if (codeBearing(decl) && !examples.some((e) => e.executable)) {
|
|
333
|
+
tally.findings.push({
|
|
334
|
+
kind: ExampleFindingKind.NoExecutableExample,
|
|
335
|
+
skill: decl.name,
|
|
336
|
+
path: decl.path,
|
|
337
|
+
detail: `declares a \`cli:\` and documents ${examples.length} command(s), ` +
|
|
338
|
+
'but none is executable, so nothing in its doc has ever been run. ' +
|
|
339
|
+
'Add one placeholder-free help-shaped example (its own `--help`).',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
tallyDeclaration(decl, runExamples(examples, run, cwd), tally);
|
|
343
|
+
}
|
|
344
|
+
return tally;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The production runner: spawn the documented command against the repo's own
|
|
348
|
+
* built CLI, in `cwd`.
|
|
349
|
+
*
|
|
350
|
+
* Two deliberate substitutions, both narrow:
|
|
351
|
+
*
|
|
352
|
+
* - The leading `canary` token becomes `node <canaryBin>`, because a doc
|
|
353
|
+
* writes the installed name and CI has a checkout.
|
|
354
|
+
* - `--allow-executable-skills` is inserted into a `skills run` invocation,
|
|
355
|
+
* ahead of any `--` separator. `isExecutableSkillAllowed` refuses `cli:`
|
|
356
|
+
* skills without a TTY, which a spawned process never has, so without the
|
|
357
|
+
* flag every example would exit 3 and the check would measure the sandbox
|
|
358
|
+
* rather than the doc. The flag is an execution-context opt-in and changes
|
|
359
|
+
* nothing about the command's behaviour once it runs. See
|
|
360
|
+
* {@link exampleArgv} for why the position matters.
|
|
361
|
+
*/
|
|
362
|
+
export function exampleArgv(command, canaryBin) {
|
|
363
|
+
const [, ...rest] = command.split(/\s+/);
|
|
364
|
+
if (rest[0] !== 'skills' || rest[1] !== 'run')
|
|
365
|
+
return [canaryBin, ...rest];
|
|
366
|
+
// The flag must land BEFORE `--`, or canary forwards it to the skill and the
|
|
367
|
+
// executable-skill guard still refuses. Appending it was the first bug this
|
|
368
|
+
// checker found, in itself: all four documented `skills run ... -- --help`
|
|
369
|
+
// examples reported exit 3, which measured the sandbox rather than the doc.
|
|
370
|
+
const sep = rest.indexOf('--');
|
|
371
|
+
const at = sep === -1 ? rest.length : sep;
|
|
372
|
+
return [
|
|
373
|
+
canaryBin,
|
|
374
|
+
...rest.slice(0, at),
|
|
375
|
+
'--allow-executable-skills',
|
|
376
|
+
...rest.slice(at),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
export function spawnRunner(canaryBin) {
|
|
380
|
+
return (command, cwd) => {
|
|
381
|
+
const res = spawnSync(process.execPath, exampleArgv(command, canaryBin), {
|
|
382
|
+
cwd,
|
|
383
|
+
encoding: 'utf-8',
|
|
384
|
+
timeout: 60_000,
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
status: res.status,
|
|
388
|
+
output: `${res.stdout ?? ''}${res.stderr ?? ''}`,
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=skill-examples.js.map
|
|
@@ -67,11 +67,25 @@ function codePointCompare(a, b) {
|
|
|
67
67
|
}
|
|
68
68
|
return ca.length - cb.length;
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Bundled skills live at `<root>/agents/skills`, where `<root>` is three
|
|
72
|
+
* directories above this module. Python: `_AGENTS_SKILLS_DIR`.
|
|
73
|
+
*
|
|
74
|
+
* The "three levels up" is a PACKAGING CONTRACT, not an implementation detail
|
|
75
|
+
* (#757). It holds in the source tree (`ts/src/core`), in the compiled tree
|
|
76
|
+
* (`ts/dist/core`), and in the published npm package (`dist/engine/core`) --
|
|
77
|
+
* but only while whatever sits at that root actually ships an `agents/skills`.
|
|
78
|
+
* It did not: `canary-test-cli@7.1.0` published `bin/` and `dist/` only, so an
|
|
79
|
+
* installed CLI resolved this to a directory that has never existed and
|
|
80
|
+
* reported every bundled skill as absent, from any cwd. Exported so
|
|
81
|
+
* `ts/test/skill-packaging.test.ts` can pin both halves of the contract.
|
|
82
|
+
*/
|
|
83
|
+
export function bundledSkillsDirFrom(moduleDir) {
|
|
84
|
+
// moduleDir = <root>/<a>/<b>/core -> the root is three levels up.
|
|
85
|
+
return resolve(moduleDir, '..', '..', '..', 'agents', 'skills');
|
|
86
|
+
}
|
|
71
87
|
function defaultAgentsSkillsDir() {
|
|
72
|
-
|
|
73
|
-
// here = ts/src/core -> repo root is three levels up (Python: parents[2]).
|
|
74
|
-
return resolve(here, '..', '..', '..', 'agents', 'skills');
|
|
88
|
+
return bundledSkillsDirFrom(dirname(fileURLToPath(import.meta.url)));
|
|
75
89
|
}
|
|
76
90
|
/**
|
|
77
91
|
* A discovered skill (Python: `SkillInfo` dataclass).
|
|
@@ -150,6 +164,47 @@ export class SkillRegistry {
|
|
|
150
164
|
}
|
|
151
165
|
return [...skills.values()].sort((a, b) => codePointCompare(a.name, b.name));
|
|
152
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The directory tiers {@link discover} consults, in precedence order.
|
|
169
|
+
*
|
|
170
|
+
* Exists so an empty discovery can state its denominator (#757). "No skills
|
|
171
|
+
* found." is a claim about the world; what discovery can actually attest is
|
|
172
|
+
* "none of THESE four roots held one", and the two read very differently to
|
|
173
|
+
* someone standing in a directory full of SKILL.md files. The local tier
|
|
174
|
+
* walks cwd up to the git root, so it renders as the range it swept rather
|
|
175
|
+
* than one line per ancestor.
|
|
176
|
+
*/
|
|
177
|
+
searchRoots(root) {
|
|
178
|
+
const searchRoot = resolve(root ?? process.cwd());
|
|
179
|
+
const ancestors = SkillRegistry.ancestorsToGitRoot(searchRoot);
|
|
180
|
+
const localDirs = ancestors.map((a) => join(a, '.canary', 'skills'));
|
|
181
|
+
const overlaysRoot = join(this.home, '.canary', 'overlays');
|
|
182
|
+
const globalDir = join(this.home, '.canary', 'skills');
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
tier: 'bundled',
|
|
186
|
+
path: this.agentsSkillsDir,
|
|
187
|
+
exists: existsSync(this.agentsSkillsDir),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
tier: 'overlay',
|
|
191
|
+
path: overlaysRoot,
|
|
192
|
+
exists: SkillRegistry.isDir(overlaysRoot),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
tier: 'global',
|
|
196
|
+
path: globalDir,
|
|
197
|
+
exists: SkillRegistry.isDir(globalDir),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
tier: 'local',
|
|
201
|
+
path: localDirs.length > 1
|
|
202
|
+
? `${localDirs[0]} (and ${localDirs.length - 1} ancestor(s) up to the git root)`
|
|
203
|
+
: (localDirs[0] ?? join(searchRoot, '.canary', 'skills')),
|
|
204
|
+
exists: localDirs.some((d) => existsSync(d)),
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
153
208
|
/** Return the SkillInfo for `name` honoring precedence, or null. */
|
|
154
209
|
find(name, root) {
|
|
155
210
|
for (const skill of this.discover(root)) {
|