diff --git a/AGENTS.md b/AGENTS.md index 564a99d..a329191 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,9 +28,10 @@ the container lifecycle and the copying of skills and env vars into it. - `bot-bottle.json` — legacy manifest of named agents (env / skills / prompt per agent), consumed by `cli.py`. See "Manifest" under "Intended design". -- `docs/INDEX.md` — pointer to the research notes. -- `docs/prds/` — product requirement docs. -- `docs/research/` — research notes (empty for now, kept tracked via `.gitkeep`). +- `docs/README.md` — docs overview; when to write which document. +- `docs/prds/` — product requirement docs (see `docs/prds/README.md` for format). +- `docs/research/` — research notes (see `docs/research/README.md`). +- `docs/decisions/` — decision records (ADR-lite). ## Conventions diff --git a/docs/INDEX.md b/docs/INDEX.md deleted file mode 100644 index 0a359c6..0000000 --- a/docs/INDEX.md +++ /dev/null @@ -1 +0,0 @@ -Research notes live in `research/`. Product requirement docs live in `prds/`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..62d6cfa --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Docs + +How this project records what it builds and why — and a guide to +picking the right document for what you're capturing. + +## When to write which document + +| Artifact | For | +|---|---| +| **PRD** (`docs/prds/`) | A feature: what to build, scope, success criteria. | +| **Research note** (`docs/research/`) | A landscape/tradeoff investigation. | +| **Decision record** (`docs/decisions/`) | A decision that isn't itself a feature — a policy, a convention, a "we will / won't do this," or a load-bearing choice made inside a larger PRD that deserves to be discoverable on its own. | + +A decision that's fully specified by a PRD doesn't need duplicating in +a decision record. Write one when the *decision* would otherwise be +buried in prose, lost in an issue thread, or have no in-repo home at +all (small requests that don't merit a PRD; non-feature choices like +merge strategy or a trust posture). diff --git a/docs/decisions/0001-merge-prs-with-rebase.md b/docs/decisions/0001-merge-prs-with-rebase.md new file mode 100644 index 0000000..283472a --- /dev/null +++ b/docs/decisions/0001-merge-prs-with-rebase.md @@ -0,0 +1,47 @@ +# ADR 0001: Merge PRs with rebase, not merge commits + +- **Status:** Accepted +- **Date:** 2026-05-28 +- **Deciders:** didericis + +## Context + +PRs need a merge strategy. Gitea offers merge-commit, squash, rebase, +and rebase-merge. The project uses [Conventional +Commits](https://www.conventionalcommits.org/) enforced by a +`commit-msg` hook, and PRDs typically land as a multi-commit PR where +each commit is meaningful on its own (e.g. PR #95: a `docs(prd)` commit, +a `feat(manifest)` implementation commit, and a `docs(manifest)` +commit). The history should stay readable and the individual +conventional commits should survive onto `main`. + +## Decision + +Merge PRs with **rebase** (Gitea's `rebase` style; `Do: "rebase"` via +the API). The branch's commits are replayed onto `main` with no merge +commit, producing a linear history that preserves each commit verbatim. + +## Consequences + +- **Linear history**, no merge bubbles; `git log --oneline` reads as a + straight sequence of conventional commits. +- **Each commit is preserved** (unlike squash, which would collapse the + PRD/impl/docs commits into one and lose the staged structure). +- **Commit SHAs are rewritten at merge.** The replayed commits on `main` + get new SHAs, and the source branch is deleted, so a link to a file + by *branch name* (`/src/branch//…`) dies at merge. This is + why links to not-yet-merged files are pinned to a **commit SHA** + (`/src/commit//…`), which stays reachable via the retained + `refs/pull//head` ref. See + `docs/research/issue-tracking-vs-in-repo-decision-history.md`. +- **Trade-off accepted:** without a merge commit, the "these commits + landed together as PR #N" grouping is not recorded in git itself — it + lives in forge state (the PR). That is a mild concession against the + keep-history-in-the-repo posture; the conventional-commit scopes and + PRD references in the messages keep changes traceable without it. + +## Links + +- `docs/research/issue-tracking-vs-in-repo-decision-history.md` — the + commit-pinning consequence above. +- Observed practice: PRs #92, #93 merged with rebase; #95 to follow. diff --git a/docs/decisions/0002-agent-identity-claimed-not-vouched.md b/docs/decisions/0002-agent-identity-claimed-not-vouched.md new file mode 100644 index 0000000..556f267 --- /dev/null +++ b/docs/decisions/0002-agent-identity-claimed-not-vouched.md @@ -0,0 +1,48 @@ +# ADR 0002: Agent-set git identity is claimed, not vouched + +- **Status:** Accepted +- **Date:** 2026-05-28 +- **Deciders:** didericis + +## Context + +PRD 0027 lifts `git.user` (name/email) to the agent layer, so an agent +file may declare its own commit identity. Agent files can live in +`$CWD/.bot-bottle/agents/` — i.e. they can be supplied by a cloned, +less-trusted repository. That raises the question of whether a +repo-supplied agent setting its own git identity is a security concern, +and whether agent identity should be gated differently for `$CWD` +agents than for `$HOME` agents. + +This record exists because the decision is a **trust posture** worth +finding on its own, separate from the feature PRD that introduced it. +The full analysis lives in PRD 0027; the decision is summarized here. + +## Decision + +Allow agents to set `git.user`, and treat an agent-declared identity as +**claimed, not vouched**. No `$CWD`-vs-`$HOME` gating on the identity +field. `git.remotes` stays bottle-only (home-only). + +## Consequences + +- A cloned repo's agent file can present any commit author name/email, + including one that reads like a real person's. This is accepted: git + authorship is **not a credential** (push auth is the bottle's remote + key/token), is **already forgeable** from inside the bottle at runtime + (`git config user.email …`), and was never a trust anchor. +- If attribution integrity ever matters, the answer is commit + **signing** (SSH/GPG), not the author field — so this decision closes + no door that was open. +- `git.remotes` is deliberately *not* lifted to the agent layer: it + carries credentials and host trust (IdentityFile, KnownHostKey) and + remains a bottle-only, home-only concern. +- Revisit if a future change ever makes commit identity load-bearing + (e.g. enforced signing keyed on author), at which point gating + `$CWD`-supplied identities would matter. + +## Links + +- PRD 0027 (`docs/prds/0027-agent-git-user-identity.md`) — full trust + analysis and schema. +- Issue #94, PR #95 — the feature this decision was made for. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..5db1730 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,42 @@ +# Decision records + +Short, durable records of decisions — one file per decision. This is a +lightweight [Architecture Decision Record](https://adr.github.io/) +practice: capture *what was decided and why* in a versioned file so the +reasoning lives in the clone, not in a Gitea issue thread or a chat log +that disappears when the host does. + +See `docs/research/issue-tracking-vs-in-repo-decision-history.md` for +the rationale behind keeping decision history in-repo, and +[`docs/README.md`](../README.md) for when to write a decision record +vs. a PRD or research note. + +## Format + +One Markdown file per decision, numbered sequentially and zero-padded +(`0001-…`, `0002-…`), matching the PRD numbering style. Keep it short — +the discipline is writing it down, not the ceremony. + +```markdown +# ADR 0000: + +- **Status:** Proposed | Accepted | Superseded by ADR NNNN +- **Date:** YYYY-MM-DD +- **Deciders:** + +## Context +What forced the decision; the constraints in play. + +## Decision +What we decided, stated plainly. + +## Consequences +What follows — the good, and the costs/trade-offs accepted. + +## Links +PRDs, research notes, issues/PRs. Gitea links are convenience +pointers; the reasoning above must stand without them. +``` + +The records are the index: `ls docs/decisions/` or skim the titles. +No hand-maintained list to keep in sync. diff --git a/docs/prds/0025-bottle-extends.md b/docs/prds/0025-bottle-extends.md index ad19db4..413d1dd 100644 --- a/docs/prds/0025-bottle-extends.md +++ b/docs/prds/0025-bottle-extends.md @@ -39,6 +39,41 @@ trust boundary*: only `$HOME` bottles can declare it, only `$HOME` bottles can be its target. Cloned repos still cannot author bottle-equivalent config. +## Alternatives considered + +The question raised in issue #88 was *where composition should live*. +Three points in that design space, recorded here so the decision +stands on its own without the issue thread: + +1. **Duplicate bottles (status quo).** Copy `dev.md` to `staging.md` + and edit. Zero new mechanism, but every shared field drifts: a + route added to `dev` is silently missing from `staging`. This is + the pain that prompted #88. + +2. **Agent-side `bottle_config:` override (the original #88 + proposal).** Let an agent file carry an inline block that merges + over its referenced bottle. Ergonomically attractive — one file, + no second bottle — but it **breaks the trust boundary**: agent + files can come from `$CWD/.bot-bottle/agents/` in a cloned repo, so + a clone could redeclare egress routes, env mappings, and git + remotes — i.e. grant itself bottle-equivalent authority over + credentials and network egress. The home-only-bottle invariant + exists precisely to stop this. + +3. **Bottle-side `extends:` (chosen).** Move composition to the + bottle layer, where it inherits the home-only property for free: + only `$HOME` bottles can declare `extends:`, and only `$HOME` + bottles can be its target. Identical duplication relief to option + 2, none of its trust erosion. The cost is that an override requires + a (home-owned) child bottle rather than an inline agent block — + which is the *point*: the override authority stays in `$HOME`. + +`extends:` wins because it solves the duplication pain entirely on the +trusted side of the agent-vs-bottle boundary. (PRD 0027 later lifts a +deliberately narrow, non-credential field — `git.user` — to the agent +layer, on the separate reasoning that commit identity is not a +capability; egress, credentials, and remotes stay bottle-only.) + ## Goals / Success Criteria - Add `extends: ` to the bottle frontmatter schema. @@ -58,9 +93,9 @@ bottle-equivalent config. ## Non-goals -- **No agent-side `bottle_config:`.** That's the design issue #88 - considered and weighed against; this PRD is the alternative - picked in the issue's design discussion. Don't reintroduce it. +- **No agent-side `bottle_config:`.** Option 2 under "Alternatives + considered" — weighed and rejected on trust grounds. Don't + reintroduce it. - **No additive list merges** (e.g., `routes: append` keyword). The `extends:` design uses full-replace for list-valued fields (see "Merge rules"); if a use case shows up that genuinely @@ -167,7 +202,7 @@ Bottles continue to be loaded from `$HOME/.bot-bottle/bottles/` only (`Manifest.from_md_dirs` is unchanged). The `extends:` field references another file in that same directory. No cwd-readable file gains the ability to declare or modify bottle config — the -attack surface from issue #88's comment thread stays closed. +attack surface from option 2 ("Alternatives considered") stays closed. If a future change ever introduces cwd-loaded bottles, the `extends:` resolver should be gated to forbid a `$CWD` bottle diff --git a/docs/prds/README.md b/docs/prds/README.md new file mode 100644 index 0000000..a947199 --- /dev/null +++ b/docs/prds/README.md @@ -0,0 +1,63 @@ +# Product requirement docs + +One PRD per feature: what to build, why, and how it's scoped. The PRD +is the durable spec — it should stand on its own without a Gitea issue +thread (see [`../README.md`](../README.md) for when a PRD is the right +document vs. a research note or a decision record). + +## Naming and numbering + +`NNNN-kebab-title.md`, zero-padded and sequential (`0024-…`, `0025-…`). +Numbers are never reused; gaps are fine (there is no 0005). The number +is assigned at creation and stays fixed for the life of the doc. + +## Status + +The `Status:` line near the top tracks the PRD's lifecycle: + +- **Draft** — proposed, not yet shipped. +- **Active** — the design has shipped to `main` and is in effect. +- **Superseded by [PRD NNNN](…)** — replaced by a later PRD; kept for history. +- **Retargeted by [PRD NNNN](…)** — folded into a later PRD's scope. + +## Format + +```markdown +# PRD NNNN: + +- **Status:** Draft +- **Author:** +- **Created:** YYYY-MM-DD +- **Issue:** # # optional — convenience pointer only + +## Summary +One paragraph: what this builds and the pain it solves. + +## Problem +The current state and why it's inadequate. + +## Goals / Success Criteria +Bullets a reviewer can check the finished work against. + +## Non-goals +What this explicitly does not do — and won't, to head off scope creep. + +## Scope +In scope / out of scope, when the boundary needs spelling out. + +## Design +How it works: schema, data flow, diagrams, algorithms as needed. + +## Implementation chunks +Ordered, mergeable steps (optional; for multi-PR features). + +## Open questions +Unresolved decisions — resolve or fold into Design before shipping. +``` + +Sections are a guide, not a straitjacket: drop the ones a given PRD +doesn't need (a small change rarely needs Scope or Implementation +chunks) and add others where they help (e.g. Testing strategy, +Alternatives considered, References). Keep the rationale self-contained +— inline the reasoning rather than linking out to an issue thread, so +the PRD survives a move off Gitea. diff --git a/docs/research/README.md b/docs/research/README.md new file mode 100644 index 0000000..6e38e86 --- /dev/null +++ b/docs/research/README.md @@ -0,0 +1,42 @@ +# Research notes + +Investigations into a question or a design space — landscape surveys, +tradeoff analyses, "should we do X or Y," assessments of an approach +before (or instead of) committing it to a PRD. A research note is where +the *thinking* lives; a PRD is where a decided feature lives, and a +decision record is where a settled choice lives (see +[`../README.md`](../README.md) for picking between them). + +Notes are opinionated. They reach a conclusion rather than dumping a +neutral survey — the point is to move a decision forward and leave a +durable record of why it went the way it did. + +## Naming + +`kebab-case-topic.md`, named by subject and **not** numbered (unlike +PRDs and decision records). Pick a name that says what was +investigated: `bash-vs-python-vs-go.md`, `pipelock-assessment.md`, +`issue-tracking-vs-in-repo-decision-history.md`. + +## Shape (freeform) + +There's no fixed template — use whatever structure fits the question. +In practice most notes share a loose shape: + +- **Open with the question** — a sentence or two on what's being + investigated and why it came up. +- **Lead with the verdict** — a `## Summary` near the top stating the + conclusion, so a reader gets the answer without reading the whole + thing. +- **Then the analysis** — whatever the argument needs: comparison + tables, per-option sections, failure-mode walkthroughs, the axes that + actually matter. +- **End with a recommendation** when the note exists to drive a + decision. + +Keep the reasoning self-contained and grounded: cite sources, link +files and PRDs, and prefer concrete evidence from this repo over +generic claims — a note should stand on its own without a chat log or a +Gitea thread. When a note's recommendation gets acted on, capture the +resulting decision in a PRD or a decision record; the note stays as the +"why we looked into it," not the system of record for the choice. diff --git a/docs/research/issue-tracking-vs-in-repo-decision-history.md b/docs/research/issue-tracking-vs-in-repo-decision-history.md new file mode 100644 index 0000000..72a5803 --- /dev/null +++ b/docs/research/issue-tracking-vs-in-repo-decision-history.md @@ -0,0 +1,196 @@ +# Tracking feature requests in Gitea vs. in-repo decision history + +Research into whether bot-bottle should track feature requests (and the +decision-making around them) as Gitea issues, given that the project +already records specs in-repo as PRDs (`docs/prds/`) and rationale as +research notes (`docs/research/`). The stated constraint is that the +*history of why we decided things* should be durable and portable — +not locked into a single hosting provider (Gitea today, conceivably +GitHub or something else tomorrow). + +## Summary + +Keep using issues, but demote them. The repository — not Gitea — is +the system of record for any decision you would be unhappy to lose. +Issues are an excellent **inbox and coordination surface** (cheap +capture, triage, async discussion, notifications, auto-linking) and a +**poor archive** (provider-locked storage, brittle numeric references, +rationale stranded in comment threads). The failure mode to avoid is the +one already present in the repo: a PRD whose reasoning is only complete +if you also read a Gitea issue thread. + +The fix is a discipline, not a tool: **every load-bearing decision gets +reified into a versioned file in the repo before the issue that prompted +it is closed.** PRDs already do this for features; the gap is (a) small +requests that never merit a PRD and (b) decisions that aren't features +at all (e.g. "we merge with rebase," "author identity is claimed-not- +vouched"). Close that gap with a lightweight in-repo decision log. Then +issues can be as disposable as Gitea makes them, and migrating off +Gitea costs you triage state, not history. + +## Why this even comes up here + +The project already leans on the repo for durable artifacts: + +- **PRDs** (`docs/prds/0001…0027`) — the spec and its rationale. +- **Research notes** (`docs/research/`) — the "why," with tradeoffs. +- **Conventional-commit history** — a machine-greppable change log. + +But the issue layer has quietly become load-bearing in places: + +- PRD 0025 says it picked "option 3" *"from the #88 design + discussion"* and that the rejected alternative lives "in issue #88's + comment thread." The PRD's rationale is therefore **incomplete without + the issue**. If Gitea is gone, the strongest argument for the chosen + design is gone with it. +- PR #89's description links `…/didericis/claude-bottle/issues/88` — + the **pre-rename** repo path (the project was Codex-bottle/claude- + bottle before the bot-bottle rebrand). That link is already + half-dead: a concrete demonstration that Gitea URLs rot under the + most routine event imaginable, a rename. +- Issue/PR numbers (`#88`, `#90`, `#94`, `#95`) are **Gitea-assigned + from a shared sequence**. They cannot be reconstructed from a clone, + and they collide/renumber on import into a different tracker. + +So the question isn't academic. The current practice is already +producing references that don't survive a rename, let alone a migration. + +## What each medium is actually good at + +| Concern | Gitea issue | In-repo file (PRD / note / log) | +|---|---|---| +| Capture friction | Near-zero — file a one-line idea | High — a PRD is a heavy artifact; a note less so | +| Triage (labels, milestones, open/closed, assignee) | Native, good | Absent / hand-rolled | +| Async discussion + notifications | Native (threads, @mentions, watch) | None — needs a PR review or out-of-band chat | +| Auto-linking (`Closes #N`, PR↔issue, commit↔issue) | Native | Manual cross-reference | +| Version control of the content | None — lives in Gitea's DB | Full — diff, blame, branch, revert | +| Travels with `git clone` | No | Yes | +| Survives a move off Gitea | Degrades (export/import; threads, authors, timestamps, refs lossy) | Unaffected | +| Survives a Gitea outage | Inaccessible | Local clone has it | +| Greppable offline / by tooling | Only via API | `grep docs/` | +| Reproducible identifiers | Gitea-assigned numbers | Filenames you control (`0027-…`) | + +The split is clean: **issues win on the live, social, coordination axes; +the repo wins on every durability and portability axis.** Nothing about +that table says "pick one." It says "use each for what it's good at, and +don't let the durable thing depend on the ephemeral one." + +## Lock-in failure modes (the cons, concretely) + +1. **Stranded rationale.** The single most valuable output of a feature + discussion — *why we rejected the obvious alternative* — usually + emerges in a thread and dies there unless someone copies it into the + spec. PRD 0025 is already in this state. +2. **Reference rot.** `Closes #88` / "see issue #90" are meaningful only + against one Gitea instance at one point in time. A rename already + broke one such link; a migration would break all of them and + silently renumber the survivors. +3. **Two sources of truth.** A PRD carries `Status: Draft`; the issue + carries open/closed. They drift. Which is authoritative? +4. **Availability coupling.** Self-hosted Gitea down (or the Tailscale + path to it down) means the backlog and its history are unreachable, + even though the code and PRDs are right there in the clone. +5. **Export is lossy.** Gitea→GitHub (or the reverse) moves issue *text* + tolerably but mangles cross-references, comment authorship for + non-mapped users, timestamps, and reactions. The graph of "#88 → PR + #89 → commit abc" does not survive intact. + +None of these are arguments against *having* issues. They're arguments +against issues being the **only** place a decision is recorded. + +## Pros of keeping issues anyway + +Worth stating plainly, because "just use the repo for everything" +overcorrects: + +- A PR per half-formed idea is absurd; issues are the right weight for + "someone should look at X someday." +- Triage state (priority, milestone, assignee, open/closed) is genuine + project-management value the repo does not natively provide. +- Notifications and threaded discussion are how a decision *gets made* + before it's ready to be written down. Killing issues doesn't move that + conversation into the repo — it moves it into chat/DMs, which is + *worse* for durability, not better. +- `Closes #N` automation and PR↔issue linkage are real ergonomics. + +The goal is not to abandon the tracker. It's to make sure that when the +tracker eventually goes away, you lose the *backlog*, not the *history*. + +## What belongs where + +- **Gitea issue** — intake, triage, status, and the live discussion. + Treat it as a **cache**: useful now, expendable later. +- **PRD (`docs/prds/`)** — the durable spec for anything that warrants + one. Rule: a PRD must be **self-contained**. Synthesize the issue + discussion into the Problem / Design / Open-questions sections; + reference the issue as a convenience pointer, never as the only home + of a load-bearing argument. (Retrofit PRD 0025: inline the #88 + "option 3 vs `bottle_config:`" reasoning so the PRD stands alone.) +- **Research note (`docs/research/`)** — the durable "why," exactly like + this file. Comparative analysis, landscape surveys, tradeoffs. +- **Commit message** — the durable "what changed and why, at this point + in the diff." +- **Decision log (proposed, see below)** — durable record of decisions + that aren't features and don't merit a PRD. + +## Closing the gap: a portable decision record + +Two classes of decision currently have no in-repo home: + +- **Sub-PRD feature requests** — too small for a PRD, but you still want + a tracked "we will / won't do this, because." Today these live only as + issues. +- **Non-feature decisions** — "merge with rebase, not merge-commit," + "agent identity is claimed-not-vouched," "bottles are home-only." + Some land inside a PRD that happens to touch them; many are folded + into chat and lost. + +Options, cheapest first: + +1. **An ADR-lite log under `docs/decisions/`.** One short Markdown file + per decision: context, decision, consequences, date, links. This is + the industry-standard Architecture Decision Record pattern, and it's + a near-exact fit for "track decision history, portably." Numbered + like PRDs (`0001-merge-with-rebase.md`). ~10 lines each; the + discipline is writing them, not the format. +2. **Reuse the journal.** The repo ships an `init-entry` skill that + writes timestamped prose to `docs/JOURNAL.md` (not yet created here). + A stream-of-thought journal is a fine home for decision *narrative* + and is already part of the toolchain — lower ceremony than ADRs, less + structured for later retrieval. The `tag-entries` skill could tag + decision entries for grep-ability. +3. **Periodic issue export.** Belt-and-suspenders: a scheduled job hits + the Gitea API and dumps open/closed issues + comments to JSON under + `docs/issues-archive/`, committed. Preserves the raw thread against + losing Gitea without changing daily workflow. Mechanical, not a + substitute for reifying rationale (a JSON dump of a thread is + evidence, not a decision). + +These compose: ADRs/journal for the *decision*, optional export for the +*raw evidence*, issues for *live coordination*. + +## Recommendation + +1. **Keep Gitea issues for intake, triage, and discussion.** Don't fight + Gitea on the things it's good at. +2. **Make the repo the system of record.** Adopt the rule: no decision + is "done" until its rationale exists in a versioned file (PRD, + research note, or decision log). The issue is a pointer, never the + sole source. +3. **Add `docs/decisions/` (ADR-lite).** Smallest change that closes the + real gap — sub-PRD requests and non-feature decisions. Start by + back-filling the few decisions already made only in threads or chat + (rebase-merge policy; the agent-identity trust call from PRD 0027). +4. **Retrofit PRD 0025** to inline its #88 rationale, removing the one + existing hard dependency on a Gitea thread. +5. **Treat issue numbers as disposable.** When a PRD/commit cites an + issue, ensure the cited content is mirrored in-repo so the citation + degrades to a dead-but-harmless link, not lost information. (The + already-broken `claude-bottle/issues/88` link is the warning.) +6. **Optional:** automate a Gitea issue export into the repo if you want + the raw threads preserved without manual transcription. + +Net: issues stay, because the alternative to issues is chat, which is +worse. But the project's durable memory must live where the project +already lives — in the clone — so that moving off Gitea, or losing it, +costs you a backlog you can rebuild, never a history you can't.