Skip to content
Open
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
25 changes: 25 additions & 0 deletions docs/adr/0000-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# NNNN. Short title of the decision

- **Status:** Proposed
- **Date:** YYYY-MM-DD
- **Deciders:** _names or roles_

## Context

What is the problem we are solving? What forces are at play (technical, organizational, performance, etc.)? Keep this focused on facts that matter for the decision β€” link out to design docs or issues for deeper background.

## Decision

The choice we are making, stated in one or two sentences. Be specific enough that a reader can tell whether a future change conflicts with this ADR.

## Consequences

What follows from the decision? Cover both positive and negative effects. Include anything contributors or agents must do (or avoid) as a result.

## Alternatives considered

For each alternative, one or two sentences on what it was and why it was not chosen. Listing the rejected options is what makes an ADR worth reading later.

## References

- Related ADRs, design docs, issues, or external resources.
35 changes: 35 additions & 0 deletions docs/adr/0001-record-architecture-decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 0001. Record architecture decisions

- **Status:** Accepted
- **Date:** 2026-05-16
- **Deciders:** td maintainers

## Context

`td` has grown a number of architectural decisions that are only visible by reading the code, `CLAUDE.md`, or scattered specs under `docs/implemented/` and `docs/deprecated/`. New contributors β€” human and agent β€” repeatedly re-derive the same context, and there is no canonical place to record _why_ a choice was made or what alternatives were rejected.

Existing `docs/` content mixes specs, plans, guides, and post-hoc investigations. None of those formats are designed to be immutable, dated, and cross-linked the way decision records need to be.

## Decision

Adopt lightweight Architecture Decision Records (ADRs) stored in `docs/adr/`. ADRs use a MADR-flavored template (`0000-template.md`) with Status, Context, Decision, Consequences, and Alternatives sections. They are numbered sequentially, immutable once Accepted, and superseded rather than rewritten.

The `docs/adr/README.md` describes when an ADR is required and the workflow for proposing, accepting, and superseding one.

## Consequences

- Future architectural changes that meet the criteria in `docs/adr/README.md` must include or update an ADR.
- Agents and contributors have a single index (`docs/adr/README.md`) to consult before proposing structural changes.
- The `docs/implemented/` and `docs/plans/` directories continue to hold long-form specs; ADRs link out to them where useful but stay short.
- Some up-front cost retroactively recording existing decisions (ADRs 0002–0004 in this initial batch).

## Alternatives considered

- **Keep using free-form docs.** Rejected: no consistent structure, no clear status lifecycle, and nothing pins down rejected alternatives.
- **Use GitHub Discussions or issues for decisions.** Rejected: not versioned with the code, harder to discover from a checkout, and decays when issues are closed or archived.
- **Heavier ADR formats (Nygard, Y-Statements with full attributes).** Rejected: the MADR-lite shape is enough for a single-repo project and lowers the bar to writing one.

## References

