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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- **An optional `INBOX.md`: what was handed to this project from outside, not yet triaged** ([ADR 0048](docs/decisions/0048-the-convention-here-the-transport-elsewhere.md)). Offered by `bootstrap` and `adopt`, unchecked by default, at the repository root: one dated line per subject, **verbatim**, and triage **marks** a line (`→ ADR 0012`, `→ PLAN.md`, `→ dropped`) rather than deleting it — the file is a record of what arrived, not a queue that empties. It is named **first** in the generated `CLAUDE.md`'s session-start list, before `PLAN.md`, because what arrived can change what the rest of the list means; no conditional was needed, the list already marks optional entries *(if present)*. **The plugin never fills it** — how lines get there is the caller's business, described nowhere here.
**The request that produced it asked for something else, and was refused.** It wanted a `SessionStart` hook shipped by the bootstrap, resolving a project slug and reading `01-projects/<slug>/inbox.md` from a private store through `VAULT_ROOT`. That would have been **the first executable this plugin ever generated** (verified: no `.sh`, no hook, nothing runtime in any template), reopening [ADR 0025](docs/decisions/0025-no-runtime-hook-no-watch.md) — *"a runtime hook is machinery against groundrules' nature… pure Markdown + JSON, no runtime"* — eight days after the same line was applied against this repository's own convenience, when an index check went to CI rather than to a `PostToolUse` hook. And it would have put **one estate's private layout into a public plugin**: `VAULT_ROOT` makes the root configurable, `01-projects/<slug>/inbox.md` is still a shape that exists in one person's store. That is the September lesson returning in a new form. The refusal held and the request was rewritten to the counter-proposal — **the convention here, the transport elsewhere** — which is the part of this worth reusing beyond inboxes.

