@erclx/aitk 2.1.0 → 2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -22,15 +22,19 @@ aitk audits list --json
22
22
 
23
23
  ## What it runs
24
24
 
25
- Twelve verbs, listed by `aitk audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
25
+ Fourteen verbs, listed by `aitk audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
26
26
 
27
- The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine, twelve runs finish in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`.
27
+ The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `aitk deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's.
28
+
29
+ Twelve of the fourteen read a tree on this disk. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction.
28
30
 
29
31
  Each is invoked as the CLI the caller is running rather than as a global `aitk`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.
30
32
 
31
33
  ## What gates and what reports
32
34
 
33
- Three findings fail the run: an unresolved context citation, a banned character, word, or spelling, and a skill folder carrying no `REQUIREMENT.md`. Those are the same three `scripts/core/verify.sh` already fails a push on, and the aggregate adds nothing to the set. Each is a fact with no false-positive class behind it.
35
+ Four findings fail the run: an unresolved context citation, a banned character, word, or spelling, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
36
+
37
+ Three of the four are the ones `scripts/core/verify.sh` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the architecture record already ranks content leaving the repository above content that stays.
34
38
 
35
39
  Everything else reports. A heavy bullet, a long entry, a board row nothing resolves, a degradation term in a comment, and an implementation reaching history ahead of its test are judgments a reader settles. A push failing on one of those teaches contributors to route around the stage, which is the split recorded across every audit here and the one this command inherits rather than moves.
36
40
 
@@ -53,7 +57,13 @@ The verdict is published as a field as well as an exit code. A reader branching
53
57
 
54
58
  A per-machine folder that is not on this disk reports as `absent` and moves neither the verdict nor the exit code. Every one of those folders is gitignored, so a fresh clone and every CI run carry none of them, and counting six expected absences as failures pins the verdict at `incomplete` forever. A signal that never changes is one nobody reads after the second time they see it.
55
59
 
56
- The allowance is narrow. It covers a per-machine corpus refusing because its folder is missing, and nothing else. A tracked tree that cannot be found is a broken checkout, and a per-machine verb refusing for any other reason is a broken verb. Both stay `unmeasured`.
60
+ Which refusals count as an absence is declared per audit rather than inferred from the corpus alone. A per-machine corpus counts a missing folder. An upstream corpus counts every reason the advisory verb publishes, since each of the three means there is nothing resolved to measure. The secret scan overrides its tracked default for the three reasons that mean the project publishes nothing at all.
61
+
62
+ Everything else stays `unmeasured`. That covers a tree git cannot list, which is a broken checkout, and a package declaring no `files` field, where a publish would pack the whole tree and the scan read none of it. Both are a corpus that exists and went unread, so softening either would report a pass over a shipped tree nobody measured.
63
+
64
+ The override exists because the corpus answers the wrong question for one audit. What earns it is `private: true`, the one declaration that a package is never published, so a project carrying it reports an absent corpus rather than a broken one. Without that the aggregate would report `incomplete` in every such project on every run, which is the permanent signal this section opens by rejecting.
65
+
66
+ `upstream` is the third corpus value, carried by the advisory check alone. Its count moves when someone publishes rather than when someone edits here, so it is kept out of the retained baseline for the mirror image of the reason gitignored scratch is, and an offline run reports it absent rather than pinning the verdict at `incomplete` on every machine without a network.
57
67
 
58
68
  Every run states how many corpora it measured against how many it skipped, including a run with no findings at all. A count of what passed reads as a verdict on the whole set unless the run also says what it never reached.
59
69
 
@@ -9,48 +9,50 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
9
9
 
10
10
  ## Project-level
11
11
 
