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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ All notable changes to forge will be documented in this file. Format follows [Ke

## [Unreleased]

## [1.5.0] — 2026-05-28

### Added

- **P1: Model tier routing** — `LLMPipe` now uses `tierrouter` for complexity-driven model selection. `nano`/`micro` → T0 (cheap), `standard` → T1 (balanced), `complex` → T2 (powerful). Controlled via `SetComplexityTier`.
- **P1: 3-layer knowledge base** (`internal/knowledge/layered.go`) — KB loader merges embedded global KB, user-scoped `~/.forge/kb/`, and project-scoped `.forge/kb/` in priority order. Project KB entries override global ones.
- **P2: OpenTelemetry checkpoint spans** — `telemetry.StartPipelineSpan` and `telemetry.EmitCheckpointSpan` emit per-checkpoint OTEL spans to `.forge/telemetry.jsonl`. Wired into the `forge ship` pipeline.
- **P2: Prometheus metrics export** — `forge metrics` command reads `.forge/token-ledger.jsonl` and outputs `forge_tokens_total` and `forge_cost_usd_total` counter series in Prometheus text format, labelled by model.
- **P2: forge undo integration** — `writeShipTrashManifest` records every ship run to `.forge/trash/<runID>/manifest.json`; `snapOnFail` takes a best-effort snapshot on checkpoint failure. Both wired into `forge ship`.
- **`forge companion`** — zero-setup AI pairing command with four subcommands: `install` (writes expert persona files for VS Code Copilot / Claude / Cursor / Windsurf), `update` (force-refreshes skill files), `status` (shows per-platform install state), `guide` (prints the vibe-coding quick-start cheatsheet with the top-10 daily prompts).
- **`forge init` companion hint** — after scaffolding completes, `forge init` prints a `forge companion install` hint so new projects are prompted to set up AI pairing immediately.
- **Command groups in `forge --help`** — 50 commands now displayed in 7 named groups: Core, Build & Ship, Analysis & Quality, Operations, AI & Automation, Config & Tools, Advanced.
- **Vibe-coding workflows in skill templates** — all platform templates (Copilot, Claude, Cursor, Windsurf) now include a "Daily Vibe-Coding Patterns" section with feature/bugfix/security/standup/review workflow examples.
- **Error code ranges** — `FORGE-6600..6649` reserved for `cli/metrics`; `FORGE-6650..6699` reserved for `cli/companion`.

### Fixed

- `companion.go` raw-string backtick syntax error — guide string switched to concatenation to allow embedded backtick characters.
- Duplicate `FORGE-6800` registration — `cmdmetrics` moved from 6800 to 6600; `cmdcompanion` moved from 6900 (unregistered) to 6650.
- `captureProvider` in `llmpipe_tier_test.go` — added missing `Capabilities()` method to satisfy `llmprovider.Provider` interface.
- `TestInvoke_FallbackToDirectWhenRouterNil` — test now uses `newLLMPipeWithProvider` (initialises `rewriter`) and then nils the router, avoiding the nil-pointer dereference.

## [1.3.0] — 2026-05-26

### Added
Expand Down
4 changes: 4 additions & 0 deletions docs/ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ You can also look up any code with `forge ask error <code>`.
| `FORGE-6550` | bugfix failed | [forge.dev/errors/6550](https://forge.dev/errors/6550) |
| `FORGE-6551` | no bug source specified — provide --bug, --finding, or --test | [forge.dev/errors/6551](https://forge.dev/errors/6551) |
| `FORGE-6552` | finding not found in review results | [forge.dev/errors/6552](https://forge.dev/errors/6552) |
| `FORGE-6553` | LLM call failed — forge bugfix cannot proceed without a working LLM | [forge.dev/errors/6553](https://forge.dev/errors/6553) |
| `FORGE-6554` | no LLM provider configured | [forge.dev/errors/6554](https://forge.dev/errors/6554) |
| `FORGE-6700` | skill operation failed (forge skill install/list/remove) | [forge.dev/errors/6700](https://forge.dev/errors/6700) |
| `FORGE-6600` | metrics export failed | [forge.dev/errors/6600](https://forge.dev/errors/6600) |
| `FORGE-6650` | companion setup failed | [forge.dev/errors/6650](https://forge.dev/errors/6650) |
25 changes: 25 additions & 0 deletions docs/RELEASE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ git commit -m "chore: prepare release vX.Y.Z"
git push
```

### 1.1 — Version scope gate (required before tagging)

Do not bump to the next minor/major by default. Pick the **smallest valid** semver scope:

| If the release contains | Bump |
|---|---|
| Bug fixes, hardening, docs, internal refactors, non-breaking behavior corrections | `PATCH` |
| Backward-compatible additive capability (new verb/flag/output field that does not break existing usage) | `MINOR` |
| Intentional breaking contract change | `MAJOR` |

Required evidence before tagging:

1. Write a short "Version Scope Decision" in the release PR/body:
- `Chosen bump`: PATCH/MINOR/MAJOR
- `Why not smaller`: one line
- `Breaking impact`: none / described
2. Confirm the relevant feature spec(s) have a top `Status Summary` block with:
- `Lifecycle`
- `Version Scope` (+ rationale)
- `Last Updated`
- `Checkpoint Progress`
3. If uncertain between `PATCH` and `MINOR`, ship `PATCH` first (or cut `-rc.N`).

### 2 — Tag and push

```sh
Expand Down Expand Up @@ -175,6 +198,8 @@ Forge follows **Semantic Versioning** (`MAJOR.MINOR.PATCH`):
| Bug fix, security patch | PATCH |
| Pre-release candidate | `x.y.z-rc.N` |

Default release behavior: **prefer PATCH unless a larger scope is clearly justified**.

All six npm packages (`@forge/cli` + 5 platform packages) are always published at the same version and kept in lockstep.

---
Expand Down
Binary file added forge.exe~
Binary file not shown.
51 changes: 32 additions & 19 deletions internal/cli/cmdbugfix/bugfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ var (
ErrBugfixFailed = errcode.Register(errcode.Code(6550), "bugfix failed")
ErrNoSourceSpecified = errcode.Register(errcode.Code(6551), "no bug source specified — provide --bug, --finding, or --test")
ErrFindingNotFound = errcode.Register(errcode.Code(6552), "finding not found in review results")
ErrLLMCallFailed = errcode.Register(errcode.Code(6553), "LLM call failed — forge bugfix cannot proceed without a working LLM")
ErrNoLLMProvider = errcode.Register(errcode.Code(6554), "no LLM provider configured")
)

// Source constants identify where the bug came from.
Expand Down Expand Up @@ -83,8 +85,16 @@ type RunContext struct {
Files []string // source file paths to include in the LLM context
ExtraCtx string // free-form additional context supplied by the caller
Model string // LLM model override (e.g. "gpt-4o", "claude-sonnet-4-5")
// testProvider may be set by tests in this package to inject a fake LLM
// provider without real network calls. It is always nil for callers outside
// package cmdbugfix (unexported field — zero value is nil).
testProvider llmprovider.Provider
}

// testProviderHook may be set by package-internal tests to inject a fake
// provider into the cobra command handler. It must remain nil in production.
var testProviderHook llmprovider.Provider

// BugfixResult is the full output of one bugfix run.
type BugfixResult struct {
Root string `json:"root"`
Expand Down Expand Up @@ -122,7 +132,7 @@ func init() {
"with --apply: writes patch to source files + regression test; appends to .forge/audit.log",
},
GatesTouched: []string{"§4 bugfix", "DEV-M1-48"},
ErrorCodes: []errcode.Code{ErrBugfixFailed, ErrNoSourceSpecified, ErrFindingNotFound},
ErrorCodes: []errcode.Code{ErrBugfixFailed, ErrNoSourceSpecified, ErrFindingNotFound, ErrLLMCallFailed, ErrNoLLMProvider},
})
}

Expand Down Expand Up @@ -193,10 +203,11 @@ func New() *cobra.Command {
}

rc := RunContext{
Stack: stack,
Files: files,
ExtraCtx: extraCtx,
Model: model,
Stack: stack,
Files: files,
ExtraCtx: extraCtx,
Model: model,
testProvider: testProviderHook,
}
result, err := Run(root, mode, bug, finding, test, rc)
if err != nil {
Expand Down Expand Up @@ -258,15 +269,18 @@ func Run(root, mode, bug, finding, test string, rcs ...RunContext) (BugfixResult
ctx := loadContext(root)

// Try LLM-backed diagnosis.
provider, err := llmprovider.Detect()
if err != nil {
// No LLM — return a structured placeholder so callers get a valid result.
result.RootCause = "LLM provider not configured. Options: set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or GH_TOKEN (GitHub Copilot — if you have a Copilot subscription, run: gh auth login)."
result.Summary = fmt.Sprintf("no LLM provider detected — cannot diagnose %s %q", result.Source, result.Input)
return result, nil
p := rc.testProvider
if p == nil {
var detectErr error
p, detectErr = llmprovider.Detect()
if detectErr != nil {
return result, errcode.New(ErrNoLLMProvider,
"set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or GH_TOKEN (GitHub Copilot — run: gh auth login)",
detectErr)
}
}

return llmBugfix(result, provider, ctx, rc)
return llmBugfix(result, p, ctx, rc)
}

// llmBugfix calls the LLM to diagnose root cause, produce a patch, and write
Expand Down Expand Up @@ -346,9 +360,9 @@ Respond with a JSON object:

resp, err := provider.Complete(context.Background(), req)
if err != nil {
result.RootCause = fmt.Sprintf("LLM call failed: %v", err)
result.Summary = "LLM call failedcannot produce fix"
return result, nil
return result, errcode.New(ErrLLMCallFailed,
fmt.Sprintf("provider error: %vcheck model name, quota, and credentials", err),
err)
}

// Parse LLM JSON response, stripping any markdown fences.
Expand All @@ -372,10 +386,9 @@ Respond with a JSON object:
}

if err := json.Unmarshal([]byte(cleaned), &parsed); err != nil {
// LLM returned non-JSON — surface the raw content as root cause.
result.RootCause = cleaned
result.Summary = "LLM response could not be parsed as JSON; raw output shown above"
return result, nil
return result, errcode.New(ErrLLMCallFailed,
"LLM response is not valid JSON — cannot produce a structured fix (raw response logged below)",
fmt.Errorf("parse error: %w; raw: %.300s", err, cleaned))
}

result.RootCause = parsed.RootCause
Expand Down
Loading
Loading