Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/big-picture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The default endpoint (`http://localhost:8000/v1`) is a convenience for a local i
| Section | What it covers |
| --- | --- |
| [Interactive CLI](/cli/interactive/) | Day-to-day REPL: slash commands, flags, sessions |
| [Plan mode](/cli/plan-mode/) | Read-only exploration before you let the model edit |
| [Plan mode](/cli/plan-mode/) | Explore → approve saves a project checklist → implement in-session |
| [How the gate is built](/loop/gate-floor/) | What acceptance check tsforge runs and how it picks tsc + ESLint |
| [When the gate fails](/loop/validation/) | Repair loop, stop conditions, error feedback to the model |
| [Greenfield scaffolding](/scaffold/boringstack/) | Standing up a new BoringStack full-stack app (the first adapter) |
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/src/content/docs/cli/interactive.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Most users run `tsforge` and stay in the interactive session.
| **Interactive** | `tsforge` | default: open-ended tasks, steering, exploration |
| **One-shot** | `tsforge "task" --accept "gate cmd"` | drive a single task to green and exit |
| **New app** | `tsforge scaffold` | stand up a full-stack [BoringStack](/scaffold/boringstack/) project, then build it with the [greenfield loop](/loop/greenfield/) |
| **Plan mode** | default: **Shift+Tab** or `/plan` to switch | read-only exploration before implementing. See [Plan mode](/cli/plan-mode/). |
| **Plan mode** | default: **Shift+Tab** or `/plan` to switch | read-only explore → approve saves a project checklist → implement in-session. See [Plan mode](/cli/plan-mode/). |

## Flags

Expand All @@ -28,7 +28,7 @@ Most users run `tsforge` and stay in the interactive session.
| `--no-gate` | skip auto gate detection |
| `--browser <html>` | append headless render check to gate |
| `--plan` | force plan mode on for an interactive session. Plan is the default anyway, so this only matters to override a repo that configured an autonomous `policy.mode`; ignored by one-shot/headless |
| `--continue` / `-c` | resume latest saved session for this dir |
| `--continue` / `-c` | resume latest saved session for this dir (restores `activePlanId`; Tasks rail reloads that plan) |
| `--resume <id>` | resume a specific session |
| `--log` | append JSONL event stream to `~/.tsforge/logs/` |

