# f0rge — build plan

Status: to be built.
This is the *how/when*. OUTLINE.md is the *what/why*. Read OUTLINE first,
then this. Engineering language: **Rust**, one binary, near-zero deps.

---

## 0. Build principles — read before every chunk

1. **Bitter lesson compliant.** The forge stays dumb; agent intelligence does
   the work. No locks, no claim protocols, no auto-resolvers, no CI
   frameworks, no merge engines. Orchestration lives in the orchestrator
   (herdr); collisions are resolved by the agents that made them.
2. **gh-literal.** Same command tree, same flags, same `--json` field names
   the models were trained on. The command tree mirrors the folder tree:
   `.forge/issues/` ↔ `f0rge issue`, `.forge/prs/` ↔ `f0rge pr`, `.forge/ci/`
   ↔ `f0rge ci run`, …
3. **Files are the API.** Markdown is truth; the CLI is the enforced view.
   The CLI never holds state — it reads and writes the same bytes an agent
   would by hand.
4. **Token-minimal everywhere surface touches an agent.** Help, lists,
   errors, the skill — budgets are defined below and *enforced by tests*.
   Elegant before complete. If a surface can be 5 lines instead of 15, 5.
5. **Nothing is deleted.** Close/archive only: `state: closed|archived` in
   frontmatter, and archive moves the folder to `.forge/archive/`. git is
   the undo log; history is the product.
6. **CI is just scripts.** Steps are files, run in filename order, first
   nonzero exit is the verdict. Local shell, docker, a remote box — the
   script decides, or `ci.env` nudges. No abstraction above the script.
7. **Merges are handled by agents.** `f0rge merge` applies a patch, runs CI,
   reports *what* conflicted, and stops. Resolution is the agent's job.
   The merge engine is `git`; the intelligence is the agent.
8. **One proven way, put where agents want it.** Agents already know gh.
   We don't invent a new UX; we put the gh layout inside the repo where the
   work happens, and the skill is a pointer, not a manual.
9. **Untrusted is the default for anything that arrives in a PR** — patches,
   `ci/steps/`, `.forge/` edits from someone else's agent. Trust is earned
   per payload, per posture, never granted by the tool (see §5).

---

## 1. Workspace (where the code lives)

```
f0rge/
├── Cargo.toml            # workspace
├── crates/
│   ├── core/             # schema, frontmatter, derived indexes, json models
│   └── cli/              # binary: arg parse, dispatch, output, git shell-outs
├── spec/                 # schema v1 (single source of truth) + golden fixtures
├── skill/                # the .pi skill text (final, token-counted)
└── tests/                # end-to-end CLI tests against temp repos
```

Dependencies: `serde`, `serde_json`, `yaml_serde`, `uuid` — all
**exact-pinned**, declared once in `[workspace.dependencies]` (see §5
supply chain). Everything else is `std`.

`git` is invoked as a subprocess (`git format-patch`, `git apply`,
`git diff`, `git merge-tree`) — never reimplemented. Arg parsing is
hand-rolled, table-driven, ~200 lines — because clap's help output is
verbose on purpose and we own every token of surface here. Timestamps
are hand-rolled RFC3339-UTC formatting from `SystemTime` (~40 lines in
core) — no `time` crate, one less dependency to trust.

Dev tooling (locked in regardless): `cargo audit`, `cargo deny`,
`cargo vet`, `cargo clippy` — all run as f0rge's own dogfood CI steps.
Both crates `#![forbid(unsafe_code)]`.

## 2. Milestones and their dependency graph

```
M0  schema v1 frozen (repo-root spec/)                — blocks everything
M1  core storage + derived indexes          — blocks all entity commands
M2  CLI skeleton + token budgets            — blocks polish; parallel w/ M3
M3  entity commands: issues, prs, labels, milestones, tasks
M4  ci run                                  — needs M1; parallel w/ M3
M5  wiki + releases                         — needs M1; parallel w/ M3
M6  merge / import                          — needs M3 (pr) + M4
M7  skill + herdr/gl0w integration + audit  — needs M3–M6
M8  hardening: concurrency, perf, migration — needs all
```

Chunks within a milestone are swarmable: each chunk below states its
inputs, outputs, interfaces, and definition of done. A subagent claims a
chunk, does it against the workspace, runs its acceptance tests, moves on.

