forgedock 1.27.0 → 1.28.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.
Files changed (127) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +10 -0
  4. package/bin/engine/telemetry-seam.mjs +37 -8
  5. package/bin/engine/telemetry-seam.test.mjs +31 -0
  6. package/bin/engine-cli.mjs +542 -38
  7. package/bin/engine.mjs +8 -4
  8. package/bin/forgedock.mjs +75 -9
  9. package/bin/gc.mjs +580 -0
  10. package/bin/lean/admission.mjs +33 -3
  11. package/bin/lean/batch.mjs +22 -4
  12. package/bin/lean/batch.test.mjs +5 -3
  13. package/bin/lean/checks.mjs +199 -0
  14. package/bin/lean/checks.test.mjs +183 -0
  15. package/bin/lean/conformance.mjs +190 -58
  16. package/bin/lean/conformance.test.mjs +116 -7
  17. package/bin/lean/credentials.mjs +29 -0
  18. package/bin/lean/depcache.mjs +312 -0
  19. package/bin/lean/depcache.test.mjs +298 -0
  20. package/bin/lean/drift.mjs +198 -0
  21. package/bin/lean/drift.test.mjs +223 -0
  22. package/bin/lean/effects.mjs +85 -8
  23. package/bin/lean/execution.e2e.test.mjs +81 -0
  24. package/bin/lean/execution.mjs +431 -0
  25. package/bin/lean/execution.test.mjs +583 -0
  26. package/bin/lean/heavy.mjs +289 -0
  27. package/bin/lean/heavy.test.mjs +207 -0
  28. package/bin/lean/lean.mjs +572 -139
  29. package/bin/lean/lean.test.mjs +1193 -31
  30. package/bin/lean/ledger.mjs +27 -2
  31. package/bin/lean/ledger.test.mjs +36 -1
  32. package/bin/lean/limits.mjs +58 -0
  33. package/bin/lean/limits.test.mjs +150 -0
  34. package/bin/lean/orchestrate.mjs +312 -19
  35. package/bin/lean/prompts.mjs +89 -15
  36. package/bin/lean/refactor.mjs +276 -0
  37. package/bin/lean/refactor.test.mjs +359 -0
  38. package/bin/lean/render.mjs +24 -5
  39. package/bin/lean/review-trigger.mjs +134 -0
  40. package/bin/lean/review-trigger.test.mjs +104 -0
  41. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  42. package/bin/lean/sandbox-remote.mjs +142 -0
  43. package/bin/lean/sched.mjs +221 -0
  44. package/bin/lean/sched.test.mjs +310 -0
  45. package/bin/lean/schedd.mjs +247 -0
  46. package/bin/lean/schedd.test.mjs +180 -0
  47. package/bin/lean/toolchain.mjs +228 -0
  48. package/bin/lean/toolchain.test.mjs +154 -0
  49. package/bin/lean/verify.mjs +20 -9
  50. package/bin/lean/verify.test.mjs +26 -3
  51. package/bin/lean/worktree.mjs +69 -0
  52. package/bin/lean/worktree.test.mjs +141 -0
  53. package/bin/runner.mjs +53 -10
  54. package/bin/stats/github-source.mjs +1 -1
  55. package/bin/stats/index.mjs +14 -2
  56. package/bin/stats/query.mjs +72 -0
  57. package/bin/stats/stats.test.mjs +54 -0
  58. package/commands/audit.md +2 -0
  59. package/commands/autopilot.md +2 -0
  60. package/commands/bootstrap.md +35 -0
  61. package/commands/cleanup.md +7 -22
  62. package/commands/forge-stats.md +2 -1
  63. package/commands/forgedock-init.md +7 -0
  64. package/commands/issue.md +89 -830
  65. package/commands/milestone.md +55 -6
  66. package/commands/orchestrate.md +10 -2
  67. package/commands/upgrade-deps.md +3 -3
  68. package/commands/work-on/build/validate.md +4 -0
  69. package/commands/work-on/close.md +2 -0
  70. package/commands/work-on/decompose.md +24 -6
  71. package/docs/CI.md +3 -3
  72. package/docs/CONFIG.md +177 -2
  73. package/docs/OUTCOME-CODES.md +8 -0
  74. package/docs/TELEMETRY.md +1 -1
  75. package/docs/site/command-reference.md +22 -0
  76. package/docs/site/getting-started.md +2 -0
  77. package/docs/site/start-a-new-project.md +327 -0
  78. package/package.json +1 -1
  79. package/scripts/affected-files-check.sh +91 -0
  80. package/scripts/affected-files-check.test.sh +80 -0
  81. package/scripts/bootstrap-create.mjs +393 -0
  82. package/scripts/bootstrap-validate.mjs +36 -2
  83. package/scripts/calibration.mjs +116 -0
  84. package/scripts/check-devdocs-freshness.sh +9 -4
  85. package/scripts/check-devdocs-freshness.test.sh +11 -1
  86. package/scripts/check-spec-markers.sh +3 -0
  87. package/scripts/check-spec-size.sh +29 -11
  88. package/scripts/check-spec-size.test.sh +27 -4
  89. package/scripts/ci-local/lib.mjs +8 -1
  90. package/scripts/ci-local/parallelism.test.mjs +27 -0
  91. package/scripts/ci-local/run.mjs +3 -3
  92. package/scripts/ci-local/scoping.mjs +82 -0
  93. package/scripts/ci-local.test.sh +1 -1
  94. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  95. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  96. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  97. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  98. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  99. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  100. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  101. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  102. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  103. package/scripts/forge-root.test.sh +1 -1
  104. package/scripts/hotspots.sh +100 -0
  105. package/scripts/hotspots.test.sh +101 -0
  106. package/scripts/issue-create.sh +197 -0
  107. package/scripts/issue-create.test.sh +166 -0
  108. package/scripts/maintenance-scan.sh +185 -0
  109. package/scripts/maintenance-scan.test.sh +89 -0
  110. package/scripts/milestone-drift.sh +206 -0
  111. package/scripts/milestone-drift.test.sh +120 -0
  112. package/scripts/refactor-verify.sh +368 -0
  113. package/scripts/refactor-verify.test.sh +202 -0
  114. package/scripts/structure-delta.sh +10 -5
  115. package/scripts/structure-delta.test.sh +3 -2
  116. package/scripts/symbol-index.mjs +253 -0
  117. package/scripts/symbol-index.py +354 -0
  118. package/scripts/symbol-index.test.sh +119 -0
  119. package/scripts/symbol-move-ts.test.sh +161 -0
  120. package/scripts/symbol-move.mjs +379 -0
  121. package/scripts/symbol-move.py +383 -0
  122. package/scripts/symbol-move.test.sh +168 -0
  123. package/templates/bootstrap/interview.md +34 -0
  124. package/templates/bootstrap/issue-body.md +42 -0
  125. package/templates/bootstrap/needs-human-issue.md +45 -0
  126. package/templates/bootstrap/stack-profiles.md +34 -0
  127. package/templates/bootstrap/technical-spike.md +75 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Create, manage, and ship milestones — the top-level planning layer for feature development
3
- argument-hint: "[create <name> | status | ship <slug> | sync <slug>]"
3
+ argument-hint: "[create <name> [--plan <file>] [--greenfield] [--spike <path>] | status | ship <slug> | sync <slug>]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -17,7 +17,7 @@ Milestones are the top-level planning unit. They group related issues into a shi
17
17
  **Hierarchy**: Milestone → Issues → Sub-issues (optional decomposition)
18
18
 
19
19
  **NEVER use plan mode (EnterPlanMode).**
20
- **NEVER use the Agent tool for issue work** — milestone dispatches issue work via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` only. The Agent tool bypasses the Skill pipeline's label state machine, FORGE annotations, and structured review. The Agent tool may be used only for coordination tasks within milestone planning itself (e.g., parallel issue list queries) — never for /work-on sub-dispatch.
20
+ **NEVER use the Agent tool for issue work** — milestone dispatches issue work via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` only. Agent bypasses the Skill pipeline's label state machine, FORGE annotations and structured review; use it only to coordinate milestone planning (e.g. parallel issue-list queries), never for /work-on sub-dispatch.
21
21
 
22
22
  <!-- FORGE:SPEC_LOADED — milestone.md loaded and active. Agent is bound by this spec. -->
23
23
 
@@ -88,10 +88,12 @@ Parse `$ARGUMENTS` to determine which action to take:
88
88
  | Input Pattern | Action |
89
89
  |--------------|--------|
90
90
  | `create <name>` or just a descriptive phrase | → **Create Milestone** |
91
+ | `create <name> --plan <file>` / `--greenfield` / `--spike <path>` | → **Create Milestone**, greenfield mode (Step 4G) |
91
92
  | `status` (no args, or "status") | → **Show All Milestones** |