### Changed
- **`AGENTS.md` investigated and deliberately deferred to M2** ([ADR 0047](docs/decisions/0047-agents-md-deferred-to-m2.md); **nothing in the plugin changes**). The open spec — OpenAI-led, donated to the Linux Foundation, 60k+ repositories — is a single predictable file of project context for AI agents, and this plugin had **no mention of it anywhere** while `docs/ROADMAP.md` M2 states the harness-neutral direction outright. The spec says nothing about coexisting with tool-specific files, so the behaviour was **measured rather than read** (Claude Code 2.1.280, three throwaway directories, a fresh `claude -p` with read tools disabled so the answer comes from what was *loaded*): `AGENTS.md` alone is read; `AGENTS.md` **beside** a `CLAUDE.md` is **ignored entirely**; a `CLAUDE.md` containing `@AGENTS.md` loads both. **That kills the cheapest-looking option** — a pointer would be read by no Claude Code session while looking installed, the failure class logged twice this week. Two further measurements are recorded so M2 does not re-derive them: the generated `CLAUDE.md` is **~86% harness-neutral**, its Claude-bound lines clustering in three identifiable blocks rather than diffusing; and `CLAUDE.md` is referenced **77 times across 10 skills** plus 10 templates, which is what makes a rename disproportionate today. The recommendation carried forward is the **`@AGENTS.md` import**, not a pointer and not a rename. One argument found along the way **does not need M2**: the methodology's entry points are written *as commands* (*"decided → `/groundrules:add-adr`"*), meaningless to another tool or to a human without the plugin — splitting would force the portable half to state the **act and the place**, which is a better generated file regardless.
- **The ADR index invariant becomes a check** (`test/check-adr-index.sh`, **dogfood only**). On 2026-09-21 an ADR landed on `main` without its row in `docs/decisions/README.md` and survived, because the only thing guarding that invariant was a line in the release checklist — read at release, days after the omission. A set comparison has exactly one correct answer, so it is a check, not a reminder, and not an eval case: an eval measures whether configuration changes behaviour, which is the wrong instrument for *is every file listed*. The script compares both directions — a file with no row, a row with no file — and flags duplicates, since a row listed twice reads as agreement until someone diffs them. Deterministic, offline, zero tokens; verified to fail as well as pass. **It runs on every pull request** (`.github/workflows/adr-index.yml` — one workflow, one job, one step; the stack is pure Markdown and JSON, so CI is a real surface and stays the size of the invariant it guards). The first placement considered was a git `pre-commit` hook, rejected on a mix of one sound reason and one that turned out false — **corrected the same day, see below**. `core.hooksPath` on the maintainer's machine does point at `ggshield`'s directory, and that setting *replaces* the hooks directory rather than adding to it, so **git itself** never looks in `.git/hooks`. But the occupying script **calls the repository's hook and fails the commit if it fails**, so a local hook would have run after all. CI is the better instrument rather than the fallback: it travels with the clone, `--no-verify` cannot walk past it, and it fires at the **pull request**, which is where the omission landed. **Proven both ways** before shipping — a throwaway PR removing an index row went red naming the file, restoring the row turned the same workflow green. A workflow whose red has never been seen is a badge. The lesson generalises in `docs/LEARNINGS.md`, sharpened by the correction: **read what occupies the slot — occupancy is not a second question, it answers the first.** Whether your control runs is a property of the occupant's *code*, not of the configuration pointing at it; here the configuration said *unreachable* and the script said *I call it myself*. Skipping that read cuts the other way too: writing into the `ggshield` directory itself, where the entries are symlinks into `dotfiles` beside a `.bak-` from a previous overwrite, **would** have destroyed a working guard while reading as installed.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The flow: `intake/` (raw, untouched) → `docs/` (synthesized, living) → `docs
| `docs/I18N.md` | Multilingual project | Supported languages and fallback, translation organization, localized formats, translation process. |
| `docs/PROCESS.md` | Phased way of working | The working-method contract: phases, validation gates, interview style, where artifacts live. Claude follows it and never skips a phase. |
| `RELEASE.md` | Project deploys somewhere | Operational release runbook: TL;DR commands, environments table, pre-release checklist, secrets, rollback, known fragilities. |
| `INBOX.md` | Work arrives from outside | **What was handed to this project** and is not yet triaged — a colleague, a ticket, another repository, a note made away from the desk. One dated line per subject, verbatim; triage marks it, never deletes it. Named **first** in the generated `CLAUDE.md`'s session-start list, so a session reads what arrived before deciding what to do. groundrules gives it a name and a place and **never fills it**: how lines get there is yours. |
| `docs/AGENT-EVALS.md` | Long agent-driven project | A log of the **agent's own** failure modes here (recurring mistakes, hallucinations, drifts) + the guard added for each. Distinct from `LEARNINGS.md` (project/domain). Fed by the checkpoint-capture ritual (before a push/release). |
| `docs/ADOPTION-LOG.md` | `adopt`, opt-in | A dated, frozen record of the adopt run — what was here, what groundrules did (and why) — with a **Remarks** section to annotate and **share back to improve groundrules**. A field → plugin feedback channel. |
| `loop/` + `## Invariants` in `CLAUDE.md` | Opt-in (off by default) | **Loop scaffolding**: `maker.md`/`verifier.md` (an independent verifier that distrusts the maker's report — the runner spawns it as a **separate** invocation with a fresh context, so the independence is enforced rather than requested, [ADR 0044](docs/decisions/0044-runner-enforces-verifier-isolation.md)), `LOOP.md`, a capped `run-loop.sh` (hard iteration ceiling), and `backlog.md` (loop-safe tasks only). Adds an `## Invariants` section the verifier enforces each iteration, and a **triage convention** for `loop/blocked.md` (the backward crossing: re-decompose / decide → ADR / fix interactively, with a `Resolution:` audit trail). **Not offered when superpowers is detected** (it defers the realization). |
Expand Down
101 changes: 101 additions & 0 deletions docs/decisions/0048-the-convention-here-the-transport-elsewhere.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!-- generated-by: groundrules v1.12.0 -->
# 0048 — An optional `INBOX.md`: the plugin ships the convention, the caller ships the transport

**Date**: 2026-09-29
**Status**: Accepted

## Context

Work reaches a project from outside — a colleague, a ticket, another repository, a note somebody
made away from the desk — before anyone in the project has looked at it. The generated `CLAUDE.md`
had no named place for that, so it landed in a chat message, in a commit body, or nowhere.

