@opengsd/gsd-pi 1.5.0-dev.4fb2d460 → 1.5.0-dev.c4c8885f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
- package/dist/resources/extensions/gsd/activity-log.js +1 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +53 -15
- package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
- package/dist/resources/extensions/gsd/auto-start.js +53 -30
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
- package/dist/resources/extensions/gsd/auto-timers.js +9 -2
- package/dist/resources/extensions/gsd/auto.js +97 -31
- package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
- package/dist/resources/extensions/gsd/config-overlay.js +7 -2
- package/dist/resources/extensions/gsd/db/engine.js +33 -0
- package/dist/resources/extensions/gsd/db-adapter.js +30 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/dist/resources/extensions/gsd/journal.js +1 -1
- package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
- package/dist/resources/extensions/gsd/metrics.js +2 -2
- package/dist/resources/extensions/gsd/paths.js +70 -49
- package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
- package/dist/resources/extensions/gsd/preferences-models.js +66 -15
- package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
- package/dist/resources/extensions/gsd/rule-registry.js +106 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
- package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
- package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
- package/dist/resources/extensions/gsd/uok/audit.js +3 -0
- package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
- package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
- package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
- package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
- package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
- package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
- package/dist/resources/extensions/gsd/workspace.js +5 -14
- package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/worktree-cli-create.d.ts +10 -0
- package/dist/worktree-cli-create.js +11 -0
- package/dist/worktree-cli-format.d.ts +12 -0
- package/dist/worktree-cli-format.js +38 -0
- package/dist/worktree-cli-plan.d.ts +16 -0
- package/dist/worktree-cli-plan.js +19 -0
- package/dist/worktree-cli-session.d.ts +13 -0
- package/dist/worktree-cli-session.js +18 -0
- package/dist/worktree-cli-status.d.ts +35 -0
- package/dist/worktree-cli-status.js +54 -0
- package/dist/worktree-cli.d.ts +1 -16
- package/dist/worktree-cli.js +28 -122
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
- package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-agent.js +113 -0
- package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
- package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator-tools.js +137 -0
- package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +4 -56
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
- package/packages/daemon/dist/orchestrator.js +13 -303
- package/packages/daemon/dist/orchestrator.js.map +1 -1
- package/packages/daemon/dist/orchestrator.test.js +11 -1
- package/packages/daemon/dist/orchestrator.test.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
- package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
- package/src/resources/extensions/gsd/activity-log.ts +1 -1
- package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +59 -14
- package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
- package/src/resources/extensions/gsd/auto-start.ts +57 -28
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
- package/src/resources/extensions/gsd/auto-timers.ts +9 -2
- package/src/resources/extensions/gsd/auto.ts +103 -30
- package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
- package/src/resources/extensions/gsd/config-overlay.ts +7 -1
- package/src/resources/extensions/gsd/db/engine.ts +36 -0
- package/src/resources/extensions/gsd/db-adapter.ts +29 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
- package/src/resources/extensions/gsd/journal.ts +1 -1
- package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
- package/src/resources/extensions/gsd/metrics.ts +2 -2
- package/src/resources/extensions/gsd/paths.ts +87 -48
- package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
- package/src/resources/extensions/gsd/preferences-models.ts +74 -20
- package/src/resources/extensions/gsd/preferences-types.ts +24 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
- package/src/resources/extensions/gsd/rule-registry.ts +111 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
- package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
- package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
- package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
- package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
- package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
- package/src/resources/extensions/gsd/types.ts +23 -4
- package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
- package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
- package/src/resources/extensions/gsd/uok/audit.ts +2 -0
- package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
- package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
- package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
- package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
- package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
- package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
- package/src/resources/extensions/gsd/workspace.ts +5 -21
- package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
GSDModelPhaseKey,
|
|
24
24
|
GSDThinkingConfig,
|
|
25
25
|
ResolvedModelConfig,
|
|
26
|
-
|
|
26
|
+
ResolvedAutoSupervisorConfig,
|
|
27
27
|
} from "./preferences-types.js";
|
|
28
28
|
import { clearGSDPreferencesCache, loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
|
29
29
|
import { getUnitPhaseChain } from "./unit-registry.js";
|
|
@@ -86,6 +86,40 @@ function resolveWinningPhase(
|
|
|
86
86
|
return undefined;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a single model field into a {@link ResolvedModelConfig}.
|
|
91
|
+
*
|
|
92
|
+
* Accepts either the legacy bare-string form or the extended object form
|
|
93
|
+
* (`{ model, provider?, fallbacks? }`). This is the shared normalization used
|
|
94
|
+
* for phase buckets so sibling single-model fields — `post_unit_hooks[].model`,
|
|
95
|
+
* `auto_supervisor.model`, and `reactive_execution.subagent_model` — honor
|
|
96
|
+
* `fallbacks[]` identically rather than accepting a bare string only (#1229).
|
|
97
|
+
*
|
|
98
|
+
* Returns undefined for an unset, blank, or model-less field.
|
|
99
|
+
*/
|
|
100
|
+
export function normalizeModelFieldConfig(
|
|
101
|
+
field: string | GSDPhaseModelConfig | undefined,
|
|
102
|
+
): ResolvedModelConfig | undefined {
|
|
103
|
+
if (!field) return undefined;
|
|
104
|
+
if (typeof field === "string") {
|
|
105
|
+
return field.trim() ? { primary: field, fallbacks: [] } : undefined;
|
|
106
|
+
}
|
|
107
|
+
if (!field.model) return undefined;
|
|
108
|
+
// When provider is explicitly set, prepend it to the model ID so the
|
|
109
|
+
// resolution code in auto.ts can do an explicit provider match.
|
|
110
|
+
const primary = field.provider && !field.model.includes("/")
|
|
111
|
+
? `${field.provider}/${field.model}`
|
|
112
|
+
: field.model;
|
|
113
|
+
// Carry the per-field `thinking` sub-field through so the dispatch site can
|
|
114
|
+
// prefer it over the session/floor level, mirroring phase-inline thinking
|
|
115
|
+
// (#1269). `undefined` is preserved as absent so behavior is unchanged.
|
|
116
|
+
return {
|
|
117
|
+
primary,
|
|
118
|
+
fallbacks: field.fallbacks ?? [],
|
|
119
|
+
...(field.thinking ? { thinking: field.thinking } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
/**
|
|
90
124
|
* Resolve model and fallbacks for a given auto-mode unit type.
|
|
91
125
|
* Returns the primary model and ordered fallbacks, or undefined if not configured.
|
|
@@ -93,6 +127,15 @@ function resolveWinningPhase(
|
|
|
93
127
|
* Supports both legacy string format and extended object format:
|
|
94
128
|
* - Legacy: `planning: claude-opus-4-6`
|
|
95
129
|
* - Extended: `planning: { model: claude-opus-4-6, fallbacks: [glm-5, minimax-m2.5] }`
|
|
130
|
+
*
|
|
131
|
+
* Hook unit types (`hook/<name>`) resolve against the matching
|
|
132
|
+
* `post_unit_hooks[]` entry's `model` field so hook sessions honor the same
|
|
133
|
+
* `fallbacks[]` chain as phase buckets — the recovery path can then switch to
|
|
134
|
+
* a fallback provider instead of hard-failing (and potentially pausing) the
|
|
135
|
+
* run on a transient provider trip (#1229).
|
|
136
|
+
*
|
|
137
|
+
* The synthetic `supervisor` unit type resolves against `auto_supervisor.model`
|
|
138
|
+
* for supervisor interventions (wrap-up warnings, timeout recovery).
|
|
96
139
|
*/
|
|
97
140
|
export function resolveModelWithFallbacksForUnit(
|
|
98
141
|
unitType: string,
|
|
@@ -107,27 +150,26 @@ export function resolveModelWithFallbacksForUnit(
|
|
|
107
150
|
...(skipProfileDefaults ? { skipProfileDefaults: true } : {}),
|
|
108
151
|
};
|
|
109
152
|
const prefs = loadEffectiveGSDPreferences(basePath, loadOpts);
|
|
110
|
-
const chain = phaseChainForUnit(unitType);
|
|
111
|
-
if (!chain) return undefined;
|
|
112
|
-
const winner = resolveWinningPhase(prefs?.preferences?.models as GSDModelConfigV2 | undefined, chain);
|
|
113
|
-
if (!winner) return undefined;
|
|
114
|
-
const phaseConfig = winner.config;
|
|
115
153
|
|
|
116
|
-
//
|
|
117
|
-
if (
|
|
118
|
-
return
|
|
154
|
+
// Supervisor interventions resolve against `auto_supervisor.model` (#1229).
|
|
155
|
+
if (unitType === "supervisor") {
|
|
156
|
+
return normalizeModelFieldConfig(prefs?.preferences?.auto_supervisor?.model);
|
|
119
157
|
}
|
|
120
158
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
// Hook sessions resolve against their own `post_unit_hooks[].model` field,
|
|
160
|
+
// which now shares the phase-bucket object form (#1229).
|
|
161
|
+
if (unitType.startsWith("hook/")) {
|
|
162
|
+
const hookName = unitType.slice("hook/".length);
|
|
163
|
+
const hooks = prefs?.preferences?.post_unit_hooks;
|
|
164
|
+
const hook = Array.isArray(hooks) ? hooks.find((h) => h.name === hookName) : undefined;
|
|
165
|
+
return normalizeModelFieldConfig(hook?.model);
|
|
166
|
+
}
|
|
126
167
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
168
|
+
const chain = phaseChainForUnit(unitType);
|
|
169
|
+
if (!chain) return undefined;
|
|
170
|
+
const winner = resolveWinningPhase(prefs?.preferences?.models as GSDModelConfigV2 | undefined, chain);
|
|
171
|
+
if (!winner) return undefined;
|
|
172
|
+
return normalizeModelFieldConfig(winner.config);
|
|
131
173
|
}
|
|
132
174
|
|
|
133
175
|
/**
|
|
@@ -157,6 +199,14 @@ export function resolveThinkingLevelForUnit(
|
|
|
157
199
|
basePath?: string,
|
|
158
200
|
availableModelIds?: string[],
|
|
159
201
|
): GSDThinkingLevel | undefined {
|
|
202
|
+
// Single-field configs — `auto_supervisor.model` and `post_unit_hooks[].model`
|
|
203
|
+
// — carry their own inline `thinking` on the object model form and have no
|
|
204
|
+
// phase-bucket chain, so the walk below never covers them. Resolve their
|
|
205
|
+
// per-field level directly (#1269).
|
|
206
|
+
if (unitType === "supervisor" || unitType.startsWith("hook/")) {
|
|
207
|
+
return resolveModelWithFallbacksForUnit(unitType, basePath, availableModelIds)?.thinking;
|
|
208
|
+
}
|
|
209
|
+
|
|
160
210
|
const loadOpts = availableModelIds !== undefined ? { availableModelIds } : undefined;
|
|
161
211
|
const prefs = loadEffectiveGSDPreferences(basePath, loadOpts)?.preferences;
|
|
162
212
|
if (!prefs) return undefined;
|
|
@@ -425,16 +475,20 @@ export function resolveDynamicRoutingConfig(): DynamicRoutingConfig {
|
|
|
425
475
|
};
|
|
426
476
|
}
|
|
427
477
|
|
|
428
|
-
export function resolveAutoSupervisorConfig():
|
|
478
|
+
export function resolveAutoSupervisorConfig(): ResolvedAutoSupervisorConfig {
|
|
429
479
|
const prefs = loadEffectiveGSDPreferences();
|
|
430
480
|
const configured = prefs?.preferences.auto_supervisor ?? {};
|
|
431
481
|
|
|
482
|
+
const modelConfig = normalizeModelFieldConfig(configured.model);
|
|
483
|
+
|
|
432
484
|
return {
|
|
433
485
|
soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
|
|
434
486
|
idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
|
|
435
487
|
hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
|
|
436
488
|
stalled_tool_timeout_minutes: configured.stalled_tool_timeout_minutes ?? 5,
|
|
437
|
-
...(
|
|
489
|
+
...(modelConfig
|
|
490
|
+
? { model: modelConfig.primary, modelFallbacks: modelConfig.fallbacks }
|
|
491
|
+
: {}),
|
|
438
492
|
};
|
|
439
493
|
}
|
|
440
494
|
|
|
@@ -312,12 +312,24 @@ export interface GSDModelConfigV2 {
|
|
|
312
312
|
export interface ResolvedModelConfig {
|
|
313
313
|
primary: string;
|
|
314
314
|
fallbacks: string[];
|
|
315
|
+
/**
|
|
316
|
+
* Per-field reasoning level carried from the object model form's `thinking`
|
|
317
|
+
* sub-field (e.g. `{ model, thinking: high }` on `post_unit_hooks[].model`,
|
|
318
|
+
* `auto_supervisor.model`, or `reactive_execution.subagent_model`). Applied at
|
|
319
|
+
* the dispatch site in preference to the session/floor level (#1269).
|
|
320
|
+
*/
|
|
321
|
+
thinking?: GSDThinkingLevel;
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
export type SkillDiscoveryMode = "auto" | "suggest" | "off";
|
|
318
325
|
|
|
319
326
|
export interface AutoSupervisorConfig {
|
|
320
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Model ID for the supervisor process. Accepts a bare model ID or the
|
|
329
|
+
* extended `{ model, provider?, fallbacks? }` object form for parity with
|
|
330
|
+
* phase buckets (#1229). Resolution normalizes it to the primary model ID.
|
|
331
|
+
*/
|
|
332
|
+
model?: string | GSDPhaseModelConfig;
|
|
321
333
|
soft_timeout_minutes?: number;
|
|
322
334
|
idle_timeout_minutes?: number;
|
|
323
335
|
hard_timeout_minutes?: number;
|
|
@@ -331,6 +343,17 @@ export interface AutoSupervisorConfig {
|
|
|
331
343
|
stalled_tool_timeout_minutes?: number;
|
|
332
344
|
}
|
|
333
345
|
|
|
346
|
+
/**
|
|
347
|
+
* The supervisor config after resolution by `resolveAutoSupervisorConfig`.
|
|
348
|
+
* Identical to {@link AutoSupervisorConfig} except `model` is always the
|
|
349
|
+
* normalized primary model ID string and `modelFallbacks` carries the ordered
|
|
350
|
+
* fallback chain from the object form accepted in preferences (#1229).
|
|
351
|
+
*/
|
|
352
|
+
export type ResolvedAutoSupervisorConfig = Omit<AutoSupervisorConfig, "model"> & {
|
|
353
|
+
model?: string;
|
|
354
|
+
modelFallbacks?: string[];
|
|
355
|
+
};
|
|
356
|
+
|
|
334
357
|
export interface RemoteQuestionsConfig {
|
|
335
358
|
channel: "slack" | "discord" | "telegram";
|
|
336
359
|
channel_id: string | number;
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SKILL_ACTIONS,
|
|
23
23
|
type WorkflowMode,
|
|
24
24
|
type GSDPreferences,
|
|
25
|
+
type GSDPhaseModelConfig,
|
|
25
26
|
type GSDSkillRule,
|
|
26
27
|
type GSDThinkingLevel,
|
|
27
28
|
} from "./preferences-types.js";
|
|
@@ -41,6 +42,62 @@ const VALID_UOK_TURN_ACTIONS = new Set<"commit" | "snapshot" | "status-only">([
|
|
|
41
42
|
"snapshot",
|
|
42
43
|
"status-only",
|
|
43
44
|
]);
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize a single model field that accepts either a bare model ID or the
|
|
47
|
+
* extended `{ model, provider?, fallbacks? }` object form used by phase buckets
|
|
48
|
+
* (#1229). Returns the sanitized value, or undefined when unset/invalid (in
|
|
49
|
+
* which case an error describing `label` is pushed onto `errors`).
|
|
50
|
+
*/
|
|
51
|
+
function sanitizeModelField(
|
|
52
|
+
raw: unknown,
|
|
53
|
+
label: string,
|
|
54
|
+
errors: string[],
|
|
55
|
+
warnings?: string[],
|
|
56
|
+
): string | GSDPhaseModelConfig | undefined {
|
|
57
|
+
if (raw === undefined) return undefined;
|
|
58
|
+
if (typeof raw === "string") {
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
if (!trimmed) {
|
|
61
|
+
errors.push(`${label} must not be an empty string`);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
if (raw && typeof raw === "object") {
|
|
67
|
+
const obj = raw as Record<string, unknown>;
|
|
68
|
+
const model = typeof obj.model === "string" ? obj.model.trim() : "";
|
|
69
|
+
if (!model) {
|
|
70
|
+
errors.push(`${label} object form requires a non-empty "model"`);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const sanitized: GSDPhaseModelConfig = { model };
|
|
74
|
+
if (typeof obj.provider === "string" && obj.provider.trim()) {
|
|
75
|
+
sanitized.provider = obj.provider.trim();
|
|
76
|
+
}
|
|
77
|
+
if (obj.fallbacks !== undefined) {
|
|
78
|
+
const fallbacks = normalizeStringArray(obj.fallbacks);
|
|
79
|
+
if (fallbacks.length > 0) sanitized.fallbacks = fallbacks;
|
|
80
|
+
}
|
|
81
|
+
// Preserve the object form's `thinking` sub-field so the dispatch site can
|
|
82
|
+
// pin the hook/supervisor/subagent reasoning level (#1269). An illegal level
|
|
83
|
+
// is warned-and-stripped, mirroring inline per-phase thinking, so a typo can't
|
|
84
|
+
// reach the resolver and be treated as explicit configuration.
|
|
85
|
+
if (obj.thinking !== undefined) {
|
|
86
|
+
if (VALID_THINKING_LEVELS.has(obj.thinking as GSDThinkingLevel)) {
|
|
87
|
+
sanitized.thinking = obj.thinking as GSDThinkingLevel;
|
|
88
|
+
} else {
|
|
89
|
+
warnings?.push(
|
|
90
|
+
`${label} thinking "${String(obj.thinking)}" is not a valid thinking level ` +
|
|
91
|
+
`(off, minimal, low, medium, high, xhigh) — ignored`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
errors.push(`${label} must be a string or an object with a "model" field`);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
44
101
|
const VALID_POST_UNIT_HOOK_CRITICALITIES = new Set(["advisory", "blocking"]);
|
|
45
102
|
const VALID_POST_UNIT_HOOK_ON_BLOCK_ACTIONS = new Set([
|
|
46
103
|
"retry-unit",
|
|
@@ -476,7 +533,15 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
476
533
|
// ─── Auto Supervisor ────────────────────────────────────────────────
|
|
477
534
|
if (preferences.auto_supervisor !== undefined) {
|
|
478
535
|
if (preferences.auto_supervisor && typeof preferences.auto_supervisor === "object") {
|
|
479
|
-
|
|
536
|
+
// Validate `model` through the shared sanitizer so the
|
|
537
|
+
// `{ model, provider?, fallbacks? }` object form is rejected on the same
|
|
538
|
+
// terms as its post_unit_hooks / reactive_execution siblings (#1229).
|
|
539
|
+
// Other supervisor fields pass through unchanged.
|
|
540
|
+
const supervisor = { ...preferences.auto_supervisor };
|
|
541
|
+
if (supervisor.model !== undefined) {
|
|
542
|
+
supervisor.model = sanitizeModelField(supervisor.model, "auto_supervisor.model", errors, warnings);
|
|
543
|
+
}
|
|
544
|
+
validated.auto_supervisor = supervisor;
|
|
480
545
|
} else {
|
|
481
546
|
errors.push("auto_supervisor must be an object");
|
|
482
547
|
}
|
|
@@ -568,8 +633,9 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
568
633
|
const mc = typeof hook.max_cycles === "number" ? hook.max_cycles : Number(hook.max_cycles);
|
|
569
634
|
validHook.max_cycles = Number.isFinite(mc) ? Math.max(1, Math.min(10, Math.round(mc))) : 1;
|
|
570
635
|
}
|
|
571
|
-
|
|
572
|
-
|
|
636
|
+
const hookModel = sanitizeModelField(hook.model, `post_unit_hooks "${name}" model`, errors, warnings);
|
|
637
|
+
if (hookModel !== undefined) {
|
|
638
|
+
validHook.model = hookModel;
|
|
573
639
|
}
|
|
574
640
|
if (typeof hook.artifact === "string" && hook.artifact.trim()) {
|
|
575
641
|
validHook.artifact = hook.artifact.trim();
|
|
@@ -1021,12 +1087,14 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1021
1087
|
}
|
|
1022
1088
|
}
|
|
1023
1089
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1090
|
+
const subagentModel = sanitizeModelField(
|
|
1091
|
+
re.subagent_model,
|
|
1092
|
+
"reactive_execution.subagent_model",
|
|
1093
|
+
errors,
|
|
1094
|
+
warnings,
|
|
1095
|
+
);
|
|
1096
|
+
if (subagentModel !== undefined) {
|
|
1097
|
+
validRe.subagent_model = subagentModel;
|
|
1030
1098
|
}
|
|
1031
1099
|
|
|
1032
1100
|
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode", "subagent_model"]);
|
|
@@ -22,8 +22,9 @@ import type {
|
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
24
24
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
26
|
import { parseUnitId } from "./unit-id.js";
|
|
27
|
+
import { resolveMilestonePath } from "./paths.js";
|
|
27
28
|
import { queryJournal, type JournalEntry } from "./journal.js";
|
|
28
29
|
import { readUnitRuntimeRecord, type UnitRuntimePhase } from "./unit-runtime.js";
|
|
29
30
|
import { extractFrontmatterVerdict } from "./verdict-parser.js";
|
|
@@ -32,13 +33,45 @@ import { extractFrontmatterVerdict } from "./verdict-parser.js";
|
|
|
32
33
|
|
|
33
34
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
34
35
|
const { milestone, slice, task } = parseUnitId(unitId);
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
// Prefer the active phase directory (flat-phase layout: .gsd/phases/<NN>-.../).
|
|
38
|
+
// Configured gate artifacts are canonical phase-level files (e.g.
|
|
39
|
+
// BROWSER-RUNTIME-EVIDENCE.md), so resolve the exact declared name first,
|
|
40
|
+
// then a task-prefixed variant for retry sentinels / rework briefs.
|
|
41
|
+
const candidates: string[] = [];
|
|
42
|
+
const phaseDir = resolveMilestonePath(basePath, milestone);
|
|
43
|
+
const isFlatPhaseDir = phaseDir !== null
|
|
44
|
+
&& !(dirname(phaseDir).endsWith("/milestones") || dirname(phaseDir).endsWith("\\milestones"));
|
|
45
|
+
if (isFlatPhaseDir) {
|
|
46
|
+
candidates.push(join(phaseDir, artifactName));
|
|
47
|
+
if (task !== undefined) {
|
|
48
|
+
candidates.push(join(phaseDir, `${task}-${artifactName}`));
|
|
49
|
+
}
|
|
37
50
|
}
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
|
|
52
|
+
// Legacy nested layout fallbacks (.gsd/milestones/<M>/slices/<S>/tasks/...).
|
|
53
|
+
const legacyBase = join(basePath, ".gsd", "milestones", milestone);
|
|
54
|
+
let legacyDefault: string;
|
|
55
|
+
if (task !== undefined && slice !== undefined) {
|
|
56
|
+
legacyDefault = join(legacyBase, "slices", slice, "tasks", `${task}-${artifactName}`);
|
|
57
|
+
candidates.push(legacyDefault);
|
|
58
|
+
candidates.push(join(legacyBase, "slices", slice, "tasks", artifactName));
|
|
59
|
+
candidates.push(join(legacyBase, "slices", slice, artifactName));
|
|
60
|
+
} else if (slice !== undefined) {
|
|
61
|
+
legacyDefault = join(legacyBase, "slices", slice, artifactName);
|
|
62
|
+
candidates.push(legacyDefault);
|
|
63
|
+
} else {
|
|
64
|
+
legacyDefault = join(legacyBase, artifactName);
|
|
40
65
|
}
|
|
41
|
-
|
|
66
|
+
candidates.push(join(legacyBase, artifactName));
|
|
67
|
+
|
|
68
|
+
const existing = candidates.find((candidate) => existsSync(candidate));
|
|
69
|
+
if (existing) return existing;
|
|
70
|
+
|
|
71
|
+
// Nothing on disk yet: prefer the active phase path when the phase dir is
|
|
72
|
+
// known, otherwise fall back to the legacy default (preserves pre-flat-phase
|
|
73
|
+
// behavior for missing-artifact diagnostics).
|
|
74
|
+
return isFlatPhaseDir ? join(phaseDir!, artifactName) : legacyDefault;
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
// ─── Dispatch Rule Conversion ──────────────────────────────────────────────
|
|
@@ -126,6 +159,13 @@ export class RuleRegistry {
|
|
|
126
159
|
forceRun?: boolean;
|
|
127
160
|
}> = [];
|
|
128
161
|
cycleCounts: Map<string, number> = new Map();
|
|
162
|
+
/**
|
|
163
|
+
* Cycle keys that have already been granted a one-shot re-dispatch after a
|
|
164
|
+
* lost/interrupted dispatch (hook charged a cycle but never produced its own
|
|
165
|
+
* unit-end). Bounds the dispatch-cycle refund to exactly one per gate so a
|
|
166
|
+
* hook that repeatedly fails to complete cannot loop forever.
|
|
167
|
+
*/
|
|
168
|
+
redispatchedGateKeys: Set<string> = new Set();
|
|
129
169
|
retryPending: boolean = false;
|
|
130
170
|
retryTrigger: { unitType: string; unitId: string; retryArtifact?: string } | null = null;
|
|
131
171
|
hookFailure: HookFailureState | null = null;
|
|
@@ -364,6 +404,14 @@ export class RuleRegistry {
|
|
|
364
404
|
pendingRetry: false,
|
|
365
405
|
};
|
|
366
406
|
|
|
407
|
+
return this._buildHookDispatch(config, triggerUnitId);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Construct the sidecar dispatch for a hook without mutating registry state. */
|
|
411
|
+
private _buildHookDispatch(
|
|
412
|
+
config: PostUnitHookConfig,
|
|
413
|
+
triggerUnitId: string,
|
|
414
|
+
): HookDispatchResult {
|
|
367
415
|
const { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
|
|
368
416
|
let prompt = config.prompt
|
|
369
417
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -375,12 +423,29 @@ export class RuleRegistry {
|
|
|
375
423
|
return {
|
|
376
424
|
hookName: config.name,
|
|
377
425
|
prompt,
|
|
378
|
-
|
|
426
|
+
// Model selection (including fallbacks[]) is handled by
|
|
427
|
+
// resolveModelWithFallbacksForUnit for the `hook/<name>` unit type (#1229).
|
|
379
428
|
unitType: `hook/${config.name}`,
|
|
380
429
|
unitId: triggerUnitId,
|
|
381
430
|
};
|
|
382
431
|
}
|
|
383
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Reconstruct the in-flight hook's dispatch from the restored `activeHook`
|
|
435
|
+
* state, without mutating the registry. Used to reconcile a persisted
|
|
436
|
+
* `activeHook` whose session-local dispatch was lost across a pause/resume
|
|
437
|
+
* or crash-recovery, so the hook actually runs instead of being charged
|
|
438
|
+
* against the next unrelated unit's close-out (#1246).
|
|
439
|
+
*/
|
|
440
|
+
getPendingHookDispatch(basePath: string): HookDispatchResult | null {
|
|
441
|
+
if (!this.activeHook) return null;
|
|
442
|
+
const config = resolvePostUnitHooks(basePath).find(
|
|
443
|
+
h => h.name === this.activeHook!.hookName,
|
|
444
|
+
);
|
|
445
|
+
if (!config) return null;
|
|
446
|
+
return this._buildHookDispatch(config, this.activeHook.triggerUnitId);
|
|
447
|
+
}
|
|
448
|
+
|
|
384
449
|
private _assessHookCompletion(
|
|
385
450
|
basePath: string,
|
|
386
451
|
hook: HookExecutionState,
|
|
@@ -503,6 +568,30 @@ export class RuleRegistry {
|
|
|
503
568
|
observedCleanExecution: boolean,
|
|
504
569
|
): HookDispatchResult | null {
|
|
505
570
|
if (!observedCleanExecution) {
|
|
571
|
+
// The hook was charged a cycle at dispatch but never produced its own
|
|
572
|
+
// unit-end — its dispatch was lost/interrupted (pause/resume, crash
|
|
573
|
+
// recovery). Refund that dispatch-consumed cycle exactly once so the gate
|
|
574
|
+
// is re-dispatched at least once instead of hard-blocking on a hook that
|
|
575
|
+
// never actually ran.
|
|
576
|
+
//
|
|
577
|
+
// This refund is intentionally UNCONDITIONAL on the first lost dispatch —
|
|
578
|
+
// it must NOT be gated on `currentCycle >= max_cycles`. A lost dispatch is
|
|
579
|
+
// a non-run: charging its cycle at dispatch time must not consume the
|
|
580
|
+
// hook's real-run budget. Gating the refund on "budget already exhausted"
|
|
581
|
+
// lets a lost dispatch below max_cycles silently eat a genuine cycle, so
|
|
582
|
+
// the hook gets one fewer real run than configured — reintroducing #1244
|
|
583
|
+
// for max_cycles >= 2 (see the "does not steal a real-run cycle" test).
|
|
584
|
+
// The redispatchedGateKeys flag bounds this to a single refund per gate,
|
|
585
|
+
// so repeated lost dispatches still terminate (total dispatches never
|
|
586
|
+
// exceed max_cycles + 1) and a hook that keeps failing blocks normally.
|
|
587
|
+
const cycleKey = hookCycleKey(config, hook);
|
|
588
|
+
if (!this.redispatchedGateKeys.has(cycleKey)) {
|
|
589
|
+
this.redispatchedGateKeys.add(cycleKey);
|
|
590
|
+
const currentCycle = this.cycleCounts.get(cycleKey);
|
|
591
|
+
if (typeof currentCycle === "number" && currentCycle > 0) {
|
|
592
|
+
this.cycleCounts.set(cycleKey, currentCycle - 1);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
506
595
|
return this._rerunGateOrBlock(config, hook, basePath, {
|
|
507
596
|
reason: `hook/${config.name} did not complete cleanly before the trigger unit resumed`,
|
|
508
597
|
});
|
|
@@ -839,6 +928,7 @@ export class RuleRegistry {
|
|
|
839
928
|
this.activeHook = null;
|
|
840
929
|
this.hookQueue = [];
|
|
841
930
|
this.cycleCounts.clear();
|
|
931
|
+
this.redispatchedGateKeys.clear();
|
|
842
932
|
this.retryPending = false;
|
|
843
933
|
this.retryTrigger = null;
|
|
844
934
|
this.hookFailure = null;
|
|
@@ -855,6 +945,7 @@ export class RuleRegistry {
|
|
|
855
945
|
persistState(basePath: string): void {
|
|
856
946
|
const state: PersistedHookState = {
|
|
857
947
|
cycleCounts: Object.fromEntries(this.cycleCounts),
|
|
948
|
+
redispatchedGateKeys: Array.from(this.redispatchedGateKeys),
|
|
858
949
|
activeHook: this.activeHook ? { ...this.activeHook } : null,
|
|
859
950
|
hookQueue: this.hookQueue.map(entry => ({
|
|
860
951
|
hookName: entry.config.name,
|
|
@@ -888,6 +979,14 @@ export class RuleRegistry {
|
|
|
888
979
|
}
|
|
889
980
|
}
|
|
890
981
|
}
|
|
982
|
+
this.redispatchedGateKeys.clear();
|
|
983
|
+
if (Array.isArray(state.redispatchedGateKeys)) {
|
|
984
|
+
for (const key of state.redispatchedGateKeys) {
|
|
985
|
+
if (typeof key === "string") {
|
|
986
|
+
this.redispatchedGateKeys.add(key);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
891
990
|
this.activeHook = state.activeHook && typeof state.activeHook === "object"
|
|
892
991
|
? { ...state.activeHook }
|
|
893
992
|
: null;
|
|
@@ -918,7 +1017,7 @@ export class RuleRegistry {
|
|
|
918
1017
|
if (existsSync(filePath)) {
|
|
919
1018
|
writeFileSync(
|
|
920
1019
|
filePath,
|
|
921
|
-
JSON.stringify({ cycleCounts: {}, activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2),
|
|
1020
|
+
JSON.stringify({ cycleCounts: {}, redispatchedGateKeys: [], activeHook: null, hookQueue: [], savedAt: new Date().toISOString() }, null, 2),
|
|
922
1021
|
"utf-8",
|
|
923
1022
|
);
|
|
924
1023
|
}
|
|
@@ -1014,7 +1113,10 @@ export class RuleRegistry {
|
|
|
1014
1113
|
return {
|
|
1015
1114
|
hookName: hook.name,
|
|
1016
1115
|
prompt,
|
|
1017
|
-
|
|
1116
|
+
// Model selection (including fallbacks[]) is resolved by dispatchHookUnit
|
|
1117
|
+
// via resolveModelWithFallbacksForUnit for the `hook/<name>` unit type,
|
|
1118
|
+
// matching the auto-mode path. Emitting the primary-only model here would
|
|
1119
|
+
// discard the configured fallback chain (#1229).
|
|
1018
1120
|
unitType: `hook/${hook.name}`,
|
|
1019
1121
|
unitId,
|
|
1020
1122
|
};
|
|
@@ -213,21 +213,13 @@ function detectArtifactDbStatusDriftForMilestone(
|
|
|
213
213
|
|
|
214
214
|
for (const task of tasks) {
|
|
215
215
|
if (isClosedStatus(task.status)) continue;
|
|
216
|
-
|
|
217
|
-
let diskTaskSummary = resolveTaskFile(
|
|
216
|
+
const diskTaskSummary = resolveTaskFile(
|
|
218
217
|
basePath,
|
|
219
218
|
milestoneId,
|
|
220
219
|
slice.id,
|
|
221
220
|
task.id,
|
|
222
221
|
"SUMMARY",
|
|
223
222
|
);
|
|
224
|
-
if (!diskTaskSummary) {
|
|
225
|
-
const phaseDir = resolveMilestonePath(basePath, milestoneId);
|
|
226
|
-
if (phaseDir) {
|
|
227
|
-
const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
|
|
228
|
-
if (existsSync(flatSummary)) diskTaskSummary = flatSummary;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
223
|
if (!diskTaskSummary || !existsSync(diskTaskSummary)) continue;
|
|
232
224
|
addUniqueDrift(drifts, seen, {
|
|
233
225
|
kind: "artifact-db-status-divergence",
|
|
@@ -20,11 +20,9 @@ import {
|
|
|
20
20
|
} from "../../gsd-db.js";
|
|
21
21
|
import {
|
|
22
22
|
resolveMilestoneFile,
|
|
23
|
-
resolveMilestonePath,
|
|
24
23
|
resolveSliceFile,
|
|
25
24
|
resolveTaskFile,
|
|
26
25
|
} from "../../paths.js";
|
|
27
|
-
import { join } from "node:path";
|
|
28
26
|
import type { GSDState } from "../../types.js";
|
|
29
27
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
30
28
|
|
|
@@ -105,23 +103,13 @@ function collectMilestoneCompletionDrift(
|
|
|
105
103
|
for (const task of getSliceTasks(mid, slice.id)) {
|
|
106
104
|
if (!COMPLETE_STATUSES.has(task.status)) continue;
|
|
107
105
|
if (task.completed_at !== null) continue;
|
|
108
|
-
|
|
109
|
-
// Legacy: resolveTaskFile returns the tasks/TID/TID-SUMMARY.md path.
|
|
110
|
-
let taskSummary = resolveTaskFile(
|
|
106
|
+
const taskSummary = resolveTaskFile(
|
|
111
107
|
ctx.basePath,
|
|
112
108
|
mid,
|
|
113
109
|
slice.id,
|
|
114
110
|
task.id,
|
|
115
111
|
"SUMMARY",
|
|
116
112
|
);
|
|
117
|
-
// Flat-phase fallback: check the phase dir directly
|
|
118
|
-
if (!taskSummary) {
|
|
119
|
-
const phaseDir = resolveMilestonePath(ctx.basePath, mid);
|
|
120
|
-
if (phaseDir) {
|
|
121
|
-
const flatSummary = join(phaseDir, `${task.id}-SUMMARY.md`);
|
|
122
|
-
if (existsSync(flatSummary)) taskSummary = flatSummary;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
113
|
if (taskSummary && existsSync(taskSummary)) {
|
|
126
114
|
drifts.push({
|
|
127
115
|
kind: "missing-completion-timestamp",
|
|
@@ -182,15 +170,7 @@ export function repairMissingCompletionTimestamp(
|
|
|
182
170
|
task.completed_at !== null ||
|
|
183
171
|
!COMPLETE_STATUSES.has(task.status)
|
|
184
172
|
) return;
|
|
185
|
-
|
|
186
|
-
let summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
|
|
187
|
-
if (!summary) {
|
|
188
|
-
const phaseDir = resolveMilestonePath(ctx.basePath, mid);
|
|
189
|
-
if (phaseDir) {
|
|
190
|
-
const flatSummary = join(phaseDir, `${tid}-SUMMARY.md`);
|
|
191
|
-
if (existsSync(flatSummary)) summary = flatSummary;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
173
|
+
const summary = resolveTaskFile(ctx.basePath, mid, sid, tid, "SUMMARY");
|
|
194
174
|
const ts = summary ? summaryMtimeIso(summary) : null;
|
|
195
175
|
if (!ts) return;
|
|
196
176
|
updateTaskStatus(mid, sid, tid, task.status, ts);
|
|
@@ -73,6 +73,20 @@ export async function reconcileBeforeDispatch(
|
|
|
73
73
|
if (!deps.dryRun) {
|
|
74
74
|
const { capturePlanningCompatIfNeeded } = await import("../compat/planning-compat.js");
|
|
75
75
|
await capturePlanningCompatIfNeeded(basePath);
|
|
76
|
+
|
|
77
|
+
// Self-heal phantom projection entries (#1257): a renamed/removed phase
|
|
78
|
+
// directory leaves stale `.compat.json` projection paths that no detector
|
|
79
|
+
// ever prunes (they all skip missing files), so the marker drifts from disk
|
|
80
|
+
// reality permanently. Drop entries whose backing file is gone before the
|
|
81
|
+
// detect passes run. Gated on !dryRun to keep detection read-only.
|
|
82
|
+
const { pruneOrphanedProjectionEntries } = await import("../compat/compat-marker.js");
|
|
83
|
+
const prunedCount = pruneOrphanedProjectionEntries(basePath);
|
|
84
|
+
if (prunedCount > 0) {
|
|
85
|
+
logWarning(
|
|
86
|
+
"reconcile",
|
|
87
|
+
`pruned ${prunedCount} orphaned projection entr${prunedCount === 1 ? "y" : "ies"} from .compat.json (backing file no longer on disk)`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
@@ -797,9 +797,11 @@ test("stuck-loop: empty ring on a freshly constructed orchestrator advances norm
|
|
|
797
797
|
assert.equal(result.kind, "advanced");
|
|
798
798
|
});
|
|
799
799
|
|
|
800
|
-
test("stuck-loop:
|
|
800
|
+
test("stuck-loop: distinct units making forward progress do not block", async (t) => {
|
|
801
|
+
// Healthy forward progress visits new units every advance — no key repeats,
|
|
802
|
+
// so no oscillation/repeat rule fires even once the window is full.
|
|
801
803
|
let i = 0;
|
|
802
|
-
const sequence = ["M001/S01/A", "M001/S01/B", "M001/S01/
|
|
804
|
+
const sequence = ["M001/S01/A", "M001/S01/B", "M001/S01/C", "M001/S01/D", "M001/S01/E", "M001/S01/F"];
|
|
803
805
|
const f = makeFixture({
|
|
804
806
|
dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: sequence[i++ % sequence.length], prompt: "p" }),
|
|
805
807
|
});
|
|
@@ -811,6 +813,43 @@ test("stuck-loop: partial fill of mixed units does not block", async (t) => {
|
|
|
811
813
|
}
|
|
812
814
|
});
|
|
813
815
|
|
|
816
|
+
test("stuck-loop #1225: two-unit oscillation blocks once the window saturates", async (t) => {
|
|
817
|
+
// Reproduces the execute-task ↔ complete-slice loop: a slice gate keeps
|
|
818
|
+
// reopening the same task, so two keys alternate forever. Neither key ever
|
|
819
|
+
// saturates the window on its own, so the old matchingCount>=SIZE gate never
|
|
820
|
+
// consulted detect-stuck and the loop ran until an external kill. The window
|
|
821
|
+
// still fills, so detect-stuck's oscillation/repeat rules must now fire.
|
|
822
|
+
let i = 0;
|
|
823
|
+
const sequence: Array<{ unitType: string; unitId: string }> = [
|
|
824
|
+
{ unitType: "execute-task", unitId: "M001/S01/T01" },
|
|
825
|
+
{ unitType: "complete-slice", unitId: "M001/S01" },
|
|
826
|
+
];
|
|
827
|
+
const f = makeFixture({
|
|
828
|
+
dispatch: () => {
|
|
829
|
+
const next = sequence[i++ % sequence.length];
|
|
830
|
+
return { action: "dispatch", unitType: next.unitType, unitId: next.unitId, prompt: "p" };
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
t.after(() => f.cleanup());
|
|
834
|
+
|
|
835
|
+
let blocked: Awaited<ReturnType<typeof f.orchestrator.advance>> | undefined;
|
|
836
|
+
for (let round = 0; round < STUCK_WINDOW_SIZE; round++) {
|
|
837
|
+
const result = await f.orchestrator.advance();
|
|
838
|
+
if (result.kind === "blocked") {
|
|
839
|
+
blocked = result;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
assert.ok(blocked, "oscillation must be detected once the window saturates");
|
|
845
|
+
if (!blocked || blocked.kind !== "blocked") return;
|
|
846
|
+
assert.equal(blocked.action, "stop");
|
|
847
|
+
assert.ok(
|
|
848
|
+
blocked.reason.startsWith("stuck-loop:"),
|
|
849
|
+
`expected stuck-loop verdict, got: ${blocked.reason}`,
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
|
|
814
853
|
test("stuck-loop: ring saturated with same unit blocks with action 'stop' and stuck-loop reason", async (t) => {
|
|
815
854
|
const f = makeFixture();
|
|
816
855
|
t.after(() => f.cleanup());
|