92
93
  | `status <slug>` | → **Show Milestone Detail** |
93
94
  | `ship <slug>` | → **Ship Milestone** |
94
95
  | `sync <slug>` | → **Sync Milestone Branch** |
96
+ | `drift <slug>` | → **Milestone Drift** |
95
97
  | `close <slug>` | → **Close Milestone** |
96
98
 
97
99
  ---
@@ -105,6 +107,25 @@ The user provides a name or description. Extract:
105
107
  - **Slug**: Lowercase, hyphenated, max 40 chars (e.g., `llm-extraction-platform`)
106
108
  - **Description**: What this milestone delivers, extracted from user's input
107
109
 
110
+ Optional greenfield flags (strip them from the name before deriving title and slug): <!-- Added: forge#4175 -->
111
+
112
+ | Flag | Meaning |
113
+ |------|---------|
114
+ | `--plan <file>` | Precomputed plan from `/bootstrap`: per issue a title, type, priority, size, dependencies, planned Affected Files and acceptance criteria. Implies greenfield mode. |
115
+ | `--greenfield` | Greenfield mode: the milestone builds code that does not exist yet, so Step 4 code-read gating is replaced by Step 4G. |
116
+ | `--spike <path>` | Spike doc declaring the `Repo layout` (implies greenfield mode). Default `docs/engineering/technical-spike.md`, repo-relative, same semantics as `commands/issue.md` `--spike`. |
117
+
118
+ ```bash
119
+ PLAN_FILE=""; GREENFIELD=false; SPIKE_DOC="docs/engineering/technical-spike.md"
120
+ # set from the flags above: --plan => PLAN_FILE + GREENFIELD=true; --greenfield => GREENFIELD=true; --spike => SPIKE_DOC + GREENFIELD=true
121
+ # A plan with a spike doc declaring a Repo layout is greenfield even without the flag (same predicate as /issue Phase 3E).
122
+ [ -n "$PLAN_FILE" ] && [ -f "$SPIKE_DOC" ] && grep -qiE '^#+[[:space:]].*repo layout' "$SPIKE_DOC" && GREENFIELD=true
123
+ # Passed to every default-repo /issue call in Step 6; empty in brownfield mode.
124
+ GREENFIELD_ARGS=""; [ "$GREENFIELD" = "true" ] && GREENFIELD_ARGS=" --greenfield --spike \"$SPIKE_DOC\""
125
+ ```
126
+
127
+ Without any of these flags `GREENFIELD=false`, `GREENFIELD_ARGS` is empty and every step below behaves as in brownfield mode.
128
+
108
129
  ### Step 2: Create the GitHub milestone
109
130
 
110
131
  ```bash
@@ -127,7 +148,9 @@ git push origin milestone/{slug}
127
148
 
128
149
  ### Step 4: Scope decomposition
129
150
 
130
- **Investigation-gated decomposition** — do NOT skip code reads. The single-pass shallow-planning approach (one agent, whole milestone, no per-issue code reads) produces under-specified issues that ship incomplete. This step mandates per-proposed-issue investigation before any issue body is written. See `work-on/decompose.md` Phase D0 for the reference pattern that enforces this gate. <!-- Added: forge#293 -->
151
+ **Greenfield mode** (`GREENFIELD=true`): skip 4A–4C and run **Step 4G** instead. Steps 2, 3 (the `milestone/{slug}` branch), 5, 6 and 6B run unchanged in both modes. Brownfield (`GREENFIELD=false`): Step 4G does not apply and 4A–4C below are mandatory. <!-- Added: forge#4175 -->
152
+
153
+ **Investigation-gated decomposition** — do NOT skip code reads: one shallow pass with no per-issue code reads yields under-specified issues that ship incomplete, so investigate each proposed issue before writing its body. See `work-on/decompose.md` Phase D0 for the reference pattern that enforces this gate. <!-- Added: forge#293 -->
131
154
 
132
155
  **4A: First pass — enumerate proposed issues (no code reads yet)**
133
156
 
@@ -153,7 +176,21 @@ For each proposed issue, after completing the per-issue investigation in Step 4B
153
176
  - **Size**: S (1-2 files), M (3-5 files), L (6+ files)
154
177
  - **Dependencies**: Which other issues must be done first (by number)
155
178
  - **Affected files**: Full list of files enumerated during the Step 4B code read (not estimated)
156
- - **Acceptance criteria**: Exhaustive and falsifiable. For coverage/refactor tasks: one criterion per affected call site or pattern, written as a grep/absence assertion. No cap on criteria count — the list must be complete enough that a reviewer can verify coverage without reading the implementation.
179
+ - **Acceptance criteria**: Exhaustive and falsifiable. For coverage/refactor tasks: one criterion per affected call site or pattern, written as a grep/absence assertion. No cap: a reviewer must be able to verify coverage without reading the implementation.
180
+
181
+ **4G: Greenfield / precomputed plan (only when `GREENFIELD=true`)** <!-- Added: forge#4175 -->
182
+
183
+ There is no code to read yet, so the per-issue code reads of 4B are skipped. The spike doc's `Repo layout` replaces them as the source of truth for paths:
184
+
185
+ 1. **Issue list**: with `--plan`, take the issues from `PLAN_FILE` as-is (title, type, priority, size, dependencies, planned Affected Files, acceptance criteria). Without a plan, enumerate them as in 4A and write each per 4C, reading the spike doc (layout, data model, route/API map) instead of code.
186
+ 2. **Validate planned paths**: write each issue body (Step 6 template) to an entity-scoped `mktemp` file and run the same deterministic check `/issue` Phase 3E uses. Exit 1 (`MISSING <path>`) or 2 (spike doc missing or no layout) → fix the plan or the spike layout; do not create that issue.
187
+ ```bash
188
+ type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
189
+ AF_CHECK_SCRIPT=$(forge_resolve affected-files-check.sh)
190
+ [ -n "$AF_CHECK_SCRIPT" ] && bash "$AF_CHECK_SCRIPT" "$PLAN_ISSUE_BODY_FILE" --spike "$SPIKE_DOC"
191
+ ```
192
+ If the helper does not resolve, leave the check to `/issue` Phase 3E, which runs it again on every Step 6 call because `GREENFIELD_ARGS` carries `--greenfield --spike`.
193
+ 3. Acceptance criteria stay exhaustive and falsifiable (4B rule 3); the plan's `Depends on` order is kept.
157
194
 
158
195
  **Project ecosystem context** (from `forge.yaml`):
159
196
  - **Primary repo** (`{GH_REPO}`): The main project codebase at `{REPO_PATH}`
@@ -168,7 +205,7 @@ Present the proposed issues to the user. Ask:
168
205
 
169
206
  ### Step 6: Create issues and assign to milestone
170
207
 
171
- **Issue body standard**: Use the **Pipeline Issue Template** from `issue.md` Phase 3D as the body structure for every issue created here. Do NOT use a bespoke inline template — the Pipeline Issue Template is the single canonical standard for all automated issue creation across the pipeline. The body content (Problem, Root Cause, Affected Files, Acceptance Criteria, Context, Dependencies) comes from the per-issue investigation in Step 4B. <!-- Added: forge#293 -->
208
+ **Issue body standard**: Use the **Pipeline Issue Template** from `issue.md` Phase 3D as the body structure for every issue created here (it is the single canonical standard; no bespoke inline template). The body content (Problem, Root Cause, Affected Files, Acceptance Criteria, Context, Dependencies) comes from the per-issue investigation in Step 4B. <!-- Added: forge#293 --> In greenfield mode it comes from the Step 4G plan instead, and Affected Files are planned paths inside the spike's `Repo layout`. <!-- Added: forge#4175 -->
172
209
 
173
210
  **Route creation through `/issue`'s programmatic invocation contract** (`commands/issue.md`, added #2085) instead of calling `gh issue create` directly — this gets dedup (Phase 2D) and mandatory-section body validation (Phase 3F) for free. <!-- Changed: forge#2086 — route through /issue create-hook -->
174
211
 
@@ -213,7 +250,7 @@ BODY_EOF
213
250
  ```
214
251
 
215
252
  ```
216
- ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{fix|feat|refactor}: {issue_title}\" --body-file \"${MILESTONE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\" --milestone \"{TITLE}\""))
253
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{fix|feat|refactor}: {issue_title}\" --body-file \"${MILESTONE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\" --milestone \"{TITLE}\"${GREENFIELD_ARGS}"))
217
254
  ```
218
255
 
