canary-test-cli 7.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/agents/skills/README.md +23 -4
  2. package/agents/skills/claude-code/canary-batwoman/SKILL.md +119 -0
  3. package/agents/skills/claude-code/canary-cassandra/SKILL.md +23 -16
  4. package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +3 -1
  5. package/agents/skills/claude-code/canary-ci-ready/SKILL.md +20 -3
  6. package/agents/skills/claude-code/canary-fleet-health/SKILL.md +1 -0
  7. package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +15 -0
  8. package/agents/skills/claude-code/canary-screech/SKILL.md +109 -0
  9. package/agents/skills/claude-code/canary-screech/scripts/blast.mjs +125 -0
  10. package/agents/skills/claude-code/canary-screech/scripts/cli.mjs +128 -0
  11. package/agents/skills/claude-code/canary-screech/scripts/cluster.mjs +97 -0
  12. package/agents/skills/claude-code/canary-screech/scripts/history.mjs +73 -0
  13. package/agents/skills/claude-code/canary-screech/scripts/redness.mjs +94 -0
  14. package/agents/skills/lib/parse-args.mjs +200 -139
  15. package/dist/engine/analysis/batwoman/audit.js +39 -0
  16. package/dist/engine/analysis/batwoman/closure.js +159 -0
  17. package/dist/engine/analysis/batwoman/gh-history.js +119 -0
  18. package/dist/engine/analysis/batwoman/probes.js +195 -0
  19. package/dist/engine/analysis/batwoman/registry.js +142 -0
  20. package/dist/engine/analysis/batwoman/render.js +194 -0
  21. package/dist/engine/analysis/batwoman/run-window.js +122 -0
  22. package/dist/engine/analysis/batwoman/text.js +84 -0
  23. package/dist/engine/analysis/batwoman/triggers.js +122 -0
  24. package/dist/engine/analysis/batwoman/verdict.js +64 -0
  25. package/dist/engine/analysis/cli.js +47 -14
  26. package/dist/engine/analysis/gh-flaky/gh-run-attempts.js +206 -0
  27. package/dist/engine/batwoman-cli.js +119 -0
  28. package/dist/engine/ci-ready-cli.js +71 -0
  29. package/dist/engine/cli-commands.js +46 -7
  30. package/dist/engine/cli.core.js +16 -0
  31. package/dist/engine/company-knowledge-cli.js +10 -2
  32. package/dist/engine/core/ci-ready.js +112 -0
  33. package/dist/engine/core/company-knowledge.js +8 -0
  34. package/dist/engine/core/migrator.js +147 -20
  35. package/dist/engine/core/permission-matrix.js +219 -0
  36. package/dist/engine/core/quality-scorer.js +13 -18
  37. package/dist/engine/core/scaling-curve.js +143 -0
  38. package/dist/engine/core/string-literals.js +3 -1
  39. package/dist/engine/core/vacuity-scanner.js +151 -6
  40. package/dist/engine/core/workflow-discovery.js +41 -23
  41. package/dist/engine/guardian/adjudication-github.js +136 -0
  42. package/dist/engine/guardian/adjudication.js +119 -340
  43. package/dist/engine/guardian/cli.js +180 -264
  44. package/dist/engine/guardian/coverage.js +2 -1
  45. package/dist/engine/guardian/diff-coverage/coverage-delta.js +162 -0
  46. package/dist/engine/guardian/diff-coverage/formats/cobertura.js +45 -1
  47. package/dist/engine/guardian/diff-coverage/orchestrator.js +25 -21
  48. package/dist/engine/guardian/diff-coverage/paths.js +5 -9
  49. package/dist/engine/guardian/diff-coverage/report-tier.js +88 -12
  50. package/dist/engine/guardian/diff-extractor.js +31 -32
  51. package/dist/engine/guardian/pr-check.js +262 -430
  52. package/dist/engine/guardian/pr-comment.js +35 -58
  53. package/dist/engine/guardian/weak-test.js +236 -0
  54. package/dist/engine/mcp-server.js +67 -4
  55. package/dist/engine/permission-matrix-cli.js +51 -0
  56. package/dist/engine/scaling-curve-cli.js +147 -0
  57. package/dist/engine/skills-cli.js +48 -32
  58. package/dist/engine/workflow-cli.js +85 -65
  59. package/package.json +1 -1