---

## M0 — Schema v1 (frozen first)

Input: OUTLINE.md §5. Output: `spec/schema.md` + golden fixtures for every
entity.

Decisions, locked now:

- Identity: uuid4, lowercase, no dashes stripped. Identity **never changes**.
- Timestamps: RFC3339, UTC, second precision (`2025-09-08T02:00:00Z`).
- Frontmatter: YAML subset (`serde_yaml`), always between `---` fences.
- Numbers: **derived, never stored.** `rank` of uuid in the total order
  `(created, uuid)` computed at list time. Same inputs → same numbers in
  any copy.
- Archive: entity gets `state: archived` and its folder moves to
  `.forge/archive/<type>/<uuid>/`. Nothing is deleted; indices skip
  archived by default.
- CI verdict: `pass | fail | running | cancelled` stored only in the `pr.md`
  `ci:` field and the run summary. Logs live in gitignored runs.
- `ci/runs/` and `releases/out/` are the only gitignored paths.

Entity schemas (all in `spec/schema.md` with one golden example file each):
`issue`, `issue-comment`, `pr`, `pr-review`, `task`, `label`, `milestone`,
`release`, `merge-record`, `ci-env`, `ci-step`.

Definition of done: every golden fixture parses and round-trips; an invalid
frontmatter yields an error naming the file and the offending field. No
byte of this schema changes after M0 without bumping the schema version in
the contract.

## M1 — Core storage + derived indexes (crates/core)

Input: M0. Output: `f0rge-core` crate, fully tested.

- Path resolution: a `Forge` struct rooted at `.forge/`, resolves every
  entity path from uuid. Paths are the only place folder layout lives.
- Atomic writes: write `tmp`, fsync, rename. No partial files, ever.
- Frontmatter parse/serialize: structs per entity, `serde`, strict field
  validation (unknown field = warning; missing required = error).
- Load/save: `load::<Issue>(uuid)`, `save(&entity)`; comments/reviews are
  separate files, loaded lazily.
- Derived indexes (pure functions, deterministic):
  - issue numbers from `(created, uuid)` order
  - kanban columns from task `status` (todo | doing | blocked | done)
  - wiki index from file tree + first H1
  - `--json` models with gh field names (`number,title,state,labels,
    assignees,milestone,body,createdAt`, `databaseId,status,conclusion`)
- Tests: unit + golden for every index; property test: index stability
  under insertion order permutation.

Definition of done: `cargo test` green; a 10k-issue directory `list`
parses under 100 ms debug build; SEC-01 + SEC-02 covered by fixtures
(size-bomb, alias-bomb, symlink-escape, oversize file → clean errors,
never a panic).

## M2 — CLI skeleton + token budgets (crates/cli)

Input: M0, M1. Output: the binary's spine: dispatch, flags, output, errors.

- Command tree (mirrors the folder tree; all gh-literal):

```
f0rge
├── issue     create list view comment close reopen assign label edit
├── pr        create diff review comment merge close list
├── task      create list move done
├── label     list create edit
├── milestone list create edit
├── ci        run list view log rerun cancel
├── wiki      get put list
├── release   create list view
├── merge     <folder|zip|branch>
├── init      [dir]
└── --skill                      # the contract, short
```

- Flags: `--title --body --label --assignee --milestone --base --head
  --draft --json --jq`. Unknown flag → exit 2 with a one-line message.
- Exit codes: 0 ok, 1 runtime error (message on stderr, one line), 2 usage.
- Output: `--json` is compact (no spaces after `:`/`,`), field order is
  fixed and documented. Human mode exists but is a thin, aligned rendering
  of the same data. Never ANSI color on non-tty; never colored for agents.
- `--jq` shells out to `jq` when present; absent → clear one-line note.

**Token budgets (hard, enforced by tests that byte-count output):**

| surface                | budget                          |
|------------------------|---------------------------------|
| `f0rge` (root help)    | ≤ 12 lines, ≤ 300 bytes         |
| any subcommand `-h`    | ≤ 8 lines, ≤ 250 bytes          |
| `f0rge --skill`        | ≤ 30 lines, ≤ 1000 bytes        |
| `list --json`          | 1 compact line per entity, no header/boilerplate |
| error                 | 1 line + exit code              |
| empty result          | `[]` or `{}` — no prose         |