219
256
  ```bash
@@ -381,6 +418,18 @@ Show detailed progress with per-issue status.
381
418
 
382
419
  ---
383
420
 
421
+ ## Action: Milestone Drift (drift <slug>)
422
+
423
+ Print verbatim (forge#4161); no LLM, no issue filing.
424
+
425
+ ```bash
426
+ git fetch origin "{STAGING_BRANCH}" "milestone/{slug}"
427
+ type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
428
+ bash "$(forge_resolve milestone-drift.sh)" "{slug}" --format text
429
+ ```
430
+
431
+ ---
432
+
384
433
  ## Action: Ship Milestone
385
434
 
386
435
  **This is the human-gated deployment step.** When the user says "ship {slug}", they've decided this feature set is ready for production.
@@ -81,6 +81,11 @@ issue right away (priority label first, then the smaller size). Never hold a rea
81
81
  lanes finish. To reduce simultaneous base-syncs, don't start two lanes that name the same function within the
82
82
  same minute; start another ready issue first instead of waiting.
83
83
 
84
+ **Maintenance budget** (off unless forge.yaml `maintenance.enabled: true`). The runner pulls open issues carrying a
85
+ `maintenance.labels` label into the batch and admits in this order: P0/P1 features > maintenance, while maintenance
86
+ starts stay within `maintenance.share` of all starts (0.2 = 1 lane in 5) > P2+ features. With `when_idle` it drains the
87
+ remaining maintenance once no feature is pending; otherwise the rest ends `held (maintenance share)` for a later batch.
88
+
84
89
  - **Batch runner (default, when `forgedock` is on PATH).** Launch the whole set as one durable batch in a single
85
90
  `Bash` call with `run_in_background: true`. Do NOT write a scheduler, wrapper or queue script, and do not launch one
86
91
  `run-issue` per issue yourself: the runner schedules from the issues' `Depends on` / `Blocked by` lines.
@@ -93,9 +98,12 @@ same minute; start another ready issue first instead of waiting.
93
98
  own session, so the batch survives this session: if the runner dies, run `forgedock orchestrate --resume <id>`
94
99
  (live lanes are re-adopted, nothing is launched twice). Change K of a running batch with
95
100
  `forgedock orchestrate --set-parallel <id> K`. The runner requeues `deferred` lanes by code (host busy after its
96
- retry-in, a usage limit holds all admissions until the reset, transient merge/push/idle failures back off; non-host
101
+ retry-in, a usage limit holds admissions for that account (backend identity) until the reset, across every batch on
102
+ the host; `forgedock limits show|clear <id>|--all` lists or drops holds; transient merge/push/idle failures back off; non-host
97
103
  deferrals stop after `orchestration.max_deferrals`), relaunches an `engine-error` once, and stops admitting after
98
- `orchestration.breaker` engine-errors (`errors` within `window_min`; held issues continue on `--resume`). Logical
104
+ `orchestration.breaker` engine-errors (`errors` within `window_min`; held issues continue on `--resume`). Lanes are
105
+ capped host-wide (`orchestration.global_parallel`, `forgedock sched cap N|show`), highest priority first; within a
106
+ priority, slots are shared across repos by weight (`forgedock sched weight <owner/repo> N`, default equal). Logical
99
107
  edges you found in step 2.4 must be written to the dependent issue as a `Depends on #X` line before launching. The
100
108
  last lines are the report table.
101
109
  - **Agent lane (only when the `forgedock` CLI is unavailable).** Spawn one background agent per issue.
@@ -257,7 +257,7 @@ Major upgrades detected (not auto-processed — require human review):
257
257
 
258
258
  ## Phase 3: Issue Creation
259
259
 