The request that surfaced it (`intake/2026-09-29-a-session-reads-its-project-inbox.md`) asked for
something else: a **`SessionStart` hook shipped by the bootstrap**, resolving a project slug,
reaching a private document store through `VAULT_ROOT`, reading `01-projects/<slug>/inbox.md` and
printing its unpicked lines. **It was refused**, on two grounds this repository had already paid
for, and rewritten to what this ADR records.

**It would have been the first executable the plugin ever generated.** Verified: no `.sh`, no hook,
nothing runtime in any template today. [ADR 0025](0025-no-runtime-hook-no-watch.md) refused that
class in its own words — *"a runtime hook is machinery against groundrules' nature (ADR 0002
'template over code'; offline-first; 'pure Markdown + JSON, no runtime')"* — and added that a hook
coupled to one harness's format does not port, which
[ADR 0047](0047-agents-md-deferred-to-m2.md) reaffirmed six days earlier for generated output. The
same line had been applied against this repository's own convenience a week before, when an index
check was sent to CI rather than to a `PostToolUse` hook; a line that holds only when it costs
someone else is not a line.

**And it would have put one estate's private layout into a public plugin.** `VAULT_ROOT` makes the
*root* configurable; `01-projects/<slug>/inbox.md` is still a shape that exists in exactly one
person's store. A bootstrapped project elsewhere would have received a hook reading a directory
convention the plugin documents nowhere. That is the September lesson — an ADR rebuilt from scratch
and a brief neutralised to remove exactly this — returning in a new form.

## Decision

**1. An optional `INBOX.md` at the repository root**, offered by `bootstrap` (Call 2b) and `adopt`,
unchecked by default: *what was handed to this project from outside, dated, verbatim, not yet
triaged.* Capture is free; deciding where a line belongs happens later. Triage **marks** a line
(`→ ADR 0012`, `→ PLAN.md`, `→ dropped`) rather than deleting it: the file is a record of what
arrived, not a queue that empties.

**2. It is named first in the generated `CLAUDE.md`'s session-start list**, before `PLAN.md` —
what arrived can change what the rest of the list means. It needs no conditional: the list already
marks optional entries *(if present)*, so one line serves both cases and nothing has to be dropped
at generation time.

**3. The plugin never fills it.** How lines arrive — typed, pasted, or written by a script of the
user's own — is the caller's business and is described nowhere in the plugin. **This is the whole
decision**: the convention is general and belongs here; the transport is particular and belongs to
whoever has a particular source. The estate that asked for this keeps its hook, in its own
dotfiles, bound to its own layout.

**4. Nothing executable, nothing naming a harness or a store.** The plugin still ships zero
runtime.

## Alternatives considered

- **Ship the `SessionStart` hook as asked** — refused, per the two grounds above. Reopening ADR
0025 for a convenience, and re-importing a private layout eight days after removing it, would
each have been a bad trade on its own.
- **Ship the hook but make the inner path configurable too** — rejected: a second environment
variable would have hidden the coupling rather than removed it, and the plugin would still be
shipping an executable it cannot test on any machine but one.
- **Reuse `intake/` instead of a new file** — tempting, and wrong. `intake/` is *upstream material
gathered before the work starts* — briefs, specs, transcripts — and its README says so. An inbox
is a **live surface** that fills during the project's life and is triaged line by line. Folding
them would blur a folder that is currently unambiguous, for one fewer file.
- **Make it non-optional** — rejected: a project where every input passes through the person
running the session gains an empty file and a line of ritual. ADR 0006's opt-in shape applies.

## Consequences

### Positive
- Work handed to a project has a named place, and a session is told to read it **first**.
- The refusal is recorded with its reasoning, so the next request of this shape is a lookup rather
than a re-derivation.
- The split — convention here, transport there — is stated as a principle, which is the part that
generalises beyond inboxes.

### Negative / Tradeoffs
- **A named place is not a filled one.** Nothing guarantees a line reaches `INBOX.md`; the plugin
deliberately does not provide the mechanism that would. For a caller with no transport, this is a
file that stays empty — which is why it is optional and unchecked by default.
- **The session-start list is a reminder, not a check** — the distinction this repository keeps
relearning. It relies on the agent reading a loaded file. That is what a Markdown-only plugin can
offer; anything stronger is runtime, and runtime is what ADR 0025 declined.
- One more optional doc in Call 2b, which keeps growing.

