@opengsd/gsd-core 1.10.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 +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- 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-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- 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 +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- 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/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- 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 +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -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-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- 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-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -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 +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- 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/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- 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 +44 -26
- 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 +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +17 -11
- 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 +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- 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-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -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 +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.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 +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- 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/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -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 +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 +13 -20
- 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 +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- 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 +4 -3
- 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 +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- 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 +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -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 +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- 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 +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -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 +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- 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 +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- 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 +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- 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 +53 -13
- 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 +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- 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 +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- 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 +2 -3
- 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 +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- 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 +162 -46
- 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-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- 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 +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- 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 +162 -46
- 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-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -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 +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- 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-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- 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/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- 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 +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +471 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +488 -0
- 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-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
21
21
|
* find-phase <phase> Find phase directory by number
|
|
22
22
|
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
|
23
|
+
* commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
|
|
24
|
+
* that refuses a commit staging
|
|
25
|
+
* .planning/ when commit_docs is false
|
|
23
26
|
* commit-to-subrepo <msg> --files f1 f2 Route commits to sub-repos
|
|
24
27
|
* verify-summary <path> Verify a SUMMARY.md file
|
|
25
28
|
* generate-slug <text> Convert text to URL-safe slug
|
|
@@ -65,9 +68,22 @@
|
|
|
65
68
|
* gaps_found-only, never call on the pass path
|
|
66
69
|
*
|
|
67
70
|
* Milestone Operations:
|
|
68
|
-
* 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)
|
|
69
77
|
* [--name <name>]
|
|
70
78
|
* [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
|
|
79
|
+
* [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
80
|
+
* Quick Tasks Completed table (#2142; opt-in, default OFF)
|
|
81
|
+
*
|
|
82
|
+
* milestone archive-quick <version> Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
83
|
+
* Quick Tasks Completed table, WITHOUT the milestone complete close-out
|
|
84
|
+
* (no ROADMAP/REQUIREMENTS/MILESTONES.md writes, no completion guards);
|
|
85
|
+
* safe against an already-completed milestone (#2142 escalation)
|
|
86
|
+
* [--dry-run] Preview what would move, mutates nothing
|
|
71
87
|
*
|
|
72
88
|
* User Story Validation:
|
|
73
89
|
* user-story validate --story "..." Validate "As a / I want to / so that" format
|
|
@@ -80,12 +96,22 @@
|
|
|
80
96
|
* drift-guard severity --status <S> Classify a symbol verdict into { severity, hardBlock }
|
|
81
97
|
* [--authority <A>] Status: VERIFIED|MISSING|AMBIGUOUS|UNCHECKABLE
|
|
82
98
|
* Authority: grep|intel|treesitter|lsp|scip (default: config-resolved)
|
|
99
|
+
* drift-guard phase-status [--phase N] Compare STATE.md vs ROADMAP.md phase status
|
|
83
100
|
*
|
|
84
101
|
* Validation:
|
|
85
102
|
* validate consistency Check phase numbering, disk/roadmap sync
|
|
86
103
|
* validate health [--repair] Check .planning/ integrity, optionally repair
|
|
87
104
|
* validate agents Check GSD agent installation status
|
|
88
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
|
+
*
|
|
89
115
|
* Progress:
|
|
90
116
|
* progress [json|table|bar] Render progress in various formats
|
|
91
117
|
*
|
|
@@ -228,13 +254,22 @@ try {
|
|
|
228
254
|
process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
|
|
229
255
|
// Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
|
|
230
256
|
// lives in ./lib) is available to load, so a direct exit is the only option.
|
|
231
|
-
//
|
|
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
|
|
232
267
|
process.exit(1);
|
|
233
268
|
}
|
|
234
269
|
|
|
235
|
-
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
270
|
+
const { ExitError, runMain, resolveContractVersion } = require('./lib/cli-exit.cjs');
|
|
236
271
|
const io = require('./lib/io.cjs');
|
|
237
|
-
const { error, ERROR_REASON, setJsonErrorMode, output } = io;
|
|
272
|
+
const { error, ERROR_REASON, setJsonErrorMode, output, formatDiagnosticToken } = io;
|
|
238
273
|
const projectRoot = require('./lib/project-root.cjs');
|
|
239
274
|
// Resolve findProjectRoot lazily at call time rather than binding it at module
|
|
240
275
|
// load. It is sourced from project-root.cjs; a call-time lookup is robust
|
|
@@ -259,8 +294,7 @@ try {
|
|
|
259
294
|
}
|
|
260
295
|
} catch { /* advisory — never block */ }
|
|
261
296
|
|
|
262
|
-
const {
|
|
263
|
-
const { resolveActiveWorkstream, applyResolvedWorkstreamEnv } = require('./lib/active-workstream-store.cjs');
|
|
297
|
+
const { resolveActiveWorkstream, applyResolvedWorkstreamEnv, peekActiveWorkstream } = require('./lib/active-workstream-store.cjs');
|
|
264
298
|
const state = require('./lib/state.cjs');
|
|
265
299
|
const phase = require('./lib/phase.cjs');
|
|
266
300
|
const roadmap = require('./lib/roadmap.cjs');
|
|
@@ -275,6 +309,7 @@ const estimateCli = require('./lib/estimate-cli.cjs');
|
|
|
275
309
|
const template = require('./lib/template.cjs');
|
|
276
310
|
const milestone = require('./lib/milestone.cjs');
|
|
277
311
|
const commands = require('./lib/commands.cjs');
|
|
312
|
+
const runtimeIdentity = require('./lib/runtime-identity.cjs');
|
|
278
313
|
const init = require('./lib/init.cjs');
|
|
279
314
|
const frontmatter = require('./lib/frontmatter.cjs');
|
|
280
315
|
const workstream = require('./lib/workstream.cjs');
|
|
@@ -286,6 +321,7 @@ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
|
|
|
286
321
|
const { routeEvalCommand } = require('./lib/eval-command-router.cjs');
|
|
287
322
|
const evalMod = require('./lib/eval.cjs');
|
|
288
323
|
const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
|
|
324
|
+
const { routePlanningCommand } = require('./lib/planning-command-router.cjs');
|
|
289
325
|
const verification = require('./lib/verification.cjs');
|
|
290
326
|
const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
|
291
327
|
// Stale-bake guard (#1688): warns once when model config changed since agents
|
|
@@ -303,9 +339,9 @@ const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-comman
|
|
|
303
339
|
const smartEntryMod = require('./lib/smart-entry.cjs');
|
|
304
340
|
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
305
341
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
306
|
-
const {
|
|
342
|
+
const { parseNamedArgsOrExit, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
|
|
307
343
|
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
|
308
|
-
const { getEffectiveAuthority, classifyDriftSeverity } = require('./lib/plan-drift-guard.cjs');
|
|
344
|
+
const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
|
|
309
345
|
|
|
310
346
|
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
|
|
311
347
|
// Non-family commands now run through their CJS handlers directly. Keep the
|
|
@@ -921,6 +957,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
921
957
|
commands.cmdCheckCommit(cwd, raw);
|
|
922
958
|
}
|
|
923
959
|
|
|
960
|
+
function routeCommitDocsGuard({ args, cwd, raw, error }) {
|
|
961
|
+
const subcommand = args[1];
|
|
962
|
+
if (subcommand === 'enable') {
|
|
963
|
+
commands.cmdCommitDocsGuardEnable(cwd, raw);
|
|
964
|
+
} else if (subcommand === 'disable') {
|
|
965
|
+
commands.cmdCommitDocsGuardDisable(cwd, raw);
|
|
966
|
+
} else {
|
|
967
|
+
error('Unknown commit-docs-guard subcommand. Available: enable, disable', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
924
971
|
function routeCommitToSubrepo({ args, cwd, raw, error }) {
|
|
925
972
|
const message = args[1];
|
|
926
973
|
const filesIndex = args.indexOf('--files');
|
|
@@ -930,7 +977,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
930
977
|
|
|
931
978
|
function routePrSubrepo({ args, cwd, raw, error }) {
|
|
932
979
|
const message = args[1];
|
|
933
|
-
|
|
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);
|
|
934
990
|
commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
|
|
935
991
|
}
|
|
936
992
|
|
|
@@ -947,7 +1003,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
947
1003
|
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
948
1004
|
} else if (subcommand === 'fill') {
|
|
949
1005
|
const templateType = args[2];
|
|
950
|
-
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);
|
|
951
1007
|
let fields = {};
|
|
952
1008
|
if (fieldsRaw) {
|
|
953
1009
|
const { safeJsonParse } = require('./lib/security.cjs');
|
|
@@ -996,14 +1052,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
996
1052
|
}
|
|
997
1053
|
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
998
1054
|
if (subcommand === 'get') {
|
|
999
|
-
frontmatter.cmdFrontmatterGet(cwd, file,
|
|
1055
|
+
frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['field'], positionals: 3 }, error).field, raw);
|
|
1000
1056
|
} else if (subcommand === 'set') {
|
|
1001
|
-
const { field, value } =
|
|
1057
|
+
const { field, value } = parseNamedArgsOrExit(args, { valueFlags: ['field', 'value'], positionals: 3 }, error);
|
|
1002
1058
|
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
1003
1059
|
} else if (subcommand === 'merge') {
|
|
1004
|
-
frontmatter.cmdFrontmatterMerge(cwd, file,
|
|
1060
|
+
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['data'], positionals: 3 }, error).data, raw);
|
|
1005
1061
|
} else if (subcommand === 'validate') {
|
|
1006
|
-
frontmatter.cmdFrontmatterValidate(cwd, file,
|
|
1062
|
+
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['schema'], positionals: 3 }, error).schema, raw);
|
|
1007
1063
|
} else {
|
|
1008
1064
|
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1009
1065
|
}
|
|
@@ -1047,6 +1103,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1047
1103
|
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1048
1104
|
}
|
|
1049
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
|
+
|
|
1050
1115
|
function routeSkillsRoot({ args, raw, error }) {
|
|
1051
1116
|
// #3024: resolve the global skills base directory for a runtime.
|
|
1052
1117
|
// The sync-skills workflow previously shelled out to install.js --skills-root,
|
|
@@ -1118,10 +1183,38 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1118
1183
|
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
|
1119
1184
|
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
|
1120
1185
|
const qtaArgs = args.slice(1);
|
|
1121
|
-
|
|
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];
|
|
1122
1211
|
if (!qtaTask) {
|
|
1123
1212
|
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
|
1124
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);
|
|
1125
1218
|
|
|
1126
1219
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1127
1220
|
if (!fs.existsSync(statePath)) {
|
|
@@ -1147,8 +1240,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1147
1240
|
// still releases the lock before the throw propagates; the transform
|
|
1148
1241
|
// throws before returning new content, so nothing is ever written).
|
|
1149
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.
|
|
1150
1250
|
state.readModifyWriteStateMd(statePath, (content) => {
|
|
1151
|
-
const result = appendQuickTaskRow(content, {
|
|
1251
|
+
const result = appendQuickTaskRow(content, {
|
|
1252
|
+
description: qtaTask,
|
|
1253
|
+
date,
|
|
1254
|
+
commit,
|
|
1255
|
+
quickId: qtaQuickId,
|
|
1256
|
+
directory: qtaDirectory,
|
|
1257
|
+
});
|
|
1152
1258
|
if (!result.ok) {
|
|
1153
1259
|
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
|
1154
1260
|
// is an expected, recoverable condition (no table / unrecognized
|
|
@@ -1159,7 +1265,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1159
1265
|
}
|
|
1160
1266
|
mutation = result.value;
|
|
1161
1267
|
return result.value.content;
|
|
1162
|
-
}, cwd);
|
|
1268
|
+
}, cwd, { resync: false });
|
|
1163
1269
|
|
|
1164
1270
|
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
|
1165
1271
|
}
|
|
@@ -1285,30 +1391,45 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1285
1391
|
return;
|
|
1286
1392
|
}
|
|
1287
1393
|
|
|
1288
|
-
// Effort argv is resolved per lane by the host's own execution policy,
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1394
|
+
// Effort argv is resolved per lane by the host's own execution policy, through the SAME
|
|
1395
|
+
// `resolve-execution` surface the bash legs used (`--host <slug>`), so the host's negotiated
|
|
1396
|
+
// effortSurface still decides whether an argument is emitted and the catalog still owns the
|
|
1397
|
+
// syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
|
|
1398
|
+
// stdout and exits, so it cannot be called in-process for a value — this spawns the same
|
|
1399
|
+
// bounded query the legs did, once per selected lane. A lane whose slug is not a known host
|
|
1400
|
+
// resolves to no effort argument at all.
|
|
1401
|
+
//
|
|
1402
|
+
// NOT `--raw` and NOT `--pick` (#2295). `--raw` prints only the resolved EFFORT ('low') with
|
|
1403
|
+
// no host-specific rendering at all. `--pick effort_argv_string` used to be the answer — the
|
|
1404
|
+
// rendered array re-joined into a string ('-c model_reasoning_effort=low') — but the caller
|
|
1405
|
+
// then had to `.split(/\s+/)` that string back apart to get an argv array, and re-splitting a
|
|
1406
|
+
// string the callee just joined is a lossy round trip: any argv element that legitimately
|
|
1407
|
+
// contains a space would come back split into two argv elements, corrupting the very argv it
|
|
1408
|
+
// was rendered to preserve. Reading the UNPICKED object instead gives both `effort_argv` (a
|
|
1409
|
+
// real string array, used verbatim, no re-splitting) and `effort_argv_value` (the bare level,
|
|
1410
|
+
// #2295's `plan.effort`) from the one spawn.
|
|
1411
|
+
const EMPTY_EFFORT = { argv: [], value: null };
|
|
1297
1412
|
const effortFor = (slug) => {
|
|
1298
1413
|
try {
|
|
1299
1414
|
const r = cp.spawnSync(
|
|
1300
1415
|
process.execPath,
|
|
1301
|
-
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker',
|
|
1302
|
-
// NOT `--raw`: that prints the resolved EFFORT ('low'), ignoring --pick. The picked
|
|
1303
|
-
// field is what carries the host-specific syntax ('--effort low' for claude,
|
|
1304
|
-
// '-c model_reasoning_effort=low' for codex), which is the whole point of asking.
|
|
1305
|
-
'--host', slug, '--pick', 'effort_argv_string'],
|
|
1416
|
+
[__filename, 'query', 'resolve-execution', 'gsd-plan-checker', '--host', slug],
|
|
1306
1417
|
{ cwd, encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 },
|
|
1307
1418
|
);
|
|
1308
|
-
if (r.status !== 0) return
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1419
|
+
if (r.status !== 0) return EMPTY_EFFORT;
|
|
1420
|
+
let parsed;
|
|
1421
|
+
try {
|
|
1422
|
+
parsed = JSON.parse(String(r.stdout || ''));
|
|
1423
|
+
} catch { return EMPTY_EFFORT; }
|
|
1424
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
|
|
1425
|
+
const argv = Array.isArray(parsed.effort_argv)
|
|
1426
|
+
? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
|
|
1427
|
+
: [];
|
|
1428
|
+
const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
|
|
1429
|
+
? parsed.effort_argv_value
|
|
1430
|
+
: null;
|
|
1431
|
+
return { argv, value };
|
|
1432
|
+
} catch { return EMPTY_EFFORT; }
|
|
1312
1433
|
};
|
|
1313
1434
|
|
|
1314
1435
|
/**
|
|
@@ -1341,7 +1462,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1341
1462
|
// so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
|
|
1342
1463
|
let r;
|
|
1343
1464
|
try {
|
|
1344
|
-
|
|
1465
|
+
const effort = effortFor(slug);
|
|
1466
|
+
r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
|
|
1345
1467
|
} catch (e) {
|
|
1346
1468
|
return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
|
|
1347
1469
|
}
|
|
@@ -1385,10 +1507,23 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1385
1507
|
// ENOENT (CreateProcess cannot start .cmd). Apply the same #2667 shim
|
|
1386
1508
|
// gate used in runWithTimeout: detect .cmd/.bat and mediate through
|
|
1387
1509
|
// cmd.exe /d /s /c with an explicit argv array (no shell:true).
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1510
|
+
//
|
|
1511
|
+
// #3275: descriptors declare BARE names, so the gate above never saw an
|
|
1512
|
+
// extension — resolve through the shared PATH+PATHEXT resolver FIRST
|
|
1513
|
+
// (the same one `hasBinary` uses, so probe and spawn can never disagree
|
|
1514
|
+
// about what the lane's binary is). POSIX keeps the bare name: Node's own
|
|
1515
|
+
// PATH search already worked there, and the #3275 acceptance contract
|
|
1516
|
+
// holds macOS/Linux behavior unchanged. A name that resolves to nothing
|
|
1517
|
+
// falls back to the declared name so the ENOENT still surfaces (#3086).
|
|
1518
|
+
// #3411: the resolve-then-mediate pair is one seam call now. Both halves had
|
|
1519
|
+
// private copies here; `projectSpawnInvocation` owns them, so a fix to either
|
|
1520
|
+
// reaches every spawn site instead of only this one.
|
|
1521
|
+
//
|
|
1522
|
+
// Unlike execTool, this lane adopts the RESOLVED path even for a non-batch
|
|
1523
|
+
// binary: that is the behavior #3445 shipped and `deps.hasBinary` answers
|
|
1524
|
+
// from the same resolver, so probe and spawn must agree on the exact file.
|
|
1525
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
1526
|
+
const { command: spawnBinary, args: spawnArgv, windowsVerbatimArguments } = projectSpawnInvocation(binary, argv);
|
|
1392
1527
|
const r = cp.spawnSync(spawnBinary, spawnArgv, {
|
|
1393
1528
|
input: opts.input,
|
|
1394
1529
|
encoding: 'utf8',
|
|
@@ -1396,6 +1531,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1396
1531
|
killSignal: 'SIGKILL',
|
|
1397
1532
|
maxBuffer: 64 * 1024 * 1024,
|
|
1398
1533
|
shell: false, // argv array only — never a shell string (no interpolation of config values)
|
|
1534
|
+
// #2483: a lane's declared env pairs merged OVER this process's environment, for this
|
|
1535
|
+
// child only. Passing a fresh object leaves `process.env` untouched, so nothing leaks
|
|
1536
|
+
// into the orchestrating session or into the next lane.
|
|
1537
|
+
...(opts.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
1538
|
+
...(windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1399
1539
|
});
|
|
1400
1540
|
return {
|
|
1401
1541
|
status: r.status,
|
|
@@ -1424,24 +1564,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1424
1564
|
// all (a probe that costs a process is a probe you avoid running, which is how the original
|
|
1425
1565
|
// Kimi probe ended up unbounded), and `shell: true` with an args array is deprecated in
|
|
1426
1566
|
// Node 26 (DEP0190) because the arguments are concatenated rather than escaped.
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
1435
|
-
for (const ext of exts) {
|
|
1436
|
-
const candidate = path.join(dir, name + ext);
|
|
1437
|
-
try {
|
|
1438
|
-
const st = fsx.statSync(candidate);
|
|
1439
|
-
if (st.isFile()) return true;
|
|
1440
|
-
} catch { /* next candidate */ }
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
return false;
|
|
1444
|
-
},
|
|
1567
|
+
//
|
|
1568
|
+
// #3275: the scan lives in `resolveSpawnBinary` now, SHARED with `deps.spawn`
|
|
1569
|
+
// above. Two private copies of "what is this declared binary?" is how the
|
|
1570
|
+
// defect hid: the probe resolved WITH PATHEXT while spawn resolved WITHOUT,
|
|
1571
|
+
// so a lane reported available for a spawn that could never start. One
|
|
1572
|
+
// resolver, both seams — if one changes, the other changes with it.
|
|
1573
|
+
hasBinary: (name) => resolveSpawnBinary(name) !== null,
|
|
1445
1574
|
configGet,
|
|
1446
1575
|
homeDir: os.homedir(),
|
|
1447
1576
|
warn: (m) => process.stderr.write(`${m}\n`),
|
|
@@ -1493,12 +1622,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1493
1622
|
`model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
|
|
1494
1623
|
);
|
|
1495
1624
|
}
|
|
1625
|
+
const instanceEffort = effortFor(entry.slug);
|
|
1496
1626
|
const overridden = resolveLanePlan({
|
|
1497
1627
|
lane,
|
|
1498
1628
|
configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
|
|
1499
1629
|
runDir,
|
|
1500
1630
|
repoRoot,
|
|
1501
|
-
effortArgs:
|
|
1631
|
+
effortArgs: instanceEffort.argv,
|
|
1632
|
+
effortValue: instanceEffort.value,
|
|
1502
1633
|
});
|
|
1503
1634
|
if (overridden.ok) {
|
|
1504
1635
|
// Preserve any instance retargeting already applied above.
|
|
@@ -1575,7 +1706,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1575
1706
|
function routeDispatchShouldFlatten({ args, cwd, raw, error }) {
|
|
1576
1707
|
// #1708 / #853: typed query replacing the `RUNTIME === 'codex'` prose rule.
|
|
1577
1708
|
//
|
|
1578
|
-
// Resolves the current runtime (GSD_RUNTIME > config.runtime >
|
|
1709
|
+
// Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
|
|
1710
|
+
// .gsd-runtime marker > 'claude'),
|
|
1579
1711
|
// looks up registry.runtimes[id].runtime.hostIntegration.dispatch, and
|
|
1580
1712
|
// calls shouldFlattenDispatch(dispatch) from host-integration.cjs.
|
|
1581
1713
|
//
|
|
@@ -1621,6 +1753,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1621
1753
|
}
|
|
1622
1754
|
}
|
|
1623
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
|
+
|
|
1624
1781
|
function routeDispatchIsolation({ args, cwd, raw, error }) {
|
|
1625
1782
|
// #2584 Phase 3 (#2627): typed query exposing the negotiated
|
|
1626
1783
|
// `dispatch.isolation` to the execute-phase wave scheduler, so the
|
|
@@ -1629,7 +1786,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1629
1786
|
// #853) for exactly this reason — it replaced a `RUNTIME === 'codex'`
|
|
1630
1787
|
// prose rule.
|
|
1631
1788
|
//
|
|
1632
|
-
// Resolves the current runtime (GSD_RUNTIME > config.runtime >
|
|
1789
|
+
// Resolves the current runtime (GSD_RUNTIME > config.runtime > per-install
|
|
1790
|
+
// .gsd-runtime marker > 'claude'),
|
|
1633
1791
|
// reads registry.runtimes[id].runtime.hostIntegration.dispatch.isolation,
|
|
1634
1792
|
// and validates it against the closed vocabulary.
|
|
1635
1793
|
//
|
|
@@ -1664,12 +1822,251 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1664
1822
|
// `per-plan-worktree-gate.md`) override the naturally-resolved mode
|
|
1665
1823
|
// while still going through this single write path. Best-effort: a
|
|
1666
1824
|
// sentinel write failure here must never fail the wave.
|
|
1667
|
-
const
|
|
1825
|
+
const decision = resolveDispatchIsolationDecision({ args, cwd });
|
|
1826
|
+
const runtimeId = decision.runtimeId;
|
|
1827
|
+
let { isolation, exec, harnessFlag } = decision;
|
|
1828
|
+
|
|
1829
|
+
// `--force-isolation <mode>` overrides the naturally-resolved mode with
|
|
1830
|
+
// context this resolver has no way to see on its own (e.g. the #2474
|
|
1831
|
+
// per-plan submodule intersection). Invalid/unrecognized values are
|
|
1832
|
+
// ignored rather than erroring — this is a best-effort recording call,
|
|
1833
|
+
// not a hard usage gate. Forcing to 'none' clears harnessFlag/exec since
|
|
1834
|
+
// neither applies to sequential dispatch.
|
|
1835
|
+
const forceIdx = args.indexOf('--force-isolation');
|
|
1836
|
+
const forcedIsolation = forceIdx !== -1 ? args[forceIdx + 1] : undefined;
|
|
1837
|
+
if (forcedIsolation && DISPATCH_ISOLATION_VOCABULARY.has(forcedIsolation)) {
|
|
1838
|
+
isolation = forcedIsolation;
|
|
1839
|
+
if (isolation === 'none') {
|
|
1840
|
+
harnessFlag = null;
|
|
1841
|
+
exec = null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const phaseIdx = args.indexOf('--phase');
|
|
1846
|
+
const phaseArg = phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--')
|
|
1847
|
+
? args[phaseIdx + 1]
|
|
1848
|
+
: null;
|
|
1849
|
+
const planIdx = args.indexOf('--plan');
|
|
1850
|
+
const planArg = planIdx !== -1 && args[planIdx + 1] && !args[planIdx + 1].startsWith('--')
|
|
1851
|
+
? args[planIdx + 1]
|
|
1852
|
+
: null;
|
|
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
|
+
|
|
1873
|
+
// Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
|
|
1874
|
+
// Never allowed to affect this query's own stdout contract or throw.
|
|
1875
|
+
try {
|
|
1876
|
+
writeDispatchIsolationSentinel(cwd, { isolation, harnessFlag, phase: phaseArg, plan: planArg });
|
|
1877
|
+
} catch {
|
|
1878
|
+
// writeDispatchIsolationSentinel already swallows its own errors into
|
|
1879
|
+
// a { recorded: false } result; this catch is defense in depth only.
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (args.indexOf('--json') !== -1) {
|
|
1883
|
+
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
1884
|
+
} else {
|
|
1885
|
+
process.stdout.write(isolation);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
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
|
+
|
|
2038
|
+
const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* Shared, side-effect-free resolution of the negotiated dispatch isolation:
|
|
2042
|
+
* runtime (GSD_RUNTIME > config.runtime > per-install .gsd-runtime marker >
|
|
2043
|
+
* 'claude') → declared
|
|
2044
|
+
* `dispatch.isolation` → harness-flag / orchestrator-exec degrade rules.
|
|
2045
|
+
* Extracted (#2486) so `routeDispatchIsolation` (the #3045 recording
|
|
2046
|
+
* dispatch path) and `routeInspectDispatchIsolation` (the read-only
|
|
2047
|
+
* inspection path) share exactly one negotiation implementation and cannot
|
|
2048
|
+
* drift apart. Resolution only — the caller decides whether the decision is
|
|
2049
|
+
* recorded to the sentinel.
|
|
2050
|
+
*/
|
|
2051
|
+
function resolveDispatchIsolationDecision({ args, cwd }) {
|
|
1668
2052
|
let isolation = 'none';
|
|
1669
2053
|
let runtimeId = null;
|
|
1670
2054
|
let exec = null;
|
|
1671
2055
|
let harnessFlag = null;
|
|
1672
2056
|
try {
|
|
2057
|
+
// Deliberately `resolveRuntime`, NOT `resolveActiveRuntime`/`loadConfig`:
|
|
2058
|
+
// loadConfig normalizes and rewrites legacy keys back to disk, and this
|
|
2059
|
+
// resolver backs the sentinel-free `inspect-dispatch-isolation` verb,
|
|
2060
|
+
// which must never write. resolveRuntime reads config.json directly.
|
|
2061
|
+
//
|
|
2062
|
+
// KNOWN LIMITATION, tracked separately: resolveRuntime stops at
|
|
2063
|
+
// GSD_RUNTIME > config.runtime > 'claude' and does not consult the
|
|
2064
|
+
// per-install `.gsd-runtime` marker, so on a non-Claude install whose
|
|
2065
|
+
// project config carries no `runtime` key this resolves 'claude'. That is
|
|
2066
|
+
// open-gsd/gsd-core#2395 — a pre-existing defect in the canonical
|
|
2067
|
+
// resolver, not introduced here, and deliberately NOT fixed in this PR
|
|
2068
|
+
// (its blast radius reaches every consumer of that resolver, so it is
|
|
2069
|
+
// being handled on its own).
|
|
1673
2070
|
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
1674
2071
|
runtimeId = resolveRuntime(cwd);
|
|
1675
2072
|
|
|
@@ -1678,7 +2075,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1678
2075
|
? registry.runtimes[runtimeId]
|
|
1679
2076
|
: null;
|
|
1680
2077
|
const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.isolation ?? null;
|
|
1681
|
-
if (typeof declared === 'string' &&
|
|
2078
|
+
if (typeof declared === 'string' && DISPATCH_ISOLATION_VOCABULARY.has(declared)) {
|
|
1682
2079
|
isolation = declared;
|
|
1683
2080
|
}
|
|
1684
2081
|
|
|
@@ -1701,14 +2098,67 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1701
2098
|
const promptIdx = args.indexOf('--prompt');
|
|
1702
2099
|
const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined;
|
|
1703
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
|
+
}
|
|
1704
2145
|
const resolution = hostIntegration.resolveOrchestratorExec(
|
|
1705
2146
|
runtimeEntry?.runtime?.orchestratorExec,
|
|
1706
2147
|
cwdTarget,
|
|
1707
2148
|
promptArg,
|
|
2149
|
+
model,
|
|
1708
2150
|
);
|
|
1709
|
-
// A host declaring orchestrator-worktree whose exec descriptor does
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
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.
|
|
1712
2162
|
if (resolution.ok) {
|
|
1713
2163
|
exec = { command: resolution.command, args: resolution.args, cwd: resolution.cwd };
|
|
1714
2164
|
} else {
|
|
@@ -1721,41 +2171,52 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1721
2171
|
exec = null;
|
|
1722
2172
|
harnessFlag = null;
|
|
1723
2173
|
}
|
|
2174
|
+
return { runtimeId, isolation, exec, harnessFlag };
|
|
2175
|
+
}
|
|
1724
2176
|
|
|
1725
|
-
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2177
|
+
function routeInspectDispatchIsolation({ args, cwd, raw }) {
|
|
2178
|
+
// #2486: sentinel-free sibling of `dispatch-isolation` for INSPECTION
|
|
2179
|
+
// surfaces — /gsd:health's W025 check and /gsd:settings' Worktrees
|
|
2180
|
+
// branching. The dispatch verb above intentionally records its resolved
|
|
2181
|
+
// decision to the isolation sentinel as an unconditional side effect
|
|
2182
|
+
// (#3045 CORE REDESIGN): correct for executor dispatch, where the record
|
|
2183
|
+
// must be structurally unskippable — but wrong for a read-only
|
|
2184
|
+
// diagnostic. A health check that records a phase:null/plan:null
|
|
2185
|
+
// sentinel can hard-block every executor dispatch for the sentinel's
|
|
2186
|
+
// lifetime, across sessions sharing the main checkout. Inspection
|
|
2187
|
+
// surfaces call this verb instead. Two claims, both narrower than
|
|
2188
|
+
// "side-effect-free", and both exactly true (#2486 review, Majors 2 & 4):
|
|
2189
|
+
//
|
|
2190
|
+
// 1. SENTINEL-FREE, not write-free. This route writes nothing itself, and
|
|
2191
|
+
// in particular never writes .gsd/dispatch-isolation-sentinel.json —
|
|
2192
|
+
// the only write that can hard-block a later executor dispatch. It is
|
|
2193
|
+
// NOT an unconditional claim of total filesystem purity: like every
|
|
2194
|
+
// gsd-tools invocation, it runs the shared bootstrap and
|
|
2195
|
+
// active-workstream resolution first. As of #3579's root-cause fix
|
|
2196
|
+
// that bootstrap resolves via the non-mutating peekActiveWorkstream
|
|
2197
|
+
// (never unlinks); an actual stale/invalid pointer is still
|
|
2198
|
+
// self-healed, but only by whichever verb's own getActiveWorkstream
|
|
2199
|
+
// call later consumes it for real — this inspection route makes no
|
|
2200
|
+
// such call, so it is now also side-effect-free on the pointer file.
|
|
2201
|
+
//
|
|
2202
|
+
// 2. SHARED NEGOTIATION, for the arguments this verb accepts. Both verbs
|
|
2203
|
+
// call resolveDispatchIsolationDecision, so the natural resolution
|
|
2204
|
+
// cannot drift. It is NOT a claim of byte-identical output for every
|
|
2205
|
+
// argv: routeDispatchIsolation applies --force-isolation AFTER the
|
|
2206
|
+
// shared helper returns, so the same argv could otherwise yield
|
|
2207
|
+
// 'none' there and the declared capability here. Rather than let a
|
|
2208
|
+
// caller receive a silently different answer, this verb REJECTS the
|
|
2209
|
+
// recording-only knobs outright — they exist to be recorded, and a
|
|
2210
|
+
// read has nothing to record.
|
|
2211
|
+
const RECORDING_ONLY_ARGS = ['--force-isolation', '--phase', '--plan'];
|
|
2212
|
+
const rejected = RECORDING_ONLY_ARGS.filter((flag) => args.indexOf(flag) !== -1);
|
|
2213
|
+
if (rejected.length > 0) {
|
|
2214
|
+
error(
|
|
2215
|
+
`inspect-dispatch-isolation: ${rejected.join(', ')} ${rejected.length === 1 ? 'is a' : 'are'} recording-only argument${rejected.length === 1 ? '' : 's'} and cannot be used on the inspection verb — it resolves the runtime's DECLARED capability and records nothing. Use 'query dispatch-isolation' if you need the override applied and the decision recorded.`,
|
|
2216
|
+
ERROR_REASON.USAGE,
|
|
2217
|
+
);
|
|
1757
2218
|
}
|
|
1758
|
-
|
|
2219
|
+
const { runtimeId, isolation, exec, harnessFlag } = resolveDispatchIsolationDecision({ args, cwd });
|
|
1759
2220
|
if (args.indexOf('--json') !== -1) {
|
|
1760
2221
|
output({ runtime: runtimeId, isolation, exec, harnessFlag }, raw);
|
|
1761
2222
|
} else {
|
|
@@ -1906,6 +2367,50 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1906
2367
|
}
|
|
1907
2368
|
}
|
|
1908
2369
|
|
|
2370
|
+
function routeResolveAgent({ args, cwd, raw, error }) {
|
|
2371
|
+
// #1689: resolve a per-plan agent_hint specialist name to the subagent_type
|
|
2372
|
+
// an Agent() call should use. Returns the name unchanged when a
|
|
2373
|
+
// matching agent file exists in the active runtime's agent dir(s);
|
|
2374
|
+
// 'gsd-executor' when the name is absent, blank, or does not resolve.
|
|
2375
|
+
// Fail-closed is the fallback (gsd-executor) — never echo an
|
|
2376
|
+
// unvalidated name, which would make Agent() error and block the wave.
|
|
2377
|
+
//
|
|
2378
|
+
// Output:
|
|
2379
|
+
// --raw (default) -> prints the resolved type (the hint, or 'gsd-executor')
|
|
2380
|
+
// --json -> prints { runtime, requested, resolved, fallback }
|
|
2381
|
+
const FALLBACK = 'gsd-executor';
|
|
2382
|
+
try {
|
|
2383
|
+
const nameIdx = args.indexOf('--name');
|
|
2384
|
+
const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
|
|
2385
|
+
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
2386
|
+
const runtimeId = resolveRuntime(cwd);
|
|
2387
|
+
const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
|
|
2388
|
+
let resolved = FALLBACK;
|
|
2389
|
+
let resolvedOk = false; // true only when resolveAgentHint returned a hit
|
|
2390
|
+
if (requested && !requested.startsWith('-')) {
|
|
2391
|
+
const hit = resolveAgentHint(requested, runtimeId, cwd);
|
|
2392
|
+
if (hit !== null) {
|
|
2393
|
+
resolved = hit;
|
|
2394
|
+
resolvedOk = true;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
// `fallback` = we did NOT honor a resolvable hint (absent/flag-shaped name,
|
|
2398
|
+
// the named agent did not resolve, or resolution errored). Requesting
|
|
2399
|
+
// gsd-executor explicitly and resolving to it is NOT a fallback.
|
|
2400
|
+
const fellBack = !resolvedOk;
|
|
2401
|
+
const jsonIdx = args.indexOf('--json');
|
|
2402
|
+
if (jsonIdx !== -1) {
|
|
2403
|
+
output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
|
|
2404
|
+
} else {
|
|
2405
|
+
process.stdout.write(String(resolved));
|
|
2406
|
+
}
|
|
2407
|
+
} catch {
|
|
2408
|
+
// Fail-closed: degrade to the legacy executor on any error so dispatch
|
|
2409
|
+
// never blocks on resolution.
|
|
2410
|
+
process.stdout.write(FALLBACK);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
1909
2414
|
function routeAgentSkills({ args, cwd, raw, error }) {
|
|
1910
2415
|
// --json emits typed IR { agent_type, block, skills_count } for test assertions
|
|
1911
2416
|
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
|
|
@@ -1962,8 +2467,19 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1962
2467
|
.filter((t) => t.length > 0);
|
|
1963
2468
|
termsOverride = list.length > 0 ? { pluralization: list } : undefined;
|
|
1964
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.
|
|
1965
2477
|
const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
|
|
1966
|
-
|
|
2478
|
+
if (typeof section !== 'string' || section.trim() === '') {
|
|
2479
|
+
output({ skipped: true, reason: 'phase_unresolved' }, raw);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const result = detectAssumptionDelta(section, termsOverride);
|
|
1967
2483
|
output(result, raw);
|
|
1968
2484
|
return;
|
|
1969
2485
|
}
|
|
@@ -2006,9 +2522,24 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2006
2522
|
const force = args.includes('--force');
|
|
2007
2523
|
// #2118: --dry-run prints a preview plan without mutating.
|
|
2008
2524
|
const dryRun = args.includes('--dry-run');
|
|
2009
|
-
|
|
2525
|
+
// #2142: quick-task archival is opt-in (default OFF) — unlike
|
|
2526
|
+
// --no-archive-phases' inverted shape, absence of this flag means
|
|
2527
|
+
// "do nothing" rather than "skip a default-on behavior".
|
|
2528
|
+
const archiveQuick = args.includes('--archive-quick');
|
|
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);
|
|
2534
|
+
} else if (subcommand === 'archive-quick') {
|
|
2535
|
+
// #2142 escalation: narrow archival-only entry point (does NOT
|
|
2536
|
+
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
2537
|
+
// guards) — safe to call against an already-completed milestone,
|
|
2538
|
+
// unlike `milestone complete --archive-quick`.
|
|
2539
|
+
const dryRun = args.includes('--dry-run');
|
|
2540
|
+
milestone.cmdQuickArchive(cwd, args[2], { dryRun }, raw);
|
|
2010
2541
|
} else {
|
|
2011
|
-
error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2542
|
+
error('Unknown milestone subcommand. Available: complete, archive-quick', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2012
2543
|
}
|
|
2013
2544
|
}
|
|
2014
2545
|
|
|
@@ -2021,11 +2552,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2021
2552
|
const subcommand = args[1];
|
|
2022
2553
|
if (subcommand === 'render-checkpoint') {
|
|
2023
2554
|
const uat = require('./lib/uat.cjs');
|
|
2024
|
-
const options =
|
|
2555
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['file'], positionals: 2 }, error);
|
|
2025
2556
|
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
2026
2557
|
} else if (subcommand === 'classify-coverage') {
|
|
2027
2558
|
const coverage = require('./lib/coverage.cjs');
|
|
2028
|
-
const options =
|
|
2559
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['summary', 'file'], positionals: 2 }, error);
|
|
2029
2560
|
coverage.cmdClassify(cwd, options, raw);
|
|
2030
2561
|
} else {
|
|
2031
2562
|
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -2050,8 +2581,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2050
2581
|
|
|
2051
2582
|
function routeScaffold({ args, cwd, raw, error }) {
|
|
2052
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.
|
|
2053
2589
|
const scaffoldOptions = {
|
|
2054
|
-
phase:
|
|
2590
|
+
phase: parseNamedArgsOrExit(args, { valueFlags: ['phase'], positionals: 'rest' }, error).phase,
|
|
2055
2591
|
name: parseMultiwordArg(args, 'name'),
|
|
2056
2592
|
};
|
|
2057
2593
|
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
@@ -2255,9 +2791,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2255
2791
|
);
|
|
2256
2792
|
}
|
|
2257
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;
|
|
2258
2803
|
// WindowsError carries a REASON code; surface it through the structured
|
|
2259
|
-
// error path so tests can assert on the typed reason.
|
|
2260
|
-
// process.exit(1) internally so we never reach the fall-through.
|
|
2804
|
+
// error path so tests can assert on the typed reason.
|
|
2261
2805
|
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
|
|
2262
2806
|
error(e.message || 'broken-windows error', e.reason);
|
|
2263
2807
|
}
|
|
@@ -3283,13 +3827,189 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3283
3827
|
return;
|
|
3284
3828
|
}
|
|
3285
3829
|
|
|
3830
|
+
if (subcommand === 'phase-status') {
|
|
3831
|
+
// #1956: deterministic STATE.md-vs-ROADMAP.md phase-status drift.
|
|
3832
|
+
const { planningDir } = require('./lib/planning-workspace.cjs');
|
|
3833
|
+
const { stateExtractField, stateCurrentPositionSlice } = require('./lib/state-document.cjs');
|
|
3834
|
+
const { findRoadmapProgressTable } = require('./lib/roadmap-parser.cjs');
|
|
3835
|
+
const { phaseKeyFromProse } = require('./lib/phase-id.cjs');
|
|
3836
|
+
// STATE.md's YAML frontmatter carries its own lowercase `status:`
|
|
3837
|
+
// scalar ahead of the body's `## Current Position` prose "Status:"
|
|
3838
|
+
// line; stateExtractField's non-scoped regex would otherwise match
|
|
3839
|
+
// that frontmatter line first (it comes first in the file) and
|
|
3840
|
+
// silently report the wrong value. Strip frontmatter so extraction
|
|
3841
|
+
// is scoped to the body.
|
|
3842
|
+
const { stripFrontmatter } = require('./lib/frontmatter.cjs');
|
|
3843
|
+
|
|
3844
|
+
const phaseIdx = args.indexOf('--phase');
|
|
3845
|
+
const phaseArg = (phaseIdx !== -1 && args[phaseIdx + 1] && !args[phaseIdx + 1].startsWith('--'))
|
|
3846
|
+
? args[phaseIdx + 1]
|
|
3847
|
+
: undefined;
|
|
3848
|
+
|
|
3849
|
+
const dir = planningDir(cwd);
|
|
3850
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
3851
|
+
const roadmapPath = path.join(dir, 'ROADMAP.md');
|
|
3852
|
+
|
|
3853
|
+
let stateContent = null;
|
|
3854
|
+
try {
|
|
3855
|
+
stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
3856
|
+
} catch {
|
|
3857
|
+
// missing_state below
|
|
3858
|
+
}
|
|
3859
|
+
if (stateContent === null) {
|
|
3860
|
+
const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
|
|
3861
|
+
output({
|
|
3862
|
+
verdict: 'uncheckable',
|
|
3863
|
+
reason: 'missing_state',
|
|
3864
|
+
phase,
|
|
3865
|
+
stateStatus: null,
|
|
3866
|
+
roadmapStatus: null,
|
|
3867
|
+
authority: 'STATE.md',
|
|
3868
|
+
}, raw);
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
let roadmapContent = null;
|
|
3873
|
+
try {
|
|
3874
|
+
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
3875
|
+
} catch {
|
|
3876
|
+
// missing_roadmap below
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
const stateBody = stripFrontmatter(stateContent);
|
|
3880
|
+
// #1956 fix: scope extraction to `## Current Position` (or `###`
|
|
3881
|
+
// in the bootstrap template) so a historical `Phase:` / `Status:`
|
|
3882
|
+
// line in an archive section (e.g. `## Session Continuity
|
|
3883
|
+
// Archive`) can't shadow the real one — same #2956 scope state.cts
|
|
3884
|
+
// uses for current_phase, via the shared owner in
|
|
3885
|
+
// state-document.cjs.
|
|
3886
|
+
//
|
|
3887
|
+
// Deliberately NO whole-body fallback here. `state.cts`'s WRITE
|
|
3888
|
+
// path falls back to the whole body when no Current Position
|
|
3889
|
+
// heading is found (legacy behavior it must preserve for
|
|
3890
|
+
// backward-compatible writes) — but that fallback is wrong for a
|
|
3891
|
+
// READ that feeds a drift finding: a STATE.md with no Current
|
|
3892
|
+
// Position heading is exactly the shape that let a stray historical
|
|
3893
|
+
// `Status:` line elsewhere in the body shadow the real value and
|
|
3894
|
+
// fabricate a 'drifted' verdict. A guess is worse than an
|
|
3895
|
+
// abstention for a drift detector, so an absent Current Position
|
|
3896
|
+
// section reports 'uncheckable' instead of guessing from the whole
|
|
3897
|
+
// document.
|
|
3898
|
+
const currentPositionBody = stateCurrentPositionSlice(stateBody);
|
|
3899
|
+
if (currentPositionBody === null) {
|
|
3900
|
+
const phase = phaseArg !== undefined ? phaseKeyFromProse(phaseArg) : null;
|
|
3901
|
+
output({
|
|
3902
|
+
verdict: 'uncheckable',
|
|
3903
|
+
reason: 'no_current_position',
|
|
3904
|
+
phase,
|
|
3905
|
+
stateStatus: null,
|
|
3906
|
+
roadmapStatus: null,
|
|
3907
|
+
authority: 'STATE.md',
|
|
3908
|
+
}, raw);
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// Resolve the target phase: --phase if given, else whatever
|
|
3913
|
+
// STATE.md's Current Position reports as current.
|
|
3914
|
+
const phase = phaseArg !== undefined
|
|
3915
|
+
? phaseKeyFromProse(phaseArg)
|
|
3916
|
+
: phaseKeyFromProse(stateExtractField(currentPositionBody, 'Phase'));
|
|
3917
|
+
|
|
3918
|
+
if (roadmapContent === null) {
|
|
3919
|
+
output({
|
|
3920
|
+
verdict: 'uncheckable',
|
|
3921
|
+
reason: 'missing_roadmap',
|
|
3922
|
+
phase,
|
|
3923
|
+
stateStatus: null,
|
|
3924
|
+
roadmapStatus: null,
|
|
3925
|
+
authority: 'STATE.md',
|
|
3926
|
+
}, raw);
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
const stateStatus = stateExtractField(currentPositionBody, 'Status');
|
|
3931
|
+
|
|
3932
|
+
// #1956/#2012: scoped to `## Progress` first (decoy-avoidance) —
|
|
3933
|
+
// see findRoadmapProgressTable's doc comment (roadmap-parser.cts).
|
|
3934
|
+
const table = findRoadmapProgressTable(roadmapContent);
|
|
3935
|
+
const matchedRow = table
|
|
3936
|
+
? table.rows.find((row) => phaseKeyFromProse(row.Phase) === phase && phase !== null)
|
|
3937
|
+
: undefined;
|
|
3938
|
+
|
|
3939
|
+
if (!matchedRow) {
|
|
3940
|
+
const result = comparePhaseStatus({ stateStatus, roadmapStatus: null });
|
|
3941
|
+
output({
|
|
3942
|
+
verdict: 'uncheckable',
|
|
3943
|
+
reason: 'phase_not_in_roadmap',
|
|
3944
|
+
phase,
|
|
3945
|
+
stateStatus,
|
|
3946
|
+
roadmapStatus: null,
|
|
3947
|
+
stateRank: result.stateRank,
|
|
3948
|
+
roadmapRank: result.roadmapRank,
|
|
3949
|
+
authority: 'STATE.md',
|
|
3950
|
+
}, raw);
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
const roadmapStatus = matchedRow.Status;
|
|
3955
|
+
const result = comparePhaseStatus({ stateStatus, roadmapStatus });
|
|
3956
|
+
output({
|
|
3957
|
+
verdict: result.verdict,
|
|
3958
|
+
phase,
|
|
3959
|
+
stateStatus,
|
|
3960
|
+
roadmapStatus,
|
|
3961
|
+
stateRank: result.stateRank,
|
|
3962
|
+
roadmapRank: result.roadmapRank,
|
|
3963
|
+
authority: 'STATE.md',
|
|
3964
|
+
}, raw);
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3286
3968
|
error(
|
|
3287
|
-
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity`,
|
|
3969
|
+
`Unknown drift-guard subcommand: ${subcommand || '(none)'}. Available: authority, severity, phase-status`,
|
|
3288
3970
|
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
|
3289
3971
|
);
|
|
3290
3972
|
}
|
|
3291
3973
|
|
|
3292
3974
|
|
|
3975
|
+
/**
|
|
3976
|
+
* #3275: resolve a DECLARED command name to the file a spawn can actually start.
|
|
3977
|
+
*
|
|
3978
|
+
* Lane descriptors (src/review-lane-descriptor.cts) declare BARE, platform-unaware
|
|
3979
|
+
* binary names ('codex', 'gemini', 'kimi', 'agy'), and `review-lane invoke`'s
|
|
3980
|
+
* `deps.spawn` + `deps.hasBinary` both need the on-disk form of that name. Before
|
|
3981
|
+
* this helper existed they disagreed: `hasBinary` scanned PATH WITH PATHEXT (so
|
|
3982
|
+
* probes reported lanes AVAILABLE on Windows) while `spawn` received the bare name
|
|
3983
|
+
* — `CreateProcess` performs no PATHEXT resolution, so every spawn-transport lane
|
|
3984
|
+
* ENOENT'd there, and the #3086 `.cmd`/`.bat` cmd.exe mediation gate never fired
|
|
3985
|
+
* because the declared name never carried an extension. One shared resolver is the
|
|
3986
|
+
* only shape that cannot drift back apart.
|
|
3987
|
+
*
|
|
3988
|
+
* win32: tries PATHEXT entries ONLY — never the bare name. npm global installs
|
|
3989
|
+
* drop an EXTENSIONLESS POSIX sh shim (`...\npm\codex`) next to `codex.CMD`; a
|
|
3990
|
+
* bare-name-first scan resolves to it, the mediation gate sees no `.cmd`, and the
|
|
3991
|
+
* ENOENT returns unchanged (field-reported on Windows 11 — see the #3275 issue
|
|
3992
|
+
* comment pinning exactly this pitfall).
|
|
3993
|
+
*
|
|
3994
|
+
* POSIX: answers EXISTENCE only (the old `hasBinary` contract, preserved
|
|
3995
|
+
* byte-for-byte) by scanning PATH for the bare name. `deps.spawn` does NOT consult
|
|
3996
|
+
* this on POSIX — the bare name goes to spawnSync unchanged and Node's own PATH
|
|
3997
|
+
* search does the work, so macOS/Linux behavior is untouched (#3275 acceptance).
|
|
3998
|
+
*
|
|
3999
|
+
* Path-like names (any '/' or '\') bypass the PATH scan: the name is already an
|
|
4000
|
+
* address, so it passes through when the file exists and is a file.
|
|
4001
|
+
*
|
|
4002
|
+
* #3411: the scan itself now lives in the declared platform seam
|
|
4003
|
+
* (`src/shell-command-projection.cts` → `resolveExecutableBinary`). This function is
|
|
4004
|
+
* the `bin/` entry point onto it and holds no copy of the logic — `CONTEXT.md`
|
|
4005
|
+
* declares that file "All OS-facing I/O; single platform seam", and a private
|
|
4006
|
+
* duplicate here is what made it untrue.
|
|
4007
|
+
*/
|
|
4008
|
+
function resolveSpawnBinary(name, platform = process.platform, env = process.env) {
|
|
4009
|
+
const { resolveExecutableBinary } = require('./lib/shell-command-projection.cjs');
|
|
4010
|
+
return resolveExecutableBinary(name, { platform, env });
|
|
4011
|
+
}
|
|
4012
|
+
|
|
3293
4013
|
const HOST_COMMAND_ROUTERS = {
|
|
3294
4014
|
// Each entry wraps its `route*Command` router so it receives the module-scope
|
|
3295
4015
|
// lib the old `case` arm passed, plus the per-dispatch context
|
|
@@ -3339,6 +4059,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3339
4059
|
'find-phase': routeFindPhase,
|
|
3340
4060
|
'commit': routeCommit,
|
|
3341
4061
|
'check-commit': routeCheckCommit,
|
|
4062
|
+
'commit-docs-guard': routeCommitDocsGuard,
|
|
3342
4063
|
'commit-to-subrepo': routeCommitToSubrepo,
|
|
3343
4064
|
'pr-subrepo': routePrSubrepo,
|
|
3344
4065
|
'verify-summary': routeVerifySummary,
|
|
@@ -3349,6 +4070,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3349
4070
|
'verification': routeVerification,
|
|
3350
4071
|
'generate-slug': routeGenerateSlug,
|
|
3351
4072
|
'current-timestamp': routeCurrentTimestamp,
|
|
4073
|
+
'runtime-identity': routeRuntimeIdentity,
|
|
3352
4074
|
'project-instruction-file': routeProjectInstructionFile,
|
|
3353
4075
|
'list-todos': routeListTodos,
|
|
3354
4076
|
'list-seeds': routeListSeeds,
|
|
@@ -3357,12 +4079,18 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3357
4079
|
'normalize-test-command': routeNormalizeTestCommand,
|
|
3358
4080
|
'dispatch-should-flatten': routeDispatchShouldFlatten,
|
|
3359
4081
|
'dispatch-isolation': routeDispatchIsolation,
|
|
4082
|
+
'inspect-dispatch-isolation': routeInspectDispatchIsolation,
|
|
3360
4083
|
'record-dispatch-isolation': routeRecordDispatchIsolation,
|
|
3361
4084
|
'resolve-dispatch-type': routeResolveDispatchType,
|
|
4085
|
+
'resolve-agent': routeResolveAgent,
|
|
3362
4086
|
'agent-skills': routeAgentSkills,
|
|
3363
4087
|
'skill-manifest': routeSkillManifest,
|
|
3364
4088
|
'history-digest': routeHistoryDigest,
|
|
3365
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,
|
|
3366
4094
|
'assumption-delta': routeAssumptionDelta,
|
|
3367
4095
|
'requirements': routeRequirements,
|
|
3368
4096
|
'gap-analysis': routeGapAnalysis,
|
|
@@ -3603,23 +4331,25 @@ function runWithTimeout(argv) {
|
|
|
3603
4331
|
// this string and HOST_COMMAND_ROUTERS/SKIP_ROOT_RESOLUTION are three
|
|
3604
4332
|
// independently hand-maintained sites and nothing previously caught them
|
|
3605
4333
|
// drifting apart when a query command was added to only one or two.
|
|
3606
|
-
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
3607
|
-
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, pr-subrepo, ' +
|
|
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' +
|
|
4335
|
+
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
|
|
3608
4336
|
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
3609
4337
|
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
3610
4338
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
3611
4339
|
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
3612
|
-
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
3613
|
-
'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, ' +
|
|
3614
|
-
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-dispatch-type, ' +
|
|
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, ' +
|
|
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, ' +
|
|
3615
4343
|
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
3616
4344
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
3617
4345
|
'Global flags:\n' +
|
|
3618
4346
|
' --raw Emit raw output without post-processing\n' +
|
|
3619
4347
|
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
3620
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' +
|
|
3621
4350
|
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
3622
|
-
' --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' +
|
|
3623
4353
|
'For command-specific argument requirements, invoke the command without args ' +
|
|
3624
4354
|
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
3625
4355
|
|
|
@@ -3638,6 +4368,10 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
3638
4368
|
// below (never as the live Set itself; see that function's doc comment).
|
|
3639
4369
|
const SKIP_ROOT_RESOLUTION = new Set([
|
|
3640
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',
|
|
3641
4375
|
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
3642
4376
|
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
3643
4377
|
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
@@ -3709,18 +4443,19 @@ function resolveMainWorktreeCwd(cwd, deps = {}) {
|
|
|
3709
4443
|
async function main() {
|
|
3710
4444
|
let args = process.argv.slice(2);
|
|
3711
4445
|
|
|
3712
|
-
//
|
|
3713
|
-
//
|
|
3714
|
-
//
|
|
3715
|
-
//
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
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).
|
|
3724
4459
|
|
|
3725
4460
|
// --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
|
|
3726
4461
|
// JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
|
|
@@ -3740,6 +4475,41 @@ async function main() {
|
|
|
3740
4475
|
setJsonErrorMode(true);
|
|
3741
4476
|
}
|
|
3742
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
|
+
|
|
3743
4513
|
// Optional cwd override for sandboxed subagents running outside project root.
|
|
3744
4514
|
let cwd = process.cwd();
|
|
3745
4515
|
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
@@ -3760,6 +4530,39 @@ async function main() {
|
|
|
3760
4530
|
error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
|
|
3761
4531
|
}
|
|
3762
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
|
+
|
|
3763
4566
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
3764
4567
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
3765
4568
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
@@ -3769,8 +4572,21 @@ async function main() {
|
|
|
3769
4572
|
// Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
|
|
3770
4573
|
let workstreamContext = null;
|
|
3771
4574
|
try {
|
|
4575
|
+
// #3579 root-cause fix: this bootstrap resolution only decides whether to
|
|
4576
|
+
// populate GSD_WORKSTREAM env for downstream routing — it is a check, not
|
|
4577
|
+
// the consuming read. Using the mutating getActiveWorkstream here
|
|
4578
|
+
// self-healed (cleared) a present-but-unresolvable pointer BEFORE the
|
|
4579
|
+
// dispatched command's own resolution/diagnostic ran, so a second read in
|
|
4580
|
+
// the same process (e.g. a subcommand's own getActiveWorkstream call, or
|
|
4581
|
+
// a fail-safe guard's diagnoseUnresolvedActiveWorkstream) observed
|
|
4582
|
+
// already-cleared state — silently falling through to a fallback marker
|
|
4583
|
+
// it should never have inherited (isolation violation), or losing the
|
|
4584
|
+
// evidence a diagnostic needed to explain why nothing resolved. peek
|
|
4585
|
+
// shares the identical resolution logic and only differs by never
|
|
4586
|
+
// calling adapter.clear(); self-heal still happens, exactly once, at
|
|
4587
|
+
// whichever call site actually consumes the workstream for real.
|
|
3772
4588
|
workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
|
|
3773
|
-
getStored:
|
|
4589
|
+
getStored: peekActiveWorkstream,
|
|
3774
4590
|
});
|
|
3775
4591
|
args = workstreamContext.args;
|
|
3776
4592
|
// Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
|
|
@@ -3859,24 +4675,45 @@ async function main() {
|
|
|
3859
4675
|
}
|
|
3860
4676
|
}
|
|
3861
4677
|
|
|
3862
|
-
|
|
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)) {
|
|
3863
4682
|
cwd = findProjectRoot(cwd);
|
|
3864
4683
|
}
|
|
3865
4684
|
|
|
3866
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).
|
|
3867
4692
|
if (pickField) {
|
|
3868
4693
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
3869
4694
|
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
3870
4695
|
});
|
|
3871
4696
|
const resolved = resolveAtFileOutput(captured);
|
|
4697
|
+
let obj;
|
|
3872
4698
|
try {
|
|
3873
|
-
|
|
3874
|
-
const value = extractField(obj, pickField);
|
|
3875
|
-
const result = value === null || value === undefined ? '' : String(value);
|
|
3876
|
-
fs.writeSync(1, result);
|
|
4699
|
+
obj = JSON.parse(resolved);
|
|
3877
4700
|
} catch {
|
|
3878
|
-
|
|
4701
|
+
error(`--pick ${formatDiagnosticToken(pickField)}: command output was not JSON`, ERROR_REASON.PICK_OUTPUT_NOT_JSON);
|
|
4702
|
+
return;
|
|
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;
|
|
3879
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);
|
|
3880
4717
|
return;
|
|
3881
4718
|
}
|
|
3882
4719
|
|
|
@@ -3938,27 +4775,68 @@ function resolveAtFileOutput(captured) {
|
|
|
3938
4775
|
return fs.readFileSync(captured.slice(6), 'utf-8');
|
|
3939
4776
|
}
|
|
3940
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
|
+
|
|
3941
4807
|
/**
|
|
3942
4808
|
* Extract a field from an object using dot-notation and bracket syntax.
|
|
3943
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).
|
|
3944
4818
|
*/
|
|
3945
4819
|
function extractField(obj, fieldPath) {
|
|
3946
4820
|
const parts = fieldPath.split('.');
|
|
3947
4821
|
let current = obj;
|
|
3948
4822
|
for (const part of parts) {
|
|
3949
|
-
if (current === null || current === undefined) return undefined;
|
|
3950
4823
|
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
|
|
3951
4824
|
if (bracketMatch) {
|
|
3952
4825
|
const key = bracketMatch[1];
|
|
3953
4826
|
const index = parseInt(bracketMatch[2], 10);
|
|
3954
|
-
current
|
|
3955
|
-
|
|
3956
|
-
|
|
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];
|
|
3957
4833
|
} else {
|
|
4834
|
+
if (!isPlainRecord(current)) return { found: false, value: undefined };
|
|
4835
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) return { found: false, value: undefined };
|
|
3958
4836
|
current = current[part];
|
|
3959
4837
|
}
|
|
3960
4838
|
}
|
|
3961
|
-
return current;
|
|
4839
|
+
return { found: true, value: current };
|
|
3962
4840
|
}
|
|
3963
4841
|
|
|
3964
4842
|
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
@@ -4024,5 +4902,24 @@ module.exports = {
|
|
|
4024
4902
|
TOP_LEVEL_USAGE,
|
|
4025
4903
|
skipsRootResolution,
|
|
4026
4904
|
resolveMainWorktreeCwd,
|
|
4905
|
+
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
4906
|
+
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
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,
|
|
4027
4924
|
};
|
|
4028
4925
|
|