260
- For each eligible package (or batch), create a GitHub issue:
260
+ For each eligible package (or batch), create a GitHub issue. Each carries the `maintenance` label, so `forgedock orchestrate` dispatches it within the maintenance budget (forge.yaml `maintenance.share`, #4197):
261
261
 
262
262
  ### 3A: Per-package issue (patch or minor, non-batched)
263
263
 
@@ -269,7 +269,7 @@ if [ "$DRY_RUN" = "true" ]; then
269
269
  else
270
270
  gh issue create $GH_FLAG \
271
271
  --title "feat(deps): upgrade {PACKAGE} from {CURRENT} to {LATEST} ({SEMVER_LEVEL})" \
272
- --label "enhancement" \
272
+ --label "enhancement" --label "maintenance" \
273
273
  --body "$(cat <<'ISSUE_EOF'
274
274
  ## Problem
275
275
 
@@ -327,7 +327,7 @@ if [ "$DRY_RUN" = "true" ]; then
327
327
  else
328
328
  gh issue create $GH_FLAG \
329
329
  --title "feat(deps): batch patch upgrades — {COUNT} packages ({DATE})" \
330
- --label "enhancement" \
330
+ --label "enhancement" --label "maintenance" \
331
331
  --body "$(cat <<'BATCH_ISSUE_EOF'
332
332
  ## Problem
333
333
 
@@ -357,8 +357,12 @@ else
357
357
  fi
358
358
  [ "$TEST_FAILED" -eq 1 ] && LEARNED_FAILED=1
359
359
  echo "TEST_FAILED=${TEST_FAILED}"
360
+ # #4183: reclaim owned, expired test containers/volumes and scratch (never unowned ones); non-fatal.
361
+ npx forgedock gc --apply --docker --tmp >/dev/null 2>&1 || true
360
362
  ```
361
363
 
364
+ Test services you start yourself (`docker run`, compose) carry the `forgedock.*` labels (#4181), use `--rm`, and are removed in a `trap` on exit.
365
+
362
366
  The configured `verification.commands.<lang>.test` always runs here, whether or not V4.8's `ci-local.sh` resolves (forge#4040): the local CI gate never runs that command, so a failing configured suite must fail V2 on its own. Only a wall-clock timeout (rc 124) is DEGRADED. Test command failures (`TEST_FAILED=1`, also reported as `LEARNED_FAILED=1`) are BLOCKING (same as `verification.commands` failures): fix and re-run. A command matched and skipped by the known-slow gate is never executed and never counted as a failure, but it is DEGRADED, not verified. Each `DEGRADED_CHECKS+=` line printed here is a carry-forward line: record it in your notes, V5 Step 1 and Step 4 rebuild `DEGRADED_CHECKS` from them (a fresh shell does not keep the variable).
363
367
 
364
368
  ---
@@ -1830,6 +1830,8 @@ if [ -n "{WORKTREE_PATH}" ] && [ -d "{WORKTREE_PATH}" ]; then
1830
1830
  else
1831
1831
  echo "Worktree cleanup skipped: path not provided or does not exist"
1832
1832
  fi
1833
+ # #4183: reap ForgeDock-owned, stale leftovers (labelled docker, tmp scratch, orphans); core lanes tear down from the ledger.
1834
+ npx forgedock gc --apply --docker --tmp --processes >/dev/null 2>&1 || true
1833
1835
  ```
1834
1836
 
1835
1837
  Set `{CLEANUP_STATUS}` based on outcome:
@@ -135,11 +135,24 @@ Re-read current state before doing anything:
135
135
  ```bash
136
136
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
137
137
 
138
- # Read investigation report (required — contains decomposition plan)
138
+ # Read investigation report (contains the decomposition plan; see the /bootstrap plan below when absent)
139
139
  gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
140
140
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
141
141
  ```
142
142
 
143
+ **Bootstrap plan** <!-- Added: forge#4175 -->: a greenfield spike issue filed by `/bootstrap` has no code to investigate, so it carries its plan in a `<!-- FORGE:BOOTSTRAP_PLAN -->` comment instead: a `### Decomposition Assessment` listing the sub-issues (titles, dependencies, `ESTIMATED_DIFF_LINES`) and an optional own-line `Spike: <path>` naming the spike doc. Only a trusted, marker-anchored comment counts (`scripts/trusted-comments.sh`, the same predicate as the resume check in Phase D1). Paste the Script resolution block first (fresh shell per Bash call). An unresolved helper or a failed read means "no bootstrap plan" (fail closed); an untrusted body never becomes a plan.
144
+
145
+ ```bash
146
+ TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh) # unresolved => empty => fail closed
147
+ BOOTSTRAP_PLAN=""
148
+ if [ -n "$TRUSTED_SCRIPT" ] && COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments); then
149
+ BOOTSTRAP_PLAN=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:BOOTSTRAP_PLAN -->' | tail -n 1 | jq -r '.' 2>/dev/null || true)
150
+ fi
151
+ PLAN_SPIKE=$(printf '%s\n' "$BOOTSTRAP_PLAN" | sed -nE 's/^Spike: *`?([^` ]+)`? *$/\1/p' | head -1)
152
+ ```
153
+
154
+ **Plan source**: when a FORGE:INVESTIGATOR report exists it is the plan source (`PLAN_SOURCE=investigate`) and the bootstrap plan is ignored. Only when no investigator report exists and `BOOTSTRAP_PLAN` is non-empty is the bootstrap plan the source (`PLAN_SOURCE=bootstrap`). Everything below that reads "the investigation report's Decomposition Assessment" reads the selected source's Decomposition Assessment.
155
+
143
156
  **MANDATORY — Owner override detection**: After reading the investigation comment, read ALL other comments on the issue to check for owner override signals:
144
157
 
145
158
  ```bash
@@ -147,7 +160,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
147
160
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
148
161
  ```
149
162
 
150
- Ignore machine-posted marker comments (`FORGE:DIFF_SIZE`, `FORGE:SIZE_OVERRIDE`) in this scan: they are never an owner override direction. Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
163
+ Ignore machine-posted marker comments (`FORGE:DIFF_SIZE`, `FORGE:SIZE_OVERRIDE`, `FORGE:BOOTSTRAP_PLAN`) in this scan: they are never an owner override direction. Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
151
164
 
152
165
  1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
153
166
  2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
@@ -161,7 +174,7 @@ Ignore machine-posted marker comments (`FORGE:DIFF_SIZE`, `FORGE:SIZE_OVERRIDE`)
161
174
  Extract from investigation report:
162
175
  - Decomposition Assessment section: list of proposed sub-issues with titles and dependencies
163
176
 
164
- **Planning-time only** <!-- Added: forge#3862 -->: decompose runs only from the investigation's Decomposition Assessment (`**YES**`, which requires separable pieces: independent modules or services, no shared files, no ordering dependency, each independently testable). There is no post-build route into this skill: build never discards a finished diff and never hands one here, so the assessment is the only plan source and there are no persisted staged hunks to reuse.
177
+ **Planning-time only** <!-- Added: forge#3862 -->: decompose runs only from the investigation's Decomposition Assessment (`**YES**`, which requires separable pieces: independent modules or services, no shared files, no ordering dependency, each independently testable), or, for a greenfield spike with no investigator report, from the trusted `/bootstrap` plan's Decomposition Assessment (forge#4175). There is no post-build route into this skill: build never discards a finished diff and never hands one here, so the assessment is the only plan source and there are no persisted staged hunks to reuse.
165
178
  - Milestone (from issue metadata)
166
179
  - Priority label (P0/P1/P2) from issue labels
167
180
 
@@ -193,9 +206,9 @@ PARENT_REF=$(printf '%s\n' "$ISSUE_BODY" | grep -E '^\*\*Parent\*\*: #[0-9]+' |
193
206
 
194
207
  If `PARENT_REF` is non-empty → BLOCKED, blocker: "Issue #{NUMBER} is a sub-issue (${PARENT_REF}) — sub-issues are never decomposed again; run /work-on build on it directly".
195
208
 
196
- **Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent → BLOCKED, blocker: "No investigation report found — run investigate first".
209
+ **Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent and there is no trusted `/bootstrap` plan (`BOOTSTRAP_PLAN` empty, Phase D0) → BLOCKED, blocker: "No investigation report found — run investigate first". A trusted bootstrap plan satisfies this guard only for an issue with no investigator report. <!-- Changed: forge#4175 -->
197
210
 
198
- **Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
211
+ **Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope". When `PLAN_SOURCE=bootstrap` the same check applies to the bootstrap plan's Decomposition Assessment; on failure the blocker is "Bootstrap plan has no decomposition plan — re-run /bootstrap for this spike".
199
212
 
200
213
  **Resume check** <!-- Added: forge#3881 -->: the marker comment drives parent edits and the D6.5 close, so only a trusted, marker-anchored comment counts (`scripts/trusted-comments.sh`, the same predicate as the review gates). Paste the Script resolution block first (fresh shell per Bash call). An unresolved helper or a failed read is treated as "no trusted comment" and the normal path continues (the D2.5 equivalent-set check still guards duplicates); an untrusted body never reaches `SUB_LIST`.
201
214
 
@@ -373,12 +386,15 @@ SUB_BODY_EOF
373
386
  ```bash
374
387
  MILESTONE_ARG=""
375
388
  [ -n "$MILESTONE_TITLE" ] && MILESTONE_ARG=" --milestone \"${MILESTONE_TITLE}\""
389
+ # Greenfield (forge#4175): a bootstrap plan naming a spike doc lets sub-issues list planned Affected Files.
390
+ SPIKE_ARG=""
391
+ [ "$PLAN_SOURCE" = "bootstrap" ] && [ -n "$PLAN_SPIKE" ] && SPIKE_ARG=" --greenfield --spike \"${PLAN_SPIKE}\""
376
392
  ```
377
393
 
378
394
  **Invoke `/issue` in programmatic mode**:
379
395
 
380
396
  ```
381
- ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"${MILESTONE_ARG}"))
397
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"${MILESTONE_ARG}${SPIKE_ARG}"))
382
398
  ```
383
399
 
384
400
  If the skill is not found → BLOCKED, blocker: "skill not found: issue".
@@ -488,6 +504,8 @@ COMMENT_URL=$(gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED
488
504
 
489
505
  This issue has been broken into sub-issues. Each sub-issue runs through its own /work-on pipeline independently.
490
506
 
507
+ **Plan source**: ${PLAN_SOURCE}
508
+
491
509
  ### Sub-Issues Created
492
510
 
493
511
  {for each sub-issue, in dependency order:}
package/docs/CI.md CHANGED
@@ -137,10 +137,10 @@ Separately, `npx forgedock run <command>` (the standalone, non-Claude-Code comma
137
137
  | Backend | When used | Credential needed |
138
138
  |---------|-----------|--------------------|
139
139
  | `cli` (via `--backend cli` or `FORGEDOCK_BACKEND=cli`) | Explicit, or auto-selected when a working `claude` CLI is detected on PATH | None — reuses the CLI's own authentication (Pro/Max OAuth or a CLI-managed key) |
140
- | `api` (via `--backend api` or `FORGEDOCK_BACKEND=api`) | Explicit, or auto-selected fallback when `claude` is not detected | `ANTHROPIC_API_KEY` |
141
- | `auto` (default) | No `--backend` / `FORGEDOCK_BACKEND` given | Prefers `cli`, falls back to `api` |
140
+ | `api` (via `--backend api` or `FORGEDOCK_BACKEND=api`) | Explicit only, or the `auto` fallback when the owner opted in (forge.yaml `execution.agent_backend: api` plus `FORGEDOCK_ALLOW_API_BACKEND=1`) | `ANTHROPIC_API_KEY` |
141
+ | `auto` (default) | No `--backend` / `FORGEDOCK_BACKEND` given | Uses `cli`; never falls back to `api` without the owner opt-in. With no CLI it fails with `CLI_BACKEND_UNAVAILABLE` |
142
142
 
143
- **When CI still needs `ANTHROPIC_API_KEY` for `forgedock run`**: CI runners generally do not have an interactively-authenticated `claude` CLI (no browser/OAuth flow available in a headless job), so `auto` detection will typically fall back to `api` there anyway. If your CI runner does install and log in the `claude` CLI (e.g. via a headless API-key-based login), you can force `--backend cli` to skip the SDK/API path entirely. Otherwise, keep `ANTHROPIC_API_KEY` configured as a repository secret (see Setup above) and either let `auto` fall back naturally or set `--backend api` / `FORGEDOCK_BACKEND=api` explicitly for a deterministic CI backend regardless of what happens to be on the runner's PATH.
143
+ **When CI still needs `ANTHROPIC_API_KEY` for `forgedock run`**: CI runners generally do not have an interactively-authenticated `claude` CLI (no browser/OAuth flow available in a headless job), so `auto` there fails with `CLI_BACKEND_UNAVAILABLE`: the API backend is never the default, because it bills an API key (#4257). If your CI runner does install and log in the `claude` CLI (e.g. via a headless API-key-based login), you can force `--backend cli` to skip the SDK/API path entirely. Otherwise, keep `ANTHROPIC_API_KEY` configured as a repository secret (see Setup above) and set `--backend api` / `FORGEDOCK_BACKEND=api` explicitly, or opt in to the `auto` fallback with forge.yaml `execution.agent_backend: api` plus `FORGEDOCK_ALLOW_API_BACKEND=1`.
144
144
 
145
145
  `npx forgedock run <command> --dry-run` always reports which backend it resolved to, without making any network call or requiring a key either way.
146
146
 
package/docs/CONFIG.md CHANGED
@@ -49,12 +49,14 @@ If you have both a local `claude` CLI and `ANTHROPIC_API_KEY` set and want to pi
49
49
  | [`repos`](#repos-optional) | No | Multi-repository routing |
50
50
  | [`project_board`](#project_board-optional) | No | GitHub Projects v2 integration |
51
51
  | [`orchestration`](#orchestration-optional) | No | `/orchestrate` concurrency and cascade admission policy |
52
+ | [`hygiene`](#hygiene-optional) | No | Automatic reclaim of ForgeDock-owned leftovers: TTLs, branch deletion, near-ceiling GC, `auto_gc` |
52
53
  | [`pipeline`](#pipeline-optional) | No | `/orchestrate` batch-engine tuning (stall detection, token budget, narration) |
53
54
  | [`build`](#build-optional) | No | `/work-on` build-phase scope check (`build.scope.*`) and review depth (`review.depth.*`) |
54
55
  | [`services`](#services-optional) | No | External service URLs and IDs |
55
56
  | [`review`](#review-optional) | No | Context injected into review agents |
56
57
  | [`devdocs`](#devdocs-optional) | No | Devdocs knowledge tree path |
57
58
  | [`verification`](#verification-optional) | No | Health-check patterns |
59
+ | [`execution`](#execution-optional) | No | `forgedock orchestrate`: run lanes in sandboxes instead of host processes (`mode: local\|remote\|hybrid`, `max_remote`) |
58
60
  | [`verify`](#verify-optional) | No | Core work-on: run lane verification on CI runners instead of the host (`mode: local\|ci\|hybrid`) |
59
61
  | [`billing`](#billing-optional) | No | Enable financial integrity audit phase |
60
62
  | [`adaptive_scripts`](#adaptive_scripts-optional) | No | Per-repo script override configuration |
@@ -407,6 +409,7 @@ orchestration:
407
409
  | `max_concurrent` | integer | No | Max concurrent top-level `/work-on` dispatches. It does not bound cascade growth; under `policy: all` it raises drain and spawn throughput together. Default: 12. |
408
410
  | `breaker.errors` / `breaker.window_min` | integer | No | `forgedock orchestrate` circuit breaker (#4121): after `errors` engine-errors within `window_min` minutes the batch stops admitting lanes, lets running lanes finish, and keeps the rest pending for `--resume`. `errors: 0` turns it off. Default: 3 in 30 |
409
411
  | `max_deferrals` | integer | No | `forgedock orchestrate`: requeues of one issue after a non-host `deferred` (usage limit, merge/push transient, CLI idle, lease held, gh unavailable) before its row is final. `host.busy` deferrals are not capped. Default: 3 |
412
+ | `global_parallel` | integer | No | `forgedock orchestrate` (#4191): host-wide cap on running lanes across every batch and repo on the host, enforced through the host lane registry (`~/.forge/sched/`, or `FORGEDOCK_SCHED_DIR`). A freed slot goes to the highest-priority queued issue (`priority:P<n>` / `P<n>` labels, unlabelled P2); within a priority, slots are shared across repos by weight (#4193: `forgedock sched weight <owner/repo> N`, `--clear` resets; default equal, FIFO inside a repo), and priority always dominates weights; running lanes are never preempted and `--parallel` K still bounds each batch. `forgedock sched cap N` overrides it live for the whole host (`--clear` reverts); `forgedock sched show` lists the weights, slots and the queue. Default: unset (no global cap) |
410
413
  | `cascade.policy` | string | No | `all` \| `balanced` \| `conservative`. Default: `balanced` |
411
414
  | `cascade.max_generation` | integer or `"unlimited"` | No | Max cascade generation admitted at Phase 1 resolve time. Default: 1 |
412
415
  | `cascade.batch_max_generation` | integer | No | Finite maximum member generation that an automated P3 batch may aggregate. Default: 2 |
@@ -429,6 +432,37 @@ orchestration:
429
432
 
430
433
  ---
431
434
 
435
+ ## `hygiene` (OPTIONAL)
436
+
437
+ Automatic reclaim of what ForgeDock leaves behind (#4183), using the `forgedock gc` reaper. Only resources ForgeDock provably owns are reclaimed: entries of a stale lane ledger, Docker containers and volumes labelled `forgedock.owner=1`, `lean-*`/`agent-*` dirs under `.claude/worktrees`, `feat/issue-<n>-lean` branches of closed issues, and ForgeDock scratch under `~/.forge/tmp`. Everything else is reported, never touched, and so are unpushed or uncommitted work, live lanes, protected branches and branches with an open PR. Block form only.
438
+
439
+ ```yaml
440
+ hygiene:
441
+ container_ttl_hours: 6
442
+ worktree_ttl_hours: 24
443
+ tmp_ttl_hours: 6
444
+ delete_merged_branches: true
445
+ max_host_load_ratio: 0.8
446
+ auto_gc: true
447
+ ```
448
+
449
+ | Field | Type | Default | Description |
450
+ |-------|------|---------|-------------|
451
+ | `container_ttl_hours` | number | `6` | Age after which owned Docker containers and volumes are reclaimed (an expired `forgedock.expires` label reclaims earlier). |
452
+ | `worktree_ttl_hours` | number | `24` | Age after which owned worktrees (clean and pushed) and abandoned lane branches are reclaimed. |
453
+ | `tmp_ttl_hours` | number | `6` | Age after which `~/.forge/tmp` run dirs (owner dead) and runner scratch (no live process) are reclaimed. |
454
+ | `delete_merged_branches` | boolean | `true` | `false`: lane branches of closed issues are reported, never deleted. |
455
+ | `max_host_load_ratio` | number in (0,1] | `0.8` | Near-ceiling trigger relative to the `engine` admission thresholds: load/core at or above ratio × `max_load_per_core`, or free memory/tmp at or below the minimum ÷ ratio. |
456
+ | `auto_gc` | boolean | `true` | `false` disables GC in `forgedock orchestrate`; `forgedock gc` and `forgedock doctor --fix` still work. |
457
+
458
+ Invalid values fall back to the default. `forgedock gc --older-than <d>` overrides every TTL for that run. Resources of a live lane (its ledger owner is alive) are always skipped.
459
+
460
+ **Where it runs**: `forgedock orchestrate` applies GC once before admitting lanes, and again before an admission when the host is near the ceiling (at most every 5 minutes). The batch report ends with `reclaimed: N item(s), SIZE (preflight A, admissions B)`, or `reclaimed: off (hygiene.auto_gc: false)`. `forgedock doctor` shows a **Resource hygiene** check (owned counts and sizes, reclaimable count, unowned stale items as warnings), and `doctor --fix` runs the reaper. `/cleanup branches` calls `forgedock gc`.
461
+
462
+ **Commands that use this section**: `orchestrate`, `doctor`, `gc`, `cleanup`
463
+
464
+ ---
465
+
432
466
  ## `pipeline` (OPTIONAL)
433
467
 
434
468
  Tuning knobs for the CLI backend and `/orchestrate`'s batch engine — invocation and stall timeouts, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
@@ -471,6 +505,27 @@ pipeline:
471
505
 
472
506
  ---
473
507
 
508
+ ## `execution` (OPTIONAL)
509
+
510
+ Owner opt-in for the paid API agent backend (#4257). The API backend is **never the default**: under `--backend auto` (the default) ForgeDock uses the `claude` CLI, and when no CLI is available it fails with `CLI_BACKEND_UNAVAILABLE` instead of billing `ANTHROPIC_API_KEY`.
511
+
512
+ ```yaml
513
+ execution:
514
+ # Use the API backend when `auto` finds no usable claude CLI. Takes effect only
515
+ # together with the env var FORGEDOCK_ALLOW_API_BACKEND=1 on the host.
516
+ agent_backend: api
517
+ ```
518
+
519
+ | Field | Type | Required | Description |
520
+ |-------|------|----------|-------------|
521
+ | `agent_backend` | string | No | `api` approves the API backend as the `auto` fallback, only when `FORGEDOCK_ALLOW_API_BACKEND=1` is also set. Any other value, or no value, means `auto` never selects `api`. |
522
+
523
+ An explicit `--backend api` / `FORGEDOCK_BACKEND=api` still selects the API backend without this section; that flag is itself the explicit choice.
524
+
525
+ **Commands that use this section**: `forgedock run`, `forgedock run-issue`, `forgedock backend-check`
526
+
527
+ ---
528
+
474
529
  ## `engine` (OPTIONAL)
475
530
 
476
531
  Per-command `--model` and `--max-turns` for the CLI backend (`forgedock run-issue` phases and `forgedock run <command>`). Opt-in: with no keys set, the `claude` invocation is unchanged and uses the CLI's own default model with no turn bound. `forge.yaml` is parsed one level deep, so settings are **flat** keys under `engine:`, named `<key>_model` and `<key>_max_turns`, where `<key>` is the command name with every non-alphanumeric character replaced by `_`.
@@ -491,7 +546,7 @@ engine:
491
546
  | `<key>_max_turns` | integer | No | Positive integer passed as `--max-turns`. Invalid values are ignored. |
492
547
 
493
548
  | `path` | string | No | `core` (default), `full`, `lean` or `auto`. `core` is the lean work-on (`bin/lean/`): a planning call that writes a frozen contract, an implement call in the same session, one read-only conformance reviewer, GitHub as the register (see `conformance`). `lean` and `auto` are aliases of `core`; `full` runs the legacy multi-phase pipeline and is required for `--retry` and `--wait-lease`. The `--path` flag overrides. `lean_reviewer_model` sets the reviewer model (default `claude-sonnet-5-5`); the builder uses `--model` or the CLI default. `forgedock orchestrate <issues...> [--parallel K] [--lane L]` runs the core per issue (K default 3, lane default `staging`) and honours `**Depends on**` ordering among the given issues. |
494
- | `conformance` | string | No | Core work-on conformance gate. The builder first writes a contract (requirements quoted verbatim from the issue and maintainers' comments, deliverables, preserve constraints, validate commands), which the engine freezes and posts as `FORGE:CONTRACT`; changes go only through typed amendments (`FORGE:AMENDMENT`). A deterministic precheck (scope, validation, deferrals) and a row-based conformance review follow, and the engine computes the verdict. `shadow` (default): the verdict is posted in `FORGE:REVIEW` and logged (`CONFORMANCE_SHADOW` event) but only what the previous reviewer blocked on still blocks (a requirement found not implemented, a regression), plus the precheck's equivalent: a validate command that ran and failed, or a deliver requirement the builder reported as not implemented (#4160). These block before any reviewer run. A validate step with no recorded result, scope creep and amendment problems are recorded only. Deferrals are not filed. `enforce`: every failure blocks (one same-session builder resume, then `needs-human` with a `conformance.*`/`review.*` code, see docs/OUTCOME-CODES.md); deferrals the reviewer accepts are filed in one batched follow-up issue (`scripts/file-deferred-followups.sh`) and linked in the PR body and a `FORGE:TRACKED` comment before merge. |
549
+ | `conformance` | string | No | Core work-on conformance gate. The builder first writes a contract (requirements quoted verbatim from the issue and maintainers' comments, deliverables, preserve constraints, validate commands), which the engine freezes and posts as `FORGE:CONTRACT`; changes go only through typed amendments (`FORGE:AMENDMENT`). A deterministic precheck (scope, validation, deferrals) and a row-based conformance review follow, and the engine computes the verdict. `shadow` (default): the verdict is posted in `FORGE:REVIEW` and logged (`CONFORMANCE_SHADOW` event) but only what the previous reviewer blocked on still blocks (a requirement found not implemented, a regression), plus the precheck's equivalent: any `conformance.unimplemented` precheck or amendment failure (a validate command that failed or has no recorded result, a deliver requirement the builder reported as not implemented, an invalid amendment; #4160). These block before any reviewer run. Scope creep and omissions are recorded only. A review still incomplete or inconsistent after one targeted re-ask (only the missing ids) ends `needs-human`, or with `review_incomplete: degraded` merges with a `review-degraded` PR label and a tracked follow-up. Accepted deferrals are filed and linked, as in enforce. `scripts/calibration.mjs --shadow` reports the per-repo shadow verdict mix and when to switch to enforce. `enforce`: every failure blocks (one same-session builder resume, then `needs-human` with a `conformance.*`/`review.*` code, see docs/OUTCOME-CODES.md); deferrals the reviewer accepts are filed in one batched follow-up issue (`scripts/file-deferred-followups.sh`) and linked in the PR body and a `FORGE:TRACKED` comment before merge. |
495
550
  | `cli_idle_minutes` | number | No | Core builder idle timeout (#4112): the builder CLI runs with `--output-format stream-json` and is killed (code `cli.idle`) when it shows no stream activity for this many minutes. The run log records the last tool call and the live child processes. Default 15. |
496
551
  | `cli_max_minutes` | number | No | Absolute ceiling for a core builder session (code `cli.timeout`), replacing the fixed 60-minute kill. A session that keeps making progress is never idle-killed. Default 180. On either kill, uncommitted work is committed as `wip(forge): checkpoint after <reason>` and the lane branch is pushed (with the push retry), the builder retries once from the checkpoint, and a second kill ends `deferred`; a rerun resumes from the branch. |
497
552
  | `lane_limits` | map | No | Cap each core lane's whole process tree (#4115), under `pipeline:` or `engine:`: `memory_max` (for example `3G`) and `cpu_quota` (for example `150%`). When set and `systemd-run --user --scope` works (probed once), the builder CLI is spawned as `systemd-run --user --scope -q -p MemoryMax=... -p CPUQuota=... -- claude ...`; elsewhere the caps are ignored. Write it nested or flat (`lane_limits_memory_max`, `lane_limits_cpu_quota`). While a builder runs the engine polls host load and memory every 15 s against the `admission` thresholds: over them it SIGSTOPs the lane's process group, under them SIGCONT, and never pauses longer than 10 minutes. |
@@ -515,6 +570,19 @@ engine:
515
570
 
516
571
  ---
517
572
 
573
+ ## `lean` (OPTIONAL)
574
+
575
+ Settings of the core work-on (`bin/lean/`) that change its contract.
576
+
577
+ ```yaml
578
+ lean:
579
+ repro_test: prefer # off | prefer (default)
580
+ ```
581
+
582
+ | Field | Type | Required | Description |
583
+ |-------|------|----------|-------------|
584
+ | `repro_test` | string | No | `prefer` (default): on an issue labelled `bug` or `type:bug`, the plan step must freeze a `repro_test` in the contract (`{"path","id","command"}`) when the bug is testable. Before review, the engine runs `scripts/acceptance-red-green.sh` for that command on the host in every verify mode: one targeted test, red on the base (head's changed test files overlaid) and green on head. Any other result, including a test that could not run, fails the precheck as `conformance.unimplemented` (it blocks in shadow too) and goes back to the builder for one in-session fix round. A bug that is not testable (specs, docs, workflows, environment) records `"repro_test":"none"` with a one-line `repro_test_reason` and the lane continues as before. `off` gives exactly the previous contract and precheck. Each proof emits a `REPRO_RED_GREEN` run-log event (`base`, `head`) and adds a trajectory decision. |
585
+
518
586
  ## `verify` (OPTIONAL)
519
587
 
520
588
  Verify-by-CI for the core work-on path (forge#4167). It moves a lane's heavy verification (type-checks, test suites, docker test containers) off the host and onto CI runners, and it catches CI-class failures (lock files, suite wiring, hangs, formatting) before the lane merges into staging. Write it as a block-style section: `parseForgeYaml` does not read inline `{ ... }` maps.
@@ -526,6 +594,7 @@ verify:
526
594
  paths_input: changed_paths # optional workflow_dispatch input that receives the changed paths (newline-separated)
527
595
  skip_paths: "*.md,docs/**" # optional, comma-separated; default *.md,*.mdx,*.rst,*.txt,docs/**,devdocs/**
528
596
  max_rounds: 2 # optional CI fix rounds before needs-human (default 2)
597
+ precheck_timeout_seconds: 300 # optional per-command timeout of the declared pre-PR checks (default 300)
529
598
  ```
