decision: move the ADR tooling into its own Go module, or keep it in madtea and improve it? #643

Open
opened 2026-09-28 18:16:43 +00:00 by hexajon · 0 comments
Owner

What happens

I want madtea's adr commands to keep agents aligned with recorded decisions. Today they do not. Agents reopen decided questions and redesign decided features.

I reviewed the points in my own agent sessions where I stopped or corrected an agent, from 2026-08-21 to 2026-09-22. At 10 of those points, an agent proposed or asked about something that a written decision already settled.

  • 9 of the 10 happened while the agent designed, planned, or answered, before any file changed. The other one was a shell command.
  • In 4 of the 10, and probably a fifth, the agent put the settled question to me through Claude Code's AskUserQuestion tool.
  • In the 2 cases I read in full, the decision was one file read away. The agent did not read it before it proposed.
  • The 10 decisions lived in six kinds of places: ADR trees, a standards document, design documents, issues, a platform pattern, and an ADR folder at a non-standard path.
  • None of the 10 happened at merge time.

The adr commands check the format of records. No part of madtea shows a relevant decision to an agent when the agent proposes or asks. The planned list, view, and rebuild actions (#507, #508) do not change that, because an agent must choose to call them. The last section lists checks that could show decisions to agents at those points.

Records this touches

Option 1 below forks two records, and it needs a new record that states both forks:

  • 0014 puts all business logic in internal/service, under two thin surfaces. Option 1 moves the ADR logic to an outside module. Both surfaces stay thin over it.
  • 0008 makes madtea's operation registry the source of record for the help and metadata of every operation. If the module exports its own action table, the source of record for the adr operations moves to the module.

Records 0034, 0036, and 0037 decide the format and the checks of the current tool.

The code today

  • internal/service/adr holds the logic in adr.go, decision_issues.go, and origin.go, about 2,060 lines. Its madr package holds the embedded MADR templates and their licenses.
  • internal/cmd/adr, internal/format/adr, and internal/mcp/tools_adr.go are the CLI and MCP wrappers, about 440 lines.
  • Two features need the forge. adr new numbers a record from the records on origin's default branch (ResolveOriginNames). adr check reads the state and comments of each linked issue (fetchIssue).
  • internal/service/adr imports five madtea packages: config, gitea, gitexec, service, and service/git. They serve those two features, the repo lookup (resolveOwnRepo), and two local git reads: user.name and madtea.adr.topLevelRepo.
  • The tests already replace those calls through package variables: fetchIssue, resolveOwnRepo, resolveTopLevelRepo, resolveOriginDefaultBranch, and newOriginServices.

Option 1: move the ADR tooling into its own Go module

The new module holds the MADR rules, the checks, the templates, the checks in the last section, and its own records and issues. madtea imports it as a library. madtea adr and madt_adr stay in madtea and call the library. madtea gives the library its forge access through one interface:

// Forge is what the library needs from a forge. madtea implements it.
type Forge interface {
    OwnRepo(ctx context.Context) (string, error)
    DefaultBranch(ctx context.Context) (string, error)
    ListDir(ctx context.Context, branch, dir string) ([]string, error)
    Issue(ctx context.Context, repo string, n int) (Issue, error)             // state and comments
    CloseIssue(ctx context.Context, repo string, n int, comment string) error // for decide, #495
}
  • Good, because a rule change ships as a release of the module, and madtea takes it with a version bump in go.mod. #642 made a Decision issues: list mandatory in every record, which broke every existing MADR tree. Under this option, that break is a module release, not a madtea release.
  • Good, because madtea stays a Forgejo/Gitea tool. Forge writes and credentials stay in madtea, behind madtea's forge write guards.
  • Good, because a new action can reach madt_adr with only a version bump. tools_adr.go already defines the actions as a table (adrActions) and a handler map (adrHandlers). If the module exports that table, madtea can register it. Each action must still pass madtea's parity test.
  • Bad, because the new repo needs a name, a module path, a forge home, releases, and a gate.
  • Bad, because a change to the interface is a breaking release of the module, and madtea must change with it. decide (#495) is one such change, because it closes issues.
  • Bad, because the move does not align any agent by itself. It only gives the checks in the last section a separate home.
  • Open: whether the module's own CLI gets forge access of its own, or works only offline and leaves the forge features to madtea. This is a credential question.

Two orders are possible:

  1. Cut in place, then move. Replace the five imports with the interface, and add a test that fails when internal/service/adr imports any madtea.lol/internal package. Then move the package to the new repo. The first step passes the madtea gate by itself. The gate's adr check step keeps working through both steps, because madtea adr check stays.
  2. New repo first. Copy the code, then cut the boundary there. madtea depends on a module with no release during the move.

Option 2: keep the ADR tooling in madtea and improve it

  • Good, because it needs no new repo, name, module path, or release process.
  • Good, because records 0008 and 0014 stay as they are.
  • Bad, because every rule change stays a madtea release.
  • Bad, because the checks in the last section are not forge work. madtea's scope then grows past Forgejo/Gitea integration.

Checks that show decisions to agents, in either option

The 10 cases show where a check could have run:

Check Cases it could reach What it needs
A hook on AskUserQuestion that matches the question against accepted records 4, probably 5 a matcher
A hook when the agent's turn ends, matched against the options that accepted records rejected 2 the rejected options written in records
A hook before a shell command 1 the decision moved from an issue into a record
One place for decisions in each repo 2 a migration, partly by hand
  • MADR lists the options under "Considered Options" and names the chosen one under "Decision Outcome". The options that were not chosen are the ideas that should not come back. adr check requires the "Considered Options" section today, but reads nothing from it.
  • A record's "Confirmation" section can name a test. My recent records 0038, 0041, and 0043 each name one. adr check does not read that section. If the check required the named test to exist, and the test to cite the record, a reversed decision would fail a test that names the record.
  • The matcher is not measured. The 10 cases can be its first test set: run a candidate matcher on their question and proposal text, and count how many find the right record.
  • Text in an agent's context is often ignored. In the same review, agents broke rules that were loaded into every one of their sessions. A blocking hook is stronger, but a false match blocks a valid question.
  • Each agent client needs its own hooks. #561 ports the existing guard hooks to pi, and each new hook needs the same port.
  • A record can be read too broadly. In one case an agent cited an accepted record to defend a hardcoded path, and I had to correct it. A tool can check a record's structure, but not the decision in it.

Open question

Which option: move the ADR tooling into its own Go module, or keep it in madtea and improve it?

Related: #488, #495, #507, #508, #528, #632, #634, #638, #639, #642.

## What happens I want madtea's `adr` commands to keep agents aligned with recorded decisions. Today they do not. Agents reopen decided questions and redesign decided features. I reviewed the points in my own agent sessions where I stopped or corrected an agent, from 2026-08-21 to 2026-09-22. At 10 of those points, an agent proposed or asked about something that a written decision already settled. - 9 of the 10 happened while the agent designed, planned, or answered, before any file changed. The other one was a shell command. - In 4 of the 10, and probably a fifth, the agent put the settled question to me through Claude Code's `AskUserQuestion` tool. - In the 2 cases I read in full, the decision was one file read away. The agent did not read it before it proposed. - The 10 decisions lived in six kinds of places: ADR trees, a standards document, design documents, issues, a platform pattern, and an ADR folder at a non-standard path. - None of the 10 happened at merge time. The `adr` commands check the format of records. No part of madtea shows a relevant decision to an agent when the agent proposes or asks. The planned `list`, `view`, and `rebuild` actions (#507, #508) do not change that, because an agent must choose to call them. The last section lists checks that could show decisions to agents at those points. ## Records this touches Option 1 below forks two records, and it needs a new record that states both forks: - 0014 puts all business logic in `internal/service`, under two thin surfaces. Option 1 moves the ADR logic to an outside module. Both surfaces stay thin over it. - 0008 makes madtea's operation registry the source of record for the help and metadata of every operation. If the module exports its own action table, the source of record for the `adr` operations moves to the module. Records 0034, 0036, and 0037 decide the format and the checks of the current tool. ## The code today - `internal/service/adr` holds the logic in `adr.go`, `decision_issues.go`, and `origin.go`, about 2,060 lines. Its `madr` package holds the embedded MADR templates and their licenses. - `internal/cmd/adr`, `internal/format/adr`, and `internal/mcp/tools_adr.go` are the CLI and MCP wrappers, about 440 lines. - Two features need the forge. `adr new` numbers a record from the records on origin's default branch (`ResolveOriginNames`). `adr check` reads the state and comments of each linked issue (`fetchIssue`). - `internal/service/adr` imports five madtea packages: `config`, `gitea`, `gitexec`, `service`, and `service/git`. They serve those two features, the repo lookup (`resolveOwnRepo`), and two local git reads: `user.name` and `madtea.adr.topLevelRepo`. - The tests already replace those calls through package variables: `fetchIssue`, `resolveOwnRepo`, `resolveTopLevelRepo`, `resolveOriginDefaultBranch`, and `newOriginServices`. ## Option 1: move the ADR tooling into its own Go module The new module holds the MADR rules, the checks, the templates, the checks in the last section, and its own records and issues. madtea imports it as a library. `madtea adr` and `madt_adr` stay in madtea and call the library. madtea gives the library its forge access through one interface: ```go // Forge is what the library needs from a forge. madtea implements it. type Forge interface { OwnRepo(ctx context.Context) (string, error) DefaultBranch(ctx context.Context) (string, error) ListDir(ctx context.Context, branch, dir string) ([]string, error) Issue(ctx context.Context, repo string, n int) (Issue, error) // state and comments CloseIssue(ctx context.Context, repo string, n int, comment string) error // for decide, #495 } ``` - Good, because a rule change ships as a release of the module, and madtea takes it with a version bump in `go.mod`. #642 made a `Decision issues:` list mandatory in every record, which broke every existing MADR tree. Under this option, that break is a module release, not a madtea release. - Good, because madtea stays a Forgejo/Gitea tool. Forge writes and credentials stay in madtea, behind madtea's forge write guards. - Good, because a new action can reach `madt_adr` with only a version bump. `tools_adr.go` already defines the actions as a table (`adrActions`) and a handler map (`adrHandlers`). If the module exports that table, madtea can register it. Each action must still pass madtea's parity test. - Bad, because the new repo needs a name, a module path, a forge home, releases, and a gate. - Bad, because a change to the interface is a breaking release of the module, and madtea must change with it. `decide` (#495) is one such change, because it closes issues. - Bad, because the move does not align any agent by itself. It only gives the checks in the last section a separate home. - Open: whether the module's own CLI gets forge access of its own, or works only offline and leaves the forge features to madtea. This is a credential question. Two orders are possible: 1. Cut in place, then move. Replace the five imports with the interface, and add a test that fails when `internal/service/adr` imports any `madtea.lol/internal` package. Then move the package to the new repo. The first step passes the madtea gate by itself. The gate's `adr check` step keeps working through both steps, because `madtea adr check` stays. 2. New repo first. Copy the code, then cut the boundary there. madtea depends on a module with no release during the move. ## Option 2: keep the ADR tooling in madtea and improve it - Good, because it needs no new repo, name, module path, or release process. - Good, because records 0008 and 0014 stay as they are. - Bad, because every rule change stays a madtea release. - Bad, because the checks in the last section are not forge work. madtea's scope then grows past Forgejo/Gitea integration. ## Checks that show decisions to agents, in either option The 10 cases show where a check could have run: | Check | Cases it could reach | What it needs | |---|---|---| | A hook on `AskUserQuestion` that matches the question against accepted records | 4, probably 5 | a matcher | | A hook when the agent's turn ends, matched against the options that accepted records rejected | 2 | the rejected options written in records | | A hook before a shell command | 1 | the decision moved from an issue into a record | | One place for decisions in each repo | 2 | a migration, partly by hand | - MADR lists the options under "Considered Options" and names the chosen one under "Decision Outcome". The options that were not chosen are the ideas that should not come back. `adr check` requires the "Considered Options" section today, but reads nothing from it. - A record's "Confirmation" section can name a test. My recent records 0038, 0041, and 0043 each name one. `adr check` does not read that section. If the check required the named test to exist, and the test to cite the record, a reversed decision would fail a test that names the record. - The matcher is not measured. The 10 cases can be its first test set: run a candidate matcher on their question and proposal text, and count how many find the right record. - Text in an agent's context is often ignored. In the same review, agents broke rules that were loaded into every one of their sessions. A blocking hook is stronger, but a false match blocks a valid question. - Each agent client needs its own hooks. #561 ports the existing guard hooks to pi, and each new hook needs the same port. - A record can be read too broadly. In one case an agent cited an accepted record to defend a hardcoded path, and I had to correct it. A tool can check a record's structure, but not the decision in it. ## Open question Which option: move the ADR tooling into its own Go module, or keep it in madtea and improve it? Related: #488, #495, #507, #508, #528, #632, #634, #638, #639, #642.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#643
No description provided.