@@ -71,52 +71,68 @@ function reportEmptyDiscovery(registry, deps) {
71
71
  'make them appear. Reinstall, or run from a Canary checkout.');
72
72
  }
73
73
  }
74
+ /**
75
+ * Print one source tier: a separating blank line only when an earlier tier
76
+ * printed, then the header, then its skills. Split out of `listCmd` to pay
77
+ * down its complexity; every helper here stays at or under the perf-ratchet
78
+ * warning threshold (10) so the paydown adds no new finding identity.
79
+ */
80
+ function printTier(deps, header, skills, verbose, printedAbove) {
81
+ if (printedAbove)
82
+ deps.out('');
83
+ deps.out(header);
84
+ for (const skill of skills)
85
+ deps.out(formatSkill(skill, verbose));
86
+ }
87
+ function byOverlayName(a, b) {
88
+ return overlayName(a).localeCompare(overlayName(b));
89
+ }
90
+ /** Overlay skills print one header per overlay, groups sorted by name. */
91
+ function printOverlayGroups(deps, overlay, verbose, printedAbove) {
92
+ let above = printedAbove;
93
+ let cur = null;
94
+ for (const skill of [...overlay].sort(byOverlayName)) {
95
+ const oname = overlayName(skill);
96
+ if (oname !== cur) {
97
+ if (above)
98
+ deps.out('');
99
+ deps.out(`${pc.bold('Overlay skills')} ${pc.dim(`(${oname} ${EM_DASH} override bundled):`)}`);
100
+ cur = oname;
101
+ above = true;
102
+ }
103
+ deps.out(formatSkill(skill, verbose));
104
+ }
105
+ }
74
106
  function listCmd(opts, deps) {
75
- const verbose = opts.verbose ?? false;
107
+ const verbose = opts.verbose === true;
76
108
  const registry = deps.makeSkillRegistry();
77
109
  const skills = registry.discover();
78
110
  if (skills.length === 0) {
79
111
  reportEmptyDiscovery(registry, deps);
80
112
  return;
81
113
  }
82
- const bundled = skills.filter((s) => s.source === 'bundled');
83
- const overlay = skills.filter((s) => s.source === 'overlay');
84
- const globalSkills = skills.filter((s) => s.source === 'global');
85
- const local = skills.filter((s) => s.source === 'local');
114
+ const bySource = (source) => skills.filter((s) => s.source === source);
115
+ const bundled = bySource('bundled');
116
+ const overlay = bySource('overlay');
117
+ const globalSkills = bySource('global');
118
+ const local = bySource('local');
119
+ let printed = false;
86
120
  if (bundled.length) {
87
- deps.out(pc.bold('Bundled skills:'));
88
- for (const skill of bundled)
89
- deps.out(formatSkill(skill, verbose));
121
+ printTier(deps, pc.bold('Bundled skills:'), bundled, verbose, printed);
122
+ printed = true;
90
123
  }
91
124
  if (overlay.length) {
92
- const sorted = [...overlay].sort((a, b) => overlayName(a).localeCompare(overlayName(b)));
93
- let idx = 0;
94
- let cur = null;
95
- for (const skill of sorted) {
96
- const oname = overlayName(skill);
97
- if (oname !== cur) {
98
- if (bundled.length || idx > 0)
99
- deps.out('');
100
- deps.out(`${pc.bold('Overlay skills')} ${pc.dim(`(${oname} ${EM_DASH} override bundled):`)}`);
101
- cur = oname;
102
- idx += 1;
103
- }
104
- deps.out(formatSkill(skill, verbose));
105
- }
125
+ printOverlayGroups(deps, overlay, verbose, printed);
126
+ printed = true;
106
127
  }
107
128
  if (globalSkills.length) {
108
- if (bundled.length || overlay.length)
109
- deps.out('');
110
- deps.out(`${pc.bold('Global skills')} ${pc.dim(`(~/.canary/skills/ ${EM_DASH} override overlay):`)}`);
111
- for (const skill of globalSkills)
112
- deps.out(formatSkill(skill, verbose));
129
+ const header = `${pc.bold('Global skills')} ${pc.dim(`(~/.canary/skills/ ${EM_DASH} override overlay):`)}`;
130
+ printTier(deps, header, globalSkills, verbose, printed);
131
+ printed = true;
113
132
  }
114
133
  if (local.length) {
115
- if (bundled.length || overlay.length || globalSkills.length)
116
- deps.out('');
117
- deps.out(`${pc.bold('Local overlay skills')} ${pc.dim('(override global):')}`);
118
- for (const skill of local)
119
- deps.out(formatSkill(skill, verbose));
134
+ const header = `${pc.bold('Local overlay skills')} ${pc.dim('(override global):')}`;
135
+ printTier(deps, header, local, verbose, printed);
120
136
  }
121
137
  }
