f0rge — build plan

docs/PLAN.md · wiki index

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:

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.

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.

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

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

surfacebudget
f0rge (root help)≤ 12 lines, ≤ 300 bytes
any subcommand -h≤ 8 lines, ≤ 250 bytes
f0rge --skill≤ 30 lines, ≤ 1000 bytes
list --json1 compact line per entity, no header/boilerplate
error1 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.

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.

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.

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.

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.

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)

idthreatmitigationlives in
SEC-01hostile 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_ymlM1
SEC-02path 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 movesM1, M2
SEC-03partial writes from crashes/killsatomic write (tmp + fsync + rename) on every saveM1
SEC-04shell injection via filenames/argsall subprocesses via argv arrays (Command::new); zero sh -c interpolation of untrusted stringsM2, M6
SEC-05hostile 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 worktreeM6
SEC-06payload runs hooks/code during mergegit apply runs no hooks; merge commits run with hooks disabled; no merge driversM6
SEC-07untrusted CI steps on your machineposture explicit: ci.env (`runner: localdocker) or --docker`; runs inherit only caller env — forge injects no secrets
SEC-08already-landed payload applied twicepatch-hash check against merges/ history before applyM6
SEC-09supply chainpins, lockfile, audit/deny/vet gates (below)M8
SEC-10information leaks in errorsone-line errors, no paths/snippets beyond the offending file name; --json whitelists fields onlyM2

Supply chain (crate pinning)

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

cratepinnotes
serde=1.0.229derive feature
serde_json=1.0.151output layer — --json everywhere
yaml_serde=0.10.7maintained 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.0v4 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.