* 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:
5
.changeset/clever-herons-chatter.md
Normal file
5
.changeset/clever-herons-chatter.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Changed
|
||||||
|
pr: 4585
|
||||||
|
---
|
||||||
|
**Planner stall detection can now be disabled explicitly** — set `planner.stall_detection_enabled` to `false` to use the runtime-native completion wait without watchdog polling; the default remains enabled, and disabling it gives up bounded automatic recovery.
|
||||||
@@ -280,6 +280,8 @@ Module owning agent-presence resolution and verification, extracted from the Cor
|
|||||||
### Config Loader Module
|
### Config Loader Module
|
||||||
Module owning project configuration loading: reads `.planning/config.json`, merges built-in defaults (`CONFIG_DEFAULTS`/`CANONICAL_CONFIG_DEFAULTS`), normalizes legacy keys, applies the active-workstream overlay, validates against the config schema, and warns on unknown keys/profile overrides. Primary interface: `loadConfigResolved(cwd, options) → ConfigResolution { config, source, degraded }` (provenance-aware, ADR-1411 P2 / #1415) — `source` ∈ `'workstream' | 'root' | 'builtin-defaults' | 'global-defaults'`; `degraded:true` when a workstream was requested but its config.json was absent (fell back to root config). `loadConfig(cwd, options) → Record<string,unknown>` is the back-compat thin wrapper over `loadConfigResolved` (byte-identical result). Loading is **not side-effect-free by default**: normalizing a legacy key marks the config dirty and writes the migrated shape back to disk, which is how a legacy project is migrated by ordinary use. `options.persist: false` (#3648) suppresses exactly that write while leaving resolution identical — the option a caller passes when it is only ASKING the config something (the Git Query Module's protected-branch predicate runs on every `execute-phase` and `ship`, and a question must not rewrite the file it asks about). It is opt-OUT, so every pre-existing caller keeps the historical write-back. Resolution is **caller-anchored, not loader-anchored**: `loadConfigResolved` resolves `cwd` as-is (no walk-up), so `loadConfig` stays byte-identical for its callers; callers that need cwd-drift tolerance (e.g. `cmdAgentSkills`) anchor to the project root via `findProjectRoot` (Project-Root Resolution Module) *before* calling `loadConfigResolved`. Helper exports: `_deepMergeConfig`, `isGitIgnored`, `_warnUnknownProfileOverrides`. Depends only on leaf modules (`configuration`, `config-schema`, `planning-workspace`, `shell-command-projection`, `core-utils`, `model-catalog`) — no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2e (#885) as the prerequisite for the model-resolver extraction (the resolvers call `loadConfig`); the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/config-loader.cjs` (generated from `src/config-loader.cts`).
|
Module owning project configuration loading: reads `.planning/config.json`, merges built-in defaults (`CONFIG_DEFAULTS`/`CANONICAL_CONFIG_DEFAULTS`), normalizes legacy keys, applies the active-workstream overlay, validates against the config schema, and warns on unknown keys/profile overrides. Primary interface: `loadConfigResolved(cwd, options) → ConfigResolution { config, source, degraded }` (provenance-aware, ADR-1411 P2 / #1415) — `source` ∈ `'workstream' | 'root' | 'builtin-defaults' | 'global-defaults'`; `degraded:true` when a workstream was requested but its config.json was absent (fell back to root config). `loadConfig(cwd, options) → Record<string,unknown>` is the back-compat thin wrapper over `loadConfigResolved` (byte-identical result). Loading is **not side-effect-free by default**: normalizing a legacy key marks the config dirty and writes the migrated shape back to disk, which is how a legacy project is migrated by ordinary use. `options.persist: false` (#3648) suppresses exactly that write while leaving resolution identical — the option a caller passes when it is only ASKING the config something (the Git Query Module's protected-branch predicate runs on every `execute-phase` and `ship`, and a question must not rewrite the file it asks about). It is opt-OUT, so every pre-existing caller keeps the historical write-back. Resolution is **caller-anchored, not loader-anchored**: `loadConfigResolved` resolves `cwd` as-is (no walk-up), so `loadConfig` stays byte-identical for its callers; callers that need cwd-drift tolerance (e.g. `cmdAgentSkills`) anchor to the project root via `findProjectRoot` (Project-Root Resolution Module) *before* calling `loadConfigResolved`. Helper exports: `_deepMergeConfig`, `isGitIgnored`, `_warnUnknownProfileOverrides`. Depends only on leaf modules (`configuration`, `config-schema`, `planning-workspace`, `shell-command-projection`, `core-utils`, `model-catalog`) — no other core dependency. Extracted from the Core module per ADR-857 rollout phase 2e (#885) as the prerequisite for the model-resolver extraction (the resolvers call `loadConfig`); the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/config-loader.cjs` (generated from `src/config-loader.cts`).
|
||||||
|
|
||||||
|
`planner.stall_detection_enabled` (#4570) is a central, default-on boolean projected as `planner_stall_detection_enabled` by this module. Root/workstream overlay happens before its type resolution; only a real boolean is honored, while an absent or hand-edited non-boolean value fails safe to the manifest default `true`. The plan-phase workflow reads it through `query config-get`, never by parsing config JSON locally, so root/project/workstream routing remains owned by the canonical configuration seams.
|
||||||
|
|
||||||
### Planning Publication Gate (`planning.pr_strict`)
|
### Planning Publication Gate (`planning.pr_strict`)
|
||||||
The seam deciding whether `.planning/` artifacts reach the REMOTE — distinct from the Planning Commit Gate below, which decides whether they reach git at all. `planning.pr_strict` (boolean, manifest default `false`) resolves through the same `loadConfigResolved` chain as its `planning.*` siblings (explicit top-level `pr_strict`, then the `planning.pr_strict` alias, then the manifest default), and is additionally registered in `SCHEMA_DEFAULTS` (`src/config.cts`) so `query config-get planning.pr_strict` answers `false` for an absent key rather than `Key not found` — `gsd-core/workflows/pr-branch.md` reads it with a plain `config-get` and must not special-case a missing key. It selects between two filter modes in that workflow: default preserves the five structural planning files plus `milestones/**` and drops nine transient subdirectories; strict drops every `.planning/` path and includes a commit only when it touches at least one file outside `.planning/`. The two path lists are declared ONCE in the workflow (`TRANSIENT_DIRS`, `STRUCTURAL_RE`) and both the un-stage step and the verification assertion are derived from them, because the prior shape declared them twice and the two steps disagreed by construction — `verify` asserted zero `.planning/` paths while `create_pr_branch` was specified to preserve five, so a correct run reported itself as failed on every phase (#2971). A third gate is deliberately NOT derived from those declarations: `PLANNING_DELETIONS` (#3679) counts DELETED `.planning/` paths via `git diff --name-status --no-renames` and must be `0` in every mode — a deleted planning path is data loss, not filtering, and name-only counting cannot see status. The two gates are independent but not orthogonal in effect: `pr_strict` is inert when `commit_docs` is `false`, since nothing is committed for the PR-branch filter to remove. Source of truth: `gsd-core/workflows/pr-branch.md`; key registered in `gsd-core/bin/shared/config-{defaults,schema}.manifest.json`.
|
The seam deciding whether `.planning/` artifacts reach the REMOTE — distinct from the Planning Commit Gate below, which decides whether they reach git at all. `planning.pr_strict` (boolean, manifest default `false`) resolves through the same `loadConfigResolved` chain as its `planning.*` siblings (explicit top-level `pr_strict`, then the `planning.pr_strict` alias, then the manifest default), and is additionally registered in `SCHEMA_DEFAULTS` (`src/config.cts`) so `query config-get planning.pr_strict` answers `false` for an absent key rather than `Key not found` — `gsd-core/workflows/pr-branch.md` reads it with a plain `config-get` and must not special-case a missing key. It selects between two filter modes in that workflow: default preserves the five structural planning files plus `milestones/**` and drops nine transient subdirectories; strict drops every `.planning/` path and includes a commit only when it touches at least one file outside `.planning/`. The two path lists are declared ONCE in the workflow (`TRANSIENT_DIRS`, `STRUCTURAL_RE`) and both the un-stage step and the verification assertion are derived from them, because the prior shape declared them twice and the two steps disagreed by construction — `verify` asserted zero `.planning/` paths while `create_pr_branch` was specified to preserve five, so a correct run reported itself as failed on every phase (#2971). A third gate is deliberately NOT derived from those declarations: `PLANNING_DELETIONS` (#3679) counts DELETED `.planning/` paths via `git diff --name-status --no-renames` and must be `0` in every mode — a deleted planning path is data loss, not filtering, and name-only counting cannot see status. The two gates are independent but not orthogonal in effect: `pr_strict` is inert when `commit_docs` is `false`, since nothing is committed for the PR-branch filter to remove. Source of truth: `gsd-core/workflows/pr-branch.md`; key registered in `gsd-core/bin/shared/config-{defaults,schema}.manifest.json`.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
|||||||
"search_gitignored": false,
|
"search_gitignored": false,
|
||||||
"sub_repos": []
|
"sub_repos": []
|
||||||
},
|
},
|
||||||
|
"planner": {
|
||||||
|
"stall_detection_enabled": true
|
||||||
|
},
|
||||||
"context": null,
|
"context": null,
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"research": true,
|
"research": true,
|
||||||
@@ -545,6 +548,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
|||||||
| `workflow.subagent_timeout` | number | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes) |
|
| `workflow.subagent_timeout` | number | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes) |
|
||||||
| `executor.stall_detect_interval_minutes` | number | `5` | Minutes between executor stall checks while an executor agent is active. The execute-phase orchestrator uses this cadence to inspect recent commits and avoid waiting forever on a silent agent. |
|
| `executor.stall_detect_interval_minutes` | number | `5` | Minutes between executor stall checks while an executor agent is active. The execute-phase orchestrator uses this cadence to inspect recent commits and avoid waiting forever on a silent agent. |
|
||||||
| `executor.stall_threshold_minutes` | number | `10` | Minutes without executor completion or expected-branch commit activity before execute-phase offers recovery choices for a possible stalled executor. |
|
| `executor.stall_threshold_minutes` | number | `10` | Minutes without executor completion or expected-branch commit activity before execute-phase offers recovery choices for a possible stalled executor. |
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | Controls bounded stall detection for the standard planner, chunked outline/per-plan planners, plan-checker, and revision planner. Set it with `gsd config-set planner.stall_detection_enabled false` to skip watchdog polling and await each agent through the runtime-native completion mechanism instead. **Warning:** `false` gives up bounded recovery if the runtime loses the completion handoff; you may need to interrupt and use the existing filesystem fallback. Planner execution and result handling are never skipped. |
|
||||||
| `planner.stall_detect_interval_minutes` | number | `5` | Minutes between planner/plan-checker stall checks while a planner or plan-checker agent is active. The plan-phase orchestrator uses this cadence to inspect on-disk `*-PLAN.md` activity and avoid waiting forever on a silent agent (#2650). |
|
| `planner.stall_detect_interval_minutes` | number | `5` | Minutes between planner/plan-checker stall checks while a planner or plan-checker agent is active. The plan-phase orchestrator uses this cadence to inspect on-disk `*-PLAN.md` activity and avoid waiting forever on a silent agent (#2650). |
|
||||||
| `planner.stall_threshold_minutes` | number | `10` | Minutes without a completion marker or fresh on-disk plan activity before plan-phase automatically surfaces the accept-plans/retry/stop recovery choice for a possible stalled planner or plan-checker (#2650). |
|
| `planner.stall_threshold_minutes` | number | `10` | Minutes without a completion marker or fresh on-disk plan activity before plan-phase automatically surfaces the accept-plans/retry/stop recovery choice for a possible stalled planner or plan-checker (#2650). |
|
||||||
| `workflow.inline_plan_threshold` | number | `3` | Maximum number of tasks in a phase before the planner generates a separate PLAN.md file instead of inlining tasks in the prompt |
|
| `workflow.inline_plan_threshold` | number | `3` | Maximum number of tasks in a phase before the planner generates a separate PLAN.md file instead of inlining tasks in the prompt |
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ GSD はプロジェクト設定を `.planning/config.json` に保存します。
|
|||||||
| `workflow.discuss_mode` | string | `'discuss'` | `/gsd-discuss-phase` のコンテキスト収集方法を制御。`'discuss'`(デフォルト)は質問を1つずつ行います。`'assumptions'` はまずコードベースを読み取り、信頼度レベル付きの構造化された仮説を生成し、誤っている点のみ修正を求めます。v1.28 で追加 |
|
| `workflow.discuss_mode` | string | `'discuss'` | `/gsd-discuss-phase` のコンテキスト収集方法を制御。`'discuss'`(デフォルト)は質問を1つずつ行います。`'assumptions'` はまずコードベースを読み取り、信頼度レベル付きの構造化された仮説を生成し、誤っている点のみ修正を求めます。v1.28 で追加 |
|
||||||
| `workflow.skip_discuss` | boolean | `false` | `true` の場合、`/gsd-autonomous` は discuss-phase を完全にスキップし、ROADMAP のフェーズ目標から最小限の CONTEXT.md を作成します。開発者の要望が PROJECT.md/REQUIREMENTS.md に十分に記載されているプロジェクトに適しています。v1.28 で追加 |
|
| `workflow.skip_discuss` | boolean | `false` | `true` の場合、`/gsd-autonomous` は discuss-phase を完全にスキップし、ROADMAP のフェーズ目標から最小限の CONTEXT.md を作成します。開発者の要望が PROJECT.md/REQUIREMENTS.md に十分に記載されているプロジェクトに適しています。v1.28 で追加 |
|
||||||
| `workflow.text_mode` | boolean | `false` | AskUserQuestion の TUI メニューをプレーンテキストの番号付きリストに置き換えます。TUI メニューが表示されない Claude Code リモートセッション(`/rc` モード)で必要です。discuss-phase で `--text` フラグを使用してセッションごとに設定することもできます。v1.28 で追加 |
|
| `workflow.text_mode` | boolean | `false` | AskUserQuestion の TUI メニューをプレーンテキストの番号付きリストに置き換えます。TUI メニューが表示されない Claude Code リモートセッション(`/rc` モード)で必要です。discuss-phase で `--text` フラグを使用してセッションごとに設定することもできます。v1.28 で追加 |
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | 標準プランナー、チャンク化されたアウトライン/プラン別プランナー、プランチェッカー、改訂プランナーの有界な停止検出を制御します。`gsd config-set planner.stall_detection_enabled false` を設定すると watchdog ポーリングを省略し、各エージェントをランタイムネイティブの完了機構で待機します。**警告:** `false` はランタイムが完了通知を失った場合の有界な復旧を放棄します。既存のファイルシステムフォールバックを使うために中断が必要になることがあります。 |
|
||||||
|
|
||||||
### 推奨プリセット
|
### 推奨プリセット
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ GSD는 프로젝트 설정을 `.planning/config.json`에 저장합니다. `/gsd-
|
|||||||
| `workflow.discuss_mode` | string | `'discuss'` | `/gsd-discuss-phase`의 컨텍스트 수집 방식을 제어합니다. `'discuss'` (기본값)는 질문을 하나씩 합니다. `'assumptions'`는 코드베이스를 먼저 읽고 신뢰도 수준이 있는 구조화된 가정을 생성하여 틀린 부분만 수정하도록 요청합니다. v1.28에서 추가 |
|
| `workflow.discuss_mode` | string | `'discuss'` | `/gsd-discuss-phase`의 컨텍스트 수집 방식을 제어합니다. `'discuss'` (기본값)는 질문을 하나씩 합니다. `'assumptions'`는 코드베이스를 먼저 읽고 신뢰도 수준이 있는 구조화된 가정을 생성하여 틀린 부분만 수정하도록 요청합니다. v1.28에서 추가 |
|
||||||
| `workflow.skip_discuss` | boolean | `false` | `true`로 설정하면 `/gsd-autonomous`가 discuss 단계를 완전히 건너뛰고 ROADMAP 단계 목표로부터 최소한의 CONTEXT.md를 작성합니다. 개발자 선호사항이 PROJECT.md/REQUIREMENTS.md에 모두 캡처된 프로젝트에 유용합니다. v1.28에서 추가 |
|
| `workflow.skip_discuss` | boolean | `false` | `true`로 설정하면 `/gsd-autonomous`가 discuss 단계를 완전히 건너뛰고 ROADMAP 단계 목표로부터 최소한의 CONTEXT.md를 작성합니다. 개발자 선호사항이 PROJECT.md/REQUIREMENTS.md에 모두 캡처된 프로젝트에 유용합니다. v1.28에서 추가 |
|
||||||
| `workflow.text_mode` | boolean | `false` | AskUserQuestion TUI 메뉴를 일반 텍스트 번호 목록으로 대체합니다. TUI 메뉴가 렌더링되지 않는 Claude Code 원격 세션 (`/rc` 모드)에 필요합니다. discuss 단계에서 `--text` 플래그로 세션별 설정도 가능합니다. v1.28에서 추가 |
|
| `workflow.text_mode` | boolean | `false` | AskUserQuestion TUI 메뉴를 일반 텍스트 번호 목록으로 대체합니다. TUI 메뉴가 렌더링되지 않는 Claude Code 원격 세션 (`/rc` 모드)에 필요합니다. discuss 단계에서 `--text` 플래그로 세션별 설정도 가능합니다. v1.28에서 추가 |
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | 표준 플래너, 청크형 개요/플랜별 플래너, 플랜 검사기, 수정 플래너의 제한된 정지 감지를 제어합니다. `gsd config-set planner.stall_detection_enabled false`로 설정하면 watchdog 폴링을 건너뛰고 각 에이전트를 런타임 네이티브 완료 메커니즘으로 기다립니다. **경고:** `false`는 런타임이 완료 전달을 잃을 때의 제한된 복구를 포기합니다. 기존 파일 시스템 폴백을 사용하려면 중단해야 할 수 있습니다. |
|
||||||
|
|
||||||
### 권장 프리셋
|
### 권장 프리셋
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ O GSD armazena as configurações do projeto em `.planning/config.json`. Criado
|
|||||||
"search_gitignored": false,
|
"search_gitignored": false,
|
||||||
"sub_repos": []
|
"sub_repos": []
|
||||||
},
|
},
|
||||||
|
"planner": {
|
||||||
|
"stall_detection_enabled": true
|
||||||
|
},
|
||||||
"context": null,
|
"context": null,
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"research": true,
|
"research": true,
|
||||||
@@ -267,6 +270,9 @@ Todos os controles de fluxo de trabalho seguem o padrão **ausente = habilitado*
|
|||||||
| `workflow.subagent_timeout` | number | `600` | Timeout em segundos para invocações individuais de subagente. Aumente para fases de pesquisa ou execução de longa duração |
|
| `workflow.subagent_timeout` | number | `600` | Timeout em segundos para invocações individuais de subagente. Aumente para fases de pesquisa ou execução de longa duração |
|
||||||
| `executor.stall_detect_interval_minutes` | number | `5` | Minutos entre verificações de travamento do executor enquanto um agente executor está ativo. O orquestrador de fase de execução usa essa cadência para inspecionar commits recentes e evitar espera eterna por um agente silencioso. |
|
| `executor.stall_detect_interval_minutes` | number | `5` | Minutos entre verificações de travamento do executor enquanto um agente executor está ativo. O orquestrador de fase de execução usa essa cadência para inspecionar commits recentes e evitar espera eterna por um agente silencioso. |
|
||||||
| `executor.stall_threshold_minutes` | number | `10` | Minutos sem conclusão do executor ou atividade de commit no branch esperado antes que a fase de execução ofereça opções de recuperação para um possível executor travado. |
|
| `executor.stall_threshold_minutes` | number | `10` | Minutos sem conclusão do executor ou atividade de commit no branch esperado antes que a fase de execução ofereça opções de recuperação para um possível executor travado. |
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | Controla a detecção limitada de travamento do planejador padrão, dos planejadores de esboço/por plano em chunks, do verificador de planos e do planejador de revisão. Use `gsd config-set planner.stall_detection_enabled false` para ignorar o polling do watchdog e aguardar cada agente pelo mecanismo de conclusão nativa do runtime. **Aviso:** `false` abre mão da recuperação limitada se o runtime perder a entrega da conclusão; pode ser necessário interromper e usar o fallback existente do sistema de arquivos. A execução e o tratamento do resultado nunca são ignorados. |
|
||||||
|
| `planner.stall_detect_interval_minutes` | number | `5` | Minutos entre verificações de travamento enquanto um planejador ou verificador de planos está ativo. O orquestrador inspeciona a atividade em disco de `*-PLAN.md` nessa cadência (#2650). |
|
||||||
|
| `planner.stall_threshold_minutes` | number | `10` | Minutos sem marcador de conclusão ou atividade recente em planos antes que a fase de planejamento ofereça automaticamente as opções aceitar/tentar novamente/parar (#2650). |
|
||||||
| `workflow.inline_plan_threshold` | number | `3` | Número máximo de tasks em uma fase antes que o planejador gere um arquivo PLAN.md separado em vez de incorporar tasks no prompt |
|
| `workflow.inline_plan_threshold` | number | `3` | Número máximo de tasks em uma fase antes que o planejador gere um arquivo PLAN.md separado em vez de incorporar tasks no prompt |
|
||||||
| `workflow.drift_threshold` | number | `3` | Número mínimo de novos elementos estruturais (novos diretórios, exportações barrel, migrações, módulos de rota) introduzidos durante uma fase antes que o gate de deriva pós-execução da base de código tome ação. Consulte [#2003](https://github.com/open-gsd/gsd-core/issues/2003). Adicionado na v1.39 |
|
| `workflow.drift_threshold` | number | `3` | Número mínimo de novos elementos estruturais (novos diretórios, exportações barrel, migrações, módulos de rota) introduzidos durante uma fase antes que o gate de deriva pós-execução da base de código tome ação. Consulte [#2003](https://github.com/open-gsd/gsd-core/issues/2003). Adicionado na v1.39 |
|
||||||
| `workflow.drift_action` | string | `warn` | O que fazer quando `workflow.drift_threshold` é excedido após `/gsd-execute-phase`. `warn` imprime uma mensagem sugerindo `/gsd-map-codebase --paths …`; `auto-remap` gera `gsd-codebase-mapper` com escopo para os caminhos afetados. Adicionado na v1.39 |
|
| `workflow.drift_action` | string | `warn` | O que fazer quando `workflow.drift_threshold` é excedido após `/gsd-execute-phase`. `warn` imprime uma mensagem sugerindo `/gsd-map-codebase --paths …`; `auto-remap` gera `gsd-codebase-mapper` com escopo para os caminhos afetados. Adicionado na v1.39 |
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ GSD 将项目设置存储在 `.planning/config.json` 中。该文件在 `/gsd-ne
|
|||||||
"search_gitignored": false,
|
"search_gitignored": false,
|
||||||
"sub_repos": []
|
"sub_repos": []
|
||||||
},
|
},
|
||||||
|
"planner": {
|
||||||
|
"stall_detection_enabled": true
|
||||||
|
},
|
||||||
"context": null,
|
"context": null,
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"research": true,
|
"research": true,
|
||||||
@@ -267,6 +270,9 @@ API 密钥字段接受字符串值(密钥本身)。也可以设置为哨兵
|
|||||||
| `workflow.subagent_timeout` | number | `600` | 单个 subagent 调用的超时秒数。对于长时间运行的研究或执行阶段可适当增加 |
|
| `workflow.subagent_timeout` | number | `600` | 单个 subagent 调用的超时秒数。对于长时间运行的研究或执行阶段可适当增加 |
|
||||||
| `executor.stall_detect_interval_minutes` | number | `5` | 执行器 agent 活跃时,执行器停滞检测的间隔分钟数。执行阶段编排器以此频率检查最近的提交,避免无限等待静默的 agent。 |
|
| `executor.stall_detect_interval_minutes` | number | `5` | 执行器 agent 活跃时,执行器停滞检测的间隔分钟数。执行阶段编排器以此频率检查最近的提交,避免无限等待静默的 agent。 |
|
||||||
| `executor.stall_threshold_minutes` | number | `10` | 执行器完成或预期分支提交活动缺失超过此分钟数后,执行阶段为可能停滞的执行器提供恢复选项。 |
|
| `executor.stall_threshold_minutes` | number | `10` | 执行器完成或预期分支提交活动缺失超过此分钟数后,执行阶段为可能停滞的执行器提供恢复选项。 |
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | 控制标准规划器、分块大纲/逐计划规划器、计划检查器和修订规划器的有界停滞检测。运行 `gsd config-set planner.stall_detection_enabled false` 可跳过 watchdog 轮询,改用运行时原生完成机制等待每个 agent。**警告:** `false` 会放弃运行时丢失完成回传时的有界恢复;届时可能需要中断并使用现有文件系统回退。规划器执行和结果处理不会被跳过。 |
|
||||||
|
| `planner.stall_detect_interval_minutes` | number | `5` | 规划器或计划检查器活跃时的停滞检查间隔分钟数。规划阶段编排器按此频率检查磁盘上的 `*-PLAN.md` 活动(#2650)。 |
|
||||||
|
| `planner.stall_threshold_minutes` | number | `10` | 没有完成标记或新的磁盘计划活动超过此分钟数后,规划阶段自动提供接受计划/重试/停止恢复选项(#2650)。 |
|
||||||
| `workflow.inline_plan_threshold` | number | `3` | 阶段中任务数量的最大值,超过此值后规划器生成单独的 PLAN.md 文件而非在提示词中内联任务 |
|
| `workflow.inline_plan_threshold` | number | `3` | 阶段中任务数量的最大值,超过此值后规划器生成单独的 PLAN.md 文件而非在提示词中内联任务 |
|
||||||
| `workflow.drift_threshold` | number | `3` | 阶段期间引入的新结构元素(新目录、桶形导出、迁移、路由模块)的最小数量,超过此值后执行后代码库漂移门禁采取行动。参见 [#2003](https://github.com/open-gsd/gsd-core/issues/2003)。v1.39 新增 |
|
| `workflow.drift_threshold` | number | `3` | 阶段期间引入的新结构元素(新目录、桶形导出、迁移、路由模块)的最小数量,超过此值后执行后代码库漂移门禁采取行动。参见 [#2003](https://github.com/open-gsd/gsd-core/issues/2003)。v1.39 新增 |
|
||||||
| `workflow.drift_action` | string | `warn` | `/gsd-execute-phase` 后超过 `workflow.drift_threshold` 时的处理方式。`warn` 打印建议运行 `/gsd-map-codebase --paths …` 的消息;`auto-remap` 派生 `gsd-codebase-mapper` 限定于受影响路径。v1.39 新增 |
|
| `workflow.drift_action` | string | `warn` | `/gsd-execute-phase` 后超过 `workflow.drift_threshold` 时的处理方式。`warn` 打印建议运行 `/gsd-map-codebase --paths …` 的消息;`auto-remap` 派生 `gsd-codebase-mapper` 限定于受影响路径。v1.39 新增 |
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | 里程碑策略的分支模板 |
|
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | 里程碑策略的分支模板 |
|
||||||
</config_schema>
|
</config_schema>
|
||||||
|
|
||||||
|
<planner_stall_detection>
|
||||||
|
|
||||||
|
`planner.stall_detection_enabled` 默认为 `true`,控制标准规划器、分块规划器、计划检查器和修订规划器的有界停滞检测。运行 `gsd config-set planner.stall_detection_enabled false` 可跳过 watchdog 轮询,并通过运行时原生完成机制等待每个 agent。**警告:** `false` 会放弃运行时丢失完成回传时的有界恢复;可能需要中断并使用现有文件系统回退。
|
||||||
|
|
||||||
|
</planner_stall_detection>
|
||||||
|
|
||||||
<commit_docs_behavior>
|
<commit_docs_behavior>
|
||||||
|
|
||||||
**当 `commit_docs: true`(默认):**
|
**当 `commit_docs: true`(默认):**
|
||||||
@@ -197,4 +203,4 @@ fi
|
|||||||
|
|
||||||
</branching_strategy_behavior>
|
</branching_strategy_behavior>
|
||||||
|
|
||||||
</planning_config>
|
</planning_config>
|
||||||
|
|||||||
@@ -68,6 +68,9 @@
|
|||||||
"sub_repos": [],
|
"sub_repos": [],
|
||||||
"granularity": "standard"
|
"granularity": "standard"
|
||||||
},
|
},
|
||||||
|
"planner": {
|
||||||
|
"stall_detection_enabled": true
|
||||||
|
},
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"context_warnings": true,
|
"context_warnings": true,
|
||||||
"workflow_guard": false
|
"workflow_guard": false
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"workflow.context_guard_mode",
|
"workflow.context_guard_mode",
|
||||||
"executor.stall_detect_interval_minutes",
|
"executor.stall_detect_interval_minutes",
|
||||||
"executor.stall_threshold_minutes",
|
"executor.stall_threshold_minutes",
|
||||||
|
"planner.stall_detection_enabled",
|
||||||
"planner.stall_detect_interval_minutes",
|
"planner.stall_detect_interval_minutes",
|
||||||
"planner.stall_threshold_minutes",
|
"planner.stall_threshold_minutes",
|
||||||
"workflow.inline_plan_threshold",
|
"workflow.inline_plan_threshold",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Configuration options for `.planning/` directory behavior.
|
|||||||
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
|
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
|
||||||
| `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. Note: if your branch is ahead of `origin/HEAD` (a diverged milestone or feature branch), GSD auto-degrades to sequential and prints a warning; set `worktree.baseRef:"head"` in `.claude/settings.local.json` to restore parallel execution. See the branch-divergence note below. |
|
| `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. Note: if your branch is ahead of `origin/HEAD` (a diverged milestone or feature branch), GSD auto-degrades to sequential and prints a warning; set `worktree.baseRef:"head"` in `.claude/settings.local.json` to restore parallel execution. See the branch-divergence note below. |
|
||||||
| `workflow.subagent_timeout` | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). |
|
| `workflow.subagent_timeout` | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). |
|
||||||
|
| `planner.stall_detection_enabled` | `true` | Default-on bounded stall detection for planner and plan-checker agents. Set to `false` to await runtime-native completion without watchdog polling; this gives up bounded automatic recovery if the completion handoff is lost. _Loader projection:_ `planner_stall_detection_enabled`. |
|
||||||
| `workflow.inline_plan_threshold` | `2` | Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to `0` to always spawn subagents. |
|
| `workflow.inline_plan_threshold` | `2` | Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to `0` to always spawn subagents. |
|
||||||
| `workflow.test_command` | `null` | Custom shell command run as the regression/test gate by execute-phase, audit-fix, and post-merge-gate. When unset, GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). Example: `npm test`. |
|
| `workflow.test_command` | `null` | Custom shell command run as the regression/test gate by execute-phase, audit-fix, and post-merge-gate. When unset, GSD auto-detects (Makefile / package.json / Cargo.toml / go.mod / pyproject.toml). Example: `npm test`. |
|
||||||
| `workflow.build_command` | `null` | Custom shell command run as the build gate by the post-merge gate. When unset, the build step is skipped/auto-detected. Example: `npm run build`. |
|
| `workflow.build_command` | `null` | Custom shell command run as the build gate by the post-merge gate. When unset, the build step is skipped/auto-detected. Example: `npm run build`. |
|
||||||
@@ -313,6 +314,14 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
|||||||
| `workflow.security_block_on` | string | `"high"` | `"critical"`, `"high"`, `"medium"`, `"low"`, `"none"` | Minimum threat severity that blocks phase advancement. The auditor counts only open threats at or above this severity toward the blocking gate (SECURITY.md `threats_open`); `none` disables severity blocking. |
|
| `workflow.security_block_on` | string | `"high"` | `"critical"`, `"high"`, `"medium"`, `"low"`, `"none"` | Minimum threat severity that blocks phase advancement. The auditor counts only open threats at or above this severity toward the blocking gate (SECURITY.md `threats_open`); `none` disables severity blocking. |
|
||||||
| `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
|
| `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
|
||||||
|
|
||||||
|
### Planner Fields
|
||||||
|
|
||||||
|
Set via the `planner.*` namespace. These settings affect only planner/plan-checker waits; executor stall controls are independent.
|
||||||
|
|
||||||
|
| Key | Type | Default | Allowed Values | Description |
|
||||||
|
|-----|------|---------|----------------|-------------|
|
||||||
|
| `planner.stall_detection_enabled` | boolean | `true` | `true`, `false` | Default-on bounded stall detection for the standard planner, chunked outline/per-plan planners, plan-checker, and revision planner. `false` skips `gsd_stall_watch` polling but still awaits and consumes the real runtime-native agent result. It gives up bounded automatic recovery if the runtime loses the completion handoff. _Loader projection:_ `planner_stall_detection_enabled`. |
|
||||||
|
|
||||||
### Ship Fields
|
### Ship Fields
|
||||||
|
|
||||||
Set via `ship.*` namespace in config.json. These fields affect `/gsd:ship` PRD-style pull request body composition only.
|
Set via `ship.*` namespace in config.json. These fields affect `/gsd:ship` PRD-style pull request body composition only.
|
||||||
|
|||||||
@@ -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:
|
**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
|
```text
|
||||||
Agent(
|
Agent(
|
||||||
prompt=filled_prompt,
|
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.
|
**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
|
## 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
|
**If the planner returns `## REVISION_CONFLICT`:** follow the shared Conflict Return protocol in
|
||||||
`gsd-core/references/revision-loop.md`, with this workflow's bindings:
|
`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
|
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
|
||||||
PLAN.md files:
|
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
|
```javascript
|
||||||
Agent(
|
Agent(
|
||||||
prompt="{same planning_context as step 8, plus:}
|
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:
|
Handle return:
|
||||||
- **`marker_received`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
|
- **`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
|
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
|
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
|
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
|
```javascript
|
||||||
Agent(
|
Agent(
|
||||||
prompt="{same planning_context as step 8, plus:}
|
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
|
**Concurrent dispatch:** issue every runnable entry's Agent() call together, in this one
|
||||||
message, before waiting on any of them.
|
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")`
|
`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).
|
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
|
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
|
Retry/Stop recovery for that one plan; it does not block verifying/committing sibling entries
|
||||||
in the same batch that already reached `marker_received`.
|
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
|
6. **Verify disk, per entry:** check `${PHASE_DIR}/{plan_id}-PLAN.md` exists for each entry that
|
||||||
reached `marker_received`. Unchanged per-plan check.
|
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
|
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`
|
stopped. After every Wave's plans are written and committed, treat this as `## PLANNING COMPLETE`
|
||||||
and continue to step 9.
|
and continue to step 9.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Bounded Stall-Detection Helpers (#2650)
|
# Bounded Stall-Detection Helpers (#2650)
|
||||||
|
|
||||||
Every planner/plan-checker spawn in `plan-phase.md` dispatches with
|
`planner.stall_detection_enabled` controls the wait policy for the five scoped
|
||||||
`run_in_background=true`, records `TS=$(date +%s)`, and then repeatedly
|
planner/plan-checker spawns. It defaults to `true`. In that default-on mode each
|
||||||
calls `gsd_stall_watch` until it returns something other than
|
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_*`
|
`waiting`/`active`. This mirrors the already-shipped `executor.stall_*`
|
||||||
pattern (`execute-phase.md`, bug #3212, commit `e7942c21b`) but — unlike
|
pattern (`execute-phase.md`, bug #3212, commit `e7942c21b`) but — unlike
|
||||||
that prose-only surveillance, which cannot run during a *blocking* `Agent()`
|
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
|
its own schedule regardless of whether the backgrounded agent's own
|
||||||
completion notification ever arrives.
|
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`
|
**Binding `{outputFile}` (load-bearing, not optional):** every `gsd_stall_watch`
|
||||||
call below takes `{outputFile}` as its second argument — a literal token the
|
call below takes `{outputFile}` as its second argument — a literal token the
|
||||||
orchestrator must substitute with the REAL path from the immediately preceding
|
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
|
This block is independent of, and never gated behind, the `query
|
||||||
teams-status` / `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` guard used for the
|
teams-status` / `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` guard used for the
|
||||||
researcher spawn — the stall path applies on every runtime, teams-active or
|
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
|
```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
|
_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_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")
|
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
|
# 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.
|
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>
|
<purpose>
|
||||||
Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts,
|
Interactive configuration of GSD power-user knobs — plan bounce, planner stall detection, node
|
||||||
inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
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
|
context window, gitignored search, graphify build timeout, runtime model tier overrides, and
|
||||||
model policy configuration (provider + budget → canonical tier mapping, or manual model ID
|
model policy configuration (provider + budget → canonical tier mapping, or manual model ID
|
||||||
assignment per cost tier).
|
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:
|
shown in parentheses:
|
||||||
|
|
||||||
Planning Tuning:
|
Planning Tuning:
|
||||||
|
- `planner.stall_detection_enabled` (default: `true`)
|
||||||
- `workflow.plan_bounce` (default: `false`)
|
- `workflow.plan_bounce` (default: `false`)
|
||||||
- `workflow.plan_bounce_passes` (default: `2`)
|
- `workflow.plan_bounce_passes` (default: `2`)
|
||||||
- `workflow.plan_bounce_script` (default: `null`)
|
- `workflow.plan_bounce_script` (default: `null`)
|
||||||
@@ -126,8 +127,21 @@ stored verbatim as a string.
|
|||||||
|
|
||||||
### Section 1 — Planning Tuning
|
### 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
|
```text
|
||||||
AskUserQuestion([
|
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>)",
|
question: "Run external plan-bounce validator against generated PLAN.md? (current: <value or false>)",
|
||||||
header: "Plan Bounce",
|
header: "Plan Bounce",
|
||||||
@@ -500,6 +514,7 @@ keys and sibling sub-objects.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example — only write keys the user changed. "Keep current" selections are skipped.
|
# 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.plan_bounce_passes 5
|
||||||
gsd_run query config-set workflow.subagent_timeout 300000
|
gsd_run query config-set workflow.subagent_timeout 300000
|
||||||
gsd_run query config-set git.base_branch main
|
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
|
```json
|
||||||
{
|
{
|
||||||
...existing_config,
|
...existing_config,
|
||||||
|
"planner": {
|
||||||
|
...existing_planner,
|
||||||
|
"stall_detection_enabled": <new|existing>
|
||||||
|
},
|
||||||
"workflow": {
|
"workflow": {
|
||||||
...existing_workflow,
|
...existing_workflow,
|
||||||
"plan_bounce": <new|existing>,
|
"plan_bounce": <new|existing>,
|
||||||
@@ -752,6 +771,7 @@ Display:
|
|||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|--------------------------------------------|-------|
|
|--------------------------------------------|-------|
|
||||||
|
| planner.stall_detection_enabled | {true/false} |
|
||||||
| workflow.plan_bounce | {on/off} |
|
| workflow.plan_bounce | {on/off} |
|
||||||
| workflow.plan_bounce_passes | {n} |
|
| workflow.plan_bounce_passes | {n} |
|
||||||
| workflow.plan_bounce_script | {path/null} |
|
| 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`
|
- [ ] Current config read from resolved `$GSD_CONFIG_PATH`
|
||||||
- [ ] Eight sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime/Output, Runtime Model Tiers, Model Policy)
|
- [ ] 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)
|
- [ ] 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
|
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
|
||||||
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
||||||
- [ ] Null-allowed fields accept an empty input as a clear
|
- [ ] Null-allowed fields accept an empty input as a clear
|
||||||
|
|||||||
@@ -162,9 +162,22 @@ const CONFIG_DEFAULTS = {
|
|||||||
research_before_questions: _getNestedConfigDefault('workflow', 'research_before_questions'), // #3894
|
research_before_questions: _getNestedConfigDefault('workflow', 'research_before_questions'), // #3894
|
||||||
smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
|
smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
|
||||||
inline_plan_threshold: _getNestedConfigDefault('workflow', 'inline_plan_threshold'), // #3801
|
inline_plan_threshold: _getNestedConfigDefault('workflow', 'inline_plan_threshold'), // #3801
|
||||||
|
planner_stall_detection_enabled: _getNestedConfigDefault('planner', 'stall_detection_enabled'),
|
||||||
max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
|
max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the planner watchdog policy from hand-edited configuration.
|
||||||
|
* Only a real JSON boolean may override the default; strings/numbers/null
|
||||||
|
* fail safe to the manifest-owned default-on behavior (#4570).
|
||||||
|
*/
|
||||||
|
function resolvePlannerStallDetectionEnabled(value: unknown): boolean {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
// A missing or skewed manifest must never convert an absent override into
|
||||||
|
// permission to disable the watchdog. The documented contract is default-on.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep-merge two plain config objects. `overlay` wins on key conflict.
|
* Deep-merge two plain config objects. `overlay` wins on key conflict.
|
||||||
* Explicit `null` in overlay overrides base (null means "unset this key").
|
* Explicit `null` in overlay overrides base (null means "unset this key").
|
||||||
@@ -932,6 +945,9 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
|
|||||||
phase_naming: get('phase_naming') ?? defaults.phase_naming,
|
phase_naming: get('phase_naming') ?? defaults.phase_naming,
|
||||||
project_code: get('project_code') ?? defaults.project_code,
|
project_code: get('project_code') ?? defaults.project_code,
|
||||||
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
||||||
|
planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(
|
||||||
|
getNested('planner', 'stall_detection_enabled'),
|
||||||
|
),
|
||||||
model_overrides: (parsed['model_overrides']) || null,
|
model_overrides: (parsed['model_overrides']) || null,
|
||||||
agent_tools: (parsed['agent_tools']) || null,
|
agent_tools: (parsed['agent_tools']) || null,
|
||||||
models: (parsed['models']) || null,
|
models: (parsed['models']) || null,
|
||||||
@@ -1091,6 +1107,9 @@ function loadConfigResolvedInternal(cwd: string, options: Record<string, unknown
|
|||||||
resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
|
resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
|
||||||
context_window: (globalDefaults['context_window']) ?? defaults.context_window,
|
context_window: (globalDefaults['context_window']) ?? defaults.context_window,
|
||||||
subagent_timeout: (globalDefaults['subagent_timeout']) ?? defaults.subagent_timeout,
|
subagent_timeout: (globalDefaults['subagent_timeout']) ?? defaults.subagent_timeout,
|
||||||
|
planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(
|
||||||
|
(globalDefaults['planner'] as Record<string, unknown> | undefined)?.['stall_detection_enabled'],
|
||||||
|
),
|
||||||
model_overrides: (globalDefaults['model_overrides']) || null,
|
model_overrides: (globalDefaults['model_overrides']) || null,
|
||||||
models: (globalDefaults['models']) || null,
|
models: (globalDefaults['models']) || null,
|
||||||
granularity: (globalDefaults['granularity']) !== undefined ? globalDefaults['granularity'] : null,
|
granularity: (globalDefaults['granularity']) !== undefined ? globalDefaults['granularity'] : null,
|
||||||
@@ -1191,6 +1210,7 @@ export = {
|
|||||||
_getNestedConfigDefault,
|
_getNestedConfigDefault,
|
||||||
_getConfigValue,
|
_getConfigValue,
|
||||||
_getConfigNested,
|
_getConfigNested,
|
||||||
|
resolvePlannerStallDetectionEnabled,
|
||||||
_deepMergeConfig,
|
_deepMergeConfig,
|
||||||
_warnedUnknownConfigKeys,
|
_warnedUnknownConfigKeys,
|
||||||
_warnedShadowedGlobalKeys,
|
_warnedShadowedGlobalKeys,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import cliExitMod = require('./cli-exit.cjs');
|
|||||||
const { ExitError } = cliExitMod;
|
const { ExitError } = cliExitMod;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
import configLoader = require('./config-loader.cjs');
|
import configLoader = require('./config-loader.cjs');
|
||||||
const { CONFIG_DEFAULTS } = configLoader;
|
const { CONFIG_DEFAULTS, resolvePlannerStallDetectionEnabled } = configLoader;
|
||||||
import { platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
|
import { platformWriteSync, platformEnsureDir } from './shell-command-projection.cjs';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
import planningWorkspace = require('./planning-workspace.cjs');
|
import planningWorkspace = require('./planning-workspace.cjs');
|
||||||
@@ -107,6 +107,7 @@ const SCHEMA_DEFAULTS: Record<string, unknown> = {
|
|||||||
'context_window': 200000,
|
'context_window': 200000,
|
||||||
'executor.stall_detect_interval_minutes': 5,
|
'executor.stall_detect_interval_minutes': 5,
|
||||||
'executor.stall_threshold_minutes': 10,
|
'executor.stall_threshold_minutes': 10,
|
||||||
|
'planner.stall_detection_enabled': CONFIG_DEFAULTS.planner_stall_detection_enabled,
|
||||||
'planner.stall_detect_interval_minutes': 5,
|
'planner.stall_detect_interval_minutes': 5,
|
||||||
'planner.stall_threshold_minutes': 10,
|
'planner.stall_threshold_minutes': 10,
|
||||||
'git.create_tag': true,
|
'git.create_tag': true,
|
||||||
@@ -185,12 +186,15 @@ function resolveSchemaDefault(cwd: string, kp: string): { found: boolean; value:
|
|||||||
* Centralizing emission here means masking can't be missed at a call site.
|
* Centralizing emission here means masking can't be missed at a call site.
|
||||||
*/
|
*/
|
||||||
function emitResolvedDefault(kp: string, value: unknown, raw: boolean): void {
|
function emitResolvedDefault(kp: string, value: unknown, raw: boolean): void {
|
||||||
|
const resolvedValue = kp === 'planner.stall_detection_enabled'
|
||||||
|
? resolvePlannerStallDetectionEnabled(value)
|
||||||
|
: value;
|
||||||
if (isSecretKey(kp)) {
|
if (isSecretKey(kp)) {
|
||||||
const masked = maskSecret(value as Parameters<typeof maskSecret>[0]);
|
const masked = maskSecret(resolvedValue as Parameters<typeof maskSecret>[0]);
|
||||||
output(masked, raw, masked);
|
output(masked, raw, masked);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
output(value, raw, String(value));
|
output(resolvedValue, raw, String(resolvedValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Validation helpers ───────────────────────────────────────────────────────
|
// ─── Validation helpers ───────────────────────────────────────────────────────
|
||||||
@@ -955,6 +959,14 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Planner watchdog opt-out (#4570) — only a real boolean may change the
|
||||||
|
// default-on policy. In particular, string "false" must not disable it.
|
||||||
|
if (kp === 'planner.stall_detection_enabled') {
|
||||||
|
if (typeof parsedValue !== 'boolean') {
|
||||||
|
error(`Invalid planner.stall_detection_enabled '${val}'. Must be a boolean (true or false).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// #3086 — git.create_tag: boolean only
|
// #3086 — git.create_tag: boolean only
|
||||||
if (kp === 'git.create_tag') {
|
if (kp === 'git.create_tag') {
|
||||||
if (typeof parsedValue !== 'boolean') {
|
if (typeof parsedValue !== 'boolean') {
|
||||||
@@ -1251,6 +1263,10 @@ function cmdConfigGet(cwd: string, keyPath: string | undefined, raw: boolean, de
|
|||||||
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kp === 'planner.stall_detection_enabled') {
|
||||||
|
current = resolvePlannerStallDetectionEnabled(current);
|
||||||
|
}
|
||||||
|
|
||||||
// Never echo plaintext for sensitive keys via config-get. Plaintext lives
|
// Never echo plaintext for sensitive keys via config-get. Plaintext lives
|
||||||
// in config.json on disk; the CLI surface always shows the masked form.
|
// in config.json on disk; the CLI surface always shows the masked form.
|
||||||
if (isSecretKey(kp)) {
|
if (isSecretKey(kp)) {
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
"reductionPct": 13.53
|
"reductionPct": 13.53
|
||||||
},
|
},
|
||||||
"plan-phase": {
|
"plan-phase": {
|
||||||
"offTokens": 28000,
|
"offTokens": 28406,
|
||||||
"onTokens": 24649,
|
"onTokens": 25055,
|
||||||
"reductionPct": 11.97
|
"reductionPct": 11.8
|
||||||
},
|
},
|
||||||
"verify-work": {
|
"verify-work": {
|
||||||
"offTokens": 13445,
|
"offTokens": 13445,
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"aggregate": {
|
||||||
"offTokens": 108858,
|
"offTokens": 109264,
|
||||||
"onTokens": 91988,
|
"onTokens": 92394,
|
||||||
"reductionPct": 15.5
|
"reductionPct": 15.44
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
"reductionPct": 20.79
|
"reductionPct": 20.79
|
||||||
},
|
},
|
||||||
"agents/gsd-code-fixer.md": {
|
"agents/gsd-code-fixer.md": {
|
||||||
"offTokens": 10740,
|
"offTokens": 10835,
|
||||||
"onTokens": 6690,
|
"onTokens": 6781,
|
||||||
"reductionPct": 37.71
|
"reductionPct": 37.42
|
||||||
},
|
},
|
||||||
"agents/gsd-code-reviewer.md": {
|
"agents/gsd-code-reviewer.md": {
|
||||||
"offTokens": 4408,
|
"offTokens": 4408,
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
"reductionPct": 9.76
|
"reductionPct": 9.76
|
||||||
},
|
},
|
||||||
"agents/gsd-debug-session-manager.md": {
|
"agents/gsd-debug-session-manager.md": {
|
||||||
"offTokens": 5027,
|
"offTokens": 5088,
|
||||||
"onTokens": 4738,
|
"onTokens": 4799,
|
||||||
"reductionPct": 5.75
|
"reductionPct": 5.68
|
||||||
},
|
},
|
||||||
"agents/gsd-doc-classifier.md": {
|
"agents/gsd-doc-classifier.md": {
|
||||||
"offTokens": 2901,
|
"offTokens": 2901,
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
"reductionPct": 17.83
|
"reductionPct": 17.83
|
||||||
},
|
},
|
||||||
"agents/gsd-eval-auditor.md": {
|
"agents/gsd-eval-auditor.md": {
|
||||||
"offTokens": 2941,
|
"offTokens": 3002,
|
||||||
"onTokens": 2808,
|
"onTokens": 2869,
|
||||||
"reductionPct": 4.52
|
"reductionPct": 4.43
|
||||||
},
|
},
|
||||||
"agents/gsd-eval-planner.md": {
|
"agents/gsd-eval-planner.md": {
|
||||||
"offTokens": 1674,
|
"offTokens": 1674,
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
"reductionPct": 42.1
|
"reductionPct": 42.1
|
||||||
},
|
},
|
||||||
"agents/gsd-intel-updater.md": {
|
"agents/gsd-intel-updater.md": {
|
||||||
"offTokens": 4353,
|
"offTokens": 4414,
|
||||||
"onTokens": 4115,
|
"onTokens": 4176,
|
||||||
"reductionPct": 5.47
|
"reductionPct": 5.39
|
||||||
},
|
},
|
||||||
"agents/gsd-mempalace-curator.md": {
|
"agents/gsd-mempalace-curator.md": {
|
||||||
"offTokens": 1160,
|
"offTokens": 1160,
|
||||||
@@ -113,14 +113,14 @@
|
|||||||
"reductionPct": 15.37
|
"reductionPct": 15.37
|
||||||
},
|
},
|
||||||
"agents/gsd-project-researcher.md": {
|
"agents/gsd-project-researcher.md": {
|
||||||
"offTokens": 5464,
|
"offTokens": 5525,
|
||||||
"onTokens": 5135,
|
"onTokens": 5196,
|
||||||
"reductionPct": 6.02
|
"reductionPct": 5.95
|
||||||
},
|
},
|
||||||
"agents/gsd-research-synthesizer.md": {
|
"agents/gsd-research-synthesizer.md": {
|
||||||
"offTokens": 3122,
|
"offTokens": 3183,
|
||||||
"onTokens": 2917,
|
"onTokens": 2978,
|
||||||
"reductionPct": 6.57
|
"reductionPct": 6.44
|
||||||
},
|
},
|
||||||
"agents/gsd-roadmapper.md": {
|
"agents/gsd-roadmapper.md": {
|
||||||
"offTokens": 6008,
|
"offTokens": 6008,
|
||||||
@@ -133,9 +133,9 @@
|
|||||||
"reductionPct": 10.54
|
"reductionPct": 10.54
|
||||||
},
|
},
|
||||||
"agents/gsd-ui-auditor.md": {
|
"agents/gsd-ui-auditor.md": {
|
||||||
"offTokens": 4145,
|
"offTokens": 6729,
|
||||||
"onTokens": 3926,
|
"onTokens": 3926,
|
||||||
"reductionPct": 5.28
|
"reductionPct": 41.66
|
||||||
},
|
},
|
||||||
"agents/gsd-ui-checker.md": {
|
"agents/gsd-ui-checker.md": {
|
||||||
"offTokens": 4499,
|
"offTokens": 4499,
|
||||||
@@ -143,9 +143,9 @@
|
|||||||
"reductionPct": 32.25
|
"reductionPct": 32.25
|
||||||
},
|
},
|
||||||
"agents/gsd-ui-researcher.md": {
|
"agents/gsd-ui-researcher.md": {
|
||||||
"offTokens": 5432,
|
"offTokens": 5493,
|
||||||
"onTokens": 4815,
|
"onTokens": 4876,
|
||||||
"reductionPct": 11.36
|
"reductionPct": 11.23
|
||||||
},
|
},
|
||||||
"agents/gsd-user-profiler.md": {
|
"agents/gsd-user-profiler.md": {
|
||||||
"offTokens": 1859,
|
"offTokens": 1859,
|
||||||
@@ -163,14 +163,14 @@
|
|||||||
"reductionPct": 33.87
|
"reductionPct": 33.87
|
||||||
},
|
},
|
||||||
"gsd-core/workflows/help/modes/full.md": {
|
"gsd-core/workflows/help/modes/full.md": {
|
||||||
"offTokens": 9950,
|
"offTokens": 9933,
|
||||||
"onTokens": 6579,
|
"onTokens": 6573,
|
||||||
"reductionPct": 33.88
|
"reductionPct": 33.83
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"aggregate": {
|
"aggregate": {
|
||||||
"offTokens": 118958,
|
"offTokens": 121986,
|
||||||
"onTokens": 94602,
|
"onTokens": 95053,
|
||||||
"reductionPct": 20.47
|
"reductionPct": 22.08
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,14 +47,21 @@ const fs = require('node:fs');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fc = require('fast-check');
|
const fc = require('fast-check');
|
||||||
const { cleanup, readFileNormalized, readWorkflowCombined } = require('./helpers.cjs');
|
const { cleanup, readFileNormalized, readWorkflowCombined, runGsdTools } = require('./helpers.cjs');
|
||||||
|
|
||||||
const REPO_ROOT = path.join(__dirname, '..');
|
const REPO_ROOT = path.join(__dirname, '..');
|
||||||
const PLAN_PHASE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase.md');
|
const PLAN_PHASE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase.md');
|
||||||
const STALL_HELPERS_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase', 'steps', 'stall-detection-helpers.md');
|
const STALL_HELPERS_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase', 'steps', 'stall-detection-helpers.md');
|
||||||
const CHUNKED_PLANNING_MODE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase', 'steps', 'chunked-planning-mode.md');
|
const CHUNKED_PLANNING_MODE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-phase', 'steps', 'chunked-planning-mode.md');
|
||||||
const CONFIG_SCHEMA_MANIFEST_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'shared', 'config-schema.manifest.json');
|
const CONFIG_SCHEMA_MANIFEST_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'shared', 'config-schema.manifest.json');
|
||||||
|
const CONFIG_DEFAULTS_MANIFEST_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'shared', 'config-defaults.manifest.json');
|
||||||
const CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'CONFIGURATION.md');
|
const CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'CONFIGURATION.md');
|
||||||
|
const PT_BR_CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'pt-BR', 'CONFIGURATION.md');
|
||||||
|
const ZH_CN_CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'zh-CN', 'CONFIGURATION.md');
|
||||||
|
const JA_JP_CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'ja-JP', 'CONFIGURATION.md');
|
||||||
|
const KO_KR_CONFIGURATION_DOCS_PATH = path.join(REPO_ROOT, 'docs', 'ko-KR', 'CONFIGURATION.md');
|
||||||
|
const ZH_CN_PLANNING_CONFIG_PATH = path.join(REPO_ROOT, 'docs', 'zh-CN', 'references', 'planning-config.md');
|
||||||
|
const SETTINGS_ADVANCED_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'settings-advanced.md');
|
||||||
|
|
||||||
function readPlanPhase() {
|
function readPlanPhase() {
|
||||||
return readFileNormalized(PLAN_PHASE_PATH);
|
return readFileNormalized(PLAN_PHASE_PATH);
|
||||||
@@ -461,6 +468,148 @@ describe('bug #2650 config schema — planner.stall_* keys mirror executor.stall
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('enhancement #4570 config contract — planner stall detection has a typed default-on opt-out', () => {
|
||||||
|
function makeProject(t, config = {}) {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4570-config-'));
|
||||||
|
t.after(() => cleanup(tmp));
|
||||||
|
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(tmp, '.planning', 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('central schema and canonical defaults manifest register planner.stall_detection_enabled=true', () => {
|
||||||
|
const schema = JSON.parse(fs.readFileSync(CONFIG_SCHEMA_MANIFEST_PATH, 'utf8'));
|
||||||
|
const defaults = JSON.parse(fs.readFileSync(CONFIG_DEFAULTS_MANIFEST_PATH, 'utf8'));
|
||||||
|
assert.ok(schema.validKeys.includes('planner.stall_detection_enabled'));
|
||||||
|
assert.equal(defaults.planner?.stall_detection_enabled, true);
|
||||||
|
assert.equal(schema.validKeys.includes('executor.stall_detection_enabled'), false,
|
||||||
|
'the planner opt-out must not introduce an executor sibling outside approved scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config-get defaults absent values to true; config-set false round-trips as boolean false', (t) => {
|
||||||
|
const tmp = makeProject(t);
|
||||||
|
const env = { HOME: tmp, USERPROFILE: tmp };
|
||||||
|
|
||||||
|
const absent = runGsdTools(['config-get', 'planner.stall_detection_enabled', '--raw'], tmp, env);
|
||||||
|
assert.equal(absent.success, true, absent.error);
|
||||||
|
assert.equal(absent.output, 'true');
|
||||||
|
|
||||||
|
const noConfig = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4570-no-config-'));
|
||||||
|
t.after(() => cleanup(noConfig));
|
||||||
|
const absentFile = runGsdTools(
|
||||||
|
['config-get', 'planner.stall_detection_enabled', '--raw'],
|
||||||
|
noConfig,
|
||||||
|
{ HOME: noConfig, USERPROFILE: noConfig },
|
||||||
|
);
|
||||||
|
assert.equal(absentFile.success, true, absentFile.error);
|
||||||
|
assert.equal(absentFile.output, 'true');
|
||||||
|
|
||||||
|
const set = runGsdTools(['config-set', 'planner.stall_detection_enabled', 'false'], tmp, env);
|
||||||
|
assert.equal(set.success, true, set.error);
|
||||||
|
const onDisk = JSON.parse(fs.readFileSync(path.join(tmp, '.planning', 'config.json'), 'utf8'));
|
||||||
|
assert.equal(onDisk.planner.stall_detection_enabled, false);
|
||||||
|
assert.equal(typeof onDisk.planner.stall_detection_enabled, 'boolean');
|
||||||
|
|
||||||
|
const roundTrip = runGsdTools(['config-get', 'planner.stall_detection_enabled', '--raw'], tmp, env);
|
||||||
|
assert.equal(roundTrip.success, true, roundTrip.error);
|
||||||
|
assert.equal(roundTrip.output, 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('property: every non-boolean CLI value is rejected without modifying config', (t) => {
|
||||||
|
const tmp = makeProject(t, { planner: { stall_detection_enabled: true }, sentinel: 'preserve' });
|
||||||
|
const configPath = path.join(tmp, '.planning', 'config.json');
|
||||||
|
const before = fs.readFileSync(configPath, 'utf8');
|
||||||
|
const printable = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789[]{}._- ';
|
||||||
|
const invalidValue = fc.array(fc.constantFrom(...printable), { minLength: 1, maxLength: 16 })
|
||||||
|
.map((chars) => chars.join(''))
|
||||||
|
.filter((value) => !['true', 'false', 'null'].includes(value));
|
||||||
|
|
||||||
|
fc.assert(
|
||||||
|
fc.property(invalidValue, (value) => {
|
||||||
|
const result = runGsdTools(
|
||||||
|
['config-set', 'planner.stall_detection_enabled', value],
|
||||||
|
tmp,
|
||||||
|
{ HOME: tmp, USERPROFILE: tmp },
|
||||||
|
);
|
||||||
|
return result.success === false && fs.readFileSync(configPath, 'utf8') === before;
|
||||||
|
}),
|
||||||
|
{ numRuns: 20 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hand-edited non-booleans fail safely to true on both config-get and Config Loader reads', (t) => {
|
||||||
|
const tmp = makeProject(t, { planner: { stall_detection_enabled: 'false' } });
|
||||||
|
const result = runGsdTools(
|
||||||
|
['config-get', 'planner.stall_detection_enabled', '--raw'],
|
||||||
|
tmp,
|
||||||
|
{ HOME: tmp, USERPROFILE: tmp },
|
||||||
|
);
|
||||||
|
assert.equal(result.success, true, result.error);
|
||||||
|
assert.equal(result.output, 'true', 'a string "false" must not disable the watchdog');
|
||||||
|
|
||||||
|
const { loadConfig } = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||||
|
assert.equal(loadConfig(tmp).planner_stall_detection_enabled, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manifest skew cannot make a non-boolean planner setting disable detection', () => {
|
||||||
|
const { resolvePlannerStallDetectionEnabled } = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||||
|
for (const value of [undefined, null, 'false', 0, {}]) {
|
||||||
|
assert.equal(resolvePlannerStallDetectionEnabled(value), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root, GSD_PROJECT, and workstream reads retain canonical scope precedence', (t) => {
|
||||||
|
const tmp = makeProject(t, { planner: { stall_detection_enabled: false } });
|
||||||
|
const projectDir = path.join(tmp, '.planning', 'product-a');
|
||||||
|
const workstreamDir = path.join(tmp, '.planning', 'workstreams', 'alpha');
|
||||||
|
fs.mkdirSync(projectDir, { recursive: true });
|
||||||
|
fs.mkdirSync(workstreamDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectDir, 'config.json'), '{"planner":{"stall_detection_enabled":true}}\n');
|
||||||
|
fs.writeFileSync(path.join(workstreamDir, 'config.json'), '{"planner":{"stall_detection_enabled":true}}\n');
|
||||||
|
const home = { HOME: tmp, USERPROFILE: tmp };
|
||||||
|
|
||||||
|
assert.equal(runGsdTools(['config-get', 'planner.stall_detection_enabled', '--raw'], tmp, home).output, 'false');
|
||||||
|
assert.equal(runGsdTools(
|
||||||
|
['config-get', 'planner.stall_detection_enabled', '--raw'], tmp,
|
||||||
|
{ ...home, GSD_PROJECT: 'product-a' },
|
||||||
|
).output, 'true');
|
||||||
|
assert.equal(runGsdTools(
|
||||||
|
['config-get', 'planner.stall_detection_enabled', '--raw'], tmp,
|
||||||
|
{ ...home, GSD_WORKSTREAM: 'alpha' },
|
||||||
|
).output, 'true');
|
||||||
|
|
||||||
|
const { loadConfig } = require('../gsd-core/bin/lib/config-loader.cjs');
|
||||||
|
assert.equal(loadConfig(tmp).planner_stall_detection_enabled, false);
|
||||||
|
assert.equal(loadConfig(tmp, { workstream: 'alpha' }).planner_stall_detection_enabled, true);
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(workstreamDir, 'config.json'), '{"planner":{}}\n');
|
||||||
|
assert.equal(runGsdTools(
|
||||||
|
['config-get', 'planner.stall_detection_enabled', '--raw'], tmp,
|
||||||
|
{ ...home, GSD_WORKSTREAM: 'alpha' },
|
||||||
|
).output, 'false', 'an omitted workstream value must inherit the root value');
|
||||||
|
assert.equal(loadConfig(tmp, { workstream: 'alpha' }).planner_stall_detection_enabled, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('English and enumerating localized docs state default, CLI opt-out, effect, and recovery loss', () => {
|
||||||
|
for (const docsPath of [CONFIGURATION_DOCS_PATH, PT_BR_CONFIGURATION_DOCS_PATH, ZH_CN_CONFIGURATION_DOCS_PATH, JA_JP_CONFIGURATION_DOCS_PATH, KO_KR_CONFIGURATION_DOCS_PATH]) {
|
||||||
|
const docs = fs.readFileSync(docsPath, 'utf8');
|
||||||
|
assert.match(docs, /`planner\.stall_detection_enabled`\s*\|\s*boolean\s*\|\s*`true`/);
|
||||||
|
assert.match(docs, /config-set planner\.stall_detection_enabled false/);
|
||||||
|
assert.match(docs, /runtime-native|nativa do runtime|运行时原生|ランタイムネイティブ|런타임 네이티브/i);
|
||||||
|
assert.match(docs, /bounded recovery|recupera[cç][aã]o limitada|有界恢复|有界な復旧|제한된 복구/i);
|
||||||
|
}
|
||||||
|
assert.match(fs.readFileSync(ZH_CN_PLANNING_CONFIG_PATH, 'utf8'), /planner\.stall_detection_enabled/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('advanced settings warns about recovery loss before offering to persist false', () => {
|
||||||
|
const settings = fs.readFileSync(SETTINGS_ADVANCED_PATH, 'utf8');
|
||||||
|
assert.match(settings, /planner\.stall_detection_enabled/);
|
||||||
|
assert.match(settings, /default:\s*`true`/);
|
||||||
|
assert.match(settings, /bounded automatic recovery[\s\S]{0,500}false/i);
|
||||||
|
assert.match(settings, /config-set planner\.stall_detection_enabled false/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('bug #2650 plan-phase — all five planner/plan-checker spawns dispatch in the background with bounded stall surveillance', () => {
|
describe('bug #2650 plan-phase — all five planner/plan-checker spawns dispatch in the background with bounded stall surveillance', () => {
|
||||||
let workflow;
|
let workflow;
|
||||||
|
|
||||||
@@ -475,10 +624,32 @@ describe('bug #2650 plan-phase — all five planner/plan-checker spawns dispatch
|
|||||||
|
|
||||||
test('stall-detection-helpers.md resolves PLANNER_STALL_INTERVAL_MINUTES / PLANNER_STALL_THRESHOLD_MINUTES from config', () => {
|
test('stall-detection-helpers.md resolves PLANNER_STALL_INTERVAL_MINUTES / PLANNER_STALL_THRESHOLD_MINUTES from config', () => {
|
||||||
const helpersDoc = readStallHelpersDoc();
|
const helpersDoc = readStallHelpersDoc();
|
||||||
|
assert.match(helpersDoc, /PLANNER_STALL_DETECTION_ENABLED=.*planner\.stall_detection_enabled/);
|
||||||
assert.match(helpersDoc, /PLANNER_STALL_INTERVAL_MINUTES=.*planner\.stall_detect_interval_minutes/);
|
assert.match(helpersDoc, /PLANNER_STALL_INTERVAL_MINUTES=.*planner\.stall_detect_interval_minutes/);
|
||||||
assert.match(helpersDoc, /PLANNER_STALL_THRESHOLD_MINUTES=.*planner\.stall_threshold_minutes/);
|
assert.match(helpersDoc, /PLANNER_STALL_THRESHOLD_MINUTES=.*planner\.stall_threshold_minutes/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('invalid or absent toggle values normalize to default-on; only boolean false disables', (t) => {
|
||||||
|
const helpersBash = extractStallHelpersBash();
|
||||||
|
for (const [stored, expected] of [[undefined, 'true'], [true, 'true'], [false, 'false'], ['false', 'true'], [0, 'true'], [null, 'true']]) {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-4570-resolve-'));
|
||||||
|
t.after(() => cleanup(tmp));
|
||||||
|
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||||
|
const config = stored === undefined ? {} : { planner: { stall_detection_enabled: stored } };
|
||||||
|
fs.writeFileSync(path.join(tmp, '.planning', 'config.json'), `${JSON.stringify(config)}\n`);
|
||||||
|
const result = runBashScript(
|
||||||
|
`${helpersBash}\nprintf '%s\\n' "$PLANNER_STALL_DETECTION_ENABLED"\n`,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
cwd: tmp,
|
||||||
|
env: { ...process.env, HOME: tmp, USERPROFILE: tmp, RUNTIME_DIR: REPO_ROOT, GSD_PROJECT: '', GSD_WORKSTREAM: '' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(result.stdout.trim(), expected, `stored ${JSON.stringify(stored)} resolved incorrectly`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('standard planner spawn (step 8) dispatches with run_in_background=true and calls gsd_stall_watch', () => {
|
test('standard planner spawn (step 8) dispatches with run_in_background=true and calls gsd_stall_watch', () => {
|
||||||
const idx = workflow.indexOf('## 8. Spawn gsd-planner Agent');
|
const idx = workflow.indexOf('## 8. Spawn gsd-planner Agent');
|
||||||
assert.notEqual(idx, -1);
|
assert.notEqual(idx, -1);
|
||||||
@@ -593,6 +764,34 @@ describe('bug #2650 plan-phase — all five planner/plan-checker spawns dispatch
|
|||||||
`expected exactly 5 gsd_stall_watch "$TS" "{outputFile}" spawn-site invocations across plan-phase.md + steps/*.md, found ${callCount}`);
|
`expected exactly 5 gsd_stall_watch "$TS" "{outputFile}" spawn-site invocations across plan-phase.md + steps/*.md, found ${callCount}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('all five spawn classes gate background surveillance and retain a runtime-native blocking result path', () => {
|
||||||
|
const mainSections = [
|
||||||
|
['standard planner', '## 8. Spawn gsd-planner Agent', '## 9. Handle Planner Return'],
|
||||||
|
['plan-checker', '## 10. Spawn gsd-plan-checker Agent', '## 11. Handle Checker Return'],
|
||||||
|
['revision planner', '## 12. Revision Loop', '## 12.5. Plan Bounce'],
|
||||||
|
];
|
||||||
|
const chunkedDoc = readChunkedPlanningMode();
|
||||||
|
const sections = mainSections.map(([label, start, end]) => {
|
||||||
|
const startAt = workflow.indexOf(start);
|
||||||
|
return [label, workflow.slice(startAt, workflow.indexOf(end, startAt))];
|
||||||
|
});
|
||||||
|
sections.push(
|
||||||
|
['chunked outline', chunkedDoc.slice(
|
||||||
|
chunkedDoc.indexOf('### 8.5.1 Outline Phase'),
|
||||||
|
chunkedDoc.indexOf('### 8.5.2 Per-Plan Tasks'),
|
||||||
|
)],
|
||||||
|
['chunked per-plan', chunkedDoc.slice(chunkedDoc.indexOf('### 8.5.2 Per-Plan Tasks'))],
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [label, section] of sections) {
|
||||||
|
assert.match(section, /PLANNER_STALL_DETECTION_ENABLED/, `${label}: missing toggle gate`);
|
||||||
|
assert.match(section, /`true`[\s\S]*run_in_background=true[\s\S]*gsd_stall_watch/,
|
||||||
|
`${label}: default-on branch must retain background watcher behavior`);
|
||||||
|
assert.match(section, /`false`[\s\S]{0,700}(?:omit|without) `?run_in_background`?[\s\S]{0,700}(?:ordinary|runtime-native)[\s\S]{0,500}(?:return|result)/i,
|
||||||
|
`${label}: explicit-off branch must omit backgrounding and await the real runtime result`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('step 7.99 documents that {outputFile} must be bound from the real Agent() return (not passed literally)', () => {
|
test('step 7.99 documents that {outputFile} must be bound from the real Agent() return (not passed literally)', () => {
|
||||||
const idx = workflow.indexOf('## 7.99. Bounded Stall-Detection Helpers');
|
const idx = workflow.indexOf('## 7.99. Bounded Stall-Detection Helpers');
|
||||||
assert.notEqual(idx, -1);
|
assert.notEqual(idx, -1);
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ const REPO_ROOT = path.join(__dirname, '..');
|
|||||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||||
const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md');
|
const PILOT_REL = path.join('gsd-core', 'workflows', 'execute-phase.md');
|
||||||
const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL);
|
const PILOT_PATH = path.join(REPO_ROOT, PILOT_REL);
|
||||||
|
const PLAN_PHASE_REL = path.join('gsd-core', 'workflows', 'plan-phase.md');
|
||||||
|
const CHUNKED_PLAN_REL = path.join('gsd-core', 'workflows', 'plan-phase', 'steps', 'chunked-planning-mode.md');
|
||||||
|
const STALL_HELPERS_REL = path.join('gsd-core', 'workflows', 'plan-phase', 'steps', 'stall-detection-helpers.md');
|
||||||
// plan-phase.md was the original #2930 pilot but was reverted to unmarked
|
// plan-phase.md was the original #2930 pilot but was reverted to unmarked
|
||||||
// (chore/2930 retarget: it sits 36 B under the ADR-857 Phase-6 PRE_PHASE6
|
// (chore/2930 retarget: it sits 36 B under the ADR-857 Phase-6 PRE_PHASE6
|
||||||
// gate and cannot absorb marker overhead) — it was a genuinely unmarked
|
// gate and cannot absorb marker overhead) — it was a genuinely unmarked
|
||||||
@@ -284,6 +287,22 @@ test('noSectionMarkerLeaksIntoEmittedArtifacts', (t) => {
|
|||||||
false,
|
false,
|
||||||
`${runtime}: emitted execute-phase.md still contains a gsd:section marker token`,
|
`${runtime}: emitted execute-phase.md still contains a gsd:section marker token`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// #4570: assert against the real installed workflow bytes for every runtime,
|
||||||
|
// after composition and runtime conversion. The background parameter itself
|
||||||
|
// is runtime vocabulary (`run_in_background` becomes `background` on Hermes),
|
||||||
|
// so parity is pinned on the shared gate and its two semantic branches.
|
||||||
|
for (const relativePath of [PLAN_PHASE_REL, CHUNKED_PLAN_REL, STALL_HELPERS_REL]) {
|
||||||
|
const installedPath = path.join(configDir, relativePath);
|
||||||
|
assert.ok(fs.existsSync(installedPath), `${runtime}: emitted ${relativePath} is missing`);
|
||||||
|
const installed = fs.readFileSync(installedPath, 'utf8');
|
||||||
|
assert.match(installed, /PLANNER_STALL_DETECTION_ENABLED/,
|
||||||
|
`${runtime}: ${relativePath} lost the planner stall-detection gate during conversion`);
|
||||||
|
assert.match(installed, /gsd_stall_watch/,
|
||||||
|
`${runtime}: ${relativePath} lost the default-on watcher branch during conversion`);
|
||||||
|
assert.match(installed, /runtime-native/,
|
||||||
|
`${runtime}: ${relativePath} lost the explicit-off ordinary completion branch during conversion`);
|
||||||
|
}
|
||||||
cleanup(root);
|
cleanup(root);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user