@guidobuilds/forge-ai 0.7.0 → 0.8.0
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/CHANGELOG.md +50 -0
- package/README.md +44 -57
- package/artifacts/forge/forge.md +7 -4
- package/artifacts/forge-adversary/forge-adversary.md +3 -0
- package/artifacts/forge-worker/forge-worker.md +4 -5
- package/artifacts/forge-worker-leaf/forge-worker-leaf.md +3 -0
- package/artifacts/using-forge/using-forge.md +2 -2
- package/bin/forge-ai.mjs +1 -1
- package/dist/src/adapters/claude-known.js +6 -2
- package/dist/src/adapters/codex-known.js +9 -0
- package/dist/src/adapters/codex.js +6 -1
- package/dist/src/adapters/grok.js +3 -0
- package/dist/src/adapters/opencode-known.js +15 -0
- package/dist/src/adapters/opencode.js +20 -4
- package/dist/src/adapters/shared.js +15 -1
- package/dist/src/cli.js +500 -49
- package/dist/src/compose.js +12 -0
- package/dist/src/dispatch-snippets.js +56 -0
- package/dist/src/executable-resolution.js +49 -0
- package/dist/src/index.js +0 -1
- package/dist/src/manifest.js +172 -14
- package/dist/src/model-preferences.js +41 -0
- package/dist/src/opencode-discovery.js +32 -0
- package/dist/src/paths.js +55 -3
- package/dist/src/platform-capabilities.js +17 -0
- package/dist/src/processor.js +66 -16
- package/dist/src/self-update.js +50 -4
- package/dist/src/writer.js +10 -12
- package/package.json +2 -2
- package/dist/src/adapters/claude-plugin.js +0 -43
- package/dist/src/adapters/codex-plugin.js +0 -98
- package/dist/src/build-plugin.js +0 -69
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,56 @@ Versions prior to 0.3.0 are not reconstructed here; see git history for earlier
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.8.0] - 2026-09-03
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`forge-ai list`** — prints every recorded install (the user-scope one, if any, plus one line per project) with Forge version, file count, platforms, and last-updated time. Answers "where is Forge installed and with what," which previously required manually reading manifest JSON files.
|
|
17
|
+
- `tests/fixtures/<platform>/<name>.golden` — committed golden-render snapshots (6 artifacts × 4 platforms) guarding against unintended drift in installed prose. Regenerate with `npm run generate-fixtures` after reviewing the diff.
|
|
18
|
+
- **Model selection.** `install`/`update` now offer an interactive model-selection step (per artifact × platform, gated on which platforms actually support a model — 3 of 4 silently drop it on skills) and accept `--model <id>` / `--model-map name=model,...` for non-interactive use (both require an explicit single `--platform`, since model ids are not portable across platforms). Choices persist in `model-preferences.json` next to the manifest and survive later `update` runs instead of silently resetting to the canonical default.
|
|
19
|
+
- **`forge-ai configure`** — change an already-installed agent's model without a full reinstall. Interactive by default; non-interactive via `--model`/`--model-map`. Rewrites only the affected files.
|
|
20
|
+
- **`forge-ai uninstall`** — the CLI's only removal path was previously "delete the files by hand"; now a manifest-driven uninstall mirrors `update`'s overwrite protection (a locally-edited file is backed up and requires `--yes`/`--force` or interactive confirmation before removal). Supports `--platform` to uninstall a single agent and `--dry-run` to preview.
|
|
21
|
+
- **OpenCode model discovery.** The per-agent model prompt (`install` and `configure`) shells out to `opencode models` (falling back to `opencode2 models` on v2) to offer exactly the models the user has real, credential-backed access to, instead of a generic list — OpenCode has no fixed model enum and its config file is not a reliable source of truth (env-var- and credential-connected providers appear in neither). Falls back to free-text entry if neither binary is available, or if a v2 install predates the `models` command (added to `opencode2` on 2026-08-06). `OPENCODE_UNKNOWN_MODEL` / `CODEX_UNKNOWN_MODEL` warnings added, matching the existing Claude/Grok pattern (OpenCode/Codex previously had no model validation at all).
|
|
22
|
+
- **OpenCode user-scope install now targets both v1 and v2, whichever actually exist.** v1 (`~/.config/opencode/`) and the v2 preview (`~/.opencode/`) read agents/skills from different directories — confirmed via `opencode2 debug config`, which doesn't list `~/.config/opencode` as a source at all. Installing only ever wrote v1's path, so a v2-only setup (no `opencode` v1 binary ever run) got files on disk that OpenCode itself would never read — `forge` silently missing from the agent list with no error. `install`/`update` now write to each generation's directory only if it already exists (`OPENCODE_USER_ROOT_NOT_FOUND` info diagnostic, defaulting to v1, if neither does yet); both get written if both exist. Project scope is unaffected — v1 and v2 already share `.opencode/` there.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`forge` installs as a real Claude Code subagent, not a skill.** Claude subagents can now spawn subagents up to depth 5 (v2.1.172+), so the historical blocker for a Claude-side orchestrator is gone. `forge` now has a structural `tools` allowlist (`Agent(forge-worker, forge-adversary)`, `TodoWrite`, `Skill`, `AskUserQuestion`) instead of being a fully advisory skill — it cannot Read/Write/Edit/Bash itself. It still keeps `AskUserQuestion` (confirmed: that tool is only stripped from bounded sub-task dispatches, not from an agent running as the session's main driver) and still loads `using-forge`/`forge-grill` via the `Skill` tool. To make it the automatic default for a project, add `{ "agent": "forge" }` to `.claude/settings.json` yourself — the installer does not write this for you (see README).
|
|
27
|
+
- **Codex agents declare an explicit `sandbox_mode`** — `read-only` for `forge` (it only dispatches), `workspace-write` for `forge-worker`/`forge-worker-leaf`/`forge-adversary` (they write files). Previously unset on all four.
|
|
28
|
+
- Grok and OpenCode installs now warn (`GROK_SKILL_DISCOVERABLE_UNENFORCED` / `OPENCODE_SKILL_DISCOVERABLE_UNENFORCED`) when installing a background-only skill (`using-forge`, `forge-grill`) that Claude can hide from direct invocation but Grok/OpenCode cannot — previously silent.
|
|
29
|
+
- **Per-user install state moved from `~/.forge-ai/` to `~/.forge/state/`.** Migrated automatically and transparently on first use of any command that touches it (`install`, `update`, `uninstall`, `list`) — the old directory is copied, never moved, and left in place with a `MIGRATED` note; nothing is deleted automatically. Nesting under `state/` means this can never collide with a project's own `.forge/<feature-slug>/` ledger, even in a repo where `$HOME` itself has been used as a Forge project.
|
|
30
|
+
- **The manifest now records the installed Forge version** (`forgeVersion`, both per-entry and per-manifest) and **merges across separate single-`--platform` runs instead of replacing** — `install --platform claude` followed by `install --platform opencode` no longer causes the first platform's files to reclassify as untracked/foreign. Existing `schemaVersion: 1` manifests are upgraded transparently on read (`forgeVersion: "unknown"`).
|
|
31
|
+
- Install/update/uninstall now warn if a legacy `~/.forge-ai/` manifest was updated more recently than the current `~/.forge/state/` one — a sign a cached old CLI is still writing to the old location.
|
|
32
|
+
- **Per-harness dispatch vocabulary in canonical artifact bodies is now composed per platform instead of hardcoded.** `forge-worker.md`, `forge.md`, and `using-forge.md` no longer spell out "via `Agent` (Claude), `task` (Grok), or `task` (OpenCode)" or a standalone "Codex fallback" section inline — a `{{snippet:id}}` reference is resolved per target platform at render time (`src/compose.ts`, `src/dispatch-snippets.ts`), failing loudly at build time if a platform has no text for a referenced snippet. This is a genuine prose change on Claude/Grok/OpenCode/Codex installs, not a no-op refactor — installed agent bodies now read slightly differently (equivalent meaning, phrased per platform instead of listing all four in one sentence). `BODY_OVER_BUDGET` is now measured on the composed, per-platform body rather than the shared canonical source.
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
36
|
+
- **Claude Code and Codex plugin marketplace distribution channels** (`forge-plugin/`, `plugins/forge/`, `.claude-plugin/`, `.agents/plugins/marketplace.json`, `src/adapters/claude-plugin.ts`, `src/adapters/codex-plugin.ts`, `src/build-plugin.ts`, the `forge-ai build-plugin` command, the plugin drift guard (`.githooks/`, `scripts/check-plugin-sync.mjs`, `scripts/verify-drift-guard.sh`), and 16 plugin-specific tests). The npm CLI (`npx @guidobuilds/forge-ai install`) is now the single supported distribution and install method for all four platforms, including Claude Code and Codex — both of which previously also had an alternative plugin-marketplace install path shipped in 0.7.0.
|
|
37
|
+
- `PluginFile` type and `writePluginFiles` (`src/model.ts`, `src/writer.ts`).
|
|
38
|
+
- Public export `export * from './adapters/codex-plugin.js'` from `src/index.ts` — anyone importing `@guidobuilds/forge-ai` programmatically for Codex plugin generation loses that entry point.
|
|
39
|
+
- `prepare`/`pretest` npm scripts, which existed solely to wire the now-removed pre-commit drift guard.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- README no longer claims the Claude Code plugin channel is "Not yet live" (it had been live since 0.7.0; the line was never updated).
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
An internal application-security audit (`report/api-report.md`, `report/api-fix.md`) found 15 findings (`A`, `B`, `C`, `M1`–`M12`); the two highest-severity ones were proven exploitable end-to-end in an isolated adversarial gate (`report/verification.md`). This release fixes the two proven findings (`A`, `B`) plus four of the supporting ones (`C`, `M1`, `M2`, `M12`), with dedicated regression tests (`tests/forge-cli.test.ts`, +22 tests). The remaining nine (`M3`–`M11`, correctness/hygiene polish — see `report/summary.md` §5) are lower-priority and not security-critical; they remain open.
|
|
48
|
+
|
|
49
|
+
- **Install-manifest entries are now scope-validated before any prune/uninstall delete or overwrite** (`src/paths.ts`, `src/manifest.ts`, `src/cli.ts`). `loadManifest` previously trusted any parseable JSON, and `pruneEntries`/`uninstall` deleted or overwrote `entry.path` with no check that it fell under a Forge-managed root — a poisoned `~/.forge/state/*.json` manifest was proven able to delete an arbitrary out-of-scope file, including via a symlinked path. Entries outside the per-scope allowlist (derived from the same `userBase`/`projectBase` roots as normal installs) are now rejected before any destructive operation.
|
|
50
|
+
- **Subprocess binaries are now resolved via an explicit `PATH` scan against an allowlist of trusted basenames, and repo-local shims are rejected** (`src/executable-resolution.ts`, `src/self-update.ts`, `src/opencode-discovery.ts`). Model discovery and self-update previously `spawnSync`'d bare command names (`opencode`, `pnpm`, `npm`, `forge-ai`) resolved from the ambient `PATH`, with model discovery additionally passing `cwd = process.cwd()` — a shim named `opencode` placed on `PATH` (e.g. via `node_modules/.bin`) was proven to execute with the user's privileges. Discovery no longer passes `cwd`, and every spawn now resolves to an absolute, allowlisted path first.
|
|
51
|
+
- **`self-update --to <version>` now allowlists its argument to a semver or `latest` spec** (`src/self-update.ts`, `src/cli.ts`) before it reaches `npm`/`pnpm install -g` — previously an arbitrary string was interpolated directly into the install command.
|
|
52
|
+
- **Manifest and model-preferences writes are now atomic** (temp-file + rename) (`src/manifest.ts`, `src/model-preferences.ts`), closing a TOCTOU window where an interrupted write could leave a corrupt state file.
|
|
53
|
+
- **Corrupt manifest/preferences files now fail safe instead of crashing the CLI with a raw stack trace** — malformed JSON or a non-object shape falls back to empty state with a diagnostic pointing at `forge-ai install` to regenerate it (`src/manifest.ts`, `src/model-preferences.ts`).
|
|
54
|
+
- **OpenCode adapter now validates `permissions` shape before emitting** (`src/adapters/opencode.ts`, `src/adapters/shared.ts`) instead of passing an unvalidated value straight through to frontmatter, matching the validation Claude/Grok/Codex already had.
|
|
55
|
+
|
|
56
|
+
### Migration from 0.7.x
|
|
57
|
+
|
|
58
|
+
- If you installed Forge via `/plugin install forge@guidobuilds` (Claude Code) or `codex plugin add forge@guidobuilds-forge` (Codex), those marketplace sources no longer exist in this repository. Remove the plugin (`/plugin uninstall forge@guidobuilds` or `codex plugin remove forge@guidobuilds-forge`, then remove the marketplace) and install via the CLI instead: `npx @guidobuilds/forge-ai install --platform claude` or `--platform codex`.
|
|
59
|
+
- If you only ever used the CLI (`npx @guidobuilds/forge-ai install/update`), nothing changes for you.
|
|
60
|
+
- Programmatic consumers importing Codex plugin generation from `@guidobuilds/forge-ai`'s package export must vendor that logic themselves; it is no longer part of this package.
|
|
61
|
+
|
|
12
62
|
## [0.7.0] - 2026-08-06
|
|
13
63
|
|
|
14
64
|
### Added
|
package/README.md
CHANGED
|
@@ -94,13 +94,22 @@ The same operating model is shared across all supported agents so the workflow s
|
|
|
94
94
|
|
|
95
95
|
## Installation
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Install with the npm CLI:
|
|
98
98
|
|
|
99
99
|
```sh
|
|
100
100
|
npx @guidobuilds/forge-ai install
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
The installer prompts for the target agent platform
|
|
103
|
+
The installer prompts for the target agent platform, whether Forge should be installed globally for your user or locally for the current project, and — interactively — which model each agent should use: keep each agent's recommended default, or choose per agent. For OpenCode, the per-agent prompt shows the models you actually have configured (via `opencode models`, falling back to free-text entry if that command isn't available), not a generic list.
|
|
104
|
+
|
|
105
|
+
To choose models non-interactively (requires an explicit single `--platform` — model ids aren't portable across platforms):
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx @guidobuilds/forge-ai install --platform claude --scope user --model opus --yes
|
|
109
|
+
npx @guidobuilds/forge-ai install --platform opencode --scope user --model-map forge=anthropic/claude-opus-4-1,forge-worker=anthropic/claude-sonnet-4-5 --yes
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`--model` applies to every agent that supports a model on the target platform; `--model-map name=model,...` sets it per artifact. Choices persist across `update` — they are not silently reset back to the canonical default. To change a model later without a full reinstall, see [Configuring models](#configuring-models) below.
|
|
104
113
|
|
|
105
114
|
To update an existing install:
|
|
106
115
|
|
|
@@ -126,52 +135,6 @@ Validate a local Forge source tree:
|
|
|
126
135
|
npx @guidobuilds/forge-ai validate --source .
|
|
127
136
|
```
|
|
128
137
|
|
|
129
|
-
### Claude Code plugin (alternative to the CLI installer)
|
|
130
|
-
|
|
131
|
-
Claude Code users can also install Forge as a plugin, using Claude Code's built-in marketplace mechanism instead of (or alongside) the CLI installer above:
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
/plugin marketplace add guidobuilds/forge
|
|
135
|
-
/plugin install forge@guidobuilds
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Things to know before switching to this channel:
|
|
139
|
-
|
|
140
|
-
- **Claude Code channel.** Codex also has its own plugin channel (below); OpenCode and Grok Build remain CLI-only.
|
|
141
|
-
- **The entry point changes.** Plugin skills are always namespaced, so the orchestrator is invoked as `/forge:forge`, not the bare `/forge` used by a CLI-pushed install. Claude can still auto-load the `forge` skill from its description without you typing a slash command at all — this is Claude Code's general skill-invocation mechanism (skills are automatically discovered and can be invoked by Claude based on task context), not something specific to the plugin — a practical mitigation, but not a guaranteed substitute for the namespaced command.
|
|
142
|
-
- **Auto-update is off by default.** Official Anthropic-managed marketplaces have auto-update enabled by default; third-party marketplaces like `guidobuilds` default to auto-update off, so you need to explicitly run `/plugin update forge@guidobuilds` (or toggle auto-update on for the `guidobuilds` marketplace) to pick up new releases.
|
|
143
|
-
- **Same source, two channels.** The plugin package and the CLI-pushed files are both generated from the same canonical `artifacts/` source (via `forge-ai build-plugin`), so behavior is consistent — the plugin is not a fork of the CLI content.
|
|
144
|
-
- **Not yet live:** `/plugin marketplace add guidobuilds/forge` requires this repo's generated `forge-plugin/` and `.claude-plugin/` directories to be committed and pushed first (see the release process in `CONTRIBUTING.md`). The marketplace/install mechanics were independently verified end-to-end against a real git-clone-based install, but not yet against a pushed `guidobuilds/forge` itself.
|
|
145
|
-
|
|
146
|
-
### Codex plugin (alternative to the CLI installer)
|
|
147
|
-
|
|
148
|
-
Codex can install Forge from this repository as a standalone, skills-only plugin:
|
|
149
|
-
|
|
150
|
-
```sh
|
|
151
|
-
codex plugin marketplace add guidobuilds/forge --ref main
|
|
152
|
-
codex plugin add forge@guidobuilds-forge
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Open a new Codex session after installation. The plugin exposes one public `forge` skill and keeps the worker, leaf, adversary, routing, and grill contracts as private references. Codex plugins cannot register `.codex/agents/*.toml`, so Forge spawns standard/default Codex sub-agents and injects the complete applicable contract. When sub-agent spawning is unavailable it uses a documented sequential inline fallback.
|
|
156
|
-
|
|
157
|
-
This is independent from the direct CLI channel:
|
|
158
|
-
|
|
159
|
-
```sh
|
|
160
|
-
npx @guidobuilds/forge-ai install --platform codex --scope user
|
|
161
|
-
npx @guidobuilds/forge-ai update --platform codex --scope user
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Choose one channel when possible. Installing both does not overwrite the other channel's files, but it can expose duplicate Forge workflows. Update or remove Forge using the same channel used to install it.
|
|
165
|
-
|
|
166
|
-
To refresh or uninstall the marketplace channel:
|
|
167
|
-
|
|
168
|
-
```sh
|
|
169
|
-
codex plugin marketplace upgrade guidobuilds-forge
|
|
170
|
-
codex plugin remove forge@guidobuilds-forge
|
|
171
|
-
# Optional after removing Forge:
|
|
172
|
-
codex plugin marketplace remove guidobuilds-forge
|
|
173
|
-
```
|
|
174
|
-
|
|
175
138
|
## How to Use
|
|
176
139
|
|
|
177
140
|
Forge installs the **same operating model** on every agent, but **how you invoke it differs per platform**, because each agent exposes different primitives (skills, subagents, agent switching). The installer is only step one — this section is how you actually drive Forge once it is installed.
|
|
@@ -193,16 +156,22 @@ What changes per platform is the **kind** each piece is installed as, and theref
|
|
|
193
156
|
|
|
194
157
|
| Piece | Installed as | How you invoke it |
|
|
195
158
|
|---|---|---|
|
|
196
|
-
| `forge` |
|
|
159
|
+
| `forge` | subagent (`model: opus`, structural `tools` allowlist) | mention it by name, `@forge`, or `claude --agent forge` |
|
|
197
160
|
| `forge-grill` | skill (`model: sonnet`, not user-invocable) | loaded automatically by `forge` before non-trivial or risk-bearing builds |
|
|
198
161
|
| `using-forge` | skill (`model: sonnet`, not user-invocable) | loaded automatically by `forge` before routing work |
|
|
199
|
-
| `forge-worker` | subagent | coordinator;
|
|
162
|
+
| `forge-worker` | subagent | coordinator; `forge` delegates via `Agent(forge-worker, forge-adversary)` |
|
|
200
163
|
| `forge-worker-leaf` | subagent | terminal shard; spawned by `forge-worker` (or orchestrator on Codex) |
|
|
201
164
|
| `forge-adversary` | subagent | the orchestrator delegates to it after build to gate risk-bearing work |
|
|
202
165
|
|
|
203
|
-
|
|
166
|
+
`forge` installs as a real Claude Code subagent with a structural `tools` allowlist (`Agent(forge-worker, forge-adversary)`, `TodoWrite`, `Skill`, `AskUserQuestion`) — it cannot Read/Write/Edit/Bash itself, only dispatch. It loads `using-forge` and `forge-grill` via the `Skill` tool and can still ask you clarifying questions directly (`AskUserQuestion` is retained for an agent running as the session's main driver, unlike a bounded sub-task dispatch). Requires **Claude Code v2.1.172+**.
|
|
204
167
|
|
|
205
|
-
|
|
168
|
+
To make `forge` the automatic default for a project (equivalent to the old `/forge` auto-load), add to `.claude/settings.json`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{ "agent": "forge" }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Without that, invoke it explicitly per session (mention it by name, `@forge`, or `claude --agent forge`). `forge-ai install`/`update`/`uninstall` manage `forge`'s own file; they do not write `settings.json` for you.
|
|
206
175
|
|
|
207
176
|
### OpenCode
|
|
208
177
|
|
|
@@ -223,7 +192,7 @@ Switch your primary agent to **`forge`**. Unlike Claude Code, the orchestrator h
|
|
|
223
192
|
| `forge`, `forge-worker`, `forge-worker-leaf`, `forge-adversary` | agents (`.toml`) | `~/.codex/agents/` (or `.codex/agents/` per project) |
|
|
224
193
|
| `using-forge`, `forge-grill` | skills | `~/.agents/skills/` (or `.agents/skills/` per project) |
|
|
225
194
|
|
|
226
|
-
The
|
|
195
|
+
The CLI writes agent `.toml` files but does not generate `AGENTS.md` or profiles.
|
|
227
196
|
|
|
228
197
|
### Project vs user scope
|
|
229
198
|
|
|
@@ -303,15 +272,33 @@ npx @guidobuilds/forge-ai@latest update
|
|
|
303
272
|
|
|
304
273
|
Forge replaces its managed agent and skill definitions in your supported agent configuration directories.
|
|
305
274
|
|
|
306
|
-
Forge records installed files in manifests under `~/.forge-ai/` so updates can safely remove files that are no longer bundled. `update` prunes stale managed files by default only when the current file still matches the recorded checksum; use `--no-prune` to keep stale managed files. `--dry-run` previews writes and deletes without changing files or manifests.
|
|
275
|
+
Forge records installed files in manifests under `~/.forge/state/` (migrated automatically and transparently from the earlier `~/.forge-ai/` on first run — the old directory is left in place, marked with a `MIGRATED` note, and never deleted automatically) so updates can safely remove files that are no longer bundled. `update` prunes stale managed files by default only when the current file still matches the recorded checksum; use `--no-prune` to keep stale managed files. `--dry-run` previews writes and deletes without changing files or manifests.
|
|
307
276
|
|
|
308
|
-
Forge
|
|
277
|
+
Forge routes a single canonical artifact to the right artifact kind per agent: the orchestrator installs as a real subagent on Claude Code, OpenCode, and Codex, and as a skill on Grok. If you installed an earlier version where Claude's orchestrator was a skill, run `update` (not `install`) so Forge prunes the now-stale `~/.claude/skills/forge/SKILL.md` left by the previous layout.
|
|
309
278
|
|
|
310
279
|
## Uninstalling
|
|
311
280
|
|
|
312
|
-
|
|
281
|
+
```sh
|
|
282
|
+
npx @guidobuilds/forge-ai uninstall
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This removes exactly the files Forge's manifest recorded for the given `--scope` (default: `user`), refusing to silently delete anything you edited locally — a locally-modified file is backed up and requires `--yes`/`--force` (or interactive confirmation) before it's removed, mirroring `update`'s overwrite protection. Pass `--platform` to uninstall a single agent instead of all of them, and `--dry-run` to preview what would be removed.
|
|
286
|
+
|
|
287
|
+
## Listing installs
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
npx @guidobuilds/forge-ai list
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Prints every recorded install — the user-scope one, if any, plus one line per project you've installed Forge into — with the Forge version, file count, platforms, and last-updated time for each.
|
|
294
|
+
|
|
295
|
+
## Configuring models
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
npx @guidobuilds/forge-ai configure --platform claude --scope user
|
|
299
|
+
```
|
|
313
300
|
|
|
314
|
-
|
|
301
|
+
Change which model an already-installed agent uses without a full reinstall. Interactively, it walks every agent on the given scope/platform that supports a model (using OpenCode's live `opencode models` output where applicable, same as `install`). Non-interactively, pass `--model <id>` or `--model-map name=model,...` (same rules as `install`: an explicit single `--platform`, no `--force`/`--yes` — the point of the command is to make a choice, not skip one). Only the affected files are rewritten; the manifest and every other installed platform are left untouched.
|
|
315
302
|
|
|
316
303
|
## Project Status
|
|
317
304
|
|
package/artifacts/forge/forge.md
CHANGED
|
@@ -3,11 +3,14 @@ name: forge
|
|
|
3
3
|
description: Forge orchestrator with dynamic runtime routing and a single worker type
|
|
4
4
|
kind: agent
|
|
5
5
|
claude:
|
|
6
|
-
kind: skill
|
|
7
6
|
model: opus
|
|
8
|
-
|
|
7
|
+
permissions:
|
|
8
|
+
tools: ["Agent(forge-worker, forge-adversary)", TodoWrite, Skill, AskUserQuestion]
|
|
9
9
|
grok:
|
|
10
10
|
kind: skill
|
|
11
|
+
codex:
|
|
12
|
+
permissions:
|
|
13
|
+
sandbox_mode: read-only
|
|
11
14
|
opencode:
|
|
12
15
|
mode: primary
|
|
13
16
|
permissions:
|
|
@@ -49,7 +52,7 @@ The `using-forge` skill owns runtime routing, operating principles, approval heu
|
|
|
49
52
|
|
|
50
53
|
## Worker model
|
|
51
54
|
- `forge-worker` is the coordinator worker; route all build and operational work to it at `DISPATCH_DEPTH: 1`.
|
|
52
|
-
- `forge-worker-leaf` is the terminal worker for bounded shards at `DISPATCH_DEPTH: 2`;
|
|
55
|
+
- `forge-worker-leaf` is the terminal worker for bounded shards at `DISPATCH_DEPTH: 2`; {{snippet:who-spawns-leaf}}.
|
|
53
56
|
- `forge-adversary` is a dedicated adversarial verification agent: dispatch it as the Definition-of-Done gate for risk-bearing work to break the build before it can reach `passing`. Never sub-delegate verify or adversary work.
|
|
54
57
|
- You may launch one worker instance for a bounded task.
|
|
55
58
|
- You may launch multiple `forge-worker` instances in sequence when one result should shape the next delegation.
|
|
@@ -89,7 +92,7 @@ QUESTIONS:
|
|
|
89
92
|
1) <question>
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
`QUESTIONS` appears only when `STATUS: blocked`.
|
|
95
|
+
`QUESTIONS` appears only when `STATUS: blocked`. {{snippet:orchestrator-delegation-requests-handling}} Trust coordinator `SUB_RESULTS` unless `partial` or `blocked`.
|
|
93
96
|
|
|
94
97
|
If output is malformed:
|
|
95
98
|
1) request one reformat retry with same task_id
|
|
@@ -8,6 +8,9 @@ claude:
|
|
|
8
8
|
grok:
|
|
9
9
|
permissions:
|
|
10
10
|
tools: [todo_write, read_file, search_replace, run_terminal_cmd, grep_search, list_dir, web_fetch, task, get_task_output, kill_task]
|
|
11
|
+
codex:
|
|
12
|
+
permissions:
|
|
13
|
+
sandbox_mode: workspace-write
|
|
11
14
|
opencode:
|
|
12
15
|
mode: subagent
|
|
13
16
|
permissions:
|
|
@@ -154,14 +157,10 @@ Spawn `forge-worker-leaf` when `DELEGATION: required`, or when **any** trigger f
|
|
|
154
157
|
### Spawn protocol
|
|
155
158
|
|
|
156
159
|
1. Decompose into bounded leaf subgoals with disjoint `files_hint` paths.
|
|
157
|
-
2.
|
|
160
|
+
2. {{snippet:worker-spawn-leaf-instruction}}
|
|
158
161
|
3. Prefer **parallel** leaves for read-only `inspect`; prefer **sequential** leaves for `build` writes unless files are strictly disjoint.
|
|
159
162
|
4. Synthesize: write durable detail to `.forge/<slug>/explore.md` or `build-log.md`; return ≤ 8 `SUMMARY` bullets plus compact `SUB_RESULTS`. Do not paste full child logs.
|
|
160
163
|
|
|
161
|
-
### Codex fallback
|
|
162
|
-
|
|
163
|
-
On harnesses without spawn tools (Codex), return `DELEGATION_REQUESTS` for the orchestrator to fan out `forge-worker-leaf` dispatches. Omit `DELEGATION_REQUESTS` when you self-spawn.
|
|
164
|
-
|
|
165
164
|
## Concurrency discipline
|
|
166
165
|
|
|
167
166
|
When the orchestrator or sibling leaves may be running in parallel:
|
|
@@ -87,7 +87,7 @@ Forge uses **two levels below the orchestrator** (`DISPATCH_DEPTH` 0 → 1 → 2
|
|
|
87
87
|
|
|
88
88
|
### Coordinator triggers
|
|
89
89
|
|
|
90
|
-
`forge-worker` **must** spawn
|
|
90
|
+
`forge-worker` **must** {{snippet:worker-must-spawn-or-delegate}} when any: ≥ 8 file reads, ≥ 6 searches, ≥ 20 tool calls, ≥ 5 files to edit, `DELEGATION: required`, or plan implies ≥ 3 shards. **Never** sub-delegate `verify` or adversary work.
|
|
91
91
|
|
|
92
92
|
### Orchestrator dispatch hints
|
|
93
93
|
|
|
@@ -100,7 +100,7 @@ TASK_ID: <unique>
|
|
|
100
100
|
|
|
101
101
|
- Unfamiliar-repo `inspect` → `DELEGATION: allowed`
|
|
102
102
|
- Narrow bugfix → `DELEGATION: forbidden`
|
|
103
|
-
|
|
103
|
+
{{snippet:codex-orchestrator-dispatch-hint}}
|
|
104
104
|
|
|
105
105
|
## Routing rules
|
|
106
106
|
|
package/bin/forge-ai.mjs
CHANGED
|
@@ -14,11 +14,15 @@ export const knownClaudeTools = new Set([
|
|
|
14
14
|
'WebFetch',
|
|
15
15
|
'WebSearch',
|
|
16
16
|
'AskUserQuestion',
|
|
17
|
-
'ExitPlanMode'
|
|
17
|
+
'ExitPlanMode',
|
|
18
|
+
'Skill'
|
|
18
19
|
]);
|
|
19
20
|
const mcpToolPattern = /^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_*-]+$/;
|
|
21
|
+
// `Agent(name, name2)` restricts which subagent types an agent running as the main
|
|
22
|
+
// session (via --agent/settings.json) is allowed to spawn — see sub-agents.md.
|
|
23
|
+
export const agentAllowlistPattern = /^Agent\([A-Za-z0-9_-]+(?:,\s*[A-Za-z0-9_-]+)*\)$/;
|
|
20
24
|
export function isKnownClaudeTool(name) {
|
|
21
|
-
return knownClaudeTools.has(name) || mcpToolPattern.test(name);
|
|
25
|
+
return knownClaudeTools.has(name) || mcpToolPattern.test(name) || agentAllowlistPattern.test(name);
|
|
22
26
|
}
|
|
23
27
|
export const knownClaudeModels = new Set([
|
|
24
28
|
'sonnet',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Codex's current model-id list was not verified against live docs at the time this was written
|
|
2
|
+
// (unlike Claude/Grok/OpenCode, which have a confirmed format). Rather than invent a shape we
|
|
3
|
+
// haven't checked, this accepts any non-empty, whitespace-free value and lets Codex itself be the
|
|
4
|
+
// real check — the same "don't guess" discipline this codebase applies elsewhere. Tighten this
|
|
5
|
+
// once Codex's actual model-id format is verified against its docs.
|
|
6
|
+
const codexModelPattern = /^\S+$/;
|
|
7
|
+
export function isKnownCodexModel(value) {
|
|
8
|
+
return codexModelPattern.test(value);
|
|
9
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { stringifyYaml } from '../frontmatter.js';
|
|
2
2
|
import { diagnostic } from '../diagnostics.js';
|
|
3
3
|
import { isRecord, tomlString } from './shared.js';
|
|
4
|
+
import { isKnownCodexModel } from './codex-known.js';
|
|
4
5
|
const safeSandboxModes = new Set(['read-only', 'workspace-write']);
|
|
5
6
|
export function renderCodexAgent(artifact) {
|
|
6
7
|
const diagnostics = [diagnostic('info', 'CODEX_PARTIAL_AGENT_SUPPORT', `Codex agent output is partial and does not generate AGENTS.md or profiles for ${artifact.name}`, { platform: 'codex' })];
|
|
7
8
|
const lines = [`name = ${tomlString(artifact.name)}`, `description = ${tomlString(artifact.description)}`, `developer_instructions = ${tomlString(artifact.body)}`];
|
|
8
|
-
if (artifact.codex?.model)
|
|
9
|
+
if (artifact.codex?.model) {
|
|
9
10
|
lines.push(`model = ${tomlString(artifact.codex.model)}`);
|
|
11
|
+
if (!isKnownCodexModel(artifact.codex.model)) {
|
|
12
|
+
diagnostics.push(diagnostic('warning', 'CODEX_UNKNOWN_MODEL', `Unknown Codex model "${artifact.codex.model}" for ${artifact.name}`, { platform: 'codex' }));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
10
15
|
const permissions = artifact.codex?.permissions;
|
|
11
16
|
if (isRecord(permissions) && typeof permissions.sandbox_mode === 'string') {
|
|
12
17
|
if (safeSandboxModes.has(permissions.sandbox_mode))
|
|
@@ -37,5 +37,8 @@ export function renderGrokSkill(artifact) {
|
|
|
37
37
|
if (artifact.grok?.model) {
|
|
38
38
|
diagnostics.push(diagnostic('info', 'GROK_SKILL_MODEL_IGNORED', `Grok skill model is not emitted for ${artifact.name}`, { platform: 'grok' }));
|
|
39
39
|
}
|
|
40
|
+
if (artifact.claude?.['user-invocable'] === false) {
|
|
41
|
+
diagnostics.push(diagnostic('warning', 'GROK_SKILL_DISCOVERABLE_UNENFORCED', `${artifact.name} is meant to be background-only, but Grok has no mechanism to hide a skill from direct invocation — it will be directly invocable here`, { platform: 'grok' }));
|
|
42
|
+
}
|
|
40
43
|
return { content: `${stringifyYaml({ name: artifact.name, description: artifact.description })}${artifact.body}\n`, diagnostics };
|
|
41
44
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// OpenCode has no fixed model enum — models.dev's catalog, filtered to whatever providers the
|
|
2
|
+
// user has credentials for (env var, stored auth, or a config/plugin-declared provider). The
|
|
3
|
+
// `provider/model-id` format is verified (opencode.ai/v2/docs/models: "Provider and model IDs are
|
|
4
|
+
// case-sensitive"); the model id itself MAY contain further slashes (e.g.
|
|
5
|
+
// `openrouter/openai/gpt-5-chat` is provider `openrouter`, model `openai/gpt-5-chat`), so this
|
|
6
|
+
// pattern only requires a first segment before the first `/`, not exactly two segments.
|
|
7
|
+
// OpenCode itself does not validate `model` at load time — a bad value fails only when the agent
|
|
8
|
+
// first runs (ModelNotFoundError) — so this pattern check is a genuine safety net, not redundant.
|
|
9
|
+
// Prefer live discovery (`opencode models`, see src/opencode-discovery.ts) when available; this is
|
|
10
|
+
// the fallback for when that isn't possible (non-interactive install, opencode not on PATH, a v2
|
|
11
|
+
// install predating the `models` command, etc).
|
|
12
|
+
const openCodeModelPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*\/.+$/;
|
|
13
|
+
export function isKnownOpenCodeModel(value) {
|
|
14
|
+
return openCodeModelPattern.test(value);
|
|
15
|
+
}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { stringifyYaml } from '../frontmatter.js';
|
|
2
2
|
import { diagnostic } from '../diagnostics.js';
|
|
3
|
+
import { isKnownOpenCodeModel } from './opencode-known.js';
|
|
4
|
+
import { isOpenCodePermissions } from './shared.js';
|
|
3
5
|
export function renderOpenCodeAgent(artifact) {
|
|
6
|
+
const diagnostics = [];
|
|
4
7
|
const fm = { description: artifact.description };
|
|
5
8
|
if (artifact.opencode?.mode)
|
|
6
9
|
fm.mode = artifact.opencode.mode;
|
|
7
|
-
if (artifact.opencode?.model)
|
|
10
|
+
if (artifact.opencode?.model) {
|
|
8
11
|
fm.model = artifact.opencode.model;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
if (!isKnownOpenCodeModel(artifact.opencode.model)) {
|
|
13
|
+
diagnostics.push(diagnostic('warning', 'OPENCODE_UNKNOWN_MODEL', `Unknown OpenCode model "${artifact.opencode.model}" for ${artifact.name}`, { platform: 'opencode' }));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (artifact.opencode?.permissions) {
|
|
17
|
+
if (isOpenCodePermissions(artifact.opencode.permissions)) {
|
|
18
|
+
fm.permission = artifact.opencode.permissions;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
diagnostics.push(diagnostic('warning', 'OPENCODE_INVALID_PERMISSIONS', `opencode.permissions for ${artifact.name} is not a flat allow/deny/ask object (≤ 64 scalar-valued keys); the block was not emitted`, { platform: 'opencode' }));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { content: `${stringifyYaml(fm)}${artifact.body}\n`, diagnostics };
|
|
12
25
|
}
|
|
13
26
|
export function renderOpenCodeSkill(artifact) {
|
|
14
27
|
const diagnostics = [];
|
|
@@ -16,5 +29,8 @@ export function renderOpenCodeSkill(artifact) {
|
|
|
16
29
|
diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_PERMISSIONS_IGNORED', `OpenCode skill permissions are not emitted for ${artifact.name}`, { platform: 'opencode' }));
|
|
17
30
|
if (artifact.opencode?.model)
|
|
18
31
|
diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_MODEL_IGNORED', `OpenCode skill model is not emitted for ${artifact.name}`, { platform: 'opencode' }));
|
|
32
|
+
if (artifact.claude?.['user-invocable'] === false) {
|
|
33
|
+
diagnostics.push(diagnostic('warning', 'OPENCODE_SKILL_DISCOVERABLE_UNENFORCED', `${artifact.name} is meant to be background-only, but OpenCode has no mechanism to hide a skill from direct invocation — it will be directly invocable here`, { platform: 'opencode' }));
|
|
34
|
+
}
|
|
19
35
|
return { content: `${stringifyYaml({ name: artifact.name, description: artifact.description })}${artifact.body}\n`, diagnostics };
|
|
20
36
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { agentAllowlistPattern } from './claude-known.js';
|
|
1
2
|
export function isRecord(value) {
|
|
2
3
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
4
|
}
|
|
4
5
|
const toolNamePattern = /^[A-Za-z0-9_*-]+$/;
|
|
5
6
|
const patternBodyPattern = /^[^,\n]+$/;
|
|
6
7
|
export function stringList(value) {
|
|
7
|
-
return Array.isArray(value) && value.every((item) => typeof item === 'string' && toolNamePattern.test(item)) ? value : undefined;
|
|
8
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string' && (toolNamePattern.test(item) || agentAllowlistPattern.test(item))) ? value : undefined;
|
|
8
9
|
}
|
|
9
10
|
export function patternList(value) {
|
|
10
11
|
return Array.isArray(value) && value.every((item) => typeof item === 'string' && item.length > 0 && patternBodyPattern.test(item)) ? value : undefined;
|
|
@@ -12,3 +13,16 @@ export function patternList(value) {
|
|
|
12
13
|
export function tomlString(value) {
|
|
13
14
|
return JSON.stringify(value);
|
|
14
15
|
}
|
|
16
|
+
const OPENCODE_PERMISSION_VALUES = new Set(['allow', 'deny', 'ask']);
|
|
17
|
+
const OPENCODE_PERMISSIONS_MAX_KEYS = 64;
|
|
18
|
+
export function isOpenCodePermissions(value) {
|
|
19
|
+
if (!isRecord(value))
|
|
20
|
+
return false;
|
|
21
|
+
const keys = Object.keys(value);
|
|
22
|
+
if (keys.length === 0 || keys.length > OPENCODE_PERMISSIONS_MAX_KEYS)
|
|
23
|
+
return false;
|
|
24
|
+
return keys.every((key) => {
|
|
25
|
+
const v = value[key];
|
|
26
|
+
return v === true || v === false || (typeof v === 'string' && OPENCODE_PERMISSION_VALUES.has(v));
|
|
27
|
+
});
|
|
28
|
+
}
|