* enhance(#4570): allow disabling planner stall detection * docs(#4570): add changelog fragment Emitted-Drift-Ack-Growth: plan-phase.md — the explicit opt-out gate covers all five planner and checker spawn classes Emitted-Drift-Ack-Growth: settings-advanced.md — the toggle prompt and bounded-recovery warning expose the new setting * chore(#4570): refresh compact-content baseline * fix(#4570): preserve default-on watchdog fallback * docs(#4570): qualify the chunked-mode orchestrator rules with the toggle The two chunked-planning-mode stall-watch imperatives read as unconditional, with the opt-out stated only in a following bullet. State the PLANNER_STALL_DETECTION_ENABLED condition inline, matching the three sites already qualified in plan-phase.md. * chore(#4570): refresh compact-content baselines against rebased next * fix(#4570): sync planner stall launcher Keep the stall-detection helper aligned with the canonical runtime launcher. * chore(#4570): refresh compact baseline after rebase --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -941,6 +941,9 @@ Every task MUST include these fields — they are NOT optional:
|
||||
|
||||
**If `CHUNKED_MODE` is `false` (default):** Spawn the planner as a single long-lived Agent:
|
||||
|
||||
**Dispatch/wait gate — `PLANNER_STALL_DETECTION_ENABLED`:**
|
||||
- **`true` (default):** use `run_in_background=true` in the Agent() call shown below, then use `gsd_stall_watch` as specified after it.
|
||||
|
||||
```text
|
||||
Agent(
|
||||
prompt=filled_prompt,
|
||||
@@ -951,7 +954,9 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES:** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md' "## PLANNING COMPLETE" "## PHASE SPLIT RECOMMENDED" "## ⚠ Source Audit" "## CHECKPOINT REACHED" "## PLANNING INCONCLUSIVE")` while waiting/active — `marker_received` -> step 9; `stalled` -> 9a.
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES (when `PLANNER_STALL_DETECTION_ENABLED` is `true`):** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md' "## PLANNING COMPLETE" "## PHASE SPLIT RECOMMENDED" "## ⚠ Source Audit" "## CHECKPOINT REACHED" "## PLANNING INCONCLUSIVE")` while waiting/active — `marker_received` -> step 9; `stalled` -> 9a.
|
||||
|
||||
- **`false`:** issue the same Agent() call but omit `run_in_background`; await its ordinary runtime-native completion and pass the real returned result to step 9. Skip `gsd_stall_watch` entirely. This is not fire-and-forget; empty, truncated, or unrecognized returns still use step 9a.
|
||||
|
||||
**If `CHUNKED_MODE` is `true`:** Skip the Agent() call above — proceed to step 8.5 instead.
|
||||
|
||||
@@ -1080,7 +1085,12 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES:** `TS=$(date +%s)`; repeat `CHECKER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md' "## VERIFICATION PASSED" "## ISSUES FOUND")` while waiting/active.
|
||||
**Dispatch/wait gate — `PLANNER_STALL_DETECTION_ENABLED`:**
|
||||
- **`true` (default):** use `run_in_background=true` in the Agent() call above, then use `gsd_stall_watch` below.
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES (when `PLANNER_STALL_DETECTION_ENABLED` is `true`):** `TS=$(date +%s)`; repeat `CHECKER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md' "## VERIFICATION PASSED" "## ISSUES FOUND")` while waiting/active.
|
||||
|
||||
- **`false`:** issue the same Agent() call but omit `run_in_background`; await its ordinary runtime-native completion and pass the real returned result to step 11. Skip `gsd_stall_watch` entirely. Treat a recognized returned marker exactly like `marker_received`; empty, truncated, or unrecognized returns still use step 11a.
|
||||
|
||||
## 11. Handle Checker Return
|
||||
|
||||
@@ -1172,7 +1182,12 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES:** (7.99; no marker, mtimes only) `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md')` while waiting/active — `stalled` -> 1) Accept as revised, to step 13, 2) Retry, 3) Stop.
|
||||
**Dispatch/wait gate — `PLANNER_STALL_DETECTION_ENABLED`:**
|
||||
- **`true` (default):** use `run_in_background=true` in the Agent() call above, then use `gsd_stall_watch` below.
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES (when `PLANNER_STALL_DETECTION_ENABLED` is `true`):** (7.99; no marker, mtimes only) `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "${PHASE_DIR}"'/*-PLAN.md')` while waiting/active — `stalled` -> 1) Accept as revised, to step 13, 2) Retry, 3) Stop.
|
||||
|
||||
- **`false`:** issue the same Agent() call but omit `run_in_background`; await its ordinary runtime-native completion and pass the real returned result into the existing revision-return handling. Skip `gsd_stall_watch` entirely; an empty, truncated, or unrecognized result keeps the existing filesystem fallback.
|
||||
|
||||
**If the planner returns `## REVISION_CONFLICT`:** follow the shared Conflict Return protocol in
|
||||
`gsd-core/references/revision-loop.md`, with this workflow's bindings:
|
||||
|
||||
@@ -50,6 +50,9 @@ Display:
|
||||
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
|
||||
PLAN.md files:
|
||||
|
||||
**Dispatch/wait gate — `PLANNER_STALL_DETECTION_ENABLED`:**
|
||||
- **`true` (default):** use `run_in_background=true` in the Agent() call below, then use `gsd_stall_watch` after it.
|
||||
|
||||
```javascript
|
||||
Agent(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
@@ -71,7 +74,9 @@ Agent(
|
||||
)
|
||||
```
|
||||
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES:** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "$OUTLINE_FILE" "## OUTLINE COMPLETE")` while waiting/active.
|
||||
**ORCHESTRATOR RULE — ALL RUNTIMES (when `PLANNER_STALL_DETECTION_ENABLED` is `true`):** `TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "$OUTLINE_FILE" "## OUTLINE COMPLETE")` while waiting/active.
|
||||
|
||||
- **`false`:** issue the same Agent() call but omit `run_in_background`; await its ordinary runtime-native completion and consume the real returned result. Skip `gsd_stall_watch` entirely and treat a recognized `## OUTLINE COMPLETE` return exactly like `marker_received`; empty or unrecognized returns keep the existing Retry/Stop path.
|
||||
|
||||
Handle return:
|
||||
- **`marker_received`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
|
||||
@@ -137,7 +142,8 @@ path regardless of `CHUNKED_PARALLEL` — there is nothing to batch.
|
||||
4. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file. The prompt
|
||||
is unchanged per plan; what changes is whether the runnable set's Agent() calls are issued one
|
||||
at a time (serial) or together in one message (concurrent, every call still carrying
|
||||
`run_in_background=true` exactly as today):
|
||||
`run_in_background=true` exactly as today when `PLANNER_STALL_DETECTION_ENABLED` is `true`
|
||||
(default)):
|
||||
```javascript
|
||||
Agent(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
@@ -161,7 +167,7 @@ path regardless of `CHUNKED_PARALLEL` — there is nothing to batch.
|
||||
**Concurrent dispatch:** issue every runnable entry's Agent() call together, in this one
|
||||
message, before waiting on any of them.
|
||||
|
||||
5. **ORCHESTRATOR RULE — ALL RUNTIMES, per batch:** for every entry dispatched in this round,
|
||||
5. **ORCHESTRATOR RULE — ALL RUNTIMES, per batch (when `PLANNER_STALL_DETECTION_ENABLED` is `true`):** for every entry dispatched in this round,
|
||||
`TS=$(date +%s)`; repeat `PLANNER_STALL_RESULT=$(gsd_stall_watch "$TS" "{outputFile}" "$PLAN_FILE" "## PLAN COMPLETE")`
|
||||
while waiting/active for THAT entry. Serial dispatch waits on one entry at a time (unchanged).
|
||||
Concurrent dispatch waits on every entry issued in step 4 before proceeding — this is the
|
||||
@@ -170,6 +176,13 @@ path regardless of `CHUNKED_PARALLEL` — there is nothing to batch.
|
||||
Retry/Stop recovery for that one plan; it does not block verifying/committing sibling entries
|
||||
in the same batch that already reached `marker_received`.
|
||||
|
||||
- **`false`:** when `PLANNER_STALL_DETECTION_ENABLED` is `false`, omit `run_in_background`
|
||||
from every call, skip `gsd_stall_watch`, and await each real returned result through the
|
||||
ordinary runtime-native completion mechanism. A concurrent batch still issues the runnable
|
||||
calls together and awaits all of their ordinary results before step 6. Treat each recognized
|
||||
`## PLAN COMPLETE` return exactly like `marker_received`; empty or unrecognized returns keep
|
||||
step 8's existing Retry/Stop path. This is never fire-and-forget.
|
||||
|
||||
6. **Verify disk, per entry:** check `${PHASE_DIR}/{plan_id}-PLAN.md` exists for each entry that
|
||||
reached `marker_received`. Unchanged per-plan check.
|
||||
|
||||
@@ -189,4 +202,3 @@ gsd_run query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files
|
||||
Move to the next Wave only once every entry in the current Wave is committed or the run was
|
||||
stopped. After every Wave's plans are written and committed, treat this as `## PLANNING COMPLETE`
|
||||
and continue to step 9.
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Bounded Stall-Detection Helpers (#2650)
|
||||
|
||||
Every planner/plan-checker spawn in `plan-phase.md` dispatches with
|
||||
`run_in_background=true`, records `TS=$(date +%s)`, and then repeatedly
|
||||
calls `gsd_stall_watch` until it returns something other than
|
||||
`planner.stall_detection_enabled` controls the wait policy for the five scoped
|
||||
planner/plan-checker spawns. It defaults to `true`. In that default-on mode each
|
||||
spawn dispatches with `run_in_background=true`, records `TS=$(date +%s)`, and
|
||||
then repeatedly calls `gsd_stall_watch` until it returns something other than
|
||||
`waiting`/`active`. This mirrors the already-shipped `executor.stall_*`
|
||||
pattern (`execute-phase.md`, bug #3212, commit `e7942c21b`) but — unlike
|
||||
that prose-only surveillance, which cannot run during a *blocking* `Agent()`
|
||||
@@ -11,6 +12,15 @@ issued as its own tool call, so it returns control to the orchestrator on
|
||||
its own schedule regardless of whether the backgrounded agent's own
|
||||
completion notification ever arrives.
|
||||
|
||||
When the key is explicitly the JSON boolean `false`, each scoped call instead
|
||||
omits `run_in_background`, waits through the runtime-native ordinary Agent()
|
||||
completion mechanism, and consumes that real returned result. It never calls
|
||||
`gsd_stall_watch`, so there are no periodic shell sleeps. This is still a wait,
|
||||
not fire-and-forget. It deliberately gives up #2650's bounded automatic
|
||||
recovery: if the runtime loses the completion handoff, the user may need to
|
||||
interrupt and use the existing filesystem fallback. The completion markers and
|
||||
empty/truncated/unrecognized-return fallback remain unchanged.
|
||||
|
||||
**Binding `{outputFile}` (load-bearing, not optional):** every `gsd_stall_watch`
|
||||
call below takes `{outputFile}` as its second argument — a literal token the
|
||||
orchestrator must substitute with the REAL path from the immediately preceding
|
||||
@@ -71,10 +81,18 @@ config-get calls.
|
||||
This block is independent of, and never gated behind, the `query
|
||||
teams-status` / `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` guard used for the
|
||||
researcher spawn — the stall path applies on every runtime, teams-active or
|
||||
not (AC2).
|
||||
not (AC2). The toggle is resolved through the canonical `config-get` seam, so
|
||||
root/project/workstream selection stays with the Config Loader rather than a
|
||||
workflow-local JSON parser. Only the exact boolean result `false` disables;
|
||||
missing, malformed, string, numeric, or otherwise unrecognized values fail safe
|
||||
to `true`.
|
||||
|
||||
```bash
|
||||
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { _gsd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
||||
PLANNER_STALL_DETECTION_ENABLED=$(gsd_run query config-get planner.stall_detection_enabled --raw 2>/dev/null || echo "true")
|
||||
# Defense in depth for hand-edited config and old runtimes: only the exact
|
||||
# canonical false token may disable the default-on recovery policy.
|
||||
[ "$PLANNER_STALL_DETECTION_ENABLED" = "false" ] || PLANNER_STALL_DETECTION_ENABLED=true
|
||||
PLANNER_STALL_INTERVAL_MINUTES=$(gsd_run query config-get planner.stall_detect_interval_minutes --raw 2>/dev/null || echo "5")
|
||||
PLANNER_STALL_THRESHOLD_MINUTES=$(gsd_run query config-get planner.stall_threshold_minutes --raw 2>/dev/null || echo "10")
|
||||
# Both values are config-controlled (.planning/config.json, editable by any repo
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers.
|
||||
|
||||
<purpose>
|
||||
Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts,
|
||||
inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
||||
Interactive configuration of GSD power-user knobs — plan bounce, planner stall detection, node
|
||||
repair, subagent timeouts, inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
||||
context window, gitignored search, graphify build timeout, runtime model tier overrides, and
|
||||
model policy configuration (provider + budget → canonical tier mapping, or manual model ID
|
||||
assignment per cost tier).
|
||||
@@ -49,6 +49,7 @@ Parse the following current values. If a key is absent, fall back to the documen
|
||||
shown in parentheses:
|
||||
|
||||
Planning Tuning:
|
||||
- `planner.stall_detection_enabled` (default: `true`)
|
||||
- `workflow.plan_bounce` (default: `false`)
|
||||
- `workflow.plan_bounce_passes` (default: `2`)
|
||||
- `workflow.plan_bounce_script` (default: `null`)
|
||||
@@ -126,8 +127,21 @@ stored verbatim as a string.
|
||||
|
||||
### Section 1 — Planning Tuning
|
||||
|
||||
Before offering the disabled choice, display this warning verbatim:
|
||||
|
||||
`Warning: disabling planner stall detection gives up bounded automatic recovery if the runtime loses an agent completion handoff. Planning still waits through the runtime-native completion mechanism, but you may need to interrupt and use the filesystem fallback.`
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Enable bounded planner/plan-checker stall detection? (current: <value or true>)",
|
||||
header: "Planner Watchdog",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (default: true)", description: "Poll for completion markers and plan-file activity, with bounded recovery." },
|
||||
{ label: "No (false)", description: "Await runtime-native completion without polling. Gives up bounded automatic recovery if completion handoff is lost." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Run external plan-bounce validator against generated PLAN.md? (current: <value or false>)",
|
||||
header: "Plan Bounce",
|
||||
@@ -500,6 +514,7 @@ keys and sibling sub-objects.
|
||||
|
||||
```bash
|
||||
# Example — only write keys the user changed. "Keep current" selections are skipped.
|
||||
gsd_run query config-set planner.stall_detection_enabled false
|
||||
gsd_run query config-set workflow.plan_bounce_passes 5
|
||||
gsd_run query config-set workflow.subagent_timeout 300000
|
||||
gsd_run query config-set git.base_branch main
|
||||
@@ -518,6 +533,10 @@ anything not listed in Sections 1–8 MUST survive the update):
|
||||
```json
|
||||
{
|
||||
...existing_config,
|
||||
"planner": {
|
||||
...existing_planner,
|
||||
"stall_detection_enabled": <new|existing>
|
||||
},
|
||||
"workflow": {
|
||||
...existing_workflow,
|
||||
"plan_bounce": <new|existing>,
|
||||
@@ -752,6 +771,7 @@ Display:
|
||||
|
||||
| Setting | Value |
|
||||
|--------------------------------------------|-------|
|
||||
| planner.stall_detection_enabled | {true/false} |
|
||||
| workflow.plan_bounce | {on/off} |
|
||||
| workflow.plan_bounce_passes | {n} |
|
||||
| workflow.plan_bounce_script | {path/null} |
|
||||
@@ -805,6 +825,7 @@ UI/AI phase gates), use /gsd:settings.
|
||||
- [ ] Current config read from resolved `$GSD_CONFIG_PATH`
|
||||
- [ ] Eight sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime/Output, Runtime Model Tiers, Model Policy)
|
||||
- [ ] Every field pre-selected to its current value (or documented default if absent)
|
||||
- [ ] Disabling planner stall detection shows the bounded-recovery warning before writing `false`
|
||||
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
|
||||
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
||||
- [ ] Null-allowed fields accept an empty input as a clear
|
||||
|
||||
Reference in New Issue
Block a user