forgedock 1.0.22 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -218
- package/bin/cinema.mjs +320 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +464 -1491
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +80 -13
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/deploy-info.md +5 -3
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/issue.md +1 -1
- package/commands/orchestrate.md +27 -9
- package/commands/quality-gate.md +80 -34
- package/commands/review-pr-agents.md +7 -7
- package/commands/review-pr.md +21 -4
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +38 -1
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +2 -2
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +45 -3
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/getting-started.md +15 -8
- package/docs/site/index.md +1 -1
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/package.json +10 -4
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Cinematic Onboarding TUI — Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-03
|
|
4
|
+
**Status:** Approved (design); pending implementation plan
|
|
5
|
+
**Supersedes/extends:** `2026-06-09-intelligent-onboarding-design.md` (Pillars A & B were built as modules but never wired into the CLI; this design closes that seam and defines the visual/experiential layer on top)
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
The onboarding audit (2026-07-03) found two classes of failure:
|
|
10
|
+
|
|
11
|
+
1. **The intelligent onboarding is dead code.** `init-detect.mjs` (confidence-scored detection), `annotatedReviewScreen` (tui.mjs:835-1170), `init-enrich-api.mjs` (BYO-key AI enrichment), `registry.mjs`, and `bin/hooks/session-start.mjs` all exist with tests — and none are reachable. `forgedock.mjs`'s `init()` uses its own inline template; `install()` never registers the SessionStart hook; `enable`/`disable`/`status` don't exist.
|
|
12
|
+
2. **The funnel lies.** README advertises `npx forgedock demo` (dead — `Unknown command`), `integrate` (dead), and a CLAUDE.md injection that never happens. `getting-started.md` sends users to `/forgedock-init`, which hard-errors without a forge.yaml. `CONFIG.md` documents `--manual`/`--verbose` flags that parse nowhere. `plugin.json` says 1.0.1 while `package.json` says 1.0.8.
|
|
13
|
+
|
|
14
|
+
Beyond correctness, the shipped experience is plain warnings-and-homework. The goal is an install that feels revolutionary.
|
|
15
|
+
|
|
16
|
+
## Decisions (locked with user)
|
|
17
|
+
|
|
18
|
+
| Decision | Choice |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| TUI stack | Zero-dependency, hand-rolled — extend existing `tui.mjs`, no frameworks |
|
|
21
|
+
| Scope | Full onboarding arc: preflight → install → detect → review → celebrate, plus SessionStart hook wiring and `enable`/`disable`/`status`. `forgedock demo` is **out of scope** (own milestone) |
|
|
22
|
+
| Motion | Full cinematic, **always** — no first-run-only fast path; every command plays its full choreography. Automatic degradation (non-TTY/CI/`NO_COLOR`) and explicit escape hatches (`--fast`, `FORGE_NO_MOTION=1`). Note: an already-configured bare invocation shows the status screen (per approved storyboard) — that screen has its own full choreography; it doesn't replay the install acts |
|
|
23
|
+
| Visual identity | **Molten Forge**: fire-orange → amber gradients, "forging" language |
|
|
24
|
+
| Logo treatment | **Chrome & Ember**: block-art F mark keeps the official metallic chrome/champagne gradient; ember lives in the wordmark and UI. Hero mark on install; compact 4-row lockup on all other commands |
|
|
25
|
+
| Flow shape | **One continuous journey**: bare `npx forgedock` plays the whole arc; `install`/`init` are aliases that jump into the relevant acts |
|
|
26
|
+
| Architecture | **Cinema layer + journey orchestrator** (new `bin/cinema.mjs` + `bin/journey.mjs`; `forgedock.mjs` becomes a thin router) |
|
|
27
|
+
|
|
28
|
+
## The Experience — five acts, one keypress
|
|
29
|
+
|
|
30
|
+
The only interaction in the happy path is a single Enter at Act IV. Total added latency from theater: motion overlays real work wherever work exists; pure-theater beats (shimmer, quench flash) total ≈1.2s.
|
|
31
|
+
|
|
32
|
+
### Act I — Ignition (~2.5s)
|
|
33
|
+
|
|
34
|
+
Chrome block-art mark materializes with a one-pass heat-shimmer sweep (~800ms); ember gradient wordmark `F O R G E D O C K` + version; rule line "lighting the forge". Preflight rows appear one-by-one, each live (`○` → braille spinner → `✔`):
|
|
35
|
+
|
|
36
|
+
- Node ≥18 (version shown)
|
|
37
|
+
- git present + repo detected
|
|
38
|
+
- Claude Code (`~/.claude` exists)
|
|
39
|
+
- GitHub CLI installed + authenticated (account shown)
|
|
40
|
+
|
|
41
|
+
A failed check renders an ember-bordered **fix card** with the exact copy-paste remedy (e.g. `winget install GitHub.cli`, `gh auth login`) and the journey **continues** with whatever still works. Preflight failures are advisory, not fatal — missing `gh` degrades detection confidence, it does not stop the forge.
|
|
42
|
+
|
|
43
|
+
### Act II — Forging (real work)
|
|
44
|
+
|
|
45
|
+
- Progress bar with molten leading edge (`█▓▒░`) and per-command name ticker while symlinking the 24 commands into `~/.claude/commands` (existing logic, restyled).
|
|
46
|
+
- **Seam-closing (new):** merge a SessionStart hook entry into `~/.claude/settings.json` (idempotent read-modify-write; preserves existing hooks; removed on `uninstall`; malformed JSON → skip with fix card, never clobber). Write the registry entry marking the directory forge-managed.
|
|
47
|
+
- Settle lines: `✔ 24 slash commands linked`, `✔ SessionStart hook registered`, `✔ directory forge-managed`.
|
|
48
|
+
|
|
49
|
+
### Act III — Reading your repository
|
|
50
|
+
|
|
51
|
+
Each detection field resolves as a live row: value + confidence badge + source, e.g.
|
|
52
|
+
`✔ owner/repo RapierCraftStudios/ForgeDock [high] git remote`.
|
|
53
|
+
Driven by the existing `detectConfig()` (init-detect.mjs). Fields: owner/repo, default branch, staging branch, project name, description, worktree base, project board (via `gh` when available). If `ANTHROPIC_API_KEY` is set, `init-enrich-api.mjs` fills review/verification sections (`✦ enriching with AI…`); if not, one quiet skip line. Spinners here cover *actual* latency (git/gh/API calls) — nothing is artificially slowed.
|
|
54
|
+
|
|
55
|
+
### Act IV — The Review (the single interaction)
|
|
56
|
+
|
|
57
|
+
The existing `annotatedReviewScreen` (tui.mjs), wired in at last and restyled to ember: numbered field table with `[high]/[med]/[low]` badges, confidence legend, low-confidence warning. `Press Enter to forge, or a number to edit a field`. Low-confidence fields are written with `# TODO(forgedock:<field>)` comments instead of blocking. Existing forge.yaml → overwrite banner + backup to `forge.yaml.bak` before write (existing behavior preserved).
|
|
58
|
+
|
|
59
|
+
### Act V — Forged (celebration)
|
|
60
|
+
|
|
61
|
+
Compact mark flashes white-hot once (quench, ~400ms), then settles:
|
|
62
|
+
|
|
63
|
+
- `Forged.` + **real elapsed time** (`install → config in 34s`) — proof, not hype
|
|
64
|
+
- Receipt lines: forge.yaml written (with TODO count), commands + hook active
|
|
65
|
+
- Boxed "what's next" card: `1. open claude in this repo` / `2. run /work-on next`
|
|
66
|
+
- Footer: docs link + star nudge
|
|
67
|
+
|
|
68
|
+
Next Claude Code session, the now-installed SessionStart hook greets the user with the command table — the experience continues past the terminal.
|
|
69
|
+
|
|
70
|
+
### Re-runs and aliases
|
|
71
|
+
|
|
72
|
+
- Bare `npx forgedock` in an already-configured repo → short status screen (compact lockup, current config summary, hook state) + offer to review config. Not the full movie.
|
|
73
|
+
- `npx forgedock install` → Acts I–II (+ III–V only if unconfigured, matching today's auto-init).
|
|
74
|
+
- `npx forgedock init` → Acts III–V.
|
|
75
|
+
- `enable` / `disable` / `status` → registry operations, compact lockup, instant.
|
|
76
|
+
- `update` / `uninstall` → existing logic, compact restyle; `uninstall` also removes the hook entry and registry entries.
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
### `bin/cinema.mjs` (new — animation engine)
|
|
81
|
+
|
|
82
|
+
Kept separate from tui.mjs (already 1,171 lines). Exports:
|
|
83
|
+
|
|
84
|
+
- Truecolor per-cell gradient rendering with 256-color fallback (`COLORTERM`/`TERM` detection); gradient text lines and gradient block art
|
|
85
|
+
- `renderMark(size)` — the chrome F mark, hero (8-row) and compact (4-row); block-art geometry approved v1 from mockups, may be pixel-tuned during implementation
|
|
86
|
+
- `shimmer(lines, opts)` — one-pass highlight sweep
|
|
87
|
+
- `reveal(rows, opts)` — staged live rows (`○` → spinner → `✔`/`✖`/fix card)
|
|
88
|
+
- `progressBar` molten variant with ticker
|
|
89
|
+
- `motionEnabled()` — the single motion gate: TTY ∧ ¬`NO_COLOR` ∧ ¬`FORGE_NO_MOTION` ∧ ¬`--fast` ∧ ¬CI
|
|
90
|
+
|
|
91
|
+
**No alternate screen buffer.** Animations redraw in place (cursor-up) only while active, then settle to static lines — scrollback keeps a complete plain receipt of the run.
|
|
92
|
+
|
|
93
|
+
### `bin/journey.mjs` (new — choreographer)
|
|
94
|
+
|
|
95
|
+
Five act functions — `preflight()`, `forge()`, `read()`, `review()`, `celebrate()` — over a shared context object; each act independently callable (for aliases) and unit-testable. Orchestrates the existing modules: `init-detect.mjs`, `init-enrich-api.mjs`, `annotatedReviewScreen`, `registry.mjs`. The forge.yaml writer moves here, driven from the detection `ConfigDraft` (single source of truth; deletes the duplicate inline template in forgedock.mjs) and emits `# TODO(forgedock:<field>)` for low-confidence values.
|
|
96
|
+
|
|
97
|
+
### `bin/forgedock.mjs` (shrinks to router)
|
|
98
|
+
|
|
99
|
+
Command dispatch → journey acts as above. Fixes: `HOME` check gains `USERPROFILE`/`os.homedir()` fallback (currently hard-fails on bare Windows shells); flag parsing added (`--fast`, `--manual`, `--verbose`). `--manual` = plain-prompt path (no detection auto-accept), making CONFIG.md's documented flags real.
|
|
100
|
+
|
|
101
|
+
### Hook wiring (the seam)
|
|
102
|
+
|
|
103
|
+
`forge()` merges into `~/.claude/settings.json`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command",
|
|
107
|
+
"command": "node \"$FORGE_HOME/bin/hooks/session-start.mjs\"" } ] } ] } }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The command path is resolved to the absolute installed package location at install time (the same resolution `install()` already performs for symlink sources) — written as an absolute path, not a literal env reference, so the hook works regardless of shell profile.
|
|
111
|
+
|
|
112
|
+
Idempotent (detects its own entry, never duplicates), preserves unrelated hooks, guards malformed JSON (skip + fix card, never overwrite), and is removed by `uninstall`. This makes the existing `session-start.mjs` (managed-active greeting / missing-config nudge / one-time unmanaged nudge) actually fire.
|
|
113
|
+
|
|
114
|
+
## Degradation ladder
|
|
115
|
+
|
|
116
|
+
1. Full cinematic — interactive TTY, truecolor
|
|
117
|
+
2. 256-color — same choreography, quantized gradients
|
|
118
|
+
3. `NO_COLOR` — monochrome, motion intact
|
|
119
|
+
4. `--fast` / `FORGE_NO_MOTION=1` — instant renders, no animation frames
|
|
120
|
+
5. Non-TTY/CI — deterministic plain sequential log, same information, zero ANSI animation; review screen auto-accepts nothing — in non-TTY, detection results are written with TODO flags and the run says so (preserves current "never block CI" behavior while still protecting existing configs via the existing non-interactive abort on overwrite)
|
|
121
|
+
6. Narrow terminals (<60 cols) — compact lockup replaces hero mark; tables wrap to stacked rows
|
|
122
|
+
|
|
123
|
+
## Error handling
|
|
124
|
+
|
|
125
|
+
- **Never a dead stop, never a lie.** Preflight fix cards + continue; degraded steps stated inline at the moment they happen and echoed in the Act V receipt (e.g. `2 fields flagged # TODO`). No silently skipped sections.
|
|
126
|
+
- **Ctrl-C per act:** restore cursor/style, print partial-state summary (`commands installed, config not written — run npx forgedock init to finish`), exit cleanly. No half-drawn screens, no orphaned temp files.
|
|
127
|
+
- **Filesystem safety unchanged or better:** forge.yaml backup before overwrite; settings.json read-modify-write with parse guard; registry writes stay atomic (existing).
|
|
128
|
+
|
|
129
|
+
## Testing
|
|
130
|
+
|
|
131
|
+
Extends the existing `node --test` suite in `bin/tests/`:
|
|
132
|
+
|
|
133
|
+
- **Acts:** unit tests with injected exec/IO stubs — preflight verdict matrix, hook-merge idempotency (twice → one entry), unrelated-hook preservation, TODO-flag emission, ConfigDraft→YAML writer output.
|
|
134
|
+
- **Degradation:** snapshot tests of the non-TTY plain log (`FORGE_NO_MOTION=1`, piped stdout) — deterministic byte-for-byte.
|
|
135
|
+
- **Cinema:** frame-function tests (gradient math, color-mode fallback selection), not timing tests — motion verified by eye; everything beneath it deterministic.
|
|
136
|
+
- **Windows first-class:** path handling, `USERPROFILE` fallback, symlink-vs-copy behavior — primary dev platform is Windows.
|
|
137
|
+
|
|
138
|
+
## Docs truth-pass (ships in this milestone)
|
|
139
|
+
|
|
140
|
+
- **README:** remove `demo` (deferred) and `integrate`; replace CLAUDE.md-injection claim with the (now true) hook story; collapse the two-step install to one `npx forgedock`; requirements list gains Node ≥18.
|
|
141
|
+
- **getting-started.md:** fix the `/forgedock-init`-without-forge.yaml dead end; document the journey.
|
|
142
|
+
- **CONFIG.md:** `--manual`/`--verbose` now real; describe actual flags only.
|
|
143
|
+
- **Versioning:** reconcile `plugin.json` (1.0.1) with `package.json` (1.0.8); ship as **1.1.0**.
|
|
144
|
+
- **demo.tape:** re-record after implementation so the README gif shows the real flow.
|
|
145
|
+
|
|
146
|
+
## Out of scope
|
|
147
|
+
|
|
148
|
+
- `npx forgedock demo` (sandbox pipeline showcase) — its own future milestone
|
|
149
|
+
- Self-healing `validate` (spec 2026-06-09 Pillar C, AI-fix loop) — future
|
|
150
|
+
- Skill-backend enrichment inside Claude Code — future; API backend + deterministic baseline only for now
|
|
151
|
+
|
|
152
|
+
## Success criteria
|
|
153
|
+
|
|
154
|
+
- Fresh `npx forgedock` on a clean machine: zero commands to remember, one Enter, ends with a valid forge.yaml, installed commands, live SessionStart hook, and next-step card — in under ~60s on a normal repo.
|
|
155
|
+
- Re-run is a status screen, not a movie sit-through beyond its own short choreography.
|
|
156
|
+
- CI/non-TTY runs produce a stable plain log and never hang on a prompt.
|
|
157
|
+
- No user-facing doc advertises a command or behavior that doesn't exist.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Durable Execution Engine — Design Spec
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-03
|
|
4
|
+
**Status:** Implemented (headless engine core) on branch feat/durable-execution-engine — see docs/superpowers/plans/2026-07-03-durable-execution-engine.md
|
|
5
|
+
**Tracking:** #1256 (keystone), epic #1320 (five foundations of autonomy)
|
|
6
|
+
**Scope of this spec:** the durable engine **core** only — phase state machine, run-log, resume, and orchestrator integration for **headless / orchestrated** runs. Verification, learning, economics, and provenance (#1315–#1319) are separate specs that build on this substrate.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Motivation
|
|
11
|
+
|
|
12
|
+
The pipeline today is a **prose-driven state machine**. `work-on.md` describes a linear walk (Phase 0→7) whose only continuation mechanism is imperative text ("do NOT stop"), and its subcommands reference a numbered state router that `work-on.md` never implements (#1308). Phase transitions therefore depend on an LLM *choosing* not to emit `end_turn` at a Skill-return boundary. This produces the recurring failure classes:
|
|
13
|
+
|
|
14
|
+
- **review→close stalls** (~half of runs merge the PR but leave the issue open).
|
|
15
|
+
- **quality-gate→3H stalls.**
|
|
16
|
+
- **"Prompt is too long" deaths** mid-run (#1305), patched with a self-counted "≥20 Skill calls" heuristic.
|
|
17
|
+
- **Lossy state** — GitHub prose comments are the de-facto database; resume re-parses them.
|
|
18
|
+
- **Blind observability** — progress is inferred from JSONL output files, which are 0-byte on Windows (#1307).
|
|
19
|
+
|
|
20
|
+
The insight that makes a fix cheap: **`bin/runner.mjs` already owns the Anthropic tool-use loop** (issue #1151). ForgeDock is not locked inside Claude Code's opaque agent loop — it drives its own loop. So the work is not "adopt a runtime from scratch," it is "make the loop we already own durable."
|
|
21
|
+
|
|
22
|
+
## 2. Goals / Non-goals
|
|
23
|
+
|
|
24
|
+
**Goals**
|
|
25
|
+
- Phase transitions are decided by **deterministic code**, never by an LLM's choice to continue.
|
|
26
|
+
- A run survives crash / kill / compaction / rate-limit and **resumes from the last committed phase** in a fresh context.
|
|
27
|
+
- State is **machine-readable and shared**, so any machine or the orchestrator can resume and observe a run.
|
|
28
|
+
- Eliminate the stall and "prompt too long" classes structurally, not with prose.
|
|
29
|
+
|
|
30
|
+
**Non-goals (explicitly out; each is its own spec / increment)**
|
|
31
|
+
- The interactive Claude Code `/work-on` path (a later increment adopts the same core via hooks).
|
|
32
|
+
- Per-turn / mid-phase durability (this spec is **per-phase** granularity).
|
|
33
|
+
- The verification gate, learning memory, economic governance, provenance (#1315–#1319).
|
|
34
|
+
- Full per-command parity for the standalone runtime (#1151 follow-up).
|
|
35
|
+
|
|
36
|
+
## 3. Locked decisions
|
|
37
|
+
|
|
38
|
+
These were settled during design and are not open:
|
|
39
|
+
|
|
40
|
+
1. **Durability granularity: per-phase.** The engine checkpoints each pipeline *phase*. A mid-phase crash re-runs that one phase (idempotently). Per-turn durability is rejected as over-complex (partial file edits / half-made commits).
|
|
41
|
+
2. **State authority: hybrid (local hot + GitHub mirror), GitHub wins on divergence.** A local append-only run-log is the crash-safe hot path; a structured `FORGE:STATE` block on the issue is the durable shared mirror. On conflict the GitHub mirror is authoritative and invalidates the local cache.
|
|
42
|
+
3. **First surface: headless / orchestrated.** The engine drives runs through `runner.mjs` — where autonomy matters most and there is no human to un-stick a stall. Interactive Claude Code adopts the same core later.
|
|
43
|
+
4. **`FORGE:STATE` is a compact index, not self-describing.** It carries the resume pointer (phase, `committed[]`, branch, pr, version, lease) — **not** the rich per-phase outputs. Rich outputs live in the existing `FORGE:INVESTIGATOR` / `ARCHITECT` / etc. structured comments (which the engine writes anyway). A fresh-clone resume reads the index for *where*, then reads those comments for *content*. This avoids duplicating data and preserves human-readable artifacts.
|
|
44
|
+
|
|
45
|
+
## 4. Architecture
|
|
46
|
+
|
|
47
|
+
Five components, each with one responsibility and a clean seam so the interactive path can later slot in as a second execution adapter.
|
|
48
|
+
|
|
49
|
+
| Component | File | Responsibility |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| **Phase state machine** | `bin/engine/phases.mjs` | Declarative table encoding the canonical phase sequence (today's prose in `work-on.md`). Each entry: `id`, `entryCondition(state)`, `command` (spec to run), `terminal?`, `reconcile(state)`. |
|
|
52
|
+
| **Engine loop** | `bin/engine.mjs` | The durable driver. Loads state, picks the next phase deterministically, invokes the phase, commits, handles failure. |
|
|
53
|
+
| **LLM-execution adapter** | `bin/runner.mjs` *(existing)* | Runs one phase = one bounded `runCommand()` with a **fresh context window**. Loop unchanged; new caller. |
|
|
54
|
+
| **Run-log store** | `bin/engine/runlog.mjs` | Append-only `.forge/runs/{issue}.jsonl`. Crash-safe local hot path. |
|
|
55
|
+
| **GitHub adapter (projector)** | `bin/engine/projector.mjs` | Reads/writes the `FORGE:STATE` block; manages the lease. Durable shared mirror + data source for `forgedock watch` (#1312). |
|
|
56
|
+
|
|
57
|
+
The engine core (`phases`, `engine`, `runlog`, `projector`) is runtime-agnostic; `runner.mjs` is the LLM adapter; the projector is the GitHub adapter.
|
|
58
|
+
|
|
59
|
+
### Engine loop (shape)
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
state = load(issue) // local log → else FORGE:STATE → else fresh
|
|
63
|
+
while (next = pickPhase(state)) { // ENGINE decides; never the LLM
|
|
64
|
+
if (next.terminal) break
|
|
65
|
+
reconcile(state, next) // make re-entry safe (idempotency)
|
|
66
|
+
result = runner.runCommand(next.command, ctx(state)) // fresh context per phase
|
|
67
|
+
switch (result.status) {
|
|
68
|
+
case 'committed': commit(state, next, result.outputs); break // append log + mirror
|
|
69
|
+
case 'failed': if (attempts(next) < maxAttempts) retry; else block(state, next); break
|
|
70
|
+
case 'blocked': terminate(state, 'needs-human', result.detail); return
|
|
71
|
+
}
|
|
72
|
+
} // no event on crash → next run drops + re-runs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 5. State model
|
|
76
|
+
|
|
77
|
+
### 5.1 Run-log (`.forge/runs/{issue}.jsonl`)
|
|
78
|
+
|
|
79
|
+
Append-only, one JSON event per line. `seq` is a monotonic per-issue version counter.
|
|
80
|
+
|
|
81
|
+
```jsonc
|
|
82
|
+
{"seq":1,"event":"RUN_START","issue":42,"run":"r_a1","lane":"staging","baseSha":"9ea38…"}
|
|
83
|
+
{"seq":2,"event":"PHASE_START","phase":"investigate"}
|
|
84
|
+
{"seq":3,"event":"PHASE_COMMIT","phase":"investigate","outputs":{"verdict":"CONFIRMED","decompose":false}}
|
|
85
|
+
{"seq":4,"event":"PHASE_START","phase":"build"}
|
|
86
|
+
{"seq":5,"event":"PHASE_FAILED","phase":"build","attempt":1,"reason":"tsc error"}
|
|
87
|
+
{"seq":6,"event":"PHASE_START","phase":"build"}
|
|
88
|
+
{"seq":7,"event":"PHASE_COMMIT","phase":"build","outputs":{"branch":"fix/pay-42","headSha":"…","skipped":false}}
|
|
89
|
+
{"seq":8,"event":"RUN_TERMINAL","reason":"merged"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Events: `RUN_START`, `PHASE_START`, `PHASE_COMMIT`, `PHASE_FAILED`, `RUN_TERMINAL`.
|
|
93
|
+
|
|
94
|
+
**The commit rule (the entire durability story):** a phase is *committed* iff a `PHASE_COMMIT` follows its latest `PHASE_START`. A `PHASE_START` with no following `PHASE_COMMIT` = a crashed / in-flight phase → **dropped and re-run**.
|
|
95
|
+
|
|
96
|
+
**Corruption tolerance:** a crash mid-write leaves a truncated final line; the reader ignores any final line that does not parse as complete JSON.
|
|
97
|
+
|
|
98
|
+
### 5.2 GitHub mirror (`FORGE:STATE`)
|
|
99
|
+
|
|
100
|
+
One machine-readable HTML-comment block the projector **overwrites in place** (not appends) after each `PHASE_COMMIT`:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<!-- FORGE:STATE
|
|
104
|
+
{"v":7,"run":"r_a1","phase":"review","committed":["investigate","context","architect","build"],
|
|
105
|
+
"branch":"fix/pay-42","pr":null,"lane":"staging","terminal":false,
|
|
106
|
+
"lease":{"by":"agent_7","until":"2026-07-03T04:10Z"}}
|
|
107
|
+
-->
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`v` mirrors the run-log `seq` at last commit — the shared version used for divergence detection. It carries the **compact index only** (see locked decision 4).
|
|
111
|
+
|
|
112
|
+
### 5.3 Resume + divergence (`GitHub wins`)
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
onStart(issue):
|
|
116
|
+
local = readRunLog(issue) // absent on fresh clone / other machine
|
|
117
|
+
remote = projector.read(issue) // FORGE:STATE, may be absent
|
|
118
|
+
state = reconcile(local, remote)
|
|
119
|
+
|
|
120
|
+
reconcile(local, remote):
|
|
121
|
+
if !remote: return local ?? fresh
|
|
122
|
+
if !local: return hydrateFromRemote(remote) // fresh clone / other machine
|
|
123
|
+
if remote.v > local.lastSeq: return hydrateFromRemote(remote) // advanced elsewhere → GitHub wins, discard local
|
|
124
|
+
if remote.v < local.lastSeq: projector.write(fromLocal(local)); return local // we crashed pre-mirror → re-mirror, keep local
|
|
125
|
+
return local // in sync
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`hydrateFromRemote` reconstructs working state from the compact index plus, where a phase's rich outputs are needed downstream, the existing `FORGE:*` structured comments on the issue.
|
|
129
|
+
|
|
130
|
+
## 6. Deterministic transitions & idempotency
|
|
131
|
+
|
|
132
|
+
**`pickPhase(state)`** = the first phase in the `phases.mjs` table whose `entryCondition(state)` holds and whose `id` is not in `state.committed`. If none qualify or a terminal condition holds → terminal. No LLM involvement — this is the fix for the stall class.
|
|
133
|
+
|
|
134
|
+
**`phase.reconcile(state)`** runs *before* the LLM on every entry, because a dropped phase re-runs and must be safe to re-enter:
|
|
135
|
+
|
|
136
|
+
- `build.reconcile`: branch already exists and is ahead of base → resume rather than re-create.
|
|
137
|
+
- `review.reconcile`: a PR already exists for the branch → adopt it, do not open a second.
|
|
138
|
+
- `close.reconcile`: PR already merged → proceed straight to close steps.
|
|
139
|
+
|
|
140
|
+
These leverage the existing `FORGE:*` markers and `gh` queries; the engine makes idempotency a required step rather than an ad-hoc convention.
|
|
141
|
+
|
|
142
|
+
## 7. Failure policy
|
|
143
|
+
|
|
144
|
+
Each phase returns a typed result; the engine acts on the **type** (replacing prose "try again / do not stop"):
|
|
145
|
+
|
|
146
|
+
| Result | Meaning | Engine action |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `committed` | phase produced outputs | write `PHASE_COMMIT`, advance |
|
|
149
|
+
| `failed` | transient (rate-limit, network, `gh` timeout, context-budget hit mid-phase) | `PHASE_FAILED`; retry same phase with backoff up to `maxAttempts` (default 3); on exhaustion → treat as `blocked` |
|
|
150
|
+
| `blocked` | deterministic human-needed (gate failed after loop, contract wrong, ancestry violation, push rejected) | `RUN_TERMINAL{needs-human}`, set label via projector, **stop visibly** |
|
|
151
|
+
| *(no event)* | crash / kill / compaction | next run drops the uncommitted `PHASE_START` and re-runs the phase |
|
|
152
|
+
|
|
153
|
+
**Visible skips (closes #1306):** a phase that no-ops (e.g. missing `verification.commands`) must emit `outputs:{skipped:true, which:"typecheck"}` into its `PHASE_COMMIT` and `FORGE:STATE`. A skip is surfaced (PR body / trajectory / review verdict), never a silent pass.
|
|
154
|
+
|
|
155
|
+
**"Prompt too long" (dissolves #1305):** fresh context per phase removes the dominant cause. If a *single* phase still exhausts context mid-run, it is a `failed` attempt whose `reconcile` resumes from committed artifacts on retry. Note honestly: an oversized single-phase input (e.g. a 10k-line review diff) remains the review phase's own concern — the engine bounds it (`runner` tool-result caps + iteration budget) but does not shrink it; that is the verification/review spec's responsibility.
|
|
156
|
+
|
|
157
|
+
## 8. Concurrency & orchestrator integration
|
|
158
|
+
|
|
159
|
+
**Lease.** `FORGE:STATE.lease = {by: agentId, until: ts}` is written on `RUN_START`, renewed on each `PHASE_COMMIT`, released on `RUN_TERMINAL`. On start, if a live lease held by another agent exists, the caller defers. This is best-effort mutual exclusion (checked on start; not an atomic CAS) — GitHub issue-edit has no compare-and-swap, so a concurrent start can still race between the lease read and the next write. It replaces the orchestrator's fuzzy "already in progress" label heuristic with a much tighter, race-*reducing* primitive.
|
|
160
|
+
|
|
161
|
+
**`/orchestrate` becomes a thin driver:**
|
|
162
|
+
- Decomposes into the wave/DAG (unchanged), then calls `engine.run(issue)` per issue.
|
|
163
|
+
- Watches `FORGE:STATE` per issue for progress — which also feeds `forgedock watch` (#1312) and removes any dependence on JSONL output files (killing the Windows-blind audit problem #1307 for orchestrated runs).
|
|
164
|
+
- **Exact stall detection:** a lease whose `until` expired on a non-terminal state = a dead agent → re-spawn `engine.run(issue)`, which resumes from the last `PHASE_COMMIT`. This replaces "resume up to 3× with nagging."
|
|
165
|
+
- File-overlap serialization remains the orchestrator's responsibility (unchanged), aided by the engine's `baseSha`/branch tracking.
|
|
166
|
+
|
|
167
|
+
## 9. Testing strategy
|
|
168
|
+
|
|
169
|
+
Correctness of a durable engine is won in tests. `runner.mjs` already lazy-imports the SDK and separates pure helpers, so the engine is tested with a **fake `runCommand`** that returns scripted phase outputs — no live LLM.
|
|
170
|
+
|
|
171
|
+
- **State-machine units** (`bin/tests/phases.test.mjs`, `engine.test.mjs`): table-driven `pickPhase` over crafted states; `reconcile` across all divergence cases (local-only, remote-only, remote-ahead, local-ahead, in-sync); the commit-detection rule.
|
|
172
|
+
- **Crash-injection harness** (the durability proof): run the engine against the fake runner, kill after each phase boundary *and* mid-phase, assert resume lands on the correct phase with no double-PR / double-commit.
|
|
173
|
+
- **Run-log** (`runlog.test.mjs`): append/read/replay; truncated-final-line tolerance.
|
|
174
|
+
- **Projector** (`projector.test.mjs`): `FORGE:STATE` serialize/parse round-trip; overwrite-in-place; lease acquire / renew / expire against a mocked `gh`.
|
|
175
|
+
- **Per-phase `reconcile`** idempotency tests (build resumes, review adopts existing PR, close handles already-merged).
|
|
176
|
+
|
|
177
|
+
## 10. Open implementation-time decision (defer to the plan)
|
|
178
|
+
|
|
179
|
+
Whether the engine runs concurrent issues as **separate processes** (matches today's background-agent model; strong isolation) or an **in-process worker pool** (one place to observe; shared rate-limit backpressure). Recommendation: **in-process pool + a git worktree per issue** for filesystem isolation (worktrees are already the pattern). Decide during planning.
|
|
180
|
+
|
|
181
|
+
## 11. Relationship to existing code & the epic
|
|
182
|
+
|
|
183
|
+
- **Reuses:** `runner.mjs` (`runCommand`, tool handlers, result caps, lazy SDK) as the unmodified LLM adapter; the `work-on` phase sequence as the seed for `phases.mjs`; the existing `FORGE:*` structured comments as rich-output storage; git worktrees for isolation.
|
|
184
|
+
- **Retires / fixes:** the phantom numbered-state router (#1308), the "≥20 Skill calls" heuristic and "prompt too long" deaths (#1305), silent verification skips (#1306), and — for orchestrated runs — the JSONL-based audit blindness (#1307).
|
|
185
|
+
- **Enables (as substrate, not in scope):** the run-log's structured `outputs` and `FORGE:STATE` are deliberately the foundation for verification (#1315), learning memory (#1316), economics (#1317), and provenance (#1318).
|
|
186
|
+
|
|
187
|
+
## 12. Definition of done
|
|
188
|
+
|
|
189
|
+
- A headless `engine.run(issue)` drives an issue through the full phase sequence with the engine deciding every transition.
|
|
190
|
+
- Killing the process at any phase boundary or mid-phase resumes correctly on the next run, with no duplicated PRs/commits (crash-injection suite green).
|
|
191
|
+
- A fresh clone / second machine resumes a run from `FORGE:STATE` alone.
|
|
192
|
+
- `/orchestrate` drives issues via `engine.run`, coordinates via the lease, and detects/recovers stalls via lease expiry.
|
|
193
|
+
- Skipped verification is visible on the PR; a `blocked` result stops the run with `needs-human`, never silently.
|
|
194
|
+
- Unit + crash-injection + projector/lease test suites pass with a mocked runner and mocked `gh`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/RapierCraftStudios/ForgeDock.git"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://github.com/RapierCraftStudios/ForgeDock#readme",
|
|
31
31
|
"bugs": {
|
|
32
32
|
"url": "https://github.com/RapierCraftStudios/ForgeDock/issues"
|
|
33
33
|
},
|
|
@@ -43,13 +43,19 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "node --test \"bin/tests/**/*.test.mjs\""
|
|
46
|
+
"test": "node --test \"bin/tests/**/*.test.mjs\"",
|
|
47
|
+
"docs:dev": "vitepress dev docs/site",
|
|
48
|
+
"docs:build": "vitepress build docs/site",
|
|
49
|
+
"docs:preview": "vitepress preview docs/site"
|
|
47
50
|
},
|
|
48
51
|
"engines": {
|
|
49
52
|
"node": ">=18"
|
|
50
53
|
},
|
|
51
54
|
"optionalDependencies": {
|
|
52
|
-
"@anthropic-ai/sdk": ">=0.30.0"
|
|
55
|
+
"@anthropic-ai/sdk": ">=0.30.0 <1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"vitepress": "^1.6.3"
|
|
53
59
|
},
|
|
54
60
|
"peerDependencies": {
|
|
55
61
|
"@anthropic-ai/claude-code": ">=2.0.0"
|
|
@@ -179,8 +179,24 @@ for var in $VAR_NAMES; do
|
|
|
179
179
|
fi
|
|
180
180
|
|
|
181
181
|
if [ -z "$FOUND_IN" ]; then
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
# Determine whether any of the optional surfaces exist in this repo.
|
|
183
|
+
# If NONE of the known surfaces exist (not even .env.example), the repo
|
|
184
|
+
# uses a layout we can't validate — emit SKIP instead of a false BLOCKING.
|
|
185
|
+
# This is the verify-sops-chain.sh pattern: unknown layout → graceful skip,
|
|
186
|
+
# not a spurious failure. <!-- Added: forge#1349 -->
|
|
187
|
+
HAS_ANY_SURFACE=0
|
|
188
|
+
[ -f "$REPO_ROOT/.env.example" ] && HAS_ANY_SURFACE=1
|
|
189
|
+
[ -f "$REPO_ROOT/docker-compose.yml" ] && HAS_ANY_SURFACE=1
|
|
190
|
+
[ -f "$REPO_ROOT/docker-compose.prod.yml" ] && HAS_ANY_SURFACE=1
|
|
191
|
+
[ -f "$REPO_ROOT/services/api/app/core/env_validation.py" ] && HAS_ANY_SURFACE=1
|
|
192
|
+
[ -f "$REPO_ROOT/scripts/decrypt-secrets.sh" ] && HAS_ANY_SURFACE=1
|
|
193
|
+
|
|
194
|
+
if [ "$HAS_ANY_SURFACE" -eq 0 ]; then
|
|
195
|
+
echo "SKIP: $var — no known env-wiring surfaces found in repo (no .env.example, docker-compose, env_validation.py, or SOPS mapping). Configure verification.env_check_surfaces in forge.yaml or add .env.example to enable this check."
|
|
196
|
+
else
|
|
197
|
+
echo "BLOCKING: $var — not found in any detected env-wiring surface (.env.example, docker-compose.yml, env_validation.py, or decrypt-secrets.sh)"
|
|
198
|
+
BLOCKING=$((BLOCKING + 1))
|
|
199
|
+
fi
|
|
184
200
|
else
|
|
185
201
|
echo "OK: $var — found in:$FOUND_IN"
|
|
186
202
|
fi
|
|
@@ -67,9 +67,9 @@ while read -r f; do
|
|
|
67
67
|
elif echo "$CONTENT" | grep -qE "\-\-header\s+[\"']Host:"; then
|
|
68
68
|
echo "OK: $f:$LINENO — Host header explicitly set (--header)"
|
|
69
69
|
else
|
|
70
|
-
echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header.
|
|
70
|
+
echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. Host-validation middleware will reject a raw IP as the Host value."
|
|
71
71
|
echo " Line: $CONTENT"
|
|
72
|
-
echo " Fix: Add -H 'Host:
|
|
72
|
+
echo " Fix: Add -H 'Host: <allowed-hostname>' — check your service's allowed_hosts configuration for the correct value"
|
|
73
73
|
BLOCKING=$((BLOCKING + 1))
|
|
74
74
|
fi
|
|
75
75
|
done < <(echo "$CURL_LINES")
|
|
@@ -96,11 +96,11 @@ if [ -n "$FRONTEND_FILES" ]; then
|
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
98
|
if [ -n "$DIRECT_CALLS" ]; then
|
|
99
|
-
echo "BLOCKING: $f — direct /api/v1/ call bypasses
|
|
99
|
+
echo "BLOCKING: $f — direct /api/v1/ call bypasses frontend proxy (auth middleware will be skipped)"
|
|
100
100
|
while IFS= read -r match; do
|
|
101
101
|
echo " $match"
|
|
102
102
|
done < <(echo "$DIRECT_CALLS")
|
|
103
|
-
echo " Fix: Use
|
|
103
|
+
echo " Fix: Use the frontend proxy route (e.g. /api/*) instead of calling the backend API directly"
|
|
104
104
|
BLOCKING=$((BLOCKING + 1))
|
|
105
105
|
else
|
|
106
106
|
echo "OK: $f — no direct backend calls"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# changed_files_file: path to a file listing changed files (one per line)
|
|
6
6
|
# repo_root: path to the repository root
|
|
7
7
|
#
|
|
8
|
-
# Layout path overrides (env vars — override defaults for
|
|
8
|
+
# Layout path overrides (env vars — override defaults for project-specific layouts):
|
|
9
9
|
# FORGE_PAGES_ROOT Root of Next.js App Router pages dir (default: web/src/app)
|
|
10
10
|
# FORGE_API_ROUTERS_DIR Dir containing FastAPI router .py files (default: services/api/app/routers)
|
|
11
11
|
# FORGE_API_MAIN Path to API app entrypoint, relative to repo root (default: services/api/app/main.py)
|
|
@@ -35,7 +35,7 @@ else
|
|
|
35
35
|
FILES=$(cat "$CHANGED_FILES")
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
-
# Layout path configuration — read from env vars, fall back to
|
|
38
|
+
# Layout path configuration — read from env vars, fall back to project defaults
|
|
39
39
|
FORGE_PAGES_ROOT="${FORGE_PAGES_ROOT:-web/src/app}"
|
|
40
40
|
FORGE_API_ROUTERS_DIR="${FORGE_API_ROUTERS_DIR:-services/api/app/routers}"
|
|
41
41
|
FORGE_API_MAIN="${FORGE_API_MAIN:-services/api/app/main.py}"
|