Definition of done: every command in the tree responds with correct exit
codes; budget tests pass; a fresh agent can discover the entire surface
from `f0rge` alone; SEC-04 + SEC-10 enforced by tests (argv-injection
fixture, one-line-error budget).

## M3 — Entity commands

Input: M1, M2. Four parallel chunks (3a–3d), each implements its commands
with golden JSON output tests and e2e tests against a temp repo.

### 3a issues
`create` (mkdir uuid dir, write issue.md, print json), `list` (derived
numbers, filters: `--label --assignee --state --milestone --search`),
`view` (body + comments), `comment` (append-only new file), `close` /
`reopen` (edit only the owning file's `state`), `assign` (append/remove
assignee — remove only by full rewrite of owning file), `label` (same),
`edit` (body/title rewrite).

### 3b prs
`create` from branch: `git format-patch` base..head → `patch.diff`, write
`pr.md` (`--base --head --draft --issue`). `diff` → head of patch.
`review`/`comment` → append review files. `merge` (see M6, minimal here:
apply patch via `git apply`, on conflict write merge-record + exit 1 with
the offending files listed, no resolution). `close` → state closed.
`list` → derived number, state, ci verdict, title.

### 3c labels + milestones
`label list|create|edit` against `labels.md` (definition list: name,
color, description). Removal of a definition is allowed; historical uses
live in entity frontmatter and never disappear. `milestone list|create|
edit` against `milestones.md` (name, due, description).

### 3d tasks/kanban
`task create` (uuid card, `status: todo`), `list` (columns derived from
status — a plain grouped render), `move <id> <col>` (rewrite owning file
status), `done` (= `move done` + optional `closed:` timestamp). No board
file, no stored column order.

Definition of done (all of M3): a full swarm loop works by hand on a temp
repo — agent creates issue → claims it → does work → files PR → you merge.
All JSON outputs match the gh field names documented in `spec/schema.md`.

## M4 — CI (`f0rge ci`)

Input: M1, M2. Output: runner + run records. No framework allowed.

- `ci run [--docker]`: iterate `steps/` in filename order; each is an
  executable; first nonzero exit stops the run (fail-fast); `--continue-on-
  error` runs all. Env: `ci.env` (non-secret) + shell defaults.
- `--docker` wraps the run in the container from `ci.env` (default: plain
  local). That's the entire abstraction. A step that needs something
  specific declares it in its own shebang/script or in `ci.env` — nothing
  else prescribes.
- Posture is explicit, never implicit (SEC-07): `ci.env` may set
  `runner: local|docker`; `--docker` overrides to docker; runs inherit
  only the caller's environment — the forge never injects secrets, and
  nothing in `.forge/` is ever granted special trust. Steps are code you
  chose to run, under the posture you chose.
- Record: `.forge/ci/runs/<ts>/summary.md` (per-step: name, duration,
  exit; final verdict = exit code) + `steps/<name>.log`. Runs dir is
  gitignored; verdicts flow into `pr.md`'s `ci:` field via `ci run <pr>`.
- `ci list` (most recent runs, verdict), `view <run>` (summary), `log
  <run> [step]`, `rerun` (new run dir, same steps), `cancel` (kill current
  run; record `cancelled`).

Definition of done: a shell step that exits 1 fails the run; a docker
step runs inside docker when `--docker`; run records match schema; a
green run's verdict lands in the associated `pr.md`; posture honored
per SEC-07.

## M5 — Wiki + releases

Input: M1, M2.

- `wiki get <path>`, `put <path> -` (stdin), `list` (index derived from
  tree + first H1; never stored). Wiki editing is a skill convention, not
  a command: pages are `.forge/wiki/` files — edit them in place with
  editor tools (or wr1te), or via `wiki put <path> -`. Paths are flat-ish
  (`docs/`, `research/`, `experiments/`).
- `release create <ver> --notes <file|->`: write `releases/<ver>.md`,
  then run `releases/build/` scripts at the tag: checkout, build per
  platform into `out/` (gitignored). `release list|view`.

Definition of done: an agent can persist research via `f0rge wiki put`,
read it back across compactions, and ship `v0.1.0` with `release create`.

## M6 — Merge / import

Input: M3b, M4. The flagship-understatement: a utility, not a ceremony.

