@opengsd/gsd-core 1.11.0 → 1.12.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/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -9
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +15 -55
- package/agents/gsd-planner.md +6 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +160 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +3 -2
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +3 -0
- package/gsd-core/templates/state.md +7 -0
- 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 +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
- 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 +22 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +34 -16
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +134 -1
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +122 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -68,7 +68,12 @@
|
|
|
68
68
|
* gaps_found-only, never call on the pass path
|
|
69
69
|
*
|
|
70
70
|
* Milestone Operations:
|
|
71
|
-
* milestone complete <version>
|
|
71
|
+
* milestone complete <version> (--confirm | --dry-run)
|
|
72
|
+
* Archive milestone, create MILESTONES.md — one of the two is required
|
|
73
|
+
* --confirm REQUIRED to mutate (#3726): the archive is irreversible (ROADMAP/
|
|
74
|
+
* REQUIREMENTS archived, phase dirs MOVED, STATE.md rewritten), so
|
|
75
|
+
* without this flag the command refuses and mutates nothing
|
|
76
|
+
* --dry-run Preview what would move, mutates nothing (no --confirm needed; #2118)
|
|
72
77
|
* [--name <name>]
|
|
73
78
|
* [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
|
|
74
79
|
* [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
@@ -98,6 +103,15 @@
|
|
|
98
103
|
* validate health [--repair] Check .planning/ integrity, optionally repair
|
|
99
104
|
* validate agents Check GSD agent installation status
|
|
100
105
|
*
|
|
106
|
+
* Planning Snapshot:
|
|
107
|
+
* planning inspect Read-only schema-v1 canonical planning snapshot
|
|
108
|
+
* (milestone identity, active phase, per-phase
|
|
109
|
+
* verification/roadmap-acceptance/UAT evidence kept
|
|
110
|
+
* separate, requirement rows with mapped-phase
|
|
111
|
+
* traceability, plan/task rows with planned+changed
|
|
112
|
+
* file provenance, and independent accepted_phases /
|
|
113
|
+
* completed_plans fractions). Takes no arguments.
|
|
114
|
+
*
|
|
101
115
|
* Progress:
|
|
102
116
|
* progress [json|table|bar] Render progress in various formats
|
|
103
117
|
*
|
|
@@ -240,13 +254,22 @@ try {
|
|
|
240
254
|
process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
|
|
241
255
|
// Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
|
|
242
256
|
// lives in ./lib) is available to load, so a direct exit is the only option.
|
|
243
|
-
//
|
|
257
|
+
// #3910: this call runs BEFORE ./lib/cli-exit.cjs is even required, so the
|
|
258
|
+
// registered-exit seam (runMain/ExitError/terminateNow) does not exist yet
|
|
259
|
+
// at this point in the process's lifetime — there is nothing to route
|
|
260
|
+
// through. This is the second (and only other) sanctioned allowlist entry
|
|
261
|
+
// for local/require-registered-exit, alongside terminateNow's own body.
|
|
262
|
+
// #3914: n/no-process-exit and local/require-registered-exit are
|
|
263
|
+
// complementary, not predecessor/successor (see eslint.config.mjs and
|
|
264
|
+
// docs/adr/3889-process-exit-contract.md) — both remain 'error' on this
|
|
265
|
+
// glob, so both need a disable directive here.
|
|
266
|
+
// eslint-disable-next-line n/no-process-exit, local/require-registered-exit
|
|
244
267
|
process.exit(1);
|
|
245
268
|
}
|
|
246
269
|
|
|
247
|
-
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
270
|
+
const { ExitError, runMain, resolveContractVersion } = require('./lib/cli-exit.cjs');
|
|
248
271
|
const io = require('./lib/io.cjs');
|
|
249
|
-
const { error, ERROR_REASON, setJsonErrorMode, output } = io;
|
|
272
|
+
const { error, ERROR_REASON, setJsonErrorMode, output, formatDiagnosticToken } = io;
|
|
250
273
|
const projectRoot = require('./lib/project-root.cjs');
|
|
251
274
|
// Resolve findProjectRoot lazily at call time rather than binding it at module
|
|
252
275
|
// load. It is sourced from project-root.cjs; a call-time lookup is robust
|
|
@@ -286,6 +309,7 @@ const estimateCli = require('./lib/estimate-cli.cjs');
|
|
|
286
309
|
const template = require('./lib/template.cjs');
|
|
287
310
|
const milestone = require('./lib/milestone.cjs');
|
|
288
311
|
const commands = require('./lib/commands.cjs');
|
|
312
|
+
const runtimeIdentity = require('./lib/runtime-identity.cjs');
|
|
289
313
|
const init = require('./lib/init.cjs');
|
|
290
314
|
const frontmatter = require('./lib/frontmatter.cjs');
|
|
291
315
|
const workstream = require('./lib/workstream.cjs');
|
|
@@ -297,6 +321,7 @@ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
|
|
|
297
321
|
const { routeEvalCommand } = require('./lib/eval-command-router.cjs');
|
|
298
322
|
const evalMod = require('./lib/eval.cjs');
|
|
299
323
|
const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
|
|
324
|
+
const { routePlanningCommand } = require('./lib/planning-command-router.cjs');
|
|
300
325
|
const verification = require('./lib/verification.cjs');
|
|
301
326
|
const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
|
302
327
|
// Stale-bake guard (#1688): warns once when model config changed since agents
|
|
@@ -314,7 +339,7 @@ const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-comman
|
|
|
314
339
|
const smartEntryMod = require('./lib/smart-entry.cjs');
|
|
315
340
|
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
316
341
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
317
|
-
const {
|
|
342
|
+
const { parseNamedArgsOrExit, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
|
|
318
343
|
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
|
319
344
|
const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
|
|
320
345
|
|
|
@@ -952,7 +977,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
952
977
|
|
|
953
978
|
function routePrSubrepo({ args, cwd, raw, error }) {
|
|
954
979
|
const message = args[1];
|
|
955
|
-
|
|
980
|
+
// #3884: the commit message is an optional leading positional the
|
|
981
|
+
// caller owns (args[1]) — but when it is OMITTED, args[1] is itself
|
|
982
|
+
// the first flag (e.g. `--repo`), and a static `positionals: 2`
|
|
983
|
+
// treats that flag's own value as an unexpected trailing positional
|
|
984
|
+
// before cmdPrSubrepo's own "commit message required" guard ever
|
|
985
|
+
// runs. Widen the boundary only when args[1] genuinely looks like a
|
|
986
|
+
// message (not flag-shaped), mirroring the same fix applied to
|
|
987
|
+
// `state complete-phase`.
|
|
988
|
+
const messagePresent = message !== undefined && !message.startsWith('--');
|
|
989
|
+
const { repo, branch } = parseNamedArgsOrExit(args, { valueFlags: ['repo', 'branch'], positionals: messagePresent ? 2 : 1 }, error);
|
|
956
990
|
commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
|
|
957
991
|
}
|
|
958
992
|
|
|
@@ -969,7 +1003,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
969
1003
|
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
970
1004
|
} else if (subcommand === 'fill') {
|
|
971
1005
|
const templateType = args[2];
|
|
972
|
-
const { phase, plan, name, type, wave, fields: fieldsRaw } =
|
|
1006
|
+
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgsOrExit(args, { valueFlags: ['phase', 'plan', 'name', 'type', 'wave', 'fields'], positionals: 3 }, error);
|
|
973
1007
|
let fields = {};
|
|
974
1008
|
if (fieldsRaw) {
|
|
975
1009
|
const { safeJsonParse } = require('./lib/security.cjs');
|
|
@@ -1018,14 +1052,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1018
1052
|
}
|
|
1019
1053
|
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
1020
1054
|
if (subcommand === 'get') {
|
|
1021
|
-
frontmatter.cmdFrontmatterGet(cwd, file,
|
|
1055
|
+
frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['field'], positionals: 3 }, error).field, raw);
|
|
1022
1056
|
} else if (subcommand === 'set') {
|
|
1023
|
-
const { field, value } =
|
|
1057
|
+
const { field, value } = parseNamedArgsOrExit(args, { valueFlags: ['field', 'value'], positionals: 3 }, error);
|
|
1024
1058
|
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
1025
1059
|
} else if (subcommand === 'merge') {
|
|
1026
|
-
frontmatter.cmdFrontmatterMerge(cwd, file,
|
|
1060
|
+
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['data'], positionals: 3 }, error).data, raw);
|
|
1027
1061
|
} else if (subcommand === 'validate') {
|
|
1028
|
-
frontmatter.cmdFrontmatterValidate(cwd, file,
|
|
1062
|
+
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['schema'], positionals: 3 }, error).schema, raw);
|
|
1029
1063
|
} else {
|
|
1030
1064
|
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1031
1065
|
}
|
|
@@ -1069,6 +1103,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1069
1103
|
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1070
1104
|
}
|
|
1071
1105
|
|
|
1106
|
+
function routeRuntimeIdentity({ raw }) {
|
|
1107
|
+
// #3146: report this runtime's package identity so a shipped workflow can
|
|
1108
|
+
// tell whether it reached THIS package's gsd-tools or a colliding one.
|
|
1109
|
+
// Kept on the CJS fast path for the same reason as current-timestamp — the
|
|
1110
|
+
// launcher preamble spawns it once per workflow run, so SDK bridge startup
|
|
1111
|
+
// would be a per-run tax on every workflow.
|
|
1112
|
+
runtimeIdentity.cmdRuntimeIdentity(raw);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1072
1115
|
function routeSkillsRoot({ args, raw, error }) {
|
|
1073
1116
|
// #3024: resolve the global skills base directory for a runtime.
|
|
1074
1117
|
// The sync-skills workflow previously shelled out to install.js --skills-root,
|
|
@@ -1140,10 +1183,38 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1140
1183
|
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
|
1141
1184
|
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
|
1142
1185
|
const qtaArgs = args.slice(1);
|
|
1143
|
-
|
|
1186
|
+
// Ambiguous boundary (ADR-3473 §8.4 Item 2 note): this command accepts
|
|
1187
|
+
// EITHER a positional free-text description (qtaArgs[0]) OR --task
|
|
1188
|
+
// <value> — the same token index is a caller-owned positional in one
|
|
1189
|
+
// input shape and a flag in the other, which a single fixed
|
|
1190
|
+
// `positionals` cursor cannot represent. `positionals: 'rest'`
|
|
1191
|
+
// disables the boundary walk (as with `init quick`) so extraction
|
|
1192
|
+
// (used for the --task form) and the `|| args[1]` fallback (used for
|
|
1193
|
+
// the positional form) both keep working unchanged.
|
|
1194
|
+
// #3356 defect 1: `--quick-id` / `--slug` / `--directory` are
|
|
1195
|
+
// OPTIONAL widenings. A caller with no quick id or task directory
|
|
1196
|
+
// (fast.md, the original #2133 caller) omits them and keeps the
|
|
1197
|
+
// exact prior ordinal-`#`/`'—'`-Directory row. A caller that DOES
|
|
1198
|
+
// have a real quick id + task dir (i.e. can match `workflows/
|
|
1199
|
+
// quick.md`'s own Step 7c row for the same inputs) supplies them
|
|
1200
|
+
// and gets the byte-equivalent canonical row `quick.md:632`
|
|
1201
|
+
// documents — closing the false-equivalence gap `quick.md:627`
|
|
1202
|
+
// claims. `--directory` wins outright when given explicitly;
|
|
1203
|
+
// otherwise a supplied `--quick-id` + `--slug` pair derives the
|
|
1204
|
+
// canonical permalink the same way `workflows/quick.md` renders it.
|
|
1205
|
+
const qtaParsed = parseNamedArgsOrExit(
|
|
1206
|
+
qtaArgs,
|
|
1207
|
+
{ valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
|
|
1208
|
+
error,
|
|
1209
|
+
);
|
|
1210
|
+
const qtaTask = qtaParsed.task || args[1];
|
|
1144
1211
|
if (!qtaTask) {
|
|
1145
1212
|
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
|
1146
1213
|
}
|
|
1214
|
+
const qtaQuickId = qtaParsed['quick-id'] || undefined;
|
|
1215
|
+
const qtaSlug = qtaParsed['slug'] || undefined;
|
|
1216
|
+
const qtaDirectory = qtaParsed['directory']
|
|
1217
|
+
|| (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
|
|
1147
1218
|
|
|
1148
1219
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1149
1220
|
if (!fs.existsSync(statePath)) {
|
|
@@ -1169,8 +1240,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1169
1240
|
// still releases the lock before the throw propagates; the transform
|
|
1170
1241
|
// throws before returning new content, so nothing is ever written).
|
|
1171
1242
|
let mutation;
|
|
1243
|
+
// #3356 defect 2: this write touches only the Quick Tasks body
|
|
1244
|
+
// table — a single appended row — so it must not trigger the
|
|
1245
|
+
// default full re-derive of the disk-derived `progress.*`
|
|
1246
|
+
// frontmatter block. `{ resync: false }` mirrors every other
|
|
1247
|
+
// body-only STATE.md writer's convention (src/state.cts's own
|
|
1248
|
+
// docstring on `readModifyWriteStateMd` prescribes it); this route
|
|
1249
|
+
// was the lone outlier still passing no options at all.
|
|
1172
1250
|
state.readModifyWriteStateMd(statePath, (content) => {
|
|
1173
|
-
const result = appendQuickTaskRow(content, {
|
|
1251
|
+
const result = appendQuickTaskRow(content, {
|
|
1252
|
+
description: qtaTask,
|
|
1253
|
+
date,
|
|
1254
|
+
commit,
|
|
1255
|
+
quickId: qtaQuickId,
|
|
1256
|
+
directory: qtaDirectory,
|
|
1257
|
+
});
|
|
1174
1258
|
if (!result.ok) {
|
|
1175
1259
|
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
|
1176
1260
|
// is an expected, recoverable condition (no table / unrecognized
|
|
@@ -1181,7 +1265,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1181
1265
|
}
|
|
1182
1266
|
mutation = result.value;
|
|
1183
1267
|
return result.value.content;
|
|
1184
|
-
}, cwd);
|
|
1268
|
+
}, cwd, { resync: false });
|
|
1185
1269
|
|
|
1186
1270
|
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
|
1187
1271
|
}
|
|
@@ -1669,6 +1753,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1669
1753
|
}
|
|
1670
1754
|
}
|
|
1671
1755
|
|
|
1756
|
+
/**
|
|
1757
|
+
* #3737 — strict read of the project-level worktree opt-out from
|
|
1758
|
+
* `.planning/config.json`. True ONLY when `workflow.use_worktrees` is the
|
|
1759
|
+
* boolean `false`; an absent key, unreadable/malformed config, or any
|
|
1760
|
+
* non-boolean value (including the string "false") degrades to false —
|
|
1761
|
+
* worktrees are ON by default, so the degraded answer is "not opted out".
|
|
1762
|
+
* Direct file read, deliberately NOT loadConfig: this resolver backs
|
|
1763
|
+
* sentinel writes and must never trigger config normalization/rewrites
|
|
1764
|
+
* (same discipline as resolveDispatchIsolationDecision's resolveRuntime
|
|
1765
|
+
* comment above). Never throws.
|
|
1766
|
+
*/
|
|
1767
|
+
function projectWorktreesOptedOut(cwd) {
|
|
1768
|
+
// #3972: single owner — planning-workspace's worktreesOptedOut ladder
|
|
1769
|
+
// (scoped own-key, root inheritance under the ws gate, strict === false).
|
|
1770
|
+
// Kept as a local name so routeDispatchIsolation's call sites read the
|
|
1771
|
+
// same as they did in #3938/#3963; the logic itself now lives beside
|
|
1772
|
+
// planningDir/planningRoot where every isolation surface can share it.
|
|
1773
|
+
try {
|
|
1774
|
+
const { worktreesOptedOut } = require('./lib/planning-workspace.cjs');
|
|
1775
|
+
return worktreesOptedOut(cwd);
|
|
1776
|
+
} catch {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1672
1781
|
function routeDispatchIsolation({ args, cwd, raw, error }) {
|
|
1673
1782
|
// #2584 Phase 3 (#2627): typed query exposing the negotiated
|
|
1674
1783
|
// `dispatch.isolation` to the execute-phase wave scheduler, so the
|
|
@@ -1742,6 +1851,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1742
1851
|
? args[planIdx + 1]
|
|
1743
1852
|
: null;
|
|
1744
1853
|
|
|
1854
|
+
// #3737: the project-level opt-out (workflow.use_worktrees === false) is
|
|
1855
|
+
// decided HERE, before the sentinel write — not only in the workflow
|
|
1856
|
+
// shell blocks that run after this resolve. Pre-fix, any plain re-query
|
|
1857
|
+
// (config re-read, wave transition, second plan dispatch) re-persisted
|
|
1858
|
+
// the naturally-resolved host capability over the `--force-isolation
|
|
1859
|
+
// none` record the dispatch-isolation reference mandates, and the guard
|
|
1860
|
+
// then denied the sequential dispatch the config explicitly asked for.
|
|
1861
|
+
// Applied AFTER --force-isolation so the documented rule holds: the
|
|
1862
|
+
// opt-out wins on every host, over both the natural resolution and any
|
|
1863
|
+
// force. Strict `=== false`: the default is worktrees ON, so an absent
|
|
1864
|
+
// key, an unreadable/malformed config, or a non-boolean value degrades
|
|
1865
|
+
// to "not opted out" (mirrors readConfigJsonBoolean's no-coercion
|
|
1866
|
+
// discipline in lib/init.cjs).
|
|
1867
|
+
if (projectWorktreesOptedOut(cwd)) {
|
|
1868
|
+
isolation = 'none';
|
|
1869
|
+
harnessFlag = null;
|
|
1870
|
+
exec = null;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1745
1873
|
// Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
|
|
1746
1874
|
// Never allowed to affect this query's own stdout contract or throw.
|
|
1747
1875
|
try {
|
|
@@ -1758,6 +1886,155 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1758
1886
|
}
|
|
1759
1887
|
}
|
|
1760
1888
|
|
|
1889
|
+
// #3714 follow-up — the dispatch seam gated only on PRESENCE of an explicit
|
|
1890
|
+
// pin, never on its VALUE, so an Anthropic-flavored global default
|
|
1891
|
+
// (~/.gsd/defaults.json model_overrides["gsd-executor"] = "sonnet"/"opus"/
|
|
1892
|
+
// "claude-*") reached `codex exec --model sonnet`: the documented #2310/
|
|
1893
|
+
// #2311 400 on a passive-posture host (ADR-1239/ADR-2313). It also let a
|
|
1894
|
+
// repo-committed .planning/config.json inject shell-hostile argv (a
|
|
1895
|
+
// `-c approval_policy=never` suffix, `$(...)`/`;` command injection,
|
|
1896
|
+
// embedded control characters) straight onto exec's argv.
|
|
1897
|
+
//
|
|
1898
|
+
// This mirrors — deliberately, not by re-derivation — the same VALUE
|
|
1899
|
+
// policy bin/install.js's generateCodexAgentToml() already applies to the
|
|
1900
|
+
// identical model_overrides["gsd-executor"] config key for the .toml
|
|
1901
|
+
// surface (bin/install.js ~3983-4046): trim; a whitespace-only value drops
|
|
1902
|
+
// silently (#3241, no warning); an Anthropic-flavored value
|
|
1903
|
+
// (isAnthropicFlavoredModel, single-sourced on bin/lib/model-catalog.cjs
|
|
1904
|
+
// per #3241 specifically so it cannot diverge across Codex-posture
|
|
1905
|
+
// surfaces) drops WITH a warning; a real pin survives verbatim. Two
|
|
1906
|
+
// additions beyond the .toml surface, both specific to this seam: the
|
|
1907
|
+
// 'inherit' sentinel (case/whitespace-insensitive) is a no-op here already
|
|
1908
|
+
// and must stay one, and a value that doesn't look like a model id at all
|
|
1909
|
+
// (the injection case above — the .toml surface never had to consider this
|
|
1910
|
+
// because TOML string-quoting isn't a shell argv boundary) is dropped with
|
|
1911
|
+
// a warning rather than ever reaching child_process argv.
|
|
1912
|
+
// Single source of truth for the model-id "allowed characters" notion
|
|
1913
|
+
// (#3714 follow-up — "Generative Fix Divergence"): the accept regex
|
|
1914
|
+
// (MODEL_ID_CHARSET_RE, used to ADMIT a pin) and the sanitizer keep-class
|
|
1915
|
+
// (MODEL_ID_SANITIZE_STRIP_RE, used to RENDER a rejected pin into a
|
|
1916
|
+
// warning) are both derived from this one character-class body so they
|
|
1917
|
+
// cannot drift apart again the way they already did once (the '@' added
|
|
1918
|
+
// for Vertex pins landed in the accept regex but not the sanitizer,
|
|
1919
|
+
// rendering "text-bison@002" as "text-bison?002" in the warning). '@' is
|
|
1920
|
+
// included for Vertex model-version pins ("text-bison@002",
|
|
1921
|
+
// "chat-bison@001"), which are legitimate model ids reachable through a
|
|
1922
|
+
// custom model_provider.
|
|
1923
|
+
// This body is interpolated raw into BOTH a positive character class
|
|
1924
|
+
// (MODEL_ID_CHARSET_RE, `[BODY]`) and a negated one
|
|
1925
|
+
// (MODEL_ID_SANITIZE_STRIP_RE, `[^BODY]`) below — only plain characters
|
|
1926
|
+
// and `x-y` ranges are safe here. A class metacharacter (`^`, `]`, `\`)
|
|
1927
|
+
// would mean different things in the two derived regexes if ever added.
|
|
1928
|
+
const MODEL_ID_CHARSET_BODY = 'A-Za-z0-9._:/@-';
|
|
1929
|
+
// The first character must be alphanumeric (#3714 hardening): a leading
|
|
1930
|
+
// '.', '_', ':', '/', or '@' has no legitimate model-id use case and, for
|
|
1931
|
+
// resolveOrchestratorExec's documented role as a GENERAL descriptor→argv
|
|
1932
|
+
// seam other hosts may adopt, a leading '@' or '/' is exactly the shape an
|
|
1933
|
+
// @-response-file or /-switch parser would key on. The leading-dash shape
|
|
1934
|
+
// is enforced separately by LEADING_DASH_RE below — it is NOT relaxed
|
|
1935
|
+
// here, since a flag-shaped value ("-c", "--config") must still fail the
|
|
1936
|
+
// resolver's own unsafe_leading_dash_model guard path via that dedicated
|
|
1937
|
+
// check, not this charset.
|
|
1938
|
+
const MODEL_ID_CHARSET_RE = new RegExp(`^[A-Za-z0-9][${MODEL_ID_CHARSET_BODY}]*$`);
|
|
1939
|
+
// Keep-class for sanitizing a REJECTED pin before it reaches the warning
|
|
1940
|
+
// (a guaranteed-reachable raw-to-TTY sink — the dispatch step runs with no
|
|
1941
|
+
// `2>` redirect). Built from the same MODEL_ID_CHARSET_BODY as the accept
|
|
1942
|
+
// regex above, so every character the matcher accepts also survives the
|
|
1943
|
+
// sanitizer unchanged, and a widened charset can never diverge from its
|
|
1944
|
+
// rendering again.
|
|
1945
|
+
//
|
|
1946
|
+
// This `g`-flagged instance is for internal `.replace()` use ONLY — a
|
|
1947
|
+
// `/g` regex is stateful (`.lastIndex` persists across calls) and
|
|
1948
|
+
// `.test()` on it alternates true/false/true across repeated calls on the
|
|
1949
|
+
// same string, a false-green trap for any test that reaches for `.test()`
|
|
1950
|
+
// instead of `.replace()`. To make that trap impossible rather than just
|
|
1951
|
+
// documenting it, this `g`-flagged object is never exported; the exported
|
|
1952
|
+
// `MODEL_ID_SANITIZE_STRIP_RE` below is a separate, non-global instance
|
|
1953
|
+
// built from the same body, safe for `.test()`/`.match()` in tests.
|
|
1954
|
+
const MODEL_ID_SANITIZE_STRIP_RE_G = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`, 'g');
|
|
1955
|
+
// Non-global companion of MODEL_ID_SANITIZE_STRIP_RE_G, exported for
|
|
1956
|
+
// tests. Do not use with `.replace()` on a value containing more than one
|
|
1957
|
+
// disallowed character — it only replaces the first match. Production
|
|
1958
|
+
// code must use the `g`-flagged instance above instead.
|
|
1959
|
+
const MODEL_ID_SANITIZE_STRIP_RE = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`);
|
|
1960
|
+
// A model id has no legitimate reason to be long; this also keeps a
|
|
1961
|
+
// pathological pin away from the Windows argv ceiling (execFileSync aborts
|
|
1962
|
+
// if argv > 32,767 chars — CLAUDE.md "Windows ARGV Overflow"). A pin over
|
|
1963
|
+
// this length is DROPPED WITH A WARNING like every other rejection, never
|
|
1964
|
+
// truncated into argv — a truncated model id is a different model id.
|
|
1965
|
+
const MODEL_ID_MAX_LENGTH = 200;
|
|
1966
|
+
const _dispatchModelPinDropWarned = new Set();
|
|
1967
|
+
function _warnDispatchModelPinDropped(agentName, rawValue, reason) {
|
|
1968
|
+
const key = `${agentName}::${rawValue}::${reason}`;
|
|
1969
|
+
if (_dispatchModelPinDropWarned.has(key)) return;
|
|
1970
|
+
_dispatchModelPinDropWarned.add(key);
|
|
1971
|
+
// Sanitize BEFORE truncating: every value that reaches this warning
|
|
1972
|
+
// failed the model-id charset test by definition (or, for the
|
|
1973
|
+
// over-length case, still only ever contains charset-legal bytes) —
|
|
1974
|
+
// sanitizing first catches raw control/escape bytes (ESC, BEL, CSI
|
|
1975
|
+
// sequences) using the identity-sanitizing pattern already used for
|
|
1976
|
+
// --as at gsd-tools.cjs:1526. Sanitizing before truncating also ensures
|
|
1977
|
+
// a truncated escape sequence can never survive (e.g. an SGR sequence
|
|
1978
|
+
// cut before its reset, leaving sticky terminal state) — truncation
|
|
1979
|
+
// only ever cuts already-safe characters.
|
|
1980
|
+
const sanitized = String(rawValue).replace(MODEL_ID_SANITIZE_STRIP_RE_G, '?');
|
|
1981
|
+
const safe = sanitized.length > 64 ? `${sanitized.slice(0, 64)}…` : sanitized;
|
|
1982
|
+
process.stderr.write(
|
|
1983
|
+
`gsd: warning — dispatch model pin for agent "${agentName}" (value "${safe}") ${reason}; ` +
|
|
1984
|
+
`dropping it so the spawned executor falls back to the session model.\n`,
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
// A value starting with '-' (or '--') is a flag/option shape, not a model
|
|
1988
|
+
// id — `-c`, `--config`, `-`, `--`, `-p` are unsafe to hand to
|
|
1989
|
+
// resolveOrchestratorExec, whose own `unsafe_leading_dash_model` guard
|
|
1990
|
+
// rejects them and fails the WHOLE resolution to `{ ok: false }` ->
|
|
1991
|
+
// exec:null -> a FATAL wave abort (executor-isolation-dispatch.md:299-303),
|
|
1992
|
+
// even on hosts (e.g. kimi-code) that declare no modelFlag at all and
|
|
1993
|
+
// previously ignored the pin entirely. This check MUST run BEFORE
|
|
1994
|
+
// MODEL_ID_CHARSET_RE below: the charset is anchored to `[A-Za-z0-9]` at
|
|
1995
|
+
// the first character, so every dash-leading value already fails the
|
|
1996
|
+
// charset test and would otherwise be swallowed by the generic "unsafe
|
|
1997
|
+
// characters" message, losing the more specific and more actionable
|
|
1998
|
+
// flag/option diagnosis. Reject here, at the VALUE-policy layer, so a
|
|
1999
|
+
// leading-dash value degrades to "no model" like every other rejected
|
|
2000
|
+
// shape, instead of reaching a resolver whose failure mode is fatal
|
|
2001
|
+
// rather than a graceful drop.
|
|
2002
|
+
const LEADING_DASH_RE = /^-/;
|
|
2003
|
+
/**
|
|
2004
|
+
* Resolve the VALUE policy for an explicit dispatch model pin. `rawValue`
|
|
2005
|
+
* is whatever resolveAgentModelOverride(..., null) returned — a string
|
|
2006
|
+
* pin, the 'inherit' sentinel, or null/''/undefined for "no explicit pin".
|
|
2007
|
+
* Returns the trimmed model string to embed, or `undefined` to emit no
|
|
2008
|
+
* --model flag at all. Never throws; never fails closed to an error —
|
|
2009
|
+
* every rejection degrades to "no model" (drop-and-warn), matching the
|
|
2010
|
+
* documented desired behavior of falling back to the session model rather
|
|
2011
|
+
* than aborting the wave.
|
|
2012
|
+
*/
|
|
2013
|
+
function resolveDispatchModelPin(agentName, rawValue) {
|
|
2014
|
+
if (typeof rawValue !== 'string') return undefined; // not a string -> no model
|
|
2015
|
+
const trimmed = rawValue.trim();
|
|
2016
|
+
if (trimmed === '') return undefined; // whitespace-only -> no model, no warning (#3241)
|
|
2017
|
+
if (trimmed.toLowerCase() === 'inherit') return undefined; // sentinel -> no model, no warning
|
|
2018
|
+
const { isAnthropicFlavoredModel } = require('./lib/model-catalog.cjs');
|
|
2019
|
+
if (isAnthropicFlavoredModel(trimmed)) {
|
|
2020
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'is an Anthropic-flavored model/alias, not a valid Codex model');
|
|
2021
|
+
return undefined;
|
|
2022
|
+
}
|
|
2023
|
+
if (LEADING_DASH_RE.test(trimmed)) {
|
|
2024
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'looks like a flag/option, not a model id (leading "-")');
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
if (!MODEL_ID_CHARSET_RE.test(trimmed)) {
|
|
2028
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'does not look like a model id (unsafe characters)');
|
|
2029
|
+
return undefined;
|
|
2030
|
+
}
|
|
2031
|
+
if (trimmed.length > MODEL_ID_MAX_LENGTH) {
|
|
2032
|
+
_warnDispatchModelPinDropped(agentName, rawValue, `exceeds the maximum model id length (${MODEL_ID_MAX_LENGTH} characters)`);
|
|
2033
|
+
return undefined;
|
|
2034
|
+
}
|
|
2035
|
+
return trimmed;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1761
2038
|
const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
1762
2039
|
|
|
1763
2040
|
/**
|
|
@@ -1821,14 +2098,67 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1821
2098
|
const promptIdx = args.indexOf('--prompt');
|
|
1822
2099
|
const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined;
|
|
1823
2100
|
const hostIntegration = require('./lib/host-integration.cjs');
|
|
2101
|
+
// #3714: resolve an EXPLICIT, non-sentinel per-agent model pin for the
|
|
2102
|
+
// spawned worktree executor. Passing `null` as the runtime resolver
|
|
2103
|
+
// (3rd arg) is LOAD-BEARING, not an oversight — it is what keeps
|
|
2104
|
+
// profile/tier-derived models out of argv. Codex's `modelMode: passive`
|
|
2105
|
+
// posture (ADR-1239) and ADR-2313 forbid GSD driving model selection on
|
|
2106
|
+
// this host; only an operator's EXPLICIT override may cross this seam.
|
|
2107
|
+
// resolveAgentModelOverride(..., null) returns a value ONLY when the
|
|
2108
|
+
// operator pinned one explicitly (measured: unpinned -> null,
|
|
2109
|
+
// "inherit" -> "inherit" meaning "use the ambient session model, don't
|
|
2110
|
+
// pass a flag", "" -> null, profile-only -> null). Do NOT swap in
|
|
2111
|
+
// resolve-model / a full model-resolver here: that resolver falls back
|
|
2112
|
+
// to a default (e.g. "sonnet") for the unpinned/profile-only cases,
|
|
2113
|
+
// and emitting that on Codex's exec argv is exactly the documented
|
|
2114
|
+
// #2310/#2311 regression (a model unknown to Codex forced into a
|
|
2115
|
+
// passive-posture host).
|
|
2116
|
+
//
|
|
2117
|
+
// Presence of a pin is necessary but not sufficient: resolveDispatchModelPin
|
|
2118
|
+
// applies the same VALUE policy the install-side .toml surface already
|
|
2119
|
+
// applies to this config key (trim / drop-inherit / drop-Anthropic-flavored
|
|
2120
|
+
// with a warning / drop-non-model-id-charset with a warning) so a global
|
|
2121
|
+
// Anthropic-flavored default or an injected config value never reaches argv.
|
|
2122
|
+
//
|
|
2123
|
+
// This whole VALUE policy — including its warning — is gated on the
|
|
2124
|
+
// resolved runtime's descriptor actually declaring a non-empty
|
|
2125
|
+
// `modelFlag`. The policy runs at this host-NEUTRAL site, so a host
|
|
2126
|
+
// with no modelFlag at all (kimi, kimi-code, opencode) was never
|
|
2127
|
+
// going to emit a --model regardless of the pin's value; running the
|
|
2128
|
+
// policy anyway produced a stderr warning claiming "dropping it so
|
|
2129
|
+
// the spawned executor falls back to the session model" on every
|
|
2130
|
+
// dispatch for such a host — misleading today, and actively wrong if
|
|
2131
|
+
// a Claude-capable host ever declares a modelFlag. When the
|
|
2132
|
+
// descriptor declares no modelFlag, skip the policy entirely: no
|
|
2133
|
+
// model, no warning, argv byte-identical to before this pin policy
|
|
2134
|
+
// existed.
|
|
2135
|
+
const declaresModelFlag = typeof runtimeEntry?.runtime?.orchestratorExec?.modelFlag === 'string' &&
|
|
2136
|
+
runtimeEntry.runtime.orchestratorExec.modelFlag.length > 0;
|
|
2137
|
+
let model;
|
|
2138
|
+
if (declaresModelFlag) {
|
|
2139
|
+
const { readGsdEffectiveModelOverrides, resolveAgentModelOverride } =
|
|
2140
|
+
require('./lib/install-model-override-resolver.cjs');
|
|
2141
|
+
const pinned = resolveAgentModelOverride(
|
|
2142
|
+
'gsd-executor', readGsdEffectiveModelOverrides(cwd), null);
|
|
2143
|
+
model = resolveDispatchModelPin('gsd-executor', pinned);
|
|
2144
|
+
}
|
|
1824
2145
|
const resolution = hostIntegration.resolveOrchestratorExec(
|
|
1825
2146
|
runtimeEntry?.runtime?.orchestratorExec,
|
|
1826
2147
|
cwdTarget,
|
|
1827
2148
|
promptArg,
|
|
2149
|
+
model,
|
|
1828
2150
|
);
|
|
1829
|
-
// A host declaring orchestrator-worktree whose exec descriptor does
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
2151
|
+
// A host declaring orchestrator-worktree whose exec descriptor does not
|
|
2152
|
+
// resolve halts THIS wave's dispatch: isolation is forced to 'none' here,
|
|
2153
|
+
// and executor-isolation-dispatch.md:299-303 treats a null exec as FATAL
|
|
2154
|
+
// (exit 1) after the worktree has already been created — it does not
|
|
2155
|
+
// degrade to sequential execution. resolveDispatchModelPin rejects any
|
|
2156
|
+
// leading-dash value (flag/option shape) before it ever reaches this
|
|
2157
|
+
// resolver specifically so it cannot trip the resolver's own
|
|
2158
|
+
// `unsafe_leading_dash_model` guard and turn a bad config value into
|
|
2159
|
+
// this fatal path; every other unresolvable model value likewise
|
|
2160
|
+
// degrades to "no --model" (session model fallback) rather than to
|
|
2161
|
+
// resolution.ok === false.
|
|
1832
2162
|
if (resolution.ok) {
|
|
1833
2163
|
exec = { command: resolution.command, args: resolution.args, cwd: resolution.cwd };
|
|
1834
2164
|
} else {
|
|
@@ -2137,8 +2467,19 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2137
2467
|
.filter((t) => t.length > 0);
|
|
2138
2468
|
termsOverride = list.length > 0 ? { pluralization: list } : undefined;
|
|
2139
2469
|
}
|
|
2470
|
+
// An unresolvable phase section is not a negative verdict. Feeding
|
|
2471
|
+
// `''` to the detector reported "examined, found nothing" for a
|
|
2472
|
+
// probe that never had input — no ROADMAP.md, or a phase number
|
|
2473
|
+
// absent from it, both read as a confident `detected:false`
|
|
2474
|
+
// (ADR-3889 failure class (c), #3909). Exit stays 0: this is an
|
|
2475
|
+
// ADR-2980 degraded result carried in the payload, and ADR-3889 P8
|
|
2476
|
+
// pins the gsd-tools exit projection at v1.
|
|
2140
2477
|
const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
|
|
2141
|
-
|
|
2478
|
+
if (typeof section !== 'string' || section.trim() === '') {
|
|
2479
|
+
output({ skipped: true, reason: 'phase_unresolved' }, raw);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const result = detectAssumptionDelta(section, termsOverride);
|
|
2142
2483
|
output(result, raw);
|
|
2143
2484
|
return;
|
|
2144
2485
|
}
|
|
@@ -2185,7 +2526,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2185
2526
|
// --no-archive-phases' inverted shape, absence of this flag means
|
|
2186
2527
|
// "do nothing" rather than "skip a default-on behavior".
|
|
2187
2528
|
const archiveQuick = args.includes('--archive-quick');
|
|
2188
|
-
|
|
2529
|
+
// #3726: explicit mutation opt-in — without --confirm (and without
|
|
2530
|
+
// --dry-run) the command refuses before touching anything. Distinct
|
|
2531
|
+
// from --force, which bypasses the narrow scope guards only.
|
|
2532
|
+
const confirm = args.includes('--confirm');
|
|
2533
|
+
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick, confirm }, raw);
|
|
2189
2534
|
} else if (subcommand === 'archive-quick') {
|
|
2190
2535
|
// #2142 escalation: narrow archival-only entry point (does NOT
|
|
2191
2536
|
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
@@ -2207,11 +2552,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2207
2552
|
const subcommand = args[1];
|
|
2208
2553
|
if (subcommand === 'render-checkpoint') {
|
|
2209
2554
|
const uat = require('./lib/uat.cjs');
|
|
2210
|
-
const options =
|
|
2555
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['file'], positionals: 2 }, error);
|
|
2211
2556
|
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
2212
2557
|
} else if (subcommand === 'classify-coverage') {
|
|
2213
2558
|
const coverage = require('./lib/coverage.cjs');
|
|
2214
|
-
const options =
|
|
2559
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['summary', 'file'], positionals: 2 }, error);
|
|
2215
2560
|
coverage.cmdClassify(cwd, options, raw);
|
|
2216
2561
|
} else {
|
|
2217
2562
|
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -2236,8 +2581,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2236
2581
|
|
|
2237
2582
|
function routeScaffold({ args, cwd, raw, error }) {
|
|
2238
2583
|
const scaffoldType = args[1];
|
|
2584
|
+
// `--name` is multi-word (consumed separately by parseMultiwordArg,
|
|
2585
|
+
// below) — a token count the single-token-per-flag boundary walk
|
|
2586
|
+
// cannot represent. `positionals: 'rest'` disables that walk for
|
|
2587
|
+
// this call, matching the existing (unchanged) permissive behavior
|
|
2588
|
+
// for --name; --phase extraction is unaffected either way.
|
|
2239
2589
|
const scaffoldOptions = {
|
|
2240
|
-
phase:
|
|
2590
|
+
phase: parseNamedArgsOrExit(args, { valueFlags: ['phase'], positionals: 'rest' }, error).phase,
|
|
2241
2591
|
name: parseMultiwordArg(args, 'name'),
|
|
2242
2592
|
};
|
|
2243
2593
|
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
@@ -2441,9 +2791,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2441
2791
|
);
|
|
2442
2792
|
}
|
|
2443
2793
|
} catch (e) {
|
|
2794
|
+
// ADR-3889: error() now throws ExitError instead of calling
|
|
2795
|
+
// process.exit(1) directly, so an ExitError raised by error() INSIDE
|
|
2796
|
+
// this try (e.g. the "Unknown windows subcommand" call above, or one
|
|
2797
|
+
// inside cmdWindowsStatus/Append/Waive/MarkFixed) lands HERE instead of
|
|
2798
|
+
// terminating uncatchably. It must be re-thrown unconditionally, before
|
|
2799
|
+
// the WindowsError name check below, or it falls through to the
|
|
2800
|
+
// generic branch and gets re-wrapped with a wrong message/reason,
|
|
2801
|
+
// discarding the original exit code.
|
|
2802
|
+
if (e instanceof ExitError) throw e;
|
|
2444
2803
|
// WindowsError carries a REASON code; surface it through the structured
|
|
2445
|
-
// error path so tests can assert on the typed reason.
|
|
2446
|
-
// process.exit(1) internally so we never reach the fall-through.
|
|
2804
|
+
// error path so tests can assert on the typed reason.
|
|
2447
2805
|
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
|
|
2448
2806
|
error(e.message || 'broken-windows error', e.reason);
|
|
2449
2807
|
}
|
|
@@ -3712,6 +4070,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3712
4070
|
'verification': routeVerification,
|
|
3713
4071
|
'generate-slug': routeGenerateSlug,
|
|
3714
4072
|
'current-timestamp': routeCurrentTimestamp,
|
|
4073
|
+
'runtime-identity': routeRuntimeIdentity,
|
|
3715
4074
|
'project-instruction-file': routeProjectInstructionFile,
|
|
3716
4075
|
'list-todos': routeListTodos,
|
|
3717
4076
|
'list-seeds': routeListSeeds,
|
|
@@ -3728,6 +4087,10 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3728
4087
|
'skill-manifest': routeSkillManifest,
|
|
3729
4088
|
'history-digest': routeHistoryDigest,
|
|
3730
4089
|
'phases': routePhases,
|
|
4090
|
+
// #2790: read-only schema-v1 planning snapshot. The router imports its own
|
|
4091
|
+
// io/planning-inspect deps, so it needs no module injection — it receives
|
|
4092
|
+
// { args, cwd, raw, error } and ignores the rest of the dispatch context.
|
|
4093
|
+
'planning': routePlanningCommand,
|
|
3731
4094
|
'assumption-delta': routeAssumptionDelta,
|
|
3732
4095
|
'requirements': routeRequirements,
|
|
3733
4096
|
'gap-analysis': routeGapAnalysis,
|
|
@@ -3968,14 +4331,14 @@ function runWithTimeout(argv) {
|
|
|
3968
4331
|
// this string and HOST_COMMAND_ROUTERS/SKIP_ROOT_RESOLUTION are three
|
|
3969
4332
|
// independently hand-maintained sites and nothing previously caught them
|
|
3970
4333
|
// drifting apart when a query command was added to only one or two.
|
|
3971
|
-
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
4334
|
+
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--project-dir <path>] [--ws <name>] [--json-errors] [--exit-contract=<v>]\n' +
|
|
3972
4335
|
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
|
|
3973
4336
|
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
3974
4337
|
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
3975
4338
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
3976
4339
|
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
3977
|
-
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
3978
|
-
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
|
|
4340
|
+
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
|
4341
|
+
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
|
|
3979
4342
|
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
|
3980
4343
|
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
3981
4344
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
@@ -3983,8 +4346,10 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
3983
4346
|
' --raw Emit raw output without post-processing\n' +
|
|
3984
4347
|
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
3985
4348
|
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
4349
|
+
' --project-dir <path> Explicit project root; skips the ancestor walk-up entirely (must already contain .planning/)\n' +
|
|
3986
4350
|
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
3987
|
-
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n
|
|
4351
|
+
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n' +
|
|
4352
|
+
' --exit-contract=<v> Exit-code contract version: v1 (default) or v2 (or set GSD_EXIT_CONTRACT)\n\n' +
|
|
3988
4353
|
'For command-specific argument requirements, invoke the command without args ' +
|
|
3989
4354
|
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
3990
4355
|
|
|
@@ -4003,6 +4368,10 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
4003
4368
|
// below (never as the live Set itself; see that function's doc comment).
|
|
4004
4369
|
const SKIP_ROOT_RESOLUTION = new Set([
|
|
4005
4370
|
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
4371
|
+
// #3146: runtime-identity is a pure local read of baked package coordinates.
|
|
4372
|
+
// It is probed from whatever cwd a workflow happens to be in — including
|
|
4373
|
+
// outside any project — so it must never require a resolvable project root.
|
|
4374
|
+
'runtime-identity',
|
|
4006
4375
|
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
4007
4376
|
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
4008
4377
|
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
@@ -4074,18 +4443,19 @@ function resolveMainWorktreeCwd(cwd, deps = {}) {
|
|
|
4074
4443
|
async function main() {
|
|
4075
4444
|
let args = process.argv.slice(2);
|
|
4076
4445
|
|
|
4077
|
-
//
|
|
4078
|
-
//
|
|
4079
|
-
//
|
|
4080
|
-
//
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4446
|
+
// These two global-flag blocks (--json-errors, --exit-contract) MUST run
|
|
4447
|
+
// BEFORE the run-with-timeout interception below. run-with-timeout treats
|
|
4448
|
+
// args[0] (post `query` stripping) as the sentinel and otherwise passes the
|
|
4449
|
+
// remaining argv straight to the wrapped child — it never reaches the
|
|
4450
|
+
// dispatcher's "Unknown command" fallback, but a global flag left in LEADING
|
|
4451
|
+
// position (e.g. `--exit-contract=v2 run-with-timeout ...`) would be spliced
|
|
4452
|
+
// out too late if these ran after, since neither block currently exists
|
|
4453
|
+
// below this point to consume it. Splicing here, before run-with-timeout's
|
|
4454
|
+
// own argv slicing, is what keeps both flags position-independent for every
|
|
4455
|
+
// command, run-with-timeout included. Do not move these back below the
|
|
4456
|
+
// run-with-timeout block (#confirmed regression: leading --exit-contract=v2
|
|
4457
|
+
// and leading --json-errors both broke run-with-timeout when these blocks
|
|
4458
|
+
// sat after it).
|
|
4089
4459
|
|
|
4090
4460
|
// --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
|
|
4091
4461
|
// JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
|
|
@@ -4105,6 +4475,41 @@ async function main() {
|
|
|
4105
4475
|
setJsonErrorMode(true);
|
|
4106
4476
|
}
|
|
4107
4477
|
|
|
4478
|
+
// --exit-contract=<v> / GSD_EXIT_CONTRACT: resolve FIRST, before the splice
|
|
4479
|
+
// below, so an invalid value (e.g. `v3`, or an empty `--exit-contract=`)
|
|
4480
|
+
// throws EARLY — matching the --json-errors block's own "detect early,
|
|
4481
|
+
// before any flag parsing that can fire error()" rationale above. This also
|
|
4482
|
+
// memoizes the resolved version into the shared contract-version cell so a
|
|
4483
|
+
// later terminateNow()/runMain() call projects against it correctly.
|
|
4484
|
+
//
|
|
4485
|
+
// The argv splice must happen here too, otherwise the dispatcher below sees
|
|
4486
|
+
// "--exit-contract=<v>" as an unknown command when the flag is given in
|
|
4487
|
+
// LEADING position (argv[0] is what the dispatcher treats as the command
|
|
4488
|
+
// name). Splice EVERY occurrence, not just the first — findExitContractFlag
|
|
4489
|
+
// only consults the first match, so a stray second token would otherwise
|
|
4490
|
+
// survive into the dispatcher and reproduce the same "Unknown command".
|
|
4491
|
+
resolveContractVersion({ argv: process.argv, env: process.env });
|
|
4492
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
4493
|
+
if (typeof args[i] === 'string' && args[i].startsWith('--exit-contract=')) {
|
|
4494
|
+
args.splice(i, 1);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
// #2351: run-with-timeout bounds a spawned command's wall clock portably
|
|
4499
|
+
// (coreutils-independent). It MUST intercept HERE, before the remaining
|
|
4500
|
+
// flag parsing below — the wrapped command's argv is opaque and may itself
|
|
4501
|
+
// contain --raw / --cwd / --pick that this dispatcher would otherwise
|
|
4502
|
+
// consume. (--json-errors / --exit-contract are handled above this block,
|
|
4503
|
+
// not below, precisely so they keep working with run-with-timeout.)
|
|
4504
|
+
{
|
|
4505
|
+
let rwt = args;
|
|
4506
|
+
if (rwt[0] === 'query') rwt = rwt.slice(1);
|
|
4507
|
+
if (rwt[0] === 'run-with-timeout') {
|
|
4508
|
+
// Return the child's exit code; runMain() maps it to process.exitCode.
|
|
4509
|
+
return runWithTimeout(rwt.slice(1));
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4108
4513
|
// Optional cwd override for sandboxed subagents running outside project root.
|
|
4109
4514
|
let cwd = process.cwd();
|
|
4110
4515
|
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
@@ -4125,6 +4530,39 @@ async function main() {
|
|
|
4125
4530
|
error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
|
|
4126
4531
|
}
|
|
4127
4532
|
|
|
4533
|
+
// #3881: --project-dir <path> is a documented (docs/CONFIGURATION.md,
|
|
4534
|
+
// "Project-Root Resolution in Multi-Repo Workspaces") explicit override of
|
|
4535
|
+
// the project root. It is idempotent under findProjectRoot's ancestor
|
|
4536
|
+
// walk-up — i.e. it short-circuits the walk-up rather than seeding it —
|
|
4537
|
+
// so it MUST be validated and applied here, before findProjectRoot ever
|
|
4538
|
+
// runs, and its result must skip that call entirely below. A relative
|
|
4539
|
+
// value resolves against process.cwd(), matching --cwd's own resolution.
|
|
4540
|
+
let projectDirExplicit = false;
|
|
4541
|
+
const projectDirEqArg = args.find(arg => arg.startsWith('--project-dir='));
|
|
4542
|
+
const projectDirIdx = args.indexOf('--project-dir');
|
|
4543
|
+
let projectDirValue;
|
|
4544
|
+
if (projectDirEqArg) {
|
|
4545
|
+
projectDirValue = projectDirEqArg.slice('--project-dir='.length).trim();
|
|
4546
|
+
if (!projectDirValue) error('Missing value for --project-dir', ERROR_REASON.USAGE);
|
|
4547
|
+
args.splice(args.indexOf(projectDirEqArg), 1);
|
|
4548
|
+
} else if (projectDirIdx !== -1) {
|
|
4549
|
+
projectDirValue = args[projectDirIdx + 1];
|
|
4550
|
+
if (!projectDirValue || projectDirValue.startsWith('--')) error('Missing value for --project-dir', ERROR_REASON.USAGE);
|
|
4551
|
+
args.splice(projectDirIdx, 2);
|
|
4552
|
+
}
|
|
4553
|
+
if (projectDirValue !== undefined) {
|
|
4554
|
+
const resolvedProjectDir = path.resolve(projectDirValue);
|
|
4555
|
+
if (!fs.existsSync(resolvedProjectDir) || !fs.statSync(resolvedProjectDir).isDirectory()) {
|
|
4556
|
+
error(`Invalid --project-dir: ${resolvedProjectDir} (path does not exist or is not a directory)`, ERROR_REASON.USAGE);
|
|
4557
|
+
}
|
|
4558
|
+
const resolvedProjectDirPlanning = path.join(resolvedProjectDir, '.planning');
|
|
4559
|
+
if (!fs.existsSync(resolvedProjectDirPlanning) || !fs.statSync(resolvedProjectDirPlanning).isDirectory()) {
|
|
4560
|
+
error(`Invalid --project-dir: ${resolvedProjectDir} (no .planning/ directory found — --project-dir must name the project root itself, not an ancestor to walk up from)`, ERROR_REASON.USAGE);
|
|
4561
|
+
}
|
|
4562
|
+
cwd = resolvedProjectDir;
|
|
4563
|
+
projectDirExplicit = true;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4128
4566
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
4129
4567
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
4130
4568
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
@@ -4237,24 +4675,45 @@ async function main() {
|
|
|
4237
4675
|
}
|
|
4238
4676
|
}
|
|
4239
4677
|
|
|
4240
|
-
|
|
4678
|
+
// #3881: an explicit --project-dir already IS the resolved project root
|
|
4679
|
+
// (validated above) — findProjectRoot's ancestor walk-up must not run
|
|
4680
|
+
// over it, per docs/CONFIGURATION.md's documented idempotence.
|
|
4681
|
+
if (!projectDirExplicit && !SKIP_ROOT_RESOLUTION.has(command)) {
|
|
4241
4682
|
cwd = findProjectRoot(cwd);
|
|
4242
4683
|
}
|
|
4243
4684
|
|
|
4244
4685
|
// When --pick is active, capture stdout and extract the requested field.
|
|
4686
|
+
// ADR-3473 §8.4 (#3365, #3358): an absent field or non-JSON command output
|
|
4687
|
+
// is a failure ("I could not answer"), never a demotion to an empty answer
|
|
4688
|
+
// at exit 0. `resolveAtFileOutput` MUST run before JSON.parse — @file:
|
|
4689
|
+
// payloads (io.cjs output() writes these for JSON > 50KB) are not
|
|
4690
|
+
// themselves JSON text, so resolving late would make every large result a
|
|
4691
|
+
// false "output was not JSON" (negative space N8).
|
|
4245
4692
|
if (pickField) {
|
|
4246
4693
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
4247
4694
|
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
4248
4695
|
});
|
|
4249
4696
|
const resolved = resolveAtFileOutput(captured);
|
|
4697
|
+
let obj;
|
|
4250
4698
|
try {
|
|
4251
|
-
|
|
4252
|
-
const value = extractField(obj, pickField);
|
|
4253
|
-
const result = value === null || value === undefined ? '' : String(value);
|
|
4254
|
-
fs.writeSync(1, result);
|
|
4699
|
+
obj = JSON.parse(resolved);
|
|
4255
4700
|
} catch {
|
|
4256
|
-
|
|
4701
|
+
error(`--pick ${formatDiagnosticToken(pickField)}: command output was not JSON`, ERROR_REASON.PICK_OUTPUT_NOT_JSON);
|
|
4702
|
+
return;
|
|
4257
4703
|
}
|
|
4704
|
+
const { found, value } = extractField(obj, pickField);
|
|
4705
|
+
if (!found) {
|
|
4706
|
+
const rootDescription = isPlainRecord(obj)
|
|
4707
|
+
? `available top-level keys: ${Object.keys(obj).map(formatKeyForDiagnosticList).join(', ') || '(none)'}`
|
|
4708
|
+
: `the command's output is a JSON ${describeJsonRootType(obj)}, not an object with that field`;
|
|
4709
|
+
error(`--pick ${formatDiagnosticToken(pickField)}: field not found; ${rootDescription}`, ERROR_REASON.PICK_FIELD_ABSENT);
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
// N1/N2: `null` and `''` are answers, not failures — an absent field
|
|
4713
|
+
// above already exited non-zero, so reaching here means the field EXISTS
|
|
4714
|
+
// and this is its real value (including `0` and `false`, #3365).
|
|
4715
|
+
const result = value === null || value === undefined ? '' : String(value);
|
|
4716
|
+
fs.writeSync(1, result);
|
|
4258
4717
|
return;
|
|
4259
4718
|
}
|
|
4260
4719
|
|
|
@@ -4316,27 +4775,68 @@ function resolveAtFileOutput(captured) {
|
|
|
4316
4775
|
return fs.readFileSync(captured.slice(6), 'utf-8');
|
|
4317
4776
|
}
|
|
4318
4777
|
|
|
4778
|
+
// A plain object root/intermediate value — everything else (null, an array,
|
|
4779
|
+
// a number, a string, a boolean) is treated as non-object for NAMED-key
|
|
4780
|
+
// lookup purposes (#3365 / #3358, ADR-3473 §8.4): only bracket notation may
|
|
4781
|
+
// reach into an array.
|
|
4782
|
+
function isPlainRecord(v) {
|
|
4783
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
// Describes the JSON root's shape for a --pick "field not found" message
|
|
4787
|
+
// when the root is NOT a plain object (so listing "top-level keys" would be
|
|
4788
|
+
// meaningless).
|
|
4789
|
+
function describeJsonRootType(v) {
|
|
4790
|
+
if (Array.isArray(v)) return 'array';
|
|
4791
|
+
if (v === null) return 'null';
|
|
4792
|
+
return typeof v;
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
// A command's JSON output can be a USER-authored document (e.g. `frontmatter
|
|
4796
|
+
// get`), so its top-level keys are untrusted the same way an argv token is.
|
|
4797
|
+
// `formatDiagnosticToken` (io.cjs) is the shared escape (see its JSDoc for
|
|
4798
|
+
// why `error()` cannot do this itself); this thin wrapper reuses that exact
|
|
4799
|
+
// escaping but strips the surrounding quotes JSON.stringify adds, so a key
|
|
4800
|
+
// list reads as "a, b, c" rather than the noisier "\"a\", \"b\", \"c\"" while
|
|
4801
|
+
// a key containing \n/\r/\t/other C0 bytes still cannot forge a second
|
|
4802
|
+
// stderr "Error:" line or span more than one line.
|
|
4803
|
+
function formatKeyForDiagnosticList(key) {
|
|
4804
|
+
return formatDiagnosticToken(key).slice(1, -1);
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4319
4807
|
/**
|
|
4320
4808
|
* Extract a field from an object using dot-notation and bracket syntax.
|
|
4321
4809
|
* Supports: 'field', 'parent.child', 'arr[-1]', 'arr[0]'
|
|
4810
|
+
*
|
|
4811
|
+
* Returns a discriminated `{ found, value }` rather than a bare value so a
|
|
4812
|
+
* caller can distinguish "the field exists and is null/''/0/false" (an
|
|
4813
|
+
* ANSWER, exit 0) from "no such field" (an absence, exit non-zero) — #3365.
|
|
4814
|
+
* Reports NOT-FOUND for: a missing key; a dotted path that dies partway; an
|
|
4815
|
+
* array index out of range (after negative-index normalization); a bracket
|
|
4816
|
+
* applied to a non-array; and any key lookup against a non-object (null, a
|
|
4817
|
+
* number, a string, a boolean, or an array root).
|
|
4322
4818
|
*/
|
|
4323
4819
|
function extractField(obj, fieldPath) {
|
|
4324
4820
|
const parts = fieldPath.split('.');
|
|
4325
4821
|
let current = obj;
|
|
4326
4822
|
for (const part of parts) {
|
|
4327
|
-
if (current === null || current === undefined) return undefined;
|
|
4328
4823
|
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
|
|
4329
4824
|
if (bracketMatch) {
|
|
4330
4825
|
const key = bracketMatch[1];
|
|
4331
4826
|
const index = parseInt(bracketMatch[2], 10);
|
|
4332
|
-
current
|
|
4333
|
-
|
|
4334
|
-
|
|
4827
|
+
if (!isPlainRecord(current)) return { found: false, value: undefined };
|
|
4828
|
+
const arr = current[key];
|
|
4829
|
+
if (!Array.isArray(arr)) return { found: false, value: undefined };
|
|
4830
|
+
const resolvedIndex = index < 0 ? arr.length + index : index;
|
|
4831
|
+
if (resolvedIndex < 0 || resolvedIndex >= arr.length) return { found: false, value: undefined };
|
|
4832
|
+
current = arr[resolvedIndex];
|
|
4335
4833
|
} else {
|
|
4834
|
+
if (!isPlainRecord(current)) return { found: false, value: undefined };
|
|
4835
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) return { found: false, value: undefined };
|
|
4336
4836
|
current = current[part];
|
|
4337
4837
|
}
|
|
4338
4838
|
}
|
|
4339
|
-
return current;
|
|
4839
|
+
return { found: true, value: current };
|
|
4340
4840
|
}
|
|
4341
4841
|
|
|
4342
4842
|
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
@@ -4405,5 +4905,21 @@ module.exports = {
|
|
|
4405
4905
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
4406
4906
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
4407
4907
|
resolveSpawnBinary,
|
|
4908
|
+
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
4909
|
+
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
4910
|
+
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|
|
4911
|
+
resolveDispatchModelPin,
|
|
4912
|
+
MODEL_ID_CHARSET_RE,
|
|
4913
|
+
// The shared character-class body both MODEL_ID_CHARSET_RE and
|
|
4914
|
+
// MODEL_ID_SANITIZE_STRIP_RE are derived from — exported so a test can
|
|
4915
|
+
// assert its own expected charset literal EQUALS this value, making a
|
|
4916
|
+
// silent widening of the production body fail the test instead of only
|
|
4917
|
+
// the (unexported) regexes built from it.
|
|
4918
|
+
MODEL_ID_CHARSET_BODY,
|
|
4919
|
+
// Non-global companion of the internal g-flagged sanitize regex — see the
|
|
4920
|
+
// comment at its definition for why the g-flagged instance is never
|
|
4921
|
+
// exported.
|
|
4922
|
+
MODEL_ID_SANITIZE_STRIP_RE,
|
|
4923
|
+
MODEL_ID_MAX_LENGTH,
|
|
4408
4924
|
};
|
|
4409
4925
|
|