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
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* default for `_WORKING_DIR` uses `??` to mirror `os.environ.get(k, cwd)`
|
|
35
35
|
* (an explicitly-empty env var stays `""`).
|
|
36
36
|
* - **ensure_ascii.** Hand-built JSON returned to the MCP host is escaped via
|
|
37
|
-
* {@link ensureAscii} (
|
|
37
|
+
* the shared {@link ensureAscii} (`util/ensure-ascii.ts`) so non-ASCII units
|
|
38
38
|
* emit `\uXXXX`, matching Python's default `json.dumps`.
|
|
39
39
|
* - **splitlines.** `context_snippets` uses {@link pySplitlines}, which drops
|
|
40
40
|
* a single trailing-newline empty tail exactly as `str.splitlines()` does.
|
|
@@ -46,12 +46,14 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
46
46
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
47
47
|
import * as z from 'zod';
|
|
48
48
|
import { DomainScanner } from './core/domain-scanner.js';
|
|
49
|
-
import { detectEnvironment } from './core/environment-detect.js';
|
|
49
|
+
import { detectEnvironment, detectUserLevel, } from './core/environment-detect.js';
|
|
50
|
+
import { effectivePersonaRegistry, personaToDict, resolvePersona, } from './core/persona.js';
|
|
50
51
|
import { CanaryTestExecutor } from './core/executor.js';
|
|
51
52
|
import { FrameworkRegistry } from './core/framework-registry.js';
|
|
52
53
|
import { HarnessMigrator } from './core/migrator.js';
|
|
53
54
|
import { findTestFiles, PatternMatcher } from './core/pattern-matcher.js';
|
|
54
55
|
import { Scaffolder } from './core/scaffolder.js';
|
|
56
|
+
import { ensureAscii } from './util/ensure-ascii.js';
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
56
58
|
// Module constants (mirror agent/mcp_server.py)
|
|
57
59
|
// ---------------------------------------------------------------------------
|
|
@@ -99,15 +101,6 @@ const MAX_FILE_FUNCTIONS = 20;
|
|
|
99
101
|
// ---------------------------------------------------------------------------
|
|
100
102
|
// Python-compatibility helpers
|
|
101
103
|
// ---------------------------------------------------------------------------
|
|
102
|
-
/**
|
|
103
|
-
* Reproduce Python's `json.dumps(..., ensure_ascii=True)` (the library default):
|
|
104
|
-
* escape every code point >= 0x80 as `\uXXXX`. Copied per-module, matching the
|
|
105
|
-
* reporter.ts / guardian pattern (the regex range is written with `\u....`
|
|
106
|
-
* escapes so this source stays ASCII).
|
|
107
|
-
*/
|
|
108
|
-
function ensureAscii(json) {
|
|
109
|
-
return json.replace(/[\u0080-\uffff]/g, (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
110
|
-
}
|
|
111
104
|
/**
|
|
112
105
|
* Split like Python's `str.splitlines()` for the common line endings: splits on
|
|
113
106
|
* `\r\n` / `\r` / `\n` and drops the single empty tail a trailing separator
|
|
@@ -436,9 +429,37 @@ export function analyzeFileImpl(filePath) {
|
|
|
436
429
|
// Context-aware persona & environment detection (#341): attach the detected
|
|
437
430
|
// BASE_URL, suite type, and SDET-vs-manual user level. The file under
|
|
438
431
|
// analysis is itself an "open file" signal for the user-level heuristic.
|
|
439
|
-
const
|
|
432
|
+
const detected = detectEnvironment(projectRoot, {
|
|
440
433
|
openFiles: [filePath],
|
|
441
|
-
})
|
|
434
|
+
});
|
|
435
|
+
// Personas (#462) — and the consumer #341 never had. Detection shipped and
|
|
436
|
+
// was read by nobody: the level was computed, attached here, and dropped.
|
|
437
|
+
// Resolving it into a persona is what turns the signal into something a
|
|
438
|
+
// skill can consult instead of re-inventing its own audience rules.
|
|
439
|
+
//
|
|
440
|
+
// The persona re-derives the user level WITHOUT the analyzed file, and that
|
|
441
|
+
// is the whole point of the second call. `filePath` is this tool's argument,
|
|
442
|
+
// not an observation of what the caller is working on, so counting it as
|
|
443
|
+
// evidence about the person would let any `.ts` file in any project with a
|
|
444
|
+
// package.json clear the two-independent-signal floor and declare the reader
|
|
445
|
+
// a senior SDET. `environment` above keeps reporting it — that contract is
|
|
446
|
+
// unchanged — and only the persona declines to count it.
|
|
447
|
+
const [personaLevel, personaSignals, personaConfidence] = detectUserLevel(projectRoot, []);
|
|
448
|
+
// Both I/O decisions are made *here* rather than inside the resolver, which
|
|
449
|
+
// is pure by design: reading the environment variable, and loading the
|
|
450
|
+
// registry (shipped personas folded with every overlay's). `CANARY_PERSONA`
|
|
451
|
+
// is the audience-depth axis and is unrelated to `canary doctor --audience`,
|
|
452
|
+
// which tags which overlay checks run.
|
|
453
|
+
const persona = personaToDict(resolvePersona({
|
|
454
|
+
explicit: process.env['CANARY_PERSONA'] ?? null,
|
|
455
|
+
detected: {
|
|
456
|
+
level: personaLevel,
|
|
457
|
+
confidence: personaConfidence,
|
|
458
|
+
signals: personaSignals,
|
|
459
|
+
},
|
|
460
|
+
registry: effectivePersonaRegistry(),
|
|
461
|
+
}));
|
|
462
|
+
const environment = detected.toDict();
|
|
442
463
|
return {
|
|
443
464
|
framework,
|
|
444
465
|
framework_source: frameworkSource,
|
|
@@ -452,6 +473,7 @@ export function analyzeFileImpl(filePath) {
|
|
|
452
473
|
existing_tests: findExistingTests(projectRoot, framework),
|
|
453
474
|
context_snippets: contextSnippets,
|
|
454
475
|
environment,
|
|
476
|
+
persona,
|
|
455
477
|
};
|
|
456
478
|
}
|
|
457
479
|
/** Python: `_write_test_file_impl`. */
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
* load/attr failure -> 6) over a dynamic `import()`, which will not resolve a
|
|
11
11
|
* Python module. No Python test exercises the cli/entry execution branches.
|
|
12
12
|
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
13
15
|
import { Command } from 'commander';
|
|
14
16
|
import pc from 'picocolors';
|
|
15
|
-
import {
|
|
17
|
+
import { CliExitError, jsonIndent2, normalizeUsageExit } from './cli-common.js';
|
|
18
|
+
import { gateOutcome } from './core/gate-result.js';
|
|
19
|
+
import { checkExamples, countDeclaredIllustrative, spawnRunner, } from './core/skill-examples.js';
|
|
20
|
+
import { SurfaceFindingKind, checkSurfaces, collectSurfaces, } from './core/skill-surfaces.js';
|
|
16
21
|
import { isExecutableSkillAllowed, resolveCliPath, } from './core/skill-registry.js';
|
|
22
|
+
import { SkillDispatchError, dispatchProseSkill, } from './core/skill-dispatch.js';
|
|
17
23
|
import { CROSS, EM_DASH } from './main-deps.js';
|
|
18
24
|
function overlayName(skill) {
|
|
19
25
|
// Clone layout: ~/.canary/overlays/<overlay>/.canary/skills/<name>/SKILL.md
|
|
@@ -37,11 +43,40 @@ function formatSkill(skill, verbose) {
|
|
|
37
43
|
line += `\n ${pc.dim(skill.path)}`;
|
|
38
44
|
return line;
|
|
39
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
|
+
}
|
|
40
74
|
function listCmd(opts, deps) {
|
|
41
75
|
const verbose = opts.verbose ?? false;
|
|
42
|
-
const
|
|
76
|
+
const registry = deps.makeSkillRegistry();
|
|
77
|
+
const skills = registry.discover();
|
|
43
78
|
if (skills.length === 0) {
|
|
44
|
-
|
|
79
|
+
reportEmptyDiscovery(registry, deps);
|
|
45
80
|
return;
|
|
46
81
|
}
|
|
47
82
|
const bundled = skills.filter((s) => s.source === 'bundled');
|
|
@@ -84,23 +119,73 @@ function listCmd(opts, deps) {
|
|
|
84
119
|
deps.out(formatSkill(skill, verbose));
|
|
85
120
|
}
|
|
86
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Tier 2: render a dispatched prose skill (#756).
|
|
124
|
+
*
|
|
125
|
+
* The determinism label leads, in both modes. A consumer merging this with a
|
|
126
|
+
* `cli:` detector's findings has to be able to see that an agent produced it;
|
|
127
|
+
* a payload that only carried the workflow text would read exactly like a
|
|
128
|
+
* deterministic run's output.
|
|
129
|
+
*/
|
|
130
|
+
function renderDispatch(dispatch, json, deps) {
|
|
131
|
+
if (json) {
|
|
132
|
+
deps.out(jsonIndent2(dispatch));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
deps.out(`${pc.bold(dispatch.skill)} ${EM_DASH} dispatched as a prose skill ` +
|
|
136
|
+
`${pc.dim('(no cli:/entry: target)')}`);
|
|
137
|
+
deps.out(`${pc.yellow('determinism:')} ${pc.bold('agent-applied')} ${EM_DASH} canary ` +
|
|
138
|
+
'applied no judgment here. The workflow below must be executed by an ' +
|
|
139
|
+
'agent runtime, and its results must not be merged with a ' +
|
|
140
|
+
"deterministic detector's findings without carrying this label.");
|
|
141
|
+
if (dispatch.requires.length) {
|
|
142
|
+
deps.out(`${pc.dim('requires:')} ${dispatch.requires.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
if (dispatch.args.length) {
|
|
145
|
+
deps.out(`${pc.dim('args (forwarded, uninterpreted):')} ${dispatch.args.join(' ')}`);
|
|
146
|
+
}
|
|
147
|
+
deps.out(`${pc.dim(`source: ${dispatch.path}`)}\n`);
|
|
148
|
+
deps.out(dispatch.instructions);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Dispatch a skill with no `cli:`/`entry:` target instead of refusing it.
|
|
152
|
+
*
|
|
153
|
+
* Deliberately reached BEFORE the `--allow-executable-skills` gate: that flag
|
|
154
|
+
* guards spawning someone else's code, and dispatch spawns nothing. Gating it
|
|
155
|
+
* would leave the 14 CLI-less skills unreachable in precisely the CI contexts
|
|
156
|
+
* #756 is about. See `core/skill-dispatch.ts` for the full reasoning.
|
|
157
|
+
*/
|
|
158
|
+
function runProseSkill(skill, args, opts, deps) {
|
|
159
|
+
try {
|
|
160
|
+
renderDispatch(dispatchProseSkill(skill, args), opts.json ?? false, deps);
|
|
161
|
+
}
|
|
162
|
+
catch (exc) {
|
|
163
|
+
if (!(exc instanceof SkillDispatchError))
|
|
164
|
+
throw exc;
|
|
165
|
+
// A dispatch that could not happen is reported as a failure, never as an
|
|
166
|
+
// empty run: exit 2 keeps the old "this skill is not runnable" contract.
|
|
167
|
+
deps.out(`${pc.red(CROSS)} Skill ${pc.bold(skill.name)}: ${exc.message}`);
|
|
168
|
+
throw new CliExitError(2);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
87
171
|
async function runCmd(name, args, opts, deps) {
|
|
88
172
|
const skill = deps.makeSkillRegistry().find(name);
|
|
89
173
|
if (skill === null) {
|
|
90
174
|
deps.out(`${pc.red(CROSS)} No skill named ${pc.bold(name)} found.`);
|
|
91
|
-
throw new
|
|
175
|
+
throw new CliExitError(1);
|
|
92
176
|
}
|
|
93
177
|
if (skill.error) {
|
|
94
178
|
deps.out(`${pc.red(CROSS)} Skill ${pc.bold(name)}: ${skill.error}`);
|
|
95
|
-
throw new
|
|
179
|
+
throw new CliExitError(2);
|
|
96
180
|
}
|
|
181
|
+
// Tier 2 (#756): a skill with no cli:/entry: is dispatched, not refused.
|
|
97
182
|
if (!skill.isExecutable) {
|
|
98
|
-
|
|
99
|
-
|
|
183
|
+
runProseSkill(skill, args, opts, deps);
|
|
184
|
+
return;
|
|
100
185
|
}
|
|
101
186
|
if (!isExecutableSkillAllowed(opts.allowExecutableSkills ?? false)) {
|
|
102
187
|
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).`);
|
|
103
|
-
throw new
|
|
188
|
+
throw new CliExitError(3);
|
|
104
189
|
}
|
|
105
190
|
const forwarded = args;
|
|
106
191
|
if (skill.cli) {
|
|
@@ -110,7 +195,7 @@ async function runCmd(name, args, opts, deps) {
|
|
|
110
195
|
}
|
|
111
196
|
catch (exc) {
|
|
112
197
|
deps.out(`${pc.red(CROSS)} ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
113
|
-
throw new
|
|
198
|
+
throw new CliExitError(4);
|
|
114
199
|
}
|
|
115
200
|
const cmd = target.endsWith('.py') ? [deps.pythonExe(), target] : [target];
|
|
116
201
|
const res = deps.runSubprocess(cmd[0], [...cmd.slice(1), ...forwarded], {
|
|
@@ -120,14 +205,14 @@ async function runCmd(name, args, opts, deps) {
|
|
|
120
205
|
// A spawn failure (missing interpreter/binary) yields status=null; Python's
|
|
121
206
|
// subprocess.run raises FileNotFoundError -> nonzero exit. Map null -> 1, not
|
|
122
207
|
// a silent 0. A normal run passes its real exit code through.
|
|
123
|
-
throw new
|
|
208
|
+
throw new CliExitError(res.status ?? 1);
|
|
124
209
|
}
|
|
125
210
|
// entry: branch -- see module docstring (Python-module semantics are not
|
|
126
211
|
// portable; the exit-code ladder is preserved).
|
|
127
212
|
const [moduleName, sep, attr] = partition(skill.entry, ':');
|
|
128
213
|
if (!moduleName || !attr || !sep) {
|
|
129
214
|
deps.out(`${pc.red(CROSS)} Skill ${pc.bold(name)} entry must be 'module:callable', got '${skill.entry}'`);
|
|
130
|
-
throw new
|
|
215
|
+
throw new CliExitError(5);
|
|
131
216
|
}
|
|
132
217
|
try {
|
|
133
218
|
const mod = (await import(moduleName));
|
|
@@ -135,14 +220,135 @@ async function runCmd(name, args, opts, deps) {
|
|
|
135
220
|
if (typeof target !== 'function')
|
|
136
221
|
throw new Error('not callable');
|
|
137
222
|
const rc = target();
|
|
138
|
-
throw new
|
|
223
|
+
throw new CliExitError(typeof rc === 'number' ? rc : 0);
|
|
139
224
|
}
|
|
140
225
|
catch (exc) {
|
|
141
|
-
if (exc instanceof
|
|
226
|
+
if (exc instanceof CliExitError)
|
|
142
227
|
throw exc;
|
|
143
228
|
deps.out(`${pc.red(CROSS)} Skill ${pc.bold(name)} entry '${skill.entry}': ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
144
|
-
throw new
|
|
229
|
+
throw new CliExitError(6);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* `skills verify` -- cross-surface consistency (#452) plus execution of the
|
|
234
|
+
* commands the docs promise (#487).
|
|
235
|
+
*
|
|
236
|
+
* **Two checks, two summary lines, deliberately.** Folding them into one
|
|
237
|
+
* denominator is the trap: 80 surfaces checked and 0 examples executed would
|
|
238
|
+
* render as a single healthy number while the half that actually executes
|
|
239
|
+
* something had abstained. Each check therefore reports its own denominator
|
|
240
|
+
* through `gateOutcome`, and the reader sees both.
|
|
241
|
+
*
|
|
242
|
+
* Classified **advisory** (exit 0): both checks are landing on a corpus nothing
|
|
243
|
+
* has ever swept, so their precision is unknown and ADR 0010 says promotion
|
|
244
|
+
* waits for the triage. Abstention is still printed loudly -- that is what the
|
|
245
|
+
* classification does *not* soften.
|
|
246
|
+
*/
|
|
247
|
+
/**
|
|
248
|
+
* Run the examples half, or explain why it could not run.
|
|
249
|
+
*
|
|
250
|
+
* The examples half spawns a CLI. When there is no built one, the honest result
|
|
251
|
+
* is a zero denominator carrying the reason -- an unrunnable runner must never
|
|
252
|
+
* look like a clean corpus.
|
|
253
|
+
*/
|
|
254
|
+
function exampleHalf(opts, surfaces, root) {
|
|
255
|
+
const bin = opts.canaryBin ?? join(root, 'ts', 'bin', 'canary.js');
|
|
256
|
+
if ((opts.runExamples ?? true) && existsSync(bin)) {
|
|
257
|
+
return checkExamples(surfaces, spawnRunner(bin), root);
|
|
145
258
|
}
|
|
259
|
+
return {
|
|
260
|
+
checked: 0,
|
|
261
|
+
findings: [],
|
|
262
|
+
skipped: [
|
|
263
|
+
{
|
|
264
|
+
name: 'every documented example',
|
|
265
|
+
reason: opts.runExamples === false
|
|
266
|
+
? '--no-run-examples was passed'
|
|
267
|
+
: `no built CLI at ${bin} (run \`npm --prefix ts run build\`)`,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Per-kind tally over the FULL enum, including kinds that scored zero.
|
|
274
|
+
*
|
|
275
|
+
* A triage backlog is read by category, and `cli-not-executable=0` is the line
|
|
276
|
+
* that says the rule ran. An absent line is indistinguishable from a rule that
|
|
277
|
+
* was never evaluated -- the reading trap this whole check exists to close.
|
|
278
|
+
*/
|
|
279
|
+
function tallyByKind(findings) {
|
|
280
|
+
return Object.values(SurfaceFindingKind)
|
|
281
|
+
.map((kind) => `${kind}=${findings.filter((f) => f.kind === kind).length}`)
|
|
282
|
+
.join(' ');
|
|
283
|
+
}
|
|
284
|
+
/** Both halves as JSON, each carrying its own `checked` and `abstained`. */
|
|
285
|
+
function verifyJson(root, surfaceResult, exampleResult) {
|
|
286
|
+
return jsonIndent2({
|
|
287
|
+
root,
|
|
288
|
+
surfaces: {
|
|
289
|
+
checked: surfaceResult.checked,
|
|
290
|
+
abstained: !(surfaceResult.checked > 0),
|
|
291
|
+
findings: surfaceResult.findings,
|
|
292
|
+
},
|
|
293
|
+
examples: {
|
|
294
|
+
checked: exampleResult.checked,
|
|
295
|
+
abstained: !(exampleResult.checked > 0),
|
|
296
|
+
findings: exampleResult.findings,
|
|
297
|
+
skipped: exampleResult.skipped ?? [],
|
|
298
|
+
// The skipped bucket, split (#707). Emitted rather than left for the
|
|
299
|
+
// consumer to derive: a CI step counting declared-illustrative examples
|
|
300
|
+
// by matching the reason string would be a second copy of a literal
|
|
301
|
+
// owned here, and it would drift to 0 silently.
|
|
302
|
+
illustrative: countDeclaredIllustrative(exampleResult.skipped ?? []),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** One summary line per half, plus the remediation text on an abstention. */
|
|
307
|
+
function verifyReport(root, surfaceResult, exampleResult, deps) {
|
|
308
|
+
for (const f of surfaceResult.findings) {
|
|
309
|
+
deps.err(`${pc.yellow(CROSS)} [${f.kind}] ${f.name}: ${f.detail}`);
|
|
310
|
+
}
|
|
311
|
+
for (const f of exampleResult.findings) {
|
|
312
|
+
deps.err(`${pc.yellow(CROSS)} [${f.kind}] ${f.skill}: ${f.detail}`);
|
|
313
|
+
}
|
|
314
|
+
const surfaceOutcome = gateOutcome(surfaceResult, 'advisory', {
|
|
315
|
+
noun: 'surface declaration(s)',
|
|
316
|
+
});
|
|
317
|
+
deps.out(`surfaces: ${surfaceOutcome.summaryLine}`);
|
|
318
|
+
deps.out(` by kind: ${tallyByKind(surfaceResult.findings)}`);
|
|
319
|
+
if (surfaceOutcome.abstained) {
|
|
320
|
+
deps.out(` no skill surface was found under ${root} ${EM_DASH} check the path, ` +
|
|
321
|
+
'or that `agents/skills/` has not been renamed.');
|
|
322
|
+
}
|
|
323
|
+
const exampleOutcome = gateOutcome(exampleResult, 'advisory', {
|
|
324
|
+
noun: 'documented example(s)',
|
|
325
|
+
});
|
|
326
|
+
deps.out(`examples: ${exampleOutcome.summaryLine}`);
|
|
327
|
+
// #707: the denominator, split. "29 unverifiable" reads as one fact and is
|
|
328
|
+
// two — blocks nobody could run, and blocks the author declared as prose.
|
|
329
|
+
// Only the first is a gap, and printing the total alone buries it.
|
|
330
|
+
const skipped = exampleResult.skipped ?? [];
|
|
331
|
+
const declared = countDeclaredIllustrative(skipped);
|
|
332
|
+
deps.out(` denominator: executed=${exampleResult.checked} ` +
|
|
333
|
+
`illustrative=${declared} ` +
|
|
334
|
+
`unverifiable=${skipped.length - declared} ` +
|
|
335
|
+
`total=${exampleResult.checked + skipped.length}`);
|
|
336
|
+
if (exampleOutcome.abstained) {
|
|
337
|
+
deps.out(` zero documented commands were executed ${EM_DASH} nothing here is ` +
|
|
338
|
+
'proven. Add a help-shaped example in a shell fence to a SKILL.md, or ' +
|
|
339
|
+
'build the engine so the runner has a CLI to spawn.');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function verifyCmd(opts, deps) {
|
|
343
|
+
const root = resolve(opts.root ?? deps.cwd());
|
|
344
|
+
const surfaces = collectSurfaces(root);
|
|
345
|
+
const surfaceResult = checkSurfaces(root);
|
|
346
|
+
const exampleResult = exampleHalf(opts, surfaces, root);
|
|
347
|
+
if (opts.json) {
|
|
348
|
+
deps.out(verifyJson(root, surfaceResult, exampleResult));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
verifyReport(root, surfaceResult, exampleResult, deps);
|
|
146
352
|
}
|
|
147
353
|
/** Python `str.partition(sep)` -> `[before, sep, after]`. */
|
|
148
354
|
function partition(s, sep) {
|
|
@@ -151,6 +357,33 @@ function partition(s, sep) {
|
|
|
151
357
|
return [s, '', ''];
|
|
152
358
|
return [s.slice(0, i), sep, s.slice(i + sep.length)];
|
|
153
359
|
}
|
|
360
|
+
/** Register `skills run` -- the tier-1 target path plus the tier-2 dispatcher. */
|
|
361
|
+
function registerRun(program, deps) {
|
|
362
|
+
program
|
|
363
|
+
.command('run')
|
|
364
|
+
.description("Invoke a skill: a code-bearing skill's cli/entry target, or a prose " +
|
|
365
|
+
"skill's workflow via the dispatcher.")
|
|
366
|
+
.argument('<name>', 'Name of the skill to invoke.')
|
|
367
|
+
.argument('[args...]', "Arguments forwarded to the skill's cli/entry.")
|
|
368
|
+
.option('--allow-executable-skills', 'Opt-in to invoking cli:/entry: skills in non-interactive (CI) contexts.')
|
|
369
|
+
.option('--json', 'For a dispatched prose skill, emit the labelled payload as JSON.')
|
|
370
|
+
.action(async (name, args, opts) => {
|
|
371
|
+
await runCmd(name, args ?? [], opts, deps);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/** Register `skills verify` -- the two-denominator cross-surface check. */
|
|
375
|
+
function registerVerify(program, deps) {
|
|
376
|
+
program
|
|
377
|
+
.command('verify')
|
|
378
|
+
.description('Check that skill surfaces agree and that documented examples still run.')
|
|
379
|
+
.option('--root <path>', 'Repository root to inspect (default: cwd).')
|
|
380
|
+
.option('--json', 'Emit both denominators and every finding as JSON.')
|
|
381
|
+
.option('--no-run-examples', 'Inspect declarations only; do not execute documented examples.')
|
|
382
|
+
.option('--canary-bin <path>', 'CLI to spawn for documented examples.')
|
|
383
|
+
.action((opts) => {
|
|
384
|
+
verifyCmd(opts, deps);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
154
387
|
/** Build the `skills` sub-app wired to `deps`. */
|
|
155
388
|
export function buildSkillsCommand(deps) {
|
|
156
389
|
const program = new Command('skills');
|
|
@@ -164,15 +397,8 @@ export function buildSkillsCommand(deps) {
|
|
|
164
397
|
.action((opts) => {
|
|
165
398
|
listCmd(opts, deps);
|
|
166
399
|
});
|
|
167
|
-
program
|
|
168
|
-
|
|
169
|
-
.description("Invoke a code-bearing skill's declared cli or entry target.")
|
|
170
|
-
.argument('<name>', 'Name of the skill to invoke.')
|
|
171
|
-
.argument('[args...]', "Arguments forwarded to the skill's cli/entry.")
|
|
172
|
-
.option('--allow-executable-skills', 'Opt-in to invoking cli:/entry: skills in non-interactive (CI) contexts.')
|
|
173
|
-
.action(async (name, args, opts) => {
|
|
174
|
-
await runCmd(name, args ?? [], opts, deps);
|
|
175
|
-
});
|
|
400
|
+
registerRun(program, deps);
|
|
401
|
+
registerVerify(program, deps);
|
|
176
402
|
for (const sub of program.commands) {
|
|
177
403
|
sub.exitOverride(normalizeUsageExit);
|
|
178
404
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one `ensure_ascii=True` implementation (#710).
|
|
3
|
+
*
|
|
4
|
+
* Python's `json.dumps` escapes every non-ASCII character to `\uXXXX` by
|
|
5
|
+
* default; `JSON.stringify` emits raw UTF-8. Every module that reproduces a
|
|
6
|
+
* Python payload byte-for-byte therefore has to post-process its stringify
|
|
7
|
+
* output, and eight of them used to do it with a private copy of this function.
|
|
8
|
+
*
|
|
9
|
+
* It lives under `util/` rather than in `cli-common.ts` (which documented it
|
|
10
|
+
* first) because the copies were spread across the `core`, `guardian`, and
|
|
11
|
+
* entry layers, and the layer model in `harness.config.json` lets nothing
|
|
12
|
+
* depend on `cli`. `util` is the one leaf every layer is allowed to reach.
|
|
13
|
+
*
|
|
14
|
+
* `ts/test/shared-helper-single-source.test.ts` fails if a ninth copy appears.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Escape every non-ASCII UTF-16 code UNIT to `\uXXXX`, matching Python
|
|
18
|
+
* `json.dumps(ensure_ascii=True)`.
|
|
19
|
+
*
|
|
20
|
+
* Iterating by code unit rather than code point is the load-bearing detail: an
|
|
21
|
+
* astral character's surrogate pair emits `\udXXX\udXXX`, exactly as CPython
|
|
22
|
+
* writes it. A code-point walk (`for (const ch of json)`) would instead reach
|
|
23
|
+
* a value above `0xffff`, which does not fit a four-hex-digit `\uXXXX` escape
|
|
24
|
+
* at all -- so it would either truncate or leave the character raw.
|
|
25
|
+
*
|
|
26
|
+
* Only the `>= 0x80` range is touched, so the ASCII escapes `JSON.stringify`
|
|
27
|
+
* already produced (`\"`, `\\`, control characters) pass through intact.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureAscii(json) {
|
|
30
|
+
let out = '';
|
|
31
|
+
for (let i = 0; i < json.length; i++) {
|
|
32
|
+
const c = json.charCodeAt(i);
|
|
33
|
+
out += c >= 0x80 ? '\\u' + c.toString(16).padStart(4, '0') : json[i];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=ensure-ascii.js.map
|
|
@@ -12,7 +12,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import pc from 'picocolors';
|
|
15
|
-
import {
|
|
15
|
+
import { CliExitError, jsonIndent2, normalizeUsageExit } from './cli-common.js';
|
|
16
16
|
import { SemanticRole, WorkflowDiscoveryError, WorkflowMapping, } from './core/workflow-discovery.js';
|
|
17
17
|
import { CHECK, CROSS, MAGNIFIER, WARN, ELLIPSIS, } from './main-deps.js';
|
|
18
18
|
/** ISO-8601 UTC timestamp truncated to seconds (Python `isoformat(timespec)`). */
|
|
@@ -38,7 +38,7 @@ async function discoverCmd(opts, deps) {
|
|
|
38
38
|
}
|
|
39
39
|
if (keys.length === 0) {
|
|
40
40
|
deps.out(`${pc.yellow('No project keys found.')} Pass ${pc.bold('--project <key>')} or add keys to ${pc.bold('.canary/company.json')} ${'\u{2192}'} ${pc.bold('jira_projects')}.`);
|
|
41
|
-
throw new
|
|
41
|
+
throw new CliExitError(1);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
const errors = [];
|
|
@@ -76,7 +76,7 @@ async function discoverCmd(opts, deps) {
|
|
|
76
76
|
}
|
|
77
77
|
if (errors.length) {
|
|
78
78
|
deps.out(`\n${pc.red(`Discovery failed for: ${errors.join(', ')}`)}`);
|
|
79
|
-
throw new
|
|
79
|
+
throw new CliExitError(1);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
function showCmd(opts, deps) {
|
|
@@ -99,7 +99,7 @@ function showCmd(opts, deps) {
|
|
|
99
99
|
}
|
|
100
100
|
if (keys.length === 0) {
|
|
101
101
|
deps.out(pc.yellow('No cached workflow mappings found.'));
|
|
102
|
-
throw new
|
|
102
|
+
throw new CliExitError(0);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
let anyFound = false;
|
|
@@ -162,7 +162,7 @@ function showCmd(opts, deps) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
if (!anyFound) {
|
|
165
|
-
throw new
|
|
165
|
+
throw new CliExitError(1);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
function initCmd(opts, deps) {
|
|
@@ -170,7 +170,7 @@ function initCmd(opts, deps) {
|
|
|
170
170
|
const mappingPath = wd.mappingPath(opts.project);
|
|
171
171
|
if (existsSync(mappingPath) && !opts.force) {
|
|
172
172
|
deps.out(`${pc.yellow(WARN)} Mapping already exists at ${mappingPath}.\nUse ${pc.bold('--force')} to overwrite.`);
|
|
173
|
-
throw new
|
|
173
|
+
throw new CliExitError(1);
|
|
174
174
|
}
|
|
175
175
|
const resolvedUrl = (opts.atlassianUrl || deps.env['ATLASSIAN_URL'] || '').replace(/\/+$/, '') || null;
|
|
176
176
|
const semanticRoles = {
|
package/dist/gate-result.d.ts
CHANGED
|
@@ -48,6 +48,17 @@ export interface GateOutcomeOptions {
|
|
|
48
48
|
/** Unit noun for the clean-pass line. Default: `'check(s)'`. */
|
|
49
49
|
noun?: string;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The errno code `e` carries, or `null` when it carries none.
|
|
53
|
+
*
|
|
54
|
+
* Load-bearing wherever a read failure is absorbed into a {@link SkipEntry}:
|
|
55
|
+
* only "the filesystem said no" may become a skip. Node PROGRAMMER errors carry
|
|
56
|
+
* a string `code` too, so keying off `typeof code === 'string'` reports a
|
|
57
|
+
* genuine defect inside the scanner as a tidy abstention with a misleading
|
|
58
|
+
* reason -- which is how a scanner learns to go quiet. Anything this returns
|
|
59
|
+
* `null` for must still throw.
|
|
60
|
+
*/
|
|
61
|
+
export declare function errnoCode(e: unknown): string | null;
|
|
51
62
|
/**
|
|
52
63
|
* D7: skipped entries render in EVERY summary line.
|
|
53
64
|
*
|
package/dist/gate-result.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// `npm test` verifies this copy has not drifted (--check runs as pretest).
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.EXIT_ABSTAINED = void 0;
|
|
10
|
+
exports.errnoCode = errnoCode;
|
|
10
11
|
exports.skippedSuffix = skippedSuffix;
|
|
11
12
|
exports.gateOutcome = gateOutcome;
|
|
12
13
|
/**
|
|
@@ -19,6 +20,23 @@ const EMDASH = '\u{2014}'; // em dash
|
|
|
19
20
|
// C0 controls (incl. \n, ESC) and DEL: a skip name must never be able to
|
|
20
21
|
// forge output lines or smuggle ANSI sequences into the summary.
|
|
21
22
|
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
|
|
23
|
+
// A libuv/POSIX errno code (`ENOENT`, `EACCES`, `EISDIR`), as opposed to a Node
|
|
24
|
+
// programmer-error code (`ERR_INVALID_ARG_TYPE`).
|
|
25
|
+
const ERRNO = /^E[A-Z0-9]+$/;
|
|
26
|
+
/**
|
|
27
|
+
* The errno code `e` carries, or `null` when it carries none.
|
|
28
|
+
*
|
|
29
|
+
* Load-bearing wherever a read failure is absorbed into a {@link SkipEntry}:
|
|
30
|
+
* only "the filesystem said no" may become a skip. Node PROGRAMMER errors carry
|
|
31
|
+
* a string `code` too, so keying off `typeof code === 'string'` reports a
|
|
32
|
+
* genuine defect inside the scanner as a tidy abstention with a misleading
|
|
33
|
+
* reason -- which is how a scanner learns to go quiet. Anything this returns
|
|
34
|
+
* `null` for must still throw.
|
|
35
|
+
*/
|
|
36
|
+
function errnoCode(e) {
|
|
37
|
+
const code = e?.code;
|
|
38
|
+
return typeof code === 'string' && ERRNO.test(code) ? code : null;
|
|
39
|
+
}
|
|
22
40
|
/**
|
|
23
41
|
* D7: skipped entries render in EVERY summary line.
|
|
24
42
|
*
|
|
@@ -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/dist/uninstall.js
CHANGED
|
@@ -128,9 +128,20 @@ const KNOWN_FLAGS = new Set([
|
|
|
128
128
|
'--apply',
|
|
129
129
|
'--include-generated',
|
|
130
130
|
]);
|
|
131
|
+
/** The scope menu, shared by `--help` and the no-scope refusal. */
|
|
132
|
+
const SCOPE_HELP = ' --global overlays and orphaned Claude Code plugin caches\n' +
|
|
133
|
+
' --project .canary/, generated reports, and the .mcp.json entry\n' +
|
|
134
|
+
' --all both\n' +
|
|
135
|
+
'Nothing is removed without --apply.\n';
|
|
131
136
|
function run(argv, deps = {}) {
|
|
132
137
|
const out = deps.out ?? process.stdout;
|
|
133
138
|
const err = deps.err ?? process.stderr;
|
|
139
|
+
// Routed before the engine's commander sees argv (#730), so `--help` lands
|
|
140
|
+
// here as a plain token. Asking how a command works is not a usage error.
|
|
141
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
142
|
+
out.write(`usage: canary uninstall <scope> [--apply]\n${SCOPE_HELP}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
134
145
|
for (const a of argv) {
|
|
135
146
|
if (!KNOWN_FLAGS.has(a)) {
|
|
136
147
|
err.write(`canary uninstall: unknown option "${a}".\n`);
|
|
@@ -145,11 +156,7 @@ function run(argv, deps = {}) {
|
|
|
145
156
|
if (argv.includes('--all'))
|
|
146
157
|
scopes.push('all');
|
|
147
158
|
if (scopes.length === 0) {
|
|
148
|
-
err.write(
|
|
149
|
-
' --global overlays and orphaned Claude Code plugin caches\n' +
|
|
150
|
-
' --project .canary/, generated reports, and the .mcp.json entry\n' +
|
|
151
|
-
' --all both\n' +
|
|
152
|
-
'Nothing is removed without --apply.\n');
|
|
159
|
+
err.write(`canary uninstall: choose a scope.\n${SCOPE_HELP}`);
|
|
153
160
|
return 1;
|
|
154
161
|
}
|
|
155
162
|
if (scopes.length > 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canary-test-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.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",
|