- `f0rge merge <folder|zip|branch>`:
  1. import the payload into a **fresh disposable worktree**, never the
     live checkout (SEC-05)
  2. `git apply --check patch.diff` against base — git's own patch path
     validation stays on; nothing is applied without passing the check
  3. against `merges/` history, compare patch hashes — if it looks already
     landed, report and stop (SEC-08)
  4. on clean apply: apply, run CI, on green update `pr.md` state → merged,
     append `merges/<ts>.md` record `{pr, applied, verdict, resolved: []}`
  5. on conflict: **stop.** Write merge-record naming the conflicting files,
     exit 1. The agent reconciles and re-runs. No auto-resolution, no
     merge driver, no rebase magic — `git` reports, the agent decides.
- Merge commits from `f0rge` disable hooks and set no merge drivers
  (SEC-06); `git apply` itself runs no hooks.

Definition of done: zip-in-a-repo → merged → record written, and a
conflicting payload fails loudly with a short, parseable conflict list;
SEC-05/06/08 fixtures green (traversal patch, hook-injecting patch,
replayed patch all fail cleanly).

## M7 — Skill + integration + audit

Input: M3–M6. Output: the shipped experience.

- `skill/skill.md`: the entire .pi skill, final text, ≤ 30 lines. It says:
  f0rge is gh, files instead of api.github.com; the folder tree is the
  truth; run `f0rge` to see the surface; never parse prose, use `--json`.
- `--skill` output: the same contract in ≤ 30 lines / ≤ 900 bytes —
  schema version, identity rules, file rules, ci verdict rule. Beautiful:
  short lines, aligned, no headings you can't act on.
- herdr/gl0w smoke test: one real loop end-to-end (issue → work → PR →
  CI → merge) run entirely by a fresh agent, measured. **Token audit:**
  count every token the agent consumed discovering and using f0rge;
  budget it, then cut it. This metric is the acceptance test for §0.4.
- Fix whatever the audit surfaces (help text, skill text, output noise).

Definition of done: audit passes; the skill fits in the budget; the loop
needs zero human help.

## M8 — Hardening

Input: all. No new features; make it boring.

- Concurrency: atomic writes everywhere (M1 already); document the
  read-modify-write race on the owning file and *stop there* — the
  reconciling agent is the fix (bitter lesson; no locks).
- Perf: 10k issues `list --json` < 300 ms release; wiki index of 500 files
  < 100 ms.
- Migration: schema version lives in the contract (`--skill`); CLI
  understands schema N and N−1 with a one-hop note in `merges/` when it
  converts a store.
- `.gitignore` defaults written by `f0rge init`: `.forge/ci/runs/`,
  `.forge/releases/out/`, `*.tmp`.
- Supply-chain gates (SEC-09): `cargo audit` + `cargo deny` + `cargo vet`
  green at known-good pins; `cargo build --locked`; re-verify the §5 pin
  table against crates.io and the RustSec DB before blessing.
- Release hashes: `f0rge release create` records sha256 of every built
  artifact in `releases/<ver>.md`.
- Sync the OUTLINE open-questions section to what got decided.

Definition of done: a repo with 10k entities and 500 wiki files is
pleasant to use; schema N−1 stores import cleanly; zero outstanding perf
or concurrency bugs in the tracker; supply-chain gates green; artifact
hashes documented.

---

## 3. Swarm protocol (how chunks get claimed)

1. A chunk lists **Inputs / Outputs / Interfaces / Definition of done**.
   Claim one, build it, run its acceptance tests, record the result in
   `build-log.md` (co-located in `.forge/wiki/docs/`) (one line per chunk: chunk, sha, verdict).
2. Monorepo, single trunk: work happens in worktrees; merges onto main
   are the same ritual as the product itself — this repo eats its own
   dogfood from M3 onward.
3. Order matters at milestone boundaries (M0 → M1 → …), not within a
   milestone (3a–3d, M4, M5 are parallel).
4. When a chunk's acceptance tests are green, the milestone is done;
   anything that smells like a design change goes back to OUTLINE.md first.

## 4. Explicit non-goals (same as OUTLINE §4)

Hosting commands, auth, api tokens, secrets, gists, codespaces, browse,
webhooks, server of any kind, a merge engine, a CI framework, a lock
service, and any UI aimed at humans that isn't also the JSON view.