### Neutral
- Projects that decline it are unchanged, and the generated `CLAUDE.md` gains one line either way.

## Notes

- Source: `intake/2026-09-29-a-session-reads-its-project-inbox.md`, second version. Its header
records the refusal of the first, which is why the file is worth reading before this ADR.
- Related: [ADR 0025](0025-no-runtime-hook-no-watch.md) (no runtime shipped),
[ADR 0006](0006-optional-specialized-docs.md) (the opt-in shape),
[ADR 0047](0047-agents-md-deferred-to-m2.md) (output stays harness-neutral),
[ADR 0022](0022-agent-evals-and-session-close.md) (`AGENT-EVALS.md`, the closest precedent for
adding an optional doc).
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ No ADR needed for trivial choices or implementation details.
| [0045](0045-the-lock-was-scoped-to-four-skills.md) | `disable-model-invocation` applied per skill: five methodology skills open, five destructive ones stay locked (refines 0003) | Accepted | 2026-09-21 |
| [0046](0046-orchestrated-working-regime.md) | Orchestrated regime: the request for work carries the authorisation for the commit (amends 0036's commit rule) | Accepted | 2026-09-21 |
| [0047](0047-agents-md-deferred-to-m2.md) | `AGENTS.md` deferred to M2, with the harness behaviour measured now (a pointer is read by nothing) | Accepted | 2026-09-23 |
| [0048](0048-the-convention-here-the-transport-elsewhere.md) | Optional `INBOX.md`: the plugin ships the convention, the caller ships the transport (a `SessionStart` hook was refused) | Accepted | 2026-09-29 |
28 changes: 28 additions & 0 deletions intake/2026-09-29-a-session-reads-its-project-inbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# A project has an inbox for what is handed to it from outside — a convention, no runtime

> From `gitlab:lozit/cockpit`, 2026-09-29. First version refused by the groundrules session the same
> evening, on ADR 0025 (no runtime hook shipped by the plugin) and on the rule that the plugin never
> describes one estate's private layout. Rewritten to the counter-proposal: **groundrules ships the
> convention, the estate ships the transport.**

## The need, generally stated

Work reaches a project from outside — a colleague, a ticket, another repository, a person's notes —
before anyone in the project has looked at it. Today the generated `CLAUDE.md` has no named place
for that, so it lands in chat, in a commit message, or nowhere. A session that starts should find
what was handed to the project since last time, without a human carrying it.

## What is to be obtained

1. An optional `INBOX.md` proposed by the bootstrap: *what was handed to this project from outside,
dated, verbatim, not yet triaged*. Capture is free; deciding where a line belongs happens later,
in the session, not at capture.
2. The generated `CLAUDE.md` names it in its "Session start — read first" list, before the plan
and the decisions: read it, triage what is there, leave nothing silently.
3. Nothing executable, nothing that names a harness, a vault, or a filesystem layout. How lines get
*into* `INBOX.md` is each estate's business (a hook, a script, a person) and is not described
here.

Acceptance: a repository bootstrapped with `INBOX.md` sees it named at session start in its
`CLAUDE.md`; a repository bootstrapped without it is unchanged; the plugin still contains no
executable and no reference to any particular estate.
1 change: 1 addition & 0 deletions skills/adopt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Pre-check based on the scan. Only offer what **doesn't already exist**:
- `docs/PROCESS.md` — pre-check if a process/method doc is detected (phased workflow, validation gates)
- `RELEASE.md` — pre-check if CI/CD or hosting config is detected (`.gitlab-ci.yml`, `.github/workflows/`, `netlify.toml`, `vercel.json`…)
- `docs/AGENT-EVALS.md` — offered (unchecked by default): a log of the agent's observed failure modes (mistakes, hallucinations, drifts) + the guard added. Distinct from `LEARNINGS.md`.
- `INBOX.md` (root) — offered (unchecked by default): what reaches this project from outside, dated and verbatim, not yet triaged. Worth offering on a brownfield project, where work routinely arrives from people who are not the one running the session. groundrules never fills it.

Skip an individual option only if that exact file already exists (then list it under "adopted", not here). If **none** of these exist yet, the full list is shown with the detected ones pre-checked.

Expand Down
Loading
Loading