530
599
 
531
600
  | Key | Type | Required | Description |
@@ -534,7 +603,8 @@ verify:
534
603
  | `workflow` | string | No | Workflow file name or id dispatched on the lane branch (`gh workflow run <workflow> --ref <branch>`). The engine waits for the `workflow_dispatch` run of the lane's head commit. Without it, the engine waits on the head commit's push-triggered check runs. |
535
604
  | `paths_input` | string | No | Name of a `workflow_dispatch` input that receives the changed paths, so the workflow can scope its jobs to the change. |
536
605
  | `skip_paths` | string | No | Comma-separated globs. A glob without `/` matches the basename anywhere; `**` spans directories. When every changed file matches, the lane skips CI verification (a docs-only lane runs no CI matrix). |
537
- | `max_rounds` | integer | No | Builder fix rounds for red CI (0–10, default 2). |
606
+ | `max_rounds` | integer | No | Builder fix rounds for red CI (0–10, default 2). Also bounds the merge-time fix rounds and the declared-check fix rounds below, in every mode. |
607
+ | `precheck_timeout_seconds` | integer | No | Timeout of each declared check command (1–3600, default 300); a timed-out command counts as failed (exit 124). |
538
608
 
539
609
  **Flow** (`ci`/`hybrid`, `bin/lean/verify.mjs` `runCiVerify`):
