Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
845 changes: 844 additions & 1 deletion .kit-baseline.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .kit/shared/memory.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@
{"id":"f47c5a","area":"cli","kind":"convention","title":"A gate that exists but is never invoked is the default failure, not the exception","body":"kit adopted its own ADR gate in #403 and no workflow, hook or agent instruction ever called it — armed and unfired for a month, while the rules provably caught violations. A gate nobody runs emits nothing, and nothing reads exactly like a clean run. self-audit-ci already proves every script a workflow points AT exists; the inverse (a gate that exists is pointed at by something) had no rule. When adding a gate to this repo, wire the invocation AND pin it with a test that strips comments and forbids continue-on-error / || true — a gate named in a comment is not a gate, and one that cannot fail the build is a report. General case tracked in #533.","refs":[],"author":"Peter Sandström <peter@sandstre.am>","ts":"2026-08-25T12:06:22.827Z","source_ref":"a49e85c","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"CNhZy+Of23FnzPSF6O95FvxAriwbLWBumewW/9QtgPN606vE4IiidXydMmKQj7JyfBJPOWRBCZM1c7QoSIr4Dw=="}
{"id":"d1814d","area":"cli","kind":"decision","title":"pre-commit excludes full npm test until suite timeouts are fixed","body":"kit-public uses externally managed .githooks. [hooks].pre-commit should require staged security scan + build, not full npm test: a real pre-commit run on 2026-08-27 hit Node test file timeouts in dist/policy-gate.test.js and dist/secrets-propagate.test.js. Re-add full npm test only after those suite timeouts are fixed or the suite is split for hook use.","refs":[],"author":"Peter Sandström <peter@sandstre.am>","ts":"2026-08-27T09:22:36.795Z","source_ref":"3cfa838","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"bbdKUzm6w6dIFEacsfFMdkuLhhwBjZ732zvs08X0ZPlv6NQ7J3CgDJ2GpZhf+M8k7Yff8gvlpPej4b+sYHWOBA=="}
{"id":"f57f88","area":"ops","kind":"convention","title":"post-merge health loop","body":"After merge, release, deploy, or tag push, do not rely on email or GitHub check conclusions alone. Run the connected health inbox command, treat red and unknown as not green, inspect each connected source, and keep acting or waiting until every connected sensor is green before telling the user the arc is closed. For this repo before a release is published, run the local built CLI via node dist/cli.js health.","refs":[],"author":"Peter Sandström <peter@sandstre.am>","ts":"2026-08-28T14:47:56.828Z","source_ref":"cc8e4a0","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"f2AEPcCAs2OVwuS6fbIm2JZG7UMoUZN4+54DI4V2b9Fmc6pDCfLkCkqnmH1/51XQd8431xlVfhrkw5mQkuuCDw=="}
{"id":"0ed015","area":"cli","kind":"decision","title":"A linter that lints skills must pass its own lint, and something must run it","body":"kit shipped a skill linter with a working --gate (exit 1) that nothing ever invoked -- not CI, not kit review, not verify-suite.sh -- while kit's only shipped SKILL.md failed that linter's scope check for as long as the linter had existed (no allowed-tools, so the skill implicitly claimed every tool). Fixed in d64c58e: the skill declares allowed-tools: Bash with its surface pinned in a snapshot, src/skill-run.ts gates every SKILL.md under skills/ and .claude/skills/, kit review gained a fifth stage, and ci.yml runs it as a hard failure with ci-adr-gate.test.ts pinning the invocation. Written as derived, not operator: an agent derived this from measurement, no human stated it. Second instance of the repo's own convention that a gate which exists but is never invoked is the default failure -- the first was the ADR gate in #403, unfired for a month.","refs":["d64c58e"],"author":"Claude <noreply@anthropic.com>","ts":"2026-08-30T06:59:28.396Z","source_ref":"81fabd7","provenance":"derived","confidence":"high"}
21 changes: 19 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets,

## Architecture decisions are a gate, and `kit check` does not run it

`docs/adr` holds five ADRs. The three accepted ones carry a `kit-enforce` block, which
makes them four deterministic rules — not prose:
`docs/adr` holds seven ADRs. The four accepted-and-enforced ones carry a `kit-enforce`
block, which makes them five deterministic rules — not prose:

- **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core).
- **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise.
The *floor itself* (exactly four runtime deps, all pinned) is **not** expressible in the
`kit-enforce` grammar and is enforced by `src/dependency-floor.test.ts`; the ADR says why.
- **ADR-0003** the check path imports no coverage-framework mappings.
- **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive`
from an asymmetry the code had obeyed for months, then accepted.

Three more are accepted but **documented, not enforced** — ADR-0004 (workflow skills live
above kit), ADR-0005 (browser substrate), and **ADR-0007** (kit may measure model + kit
residual risk, but owns only the rig — the frozen input, ingest schema, deterministic
adjudication and receipts — never the judgement).

`node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check`
does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill)
Expand All @@ -31,3 +40,11 @@ does. So before opening a PR that adds a dependency, moves an import, or touches

Adding one of those imports is an ADR-level decision, not a code change: amend or supersede
the ADR in the same PR, or the gate will refuse the code and cite the ADR that refused it.

**When a decision does not fit the `kit-enforce` grammar, say where it lives instead of
dropping it.** Three limits are measured and will bite again: `paths` has no negation,
`forbid_pattern`/`require_pattern` match line by line, and the import extractor is
text-level (a fixture string that looks like an import counts as one). An ADR in that
position declares `enforced_by: [src/x.test.ts]` in its frontmatter — `adr list` prints it
and `adr check` fails if the file is missing. Silently declaring more than you enforce is
the failure this exists to prevent; ADR-0002 sat that way for months.
21 changes: 19 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets,

## Architecture decisions are a gate, and `kit check` does not run it

`docs/adr` holds five ADRs. The three accepted ones carry a `kit-enforce` block, which
makes them four deterministic rules — not prose:
`docs/adr` holds seven ADRs. The four accepted-and-enforced ones carry a `kit-enforce`
block, which makes them five deterministic rules — not prose:

- **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core).
- **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise.
The *floor itself* (exactly four runtime deps, all pinned) is **not** expressible in the
`kit-enforce` grammar and is enforced by `src/dependency-floor.test.ts`; the ADR says why.
- **ADR-0003** the check path imports no coverage-framework mappings.
- **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive`
from an asymmetry the code had obeyed for months, then accepted.

Three more are accepted but **documented, not enforced** — ADR-0004 (workflow skills live
above kit), ADR-0005 (browser substrate), and **ADR-0007** (kit may measure model + kit
residual risk, but owns only the rig — the frozen input, ingest schema, deterministic
adjudication and receipts — never the judgement).

`node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check`
does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill)
Expand All @@ -44,3 +53,11 @@ does. So before opening a PR that adds a dependency, moves an import, or touches

Adding one of those imports is an ADR-level decision, not a code change: amend or supersede
the ADR in the same PR, or the gate will refuse the code and cite the ADR that refused it.

**When a decision does not fit the `kit-enforce` grammar, say where it lives instead of
dropping it.** Three limits are measured and will bite again: `paths` has no negation,
`forbid_pattern`/`require_pattern` match line by line, and the import extractor is
text-level (a fixture string that looks like an import counts as one). An ADR in that
position declares `enforced_by: [src/x.test.ts]` in its frontmatter — `adr list` prints it
and `adr check` fails if the file is missing. Silently declaring more than you enforce is
the failure this exists to prevent; ADR-0002 sat that way for months.
2 changes: 1 addition & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ port = 3107
| `kit standards [--category general\|specific\|plugins\|platform\|<lang>] [--enforce]` | Dev-standards gate: general metrics (complexity/duplication/size via lizard/jscpd/scc) + per-language linters (11 langs) + user plugins (`.kit/standards.d/`) + container (hadolint). Warn by default; `--enforce` fails net-new findings AND setup gaps. |
| `kit standards freeze` | Snapshot only the standards dimensions into `.kit-baseline.json`. |
| `kit review` | Meta-runner — `check + design + standards + adr + skill` gate for PR. The `skill` stage runs module discipline over every shipped `SKILL.md` (contract, trigger collision, bounded tool scope, snapshot drift); a repo with no skills skips honestly. |
| `kit adr [check\|list\|freeze\|derive]` | ADR → gate: enforce accepted ADRs' `kit-enforce` rules (`forbid_pattern` / `require_pattern` / `forbid_import`, incl. transitive and cross-package via `follow_packages`), cited to the ADR. `list` shows status; `freeze` baselines existing findings; `derive` proposes ADRs the code already obeys (absent import edges with a populated reverse), each re-run through the real evaluator before it is shown and emitted as `status: proposed` so it gates nothing until a human accepts it. Zero-LLM (prose is never interpreted). |
| `kit adr [check\|list\|freeze\|derive]` | ADR → gate: enforce accepted ADRs' `kit-enforce` rules (`forbid_pattern` / `require_pattern` / `forbid_import`, incl. transitive and cross-package via `follow_packages`), cited to the ADR. `list` shows status plus any `enforced_by:` frontmatter pointing at enforcement the grammar cannot express (verified to exist by `check`, so it cannot rot); `freeze` baselines existing findings; `derive` proposes ADRs the code already obeys (absent import edges with a populated reverse), each re-run through the real evaluator before it is shown and emitted as `status: proposed` so it gates nothing until a human accepts it. Zero-LLM (prose is never interpreted). |
| `kit baseline [freeze]` | Snapshot current acceptable warnings (incl. standards + ADR) to `.kit-baseline.json`. |
| `kit analyze [--write]` | Mine git history + framework markers → draft `CLAUDE.md` / `RULES.md`. |

Expand Down
36 changes: 36 additions & 0 deletions docs/adr/0002-dependency-floor.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
id: ADR-0002
title: Dependency floor — four runtime deps, stdlib otherwise
status: accepted
enforced_by: [src/dependency-floor.test.ts]
---

# ADR-0002: Dependency floor
Expand All @@ -20,6 +21,41 @@ Adding a runtime dependency is an ADR-level decision, not a convenience call.
The rule below blocks the common utility imports outright; anything else new
must argue its case in a PR that updates this ADR.

## Where the floor is actually enforced — and why not here

**Amended 2026-08-29.** This ADR declared more than its `kit-enforce` block enforced, and
the gap was found the only way such gaps are found: by breaking the rule and watching the
gate stay green. `kit pkg npm:jscpd` added a fifth runtime dependency to `package.json`,
and `kit adr check` reported `✓ 4 enforced ADR(s) — no new violations`.

The block below is a **deny-list of twelve named packages, matched at the import level in
`src/**`**. It cannot see a dependency that is not on the list, and it cannot see one that
has been added to the manifest but not yet imported. The title says "four runtime deps";
the rule says "not these twelve".

The floor is **not expressible in the `kit-enforce` grammar**, for two structural reasons:

1. `package.json` is not in the file set the ADR gate walks — `CODE_EXTS` in
`src/commands/adr.ts` lists source extensions only, so no rule here can ever apply to a
manifest.
2. `forbid_pattern` and `require_pattern` are matched **line by line**
(`firstMatchingLine` splits on newlines). An entry in `dependencies` is textually
identical to one in `devDependencies`, so a line-based regex cannot tell them apart, and
a multi-line pattern pinning the whole block cannot match at all.

Widening the walk and adding block-aware matching to serve one rule is a larger change than
the rule is worth. So the floor is enforced by **`src/dependency-floor.test.ts`**, which
asserts the exact runtime dependency set and that every version is pinned rather than a
range. It runs in the same CI job as everything else and fails the moment either claim
stops being true.

The `kit-enforce` block stays: it catches the utility-import case earlier and more cheaply,
in review rather than in the suite. It is a first line, not the line.

**A dev tool kit shells out to — a scanner, a linter — is not a runtime dependency.** It is
installed as a tool and must never be added to `dependencies`; that mistake is what exposed
this gap.

```toml kit-enforce
[[forbid_import]]
import = "^(lodash|lodash-es|lodash\\.|underscore|ramda|axios|node-fetch|request|moment|dayjs|bluebird|jquery)"
Expand Down
77 changes: 77 additions & 0 deletions docs/adr/0006-utils-is-a-leaf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
id: ADR-0006
title: src/utils is a leaf — it depends on nothing in the repo
status: accepted
---

# ADR-0006: src/utils is a leaf

## Decision

No file under `src/utils/**` imports anything else in this repository. It may use Node
builtins and declared dependencies; it may not reach into `src/` — not a sibling
subsystem, not the root, not the command layer.

## Rationale

This was not decided in a meeting. It was **derived from the code by `kit adr derive`**
and then confirmed by measurement, which is the point: a constraint the whole repo has
obeyed is a decision whether or not anyone wrote it down.

Measured on the import graph at the time of writing:

| direction | edges |
|---|---|
| `commands` → `utils` | 113 distinct file pairs |
| `(root)` → `utils` | 86 |
| `utils` → **anything in `src/`** | **0** |

`kit adr derive` proposed three separate candidates from that asymmetry (`utils →
commands` at 113, `utils → adapters` at 8, `utils → memory` at 5). They are one rule:
`utils` imports **nothing**. Enforcing the general form is simpler than enforcing three
special cases, and it covers a subsystem that does not exist yet.

Why it matters beyond tidiness: `utils` is the most fanned-in directory in the repo.
Anything it imports is imported, transitively, by almost everything. A single upward
import from `utils` into a subsystem creates a cycle through the majority of the tree and
makes the affected modules untestable in isolation. Keeping the bottom of the stack at
the bottom is what makes the layers above it movable.

## Consequences

- A helper in `utils` that needs a subsystem's type or function does not belong in
`utils`. Move it to the subsystem, or push the dependency the other way (pass the value
in rather than reaching for it).
- The rule is stated as "no parent-relative import", which is broader than "no sibling
subsystem". That is deliberate: `../` from `src/utils/**` can only ever leave `utils`,
so the broad form has no false positives and needs no maintenance when a directory is
added.
- Superseding this is an ADR-level act. If `utils` genuinely needs to depend on something,
amend or supersede this file in the same PR — the gate will otherwise refuse the code
and cite this ADR.

## Scope of the evidence, stated honestly

This records that the repository behaves as if the decision were made, and that we have
now made it. It is a snapshot of the import graph, not a reconstruction of anyone's
intent — see `kit adr derive`'s own limits (TS/JS + Python relative imports, one source
root, top-level buckets).

Two related candidates were deliberately **not** accepted:

- **"No subsystem imports the command layer"** — true of all 16 subsystems, not just the
six above the evidence floor, but the general form (`paths = "src/*/**"`) fires on
`src/commands/adr-derive.test.ts`, where a test fixture *string* containing
`import "../commands/x.js"` is read as a real import by the text-level extractor. The
glob grammar has no negation, so "every subsystem except `src/commands/**`" cannot be
expressed today. Left underived rather than encoded wrong.
- **`profile` does not import `exec-broker`** (support 8) — a lone pair with no wider
pattern behind it. Insufficient evidence that it is a decision rather than an ordering
accident.

```toml kit-enforce
[[forbid_import]]
import = "^\\.\\./"
paths = "src/utils/**"
message = "src/utils is a leaf (ADR-0006) — it imports nothing from the rest of the repo; move the helper to the subsystem, or pass the value in"
```
Loading
Loading