Expand All @@ -40,7 +40,7 @@ Model endpoint overrides: `TSFORGE_BASE_URL`, `TSFORGE_MODEL`. See [Environment
| --- | --- |
| `/help` | list commands |
| `/scaffold` | create a new full-stack project here (BoringStack / Astro) via the wizard |
| `/plan` | toggle plan mode (on by default) |
| `/plan` | toggle plan mode (on by default; approve saves the checklist and implements) |
| `/config` | settings hub: model (switch/add), mode, gate, editable scope, and tools (web, TDD); each with a description + live value |
| `/gate <cmd>` | set gate command (`/gate` alone clears) |
| `/files <globs>` | set editable scope |
Expand Down
73 changes: 51 additions & 22 deletions apps/docs/src/content/docs/cli/plan-mode.mdx
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
---
title: Plan mode
description: Read-only exploration before the model is allowed to edit files.
description: Read-only exploration, then approve to save a session-bound plan checklist and implement in the same session.
---

Plan mode is a safety rail for ambiguous work. The model can **read** your repo and **propose** a plan, but it cannot **edit** until you say go.
Plan mode is the default interactive posture. The model can **read** your repo and **propose** a plan, but it cannot **edit** until you approve. On approve, tsforge validates the model's fenced plan JSON, writes a session-bound plan file, shows it in the Tasks rail, and the same session implements it.

**It is the default for a fresh interactive session.** tsforge explores, asks the few clarifying questions that matter, and proposes a plan before it writes anything. The status bar shows the current mode as a `◆ plan` / `◆ normal` chip.
```text
plan mode (default)
→ discuss or paste
→ model calls present_plan { goal, items }
→ TUI renders PLAN card (+ Tasks preview)
→ approve / go / lgtm
→ <cwd>/.tsforge/worklist/plans/<planId>.json
→ session.activePlanId binds that plan
→ implement in-session (task_* tools + /gate as usual)
→ finished = gate green AND checklist done
→ tsforge --continue resumes the session (+ activePlanId + rail)
```

The status bar shows `◆ plan` / `◆ normal`.

## Switching modes

- Press **Shift+Tab** to cycle the mode (plan → normal → …), or type **`/plan`** to toggle it
- When the plan looks right, reply **`approve`**, **`go`**, or **`lgtm`**. The model implements it
- When the plan looks right, reply **`approve`**, **`go`**, or **`lgtm`**

There is no disable *flag*: it's a mode you cycle with Shift+Tab. (`tsforge --plan` forces plan mode on for an interactive session even in a repo that configured an autonomous `policy.mode`. One-shot and headless runs are autonomous regardless.)

## What gets saved on approve

When the plan is ready, the model calls **`present_plan`** with a structured `{ goal, items }` tree (nested `children`, optional `detail` / `files` / `verify`). The harness validates it, holds it as a **pending proposal**, and renders a PLAN card in the TUI (not a JSON dump in chat). Revise by calling `present_plan` again.

Approve (`approve` / `go` / `lgtm`):

1. Takes the pending proposal from `present_plan` (fenced JSON in chat is a fallback only)
2. Writes **`<project>/.tsforge/worklist/plans/<planId>.json`**, updates `index.json`, sets the session's **`activePlanId`**
3. Updates the Tasks side rail
4. Turns plan mode off and continues in the same session

If there is no pending plan (and no valid fenced JSON fallback), approve is refused — stay in plan mode.

Concurrent sessions in one project each bind their own `activePlanId`; plan files do not clobber.

Resume with **`tsforge --continue`** (or `--resume <id>`): the conversation and `activePlanId` return from the session store, and the Tasks rail reloads that plan.

## Task tools (after approve)

When `activePlanId` is set:

- `task_list` — nested tree with ids
- `task_focus` — set the active item
- `task_complete` / `task_uncomplete` — status changes (**tools only**; not invented in prose)

`task_complete` **runs the acceptance gate** and only marks the item done when green. If the gate is red, the item stays open and the errors are returned — fix, then complete again. Do not put “run tests / lint / the gate” as a checklist item; that is the harness’s job on every complete. A session cannot claim finished while the bound plan still has open items.

## What the model can do in plan mode

Read tools only:
Expand All @@ -22,26 +62,15 @@ Read tools only:
- `git_context`: structured, read-only repo history/diffs (see [Git context](/reference/flags/#git-context))
- `run` for **read-only** shell commands (no installs, no writes)

Blocked until approval: `edit`, `create`, `edit_lines`, scaffolders.

## General plan mode

Good for: "explore this repo and tell me how you'd refactor auth" or "what files would you touch for feature X?"

After approval, tsforge switches to the normal implement loop with full write tools.
Blocked until approval: `edit`, `create`, `edit_lines`, scaffolders, and the `task_*` tools (offered only after a plan is bound).

## Whole-app builds
## Whole-app builds (different path)

Plan mode is for **exploring and approving a change** in a repo. Building a *new*
app from scratch is a different flow: [`tsforge scaffold`](/scaffold/boringstack/)
stands up BoringStack, then the [greenfield loop](/loop/greenfield/) drives the app
to green one resource at a time. The harness runs the generators + wiring and the
model fills the domain, each feature verified and frozen before the next. That loop
plans and checkpoints per feature on its own, so it doesn't need plan mode.
Plan mode is for **interactive work in a repo**. Building a *new* app from scratch is a separate headless loop: [`tsforge scaffold`](/scaffold/boringstack/) then [`tsforge --greenfield`](/loop/greenfield/), which keeps its own checklist under `.tsforge/greenfield/`. That path does not use plan-mode approve or the Tasks rail plan files.

## Leaving plan mode
## Leaving plan mode without approving

- **Shift+Tab** (or `/plan`) drops to normal mode for hands-on edits in a repo you know
- One-shot runs (`tsforge "task" --accept …`) and headless/eval runs are autonomous already. Plan mode is interactive-only, since it needs a human to approve
- **Shift+Tab** (or `/plan`) drops to normal mode for hands-on edits
- One-shot runs (`tsforge "task" --accept …`) and headless/eval runs are autonomous already

→ [Greenfield scaffolding](/scaffold/boringstack/) · [Interactive CLI](/cli/interactive/)
→ [Greenfield builds](/loop/greenfield/) · [Interactive CLI](/cli/interactive/)
2 changes: 2 additions & 0 deletions apps/docs/src/content/docs/loop/greenfield.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Most of tsforge drives **one change to green**. Greenfield mode drives a **whole
tsforge --greenfield "build a kanban board" --accept "bun run build"
```

This is the **autonomous / headless** checklist loop (scaffold and app builds). Interactive sessions use a different path: [plan mode](/cli/plan-mode/) → approve → `.tsforge/worklist/plans/<planId>.json` (session-bound) → implement in the same REPL (`--continue`). Do not confuse the two folders.

## How it works

1. **Plan**: a planner model turns your one-line goal into a high-level spec and a flat feature checklist (sprints, not file-level steps). Written to `.tsforge/greenfield/`.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ tsforge --greenfield "build a kanban board" --accept "bun run build"
tsforge --greenfield "..." --notify 'curl -s "$WEBHOOK?s=$TSFORGE_STATUS"'
```

`--greenfield` (or a recipe with `mode: "greenfield"`) plans a feature checklist and drives it to all-green one feature at a time, persisting state under `.tsforge/greenfield/` so a long run resumes. Each feature is verified by the gate, the browser oracle, and a reject-by-default judge. `--notify <cmd>` runs a shell command on completion with the outcome in `$TSFORGE_STATUS`. See [Greenfield builds](/loop/greenfield/).
`--greenfield` (or a recipe with `mode: "greenfield"`) is the **headless** whole-app checklist loop: it plans features and drives them to all-green one at a time under `.tsforge/greenfield/`. It is separate from interactive [plan mode](/cli/plan-mode/), which saves a session-bound plan under `.tsforge/worklist/plans/` on approve and continues in the same session (`--continue`). Each greenfield feature is verified by the gate, the browser oracle, and a reject-by-default judge. `--notify <cmd>` runs a shell command on completion with the outcome in `$TSFORGE_STATUS`. See [Greenfield builds](/loop/greenfield/).

## Drive one change, then review it

Expand Down
23 changes: 12 additions & 11 deletions packages/core/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- Generated by `bun run arch:build` from packages/core/src. Do not edit by hand. -->

Derived from `packages/core/src`: **30 subsystems**, **549 files**, **98932 lines**, **135 cross-subsystem edges**.
Derived from `packages/core/src`: **30 subsystems**, **578 files**, **107055 lines**, **136 cross-subsystem edges**.


This page is the exhaustive record: every subsystem, every cross-subsystem edge, and
Expand All @@ -13,22 +13,22 @@ inventory, see the hand-drawn map on [Internals](/internals/).

| Subsystem | Purpose | Tier | Files | Lines | Fan-in | Fan-out |
| --- | --- | --- | --- | --- | --- | --- |
| `loop` | The drive-to-green engine: turns, tools, gate settling, steering, adapters | core | 105 | 30343 | 7 | 21 |
| `loop` | The drive-to-green engine: turns, tools, gate settling, steering, adapters | core | 115 | 32941 | 7 | 22 |
| `rule-packs` | The ESLint rule packs the gate enforces, grouped by stack | core | 164 | 19000 | 3 | 3 |
| `cli` | Argument parsing, the interactive REPL, and per-mode wiring | core | 20 | 6506 | 2 | 18 |
| `render` | Terminal UI — status bar, menus, wizards, markdown, diffs, spinners | core | 21 | 4576 | 6 | 5 |
| `render` | Terminal UI — status bar, menus, wizards, markdown, diffs, spinners | core | 40 | 9396 | 7 | 5 |
| `cli` | Argument parsing, the interactive REPL, and per-mode wiring | core | 20 | 7058 | 2 | 18 |
| `meta-rules` | Gate rules that need no AST — config shape, CI wiring, supply chain | core | 42 | 3424 | 1 | 2 |
| `self-harness` | Lets the harness propose, trial, and keep edits to its own prompts and rules | optional | 15 | 3367 | 2 | 9 |
| `inference` | OpenAI-compatible provider: streaming, tool calls, reasoning, token usage | core | 12 | 3022 | 10 | 3 |
| `agent` | Tool schemas, the model-as-agent wrapper, and the malformed-tool-call repair ladder | core | 10 | 2644 | 8 | 9 |
| `agent` | Tool schemas, the model-as-agent wrapper, and the malformed-tool-call repair ladder | core | 10 | 2768 | 8 | 9 |
| `scaffold` | Stands up a new project from an archetype and configures its gate | optional | 15 | 2526 | 3 | 2 |
| `editor` | The terminal input-line editor behind the REPL prompt | core | 10 | 2399 | 2 | 2 |
| `(root)` | CLI entry, model registry, session persistence — the loose files in src/ | core | 6 | 2315 | 5 | 15 |
| `(root)` | CLI entry, model registry, session persistence — the loose files in src/ | core | 6 | 2322 | 5 | 15 |
| `config` | tsforge.config.json, profiles, recipes, agent specs, and external plugins | core | 9 | 2250 | 6 | 8 |
| `reviewers` | Independent review panel that grades a change before it is trusted | optional | 9 | 2212 | 1 | 3 |
| `eval` | Run scoring, failure classification, and the quality judge | optional | 10 | 1817 | 4 | 4 |
| `files` | Reading, creating, and hash-anchored editing of workspace files | core | 9 | 1592 | 5 | 1 |
| `policy` | Decides which actions are allowed in the current mode before they run | core | 5 | 1249 | 5 | 3 |
| `policy` | Decides which actions are allowed in the current mode before they run | core | 5 | 1257 | 5 | 3 |
| `gate` | Composes and runs the deterministic gate: linter, stages, tool paths | core | 10 | 1181 | 5 | 5 |
| `architecture` ⚠️ | Derives this map from source so the docs cannot drift from the code | optional | 8 | 1118 | 0 | 0 |
| `lib` | Shared primitives — fs, json, guards, scope globs, SSRF checks, clipboard | core | 17 | 1082 | 24 | 0 |
Expand All @@ -38,7 +38,7 @@ inventory, see the hand-drawn map on [Internals](/internals/).
| `browser` | Headless Chromium oracle that render-checks a page as a gate stage | optional | 3 | 683 | 1 | 1 |
| `spec` | Task and spec shapes, spec parsing, and test generation from intent | core | 6 | 630 | 6 | 6 |
| `stack-detection` | Detects the project's stack and picks which rule packs apply | core | 4 | 579 | 7 | 1 |
| `setup` | Onboarding wizard that writes a project's initial tsforge config | optional | 4 | 539 | 2 | 5 |
| `setup` | Onboarding wizard that writes a project's initial tsforge config | optional | 4 | 553 | 2 | 5 |
| `validate` | Runs the gate command and parses tool output into structured errors | core | 7 | 475 | 7 | 2 |
| `codebase` | Structural workspace map and hub ranking used to seed prompt context | core | 6 | 472 | 2 | 4 |
| `proptest` ⚠️ | Derives property-based test inputs from TypeScript types | optional | 3 | 364 | 0 | 0 |
Expand All @@ -56,17 +56,18 @@ buries the ones someone can actually go and break.

| Pair | One way | The other |
| --- | --- | --- |
| `(root)` ↔ `cli` | `cli.ts:29` → `./cli/args` | `cli/banner.ts:6` → `../session-store` |
| `(root)` ↔ `cli` | `cli.ts:29` → `./cli/args` | `cli/config-menu.ts:10` → `../models-config` |
| `(root)` ↔ `inference` | `classify.ts:1` → `./inference` | `inference/image-gen.ts:4` → `../models-config` |
| `(root)` ↔ `loop` | `cli.ts:13` → `./loop` | `loop/expert-handoff.ts:21` → `../models-config` |
| `agent` ↔ `inference` | `agent/agent-runner.ts:13` → `../inference` | `inference/wire.ts:9` → `../agent` |
| `agent` ↔ `loop` | `agent/agent-runner.ts:16` → `../loop/loop.types` | `loop/model-call.ts:6` → `../agent` |
| `agent` ↔ `policy` | `agent/agent-runner.ts:15` → `../policy` | `policy/classify.ts:1` → `../agent` |
| `agent` ↔ `spec` | `agent/agent.types.ts:1` → `../spec` | `spec/generate-tests.ts:5` → `../agent` |
| `cli` ↔ `render` | `cli/banner.ts:5` → `../render` | `render/command-menu.ts:2` → `../cli/commands` |
| `cli` ↔ `render` | `cli/banner.ts:9` → `../render` | `render/command-menu.ts:2` → `../cli/commands` |
| `config` ↔ `rule-packs` | `config/external-plugins.ts:5` → `../rule-packs` | `rule-packs/index.ts:113` → `../config/plugin-fingerprint` |
| `editor` ↔ `render` | `editor/view.ts:2` → `../render/style` | `render/width.ts:1` → `../editor/segments` |
| `eval` ↔ `loop` | `eval/failure-class.ts:1` → `../loop/loop.types` | `loop/quality.ts:7` → `../eval` |
| `loop` ↔ `render` | `loop/worklist/panel.ts:3` → `../../render/frame/ansi-plain` | `render/agent-tree.ts:8` → `../loop/loop.types` |
| `loop` ↔ `self-harness` | `loop/feedback/rule-docs.ts:3` → `../../self-harness/overlay` | `self-harness/build-evidence.ts:3` → `../loop` |
| `loop` ↔ `spec` | `loop/feedback/feedback.ts:2` → `../../spec` | `spec/generate-tests.ts:4` → `../loop` |
| `spec` ↔ `validate` | `spec/generate-tests.ts:8` → `../validate` | `validate/accept.ts:1` → `../spec` |
Expand Down Expand Up @@ -98,7 +99,7 @@ Async functions returning an exit code, declared under the CLI — the commands.
| `main` | `cli.ts:754` |
| `mapMode` | `cli.ts:481` |
| `recipesMode` | `cli.ts:500` |
| `repl` | `cli/repl.ts:552` |
| `repl` | `cli/repl.ts:609` |
| `reviewMode` | `cli.ts:181` |
| `runOnce` | `cli.ts:93` |
| `runTraceCommand` | `cli/repl-commands.ts:109` |
Expand Down
Loading
Loading