12
- | Command | Purpose |
13
- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
14
- | `aitk init [path]` | Bootstrap a project with selected toolkit domains |
15
- | `aitk sync [path]` | Sync all installed domains in a target project |
16
- | `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
17
- | `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
18
- | `aitk sandbox reset` | Reset sandbox to baseline |
19
- | `aitk sandbox clean` | Wipe the sandbox |
20
- | `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
21
- | `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
22
- | `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
23
- | `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
- | `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
- | `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
26
- | `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
27
- | `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/`, or open a GitHub issue with `--github` |
28
- | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
29
- | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
30
- | `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
31
- | `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
32
- | `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`) |
33
- | `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
34
- | `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
35
- | `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
36
- | `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
37
- | `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
38
- | `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
39
- | `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
40
- | `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
41
- | `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
42
- | `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
43
- | `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
44
- | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
45
- | `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
46
- | `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
47
- | `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
48
- | `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
49
- | `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
50
- | `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
51
- | `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
52
- | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
53
- | `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
12
+ | Command | Purpose |
13
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `aitk init [path]` | Bootstrap a project with selected toolkit domains |
15
+ | `aitk sync [path]` | Sync all installed domains in a target project |
16
+ | `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
17
+ | `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
18
+ | `aitk sandbox reset` | Reset sandbox to baseline |
19
+ | `aitk sandbox clean` | Wipe the sandbox |
20
+ | `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
21
+ | `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
22
+ | `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
23
+ | `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
+ | `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
+ | `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
26
+ | `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
27
+ | `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/`, or open a GitHub issue with `--github` |
28
+ | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
29
+ | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
30
+ | `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
31
+ | `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
32
+ | `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`) |
33
+ | `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
34
+ | `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
35
+ | `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
36
+ | `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
37
+ | `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
38
+ | `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
39
+ | `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
40
+ | `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
41
+ | `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
42
+ | `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
43
+ | `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
44
+ | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
45
+ | `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
46
+ | `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
47
+ | `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
48
+ | `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
49
+ | `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
50
+ | `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
51
+ | `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
52
+ | `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
53
+ | `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
54
+ | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
55
+ | `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
54
56
 
55
57
  ## Domain commands
56
58
 
@@ -72,6 +74,8 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
72
74
  | `comments` | `scan` |
73
75
  | `context` | `audit` |
74
76
  | `markdown` | `audit` |
77
+ | `secrets` | `scan` |
78
+ | `deps` | `audit` |
75
79
  | `audits` | `run`, `list` |
76
80
 
77
81
  Common patterns:
@@ -26,6 +26,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
26
26
  - [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
27
27
  - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
28
28
  - [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
29
+ - [State-scoped risk](state-scoped-risk.md): Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
29
30
  - [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
30
31
  - [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
31
32
  - [Test order](test-order.md): Reading where an implementation reached history ahead of its test, how a pair is decided, the three verdicts, the coverage the pairing cannot reach, and why the check reports rather than gates
@@ -129,8 +129,23 @@ SANDBOX_SCENARIO=sync aitk sandbox infra:tooling
129
129
 
130
130
  # Read every audit as one record. Exit 2 is a fact, 3 an audit that did not report
131
131
  aitk audits run --json
132
+
133
+ # Read committed state rather than an arriving change. Exit 2 carries findings
134
+ aitk secrets scan --json
135
+ aitk deps audit --json
132
136
  ```
133
137
 
138
+ The two state-scoped verbs both exit 2 on findings and mean different things by
139
+ it. A credential in the shipped tree is a fact and fails `aitk audits run`, while
140
+ a published advisory is a judgment that reports and moves no verdict. Neither
141
+ reads a diff, so a consumer already running a review surface gets no overlap.
142
+
143
+ Both refuse rather than report clean when they have no corpus, and the refusal
144
+ reason is the field to branch on. A project publishing nothing refuses the secret
145
+ scan with `no-manifest`, and one whose dependencies are not installed refuses the
146
+ advisory check with `no-lockfile`. The aggregate reads those as an absent corpus
147
+ rather than a broken run, so neither pins its verdict at `incomplete`.
148
+
134
149
  `aitk tooling sync` is the one verb above whose flag is mandatory headlessly. It
135
150
  overwrites every golden config a stack ships, which reaches the CI workflow, the
136
151
  git hooks, the end-to-end harness, and the shell scripts under `scripts/`, so a
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: State-scoped risk
3
+ description: Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
4
+ ---
5
+
6
+ # State-scoped risk
7
+
8
+ Every review surface a session can reach is scoped to a change. `claude-review` reads the branch diff, `claude-pr-review` reads a pull request, and `code-review` takes a diff, a branch, or a path. A risk that arrived before the range under review is invisible to all three by construction, which is what these two commands answer.
9
+
10
+ | Question | Command |
11
+ | -------------------------------------------------------- | ------------------- |
12
+ | Does a credential sit in the tree this repository ships? | `aitk secrets scan` |
13
+ | Does a resolved dependency carry a published advisory? | `aitk deps audit` |
14
+
15
+ Neither reads a range. That boundary is deliberate: change-scoped correctness review already exists, and a state-scoped bug scan would be a different product competing with it. Both register in `aitk audits run`, so a reader who runs the aggregate gets them without knowing they exist.
16
+
17
+ ## Secrets in the shipped tree
18
+
19
+ ```bash
20
+ aitk secrets scan
21
+ aitk secrets scan --json
22
+ aitk secrets scan ../my-app
23
+ ```
24
+
25
+ The corpus is the package's own `files` field rather than a list the check keeps. That field is the single statement of which trees leave this repository, so a second list beside it would answer the same question and drift. It also carries the negations the publish already makes, which is what puts the sandbox tree, the eval tree, and every test file out of scope by the rule that keeps them out of the tarball rather than by an exclusion this check invented.
26
+
27
+ The plugin reaches a target by a different route, loading live from `claude/` rather than from a tarball. That folder is a `files` entry too, and its `standards` and `snippets` symlinks resolve into trees the field lists in their own right, so both routes land inside the same corpus.
28
+
29
+ Three root files are read whether or not the field names them, being `package.json`, the readme, and the license, since npm packs those on every publish. A negation still removes one, because a field that excludes a file outranks the default that included it.
30
+
31
+ ### What the corpus leaves out
32
+
33
+ The corpus answers what the package publishes, which is narrower than what the repository holds. Measured against this repository on 2026-08-21, the scan read 544 files and left 593 of the 1139 git lists unread. Those include everything under `.claude/`, `wiki/`, and `internal/`, the workflow definitions under `.github/`, and the trees the publish negations remove, being `scripts/sandbox/`, `scripts/eval/`, `src/capture/`, and every test file.
34
+
35
+ A public repository makes that gap readable by anyone, so a clean run means no credential in the published tree rather than none in the repository. The run states the number on every pass, including a clean one, so the bound travels with the verdict. Widening the corpus to every tracked file is a separate decision, since the row this implements puts the shipped tree first on the record's rule that content leaving the repository gates harder than content that stays.
36
+
37
+ ### What it keys on
38
+
39
+ Every pattern matches an issued value and none of them matches a word. A scan keyed on `password`, `secret`, or `token` fires on the environment reads, the workflow inputs, and the prose that name those things, and this repository ships all three. Keying on values instead is what makes the exclusion set empty: measured across the shipped tree on 2026-08-21, 544 files produced zero findings with nothing exempted.
40
+
41
+ The set covers issuer-stamped shapes, being AWS access key ids, GitHub tokens in both forms, Google API keys, Slack tokens and webhooks, Stripe live keys, Anthropic and OpenAI keys, npm tokens, and private key block headers. What it does not reach is a credential no issuer stamps recognizably, which no exclusion policy would have helped with either.
42
+
43
+ A reported value is redacted to its two ends. Those are what a reader needs to find it in the file and to tell one match from another, and the middle is the part no report should carry.
44
+
45
+ ### Exempting a deliberate value
46
+
47
+ A line carrying a credential-shaped value on purpose takes an inline marker, either on the line itself or on the line directly above it:
48
+
49
+ ```bash
50
+ # aitk-allow-secret: documented sample from the vendor's own reference
51
+ AWS_KEY="<the sample value>"
52
+ ```
53
+
54
+ Only a marker naming a reason counts. A bare token reads as a line that meant to say something and did not, which is the rule `stub: true` already applies to a seed field set to anything but `true`, and honoring it would let a typo mute a finding.
55
+
56
+ The exemption travels with the line rather than sitting in a path list away from it, so a reader meeting a muted match finds the reason on the spot. A path allow-list was weighed and declined: the noise it would target is word-keyed and spread past the fixture trees, so it would hide part of the noise and none of the risk.
57
+
58
+ ### Exit codes and gating
59
+
60
+ Exit codes are `0` when the shipped tree carries no credential-shaped value, `1` for a refusal, and `2` for at least one value found.
61
+
62
+ This is the one entry in `aitk audits run` that gates without a `verify.sh` stage behind it. A credential in the published tree is a fact rather than a judgment, which is the test the catalog asks any gating addition to pass, and the architecture record already ranks content leaving the repository above content that stays.
63
+
64
+ A refusal is never a clean tree. Five reasons produce one, and each exits `1`, because zero findings over zero files reads in the report exactly like zero findings over the whole shipped tree.
65
+
66
+ | Reason | What it means | In the aggregate |
67
+ | ------------------ | ------------------------------------------------------------ | ---------------- |
68
+ | `no-manifest` | No `package.json`, so nothing is published from this tree | absent |
69
+ | `no-publish` | The manifest declares `private`, so it publishes nothing | absent |
70
+ | `no-shipped-files` | The `files` field matched nothing git lists | absent |
71
+ | `no-files-field` | A publish would pack the whole tree, and none of it was read | unmeasured |
72
+ | `no-git` | git could not list the tree, so the corpus is unknown | unmeasured |
73
+
74
+ The split turns on whether a corpus exists. The first three mean this project publishes nothing, which is where most targets installing this CLI sit, so reporting them as unmeasured would pin the aggregate at `incomplete` there forever.
75
+
76
+ The last two mean a corpus exists and went unread, so neither is softened. `private` is what separates them, since a manifest with no `files` field publishes everything rather than nothing and the field alone cannot tell those apart. Declaring `private: true` is the way a project that never publishes says so.
77
+
78
+ ## Advisories against the resolved dependencies
79
+
80
+ ```bash
81
+ aitk deps audit
82
+ aitk deps audit --json
83
+ ```
84
+
85
+ The check shells the runtime's own advisory command rather than carrying an index. A vendored advisory database is a second corpus to keep current, and what this is worth is the report rather than the data.
86
+
87
+ It refuses for three reasons, and each is an absence rather than a break: no `package.json`, no lockfile beside it, and no record back from the lookup. The lockfile is checked here rather than left to the underlying command, because a project whose dependencies were never resolved needs an install and not a retry, and one message naming the network would send half the readers at the wrong cause.
88
+
89
+ That buys the one failure mode no other audit here carries. The command reaches a network, so an unreachable index has to be told from a tree with nothing against it. The exit code cannot separate them, since the underlying command exits non-zero both on advisories found and on a lookup that failed, so the record on stdout is what decides: output that parses is a measurement, and output that does not is a refusal under `no-record`.
90
+
91
+ Exit codes are `0` when nothing is published against the resolved set, `1` for a refusal, and `2` for at least one advisory.
92
+
93
+ ### Why it reports rather than gates
94
+
95
+ A published advisory is a fact about the index and a judgment about this tree, since the upgrade closing it may not exist yet. A push failing on one would teach a contributor to route around the stage while nothing about the dependency has changed, which is the split every other reporting measure here rests on.
96
+
97
+ The corpus is recorded as `upstream`, a third value beside `tracked` and `per-machine`. Its count moves when someone publishes rather than when someone edits here, so a retained baseline would report growth against a tree nobody touched, and the aggregate keeps it out of the record for the mirror image of the reason it keeps gitignored scratch out.
98
+
99
+ The same value decides what an offline run reports. An index this machine could not reach is an ordinary absence rather than a broken checkout, so it lands as `absent` and moves no verdict. Reading it as unmeasured would pin the aggregate at `incomplete` on every machine without a network, which is a signal nobody reads after the second time they see it.
100
+
101
+ ## Reading either from a skill
102
+
103
+ An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the `--json` record instead.
104
+
105
+ The secret scan publishes `findings`, `files`, `skipped`, and `listed`, where `listed` is everything git reports so a consumer can state the bound alongside the verdict. The advisory check publishes `advisories` and a `severities` object. A refusal from either publishes `reason` and `message` and no measurement keys at all, which is what separates it from a clean run.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -53,6 +53,8 @@ export type Delta =
53
53
  | { readonly id: string; readonly kind: 'unrecorded' }
54
54
  /** Gitignored scratch, whose counts are one machine's and answer nobody else. */
55
55
  | { readonly id: string; readonly kind: 'per-machine' }
56
+ /** A network index, whose count moves when someone publishes rather than edits. */
57
+ | { readonly id: string; readonly kind: 'upstream' }
56
58
  /** The audit did not report, so there is nothing to compare. */
57
59
  | { readonly id: string; readonly kind: 'unmeasured' }
58
60
 
@@ -62,7 +64,9 @@ export type Delta =
62
64
  * Only a tracked corpus is retained. A gitignored record folder holds one
63
65
  * machine's session scratch, so committing its counts writes a floor no other
64
66
  * clone can reproduce, and every contributor would read a regression against a
65
- * number that describes somebody else's disk.
67
+ * number that describes somebody else's disk. An upstream count is left out
68
+ * for the mirror-image reason: it moves when an advisory is published, so a
69
+ * recorded floor would report growth against a tree nobody touched.
66
70
  *
67
71
  * An audit that did not report is left out rather than written as zero. Zeros
68
72
  * there record a clean corpus nobody measured, and the next run reads its real
@@ -95,6 +99,9 @@ export function compareBaseline(
95
99
  results: readonly AuditResult[],
96
100
  ): Delta[] {
97
101
  return results.map((result) => {
102
+ if (result.corpus === 'upstream') {
103
+ return { id: result.id, kind: 'upstream' as const }
104
+ }
98
105
  if (!result.tracked) return { id: result.id, kind: 'per-machine' as const }
99
106
  if (result.counts === undefined) {
100
107
  return { id: result.id, kind: 'unmeasured' as const }
@@ -1,4 +1,6 @@
1
+ import type { AuditRefusal } from '@/deps/audit'
1
2
  import type { ValidateRefusal as RecordRefusal } from '@/records/validate'
3
+ import type { ScanRefusal } from '@/secrets/scan'
2
4
  import type { ValidateRefusal as BoardRefusal } from '@/tasks/validate'
3
5
 
4
6
  /**
@@ -16,6 +18,17 @@ export type Corpus =
16
18
  | 'tracked'
17
19
  /** Gitignored session scratch, so the numbers are one machine's alone. */
18
20
  | 'per-machine'
21
+ /**
22
+ * Read from an index off this machine, so the count moves when someone
23
+ * publishes rather than when someone edits here.
24
+ *
25
+ * Its own member rather than either of the two above. A baseline would
26
+ * record growth nobody caused, which is what keeps it out of the retained
27
+ * set alongside per-machine scratch, and an index this run could not reach
28
+ * is an ordinary absence rather than the broken checkout a missing tracked
29
+ * tree would be.
30
+ */
31
+ | 'upstream'
19
32
 
20
33
  export type AuditStatus =
21
34
  /** The audit reported and every count it produced is zero. */
@@ -47,6 +60,17 @@ export interface AuditSpec {
47
60
  */
48
61
  readonly gatingExits: readonly number[]
49
62
  readonly corpus: Corpus
63
+ /**
64
+ * Refusal reasons that mean this audit has no corpus here rather than that
65
+ * it broke, overriding what the corpus alone would allow.
66
+ *
67
+ * Present only where the corpus answers wrongly. A tracked corpus normally
68
+ * allows nothing, since a tree that ships to targets and cannot be found is
69
+ * a broken checkout, and the secret scan is the exception: a project that
70
+ * publishes nothing has no shipped tree to read, which is an ordinary state
71
+ * rather than a defect.
72
+ */
73
+ readonly absentReasons?: readonly string[]
50
74
  /**
51
75
  * Pulls the counts worth retaining out of this verb's record.
52
76
  *
@@ -62,7 +86,10 @@ export interface AuditResult {
62
86
  readonly id: string
63
87
  readonly label: string
64
88
  readonly status: AuditStatus
89
+ /** Retained by the baseline, which is `tracked` alone. */
65
90
  readonly tracked: boolean
91
+ /** Why it is or is not retained, which `tracked` alone cannot say. */
92
+ readonly corpus: Corpus
66
93
  readonly exitCode: number
67
94
  readonly counts?: Record<string, number>
68
95
  /** Why the audit did not report, present only on `unmeasured`. */
@@ -271,6 +298,26 @@ function boardCounts(record: unknown): Record<string, number> | undefined {
271
298
  })
272
299
  }
273
300
 
301
+ /**
302
+ * Reads the advisory verb's per-severity object rather than a total.
303
+ *
304
+ * A single count folds a critical advisory into a low one, and the response to
305
+ * the two differs. The keys are read off the record rather than listed here,
306
+ * so a severity the index adds is carried instead of silently dropped.
307
+ */
308
+ function advisoryCounts(record: unknown): Record<string, number> | undefined {
309
+ const severities = asObject(asObject(record)?.severities)
310
+ if (severities === undefined) return undefined
311
+
312
+ const counts: Record<string, number> = {}
313
+ for (const [severity, value] of Object.entries(severities)) {
314
+ if (typeof value !== 'number') return undefined
315
+ counts[`advisories-${severity}`] = value
316
+ }
317
+
318
+ return counts
319
+ }
320
+
274
321
  function findingsOnly(record: unknown): Record<string, number> | undefined {
275
322
  const root = asObject(record)
276
323
  if (root === undefined) return undefined
@@ -318,10 +365,12 @@ const RECORD_KINDS: readonly (readonly [string, Corpus])[] = [
318
365
  /**
319
366
  * Every audit the aggregate runs.
320
367
  *
321
- * `context`, `markdown`, and `skills` are the three that gate, which is exactly
322
- * the set `scripts/core/verify.sh` already fails a push on. Adding a fourth
323
- * here widens what fails a push without anyone deciding to, and the split this
324
- * repository records gates a fact and reports a judgment.
368
+ * `context`, `markdown`, and `skills` gate because `scripts/core/verify.sh`
369
+ * already fails a push on each. `secrets` is the one entry that gates without
370
+ * a stage behind it, added deliberately rather than as a side effect, since a
371
+ * credential in the published tree is a fact and the split this repository
372
+ * records gates a fact and reports a judgment. Weigh any further addition
373
+ * against that test rather than against the count.
325
374
  *
326
375
  * Each verb runs once in its fullest form. Running the gating half separately
327
376
  * would walk the same tree twice for a number the full record already carries.
@@ -386,6 +435,46 @@ export const AUDITS: readonly AuditSpec[] = [
386
435
  corpus: 'tracked',
387
436
  counts: testOrderCounts,
388
437
  },
438
+ {
439
+ id: 'secrets',
440
+ label: 'Shipped tree secrets',
441
+ argv: ['secrets', 'scan', '--json'],
442
+ // The fourth gate, and the first added since the note above was written.
443
+ // A credential-shaped value sitting in the tree this repository publishes
444
+ // is a fact rather than a judgment, which is the test that note asks any
445
+ // addition to pass. It is decided here and stated in the context entry
446
+ // rather than arriving as a side effect of registering a measure.
447
+ gatingExits: [EXIT_FINDINGS],
448
+ corpus: 'tracked',
449
+ // The three reasons that mean this tree publishes nothing, which is where
450
+ // most targets installing this CLI sit. Without the allowance the
451
+ // aggregate reports `incomplete` on every run in every such project and
452
+ // never changes, which is the permanent signal the per-machine allowance
453
+ // exists against.
454
+ //
455
+ // Two reasons are deliberately left out, and both are a corpus that exists
456
+ // and went unread. `no-git` is a broken checkout, and `no-files-field` is a
457
+ // publish that would pack the whole tree, so calling either an absence
458
+ // would report a pass over a shipped tree nobody measured.
459
+ absentReasons: [
460
+ 'no-manifest',
461
+ 'no-publish',
462
+ 'no-shipped-files',
463
+ ] satisfies ScanRefusal[],
464
+ counts: findingsOnly,
465
+ },
466
+ {
467
+ id: 'deps',
468
+ label: 'Dependency advisories',
469
+ argv: ['deps', 'audit', '--json'],
470
+ // Reports rather than gates, on the same split. A published advisory is a
471
+ // fact about the index and a judgment about this tree, since the upgrade
472
+ // may not exist yet, and a push failing on one teaches a contributor to
473
+ // route around the stage while nothing about the dependency has changed.
474
+ gatingExits: [],
475
+ corpus: 'upstream',
476
+ counts: advisoryCounts,
477
+ },
389
478
  ]
390
479
 
391
480
  export function auditFor(id: string): AuditSpec | undefined {
@@ -432,6 +521,25 @@ const ABSENT_REASONS: readonly (RecordRefusal | BoardRefusal)[] = [
432
521
  'no-board',
433
522
  ]
434
523
 
524
+ /**
525
+ * Every reason the advisory verb refuses for, all of which are an absence.
526
+ *
527
+ * An index it could not reach, a project that is not JavaScript, and one whose
528
+ * dependencies were never resolved are three states in which there is nothing
529
+ * to measure rather than something broken. The verb has no fourth reason, so
530
+ * this is its whole union rather than a chosen subset, and typing it that way
531
+ * fails the build if a later reason arrives without this decision being made.
532
+ *
533
+ * Typed against that module's own union for the reason the record reasons are:
534
+ * a literal here would go on matching nothing after a rename and turn every
535
+ * offline run back into an unmeasured audit.
536
+ */
537
+ const ADVISORY_ABSENT_REASONS: readonly AuditRefusal[] = [
538
+ 'no-record',
539
+ 'no-lockfile',
540
+ 'no-manifest',
541
+ ]
542
+
435
543
  /**
436
544
  * Whether a refusal is a folder this machine never created rather than a break.
437
545
  *
@@ -443,15 +551,25 @@ const ABSENT_REASONS: readonly (RecordRefusal | BoardRefusal)[] = [
443
551
  * A tracked corpus gets no such allowance. That tree ships to targets, so a
444
552
  * checkout that cannot find it is broken, and reading the absence as ordinary
445
553
  * would report a pass over a corpus nobody measured.
554
+ *
555
+ * An upstream corpus takes the allowance for a different reason. Its index is
556
+ * off this machine, so an offline run reaches nothing through no fault of the
557
+ * tree, and pinning the verdict at incomplete every time the network is down
558
+ * is the same signal-nobody-reads failure the per-machine case already names.
446
559
  */
560
+ function absentReasonsFor(spec: AuditSpec): readonly string[] {
561
+ if (spec.absentReasons !== undefined) return spec.absentReasons
562
+ if (spec.corpus === 'per-machine') return ABSENT_REASONS
563
+ if (spec.corpus === 'upstream') return ADVISORY_ABSENT_REASONS
564
+ return []
565
+ }
566
+
447
567
  function isExpectedAbsence(spec: AuditSpec, record: unknown): boolean {
448
- if (spec.corpus !== 'per-machine') return false
568
+ const allowed = absentReasonsFor(spec)
569
+ if (allowed.length === 0) return false
449
570
 
450
571
  const reason = asObject(record)?.reason
451
- return (
452
- typeof reason === 'string' &&
453
- (ABSENT_REASONS as readonly string[]).includes(reason)
454
- )
572
+ return typeof reason === 'string' && allowed.includes(reason)
455
573
  }
456
574
 
457
575
  /**
@@ -471,6 +589,7 @@ export function classify(
471
589
  id: spec.id,
472
590
  label: spec.label,
473
591
  tracked: isTracked(spec),
592
+ corpus: spec.corpus,
474
593
  exitCode,
475
594
  }
476
595
 
package/src/audits/run.ts CHANGED
@@ -85,6 +85,7 @@ export async function runAudits(
85
85
  label: spec.label,
86
86
  status: 'unmeasured' as const,
87
87
  tracked: spec.corpus === 'tracked',
88
+ corpus: spec.corpus,
88
89
  exitCode: 1,
89
90
  reason: `could not be started: ${error instanceof Error ? error.message : String(error)}`,
90
91
  }
package/src/cli.ts CHANGED
@@ -26,6 +26,8 @@ import { register as markdown } from '@/commands/markdown'
26
26
  import { register as records } from '@/commands/records'
27
27
  import { register as sessions } from '@/commands/sessions'
28
28
  import { register as audits } from '@/commands/audits'
29
+ import { register as secrets } from '@/commands/secrets'
30
+ import { register as deps } from '@/commands/deps'
29
31
  import { register as upgrade } from '@/commands/upgrade'
30
32
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
31
33
  import { palette } from '@/ui'
@@ -63,6 +65,8 @@ function showHelp(): void {
63
65
  `${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
64
66
  `${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, size, push, pull)${NC}`,
65
67
  `${GREY}│${NC} sessions [cmd] ${GREY}# Resolve live sessions to worktree and branch (list)${NC}`,
68
+ `${GREY}│${NC} secrets [cmd] ${GREY}# Read the shipped tree for credential-shaped values (scan)${NC}`,
69
+ `${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
66
70
  `${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
67
71
  `${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
68
72
  `${GREY}│${NC}`,
@@ -104,6 +108,8 @@ function showHelp(): void {
104
108
  `${GREY}│${NC} aitk records size --json`,
105
109
  `${GREY}│${NC} aitk records push --json`,
106
110
  `${GREY}│${NC} aitk sessions list --json`,
111
+ `${GREY}│${NC} aitk secrets scan --json`,
112
+ `${GREY}│${NC} aitk deps audit --json`,
107
113
  `${GREY}│${NC} aitk audits run --json`,
108
114
  `${GREY}│${NC} aitk upgrade --json`,
109
115
  `${GREY}└${NC}`,
@@ -148,6 +154,8 @@ context(program)
148
154
  markdown(program)
149
155
  records(program)
150
156
  sessions(program)
157
+ secrets(program)
158
+ deps(program)
151
159
  audits(program)
152
160
  upgrade(program)
153
161