@opengsd/gsd-core 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -123,7 +123,13 @@ function resolveTierEntry({ runtime, tier, overrides }) {
|
|
|
123
123
|
*/
|
|
124
124
|
function _resolveRuntimeTier(config, tier) {
|
|
125
125
|
return resolveTierEntry({
|
|
126
|
-
|
|
126
|
+
// #4505/#4495: TIER SELECTION follows the runtime that is actually
|
|
127
|
+
// resolving, not whatever the config file happens to say. `config.runtime`
|
|
128
|
+
// is frequently absent (the runtime is normally known from GSD_RUNTIME or
|
|
129
|
+
// the per-install marker), and reading it raw made
|
|
130
|
+
// `model_profile_overrides.<runtime>.<tier>` silently inert for every
|
|
131
|
+
// install that did not also write the key by hand.
|
|
132
|
+
runtime: resolveActiveRuntime(config),
|
|
127
133
|
tier,
|
|
128
134
|
overrides: config['model_profile_overrides'],
|
|
129
135
|
});
|
|
@@ -142,10 +148,19 @@ function _resolveRuntimeTier(config, tier) {
|
|
|
142
148
|
*
|
|
143
149
|
* This helper reads ONLY the user's override entry for the effective claude
|
|
144
150
|
* runtime and tier — never the builtin claude tier map — so an install with no
|
|
145
|
-
* override is byte-identical to before the fix.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
151
|
+
* override is byte-identical to before the fix.
|
|
152
|
+
*
|
|
153
|
+
* #4505 CHANGED how the runtime reaches here. #4192 originally passed
|
|
154
|
+
* `config['runtime']` and recorded that the value policy "must key off the
|
|
155
|
+
* config the operator wrote, NOT resolveActiveRuntime". That reading turned out
|
|
156
|
+
* to defeat #4192's own stated principle — that an explicit pin must not be
|
|
157
|
+
* silently UNPINNED. With `config.runtime` absent and GSD_RUNTIME=opencode, the
|
|
158
|
+
* config-keyed read treated the session as claude and collapsed an explicit
|
|
159
|
+
* `claude-opus-4-8` pin to the bare alias `opus`, which is a Claude-only token
|
|
160
|
+
* the actual runtime cannot spawn. The runtime is now the ACTIVE one, so the
|
|
161
|
+
* claude value policy applies exactly when claude is what is running. No test
|
|
162
|
+
* pinned the previous behaviour; the ones covering #4192's pins and the
|
|
163
|
+
* alias-native posture all still pass.
|
|
149
164
|
*
|
|
150
165
|
* Value policy mirrors the model_overrides path (#2041/#4192): an override
|
|
151
166
|
* value that maps to a current tier alias collapses to that alias
|
|
@@ -155,10 +170,12 @@ function _resolveRuntimeTier(config, tier) {
|
|
|
155
170
|
* Malformed entries (no usable `model` string) return null so the caller falls
|
|
156
171
|
* through to normal alias resolution (ADR-443 D1: invalid values fall through).
|
|
157
172
|
*/
|
|
158
|
-
function resolveClaudeTierOverrideModel(
|
|
173
|
+
function resolveClaudeTierOverrideModel(
|
|
174
|
+
// The ACTIVE runtime (#4505), not the raw config key — see docblock.
|
|
175
|
+
activeRuntime, tier, overrides) {
|
|
159
176
|
if (!tier || tier === 'inherit')
|
|
160
177
|
return null;
|
|
161
|
-
const effectiveRuntime =
|
|
178
|
+
const effectiveRuntime = activeRuntime || 'claude';
|
|
162
179
|
if (effectiveRuntime !== 'claude')
|
|
163
180
|
return null; // non-claude runtimes resolve at step 3
|
|
164
181
|
const overridesMap = overrides;
|
|
@@ -264,13 +281,18 @@ function _resetModelOverrideWarningCacheForTests() {
|
|
|
264
281
|
* `Object.hasOwn` lookup defeats `__proto__`/`constructor` lookups on the plain
|
|
265
282
|
* object literal so those reserved keys cannot return a truthy non-string.
|
|
266
283
|
*/
|
|
267
|
-
function mapClaudeOverrideForRuntime(override,
|
|
284
|
+
function mapClaudeOverrideForRuntime(override,
|
|
285
|
+
// The ACTIVE runtime (#4505). Previously the raw `config['runtime']`, which
|
|
286
|
+
// made an explicit claude pin collapse to a bare alias whenever the runtime
|
|
287
|
+
// was declared via GSD_RUNTIME or the install marker rather than the config —
|
|
288
|
+
// handing a Claude-only token to a runtime that cannot spawn it.
|
|
289
|
+
activeRuntime, agentType) {
|
|
268
290
|
// Defensive: model_overrides is typed Record<string,string> but a malformed
|
|
269
291
|
// config could surface a non-string; pass through verbatim (preserving the
|
|
270
292
|
// pre-fix no-crash behaviour) and let the downstream Agent tool reject it.
|
|
271
293
|
if (typeof override !== 'string')
|
|
272
294
|
return override;
|
|
273
|
-
const onClaude = !
|
|
295
|
+
const onClaude = !activeRuntime || activeRuntime === 'claude';
|
|
274
296
|
if (!onClaude)
|
|
275
297
|
return override;
|
|
276
298
|
// Object.hasOwn guards against __proto__/constructor returning a truthy
|
|
@@ -480,7 +502,7 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
480
502
|
? modelOverrides[agentType]
|
|
481
503
|
: undefined;
|
|
482
504
|
if (override) {
|
|
483
|
-
const mapped = mapClaudeOverrideForRuntime(override, config
|
|
505
|
+
const mapped = mapClaudeOverrideForRuntime(override, resolveActiveRuntime(config), agentType);
|
|
484
506
|
if (mapped !== null)
|
|
485
507
|
return mapped;
|
|
486
508
|
// Unmappable Claude ID — fall through to tier resolution (matches model_policy).
|
|
@@ -497,10 +519,19 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
497
519
|
const agentModels = Object.hasOwn(modelProfilesMapForModel, agentType) ? modelProfilesMapForModel[agentType] : undefined;
|
|
498
520
|
const tier = computeProfileTier(config, agentType);
|
|
499
521
|
// 2.5. model_policy preset (#49, #1133)
|
|
522
|
+
// `configRuntime` is retained ONLY as the EXPLICIT-OPT-IN signal for step 3's
|
|
523
|
+
// precedence over the omit gate (see the note there). Every actual runtime
|
|
524
|
+
// question — which tier map to read, which value policy applies — now uses
|
|
525
|
+
// the active runtime (#4505).
|
|
500
526
|
const configRuntime = config['runtime'];
|
|
527
|
+
// #4505/#4495: GSD_RUNTIME -> config.runtime -> per-install marker -> 'claude'.
|
|
528
|
+
// Never undefined, so the "no runtime declared anywhere" case still resolves
|
|
529
|
+
// 'claude' and step 3's deliberate claude skip (#1156/#2297/#4192) is
|
|
530
|
+
// unchanged for every existing install.
|
|
531
|
+
const activeRuntime = resolveActiveRuntime(config);
|
|
501
532
|
if (tier && tier !== 'inherit') {
|
|
502
|
-
const onClaude =
|
|
503
|
-
const effectiveRuntime =
|
|
533
|
+
const onClaude = activeRuntime === 'claude';
|
|
534
|
+
const effectiveRuntime = activeRuntime;
|
|
504
535
|
const mergedPolicy = config['model_policy']
|
|
505
536
|
? { ...config['model_policy'], runtime: effectiveRuntime }
|
|
506
537
|
: null;
|
|
@@ -525,8 +556,49 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
525
556
|
warnModelPolicyUnmappable(agentType, policyModel, tier);
|
|
526
557
|
}
|
|
527
558
|
}
|
|
528
|
-
// 3
|
|
529
|
-
|
|
559
|
+
// #4505: the omit DECISION is computed here, ahead of step 3, though the
|
|
560
|
+
// return still happens at step 4 below.
|
|
561
|
+
//
|
|
562
|
+
// Step 3 was previously unreachable unless the operator had written `runtime`
|
|
563
|
+
// into the config. Keying it off the ACTIVE runtime — the #4495 fix — makes it
|
|
564
|
+
// reachable for env- and marker-declared runtimes too, which newly exposes an
|
|
565
|
+
// ordering the corpus already decided, in two places that only coexisted
|
|
566
|
+
// because step 3 could not fire:
|
|
567
|
+
//
|
|
568
|
+
// #2517 `runtime:"codex"` + resolve_model_ids:"omit" -> the codex tier
|
|
569
|
+
// model. "Explicit non-Claude opt-in wins" — the operator naming a
|
|
570
|
+
// runtime in the project config outranks an omit.
|
|
571
|
+
// #4717 (user-sanctioned decision a — supersedes #2297 acceptance #4):
|
|
572
|
+
// a DETECTED runtime now constitutes that opt-in too. The identity
|
|
573
|
+
// fill in config-loader materializes GSD_RUNTIME / the per-install
|
|
574
|
+
// marker into `config.runtime` when it is empty, so a genuinely
|
|
575
|
+
// installed non-Claude runtime resolves its own tier map instead of
|
|
576
|
+
// the omit's "". The shared "omit" remains a Claude protection:
|
|
577
|
+
// marker/env = claude still ignores it (native aliases), and
|
|
578
|
+
// garbage runtime values still fail safe to "" (guard below).
|
|
579
|
+
//
|
|
580
|
+
// So the opt-in signal is the `runtime` KEY — written by the operator or
|
|
581
|
+
// materialized by the #4717 fill: step 3 reads the active runtime's tier
|
|
582
|
+
// map, but only outranks the omit gate when that key canonicalizes to a
|
|
583
|
+
// recognised non-Claude runtime.
|
|
584
|
+
const omitApplies = config['resolve_model_ids'] === 'omit'
|
|
585
|
+
&& (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(activeRuntime));
|
|
586
|
+
// CANONICALIZED, not the raw field. Comparing the raw value against the literal
|
|
587
|
+
// 'claude' made every spelling that is not exactly that string count as a
|
|
588
|
+
// non-Claude opt-in and outrank the omit gate: `runtime:"Claude"`,
|
|
589
|
+
// `runtime:"claude-code"` and even `runtime:5` each emitted a model id where
|
|
590
|
+
// origin/next returned "". That is the #2297 acceptance-#4 case this very
|
|
591
|
+
// block claims to preserve, failing OPEN. (Security review of #4505.)
|
|
592
|
+
//
|
|
593
|
+
// null covers both "not a string" and "not a runtime we recognise", and both
|
|
594
|
+
// must read as NOT an opt-in: an unrecognised value is not evidence the
|
|
595
|
+
// operator deliberately chose a non-Claude runtime, so it must not buy an
|
|
596
|
+
// escalation past an explicit omit.
|
|
597
|
+
const configRuntimeCanonical = (0, runtime_name_policy_cjs_1.canonicalizeRuntimeName)(configRuntime);
|
|
598
|
+
const explicitNonClaudeOptIn = configRuntimeCanonical !== null && configRuntimeCanonical !== 'claude';
|
|
599
|
+
// 3. Runtime-aware resolution (#2517), keyed off the ACTIVE runtime (#4505).
|
|
600
|
+
if (activeRuntime !== 'claude' && tier && tier !== 'inherit'
|
|
601
|
+
&& (explicitNonClaudeOptIn || !omitApplies)) {
|
|
530
602
|
const entry = _resolveRuntimeTier(config, tier);
|
|
531
603
|
if (entry?.model)
|
|
532
604
|
return entry.model;
|
|
@@ -541,8 +613,7 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
541
613
|
// NOTE: a non-Claude runtime that HAS a populated runtime-tier map already
|
|
542
614
|
// returned its own model id at step 3 above, before this gate — for those the
|
|
543
615
|
// explicit-project-omit honoring here is moot (step 3 wins, by #2517 design).
|
|
544
|
-
if (
|
|
545
|
-
&& (projectExplicitlySetsOmit(cwd) || !RUNTIMES_WITH_NATIVE_ALIASES.has(resolveActiveRuntime(config)))) {
|
|
616
|
+
if (omitApplies) {
|
|
546
617
|
return '';
|
|
547
618
|
}
|
|
548
619
|
// 4.5. Claude-runtime tier override (#4192 Finding 1). Sits AFTER the omit
|
|
@@ -554,10 +625,30 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
554
625
|
// `model_profile_overrides.claude.<tier>` entry for this tier — see
|
|
555
626
|
// resolveClaudeTierOverrideModel for why the builtin map stays out.
|
|
556
627
|
if (tier && tier !== 'inherit') {
|
|
557
|
-
const claudeOverrideModel = resolveClaudeTierOverrideModel(
|
|
628
|
+
const claudeOverrideModel = resolveClaudeTierOverrideModel(activeRuntime, tier, config['model_profile_overrides']);
|
|
558
629
|
if (claudeOverrideModel !== null)
|
|
559
630
|
return claudeOverrideModel;
|
|
560
631
|
}
|
|
632
|
+
// 4.75 dynamic_routing.tier_models (#4505 / #3024).
|
|
633
|
+
//
|
|
634
|
+
// Position is the documented composition, not a convenience:
|
|
635
|
+
// docs/features/dynamic-routing-with-failure-tier-escalation.md —
|
|
636
|
+
// "model_overrides always wins; dynamic_routing.tier_models[<tier>] resolves
|
|
637
|
+
// above models.<phase_type> and model_profile."
|
|
638
|
+
// So it sits BELOW model_overrides (step 1), the model_policy preset (2.5),
|
|
639
|
+
// the runtime tier map (3), the resolve_model_ids:"omit" gate (4) and the
|
|
640
|
+
// claude tier override (4.5) — and ABOVE the profile lookup (5).
|
|
641
|
+
//
|
|
642
|
+
// An earlier cut of this fix routed every call site through resolveModelForTier
|
|
643
|
+
// instead, which returns the tier model directly and therefore skipped steps 3,
|
|
644
|
+
// 4 and 4.5 entirely: with `resolve_model_ids:"omit"` and a non-Claude runtime
|
|
645
|
+
// it handed out a model id where the gate had returned "". Putting the step
|
|
646
|
+
// here keeps every higher-precedence layer reachable.
|
|
647
|
+
if (tier !== 'inherit') {
|
|
648
|
+
const routed = dynamicRoutingModel(config, agentType, 0);
|
|
649
|
+
if (routed !== null)
|
|
650
|
+
return routed;
|
|
651
|
+
}
|
|
561
652
|
// 5. Profile lookup (Claude-native default).
|
|
562
653
|
if (!agentModels) {
|
|
563
654
|
return profile === 'quality' ? 'opus'
|
|
@@ -614,6 +705,50 @@ function assertValidGranularityOverride(override, fail) {
|
|
|
614
705
|
fail(`invalid granularity '${override}' (valid: ${[...VALID_GRANULARITIES].join(', ')})`);
|
|
615
706
|
}
|
|
616
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* #4505 — the ONE implementation of `dynamic_routing.tier_models` lookup.
|
|
710
|
+
*
|
|
711
|
+
* Returns the configured model for this agent's routing tier at `attempt`, or
|
|
712
|
+
* null when dynamic routing does not apply (disabled, absent, no tier table, an
|
|
713
|
+
* agent with no default routing tier, or no entry for the tier). Both entry
|
|
714
|
+
* points call it, so the first-spawn value and the escalated value can never
|
|
715
|
+
* drift apart — the "Generative Fix Divergence" this repo names by name.
|
|
716
|
+
*
|
|
717
|
+
* `attempt` 0 means "no escalation yet", which is the documented FIRST-SPAWN
|
|
718
|
+
* case, NOT "skip dynamic routing":
|
|
719
|
+
* docs/features/dynamic-routing-with-failure-tier-escalation.md —
|
|
720
|
+
* "enabled: true — the resolver picks tier_models[default_tier] for the first
|
|
721
|
+
* spawn and escalates one tier up on orchestrator-detected soft failure."
|
|
722
|
+
*/
|
|
723
|
+
function dynamicRoutingModel(config, agentType, attempt) {
|
|
724
|
+
const dr = config['dynamic_routing'];
|
|
725
|
+
if (!dr || typeof dr !== 'object' || dr['enabled'] !== true)
|
|
726
|
+
return null;
|
|
727
|
+
const tierModels = dr['tier_models'];
|
|
728
|
+
if (!tierModels || typeof tierModels !== 'object')
|
|
729
|
+
return null;
|
|
730
|
+
const defaultTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
731
|
+
if (!defaultTier || !(VALID_AGENT_TIERS).has(defaultTier))
|
|
732
|
+
return null;
|
|
733
|
+
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
734
|
+
? dr['max_escalations']
|
|
735
|
+
: 1;
|
|
736
|
+
const escalationEnabled = dr['escalate_on_failure'] !== false;
|
|
737
|
+
const effectiveAttempt = escalationEnabled ? Math.min(attempt, maxEscalations) : 0;
|
|
738
|
+
let tier = defaultTier;
|
|
739
|
+
for (let i = 0; i < effectiveAttempt; i += 1) {
|
|
740
|
+
const next = (nextTier)(tier);
|
|
741
|
+
if (!next || next === tier)
|
|
742
|
+
break;
|
|
743
|
+
tier = next;
|
|
744
|
+
}
|
|
745
|
+
// Own-property guard: `tier_models` is a config-supplied plain object, so a
|
|
746
|
+
// prototype-chain key must not resolve an inherited member.
|
|
747
|
+
const alias = Object.hasOwn(tierModels, tier) ? tierModels[tier] : undefined;
|
|
748
|
+
if (typeof alias !== 'string' || alias.length === 0)
|
|
749
|
+
return null;
|
|
750
|
+
return alias;
|
|
751
|
+
}
|
|
617
752
|
/**
|
|
618
753
|
* #3024 — Resolve a model for a specific dynamic-routing attempt.
|
|
619
754
|
*/
|
|
@@ -629,45 +764,19 @@ function resolveModelForTier(cwd, agentType, attempt) {
|
|
|
629
764
|
? modelOverrides[agentType]
|
|
630
765
|
: undefined;
|
|
631
766
|
if (override) {
|
|
632
|
-
const mapped = mapClaudeOverrideForRuntime(override, config
|
|
767
|
+
const mapped = mapClaudeOverrideForRuntime(override, resolveActiveRuntime(config), agentType);
|
|
633
768
|
if (mapped !== null)
|
|
634
769
|
return mapped;
|
|
635
770
|
// Unmappable Claude ID — fall through to dynamic_routing / model_policy resolution.
|
|
636
771
|
}
|
|
637
|
-
|
|
772
|
+
// #4505: same active-runtime rule as resolveModelInternal step 3.
|
|
773
|
+
if (config['model_policy'] && resolveActiveRuntime(config) !== 'claude') {
|
|
638
774
|
return resolveModelInternal(cwd, agentType);
|
|
639
775
|
}
|
|
640
|
-
const
|
|
641
|
-
if (
|
|
642
|
-
return
|
|
643
|
-
|
|
644
|
-
const tierModels = dr['tier_models'];
|
|
645
|
-
if (!tierModels || typeof tierModels !== 'object') {
|
|
646
|
-
return resolveModelInternal(cwd, agentType);
|
|
647
|
-
}
|
|
648
|
-
const defaultTier = (AGENT_DEFAULT_TIERS)[agentType];
|
|
649
|
-
if (!defaultTier || !(VALID_AGENT_TIERS).has(defaultTier)) {
|
|
650
|
-
return resolveModelInternal(cwd, agentType);
|
|
651
|
-
}
|
|
652
|
-
const maxEscalations = Number.isInteger(dr['max_escalations']) && dr['max_escalations'] >= 0
|
|
653
|
-
? dr['max_escalations']
|
|
654
|
-
: 1;
|
|
655
|
-
const escalationEnabled = dr['escalate_on_failure'] !== false;
|
|
656
|
-
const effectiveAttempt = escalationEnabled
|
|
657
|
-
? Math.min(attemptN, maxEscalations)
|
|
658
|
-
: 0;
|
|
659
|
-
let tier = defaultTier;
|
|
660
|
-
for (let i = 0; i < effectiveAttempt; i += 1) {
|
|
661
|
-
const next = (nextTier)(tier);
|
|
662
|
-
if (!next || next === tier)
|
|
663
|
-
break;
|
|
664
|
-
tier = next;
|
|
665
|
-
}
|
|
666
|
-
const alias = tierModels[tier];
|
|
667
|
-
if (typeof alias !== 'string' || alias.length === 0) {
|
|
668
|
-
return resolveModelInternal(cwd, agentType);
|
|
669
|
-
}
|
|
670
|
-
return alias;
|
|
776
|
+
const alias = dynamicRoutingModel(config, agentType, attemptN);
|
|
777
|
+
if (alias !== null)
|
|
778
|
+
return alias;
|
|
779
|
+
return resolveModelInternal(cwd, agentType);
|
|
671
780
|
}
|
|
672
781
|
/**
|
|
673
782
|
* Keep only usable model ids: non-empty strings. A malformed config can put
|
|
@@ -186,11 +186,16 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
186
186
|
},
|
|
187
187
|
'uat-passed': (_ctx) => {
|
|
188
188
|
let requireVerification = false;
|
|
189
|
+
let uatOnly = false;
|
|
189
190
|
const positional = [];
|
|
190
191
|
for (const token of args.slice(2)) {
|
|
191
192
|
if (token === '--require-verification') {
|
|
192
193
|
requireVerification = true;
|
|
193
194
|
}
|
|
195
|
+
else if (token === '--uat-only') {
|
|
196
|
+
// #4663: evaluate UAT rows only (verification-status blockers skipped).
|
|
197
|
+
uatOnly = true;
|
|
198
|
+
}
|
|
194
199
|
else if (token === '--raw') {
|
|
195
200
|
// --raw is handled by the outer CLI layer; accepted here silently
|
|
196
201
|
}
|
|
@@ -201,7 +206,10 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
|
|
201
206
|
positional.push(token);
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
|
-
|
|
209
|
+
if (requireVerification && uatOnly) {
|
|
210
|
+
return makeInvalidArgs('--uat-only', '--uat-only and --require-verification are mutually exclusive');
|
|
211
|
+
}
|
|
212
|
+
phase.cmdPhaseUatPassed(cwd, positional[0], raw, { policy: { requireVerification, uatOnly } });
|
|
205
213
|
return { ok: true, data: null };
|
|
206
214
|
},
|
|
207
215
|
// #1437 — list plan files for a phase
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical bracket convention card (#3638 / ADR-612 PR-5).
|
|
4
|
+
*
|
|
5
|
+
* This module is the sole editable source of the compact grammar diagram.
|
|
6
|
+
* Render sites import phaseIdCard(); generated documentation is held to the
|
|
7
|
+
* same bytes by tests/phase-id-card.test.cjs. PR-6 can therefore inject the
|
|
8
|
+
* card broadly without inventing another copy.
|
|
9
|
+
*/
|
|
10
|
+
const PHASE_ID_CARD = [
|
|
11
|
+
'[GSD.02] 05.03-01',
|
|
12
|
+
' │ │ │ │ │',
|
|
13
|
+
' │ │ │ │ └── plan 01',
|
|
14
|
+
' │ │ │ └────── subphase 03',
|
|
15
|
+
' │ │ └───────── phase 05',
|
|
16
|
+
' │ └───────────── milestone 02',
|
|
17
|
+
' └───────────────── project GSD',
|
|
18
|
+
].join('\n');
|
|
19
|
+
const PHASE_ID_LEGEND = "milestone = bracket integer; dots = phase-levels; one hyphen = plan; "
|
|
20
|
+
+ "no 'Phase' word, no vX.Y";
|
|
21
|
+
function phaseIdCard(options = {}) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
if (options.title)
|
|
24
|
+
parts.push(options.title, '');
|
|
25
|
+
parts.push(PHASE_ID_CARD, '', PHASE_ID_LEGEND);
|
|
26
|
+
return parts.join('\n');
|
|
27
|
+
}
|
|
28
|
+
module.exports = {
|
|
29
|
+
phaseIdCard,
|
|
30
|
+
PHASE_ID_CARD,
|
|
31
|
+
PHASE_ID_LEGEND,
|
|
32
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase-ID display adapters (#3638 / ADR-612 PR-5).
|
|
4
|
+
*
|
|
5
|
+
* STATE.md and milestone metadata still expose the legacy `vN.0` marker on
|
|
6
|
+
* this stacked base. Display surfaces need to translate that metadata into a
|
|
7
|
+
* bracket identity without growing another renderer. These pure adapters do
|
|
8
|
+
* only the boundary normalization, then delegate identity parsing/rendering to
|
|
9
|
+
* phase-id.cts's canonical pair.
|
|
10
|
+
*/
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
|
+
const phaseId = require("./phase-id.cjs");
|
|
13
|
+
const { parsePhaseId, renderMilestoneId, renderPhaseId } = phaseId;
|
|
14
|
+
function canonicalNumeric(value) {
|
|
15
|
+
let offset = 0;
|
|
16
|
+
while (offset < value.length - 1 && value[offset] === '0')
|
|
17
|
+
offset++;
|
|
18
|
+
const stripped = value.slice(offset);
|
|
19
|
+
return stripped.length < 2 ? stripped.padStart(2, '0') : stripped;
|
|
20
|
+
}
|
|
21
|
+
function isDigits(value) {
|
|
22
|
+
return value.length > 0
|
|
23
|
+
&& [...value].every(char => char >= '0' && char <= '9');
|
|
24
|
+
}
|
|
25
|
+
function milestoneToken(value) {
|
|
26
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
27
|
+
return null;
|
|
28
|
+
const raw = String(value).replace(/^v/i, '');
|
|
29
|
+
const parts = raw.split('.');
|
|
30
|
+
if (parts.length > 2 || parts.some(part => !isDigits(part)))
|
|
31
|
+
return null;
|
|
32
|
+
return canonicalNumeric(parts[0]);
|
|
33
|
+
}
|
|
34
|
+
function phaseToken(value) {
|
|
35
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
36
|
+
return null;
|
|
37
|
+
const raw = String(value);
|
|
38
|
+
const parts = raw.split('.');
|
|
39
|
+
if (parts.length > 2 || parts.some(part => !isDigits(part)))
|
|
40
|
+
return null;
|
|
41
|
+
return parts.map(canonicalNumeric).join('.');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render a bracket phase display from legacy milestone metadata.
|
|
45
|
+
* Returns null for incomplete or invalid metadata so cosmetic callers can
|
|
46
|
+
* degrade without breaking a command/statusline render.
|
|
47
|
+
*/
|
|
48
|
+
function renderBracketPhaseDisplay(milestone, phase, projectCode) {
|
|
49
|
+
const project = typeof projectCode === 'string' ? projectCode : '';
|
|
50
|
+
const mm = milestoneToken(milestone);
|
|
51
|
+
const pp = phaseToken(phase);
|
|
52
|
+
if (!project || mm === null || pp === null)
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
return renderPhaseId(parsePhaseId(`${project}.${mm}-${pp}`));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Render only the bracket milestone label, still through canonical parsing. */
|
|
62
|
+
function renderBracketMilestoneDisplay(milestone, projectCode) {
|
|
63
|
+
const project = typeof projectCode === 'string' ? projectCode : '';
|
|
64
|
+
const mm = milestoneToken(milestone);
|
|
65
|
+
if (!project || mm === null)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
// The sentinel phase is a validation vehicle, not a rendering artifact.
|
|
69
|
+
return renderMilestoneId(parsePhaseId(`${project}.${mm}-00`));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
module.exports = {
|
|
76
|
+
renderBracketPhaseDisplay,
|
|
77
|
+
renderBracketMilestoneDisplay,
|
|
78
|
+
};
|
|
@@ -59,6 +59,20 @@ const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]{0,200}\\))?';
|
|
|
59
59
|
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
|
60
60
|
// introduced outside this module without a `// phase-id-owner:` justification.
|
|
61
61
|
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
|
62
|
+
// #4764: a phase REFERENCE in depends-on PROSE — the token in context, directly
|
|
63
|
+
// following "Phase"/"Phases", with bare-token list continuation ("Phases 1 and 2",
|
|
64
|
+
// "Phase 1, 2, and 3", "Phase 1-3"). Built HERE, beside the token grammar it is
|
|
65
|
+
// anchored on, because two readers consume Depends-on prose (init.manager's
|
|
66
|
+
// dep_phases and planning-inspect's dependencies) and re-deriving the anchored
|
|
67
|
+
// form at each would drift exactly the way PHASE_NUMBER_TOKEN_SOURCE's own
|
|
68
|
+
// anti-divergence rule exists to prevent. A bare digit run is the right shape
|
|
69
|
+
// test for a phase id and the WRONG test for a reference: the whole-field
|
|
70
|
+
// scrape this replaces pulled calendar dates ("2026-09-14" → 2026/09/14), git
|
|
71
|
+
// shas ("8bf403100d" → 8b/403100d/…) and ledger ids (WINDOWS #1843) in as
|
|
72
|
+
// dependencies. The `-` separator deliberately extracts range ENDPOINTS only
|
|
73
|
+
// ("Phase 1-3" → 1, 3) — the pre-#4764 behavior; interior enumeration stays
|
|
74
|
+
// out (a range's middle is not written as a reference).
|
|
75
|
+
const PHASE_DEP_REF_SOURCE = `\\bphases?\\s+(${PHASE_NUMBER_TOKEN_SOURCE}(?:(?:\\s*,\\s*(?:and\\s+)?|\\s+and\\s+|\\s*&\\s*|\\s+(?:to|through)\\s+|\\s*-\\s*)${PHASE_NUMBER_TOKEN_SOURCE})*)`;
|
|
62
76
|
// #2528 review: the CASE-FLEXIBLE renderings of the two sources above, for call
|
|
63
77
|
// sites that scan directory names (where a project code or a variant suffix may
|
|
64
78
|
// legitimately be lowercase) and therefore cannot use a case-sensitive class.
|
|
@@ -308,6 +322,30 @@ function phaseHeadingPrefixSrcFor(baseline, convention, capturing = false) {
|
|
|
308
322
|
const bracketAlt = `\\[${id}\\][ \\t]*(?:Phase\\s+|(?=\\d))`;
|
|
309
323
|
return `(?:${bracketAlt}|${base})`;
|
|
310
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* The single owner of the "scan a whole document for every phase heading"
|
|
327
|
+
* pattern (#4865 / ADR-4910 §8) — anchored (`^ {0,3}#{2,4}`, so indentation up
|
|
328
|
+
* to 3 spaces is tolerated but a heading is never matched mid-line), global,
|
|
329
|
+
* multiline, convention-aware via `phaseHeadingPrefixSrcFor`. Returns the
|
|
330
|
+
* capture-group layout alongside the regex so a caller never re-derives the
|
|
331
|
+
* bracket-convention offset (`const G = convention === 'bracket' ? 1 : 0`)
|
|
332
|
+
* itself — a second, independently-computed offset is exactly the kind of
|
|
333
|
+
* drift `scripts/lint-phase-id-drift.cjs` exists to catch structurally, not
|
|
334
|
+
* just by pattern text.
|
|
335
|
+
*
|
|
336
|
+
* Distinct from `buildPhaseHeadingRegex` (`src/roadmap.cts`), which searches
|
|
337
|
+
* for ONE already-known phase number (anchored `^`, no `g` flag, phase number
|
|
338
|
+
* interpolated literally) — a different contract for a different question.
|
|
339
|
+
* This function answers "which phases exist in this content", not "does this
|
|
340
|
+
* specific phase exist".
|
|
341
|
+
*/
|
|
342
|
+
function buildPhaseHeadingScanRegex(baseline, convention) {
|
|
343
|
+
const bracketGroup = convention === 'bracket' ? 1 : null;
|
|
344
|
+
const phaseNumGroup = bracketGroup ? 2 : 1;
|
|
345
|
+
const phaseNameGroup = phaseNumGroup + 1;
|
|
346
|
+
const regex = new RegExp(`^ {0,3}#{2,4}\\s*${phaseHeadingPrefixSrcFor(baseline, convention, true)}(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}:\\s*([^\\n]+)`, 'gim');
|
|
347
|
+
return { regex, bracketGroup, phaseNumGroup, phaseNameGroup };
|
|
348
|
+
}
|
|
311
349
|
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
312
350
|
const input = String(value);
|
|
313
351
|
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
@@ -451,10 +489,27 @@ function parsePhaseId(input) {
|
|
|
451
489
|
// tokens unchanged.
|
|
452
490
|
throw new Error(`parsePhaseId: not a bracket phase id: ${JSON.stringify(input)}`);
|
|
453
491
|
}
|
|
492
|
+
function renderMilestoneId(id) {
|
|
493
|
+
return `[${id.project}.${id.milestone}]`;
|
|
494
|
+
}
|
|
454
495
|
function renderPhaseId(id) {
|
|
455
496
|
const sub = id.subphase ? `.${id.subphase}` : '';
|
|
456
497
|
const plan = id.plan ? `-${id.plan}` : '';
|
|
457
|
-
return
|
|
498
|
+
return `${renderMilestoneId(id)} ${id.phase}${sub}${plan}`;
|
|
499
|
+
}
|
|
500
|
+
/** Whether a legacy phase token has a lossless spelling in bracket display grammar. */
|
|
501
|
+
function isBracketPhaseTokenRepresentable(token) {
|
|
502
|
+
try {
|
|
503
|
+
const bracketToken = normalizePhaseName(token)
|
|
504
|
+
.split('.')
|
|
505
|
+
.map((segment) => segment.padStart(2, '0'))
|
|
506
|
+
.join('.');
|
|
507
|
+
parsePhaseId(`[GSD.00] ${bracketToken}`);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
458
513
|
}
|
|
459
514
|
// PhaseId is a structural type: nothing forces a caller through parsePhaseId,
|
|
460
515
|
// so toDir cannot trust project/milestone/phase/subphase are already
|
|
@@ -1017,7 +1072,7 @@ function isPhaseArtifact(fileName, phaseDirName, convention) {
|
|
|
1017
1072
|
* whichever path produced the candidates, which is what makes a bracket dir
|
|
1018
1073
|
* read exactly what its legacy twin reads.
|
|
1019
1074
|
*/
|
|
1020
|
-
function
|
|
1075
|
+
function expandPhaseTokenCandidates(rawCandidates) {
|
|
1021
1076
|
// Each reading is compared in BOTH its padded and de-padded form: files are
|
|
1022
1077
|
// written padded by `normalizePhaseName` (`cmdScaffold`) while directories
|
|
1023
1078
|
// are often not (`1-unpadded`), and legacy trees carry the reverse pairing.
|
|
@@ -1025,9 +1080,19 @@ function matchesPhaseTokenCandidates(fileName, rawCandidates) {
|
|
|
1025
1080
|
// sub-phase (`03A`, `03.1`) has no meaningful de-padded form and is left
|
|
1026
1081
|
// alone, so this only ever ADDS a reading and can never drop one.
|
|
1027
1082
|
const depad = (t) => (/^\d+$/.test(t) ? String(Number(t)) : t);
|
|
1028
|
-
|
|
1083
|
+
return new Set(rawCandidates
|
|
1029
1084
|
.flatMap(t => [t, normalizePhaseName(t), depad(t)])
|
|
1030
1085
|
.map(t => t.toUpperCase()));
|
|
1086
|
+
}
|
|
1087
|
+
function matchesPhaseTokenCandidates(fileName, rawCandidates) {
|
|
1088
|
+
const candidates = expandPhaseTokenCandidates(rawCandidates);
|
|
1089
|
+
if (matchPhaseTokenCandidateSpan(fileName, candidates) !== null)
|
|
1090
|
+
return true;
|
|
1091
|
+
// FIX 2: token-less filename (bare "VERIFICATION.md"/"UAT.md") — containment
|
|
1092
|
+
// in this phase's own directory listing is sufficient.
|
|
1093
|
+
return derivePhaseTokenSegments(fileName).tokenSegments.length === 0;
|
|
1094
|
+
}
|
|
1095
|
+
function matchPhaseTokenCandidateSpan(fileName, candidates) {
|
|
1031
1096
|
const fileUpper = fileName.toUpperCase();
|
|
1032
1097
|
for (const candidate of candidates) {
|
|
1033
1098
|
// A dotted sub-phase segment (e.g. `01.1-CONTEXT.md`) is a legitimate
|
|
@@ -1053,11 +1118,43 @@ function matchesPhaseTokenCandidates(fileName, rawCandidates) {
|
|
|
1053
1118
|
if (fileUpper.startsWith(`${candidate}-`) ||
|
|
1054
1119
|
fileUpper.startsWith(`${candidate}.`) ||
|
|
1055
1120
|
fileUpper.startsWith(`${candidate}_`))
|
|
1056
|
-
return
|
|
1121
|
+
return { start: 0, end: candidate.length };
|
|
1057
1122
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Return the exact leading token span by which the phase-artifact reader
|
|
1127
|
+
* attributes a phase-qualified filename to `phaseDirName`.
|
|
1128
|
+
*
|
|
1129
|
+
* Membership is first decided by `isPhaseArtifact`, then the span is selected
|
|
1130
|
+
* from the same padded, de-padded, case-folded candidate set used by
|
|
1131
|
+
* `matchesPhaseTokenCandidates`. Token-less containment fallbacks return null
|
|
1132
|
+
* because there is no phase token in the filename to replace.
|
|
1133
|
+
*/
|
|
1134
|
+
function phaseArtifactTokenSpan(fileName, phaseDirName, convention) {
|
|
1135
|
+
if (!isPhaseArtifact(fileName, phaseDirName, convention))
|
|
1136
|
+
return null;
|
|
1137
|
+
if (convention === 'bracket') {
|
|
1138
|
+
const bracketDir = phaseDirName.match(BRACKET_DIR_TOKEN_RE);
|
|
1139
|
+
if (bracketDir) {
|
|
1140
|
+
const qualified = fileName.match(BRACKET_QUALIFIED_KEY_RE);
|
|
1141
|
+
if (qualified && bracketQualifiedKey(fileName, convention) !== null) {
|
|
1142
|
+
return { start: 0, end: qualified[0].length };
|
|
1143
|
+
}
|
|
1144
|
+
const bracketCandidates = expandPhaseTokenCandidates([bracketDir[1]]);
|
|
1145
|
+
return matchPhaseTokenCandidateSpan(fileName, bracketCandidates);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const { tokenSegments } = derivePhaseTokenSegments(phaseDirName);
|
|
1149
|
+
if (tokenSegments.length === 0)
|
|
1150
|
+
return null;
|
|
1151
|
+
const literalToken = extractPhaseToken(phaseDirName);
|
|
1152
|
+
const strippedDir = stripProjectCodePrefix(phaseDirName);
|
|
1153
|
+
const strippedToken = strippedDir !== phaseDirName ? extractPhaseToken(strippedDir) : literalToken;
|
|
1154
|
+
const leadingRunMatch = strippedDir.match(LEADING_DIGIT_RUN_RE);
|
|
1155
|
+
const rawCandidates = [literalToken, strippedToken, leadingRunMatch?.[1]].filter((token) => Boolean(token));
|
|
1156
|
+
const candidates = expandPhaseTokenCandidates(rawCandidates);
|
|
1157
|
+
return matchPhaseTokenCandidateSpan(fileName, candidates);
|
|
1061
1158
|
}
|
|
1062
1159
|
/**
|
|
1063
1160
|
* #3511: scope `fileNames` to the subset that passes
|
|
@@ -1538,6 +1635,7 @@ module.exports = {
|
|
|
1538
1635
|
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
1539
1636
|
OPTIONAL_PHASE_TAG_SOURCE,
|
|
1540
1637
|
PHASE_NUMBER_TOKEN_SOURCE,
|
|
1638
|
+
PHASE_DEP_REF_SOURCE,
|
|
1541
1639
|
CASE_FLEXIBLE_PROJECT_CODE_PREFIX_SOURCE,
|
|
1542
1640
|
CASE_FLEXIBLE_PHASE_NUMBER_TOKEN_SOURCE,
|
|
1543
1641
|
PHASE_CONTINUATION_SEGMENT_SOURCE,
|
|
@@ -1554,6 +1652,7 @@ module.exports = {
|
|
|
1554
1652
|
BASE_PHASE_LABEL_PREFIX_SRC,
|
|
1555
1653
|
PHASE_HEADING_BASELINE,
|
|
1556
1654
|
phaseHeadingPrefixSrcFor,
|
|
1655
|
+
buildPhaseHeadingScanRegex,
|
|
1557
1656
|
foldBracketId,
|
|
1558
1657
|
bracketQualifiedKey,
|
|
1559
1658
|
stripProjectCodePrefix,
|
|
@@ -1561,7 +1660,9 @@ module.exports = {
|
|
|
1561
1660
|
getMilestoneFromPhaseId,
|
|
1562
1661
|
getPhaseDirFromPhaseId,
|
|
1563
1662
|
parsePhaseId,
|
|
1663
|
+
renderMilestoneId,
|
|
1564
1664
|
renderPhaseId,
|
|
1665
|
+
isBracketPhaseTokenRepresentable,
|
|
1565
1666
|
toDir,
|
|
1566
1667
|
SENTINEL_RANGES,
|
|
1567
1668
|
isSentinelPhaseId,
|
|
@@ -1571,6 +1672,7 @@ module.exports = {
|
|
|
1571
1672
|
comparePhaseNum,
|
|
1572
1673
|
extractPhaseToken,
|
|
1573
1674
|
isPhaseArtifact,
|
|
1675
|
+
phaseArtifactTokenSpan,
|
|
1574
1676
|
scopeToPhase,
|
|
1575
1677
|
phaseTokenMatches,
|
|
1576
1678
|
matchPhaseDirs,
|