540
610
 
@@ -546,10 +616,114 @@ verify:
546
616
 
547
617
  Each verification emits a `CI_VERIFY` run-log/telemetry event (`mode`, `round`, `result`, `skipped`, `run_url`) and adds a **Verify** row to the trajectory.
548
618
 
619
+ **Every mode, including the default `local` (forge#4240):**
620
+
621
+ - **Declared check commands before the PR.** Each `learned.test_commands` entry (a block list under `learned:`) runs with `bash -c` in the lane worktree after the build commit, before review and before the PR, with the per-command timeout above. These run on the host and use no Actions minutes. If they all pass, there is no extra builder turn. A failure resumes the builder in its session with the command, its exit code and the output tail, then the commands run again, up to `max_rounds`. A round with no new commit, or exhausted rounds, is recorded as an anomaly and the lane continues, because CI stays the gate. The engine has no built-in command list: to run `scripts/ci-local.sh`, list it here. Each round emits a `PRECHECK_COMMANDS` run-log event (`round`, `results: [{command, rc}]`).
622
+ - **Fix rounds at merge.** When a required check fails on the PR (`engine.merge_wait: required`), the failed jobs' logs (`gh run view --log-failed`, capped) go back to the builder session. The fixed head is re-reviewed and pushed, then the wait starts again. Still red after `max_rounds` (one budget shared with the `ci`/`hybrid` rounds above), or a round with no new commit, ends `needs-human` (`ci.red`). `max_rounds: 0` ends `ci.red` on the first failure. Each round emits a `CI_FIX_ROUND` run-log event (`trigger`, `round`, `jobs`). `merge_wait: all` is unchanged.
623
+
549
624
  **Cost:** `ci`/`hybrid` spend GitHub Actions minutes before review, so they are opt-in per repo. On the green path they add no LLM calls; each red CI round adds one builder resume. The merge gate (`engine.merge_wait`) is unchanged.
550
625
 
551
626
  **Commands that use this section**: core work-on (`forgedock run-issue`, `forgedock orchestrate`; `bin/engine-cli.mjs` `readVerify`)
552
627
 
628
+ ## `execution` (OPTIONAL)
629
+
630
+ Where `forgedock orchestrate` runs its lanes (forge#4253). Block style only.
631
+
632
+ ```yaml
633
+ execution:
634
+ mode: remote # local (default) | remote | hybrid
635
+ max_remote: 4 # remote lanes running at once (default 0 = remote off)
636
+ provider: container # default container when mode is not local
637
+ image: my/lane-image:1 # container provider: an image with git and forgedock; never pulled
638
+ secrets: GH_TOKEN,ANTHROPIC_API_KEY # env var NAMES forwarded to the sandbox; nothing else is
639
+ # provider: remote (forge#4254), only when named explicitly:
640
+ # endpoint: https://runner-pool.example/v1 # sandbox HTTP API (protocol below)
641
+ # token_env: FORGEDOCK_SANDBOX_TOKEN # credential NAME: read from the env, else the OS keychain
642
+ # cache_store: s3://lane-cache # shared cache store the sandbox mounts
643
+ # allow_actions: false # true only to accept a GitHub Actions endpoint
644
+ # execution budget (forge#4255): flat keys, forge.yaml is parsed one level deep
645
+ budget_minutes: 600 # hard cap on sandbox-minutes per batch
646
+ budget_usd: 25 # hard cap on estimated sandbox USD per batch
647
+ day_budget_minutes: 2000 # hard cap on sandbox-minutes per host UTC day (all batches, schedd or not)
648
+ day_budget_usd: 80 # hard cap on estimated sandbox USD per host UTC day
649
+ on_budget: stop # stop (default) | local: what the queue does once a cap is reached
650
+ cost_per_minute: 0.04 # USD per sandbox-minute when the provider reports no cost (default 0)
651
+ ```
652
+
653
+ | Key | Type | Required | Description |
654
+ |-----|------|----------|-------------|
655
+ | `mode` | string | No | `local` (default): lanes are detached host processes, as before. `remote`: lanes run in sandboxes of `provider`. `hybrid`: each lane is placed from facts (forge#4256, below). |
656
+ | `max_remote` | integer | No | Remote lanes of the batch that may run at once. They take no slot of the host lane cap (`orchestration.global_parallel`, `forgedock sched cap`); the batch `--parallel` still applies. `0` (default) keeps lanes local. |
657
+ | `provider` | string | No | `container` (reference: an ephemeral local container), `remote` (a runner pool or remote session over HTTP, forge#4254) or `local`. The default is `container` off `local` mode; `remote` is used only when named. Unknown names fail the batch start. |
658
+ | `image` | string | No | Container provider image; it must provide `git` and `forgedock`. Started with `--pull never`. |
659
+ | `secrets` | string | No | Comma-separated env var names copied from the dispatcher's env into the sandbox. The sandbox also gets `GH_REPO` (the target repo) and `FORGEDOCK_BATCH_ID`, and nothing else from the host. |
660
+ | `endpoint` | string | `remote` | Base URL of the sandbox API. A GitHub Actions API (`api.github.com`, or any `/actions/` path) is refused unless `allow_actions: true`, so pre-review Actions minutes are never spent implicitly. Missing: the batch start fails (`execution.provider.misconfigured`). |
661
+ | `token_env` | string | No | Name of the API credential, sent as `Authorization: Bearer`. Read from the env, else the OS keychain (service `forgedock`, account = the name: `secret-tool store --label forgedock service forgedock account NAME` on Linux, `security add-generic-password -s forgedock -a NAME -w` on macOS). Never written to the ledger, the batch state, logs or GitHub. |
662
+ | `cache_store` | string | No | Shared cache store the remote sandbox mounts (sent as `cache.store` with `cache.key` = the repo). |
663
+ | `allow_actions` | boolean | No | `true` accepts a GitHub Actions endpoint. Default `false`. |
664
+ | `budget_minutes`, `budget_usd` | number | No | The `execution.budget` per batch: once the batch's sandbox minutes / estimated USD (settled lanes plus the running ones' elapsed time) reach the cap, no new remote lane is admitted. Missing, non-numeric or negative means no cap. |
665
+ | `day_budget_minutes`, `day_budget_usd` | number | No | The same cap per host UTC day, summed over every batch on the host from the host-day store (`execution-budget.json` in `~/.forge/sched/`, or `FORGEDOCK_EXECUTION_BUDGET_FILE`). A corrupt or unreadable store refuses remote lanes while a day cap is set; remove the file to reset it. |
666
+ | `on_budget` | string | No | `stop` (default): running lanes finish and the rest end `held` (`execution budget: ...`) for a later `--resume`. `local`: the rest run as detached host lanes. |
667
+ | `cost_per_minute` | number | No | USD per sandbox-minute, used when the provider reports no cost of its own (the `container` provider never does). Default `0`. |
668
+
669
+ **Lifecycle:** each sandbox is listed in the lane ledger (`~/.forge/ledger/<batch>/sandbox-<n>.json`, kind `sandbox`) before it starts and removed from it only after teardown succeeded: on success, failure, a lane that exits without a result, and adopt after `--resume`. A sandbox left by a killed dispatcher is reaped by `forgedock gc --apply` (scope `--sandboxes`). The container provider mounts the checkout's git dir read-only at `/src` (cloned to the container's own `/work`), `~/.forge/verify-cache` read-only at `/cache`, and the batch `lanes/` dir at `/result`.
670
+
671
+ **Remote provider** (`bin/lean/sandbox-remote.mjs`, forge#4254). The sandbox is addressed by the id ForgeDock picks (`fdrsbx-<batch>-<issue>`), JSON over HTTP:
672
+
673
+ | Request | Response |
674
+ |---------|----------|
675
+ | `POST /sandboxes` | Launch spec `{id, issue, batch, image, repo: {slug, url, ref}, cache: {store, key} \| null, env, command, result_file}`. The sandbox clones `repo.url` at `repo.ref`, mounts the cache, runs `command` (`forgedock run-issue ...`) with exactly `env` (the `secrets` names, resolved from the env or keychain, plus `GH_REPO` and `FORGEDOCK_BATCH_ID`). `429`/`503` = no capacity (`Retry-After` honoured). |
676
+ | `GET /sandboxes/:id` | `{state}`: `pending`/`starting`/`running`, `exited`, or `404` = gone |
677
+ | `GET /sandboxes/:id/logs?cursor=N` | `{text, cursor}`: lane log since `cursor`; appended to the batch lane log with secret values redacted |
678
+ | `GET /sandboxes/:id/result` | The lane's result file (`404` until written) |
679
+ | `GET /sandboxes/:id/usage` | `{minutes}`: billable sandbox minutes, set on the handle and the lane result (`sandbox.minutes`); without it, wall-clock minutes |
680
+ | `DELETE /sandboxes/:id` | Teardown; `404` counts as torn down |
681
+
682
+ **Start failures** (every provider): a sandbox that cannot start ends the lane `needs-human` with code `sandbox.start_failed`, not relaunched and not counted by the engine-error breaker; a capacity error ends it `deferred` (`sandbox.capacity`, backoff, bounded by `orchestration.max_deferrals`). See docs/OUTCOME-CODES.md. `forgedock gc --apply --sandboxes` tears a stale remote sandbox down through the same API (the checkout's forge.yaml `execution`).
683
+
684
+ **Hybrid placement** (`mode: hybrid`, forge#4256, `bin/lean/execution.mjs` `placeLane`, no model call). Each lane is placed when it is admitted. The first matching rule wins:
685
+
686
+ 1. `max_remote: 0` → local.
687
+ 2. `verify.mode: ci` → local. Heavy validation already runs, and is billed, on CI, so the lane is not billed twice.
688
+ 3. A docs-only lane (issue label `docs`, `documentation` or `type:docs`) → local.
689
+ 4. Heavy expected validation → remote. This means a declared check command (`learned.test_commands`) is a type check, linter, test runner or build, or the repo has a heavy toolchain marker: `tsconfig.json`, an eslint config, a package.json `test`/`lint`/`typecheck`/`build` script, `Cargo.toml`, `go.mod`, `pyproject.toml`/`setup.cfg`/`pytest.ini`, `pom.xml` or `build.gradle`.
690
+ 5. A host without headroom → remote. "Without headroom" means MemAvailable is below `engine.min_free_mem_mb` + `engine.heavy_step_mem_mb`, or the load per core is above `engine.max_load_per_core` when that is set.
691
+ 6. Otherwise → local.
692
+
693
+ A lane placed remote runs on the host instead when `max_remote` remote lanes are already running, or when the execution budget refuses it. In hybrid mode this happens whatever `on_budget` says. The decision `{where, reason}` (for example `fallback to local: max_remote 4 reached (heavy validation: tsc)`) is recorded:
694
+
695
+ - in the batch row (`placement`);
696
+ - in the trajectory (`Placement: ...`);
697
+ - in the lane's FORGE:STATE as `placement: {where, reason, mode}`. The lane gets it as `run-issue --placement <local|remote> --placement-reason <text>`.
698
+
699
+ **Cost and budget:** every remote lane's sandbox minutes and estimated cost (the provider's own usage report, e.g. the remote provider's `/usage` minutes, else wall-clock minutes × `cost_per_minute`) land in its row (`sandbox: {minutes, cost_usd, attempts}`, summed over relaunches), in `report.json`, in the printed table (`sbx-min`, `sbx-$` next to the token `cost`, shown only when a lane used a sandbox), in a `sandbox:` summary line with the budget state, and in FORGE:TELEMETRY as one `sandbox_usage` event per lane. The caps are plain arithmetic, no model call. The batch total is kept in the batch `state.json` and the day total in the host-day store, so both survive `--resume` and a schedd restart.
700
+
701
+ **Commands that use this section**: `forgedock orchestrate` (`bin/engine-cli.mjs` `readExecution`), `forgedock gc`
702
+
703
+ ## `env` (OPTIONAL)
704
+
705
+ Lane environment for the core work-on (#4163). With no keys set, lanes still pin the target repo's own toolchain:
706
+
707
+ - **Package manager**: `package.json` `packageManager` (`pnpm|yarn|npm@x.y.z`, a `+sha…` suffix is ignored), else a `pnpm`/`yarn` line in `.tool-versions`. After the worktree is added the engine writes shims for it (`pnpm`+`pnpx`, `yarn`, or `npm`+`npx`) to `<run tmp>/toolchain/bin`. Each shim runs `corepack <pm>@<version>` (npm always via `npx -y`), or `npx -y` when corepack is not on PATH (yarn ≥ 2 via `@yarnpkg/cli-dist`). The shim dir goes first on the PATH of every runner call (builder and reviewer), so the builder's shell, its validate commands and git hooks (lefthook, husky) resolve the pinned version. `COREPACK_ENABLE_DOWNLOAD_PROMPT=0` and `COREPACK_ENABLE_STRICT=0` are set. The builder prompt says the toolchain is pinned and not to install global shims.
708
+ - **Node**: `.nvmrc`, else `.node-version`, else `.tool-versions` `nodejs`, else `package.json` `engines.node`. When the host `node` does not satisfy it, the run's trajectory shows a warning under **Anomalies**. Node is not switched.
709
+
710
+ A repo with no pins and no `env` keys runs exactly as before: no shim dir, no env change, no prompt change.
711
+
712
+ ```yaml
713
+ env:
714
+ toolchain: "pnpm@10.4.1" # optional: overrides the detected package manager; "off" disables detection
715
+ setup: "bash scripts/reconcile-dev-db.sh" # optional: run once per fresh lane worktree, before the builder
716
+ sparse_max_kb: 1024 # optional: tracked files this size or larger stay out of lane worktrees; 0 = full checkout
717
+ ```
718
+
719
+ | Field | Type | Required | Description |
720
+ |-------|------|----------|-------------|
721
+ | `toolchain` | string | No | `<pnpm\|yarn\|npm>@<x.y.z>` replaces the detected package-manager pin; `off` disables all toolchain detection. An invalid value is ignored and recorded as an anomaly. |
722
+ | `setup` | string | No | Shell command (`bash -c`) run once in each fresh lane worktree before the builder starts, with the lane's PATH and `TMPDIR`. Use it for environment drift the repo's migrations don't cover, for example a dev-database reconcile script. Timeout 15 min. A non-zero exit ends the lane `needs-human` with code `env.setup_failed`: an environment error, not a build failure. The builder never runs. |
723
+ | `sparse_max_kb` | integer | No | Default `1024`. Lane worktrees are sparse checkouts: tracked files of this many KB or more (generated JSON, screenshots, PDFs, media) are not written to disk, which cuts per-lane disk in repos that track large assets (#4221). Lockfiles, manifests (`package.json`, `pyproject.toml`, `requirements*.txt`, ...), toolchain pins, `tsconfig*.json`, `forge.yaml`, `.gitattributes` and `.gitignore` are always checked out. A repo with no such file gets a plain checkout. The builder prompt names the absent files; `git sparse-checkout add /<path>` brings one back. `0` disables it. Git sets `extensions.worktreeConfig=true` in the repo's shared config; the sparse setting itself is per lane worktree, so the main checkout is unchanged. If the sparse setup fails, the lane uses a full checkout. |
724
+
725
+ **Commands that use this section**: `forgedock run-issue`, `forgedock orchestrate` (core path; `bin/engine-cli.mjs` `readEnvConfig`)
726
+
553
727
  ---
554
728
 
555
729
  ## `build` (OPTIONAL)
@@ -1141,6 +1315,7 @@ These variables are read from the operator's runner environment, not from `forge
1141
1315
  | `FORGE_TRAIL_OVERRIDE_ASSOCIATIONS` | the resolved `FORGE_TRAIL_TRUSTED_ASSOCIATIONS` value | Comma-separated `author_association` values prefiltering break-glass override candidates (the permission API stays authoritative). Never affects which FORGE markers count toward the gate. An approver with concealed org membership can show `CONTRIBUTOR`/`NONE`, so widen this variable (not the trail list) to accept them. An explicitly empty value means no override candidates. |
1142
1316
  | `FORGE_TRAIL_PIPELINE_LOGINS` | empty | Comma-separated GitHub logins (case-insensitive) that are the pipeline's own identity and can never approve a phase-trail break-glass override. The authors of trusted FORGE marker comments on the issue are added automatically. |
1143
1317
  | `FORGE_TRAIL_QG_SINCE` | `2026-10-07T03:40:12Z` | ISO-8601 UTC cutoff for the legacy `FORGE:QUALITY_GATE` grace. An issue whose trusted `FORGE:BUILDER:COMPLETE` comment was last updated before this time has `QUALITY_GATE` waived. Set to an empty string to disable the grace. A malformed or future value (including a runner clock earlier than this default) fails closed: the verifier exits 2, an infrastructure block rather than `MISSING`. |
1318
+ | `FORGEDOCK_ALLOW_API_BACKEND` | unset | Set to `1` (host owner approval) together with forge.yaml `execution.agent_backend: api` to let `--backend auto` use the paid API backend when no `claude` CLI is available. Without both, `auto` never selects `api` (#4257). |
1144
1319
  | `FORGE_TELEMETRY` | `on` | Set to `off` to disable gate telemetry. Gate scripts append one structured record per decision to a trusted `FORGE:TELEMETRY v1` comment; emission is best-effort and never changes a gate's output or exit code. See [TELEMETRY.md](TELEMETRY.md). |
1145
1320
 
1146
1321
  ### Install root resolution