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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 of `canary skills run`: the dispatcher (#756).
|
|
3
|
+
*
|
|
4
|
+
* Canary shipped tier 1 (a skill declaring `cli:`/`entry:` is spawned) and
|
|
5
|
+
* tier 3 (a prose skill is unreachable), and nothing between. 14 of canary's 21
|
|
6
|
+
* skills carry no `cli:`, so no orchestrator, CI step, or sibling skill could
|
|
7
|
+
* invoke them at all -- a have/have-not split that costs far more here than the
|
|
8
|
+
* same split costs harness, where the dispatcher runs the CLI-less majority.
|
|
9
|
+
*
|
|
10
|
+
* ## What "running a prose skill" means here, honestly
|
|
11
|
+
*
|
|
12
|
+
* Canary is a CLI. It has no agent runtime, and it is not going to grow one to
|
|
13
|
+
* close this gap. So the dispatcher does the one thing a CLI can do faithfully:
|
|
14
|
+
* it RESOLVES the skill and hands back its executable contract -- identity,
|
|
15
|
+
* declared runtime requirements, and the workflow text an agent is to apply --
|
|
16
|
+
* with the tier and the determinism stated on the payload. The caller gets a
|
|
17
|
+
* resolved, machine-readable handle to a real skill instead of exit 2.
|
|
18
|
+
*
|
|
19
|
+
* What it deliberately does NOT do is apply the workflow and present the result
|
|
20
|
+
* as canary's. That would be canary claiming an answer it did not compute.
|
|
21
|
+
*
|
|
22
|
+
* ## Determinism labelling (issue design question 2)
|
|
23
|
+
*
|
|
24
|
+
* Every dispatch is stamped `determinism: 'agent-applied'`, against
|
|
25
|
+
* `'deterministic'` for a `cli:` skill. A consumer merging findings across
|
|
26
|
+
* skills must be able to tell a scanner's output from an agent's reading of a
|
|
27
|
+
* ruleset; without the label the two look interchangeable, which is exactly the
|
|
28
|
+
* confusion #755 documents about cassandra.
|
|
29
|
+
*
|
|
30
|
+
* ## Why no `--allow-executable-skills` equivalent (design question 3)
|
|
31
|
+
*
|
|
32
|
+
* That flag exists because a freshly cloned overlay can carry a `cli:` script,
|
|
33
|
+
* and invoking it runs someone else's code on the next CI run. Dispatch runs
|
|
34
|
+
* nothing: it reads a markdown file the registry already read at discovery and
|
|
35
|
+
* prints it. There is no new execution to gate, so gating it would be
|
|
36
|
+
* ceremony -- and ceremony that would keep the 14 skills unreachable in exactly
|
|
37
|
+
* the non-interactive contexts the issue is about. The trust boundary moves to
|
|
38
|
+
* whatever the caller does with the returned text, which is the caller's gate
|
|
39
|
+
* to own, and the payload labels itself so the caller can see what it holds.
|
|
40
|
+
*
|
|
41
|
+
* ## Failure mode (design question 4)
|
|
42
|
+
*
|
|
43
|
+
* A skill that could not be dispatched raises {@link SkillDispatchError}. An
|
|
44
|
+
* unreadable or bodyless SKILL.md is a failure, never an empty success -- a
|
|
45
|
+
* dispatcher that returned "nothing to do" for a skill it could not read would
|
|
46
|
+
* be indistinguishable from one that ran and found nothing.
|
|
47
|
+
*/
|
|
48
|
+
import { readFileSync } from 'node:fs';
|
|
49
|
+
import { errnoCode } from './gate-result.js';
|
|
50
|
+
// Written as an escape so this source stays ASCII, matching gate-result.ts.
|
|
51
|
+
const EMDASH = '\u{2014}';
|
|
52
|
+
/** A dispatch that could not be completed. Never degrades to an empty result. */
|
|
53
|
+
export class SkillDispatchError extends Error {
|
|
54
|
+
skill;
|
|
55
|
+
constructor(skill, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'SkillDispatchError';
|
|
58
|
+
this.skill = skill;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Strip a leading `---` frontmatter block, leaving the workflow prose.
|
|
63
|
+
*
|
|
64
|
+
* Mirrors the delimiter handling in `SkillRegistry.parseFrontmatter`: an
|
|
65
|
+
* unterminated block means the whole file was frontmatter, and there is no
|
|
66
|
+
* body to hand back.
|
|
67
|
+
*/
|
|
68
|
+
export function skillBody(text) {
|
|
69
|
+
if (!text.startsWith('---'))
|
|
70
|
+
return text.trim();
|
|
71
|
+
const rest = text.split('\n').slice(1);
|
|
72
|
+
const end = rest.findIndex((l) => l.trim() === '---');
|
|
73
|
+
return end === -1
|
|
74
|
+
? ''
|
|
75
|
+
: rest
|
|
76
|
+
.slice(end + 1)
|
|
77
|
+
.join('\n')
|
|
78
|
+
.trim();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a prose skill into its dispatch payload.
|
|
82
|
+
*
|
|
83
|
+
* @throws {SkillDispatchError} when SKILL.md cannot be read, or holds no body.
|
|
84
|
+
*/
|
|
85
|
+
export function dispatchProseSkill(skill, args) {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = readFileSync(skill.path, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
catch (exc) {
|
|
91
|
+
const code = errnoCode(exc);
|
|
92
|
+
if (code === null)
|
|
93
|
+
throw exc;
|
|
94
|
+
throw new SkillDispatchError(skill.name, `cannot read ${skill.path} (${code}) ${EMDASH} the skill was ` +
|
|
95
|
+
'discovered but its workflow could not be loaded.');
|
|
96
|
+
}
|
|
97
|
+
const instructions = skillBody(text);
|
|
98
|
+
if (!instructions) {
|
|
99
|
+
throw new SkillDispatchError(skill.name, `${skill.path} carries frontmatter but no workflow body ${EMDASH} ` +
|
|
100
|
+
'there is nothing to dispatch. Reporting this as an empty run would ' +
|
|
101
|
+
'be indistinguishable from a skill that ran and found nothing.');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
skill: skill.name,
|
|
105
|
+
path: skill.path,
|
|
106
|
+
tier: 'dispatcher',
|
|
107
|
+
determinism: 'agent-applied',
|
|
108
|
+
requires_agent_runtime: true,
|
|
109
|
+
requires: skill.requires,
|
|
110
|
+
description: skill.description,
|
|
111
|
+
instructions,
|
|
112
|
+
args,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=skill-dispatch.js.map
|
|
@@ -59,6 +59,48 @@ const PLACEHOLDER = /[<>${}|`*\\]/;
|
|
|
59
59
|
const HELP_FLAGS = new Set(['--help', '-h', '--version', '-V']);
|
|
60
60
|
/** Non-mutating subcommands worth executing even without a help flag. */
|
|
61
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
|
+
}
|
|
62
104
|
/** How an example turned out. */
|
|
63
105
|
export var ExampleVerdict;
|
|
64
106
|
(function (ExampleVerdict) {
|
|
@@ -75,6 +117,14 @@ export var ExampleFindingKind;
|
|
|
75
117
|
ExampleFindingKind["ExampleFailed"] = "example-failed";
|
|
76
118
|
/** A code-bearing skill's doc offers no command to execute at all. */
|
|
77
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";
|
|
78
128
|
})(ExampleFindingKind || (ExampleFindingKind = {}));
|
|
79
129
|
/**
|
|
80
130
|
* Whether `line` closes the currently open fence.
|
|
@@ -95,6 +145,11 @@ function fencedShellLines(text) {
|
|
|
95
145
|
const lines = text.split('\n');
|
|
96
146
|
let fence = null;
|
|
97
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;
|
|
98
153
|
for (let i = 0; i < lines.length; i++) {
|
|
99
154
|
const line = lines[i];
|
|
100
155
|
const delimiter = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/.exec(line);
|
|
@@ -104,16 +159,24 @@ function fencedShellLines(text) {
|
|
|
104
159
|
if (delimiter) {
|
|
105
160
|
fence = delimiter[1];
|
|
106
161
|
shell = SHELL_FENCES.has((delimiter[2] ?? '').toLowerCase());
|
|
162
|
+
illustrative = pendingMarker;
|
|
163
|
+
pendingMarker = false;
|
|
164
|
+
continue;
|
|
107
165
|
}
|
|
166
|
+
if (ILLUSTRATIVE_MARKER.test(line))
|
|
167
|
+
pendingMarker = true;
|
|
168
|
+
else if (line.trim() !== '')
|
|
169
|
+
pendingMarker = false;
|
|
108
170
|
continue;
|
|
109
171
|
}
|
|
110
172
|
if (closesFence(line, delimiter, fence)) {
|
|
111
173
|
fence = null;
|
|
112
174
|
shell = false;
|
|
175
|
+
illustrative = false;
|
|
113
176
|
continue;
|
|
114
177
|
}
|
|
115
178
|
if (shell)
|
|
116
|
-
out.push({ line: i + 1, raw: line });
|
|
179
|
+
out.push({ line: i + 1, raw: line, illustrative });
|
|
117
180
|
}
|
|
118
181
|
return out;
|
|
119
182
|
}
|
|
@@ -146,7 +209,7 @@ function classify(command) {
|
|
|
146
209
|
*/
|
|
147
210
|
export function extractExamples(text, skill, path) {
|
|
148
211
|
const out = [];
|
|
149
|
-
for (const { line, raw } of fencedShellLines(text)) {
|
|
212
|
+
for (const { line, raw, illustrative } of fencedShellLines(text)) {
|
|
150
213
|
// Strip a `$ ` or `> ` shell prompt; a doc that shows a prompt is still
|
|
151
214
|
// documenting the command after it.
|
|
152
215
|
const command = raw
|
|
@@ -157,8 +220,31 @@ export function extractExamples(text, skill, path) {
|
|
|
157
220
|
continue;
|
|
158
221
|
if (command !== 'canary' && !command.startsWith('canary '))
|
|
159
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
|
+
}
|
|
160
238
|
const { executable, reason } = classify(command);
|
|
161
|
-
out.push({
|
|
239
|
+
out.push({
|
|
240
|
+
skill,
|
|
241
|
+
path,
|
|
242
|
+
command,
|
|
243
|
+
line,
|
|
244
|
+
executable,
|
|
245
|
+
declaredIllustrative: false,
|
|
246
|
+
reason,
|
|
247
|
+
});
|
|
162
248
|
}
|
|
163
249
|
return out;
|
|
164
250
|
}
|
|
@@ -239,6 +325,20 @@ export function checkExamples(surfaces, run, cwd) {
|
|
|
239
325
|
}
|
|
240
326
|
continue;
|
|
241
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
|
+
}
|
|
242
342
|
tallyDeclaration(decl, runExamples(examples, run, cwd), tally);
|
|
243
343
|
}
|
|
244
344
|
return tally;
|
|
@@ -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)) {
|
|
@@ -59,7 +59,9 @@ export function blankStringContent(code, options = {}) {
|
|
|
59
59
|
const spans = literalContentSpans(code, options.python === true);
|
|
60
60
|
if (spans.length === 0)
|
|
61
61
|
return code;
|
|
62
|
-
|
|
62
|
+
// `split('')`, not `[...code]`: spans are UTF-16 offsets, and spreading by
|
|
63
|
+
// code point would collapse each surrogate pair and shift every offset (#861).
|
|
64
|
+
const out = code.split('');
|
|
63
65
|
for (const [start, end] of spans) {
|
|
64
66
|
for (let i = start; i < end; i += 1) {
|
|
65
67
|
// Newlines survive so line numbering is unchanged; everything else goes.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one answer to "which files in this tree are tests?" (#755).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `cli-commands.ts`, where it was private, because
|
|
5
|
+
* `canary-cassandra`'s skill CLI needs the SAME answer as `canary
|
|
6
|
+
* vacuity-check`. The four Tier-0 detectors are meant to be mergeable by a
|
|
7
|
+
* single consumer, and two collectors disagreeing about the denominator is the
|
|
8
|
+
* quietest way for that to stop being true: the same run would report a
|
|
9
|
+
* different `checked` depending on which door it came through.
|
|
10
|
+
*
|
|
11
|
+
* The walk's ignore set is load-bearing (#566): a dependency's own test suite
|
|
12
|
+
* is not the consumer's to fix. One downstream run before that fix produced 254
|
|
13
|
+
* of 256 findings inside `node_modules`, with the only `critical` in vendored
|
|
14
|
+
* code.
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { basename, join } from 'node:path';
|
|
18
|
+
import { JS_TEST_EXTENSIONS } from './static-linter.js';
|
|
19
|
+
/** True when `p` is a readable directory. A missing path is not one. */
|
|
20
|
+
export function isDir(p) {
|
|
21
|
+
try {
|
|
22
|
+
return statSync(p).isDirectory();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Directories never worth walking; see the module docstring for why. */
|
|
29
|
+
const IGNORED_DIRS = new Set([
|
|
30
|
+
'node_modules',
|
|
31
|
+
'.git',
|
|
32
|
+
'__pycache__',
|
|
33
|
+
'.venv',
|
|
34
|
+
'venv',
|
|
35
|
+
'dist',
|
|
36
|
+
'build',
|
|
37
|
+
'.next',
|
|
38
|
+
'.nuxt',
|
|
39
|
+
]);
|
|
40
|
+
function walkFiles(dir) {
|
|
41
|
+
const out = [];
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
const full = join(dir, e.name);
|
|
51
|
+
if (e.isDirectory()) {
|
|
52
|
+
if (!IGNORED_DIRS.has(e.name))
|
|
53
|
+
out.push(...walkFiles(full));
|
|
54
|
+
}
|
|
55
|
+
else if (e.isFile())
|
|
56
|
+
out.push(full);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `test_*.py` plus `*.test.*` / `*.spec.*` over every extension the scanners
|
|
62
|
+
* can actually read -- `.mjs` and `.cjs` included, which is the half of #566
|
|
63
|
+
* that made a directory of ESM tests collect zero files.
|
|
64
|
+
*/
|
|
65
|
+
const JS_TEST_FILE_RE = new RegExp(`\\.(test|spec)\\.(${JS_TEST_EXTENSIONS.map((e) => e.slice(1)).join('|')})$`);
|
|
66
|
+
/** Recursive test-file glob matching Python's `rglob` union, sorted by path. */
|
|
67
|
+
export function collectTestFiles(dir) {
|
|
68
|
+
return walkFiles(dir)
|
|
69
|
+
.filter((p) => {
|
|
70
|
+
const b = basename(p);
|
|
71
|
+
return ((b.startsWith('test_') && b.endsWith('.py')) || JS_TEST_FILE_RE.test(b));
|
|
72
|
+
})
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
/** Human-readable list of what {@link collectTestFiles} looks for. */
|
|
76
|
+
export const SCANNABLE_DESC = `test_*.py, *.test|spec.{${JS_TEST_EXTENSIONS.map((e) => e.slice(1)).join(',')}}`;
|
|
77
|
+
//# sourceMappingURL=test-files.js.map
|