## 5. Security model, supply chain & trust

### Trust model (who trusts what)

You trust your own repo. Untrusted input arrives in exactly two shapes,
and both are handled by the same ritual — never by trusting the tool:

1. **PR payloads** — `patch.diff`, `ci/steps/`, `.forge/` edits from an
   agent you did not write. Ritual: apply in a disposable worktree, run
   their steps under the posture you chose (local or docker), have an
   agent review, merge only what you accept.
2. **Foreign repos** — a `.forge/` zip imported via `f0rge merge`. Same
   as (1), plus: their entities are untrusted YAML and their wiki is
   untrusted markdown until reviewed.

Secrets never enter the repo or payloads (OUTLINE §7). The forge has no
server, no network listeners, no auth surface; its attack surface is the
filesystem and the subprocesses it runs.

### Threat list and mitigations (SEC ids referenced from milestones)

| id | threat | mitigation | lives in |
|----|--------|-----------|----------|
| SEC-01 | hostile frontmatter (YAML bombs, alias expansion, giant files) | size cap 256 KiB + depth cap before parse; strict schema (unknown field = warning, missing required = error); `yaml_serde` (maintained), never `serde_yaml`/`serde_yml` | M1 |
| SEC-02 | path escape via `file`/entity reads (symlink out of `.forge/`) | canonicalize + prefix check on every resolved path; reject symlinks that escape; don't follow on archive moves | M1, M2 |
| SEC-03 | partial writes from crashes/kills | atomic write (tmp + fsync + rename) on every save | M1 |
| SEC-04 | shell injection via filenames/args | all subprocesses via argv arrays (`Command::new`); zero `sh -c` interpolation of untrusted strings | M2, M6 |
| SEC-05 | hostile patches (path traversal, writes outside repo) | `git apply --check` first; git's built-in patch path validation stays on; apply only in a fresh disposable worktree | M6 |
| SEC-06 | payload runs hooks/code during merge | `git apply` runs no hooks; merge commits run with hooks disabled; no merge drivers | M6 |
| SEC-07 | untrusted CI steps on your machine | posture explicit: `ci.env` (`runner: local|docker`) or `--docker`; runs inherit only caller env — forge injects no secrets | M4 |
| SEC-08 | already-landed payload applied twice | patch-hash check against `merges/` history before apply | M6 |
| SEC-09 | supply chain | pins, lockfile, audit/deny/vet gates (below) | M8 |
| SEC-10 | information leaks in errors | one-line errors, no paths/snippets beyond the offending file name; `--json` whitelists fields only | M2 |

### Supply chain (crate pinning)

* All deps are **exact-pinned** (`=`) in `Cargo.toml`, declared once in
  `[workspace.dependencies]`. **No `^`, no floats, no git deps.**
* `Cargo.lock` is committed and blessed; builds use `cargo build --locked`.
* At M1: pin table verified against crates.io + RustSec with `cargo audit`
  and `cargo deny` (licenses, duplicate versions). Re-verified on **every**
  CI run as `00_audit.sh` in f0rge's own `.forge/ci/steps/` — dogfood.
* `cargo vet` audit trail for transitive deps; a crate without a vet entry
  is flagged, never silently trusted.
* Both crates `#![forbid(unsafe_code)]`; CI builds `RUSTFLAGS="-D warnings"`.

Exact pins as of plan time (re-verify at M1 before blessing):

| crate | pin | notes |
|-------|-----|-------|
| serde | =1.0.229 | `derive` feature |
| serde_json | =1.0.151 | output layer — `--json` everywhere |
| yaml_serde | =0.10.7 | maintained fork of serde_yaml by the YAML org; check RustSec at M1, roll to the 1.x line if 0.10.x gains advisories |
| uuid | =1.26.0 | `v4` feature only |

Do-not-use list (subagents: never "update" onto these): `serde_yaml`
(deprecated/archived, `0.9.34+deprecated`), `serde_yml` (RUSTSEC-2025-0068,
unsound + unmaintained), `serde_yml` shims (noyalib/libyml/compat). Anything
else requires pin + audit + vet with the reason written down.

External trust: `git` and optional `jq` come from the system; the merge
path uses only the `git` plumbing named in §1; a missing `jq` degrades to
a one-line note, never a hand-rolled fallback parser.