- [MADR](https://adr.github.io/madr/) β€” Markdown Architecture Decision Records.
- Michael Nygard, ["Documenting Architecture Decisions"](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions).
39 changes: 39 additions & 0 deletions docs/adr/0002-sqlite-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 0002. SQLite as the issue store

- **Status:** Accepted
- **Date:** 2026-05-16
- **Deciders:** td maintainers

## Context

`td` is a per-project CLI for issue and session tracking that runs locally for developers and AI agents. It needs durable structured storage for issues, logs, handoffs, work sessions, boards, and review state. Concurrent access from a CLI, a TUI monitor, and embedded sidecar processes is common; network-attached databases would force every invocation through a server and break the offline, per-checkout model.

The storage layer also has to support relatively rich queries (filters, joins across issues/logs/handoffs, ordering by board position) and frequent schema evolution β€” `internal/db/schema.go` has already moved through 30+ schema versions.

## Decision

Use SQLite as the single source of truth for project state. The database lives at `<project>/.todos/issues.db`, scoped to the project directory and committed to no remote by default. Schema is defined in `internal/db/schema.go` with an integer `SchemaVersion` and idempotent migration logic; access goes through `internal/db/` helpers.

Cross-machine synchronization is layered on top via the sync subsystem (see `docs/sync-*-guide.md` and `docs/implemented/sync-plan-03-merged.md`), not by switching storage engines.

## Consequences

- Zero-config local install: `td` works on any machine with the binary; no external database to provision.
- Each project has an isolated DB; switching projects is just `cd`. This matches the way agents are scoped to a checkout.
- Multi-process writers (CLI + monitor + sidecar) must coordinate via WAL mode and short transactions; long-running readers should not block writers.
- Schema changes require bumping `SchemaVersion` and writing migrations; ad-hoc schema drift is not supported.
- Cross-host collaboration requires the sync layer; the DB file itself is not safe to share over network filesystems.
- An explicit decision to _not_ adopt a server DB (Postgres, Turso/libSQL) is recorded; see the deprecated `docs/deprecated/spec-turso-libsql-support.md` for the libSQL exploration and its outcome.

## Alternatives considered

- **Flat files (JSON / YAML per issue).** Rejected: poor query performance, painful concurrent edits, and weak guarantees on partial writes.
- **Embedded key-value store (BoltDB, Badger).** Rejected: relational queries across issues/logs/handoffs would be reimplemented by hand; SQLite gives them for free.
- **Server database (Postgres).** Rejected: breaks the offline, per-checkout model and adds a deployment dependency for every user.
- **libSQL / Turso as primary storage.** Explored and deprecated (`docs/deprecated/spec-turso-libsql-support.md`); kept SQLite local with optional sync layered on top instead.

## References

- `internal/db/schema.go` β€” schema and version constant.
- `docs/sync-client-guide.md`, `docs/sync-server-ops-guide.md` β€” sync layer that complements local SQLite.
- `docs/deprecated/spec-turso-libsql-support.md` β€” rejected libSQL plan.
49 changes: 49 additions & 0 deletions docs/adr/0003-delegated-review-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 0003. Delegated review policy

- **Status:** Accepted
- **Date:** 2026-05-16
- **Deciders:** td maintainers

## Context

`td` is used heavily by AI agents that can both implement and close their own work. Earlier versions enforced a strict review policy: the session that recorded any implementation activity on an issue could not also close it. That rule prevented unchecked self-review, but it also blocked legitimate orchestrator patterns where a parent agent delegates implementation to one sub-agent, review to another, and then performs the administrative close itself.

Two intermediate modes accumulated over time:

- `strict` β€” original behavior; no prior involvement of any kind is allowed when closing.
- `balanced` β€” strict, plus a creator-approval exception when a `--reason` is supplied.

Neither cleanly supported the orchestrator β†’ implementer β†’ reviewer β†’ orchestrator flow that AI-driven workflows actually use, and bolt-on exceptions made the policy hard to reason about. See `docs/implemented/spec-agent-review-bypass-prevention.md`, `docs/implemented/spec-balanced-review-policy.md`, and `docs/implemented/spec-delegated-review-closure.md` for the history.

## Decision

Introduce a third mode, `delegated`, controlled by the `review_policy_mode` setting (env `TD_FEATURE_REVIEW_POLICY_MODE` or `td feature set review_policy_mode delegated`). Under `delegated`:

- A review attestation must come from a session that did **not** participate in implementation.
- Once an independent approval is recorded (`td approve --record-only --reason ...`), **any** session β€” including the orchestrator that submitted for review β€” may perform the final close via `td approve --reason ...`.
- The closer is audited separately from the reviewer via `closed_by_session`.

Shared eligibility logic lives in `internal/reviewpolicy/`. `strict` and `balanced` remain available for backward compatibility; `delegated` is opt-in now and is expected to become the default in a future release.

## Consequences

- Orchestrator agents can drive the full lifecycle (`add β†’ start β†’ handoff β†’ review β†’ approve --record-only β†’ approve`) by delegating to sub-agents without faking new sessions just to satisfy the guardrail. `CLAUDE.md` explicitly forbids starting a new session mid-work for this reason.
- Audit trail remains intact: the independent reviewer is recorded, and the closer is recorded separately. A close by a non-reviewer requires `--reason`.
- Three modes increase surface area for configuration and tests; `internal/reviewpolicy/` centralizes the rules to keep them reviewable.
- Documentation and onboarding must explain which mode is active; `CLAUDE.md` is the canonical reference for agents.

## Alternatives considered

- **Stay on `strict` only.** Rejected: forces orchestrator agents into awkward workarounds (new sessions per phase) that obscure the real audit trail.
- **Make `balanced` the default and extend it.** Rejected: the creator-approval exception is a narrower carve-out than what delegated workflows need, and stacking more exceptions onto `balanced` would muddy its semantics.
- **Drop the review guardrail entirely for agents.** Rejected: the guardrail's purpose β€” preventing unchecked self-review β€” still holds. Delegation preserves the guarantee that an independent session reviewed the work.
- **Encode policy in each command.** Rejected: leads to drift between `td review`, `td approve`, and the monitor. The `internal/reviewpolicy/` package was introduced specifically to keep these aligned.

## References

- `CLAUDE.md` β€” "Review Model (Delegated Review)" section.
- `internal/reviewpolicy/` β€” shared eligibility logic.
- `docs/implemented/spec-agent-review-bypass-prevention.md`
- `docs/implemented/spec-balanced-review-policy.md`
- `docs/implemented/spec-delegated-review-closure.md`
- `docs/plans/orchestrator-review-closure-plan.md`
45 changes: 45 additions & 0 deletions docs/adr/0004-session-scoping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 0004. Session scoping by branch and agent

- **Status:** Accepted
- **Date:** 2026-05-16
- **Deciders:** td maintainers

## Context

`td` work happens across many concurrent contexts: a developer in a terminal, the TUI monitor, several AI agents (Claude Code, Cursor, codex, etc.) running in parallel, and short-lived sub-agents spawned by an orchestrator. Every implementation log, handoff, and review action must be attributable to a session β€” both for audit and for the [delegated review policy](0003-delegated-review-policy.md) to decide whether a reviewer is independent.

Two failure modes shaped the design:

- **Too coarse.** A single "current session" per machine collapses parallel agent work into one identity, so review independence cannot be enforced.
- **Too fine.** A fresh session per command shreds continuity; logs from a single piece of work scatter across many session IDs and handoffs lose their referent.

Earlier iterations also tried environment-variable based session IDs, which agents could trivially set or copy, defeating the guardrail.

## Decision

Sessions are DB-backed and scoped by the tuple `(git branch, agent fingerprint)`. The fingerprint is derived from the agent's stable parent process and type (see `internal/session/agent_fingerprint.go`); the branch comes from `git`. The first call from a given branch+agent reuses any existing session row; otherwise `td` creates one. `td session --new` forces a new session on the same branch+agent intentionally; the policy in `CLAUDE.md` is to **not** rotate sessions mid-work.

Session identity, lookup, and creation live in `internal/session/`. Sessions are stored in the project's SQLite DB ([ADR 0002](0002-sqlite-storage.md)).

## Consequences

- Parallel agents on the same checkout get distinct sessions automatically, even without any explicit naming β€” review independence checks can rely on session IDs.
- Switching branches naturally starts a new session, matching how feature work is organized.
- Sub-agents spawned with their own process tree (different PID parent) get their own fingerprint and thus their own session, which is what the delegated review flow assumes.
- Agents cannot fake a different session by setting an env var; they would need a different process tree or branch.
- The branch dimension means worktrees and branch-per-task workflows produce many sessions; this is intentional and lets the audit trail follow the work.
- `td session --new` exists for the rare case where an operator legitimately needs a fresh session; documentation warns against using it to bypass review rules.

## Alternatives considered

- **Per-process sessions.** Rejected: every CLI invocation would be its own session, destroying continuity for logs and handoffs.
- **Per-terminal (TTY-keyed) sessions.** Rejected: agents often run headless without a TTY, and a single TTY can host multiple agents.
- **Env-var driven session IDs (`TD_SESSION_ID`).** Rejected: trivially spoofable, which would let an agent pose as the reviewer of its own work.
- **Per-user sessions.** Rejected: too coarse for multi-agent workflows on one machine; would re-introduce the self-review problem the review policy is designed to prevent.

## References

- `internal/session/session.go` β€” `GetOrCreate`, `ForceNewSession`, branch+agent lookup.
- `internal/session/agent_fingerprint.go` β€” agent fingerprint derivation.
- `docs/implemented/proposal-session-identity.md` β€” original design discussion.
- [ADR 0003](0003-delegated-review-policy.md) β€” relies on session independence.
42 changes: 42 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Architecture Decision Records

This directory holds Architecture Decision Records (ADRs) for `td`.

## What is an ADR?

An ADR captures a single architectural decision: its context, the choice made, and the consequences that follow. ADRs are short, dated, and immutable once accepted β€” superseded decisions get a new ADR that links back to the original.

We use a lightweight [MADR](https://adr.github.io/madr/)-flavored format. See [`0000-template.md`](0000-template.md) for the template.

## When to write an ADR

Write an ADR when a change:

- Picks one approach over reasonable alternatives that other contributors might revisit later.
- Affects more than one package or crosses a public boundary (CLI, DB schema, sync protocol, sub-agent contracts).
- Codifies a policy that an agent or contributor is expected to follow (review rules, session scoping, storage location, etc.).

Tactical refactors, dependency bumps, and bug fixes do not need an ADR.

## Process

1. Copy `0000-template.md` to `NNNN-short-title.md`, using the next free number.
2. Fill in Status, Context, Decision, Consequences, and Alternatives. Keep it under ~300 lines.
3. Open a PR. Discuss the decision in the PR thread, not by rewriting the ADR repeatedly.
4. On merge, set Status to `Accepted`. If a later ADR overrides this one, update Status to `Superseded by NNNN` and add a link.

## Status values

- `Proposed` β€” under discussion in a PR.
- `Accepted` β€” merged and in effect.
- `Superseded by NNNN-...` β€” replaced by a later ADR.
- `Deprecated` β€” no longer in effect, but not directly replaced.

## Index

| # | Title | Status |
| ---- | -------------------------------------------------------------------- | -------- |
| 0001 | [Record architecture decisions](0001-record-architecture-decisions.md) | Accepted |
| 0002 | [SQLite as the issue store](0002-sqlite-storage.md) | Accepted |
| 0003 | [Delegated review policy](0003-delegated-review-policy.md) | Accepted |
| 0004 | [Session scoping by branch and agent](0004-session-scoping.md) | Accepted |