122
138
  /**
@@ -79,29 +79,94 @@ async function discoverCmd(opts, deps) {
79
79
  throw new CliExitError(1);
80
80
  }
81
81
  }
82
- function showCmd(opts, deps) {
83
- const wd = deps.makeWorkflowDiscovery();
82
+ /**
83
+ * Which project keys `show` should report on.
84
+ *
85
+ * `--project` names one; otherwise every cached `.canary/workflow-*.json` is
86
+ * listed. An unreadable `.canary` yields no keys rather than throwing: a
87
+ * missing cache is the ordinary first-run state, not an error.
88
+ */
89
+ function resolveProjectKeys(opts, deps) {
90
+ if (opts.project)
91
+ return [opts.project];
84
92
  let keys = [];
85
- if (opts.project) {
86
- keys = [opts.project];
93
+ const canaryDir = join(deps.cwd(), '.canary');
94
+ if (existsSync(canaryDir)) {
95
+ try {
96
+ keys = readdirSync(canaryDir)
97
+ .filter((f) => f.startsWith('workflow-') && f.endsWith('.json'))
98
+ .map((f) => f.slice('workflow-'.length, -'.json'.length));
99
+ }
100
+ catch {
101
+ keys = [];
102
+ }
87
103
  }
88
- else {
89
- const canaryDir = join(deps.cwd(), '.canary');
90
- if (existsSync(canaryDir)) {
91
- try {
92
- keys = readdirSync(canaryDir)
93
- .filter((f) => f.startsWith('workflow-') && f.endsWith('.json'))
94
- .map((f) => f.slice('workflow-'.length, -'.json'.length));
95
- }
96
- catch {
97
- keys = [];
98
- }
104
+ if (keys.length === 0) {
105
+ deps.out(pc.yellow('No cached workflow mappings found.'));
106
+ throw new CliExitError(0);
107
+ }
108
+ return keys;
109
+ }
110
+ /** One mapping as JSON: the whole document, or just its semantic roles. */
111
+ function mappingAsJson(mapping, rolesOnly) {
112
+ if (!rolesOnly)
113
+ return mapping.toJson();
114
+ const rolesDict = {};
115
+ for (const [r, sr] of Object.entries(mapping.semantic_roles)) {
116
+ rolesDict[r] = { status_name: sr.status_name, issue_type: sr.issue_type };
117
+ }
118
+ return jsonIndent2(rolesDict);
119
+ }
120
+ /** `role -> status` lines, indented under a heading. Shared by both modes. */
121
+ function printSemanticRoles(roles, lead, deps) {
122
+ deps.out(`${lead}${pc.bold('Semantic roles:')}`);
123
+ for (const [role, sr] of Object.entries(roles)) {
124
+ deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
125
+ }
126
+ }
127
+ /** Each issue type, its statuses, and its transitions. */
128
+ function printIssueTypes(issueTypes, deps) {
129
+ for (const it of issueTypes) {
130
+ deps.out(`\n ${pc.bold(it.name)}`);
131
+ for (const s of it.statuses) {
132
+ deps.out(` [${s.category}] ${s.name}`);
99
133
  }
100
- if (keys.length === 0) {
101
- deps.out(pc.yellow('No cached workflow mappings found.'));
102
- throw new CliExitError(0);
134
+ if (it.transitions.length === 0)
135
+ continue;
136
+ deps.out(' Transitions:');
137
+ for (const t of it.transitions) {
138
+ deps.out(` ${t.from_status} ${'\u{2192}'} ${t.to_status} ${pc.dim(`(${t.name})`)}`);
103
139
  }
104
140
  }
141
+ }
142
+ /**
143
+ * One cached mapping, rendered for a human.
144
+ *
145
+ * Extracted from `showCmd`, which had grown to hold key resolution, JSON
146
+ * emission and this rendering at once -- and carried the semantic-roles block
147
+ * TWICE, once per mode, which is why the two had already drifted in their
148
+ * indentation.
149
+ */
150
+ function printMapping(key, mapping, rolesOnly, deps) {
151
+ const confirmedTag = mapping.role_annotations_confirmed
152
+ ? pc.green('confirmed')
153
+ : pc.yellow('unconfirmed');
154
+ deps.out(`\n${pc.bold(key)} ${pc.dim(`source=${mapping.source} discovered=${mapping.discovered_at} roles=${confirmedTag}`)}`);
155
+ const hasRoles = Object.keys(mapping.semantic_roles).length > 0;
156
+ if (rolesOnly) {
157
+ if (hasRoles)
158
+ printSemanticRoles(mapping.semantic_roles, ' ', deps);
159
+ else
160
+ deps.out(` ${pc.yellow('No semantic roles resolved yet.')}`);
161
+ return;
162
+ }
163
+ printIssueTypes(mapping.issue_types, deps);
164
+ if (hasRoles)
165
+ printSemanticRoles(mapping.semantic_roles, '\n ', deps);
166
+ }
167
+ function showCmd(opts, deps) {
168
+ const wd = deps.makeWorkflowDiscovery();
169
+ const keys = resolveProjectKeys(opts, deps);
105
170
  let anyFound = false;
106
171
  for (const key of keys) {
107
172
  const mapping = wd.show(key);
@@ -111,55 +176,10 @@ function showCmd(opts, deps) {
111
176
  }
112
177
  anyFound = true;
113
178
  if (opts.json) {
114
- if (opts.rolesOnly) {
115
- const rolesDict = {};
116
- for (const [r, sr] of Object.entries(mapping.semantic_roles)) {
117
- rolesDict[r] = {
118
- status_name: sr.status_name,
119
- issue_type: sr.issue_type,
120
- };
121
- }
122
- deps.out(jsonIndent2(rolesDict));
123
- }
124
- else {
125
- deps.out(mapping.toJson());
126
- }
127
- continue;
128
- }
129
- const confirmedTag = mapping.role_annotations_confirmed
130
- ? pc.green('confirmed')
131
- : pc.yellow('unconfirmed');
132
- deps.out(`\n${pc.bold(key)} ${pc.dim(`source=${mapping.source} discovered=${mapping.discovered_at} roles=${confirmedTag}`)}`);
133
- if (opts.rolesOnly) {
134
- if (Object.keys(mapping.semantic_roles).length) {
135
- deps.out(` ${pc.bold('Semantic roles:')}`);
136
- for (const [role, sr] of Object.entries(mapping.semantic_roles)) {
137
- deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
138
- }
139
- }
140
- else {
141
- deps.out(` ${pc.yellow('No semantic roles resolved yet.')}`);
142
- }
179
+ deps.out(mappingAsJson(mapping, opts.rolesOnly === true));
143
180
  continue;
144
181
  }
145
- for (const it of mapping.issue_types) {
146
- deps.out(`\n ${pc.bold(it.name)}`);
147
- for (const s of it.statuses) {
148
- deps.out(` [${s.category}] ${s.name}`);
149
- }
150
- if (it.transitions.length) {
151
- deps.out(' Transitions:');
152
- for (const t of it.transitions) {
153
- deps.out(` ${t.from_status} ${'\u{2192}'} ${t.to_status} ${pc.dim(`(${t.name})`)}`);
154
- }
155
- }
156
- }
157
- if (Object.keys(mapping.semantic_roles).length) {
158
- deps.out(`\n ${pc.bold('Semantic roles:')}`);
159
- for (const [role, sr] of Object.entries(mapping.semantic_roles)) {
160
- deps.out(` ${role.padEnd(20)} ${'\u{2192}'} '${sr.status_name}' ${pc.dim(`(${sr.issue_type})`)}`);
161
- }
162
- }
182
+ printMapping(key, mapping, opts.rolesOnly === true, deps);
163
183
  }
164
184
  if (!anyFound) {
165
185
  throw new CliExitError(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-test-cli",
3
- "version": "7.2.0",
3
+ "version": "8.0.0",
4
4
  "description": "Canary — AI-powered test automation agent",
5
5
  "license": "MIT",
6
6
  "repository": {