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.
Files changed (122) hide show
  1. package/agents/skills/README.md +308 -0
  2. package/agents/skills/canary:generate.md +49 -0
  3. package/agents/skills/canary:init.md +37 -0
  4. package/agents/skills/canary:migrate.md +66 -0
  5. package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
  6. package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
  7. package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
  8. package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
  9. package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
  10. package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
  11. package/agents/skills/claude-code/canary-cassandra/SKILL.md +180 -0
  12. package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
  13. package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
  14. package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -0
  15. package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
  16. package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
  17. package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
  18. package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
  19. package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
  20. package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
  21. package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
  22. package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
  23. package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
  24. package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
  25. package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
  26. package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
  27. package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
  28. package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
  29. package/agents/skills/claude-code/canary-fleet-health/SKILL.md +196 -0
  30. package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
  31. package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
  32. package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
  33. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
  34. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
  35. package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
  36. package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
  37. package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
  38. package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
  39. package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
  40. package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
  41. package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
  42. package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +129 -0
  43. package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
  44. package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
  45. package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
  46. package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
  47. package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
  48. package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
  49. package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
  50. package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
  51. package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
  52. package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
  53. package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
  54. package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
  55. package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
  56. package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
  57. package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
  58. package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
  59. package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
  60. package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
  61. package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
  62. package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
  63. package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
  64. package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
  65. package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
  66. package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
  67. package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
  68. package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
  69. package/agents/skills/lib/parse-args.mjs +214 -0
  70. package/dist/engine/analysis/cli.js +116 -54
  71. package/dist/engine/analysis/engine.js +34 -16
  72. package/dist/engine/analysis/reports.js +5 -4
  73. package/dist/engine/cli-commands.js +252 -106
  74. package/dist/engine/cli-common.js +15 -24
  75. package/dist/engine/cli.core.js +37 -11
  76. package/dist/engine/cli.js +2 -2
  77. package/dist/engine/company-knowledge-cli.js +2 -2
  78. package/dist/engine/core/adoption.js +408 -0
  79. package/dist/engine/core/framework-probes.js +7 -7
  80. package/dist/engine/core/fs-glob.js +2 -2
  81. package/dist/engine/core/gate-result.js +17 -0
  82. package/dist/engine/core/migrator.js +9 -17
  83. package/dist/engine/core/pattern-matcher.js +23 -5
  84. package/dist/engine/core/persona.js +421 -0
  85. package/dist/engine/core/promotion-verdict.js +261 -0
  86. package/dist/engine/core/quality-scorer.js +15 -2
  87. package/dist/engine/core/reporter.js +1 -9
  88. package/dist/engine/core/skill-dispatch.js +115 -0
  89. package/dist/engine/core/skill-examples.js +392 -0
  90. package/dist/engine/core/skill-registry.js +59 -4
  91. package/dist/engine/core/skill-surfaces.js +307 -0
  92. package/dist/engine/core/static-linter.js +310 -38
  93. package/dist/engine/core/test-files.js +77 -0
  94. package/dist/engine/core/ticket-updater.js +1 -7
  95. package/dist/engine/core/vacuity-scanner.js +726 -0
  96. package/dist/engine/core/workflow-discovery.js +2 -8
  97. package/dist/engine/core/workspace-detect.js +7 -6
  98. package/dist/engine/data/personas/registry.json +36 -0
  99. package/dist/engine/guardian/adjudication.js +5 -5
  100. package/dist/engine/guardian/analysis-emit.js +19 -28
  101. package/dist/engine/guardian/cli.js +150 -51
  102. package/dist/engine/guardian/coverage.js +1 -1
  103. package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
  104. package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
  105. package/dist/engine/guardian/pr-check.js +322 -33
  106. package/dist/engine/guardian/pr-comment.js +4 -3
  107. package/dist/engine/history/cli.js +210 -6
  108. package/dist/engine/history/ndjson-store.js +9 -5
  109. package/dist/engine/history/record.js +34 -5
  110. package/dist/engine/history/run-recorder.js +165 -0
  111. package/dist/engine/history/schema.js +25 -7
  112. package/dist/engine/history/store.js +9 -0
  113. package/dist/engine/mcp-server.js +35 -13
  114. package/dist/engine/skills-cli.js +249 -23
  115. package/dist/engine/util/ensure-ascii.js +37 -0
  116. package/dist/engine/workflow-cli.js +6 -6
  117. package/dist/gate-result.d.ts +11 -0
  118. package/dist/gate-result.js +18 -0
  119. package/dist/reporters/testtracker.d.ts +1 -1
  120. package/dist/reporters/testtracker.js +1 -1
  121. package/dist/uninstall.js +12 -5
  122. package/package.json +3 -2
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: canary-ship
3
+ version: '1.0.0'
4
+ description: >-
5
+ Run the canary ship gate on a finished change: spawn parallel adversarial
6
+ reviewers on the diff, resolve every confirmed finding with a regression test,
7
+ then commit, open a PR, and squash-merge while watching CI to green. Use this
8
+ whenever an implemented, locally-green change on a feature branch should be
9
+ merged — when the user says ship it, or review resolve commit push pr merge,
10
+ or get this merged when it is green, or take it the rest of the way, or right
11
+ after you finish implementing a feature or fix and the next step is
12
+ integration. It bakes in this repo's conventions (no co-author trailer, squash
13
+ merge, prettier, exclude local IDE churn, update the roadmap). NOT for writing
14
+ the implementation itself, and NOT a substitute for finishing the work — the
15
+ change must already exist and pass tests locally before the gate runs.
16
+ ---
17
+
18
+ # Canary Ship
19
+
20
+ The last mile of a change is where quality is won or lost: a plausible-looking
21
+ diff merges, and the bug nobody looked for ships with it. This skill is the
22
+ **ship gate** — the disciplined path from "the code is written and tests pass"
23
+ to "it's merged on `main` with CI green." Its spine is a real adversarial review
24
+ _before_ the merge button, not after, because the cheapest place to catch a
25
+ defect is the one before it reaches `main`.
26
+
27
+ It is not a code generator. It assumes the change already exists on a feature
28
+ branch and passes the suite locally. Its job is to pressure-test that change,
29
+ resolve what the pressure surfaces, and integrate it under this repo's
30
+ conventions without you having to re-remember them each time.
31
+
32
+ ## When this fires vs. when it doesn't
33
+
34
+ Use it when the work is **done and green** and the remaining verb is "ship",
35
+ "merge", "PR it", "take it home". Do **not** use it to do the implementation, to
36
+ merge something whose tests you haven't run, or to force a merge past a failing
37
+ gate. If the change isn't on a branch yet, make the branch first (see
38
+ [AGENTS.md](../../../../AGENTS.md) branch hygiene) — never ship from `main`.
39
+
40
+ ## Preconditions (verify before starting)
41
+
42
+ - On a feature branch, not `main` (`git branch --show-current`).
43
+ - The change is implemented and the relevant suite + lint pass locally. If you
44
+ can't confirm that, stop and run them — the gate reviews a _finished_ change.
45
+ - `git fetch origin` first, then know your diff base (`origin/main...`). Other
46
+ sessions may be active on this repo; trust the remote, not stale local state.
47
+
48
+ ## The pipeline
49
+
50
+ Five phases. Do them in order; each depends on the last. Track them as todos so
51
+ none is silently skipped.
52
+
53
+ ### 1 · Review — two independent reviewers, in parallel, on the diff
54
+
55
+ Spawn **two** review subagents in the _same_ turn so they run concurrently, each
56
+ scoped to the actual change (not the whole repo):
57
+
58
+ - a **correctness/design** pass — `harness-code-reviewer` — hunting real bugs,
59
+ broken contracts, and inconsistencies;
60
+ - an **adversarial** pass — `harness-adversarial-reviewer` — constructing
61
+ failure scenarios: assumption violations, composition failures across _all_
62
+ callers of anything you changed, and blind spots in new guards or tests.
63
+
64
+ Give each reviewer the changed-file list, the design intent, and the specific
65
+ risk areas you already suspect (a contract change? a new parser? an import
66
+ cycle?). Ask for **confirmed findings with a concrete failure scenario**
67
+ (specific input/state → bad outcome), ranked by severity — not style nits.
68
+
69
+ Why two, why adversarial, why before the merge: a single reviewer rubber-stamps;
70
+ two independent ones that disagree surface the real seam. The adversarial lens
71
+ is the one that asks "who _else_ calls this?" — the question that catches the
72
+ regression your own change introduced in a caller you weren't looking at. (This
73
+ is not hypothetical; it is the failure mode this gate exists to catch.)
74
+
75
+ If the `harness-code-review` pipeline (the `/harness code-review` skill) is
76
+ available and you prefer it, it can stand in for the correctness pass — but keep
77
+ a distinct adversarial pass alongside it. Wait for both to report before editing
78
+ anything, so you resolve in one pass and don't edit files a reviewer is still
79
+ reading.
80
+
81
+ ### 2 · Resolve — fix confirmed findings, each with a regression test
82
+
83
+ For every **confirmed** finding, fix it and add a test that would have caught
84
+ it. A fix without a regression test is a fix that silently comes back. Rank by
85
+ severity and resolve top-down.
86
+
87
+ You do not have to accept every finding — but rejecting one is a technical
88
+ judgment you must be able to defend, not a convenience. When you skip a finding,
89
+ say why (e.g. "pre-existing, out of scope"; "the quadrant is empty and pinned by
90
+ an invariant test"). When a reviewer independently confirms another reviewer's
91
+ finding, treat that convergence as strong signal it's real.
92
+
93
+ Then re-run the affected suite **and** the full suite + lint. Green here is the
94
+ gate for phase 3 — a partial run is not confirmation.
95
+
96
+ ### 3 · Commit — stage the change only, format, no co-author trailer
97
+
98
+ - **Stage precisely.** Add only the files that belong to this change. This repo
99
+ often carries local IDE/agent churn (`.claude/settings.json`, `.gitignore`,
100
+ editor caches) that is _not_ yours to commit — exclude it explicitly and
101
+ double-check `git status` before committing.
102
+ - **Format first.** Run `npx prettier --write` on any changed `*.md` / `*.yaml`
103
+ / `*.ts` _before_ committing. Prettier is a **CI** gate here
104
+ (`npm run format:check` in `harness-quality.yml`), so an unformatted diff
105
+ commits fine but fails CI _after_ you push — format now to avoid the red
106
+ round-trip. (The pre-commit hook itself runs the roadmap-comment guard,
107
+ **markdownlint** on staged `.md`, and the security-ledger refresh — not
108
+ prettier.) Watch for prettier mangling inline code in prose (e.g. `foo(bar)` →
109
+ `foo (bar)`); reword to avoid a paren at a wrap boundary if it does.
110
+ - **Doc drift + roadmap.** Before committing, check whether the change made any
111
+ doc stale, and update `docs/roadmap.md` (mark the item done, with a summary of
112
+ what shipped and any reviewed-and-resolved findings). Shipping code with a
113
+ stale roadmap is drift this project specifically tracks.
114
+ - **Commit message:** conventional (`feat(scope): …`), imperative subject, body
115
+ explaining the _why_ and the review outcome. **No `Co-Authored-By` trailer** —
116
+ this project's standing preference. Let the pre-commit hooks run.
117
+
118
+ ### 4 · PR — push and open with a structured body
119
+
120
+ `git push -u origin <branch>`, then `gh pr create --base main` with a body that
121
+ states **What / How / Review (findings resolved) / Tests**. The review section
122
+ is the honest part — name the findings the gate caught and how they were fixed.
123
+ Search for a PR template in `.github/` first and honor it if present.
124
+
125
+ ### 5 · Merge — then watch CI, sync, and prune
126
+
127
+ `gh pr merge <n> --squash --auto --delete-branch` (this repo squashes).
128
+ Repo-level auto-merge is enabled (`allow_auto_merge: true`) and there are 13
129
+ required status checks, with `strict_required_status_checks_policy: true` on
130
+ ruleset 16189198 — so a PR cannot merge until its branch is up to date with
131
+ `main` and every required check is green. `--auto` is therefore the right
132
+ default: it lands the PR the moment both hold.
133
+
134
+ Auto-merge **waits**; it does not update the branch. Whenever `main` moves the
135
+ PR goes stale, so run `gh pr update-branch <n>` and let the checks re-run.
136
+ `--delete-branch` is still needed explicitly since the repo does not auto-delete
137
+ on merge.
138
+
139
+ Merging immediately means CI runs _post-merge_ — so "merge when green" is on
140
+ you: **watch the post-merge CI runs to completion**
141
+ (`gh run watch <id> --exit-status`, or poll `gh run list --branch main`) and
142
+ confirm every check passes. If one fails, that is a real regression on `main` —
143
+ fix it forward immediately, don't leave it red.
144
+
145
+ Then sync and clean up: `git checkout main && git pull --ff-only`, delete the
146
+ merged local branch, and `git fetch --prune`.
147
+
148
+ ## Conventions this gate enforces (the point of packaging it)
149
+
150
+ | Convention | Why |
151
+ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
152
+ | No `Co-Authored-By` trailer | Standing user preference for this repo |
153
+ | Squash merge (`--squash --auto --delete-branch`) | Repo squashes; auto-merge is on and `strict` is true, so queue it and update the branch when `main` moves; branches aren't auto-deleted |
154
+ | Prettier `--write` before commit | Formatting is a CI gate (`format:check`); avoids a red CI round-trip post-push |
155
+ | Exclude local IDE/agent churn from staging | `settings.json`/caches aren't part of the change |
156
+ | Update `docs/roadmap.md` + check doc drift | Drift this project explicitly tracks |
157
+ | Adversarial review before merge, not after | Cheapest defect-catch is before `main` |
158
+
159
+ ## Rationalizations to reject
160
+
161
+ - _"It's a small change, skip the review."_ Small diffs introduce the caller
162
+ regressions no one looks for — the review is cheapest exactly when the change
163
+ looks trivial.
164
+ - _"Tests pass, just merge."_ Passing tests prove the paths you thought of. The
165
+ adversarial pass is for the paths you didn't.
166
+ - _"CI has no required checks, so I don't need to watch it."_ Then nothing else
167
+ will — post-merge red on `main` is yours to catch.
168
+ - _"I'll fix the doc/roadmap later."_ Later is how drift accumulates; it's one
169
+ edit now.
170
+
171
+ ## Stop conditions
172
+
173
+ Stop and hand back to the human when: a confirmed finding needs a product
174
+ decision you can't make; the review surfaces a design flaw that warrants
175
+ re-scoping rather than patching; a merge conflict needs human judgment; or CI
176
+ fails for a reason you can't safely fix forward. Never force-merge past a red
177
+ gate or an unresolved confirmed finding.
@@ -0,0 +1,16 @@
1
+ name: canary-ship
2
+ version: '1.0.0'
3
+ description: >-
4
+ Run the canary ship gate on a finished, locally-green change - spawn parallel
5
+ adversarial reviewers on the diff, resolve every confirmed finding with a
6
+ regression test, then commit, open a PR, and squash-merge while watching
7
+ CI to green. Bakes in this repo's conventions (no co-author trailer, squash,
8
+ prettier, exclude local IDE churn, update the roadmap).
9
+ stability: static
10
+ triggers:
11
+ - manual
12
+ platforms:
13
+ - claude-code
14
+ type: rigid
15
+ tools: []
16
+ tier: 2
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: canary-strix
3
+ description: >
4
+ Keeps company, client and consumer identifiers out of a public repository —
5
+ scanning both tracked file contents and the AUTHORSHIP of the commits a change
6
+ adds (author, committer, and `Co-authored-by:` trailers), because a company
7
+ email never appears in a file, only in commit metadata. Use when the user says
8
+ "is anything leaking into this public repo", "scan for client names", "did we
9
+ commit a company email", "open-source hygiene", or before making a repo
10
+ public. Deterministic — no LLM, no network, no execution. NOT a secret scanner
11
+ (use gitleaks/trufflehog for credentials); this is about IDENTIFIERS — who you
12
+ work for and who your customers are.
13
+ cli: scripts/cli.mjs
14
+ requires: [node>=20]
15
+ ---
16
+
17
+ # canary-strix
18
+
19
+ A public repository leaks who you work for in two places, and only one of them
20
+ is a file.
21
+
22
+ ## The two surfaces
23
+
24
+ **Tracked file contents** (`STRIX-001`) — the obvious half. A client name in a
25
+ comment, a fixture, a changelog entry.
26
+
27
+ **Commit authorship** (`STRIX-002`) — the half that gets missed. A company email
28
+ never appears _in_ a tracked file; it lives in the metadata of the commit
29
+ carrying it. A tree can scan perfectly clean while every commit in it is stamped
30
+ with a company address. This is not hypothetical: canary itself accumulated 190
31
+ such commits, every one from a clone that inherited a global `user.email`
32
+ instead of the repo's pin.
33
+
34
+ The authorship scan reads the author, the committer, **and** `Co-authored-by:`
35
+ style trailers. GitHub renders a co-author as a linked contributor on the public
36
+ commit page, which makes a trailer a _more_ visible identity surface than the
37
+ author field.
38
+
39
+ ## Where terms come from
40
+
41
+ Three sources, unioned. Which one is right depends on whether the repo is
42
+ public:
43
+
44
+ | Source | Committed? | Use when |
45
+ | ----------------------------------- | ---------- | -------------------------------------------- |
46
+ | `CANARY_PROPRIETARY_DENYLIST` env | no | **public repo** — supply it as a CI secret |
47
+ | `.proprietary-denylist` (gitignore) | no | **public repo** — the same list, at the desk |
48
+ | `.canary/company.json` | **yes** | **private repo only** |
49
+
50
+ `company.json` is the convenient option and the one to be careful with: it is
51
+ committed, so on a public repo it publishes exactly the list of identifiers you
52
+ are trying to hide. The CLI says so out loud when terms come from there.
53
+
54
+ Prefer **one term per line** in the secret. GitHub masks a multi-line secret
55
+ line by line, so each term is masked individually in a log; a comma-joined value
56
+ is only masked as the whole string, which is never the form that appears in
57
+ output.
58
+
59
+ ## Zero terms is not a clean scan
60
+
61
+ With no terms configured this cannot match anything, whatever it is pointed at.
62
+ That is an abstention, and it exits **3** under `--strict` rather than reporting
63
+ a pass. The same applies when only half the scan ran — files read but commits
64
+ unreadable is a partial result, not a green one.
65
+
66
+ Findings never echo the matched value. On a public repo the CI log is
67
+ world-readable, so printing the identifier would publish the thing the scan
68
+ exists to protect, on exactly the lines that trip it. A finding names the file
69
+ and line, or the commit and which field matched, plus a local command to inspect
70
+ it.
71
+
72
+ ## Matching
73
+
74
+ Terms are authored as prose (`Acme Health`, `Acme Inc.`) and subjects are not:
75
+ an email domain drops or changes the separator. A naive `\b<term>\b` misses
76
+ `dev@acmehealth.example`, misses `dev@acme-health.example`, and — for a term
77
+ ending in punctuation like `Acme Inc.` — matches nothing at all, ever, because
78
+ `\b` after `.` requires a word character. Those failures are silent and they
79
+ fail **open**.
80
+
81
+ So a term is split into alphanumeric tokens, joined with a separator-tolerant
82
+ gap, and bounded with unicode-aware lookarounds. Boundaries still hold: `Acme`
83
+ does not match `acmecorp`.
84
+
85
+ ## Invocation
86
+
87
+ ```bash
88
+ # Advisory scan of the current repo (exit 0 regardless):
89
+ canary skills run canary-strix
90
+
91
+ # Gate a CI step — findings or an abstention fail it:
92
+ canary skills run canary-strix -- --strict
93
+
94
+ # Files only, skipping commit authorship:
95
+ canary skills run canary-strix -- --files-only
96
+
97
+ # An explicit range instead of the CI event:
98
+ canary skills run canary-strix -- --range origin/main..HEAD
99
+
100
+ # Machine-readable:
101
+ canary skills run canary-strix -- --json
102
+
103
+ # Usage and the full flag list (exits 0):
104
+ canary skills run canary-strix -- --help
105
+ ```
106
+
107
+ On CI the commit range is resolved from the event: `GITHUB_BASE_REF` (with
108
+ `GITHUB_PR_HEAD_SHA`) on a pull request, `GITHUB_EVENT_BEFORE` on a push. Pass
109
+ `GITHUB_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}` — without it the
110
+ range ends at the checked-out `refs/pull/N/merge` commit, which GitHub
111
+ synthesises per event and authors with the PR author's _account_ email. That
112
+ commit is discarded at merge, so scanning it reports a leak that cannot happen.
113
+
114
+ ## Exit codes
115
+
116
+ | Code | Meaning |
117
+ | ---- | --------------------------------------------------------------- |
118
+ | 0 | advisory run, or `--strict` with a complete scan and no finding |
119
+ | 1 | `--strict`, findings |
120
+ | 2 | usage error |
121
+ | 3 | `--strict`, abstained — no terms, or only half the scan ran |
122
+
123
+ ## Fidelity limits
124
+
125
+ Literal terms, not inference. It will not catch a client referred to only by a
126
+ nickname, an internal hostname that is not on the list, or a codename nobody
127
+ wrote down. The list is the denominator, and the CLI always prints it.
128
+
129
+ Secrets and credentials are out of scope — that is a different tool and a
130
+ different failure mode.
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ // canary-strix -- keep company and consumer identifiers out of a public repo.
3
+ //
4
+ // --root <path> repository to scan (default: the current directory).
5
+ // --range <spec> commit range for the authorship scan; otherwise resolved
6
+ // from the CI event (GITHUB_BASE_REF / GITHUB_EVENT_BEFORE).
7
+ // --files-only skip the authorship scan.
8
+ // --json emit machine-readable findings instead of human text.
9
+ // --strict exit 1 when there are findings (default is advisory: 0).
10
+ //
11
+ // Deterministic: no LLM, no network, no execution.
12
+ //
13
+ // Invoked via `canary skills run canary-strix -- [--json] [--strict] ...`.
14
+
15
+ import {
16
+ createParser,
17
+ formatUsageError,
18
+ EXIT_USAGE,
19
+ } from '../../../lib/parse-args.mjs';
20
+ import {
21
+ loadTerms,
22
+ DENYLIST_ENV,
23
+ DENYLIST_FILE,
24
+ COMPANY_FILE,
25
+ } from './terms.mjs';
26
+ import {
27
+ compileTerms,
28
+ scanFiles,
29
+ scanAuthorship,
30
+ resolveRange,
31
+ } from './scanner.mjs';
32
+
33
+ export const SCHEMA_VERSION = 1;
34
+
35
+ const PREFIX = 'canary-strix:';
36
+
37
+ /** Reserved CLI-wide: exit 3 means "abstained -- verified zero items". */
38
+ const EXIT_ABSTAINED = 3;
39
+
40
+ const ARROW = '\u{2192}';
41
+
42
+ const USAGE =
43
+ 'usage: canary-strix [-h] [--json] [--strict] [--files-only] [--root PATH]\n' +
44
+ ' [--range SPEC]\n' +
45
+ '\n' +
46
+ 'Keep company and consumer identifiers out of a public repo.\n' +
47
+ '\n' +
48
+ 'options:\n' +
49
+ ' -h, --help show this help message and exit\n' +
50
+ ' --root PATH repository to scan (default: the current directory)\n' +
51
+ ' --range SPEC commit range for the authorship scan (default: the CI event)\n' +
52
+ ' --files-only skip the authorship scan\n' +
53
+ ' --json emit machine-readable findings instead of human text\n' +
54
+ ' --strict exit 1 when there are findings (default is advisory: exit 0)\n' +
55
+ '\n' +
56
+ 'rules:\n' +
57
+ ' STRIX-001 company identifier in a tracked file\n' +
58
+ ' STRIX-002 company identity on a commit (author, committer or trailer)\n' +
59
+ '\n' +
60
+ 'terms come from, unioned:\n' +
61
+ ` ${DENYLIST_ENV} a CI secret -- the only source safe for a PUBLIC repo\n` +
62
+ ` ${DENYLIST_FILE} gitignored, so the desk catches it before a push\n` +
63
+ ` ${COMPANY_FILE} committed: PRIVATE repos only\n` +
64
+ '\n' +
65
+ 'ZERO TERMS IS NOT A CLEAN SCAN. With no terms configured this cannot match\n' +
66
+ 'anything, and it exits 3 under --strict rather than reporting a pass.';
67
+
68
+ export const CLI_SPEC = {
69
+ prog: 'canary-strix',
70
+ booleans: {
71
+ '--json': 'json',
72
+ '--strict': 'strict',
73
+ '--files-only': 'filesOnly',
74
+ },
75
+ values: {
76
+ '--root': { key: 'root' },
77
+ '--range': { key: 'range' },
78
+ },
79
+ };
80
+
81
+ const parseArgs = createParser(CLI_SPEC);
82
+
83
+ function analyse(opts, env = process.env) {
84
+ const root = opts.root ?? '.';
85
+ const { terms, sources, committedSource, implausible } = loadTerms(root, env);
86
+
87
+ // A denylist of zero terms matches nothing, whatever it is pointed at. That
88
+ // is an abstention in every environment -- the single most important line in
89
+ // this file, because the alternative reads exactly like a clean repo.
90
+ if (!terms.length) {
91
+ return {
92
+ abstained: 'no terms configured',
93
+ terms,
94
+ implausible,
95
+ sources,
96
+ implausible,
97
+ findings: [],
98
+ };
99
+ }
100
+
101
+ const matchers = compileTerms(terms);
102
+ const files = scanFiles(root, matchers);
103
+ if (files.unavailable) {
104
+ return {
105
+ abstained: files.unavailable,
106
+ terms,
107
+ sources,
108
+ implausible,
109
+ findings: [],
110
+ };
111
+ }
112
+
113
+ const findings = [...files.findings];
114
+ let authorship = { skipped: '--files-only' };
115
+ if (!opts.filesOnly) {
116
+ authorship = scanAuthorship(root, matchers, resolveRange(env, opts.range));
117
+ if (authorship.findings) findings.push(...authorship.findings);
118
+ }
119
+
120
+ return {
121
+ terms,
122
+ sources,
123
+ committedSource,
124
+ implausible,
125
+ files,
126
+ authorship,
127
+ findings,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Warns about denylist entries that do not look like identifiers.
133
+ *
134
+ * Junk in the denylist inflates the denominator, which is the half of #818
135
+ * that outlives the parser bug: `15 term(s)` read as fifteen protections when
136
+ * seven were real, on the last line of defence before a company name reaches a
137
+ * public repo.
138
+ *
139
+ * Reports SHAPE, never the value. The denylist holds exactly the names this
140
+ * scan exists to keep out of a public repo and this line goes to a CI log, so
141
+ * printing a term to complain about it would be the leak itself.
142
+ */
143
+ function reportImplausible(terms) {
144
+ const odd = terms ?? [];
145
+ if (odd.length === 0) return;
146
+ const first = odd[0];
147
+ console.log(
148
+ `${PREFIX} WARNING: ${odd.length} denylist term(s) do not look like identifiers ` +
149
+ `(3+ all-lowercase words, e.g. ${first.split(/\s+/).length} words starting ` +
150
+ `${JSON.stringify(first.slice(0, 3))}). They are still matched; check the ` +
151
+ `denylist for stray prose.`,
152
+ );
153
+ }
154
+
155
+ function reportJson(result) {
156
+ console.log(
157
+ JSON.stringify(
158
+ {
159
+ schema_version: SCHEMA_VERSION,
160
+ skill: 'canary-strix',
161
+ abstained: result.abstained ?? null,
162
+ term_count: result.terms.length,
163
+ sources: result.sources,
164
+ files_scanned: result.files?.scanned ?? 0,
165
+ commits_scanned: result.authorship?.scanned ?? 0,
166
+ findings: result.findings,
167
+ },
168
+ null,
169
+ 2,
170
+ ),
171
+ );
172
+ }
173
+
174
+ /** Caveats that qualify an otherwise-clean line. Never silent. */
175
+ function reportCaveats(result) {
176
+ if (result.committedSource) {
177
+ console.error(
178
+ `${PREFIX} note: terms were read from ${COMPANY_FILE}, which is ` +
179
+ 'COMMITTED. On a public repo that publishes the list of identifiers ' +
180
+ `you are hiding -- use ${DENYLIST_ENV} or ${DENYLIST_FILE} instead.`,
181
+ );
182
+ }
183
+ if (result.authorship?.unavailable) {
184
+ console.error(
185
+ `${PREFIX} authorship scan could not run: ${result.authorship.unavailable}. ` +
186
+ 'Files were scanned; commits were not.',
187
+ );
188
+ }
189
+ }
190
+
191
+ function report(result, json) {
192
+ if (json) return reportJson(result);
193
+
194
+ if (result.abstained) {
195
+ console.error(
196
+ `${PREFIX} ABSTAINED -- ${result.abstained}.\n` +
197
+ 'Nothing was matched, so this is not a clean result. Configure terms in ' +
198
+ `${DENYLIST_ENV}, ${DENYLIST_FILE}, or ${COMPANY_FILE}.`,
199
+ );
200
+ return;
201
+ }
202
+
203
+ for (const f of result.findings) {
204
+ const where = f.file ? `${f.file}:${f.line}` : f.commit;
205
+ const what = f.fields ? ` (${f.fields.join('/')})` : '';
206
+ console.log(`${f.rule} ${where}${what}\n ${ARROW} ${f.detail}`);
207
+ }
208
+
209
+ // The denominators, always -- a verdict without them cannot be checked.
210
+ console.log(
211
+ `${PREFIX} ${result.findings.length} finding(s) over ` +
212
+ `${result.files.scanned} file(s) and ` +
213
+ `${result.authorship?.scanned ?? 0} commit(s), ` +
214
+ `${result.terms.length} term(s) from ${result.sources.join(' + ')}.`,
215
+ );
216
+ reportImplausible(result.implausible);
217
+ reportCaveats(result);
218
+ }
219
+
220
+ export function main(argv = [], env = process.env) {
221
+ const { opts, help, error } = parseArgs(argv);
222
+
223
+ if (help) {
224
+ console.log(USAGE);
225
+ return 0;
226
+ }
227
+ if (error) {
228
+ console.error(formatUsageError(CLI_SPEC.prog, error));
229
+ return EXIT_USAGE;
230
+ }
231
+
232
+ const result = analyse(opts, env);
233
+ report(result, opts.json);
234
+
235
+ // Advisory by default: findings are loud, the exit is not. Under --strict the
236
+ // exit-code contract applies, and an abstention takes 3 -- distinct from 1,
237
+ // "found something real".
238
+ if (!opts.strict) return 0;
239
+ if (result.abstained) return EXIT_ABSTAINED;
240
+ if (result.findings.length) return 1;
241
+ // Files scanned but commits not: a PARTIAL scan. Zero findings over a half
242
+ // that never ran is not a pass, and exiting 0 here is precisely the
243
+ // silently-degraded green this skill exists to make impossible. Findings
244
+ // still outrank it above — something real is more informative than "one
245
+ // half was dark".
246
+ if (result.authorship?.unavailable) return EXIT_ABSTAINED;
247
+ return 0;
248
+ }
249
+
250
+ // `process.exitCode`, not `process.exit()`: a large `--json` payload exceeds
251
+ // the pipe buffer and `process.exit` tears the process down mid-write, leaving
252
+ // truncated JSON that still exits 0 (#791).
253
+ if (import.meta.url === `file://${process.argv[1]}`) {
254
+ process.exitCode = main(process.argv.slice(2));
255
+ }