@opengsd/gsd-core 1.11.0 → 1.13.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/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- 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/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- 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/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -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/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- 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/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- 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/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
- 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 +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- 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 +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- 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 +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- 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 +36 -17
- 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 +210 -1
- 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 +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -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 +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -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-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* list-todos [area] Count and enumerate pending todos
|
|
31
31
|
* list-seeds [status] List captured seeds (optional status filter)
|
|
32
32
|
* verify-path-exists <path> Check file/directory existence
|
|
33
|
+
* quick-tasks-migrate Migrate STATE.md's "Quick Tasks Completed"
|
|
34
|
+
* table onto the canonical schema (#3730).
|
|
35
|
+
* No-op when absent or already canonical.
|
|
33
36
|
* quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
|
|
34
37
|
* Completed" table (schema-backed via
|
|
35
38
|
* markdown-table.cjs; #2133/ADR-2143).
|
|
@@ -68,7 +71,12 @@
|
|
|
68
71
|
* gaps_found-only, never call on the pass path
|
|
69
72
|
*
|
|
70
73
|
* Milestone Operations:
|
|
71
|
-
* milestone complete <version>
|
|
74
|
+
* milestone complete <version> (--confirm | --dry-run)
|
|
75
|
+
* Archive milestone, create MILESTONES.md — one of the two is required
|
|
76
|
+
* --confirm REQUIRED to mutate (#3726): the archive is irreversible (ROADMAP/
|
|
77
|
+
* REQUIREMENTS archived, phase dirs MOVED, STATE.md rewritten), so
|
|
78
|
+
* without this flag the command refuses and mutates nothing
|
|
79
|
+
* --dry-run Preview what would move, mutates nothing (no --confirm needed; #2118)
|
|
72
80
|
* [--name <name>]
|
|
73
81
|
* [--no-archive-phases] Skip moving phase dirs to milestones/vX.Y-phases/ (archived by default)
|
|
74
82
|
* [--archive-quick] Move .planning/quick/* dirs to milestones/vX.Y-quick/ + reset the
|
|
@@ -98,6 +106,15 @@
|
|
|
98
106
|
* validate health [--repair] Check .planning/ integrity, optionally repair
|
|
99
107
|
* validate agents Check GSD agent installation status
|
|
100
108
|
*
|
|
109
|
+
* Planning Snapshot:
|
|
110
|
+
* planning inspect Read-only schema-v1 canonical planning snapshot
|
|
111
|
+
* (milestone identity, active phase, per-phase
|
|
112
|
+
* verification/roadmap-acceptance/UAT evidence kept
|
|
113
|
+
* separate, requirement rows with mapped-phase
|
|
114
|
+
* traceability, plan/task rows with planned+changed
|
|
115
|
+
* file provenance, and independent accepted_phases /
|
|
116
|
+
* completed_plans fractions). Takes no arguments.
|
|
117
|
+
*
|
|
101
118
|
* Progress:
|
|
102
119
|
* progress [json|table|bar] Render progress in various formats
|
|
103
120
|
*
|
|
@@ -240,13 +257,22 @@ try {
|
|
|
240
257
|
process.stderr.write((bootErr && bootErr.message ? bootErr.message : String(bootErr)) + '\n');
|
|
241
258
|
// Fatal bootstrap failure before the CLI's ExitError/runMain machinery (which
|
|
242
259
|
// lives in ./lib) is available to load, so a direct exit is the only option.
|
|
243
|
-
//
|
|
260
|
+
// #3910: this call runs BEFORE ./lib/cli-exit.cjs is even required, so the
|
|
261
|
+
// registered-exit seam (runMain/ExitError/terminateNow) does not exist yet
|
|
262
|
+
// at this point in the process's lifetime — there is nothing to route
|
|
263
|
+
// through. This is the second (and only other) sanctioned allowlist entry
|
|
264
|
+
// for local/require-registered-exit, alongside terminateNow's own body.
|
|
265
|
+
// #3914: n/no-process-exit and local/require-registered-exit are
|
|
266
|
+
// complementary, not predecessor/successor (see eslint.config.mjs and
|
|
267
|
+
// docs/adr/3889-process-exit-contract.md) — both remain 'error' on this
|
|
268
|
+
// glob, so both need a disable directive here.
|
|
269
|
+
// eslint-disable-next-line n/no-process-exit, local/require-registered-exit
|
|
244
270
|
process.exit(1);
|
|
245
271
|
}
|
|
246
272
|
|
|
247
|
-
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
273
|
+
const { ExitError, runMain, resolveContractVersion } = require('./lib/cli-exit.cjs');
|
|
248
274
|
const io = require('./lib/io.cjs');
|
|
249
|
-
const { error, ERROR_REASON, setJsonErrorMode, output } = io;
|
|
275
|
+
const { error, ERROR_REASON, setJsonErrorMode, output, formatDiagnosticToken } = io;
|
|
250
276
|
const projectRoot = require('./lib/project-root.cjs');
|
|
251
277
|
// Resolve findProjectRoot lazily at call time rather than binding it at module
|
|
252
278
|
// load. It is sourced from project-root.cjs; a call-time lookup is robust
|
|
@@ -286,6 +312,7 @@ const estimateCli = require('./lib/estimate-cli.cjs');
|
|
|
286
312
|
const template = require('./lib/template.cjs');
|
|
287
313
|
const milestone = require('./lib/milestone.cjs');
|
|
288
314
|
const commands = require('./lib/commands.cjs');
|
|
315
|
+
const runtimeIdentity = require('./lib/runtime-identity.cjs');
|
|
289
316
|
const init = require('./lib/init.cjs');
|
|
290
317
|
const frontmatter = require('./lib/frontmatter.cjs');
|
|
291
318
|
const workstream = require('./lib/workstream.cjs');
|
|
@@ -297,6 +324,7 @@ const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
|
|
|
297
324
|
const { routeEvalCommand } = require('./lib/eval-command-router.cjs');
|
|
298
325
|
const evalMod = require('./lib/eval.cjs');
|
|
299
326
|
const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
|
|
327
|
+
const { routePlanningCommand } = require('./lib/planning-command-router.cjs');
|
|
300
328
|
const verification = require('./lib/verification.cjs');
|
|
301
329
|
const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
|
302
330
|
// Stale-bake guard (#1688): warns once when model config changed since agents
|
|
@@ -309,12 +337,17 @@ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
|
|
|
309
337
|
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
|
|
310
338
|
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
|
|
311
339
|
const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
|
|
340
|
+
// #3676 (Phase 4, epic #3344): quick-batch is a first-party, always-on
|
|
341
|
+
// command family (like `/gsd:quick`) — wired directly into
|
|
342
|
+
// HOST_COMMAND_ROUTERS, not the opt-in capability-registry/`activationKey`
|
|
343
|
+
// path graphify uses.
|
|
344
|
+
const { routeQuickBatchCommand } = require('./lib/quick-batch-command-router.cjs');
|
|
312
345
|
const { routeCapabilityCommand } = require('./lib/capability-command-router.cjs');
|
|
313
346
|
const { routeAgentCommand, AGENT_FAILURE_CLASSES } = require('./lib/agent-command-router.cjs');
|
|
314
347
|
const smartEntryMod = require('./lib/smart-entry.cjs');
|
|
315
348
|
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
|
|
316
349
|
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
|
|
317
|
-
const {
|
|
350
|
+
const { parseNamedArgsOrExit, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
|
|
318
351
|
const { cmdGitBaseBranch } = require('./lib/git-base-branch.cjs');
|
|
319
352
|
const { getEffectiveAuthority, classifyDriftSeverity, comparePhaseStatus } = require('./lib/plan-drift-guard.cjs');
|
|
320
353
|
|
|
@@ -952,7 +985,16 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
952
985
|
|
|
953
986
|
function routePrSubrepo({ args, cwd, raw, error }) {
|
|
954
987
|
const message = args[1];
|
|
955
|
-
|
|
988
|
+
// #3884: the commit message is an optional leading positional the
|
|
989
|
+
// caller owns (args[1]) — but when it is OMITTED, args[1] is itself
|
|
990
|
+
// the first flag (e.g. `--repo`), and a static `positionals: 2`
|
|
991
|
+
// treats that flag's own value as an unexpected trailing positional
|
|
992
|
+
// before cmdPrSubrepo's own "commit message required" guard ever
|
|
993
|
+
// runs. Widen the boundary only when args[1] genuinely looks like a
|
|
994
|
+
// message (not flag-shaped), mirroring the same fix applied to
|
|
995
|
+
// `state complete-phase`.
|
|
996
|
+
const messagePresent = message !== undefined && !message.startsWith('--');
|
|
997
|
+
const { repo, branch } = parseNamedArgsOrExit(args, { valueFlags: ['repo', 'branch'], positionals: messagePresent ? 2 : 1 }, error);
|
|
956
998
|
commands.cmdPrSubrepo(cwd, repo, branch, message, raw);
|
|
957
999
|
}
|
|
958
1000
|
|
|
@@ -969,7 +1011,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
969
1011
|
template.cmdTemplateSelect(cwd, args[2], raw);
|
|
970
1012
|
} else if (subcommand === 'fill') {
|
|
971
1013
|
const templateType = args[2];
|
|
972
|
-
const { phase, plan, name, type, wave, fields: fieldsRaw } =
|
|
1014
|
+
const { phase, plan, name, type, wave, fields: fieldsRaw } = parseNamedArgsOrExit(args, { valueFlags: ['phase', 'plan', 'name', 'type', 'wave', 'fields'], positionals: 3 }, error);
|
|
973
1015
|
let fields = {};
|
|
974
1016
|
if (fieldsRaw) {
|
|
975
1017
|
const { safeJsonParse } = require('./lib/security.cjs');
|
|
@@ -1018,14 +1060,14 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1018
1060
|
}
|
|
1019
1061
|
// CJS fallback (SDK unavailable or unknown subcommand)
|
|
1020
1062
|
if (subcommand === 'get') {
|
|
1021
|
-
frontmatter.cmdFrontmatterGet(cwd, file,
|
|
1063
|
+
frontmatter.cmdFrontmatterGet(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['field'], positionals: 3 }, error).field, raw);
|
|
1022
1064
|
} else if (subcommand === 'set') {
|
|
1023
|
-
const { field, value } =
|
|
1065
|
+
const { field, value } = parseNamedArgsOrExit(args, { valueFlags: ['field', 'value'], positionals: 3 }, error);
|
|
1024
1066
|
frontmatter.cmdFrontmatterSet(cwd, file, field, value !== null ? value : undefined, raw);
|
|
1025
1067
|
} else if (subcommand === 'merge') {
|
|
1026
|
-
frontmatter.cmdFrontmatterMerge(cwd, file,
|
|
1068
|
+
frontmatter.cmdFrontmatterMerge(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['data'], positionals: 3 }, error).data, raw);
|
|
1027
1069
|
} else if (subcommand === 'validate') {
|
|
1028
|
-
frontmatter.cmdFrontmatterValidate(cwd, file,
|
|
1070
|
+
frontmatter.cmdFrontmatterValidate(cwd, file, parseNamedArgsOrExit(args, { valueFlags: ['schema'], positionals: 3 }, error).schema, raw);
|
|
1029
1071
|
} else {
|
|
1030
1072
|
error('Unknown frontmatter subcommand. Available: get, set, merge, validate', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
1031
1073
|
}
|
|
@@ -1069,6 +1111,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1069
1111
|
commands.cmdCurrentTimestamp(args[1] || 'full', raw);
|
|
1070
1112
|
}
|
|
1071
1113
|
|
|
1114
|
+
function routeRuntimeIdentity({ raw }) {
|
|
1115
|
+
// #3146: report this runtime's package identity so a shipped workflow can
|
|
1116
|
+
// tell whether it reached THIS package's gsd-tools or a colliding one.
|
|
1117
|
+
// Kept on the CJS fast path for the same reason as current-timestamp — the
|
|
1118
|
+
// launcher preamble spawns it once per workflow run, so SDK bridge startup
|
|
1119
|
+
// would be a per-run tax on every workflow.
|
|
1120
|
+
runtimeIdentity.cmdRuntimeIdentity(raw);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1072
1123
|
function routeSkillsRoot({ args, raw, error }) {
|
|
1073
1124
|
// #3024: resolve the global skills base directory for a runtime.
|
|
1074
1125
|
// The sync-skills workflow previously shelled out to install.js --skills-root,
|
|
@@ -1134,16 +1185,72 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1134
1185
|
commands.cmdVerifyPathExists(cwd, args[1], raw);
|
|
1135
1186
|
}
|
|
1136
1187
|
|
|
1188
|
+
function routeQuickTasksMigrate({ cwd, raw }) {
|
|
1189
|
+
// #3730 (option b, maintainer decision 2026-09-02): the supported repair
|
|
1190
|
+
// path for a legacy Quick Tasks table GSD's own pre-registry prose created.
|
|
1191
|
+
// Silent no-op when the section is absent or the table is already canonical
|
|
1192
|
+
// — the quick/fast workflows call this before their first append, so the
|
|
1193
|
+
// migration runs exactly once, on the first quick run, unprompted otherwise.
|
|
1194
|
+
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1195
|
+
if (!fs.existsSync(statePath)) {
|
|
1196
|
+
output({ ok: true, migrated: false, reason: `STATE.md not found at ${statePath}` }, raw);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const { migrateQuickTasksTable } = require('./lib/markdown-table.cjs');
|
|
1200
|
+
let report;
|
|
1201
|
+
state.readModifyWriteStateMd(statePath, (content) => {
|
|
1202
|
+
const result = migrateQuickTasksTable(content);
|
|
1203
|
+
if (!result.ok) {
|
|
1204
|
+
throw new ExitError(1, `⚠ quick-tasks-migrate: ${result.reason}`);
|
|
1205
|
+
}
|
|
1206
|
+
report = result.value;
|
|
1207
|
+
return result.value.content;
|
|
1208
|
+
}, cwd, { resync: false });
|
|
1209
|
+
output({
|
|
1210
|
+
ok: true,
|
|
1211
|
+
migrated: report.migrated,
|
|
1212
|
+
...(report.migrated ? { from: report.from, rows: report.rows } : {}),
|
|
1213
|
+
}, raw);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1137
1216
|
function routeQuickTasksAppend({ args, cwd, raw, error }) {
|
|
1138
1217
|
// #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
|
|
1139
1218
|
// `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
|
|
1140
1219
|
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
|
1141
1220
|
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
|
1142
1221
|
const qtaArgs = args.slice(1);
|
|
1143
|
-
|
|
1222
|
+
// Ambiguous boundary (ADR-3473 §8.4 Item 2 note): this command accepts
|
|
1223
|
+
// EITHER a positional free-text description (qtaArgs[0]) OR --task
|
|
1224
|
+
// <value> — the same token index is a caller-owned positional in one
|
|
1225
|
+
// input shape and a flag in the other, which a single fixed
|
|
1226
|
+
// `positionals` cursor cannot represent. `positionals: 'rest'`
|
|
1227
|
+
// disables the boundary walk (as with `init quick`) so extraction
|
|
1228
|
+
// (used for the --task form) and the `|| args[1]` fallback (used for
|
|
1229
|
+
// the positional form) both keep working unchanged.
|
|
1230
|
+
// #3356 defect 1: `--quick-id` / `--slug` / `--directory` are
|
|
1231
|
+
// OPTIONAL widenings. A caller with no quick id or task directory
|
|
1232
|
+
// (fast.md, the original #2133 caller) omits them and keeps the
|
|
1233
|
+
// exact prior ordinal-`#`/`'—'`-Directory row. A caller that DOES
|
|
1234
|
+
// have a real quick id + task dir (i.e. can match `workflows/
|
|
1235
|
+
// quick.md`'s own Step 7c row for the same inputs) supplies them
|
|
1236
|
+
// and gets the byte-equivalent canonical row `quick.md:632`
|
|
1237
|
+
// documents — closing the false-equivalence gap `quick.md:627`
|
|
1238
|
+
// claims. `--directory` wins outright when given explicitly;
|
|
1239
|
+
// otherwise a supplied `--quick-id` + `--slug` pair derives the
|
|
1240
|
+
// canonical permalink the same way `workflows/quick.md` renders it.
|
|
1241
|
+
const qtaParsed = parseNamedArgsOrExit(
|
|
1242
|
+
qtaArgs,
|
|
1243
|
+
{ valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
|
|
1244
|
+
error,
|
|
1245
|
+
);
|
|
1246
|
+
const qtaTask = qtaParsed.task || args[1];
|
|
1144
1247
|
if (!qtaTask) {
|
|
1145
1248
|
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
|
1146
1249
|
}
|
|
1250
|
+
const qtaQuickId = qtaParsed['quick-id'] || undefined;
|
|
1251
|
+
const qtaSlug = qtaParsed['slug'] || undefined;
|
|
1252
|
+
const qtaDirectory = qtaParsed['directory']
|
|
1253
|
+
|| (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
|
|
1147
1254
|
|
|
1148
1255
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1149
1256
|
if (!fs.existsSync(statePath)) {
|
|
@@ -1169,8 +1276,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1169
1276
|
// still releases the lock before the throw propagates; the transform
|
|
1170
1277
|
// throws before returning new content, so nothing is ever written).
|
|
1171
1278
|
let mutation;
|
|
1279
|
+
// #3356 defect 2: this write touches only the Quick Tasks body
|
|
1280
|
+
// table — a single appended row — so it must not trigger the
|
|
1281
|
+
// default full re-derive of the disk-derived `progress.*`
|
|
1282
|
+
// frontmatter block. `{ resync: false }` mirrors every other
|
|
1283
|
+
// body-only STATE.md writer's convention (src/state.cts's own
|
|
1284
|
+
// docstring on `readModifyWriteStateMd` prescribes it); this route
|
|
1285
|
+
// was the lone outlier still passing no options at all.
|
|
1172
1286
|
state.readModifyWriteStateMd(statePath, (content) => {
|
|
1173
|
-
const result = appendQuickTaskRow(content, {
|
|
1287
|
+
const result = appendQuickTaskRow(content, {
|
|
1288
|
+
description: qtaTask,
|
|
1289
|
+
date,
|
|
1290
|
+
commit,
|
|
1291
|
+
quickId: qtaQuickId,
|
|
1292
|
+
directory: qtaDirectory,
|
|
1293
|
+
});
|
|
1174
1294
|
if (!result.ok) {
|
|
1175
1295
|
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
|
1176
1296
|
// is an expected, recoverable condition (no table / unrecognized
|
|
@@ -1181,7 +1301,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1181
1301
|
}
|
|
1182
1302
|
mutation = result.value;
|
|
1183
1303
|
return result.value.content;
|
|
1184
|
-
}, cwd);
|
|
1304
|
+
}, cwd, { resync: false });
|
|
1185
1305
|
|
|
1186
1306
|
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
|
1187
1307
|
}
|
|
@@ -1202,7 +1322,8 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1202
1322
|
const fsx = require('node:fs');
|
|
1203
1323
|
const os = require('node:os');
|
|
1204
1324
|
const { REVIEWER_LANES, mergeReviewerLanes } = require('./lib/review-lane-descriptor.cjs');
|
|
1205
|
-
const { resolveLanePlan } = require('./lib/review-lane-invocation.cjs');
|
|
1325
|
+
const { resolveLanePlan, resolveLaneEffort } = require('./lib/review-lane-invocation.cjs');
|
|
1326
|
+
const modelCatalog = require('./lib/model-catalog.cjs');
|
|
1206
1327
|
const runner = require('./lib/review-lane-runner.cjs');
|
|
1207
1328
|
const cfgLoader = require('./lib/config-loader.cjs');
|
|
1208
1329
|
const capabilityLoader = require('./lib/capability-loader.cjs');
|
|
@@ -1214,13 +1335,15 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1214
1335
|
const sub = args[1];
|
|
1215
1336
|
// Fail fast on an unrecognized subcommand. Without this check, `sub` fell through
|
|
1216
1337
|
// to the `sub !== 'invoke'` usage-error branch far below (after loading the
|
|
1217
|
-
// capability registry AND building a per-lane plan for every lane
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1338
|
+
// capability registry AND building a per-lane plan for every lane) before ever
|
|
1339
|
+
// reporting the error. That made an invalid subcommand slow instead of instant, and
|
|
1340
|
+
// under bench load `review-lane bogus` could exceed a caller's spawn timeout and be
|
|
1341
|
+
// killed before writing anything to stderr — the CI-observed failure was empty stdout
|
|
1342
|
+
// AND stderr, not the expected usage message (#3148). The plan path used to be far
|
|
1343
|
+
// heavier still: it spawned one child `query resolve-execution` process PER LANE to
|
|
1344
|
+
// fetch effort, up to 12 subprocess spawns for the default set. #4255 resolves effort
|
|
1345
|
+
// in-process from the lane's own declaration, so that cost is gone; the fail-fast
|
|
1346
|
+
// check stays because building 12 plans is still work an unrecognized sub should skip.
|
|
1224
1347
|
// `plan`/`invoke` are the only subs that need the expensive plan-building path
|
|
1225
1348
|
// below; `sections`/`flags` return earlier still. Anything else errors here, before
|
|
1226
1349
|
// any of that work starts.
|
|
@@ -1307,46 +1430,29 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1307
1430
|
return;
|
|
1308
1431
|
}
|
|
1309
1432
|
|
|
1310
|
-
// Effort argv is resolved
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
// syntax (ADR-1239 #2481, ADR-443's escalation ladder). `cmdResolveExecution` writes to
|
|
1314
|
-
// stdout and exits, so it cannot be called in-process for a value — this spawns the same
|
|
1315
|
-
// bounded query the legs did, once per selected lane. A lane whose slug is not a known host
|
|
1316
|
-
// resolves to no effort argument at all.
|
|
1433
|
+
// Effort argv is resolved from the LANE's own review configuration (#4255), then rendered
|
|
1434
|
+
// through the host's negotiated `effortSurface` so ADR-1239/#2481's trust-boundary invariant
|
|
1435
|
+
// still decides whether an argument is emitted at all and the catalog still owns the syntax.
|
|
1317
1436
|
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1328
|
-
|
|
1437
|
+
// What this replaced: a `query resolve-execution gsd-plan-checker --host <slug>` spawn per
|
|
1438
|
+
// lane. The agent id was a hardcoded literal, so the `--host` argument chose only the argv
|
|
1439
|
+
// RENDERING while the LEVEL always came from the installed plan-checker's frontmatter — `low`
|
|
1440
|
+
// under every shipped model profile. Every prompt-fed reviewer therefore ran at a fast
|
|
1441
|
+
// structural verifier's effort, and because the rendered argument is a CLI config override it
|
|
1442
|
+
// silently beat the effort the operator had configured for that CLI. At `low` a large
|
|
1443
|
+
// source-grounded prompt makes a model end its turn with no final message, so the lane came
|
|
1444
|
+
// back empty and the stub read as a crash.
|
|
1445
|
+
//
|
|
1446
|
+
// `resolveLaneEffort` is pure and lives beside the other lane resolution; this closure only
|
|
1447
|
+
// injects the rendering, which needs the registry and the catalog.
|
|
1448
|
+
const renderLaneEffort = (host, level) => {
|
|
1329
1449
|
try {
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
);
|
|
1335
|
-
if (r.status !== 0) return EMPTY_EFFORT;
|
|
1336
|
-
let parsed;
|
|
1337
|
-
try {
|
|
1338
|
-
parsed = JSON.parse(String(r.stdout || ''));
|
|
1339
|
-
} catch { return EMPTY_EFFORT; }
|
|
1340
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return EMPTY_EFFORT;
|
|
1341
|
-
const argv = Array.isArray(parsed.effort_argv)
|
|
1342
|
-
? parsed.effort_argv.filter((a) => typeof a === 'string' && a !== '')
|
|
1343
|
-
: [];
|
|
1344
|
-
const value = typeof parsed.effort_argv_value === 'string' && parsed.effort_argv_value
|
|
1345
|
-
? parsed.effort_argv_value
|
|
1346
|
-
: null;
|
|
1347
|
-
return { argv, value };
|
|
1348
|
-
} catch { return EMPTY_EFFORT; }
|
|
1450
|
+
const surface = commands.effortSurfaceForHost(cwd, host);
|
|
1451
|
+
const r = modelCatalog.renderEffortArgv(host, level, surface);
|
|
1452
|
+
return { argv: Array.isArray(r && r.argv) ? r.argv : [], value: (r && r.value) || null };
|
|
1453
|
+
} catch { return { argv: [], value: null }; }
|
|
1349
1454
|
};
|
|
1455
|
+
const effortFor = (lane) => resolveLaneEffort(lane, configGet, renderLaneEffort);
|
|
1350
1456
|
|
|
1351
1457
|
/**
|
|
1352
1458
|
* Per-lane prompt budget (#2797 semantics, preserved exactly).
|
|
@@ -1378,7 +1484,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1378
1484
|
// so losing all of them to one bad manifest is strictly worse. Belt and braces on purpose.
|
|
1379
1485
|
let r;
|
|
1380
1486
|
try {
|
|
1381
|
-
const effort = effortFor(
|
|
1487
|
+
const effort = effortFor(lane);
|
|
1382
1488
|
r = resolveLanePlan({ lane, configGet, runDir, repoRoot, effortArgs: effort.argv, effortValue: effort.value });
|
|
1383
1489
|
} catch (e) {
|
|
1384
1490
|
return { slug, ok: false, reason: 'malformed_lane', detail: `resolver threw: ${e && e.message ? e.message : String(e)}` };
|
|
@@ -1538,7 +1644,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1538
1644
|
`model override (it declares no modelConfigKey). The review will use the CLI's own default.\n`,
|
|
1539
1645
|
);
|
|
1540
1646
|
}
|
|
1541
|
-
const instanceEffort = effortFor(
|
|
1647
|
+
const instanceEffort = effortFor(lane);
|
|
1542
1648
|
const overridden = resolveLanePlan({
|
|
1543
1649
|
lane,
|
|
1544
1650
|
configGet: (k) => (key && k === key ? instanceModel : configGet(k)),
|
|
@@ -1669,6 +1775,31 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1669
1775
|
}
|
|
1670
1776
|
}
|
|
1671
1777
|
|
|
1778
|
+
/**
|
|
1779
|
+
* #3737 — strict read of the project-level worktree opt-out from
|
|
1780
|
+
* `.planning/config.json`. True ONLY when `workflow.use_worktrees` is the
|
|
1781
|
+
* boolean `false`; an absent key, unreadable/malformed config, or any
|
|
1782
|
+
* non-boolean value (including the string "false") degrades to false —
|
|
1783
|
+
* worktrees are ON by default, so the degraded answer is "not opted out".
|
|
1784
|
+
* Direct file read, deliberately NOT loadConfig: this resolver backs
|
|
1785
|
+
* sentinel writes and must never trigger config normalization/rewrites
|
|
1786
|
+
* (same discipline as resolveDispatchIsolationDecision's resolveRuntime
|
|
1787
|
+
* comment above). Never throws.
|
|
1788
|
+
*/
|
|
1789
|
+
function projectWorktreesOptedOut(cwd) {
|
|
1790
|
+
// #3972: single owner — planning-workspace's worktreesOptedOut ladder
|
|
1791
|
+
// (scoped own-key, root inheritance under the ws gate, strict === false).
|
|
1792
|
+
// Kept as a local name so routeDispatchIsolation's call sites read the
|
|
1793
|
+
// same as they did in #3938/#3963; the logic itself now lives beside
|
|
1794
|
+
// planningDir/planningRoot where every isolation surface can share it.
|
|
1795
|
+
try {
|
|
1796
|
+
const { worktreesOptedOut } = require('./lib/planning-workspace.cjs');
|
|
1797
|
+
return worktreesOptedOut(cwd);
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1672
1803
|
function routeDispatchIsolation({ args, cwd, raw, error }) {
|
|
1673
1804
|
// #2584 Phase 3 (#2627): typed query exposing the negotiated
|
|
1674
1805
|
// `dispatch.isolation` to the execute-phase wave scheduler, so the
|
|
@@ -1742,6 +1873,25 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1742
1873
|
? args[planIdx + 1]
|
|
1743
1874
|
: null;
|
|
1744
1875
|
|
|
1876
|
+
// #3737: the project-level opt-out (workflow.use_worktrees === false) is
|
|
1877
|
+
// decided HERE, before the sentinel write — not only in the workflow
|
|
1878
|
+
// shell blocks that run after this resolve. Pre-fix, any plain re-query
|
|
1879
|
+
// (config re-read, wave transition, second plan dispatch) re-persisted
|
|
1880
|
+
// the naturally-resolved host capability over the `--force-isolation
|
|
1881
|
+
// none` record the dispatch-isolation reference mandates, and the guard
|
|
1882
|
+
// then denied the sequential dispatch the config explicitly asked for.
|
|
1883
|
+
// Applied AFTER --force-isolation so the documented rule holds: the
|
|
1884
|
+
// opt-out wins on every host, over both the natural resolution and any
|
|
1885
|
+
// force. Strict `=== false`: the default is worktrees ON, so an absent
|
|
1886
|
+
// key, an unreadable/malformed config, or a non-boolean value degrades
|
|
1887
|
+
// to "not opted out" (mirrors readConfigJsonBoolean's no-coercion
|
|
1888
|
+
// discipline in lib/init.cjs).
|
|
1889
|
+
if (projectWorktreesOptedOut(cwd)) {
|
|
1890
|
+
isolation = 'none';
|
|
1891
|
+
harnessFlag = null;
|
|
1892
|
+
exec = null;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1745
1895
|
// Side-effect write (#3045 CORE REDESIGN) — see the doc comment above.
|
|
1746
1896
|
// Never allowed to affect this query's own stdout contract or throw.
|
|
1747
1897
|
try {
|
|
@@ -1758,6 +1908,261 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1758
1908
|
}
|
|
1759
1909
|
}
|
|
1760
1910
|
|
|
1911
|
+
/**
|
|
1912
|
+
* #3673 (ADR-1239 Phase 1) — typed, PURE-READ query exposing the negotiated
|
|
1913
|
+
* `dispatch.maxConcurrency` numeric sub-field. Sibling of `dispatch-isolation`
|
|
1914
|
+
* above, but — per the #3673 design doc's explicit rejection of a write path
|
|
1915
|
+
* for this query — writes NOTHING: no sentinel file, no side effect at all.
|
|
1916
|
+
* A read-only capacity lookup for a later (Phase 4) scheduler to consult.
|
|
1917
|
+
*
|
|
1918
|
+
* Precedence:
|
|
1919
|
+
* 1. GSD_DISPATCH_MAX_CONCURRENCY, if it parses as a positive safe
|
|
1920
|
+
* integer → source: "live". A malformed value (non-numeric, zero,
|
|
1921
|
+
* negative, fractional) is treated exactly as if the variable were
|
|
1922
|
+
* unset — it falls through to tier 2, never errors.
|
|
1923
|
+
* 2. registry.runtimes[id].runtime.hostIntegration.dispatch.maxConcurrency,
|
|
1924
|
+
* if it is a positive safe integer → source: "descriptor". The
|
|
1925
|
+
* "undocumented" sentinel and any other malformed shape fall through
|
|
1926
|
+
* to tier 3.
|
|
1927
|
+
* 3. 1 (the fail-closed floor) → source: "fallback".
|
|
1928
|
+
*
|
|
1929
|
+
* `isPositiveSafeInteger`, required below from `./lib/host-integration.cjs`,
|
|
1930
|
+
* is the SAME exported predicate src/host-integration.cts's
|
|
1931
|
+
* negotiateHostCapabilities applies to the descriptor value — applied
|
|
1932
|
+
* identically to both the env value and the descriptor value here so the
|
|
1933
|
+
* two tiers can never silently disagree on what counts as valid.
|
|
1934
|
+
*
|
|
1935
|
+
* `reason` names why the WINNING tier (or the fallback) was chosen: "ok" on
|
|
1936
|
+
* the happy path (live or descriptor honored), else "missing" (descriptor
|
|
1937
|
+
* omitted the field), "undocumented" (descriptor sentinel), "non_integer"
|
|
1938
|
+
* (fractional or NaN), "unsafe_integer" (integer but outside
|
|
1939
|
+
* Number.isSafeInteger's range), "non_positive" (zero or negative),
|
|
1940
|
+
* "non_numeric" (string/object/array/null/boolean), or "unknown_runtime"
|
|
1941
|
+
* (no registry entry for the resolved runtime id — resolution itself never
|
|
1942
|
+
* throws; failure fails closed to the same fallback tier).
|
|
1943
|
+
*
|
|
1944
|
+
* Output:
|
|
1945
|
+
* --raw → prints exactly one positive integer, nothing else
|
|
1946
|
+
* --json → prints { runtime, capacity, declared, source, reason }
|
|
1947
|
+
* default → same as --raw
|
|
1948
|
+
*/
|
|
1949
|
+
function routeDispatchCapacity({ args, cwd, raw }) {
|
|
1950
|
+
const { UNDOCUMENTED, isPositiveSafeInteger } = require('./lib/host-integration.cjs');
|
|
1951
|
+
|
|
1952
|
+
let runtimeId = null;
|
|
1953
|
+
let runtimeEntry = null;
|
|
1954
|
+
try {
|
|
1955
|
+
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
|
|
1956
|
+
runtimeId = resolveRuntime(cwd);
|
|
1957
|
+
const registry = require('./lib/capability-registry.cjs');
|
|
1958
|
+
runtimeEntry = registry.runtimes != null ? registry.runtimes[runtimeId] : null;
|
|
1959
|
+
} catch {
|
|
1960
|
+
runtimeEntry = null;
|
|
1961
|
+
}
|
|
1962
|
+
const declared = runtimeEntry?.runtime?.hostIntegration?.dispatch?.maxConcurrency ?? null;
|
|
1963
|
+
|
|
1964
|
+
let liveValue = null;
|
|
1965
|
+
const rawEnv = process.env.GSD_DISPATCH_MAX_CONCURRENCY;
|
|
1966
|
+
if (typeof rawEnv === 'string' && rawEnv.trim().length > 0) {
|
|
1967
|
+
const parsed = Number(rawEnv.trim());
|
|
1968
|
+
if (isPositiveSafeInteger(parsed)) {
|
|
1969
|
+
liveValue = parsed;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
let capacity;
|
|
1974
|
+
let source;
|
|
1975
|
+
let reason;
|
|
1976
|
+
if (liveValue !== null) {
|
|
1977
|
+
capacity = liveValue;
|
|
1978
|
+
source = 'live';
|
|
1979
|
+
reason = 'ok';
|
|
1980
|
+
} else if (runtimeEntry == null) {
|
|
1981
|
+
capacity = 1;
|
|
1982
|
+
source = 'fallback';
|
|
1983
|
+
reason = 'unknown_runtime';
|
|
1984
|
+
} else if (declared === UNDOCUMENTED) {
|
|
1985
|
+
capacity = 1;
|
|
1986
|
+
source = 'fallback';
|
|
1987
|
+
reason = 'undocumented';
|
|
1988
|
+
} else if (isPositiveSafeInteger(declared)) {
|
|
1989
|
+
capacity = declared;
|
|
1990
|
+
source = 'descriptor';
|
|
1991
|
+
reason = 'ok';
|
|
1992
|
+
} else if (declared === null || declared === undefined) {
|
|
1993
|
+
capacity = 1;
|
|
1994
|
+
source = 'fallback';
|
|
1995
|
+
reason = 'missing';
|
|
1996
|
+
} else if (typeof declared !== 'number') {
|
|
1997
|
+
capacity = 1;
|
|
1998
|
+
source = 'fallback';
|
|
1999
|
+
reason = 'non_numeric';
|
|
2000
|
+
} else if (!Number.isSafeInteger(declared)) {
|
|
2001
|
+
capacity = 1;
|
|
2002
|
+
source = 'fallback';
|
|
2003
|
+
reason = Number.isInteger(declared) ? 'unsafe_integer' : 'non_integer';
|
|
2004
|
+
} else {
|
|
2005
|
+
capacity = 1;
|
|
2006
|
+
source = 'fallback';
|
|
2007
|
+
reason = 'non_positive';
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
if (args.indexOf('--json') !== -1) {
|
|
2011
|
+
output({ runtime: runtimeId, capacity, declared, source, reason }, raw);
|
|
2012
|
+
} else {
|
|
2013
|
+
process.stdout.write(String(capacity));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// #3714 follow-up — the dispatch seam gated only on PRESENCE of an explicit
|
|
2018
|
+
// pin, never on its VALUE, so an Anthropic-flavored global default
|
|
2019
|
+
// (~/.gsd/defaults.json model_overrides["gsd-executor"] = "sonnet"/"opus"/
|
|
2020
|
+
// "claude-*") reached `codex exec --model sonnet`: the documented #2310/
|
|
2021
|
+
// #2311 400 on a passive-posture host (ADR-1239/ADR-2313). It also let a
|
|
2022
|
+
// repo-committed .planning/config.json inject shell-hostile argv (a
|
|
2023
|
+
// `-c approval_policy=never` suffix, `$(...)`/`;` command injection,
|
|
2024
|
+
// embedded control characters) straight onto exec's argv.
|
|
2025
|
+
//
|
|
2026
|
+
// This mirrors — deliberately, not by re-derivation — the same VALUE
|
|
2027
|
+
// policy bin/install.js's generateCodexAgentToml() already applies to the
|
|
2028
|
+
// identical model_overrides["gsd-executor"] config key for the .toml
|
|
2029
|
+
// surface (bin/install.js ~3983-4046): trim; a whitespace-only value drops
|
|
2030
|
+
// silently (#3241, no warning); an Anthropic-flavored value
|
|
2031
|
+
// (isAnthropicFlavoredModel, single-sourced on bin/lib/model-catalog.cjs
|
|
2032
|
+
// per #3241 specifically so it cannot diverge across Codex-posture
|
|
2033
|
+
// surfaces) drops WITH a warning; a real pin survives verbatim. Two
|
|
2034
|
+
// additions beyond the .toml surface, both specific to this seam: the
|
|
2035
|
+
// 'inherit' sentinel (case/whitespace-insensitive) is a no-op here already
|
|
2036
|
+
// and must stay one, and a value that doesn't look like a model id at all
|
|
2037
|
+
// (the injection case above — the .toml surface never had to consider this
|
|
2038
|
+
// because TOML string-quoting isn't a shell argv boundary) is dropped with
|
|
2039
|
+
// a warning rather than ever reaching child_process argv.
|
|
2040
|
+
// Single source of truth for the model-id "allowed characters" notion
|
|
2041
|
+
// (#3714 follow-up — "Generative Fix Divergence"): the accept regex
|
|
2042
|
+
// (MODEL_ID_CHARSET_RE, used to ADMIT a pin) and the sanitizer keep-class
|
|
2043
|
+
// (MODEL_ID_SANITIZE_STRIP_RE, used to RENDER a rejected pin into a
|
|
2044
|
+
// warning) are both derived from this one character-class body so they
|
|
2045
|
+
// cannot drift apart again the way they already did once (the '@' added
|
|
2046
|
+
// for Vertex pins landed in the accept regex but not the sanitizer,
|
|
2047
|
+
// rendering "text-bison@002" as "text-bison?002" in the warning). '@' is
|
|
2048
|
+
// included for Vertex model-version pins ("text-bison@002",
|
|
2049
|
+
// "chat-bison@001"), which are legitimate model ids reachable through a
|
|
2050
|
+
// custom model_provider.
|
|
2051
|
+
// This body is interpolated raw into BOTH a positive character class
|
|
2052
|
+
// (MODEL_ID_CHARSET_RE, `[BODY]`) and a negated one
|
|
2053
|
+
// (MODEL_ID_SANITIZE_STRIP_RE, `[^BODY]`) below — only plain characters
|
|
2054
|
+
// and `x-y` ranges are safe here. A class metacharacter (`^`, `]`, `\`)
|
|
2055
|
+
// would mean different things in the two derived regexes if ever added.
|
|
2056
|
+
const MODEL_ID_CHARSET_BODY = 'A-Za-z0-9._:/@-';
|
|
2057
|
+
// The first character must be alphanumeric (#3714 hardening): a leading
|
|
2058
|
+
// '.', '_', ':', '/', or '@' has no legitimate model-id use case and, for
|
|
2059
|
+
// resolveOrchestratorExec's documented role as a GENERAL descriptor→argv
|
|
2060
|
+
// seam other hosts may adopt, a leading '@' or '/' is exactly the shape an
|
|
2061
|
+
// @-response-file or /-switch parser would key on. The leading-dash shape
|
|
2062
|
+
// is enforced separately by LEADING_DASH_RE below — it is NOT relaxed
|
|
2063
|
+
// here, since a flag-shaped value ("-c", "--config") must still fail the
|
|
2064
|
+
// resolver's own unsafe_leading_dash_model guard path via that dedicated
|
|
2065
|
+
// check, not this charset.
|
|
2066
|
+
const MODEL_ID_CHARSET_RE = new RegExp(`^[A-Za-z0-9][${MODEL_ID_CHARSET_BODY}]*$`);
|
|
2067
|
+
// Keep-class for sanitizing a REJECTED pin before it reaches the warning
|
|
2068
|
+
// (a guaranteed-reachable raw-to-TTY sink — the dispatch step runs with no
|
|
2069
|
+
// `2>` redirect). Built from the same MODEL_ID_CHARSET_BODY as the accept
|
|
2070
|
+
// regex above, so every character the matcher accepts also survives the
|
|
2071
|
+
// sanitizer unchanged, and a widened charset can never diverge from its
|
|
2072
|
+
// rendering again.
|
|
2073
|
+
//
|
|
2074
|
+
// This `g`-flagged instance is for internal `.replace()` use ONLY — a
|
|
2075
|
+
// `/g` regex is stateful (`.lastIndex` persists across calls) and
|
|
2076
|
+
// `.test()` on it alternates true/false/true across repeated calls on the
|
|
2077
|
+
// same string, a false-green trap for any test that reaches for `.test()`
|
|
2078
|
+
// instead of `.replace()`. To make that trap impossible rather than just
|
|
2079
|
+
// documenting it, this `g`-flagged object is never exported; the exported
|
|
2080
|
+
// `MODEL_ID_SANITIZE_STRIP_RE` below is a separate, non-global instance
|
|
2081
|
+
// built from the same body, safe for `.test()`/`.match()` in tests.
|
|
2082
|
+
const MODEL_ID_SANITIZE_STRIP_RE_G = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`, 'g');
|
|
2083
|
+
// Non-global companion of MODEL_ID_SANITIZE_STRIP_RE_G, exported for
|
|
2084
|
+
// tests. Do not use with `.replace()` on a value containing more than one
|
|
2085
|
+
// disallowed character — it only replaces the first match. Production
|
|
2086
|
+
// code must use the `g`-flagged instance above instead.
|
|
2087
|
+
const MODEL_ID_SANITIZE_STRIP_RE = new RegExp(`[^${MODEL_ID_CHARSET_BODY}]`);
|
|
2088
|
+
// A model id has no legitimate reason to be long; this also keeps a
|
|
2089
|
+
// pathological pin away from the Windows argv ceiling (execFileSync aborts
|
|
2090
|
+
// if argv > 32,767 chars — CLAUDE.md "Windows ARGV Overflow"). A pin over
|
|
2091
|
+
// this length is DROPPED WITH A WARNING like every other rejection, never
|
|
2092
|
+
// truncated into argv — a truncated model id is a different model id.
|
|
2093
|
+
const MODEL_ID_MAX_LENGTH = 200;
|
|
2094
|
+
const _dispatchModelPinDropWarned = new Set();
|
|
2095
|
+
function _warnDispatchModelPinDropped(agentName, rawValue, reason) {
|
|
2096
|
+
const key = `${agentName}::${rawValue}::${reason}`;
|
|
2097
|
+
if (_dispatchModelPinDropWarned.has(key)) return;
|
|
2098
|
+
_dispatchModelPinDropWarned.add(key);
|
|
2099
|
+
// Sanitize BEFORE truncating: every value that reaches this warning
|
|
2100
|
+
// failed the model-id charset test by definition (or, for the
|
|
2101
|
+
// over-length case, still only ever contains charset-legal bytes) —
|
|
2102
|
+
// sanitizing first catches raw control/escape bytes (ESC, BEL, CSI
|
|
2103
|
+
// sequences) using the identity-sanitizing pattern already used for
|
|
2104
|
+
// --as at gsd-tools.cjs:1526. Sanitizing before truncating also ensures
|
|
2105
|
+
// a truncated escape sequence can never survive (e.g. an SGR sequence
|
|
2106
|
+
// cut before its reset, leaving sticky terminal state) — truncation
|
|
2107
|
+
// only ever cuts already-safe characters.
|
|
2108
|
+
const sanitized = String(rawValue).replace(MODEL_ID_SANITIZE_STRIP_RE_G, '?');
|
|
2109
|
+
const safe = sanitized.length > 64 ? `${sanitized.slice(0, 64)}…` : sanitized;
|
|
2110
|
+
process.stderr.write(
|
|
2111
|
+
`gsd: warning — dispatch model pin for agent "${agentName}" (value "${safe}") ${reason}; ` +
|
|
2112
|
+
`dropping it so the spawned executor falls back to the session model.\n`,
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
// A value starting with '-' (or '--') is a flag/option shape, not a model
|
|
2116
|
+
// id — `-c`, `--config`, `-`, `--`, `-p` are unsafe to hand to
|
|
2117
|
+
// resolveOrchestratorExec, whose own `unsafe_leading_dash_model` guard
|
|
2118
|
+
// rejects them and fails the WHOLE resolution to `{ ok: false }` ->
|
|
2119
|
+
// exec:null -> a FATAL wave abort (executor-isolation-dispatch.md:299-303),
|
|
2120
|
+
// even on hosts (e.g. kimi-code) that declare no modelFlag at all and
|
|
2121
|
+
// previously ignored the pin entirely. This check MUST run BEFORE
|
|
2122
|
+
// MODEL_ID_CHARSET_RE below: the charset is anchored to `[A-Za-z0-9]` at
|
|
2123
|
+
// the first character, so every dash-leading value already fails the
|
|
2124
|
+
// charset test and would otherwise be swallowed by the generic "unsafe
|
|
2125
|
+
// characters" message, losing the more specific and more actionable
|
|
2126
|
+
// flag/option diagnosis. Reject here, at the VALUE-policy layer, so a
|
|
2127
|
+
// leading-dash value degrades to "no model" like every other rejected
|
|
2128
|
+
// shape, instead of reaching a resolver whose failure mode is fatal
|
|
2129
|
+
// rather than a graceful drop.
|
|
2130
|
+
const LEADING_DASH_RE = /^-/;
|
|
2131
|
+
/**
|
|
2132
|
+
* Resolve the VALUE policy for an explicit dispatch model pin. `rawValue`
|
|
2133
|
+
* is whatever resolveAgentModelOverride(..., null) returned — a string
|
|
2134
|
+
* pin, the 'inherit' sentinel, or null/''/undefined for "no explicit pin".
|
|
2135
|
+
* Returns the trimmed model string to embed, or `undefined` to emit no
|
|
2136
|
+
* --model flag at all. Never throws; never fails closed to an error —
|
|
2137
|
+
* every rejection degrades to "no model" (drop-and-warn), matching the
|
|
2138
|
+
* documented desired behavior of falling back to the session model rather
|
|
2139
|
+
* than aborting the wave.
|
|
2140
|
+
*/
|
|
2141
|
+
function resolveDispatchModelPin(agentName, rawValue) {
|
|
2142
|
+
if (typeof rawValue !== 'string') return undefined; // not a string -> no model
|
|
2143
|
+
const trimmed = rawValue.trim();
|
|
2144
|
+
if (trimmed === '') return undefined; // whitespace-only -> no model, no warning (#3241)
|
|
2145
|
+
if (trimmed.toLowerCase() === 'inherit') return undefined; // sentinel -> no model, no warning
|
|
2146
|
+
const { isAnthropicFlavoredModel } = require('./lib/model-catalog.cjs');
|
|
2147
|
+
if (isAnthropicFlavoredModel(trimmed)) {
|
|
2148
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'is an Anthropic-flavored model/alias, not a valid Codex model');
|
|
2149
|
+
return undefined;
|
|
2150
|
+
}
|
|
2151
|
+
if (LEADING_DASH_RE.test(trimmed)) {
|
|
2152
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'looks like a flag/option, not a model id (leading "-")');
|
|
2153
|
+
return undefined;
|
|
2154
|
+
}
|
|
2155
|
+
if (!MODEL_ID_CHARSET_RE.test(trimmed)) {
|
|
2156
|
+
_warnDispatchModelPinDropped(agentName, rawValue, 'does not look like a model id (unsafe characters)');
|
|
2157
|
+
return undefined;
|
|
2158
|
+
}
|
|
2159
|
+
if (trimmed.length > MODEL_ID_MAX_LENGTH) {
|
|
2160
|
+
_warnDispatchModelPinDropped(agentName, rawValue, `exceeds the maximum model id length (${MODEL_ID_MAX_LENGTH} characters)`);
|
|
2161
|
+
return undefined;
|
|
2162
|
+
}
|
|
2163
|
+
return trimmed;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1761
2166
|
const DISPATCH_ISOLATION_VOCABULARY = new Set(['harness-worktree', 'orchestrator-worktree', 'none']);
|
|
1762
2167
|
|
|
1763
2168
|
/**
|
|
@@ -1821,14 +2226,67 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1821
2226
|
const promptIdx = args.indexOf('--prompt');
|
|
1822
2227
|
const promptArg = promptIdx !== -1 ? args[promptIdx + 1] : undefined;
|
|
1823
2228
|
const hostIntegration = require('./lib/host-integration.cjs');
|
|
2229
|
+
// #3714: resolve an EXPLICIT, non-sentinel per-agent model pin for the
|
|
2230
|
+
// spawned worktree executor. Passing `null` as the runtime resolver
|
|
2231
|
+
// (3rd arg) is LOAD-BEARING, not an oversight — it is what keeps
|
|
2232
|
+
// profile/tier-derived models out of argv. Codex's `modelMode: passive`
|
|
2233
|
+
// posture (ADR-1239) and ADR-2313 forbid GSD driving model selection on
|
|
2234
|
+
// this host; only an operator's EXPLICIT override may cross this seam.
|
|
2235
|
+
// resolveAgentModelOverride(..., null) returns a value ONLY when the
|
|
2236
|
+
// operator pinned one explicitly (measured: unpinned -> null,
|
|
2237
|
+
// "inherit" -> "inherit" meaning "use the ambient session model, don't
|
|
2238
|
+
// pass a flag", "" -> null, profile-only -> null). Do NOT swap in
|
|
2239
|
+
// resolve-model / a full model-resolver here: that resolver falls back
|
|
2240
|
+
// to a default (e.g. "sonnet") for the unpinned/profile-only cases,
|
|
2241
|
+
// and emitting that on Codex's exec argv is exactly the documented
|
|
2242
|
+
// #2310/#2311 regression (a model unknown to Codex forced into a
|
|
2243
|
+
// passive-posture host).
|
|
2244
|
+
//
|
|
2245
|
+
// Presence of a pin is necessary but not sufficient: resolveDispatchModelPin
|
|
2246
|
+
// applies the same VALUE policy the install-side .toml surface already
|
|
2247
|
+
// applies to this config key (trim / drop-inherit / drop-Anthropic-flavored
|
|
2248
|
+
// with a warning / drop-non-model-id-charset with a warning) so a global
|
|
2249
|
+
// Anthropic-flavored default or an injected config value never reaches argv.
|
|
2250
|
+
//
|
|
2251
|
+
// This whole VALUE policy — including its warning — is gated on the
|
|
2252
|
+
// resolved runtime's descriptor actually declaring a non-empty
|
|
2253
|
+
// `modelFlag`. The policy runs at this host-NEUTRAL site, so a host
|
|
2254
|
+
// with no modelFlag at all (kimi, kimi-code, opencode) was never
|
|
2255
|
+
// going to emit a --model regardless of the pin's value; running the
|
|
2256
|
+
// policy anyway produced a stderr warning claiming "dropping it so
|
|
2257
|
+
// the spawned executor falls back to the session model" on every
|
|
2258
|
+
// dispatch for such a host — misleading today, and actively wrong if
|
|
2259
|
+
// a Claude-capable host ever declares a modelFlag. When the
|
|
2260
|
+
// descriptor declares no modelFlag, skip the policy entirely: no
|
|
2261
|
+
// model, no warning, argv byte-identical to before this pin policy
|
|
2262
|
+
// existed.
|
|
2263
|
+
const declaresModelFlag = typeof runtimeEntry?.runtime?.orchestratorExec?.modelFlag === 'string' &&
|
|
2264
|
+
runtimeEntry.runtime.orchestratorExec.modelFlag.length > 0;
|
|
2265
|
+
let model;
|
|
2266
|
+
if (declaresModelFlag) {
|
|
2267
|
+
const { readGsdEffectiveModelOverrides, resolveAgentModelOverride } =
|
|
2268
|
+
require('./lib/install-model-override-resolver.cjs');
|
|
2269
|
+
const pinned = resolveAgentModelOverride(
|
|
2270
|
+
'gsd-executor', readGsdEffectiveModelOverrides(cwd), null);
|
|
2271
|
+
model = resolveDispatchModelPin('gsd-executor', pinned);
|
|
2272
|
+
}
|
|
1824
2273
|
const resolution = hostIntegration.resolveOrchestratorExec(
|
|
1825
2274
|
runtimeEntry?.runtime?.orchestratorExec,
|
|
1826
2275
|
cwdTarget,
|
|
1827
2276
|
promptArg,
|
|
2277
|
+
model,
|
|
1828
2278
|
);
|
|
1829
|
-
// A host declaring orchestrator-worktree whose exec descriptor does
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
2279
|
+
// A host declaring orchestrator-worktree whose exec descriptor does not
|
|
2280
|
+
// resolve halts THIS wave's dispatch: isolation is forced to 'none' here,
|
|
2281
|
+
// and executor-isolation-dispatch.md:299-303 treats a null exec as FATAL
|
|
2282
|
+
// (exit 1) after the worktree has already been created — it does not
|
|
2283
|
+
// degrade to sequential execution. resolveDispatchModelPin rejects any
|
|
2284
|
+
// leading-dash value (flag/option shape) before it ever reaches this
|
|
2285
|
+
// resolver specifically so it cannot trip the resolver's own
|
|
2286
|
+
// `unsafe_leading_dash_model` guard and turn a bad config value into
|
|
2287
|
+
// this fatal path; every other unresolvable model value likewise
|
|
2288
|
+
// degrades to "no --model" (session model fallback) rather than to
|
|
2289
|
+
// resolution.ok === false.
|
|
1832
2290
|
if (resolution.ok) {
|
|
1833
2291
|
exec = { command: resolution.command, args: resolution.args, cwd: resolution.cwd };
|
|
1834
2292
|
} else {
|
|
@@ -2137,8 +2595,19 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2137
2595
|
.filter((t) => t.length > 0);
|
|
2138
2596
|
termsOverride = list.length > 0 ? { pluralization: list } : undefined;
|
|
2139
2597
|
}
|
|
2598
|
+
// An unresolvable phase section is not a negative verdict. Feeding
|
|
2599
|
+
// `''` to the detector reported "examined, found nothing" for a
|
|
2600
|
+
// probe that never had input — no ROADMAP.md, or a phase number
|
|
2601
|
+
// absent from it, both read as a confident `detected:false`
|
|
2602
|
+
// (ADR-3889 failure class (c), #3909). Exit stays 0: this is an
|
|
2603
|
+
// ADR-2980 degraded result carried in the payload, and ADR-3889 P8
|
|
2604
|
+
// pins the gsd-tools exit projection at v1.
|
|
2140
2605
|
const section = roadmap.getRoadmapPhaseWithFallback(cwd, phaseNum);
|
|
2141
|
-
|
|
2606
|
+
if (typeof section !== 'string' || section.trim() === '') {
|
|
2607
|
+
output({ skipped: true, reason: 'phase_unresolved' }, raw);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const result = detectAssumptionDelta(section, termsOverride);
|
|
2142
2611
|
output(result, raw);
|
|
2143
2612
|
return;
|
|
2144
2613
|
}
|
|
@@ -2185,7 +2654,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2185
2654
|
// --no-archive-phases' inverted shape, absence of this flag means
|
|
2186
2655
|
// "do nothing" rather than "skip a default-on behavior".
|
|
2187
2656
|
const archiveQuick = args.includes('--archive-quick');
|
|
2188
|
-
|
|
2657
|
+
// #3726: explicit mutation opt-in — without --confirm (and without
|
|
2658
|
+
// --dry-run) the command refuses before touching anything. Distinct
|
|
2659
|
+
// from --force, which bypasses the narrow scope guards only.
|
|
2660
|
+
const confirm = args.includes('--confirm');
|
|
2661
|
+
milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun, archiveQuick, confirm }, raw);
|
|
2189
2662
|
} else if (subcommand === 'archive-quick') {
|
|
2190
2663
|
// #2142 escalation: narrow archival-only entry point (does NOT
|
|
2191
2664
|
// touch ROADMAP/REQUIREMENTS/MILESTONES.md, runs no completion
|
|
@@ -2207,11 +2680,11 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2207
2680
|
const subcommand = args[1];
|
|
2208
2681
|
if (subcommand === 'render-checkpoint') {
|
|
2209
2682
|
const uat = require('./lib/uat.cjs');
|
|
2210
|
-
const options =
|
|
2683
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['file'], positionals: 2 }, error);
|
|
2211
2684
|
uat.cmdRenderCheckpoint(cwd, options, raw);
|
|
2212
2685
|
} else if (subcommand === 'classify-coverage') {
|
|
2213
2686
|
const coverage = require('./lib/coverage.cjs');
|
|
2214
|
-
const options =
|
|
2687
|
+
const options = parseNamedArgsOrExit(args, { valueFlags: ['summary', 'file'], positionals: 2 }, error);
|
|
2215
2688
|
coverage.cmdClassify(cwd, options, raw);
|
|
2216
2689
|
} else {
|
|
2217
2690
|
error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
@@ -2226,7 +2699,20 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2226
2699
|
function routeTodo({ args, cwd, raw, error }) {
|
|
2227
2700
|
const subcommand = args[1];
|
|
2228
2701
|
if (subcommand === 'complete') {
|
|
2229
|
-
|
|
2702
|
+
// #4096: this verb's flag surface is closed. `--dry-run` is parsed
|
|
2703
|
+
// and plumbed (mirroring routeMilestone / #2118), and any OTHER
|
|
2704
|
+
// flag fails loudly instead of being silently ignored — an
|
|
2705
|
+
// accepted-but-ignored safety flag converts a deliberate preview
|
|
2706
|
+
// into the mutation it was meant to avoid. (`--raw` is spliced by
|
|
2707
|
+
// the dispatcher before routing; it stays in the set as a guard
|
|
2708
|
+
// against that splice ever moving.)
|
|
2709
|
+
const TODO_COMPLETE_KNOWN_FLAGS = new Set(['--dry-run', '--raw']);
|
|
2710
|
+
for (const a of args.slice(3)) {
|
|
2711
|
+
if (typeof a === 'string' && a.startsWith('-') && !TODO_COMPLETE_KNOWN_FLAGS.has(a)) {
|
|
2712
|
+
error(`Unknown flag for todo complete: ${a}`, ERROR_REASON.USAGE);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
commands.cmdTodoComplete(cwd, args[2], { dryRun: args.includes('--dry-run') }, raw);
|
|
2230
2716
|
} else if (subcommand === 'match-phase') {
|
|
2231
2717
|
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
|
2232
2718
|
} else {
|
|
@@ -2236,8 +2722,13 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2236
2722
|
|
|
2237
2723
|
function routeScaffold({ args, cwd, raw, error }) {
|
|
2238
2724
|
const scaffoldType = args[1];
|
|
2725
|
+
// `--name` is multi-word (consumed separately by parseMultiwordArg,
|
|
2726
|
+
// below) — a token count the single-token-per-flag boundary walk
|
|
2727
|
+
// cannot represent. `positionals: 'rest'` disables that walk for
|
|
2728
|
+
// this call, matching the existing (unchanged) permissive behavior
|
|
2729
|
+
// for --name; --phase extraction is unaffected either way.
|
|
2239
2730
|
const scaffoldOptions = {
|
|
2240
|
-
phase:
|
|
2731
|
+
phase: parseNamedArgsOrExit(args, { valueFlags: ['phase'], positionals: 'rest' }, error).phase,
|
|
2241
2732
|
name: parseMultiwordArg(args, 'name'),
|
|
2242
2733
|
};
|
|
2243
2734
|
commands.cmdScaffold(cwd, scaffoldType, scaffoldOptions, raw);
|
|
@@ -2441,9 +2932,17 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
2441
2932
|
);
|
|
2442
2933
|
}
|
|
2443
2934
|
} catch (e) {
|
|
2935
|
+
// ADR-3889: error() now throws ExitError instead of calling
|
|
2936
|
+
// process.exit(1) directly, so an ExitError raised by error() INSIDE
|
|
2937
|
+
// this try (e.g. the "Unknown windows subcommand" call above, or one
|
|
2938
|
+
// inside cmdWindowsStatus/Append/Waive/MarkFixed) lands HERE instead of
|
|
2939
|
+
// terminating uncatchably. It must be re-thrown unconditionally, before
|
|
2940
|
+
// the WindowsError name check below, or it falls through to the
|
|
2941
|
+
// generic branch and gets re-wrapped with a wrong message/reason,
|
|
2942
|
+
// discarding the original exit code.
|
|
2943
|
+
if (e instanceof ExitError) throw e;
|
|
2444
2944
|
// WindowsError carries a REASON code; surface it through the structured
|
|
2445
|
-
// error path so tests can assert on the typed reason.
|
|
2446
|
-
// process.exit(1) internally so we never reach the fall-through.
|
|
2945
|
+
// error path so tests can assert on the typed reason.
|
|
2447
2946
|
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
|
|
2448
2947
|
error(e.message || 'broken-windows error', e.reason);
|
|
2449
2948
|
}
|
|
@@ -3712,14 +4211,19 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3712
4211
|
'verification': routeVerification,
|
|
3713
4212
|
'generate-slug': routeGenerateSlug,
|
|
3714
4213
|
'current-timestamp': routeCurrentTimestamp,
|
|
4214
|
+
'runtime-identity': routeRuntimeIdentity,
|
|
3715
4215
|
'project-instruction-file': routeProjectInstructionFile,
|
|
3716
4216
|
'list-todos': routeListTodos,
|
|
3717
4217
|
'list-seeds': routeListSeeds,
|
|
3718
4218
|
'verify-path-exists': routeVerifyPathExists,
|
|
3719
4219
|
'quick-tasks-append': routeQuickTasksAppend,
|
|
4220
|
+
'quick-tasks-migrate': routeQuickTasksMigrate,
|
|
4221
|
+
// #3676 (Phase 4, epic #3344): quick-batch coordination verbs.
|
|
4222
|
+
'quick-batch': routeQuickBatchCommand,
|
|
3720
4223
|
'normalize-test-command': routeNormalizeTestCommand,
|
|
3721
4224
|
'dispatch-should-flatten': routeDispatchShouldFlatten,
|
|
3722
4225
|
'dispatch-isolation': routeDispatchIsolation,
|
|
4226
|
+
'dispatch-capacity': routeDispatchCapacity,
|
|
3723
4227
|
'inspect-dispatch-isolation': routeInspectDispatchIsolation,
|
|
3724
4228
|
'record-dispatch-isolation': routeRecordDispatchIsolation,
|
|
3725
4229
|
'resolve-dispatch-type': routeResolveDispatchType,
|
|
@@ -3728,6 +4232,10 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
3728
4232
|
'skill-manifest': routeSkillManifest,
|
|
3729
4233
|
'history-digest': routeHistoryDigest,
|
|
3730
4234
|
'phases': routePhases,
|
|
4235
|
+
// #2790: read-only schema-v1 planning snapshot. The router imports its own
|
|
4236
|
+
// io/planning-inspect deps, so it needs no module injection — it receives
|
|
4237
|
+
// { args, cwd, raw, error } and ignores the rest of the dispatch context.
|
|
4238
|
+
'planning': routePlanningCommand,
|
|
3731
4239
|
'assumption-delta': routeAssumptionDelta,
|
|
3732
4240
|
'requirements': routeRequirements,
|
|
3733
4241
|
'gap-analysis': routeGapAnalysis,
|
|
@@ -3968,23 +4476,25 @@ function runWithTimeout(argv) {
|
|
|
3968
4476
|
// this string and HOST_COMMAND_ROUTERS/SKIP_ROOT_RESOLUTION are three
|
|
3969
4477
|
// independently hand-maintained sites and nothing previously caught them
|
|
3970
4478
|
// drifting apart when a query command was added to only one or two.
|
|
3971
|
-
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
|
|
4479
|
+
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--project-dir <path>] [--ws <name>] [--json-errors] [--exit-contract=<v>]\n' +
|
|
3972
4480
|
'Commands: agent, agent-skills, assumption-delta, audit-open, audit-uat, check, check-commit, commit, commit-docs-guard, commit-to-subrepo, pr-subrepo, ' +
|
|
3973
4481
|
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, normalize-test-command, ' +
|
|
3974
4482
|
'context-predicates, current-timestamp, detect-custom-files, docs-init, drift-guard, effort, extract-messages, find-phase, ' +
|
|
3975
4483
|
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
|
3976
4484
|
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
|
3977
|
-
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
|
3978
|
-
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
|
|
3979
|
-
'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, ' +
|
|
4485
|
+
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
|
4486
|
+
'profile-sample, progress, project-instruction-file, prompt-budget, quick-batch, quick-tasks-append, quick-tasks-migrate, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
|
|
4487
|
+
'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
|
3980
4488
|
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
3981
4489
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
3982
4490
|
'Global flags:\n' +
|
|
3983
4491
|
' --raw Emit raw output without post-processing\n' +
|
|
3984
4492
|
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
|
|
3985
4493
|
' --cwd <path> Override working directory for project-root resolution\n' +
|
|
4494
|
+
' --project-dir <path> Explicit project root; skips the ancestor walk-up entirely (must already contain .planning/)\n' +
|
|
3986
4495
|
' --ws <name> Override active workstream (or set GSD_WORKSTREAM)\n' +
|
|
3987
|
-
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n
|
|
4496
|
+
' --json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)\n' +
|
|
4497
|
+
' --exit-contract=<v> Exit-code contract version: v1 (default) or v2 (or set GSD_EXIT_CONTRACT)\n\n' +
|
|
3988
4498
|
'For command-specific argument requirements, invoke the command without args ' +
|
|
3989
4499
|
'(e.g. `gsd-tools phase add`) — the resulting error lists what is required.';
|
|
3990
4500
|
|
|
@@ -4003,6 +4513,10 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
4003
4513
|
// below (never as the live Set itself; see that function's doc comment).
|
|
4004
4514
|
const SKIP_ROOT_RESOLUTION = new Set([
|
|
4005
4515
|
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
|
4516
|
+
// #3146: runtime-identity is a pure local read of baked package coordinates.
|
|
4517
|
+
// It is probed from whatever cwd a workflow happens to be in — including
|
|
4518
|
+
// outside any project — so it must never require a resolvable project root.
|
|
4519
|
+
'runtime-identity',
|
|
4006
4520
|
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
|
4007
4521
|
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
|
4008
4522
|
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
|
@@ -4074,18 +4588,19 @@ function resolveMainWorktreeCwd(cwd, deps = {}) {
|
|
|
4074
4588
|
async function main() {
|
|
4075
4589
|
let args = process.argv.slice(2);
|
|
4076
4590
|
|
|
4077
|
-
//
|
|
4078
|
-
//
|
|
4079
|
-
//
|
|
4080
|
-
//
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4591
|
+
// These two global-flag blocks (--json-errors, --exit-contract) MUST run
|
|
4592
|
+
// BEFORE the run-with-timeout interception below. run-with-timeout treats
|
|
4593
|
+
// args[0] (post `query` stripping) as the sentinel and otherwise passes the
|
|
4594
|
+
// remaining argv straight to the wrapped child — it never reaches the
|
|
4595
|
+
// dispatcher's "Unknown command" fallback, but a global flag left in LEADING
|
|
4596
|
+
// position (e.g. `--exit-contract=v2 run-with-timeout ...`) would be spliced
|
|
4597
|
+
// out too late if these ran after, since neither block currently exists
|
|
4598
|
+
// below this point to consume it. Splicing here, before run-with-timeout's
|
|
4599
|
+
// own argv slicing, is what keeps both flags position-independent for every
|
|
4600
|
+
// command, run-with-timeout included. Do not move these back below the
|
|
4601
|
+
// run-with-timeout block (#confirmed regression: leading --exit-contract=v2
|
|
4602
|
+
// and leading --json-errors both broke run-with-timeout when these blocks
|
|
4603
|
+
// sat after it).
|
|
4089
4604
|
|
|
4090
4605
|
// --json-errors / GSD_JSON_ERRORS=1: when active, error() emits structured
|
|
4091
4606
|
// JSON ({ ok: false, reason: <ERROR_REASON code>, message }) to stderr
|
|
@@ -4105,6 +4620,41 @@ async function main() {
|
|
|
4105
4620
|
setJsonErrorMode(true);
|
|
4106
4621
|
}
|
|
4107
4622
|
|
|
4623
|
+
// --exit-contract=<v> / GSD_EXIT_CONTRACT: resolve FIRST, before the splice
|
|
4624
|
+
// below, so an invalid value (e.g. `v3`, or an empty `--exit-contract=`)
|
|
4625
|
+
// throws EARLY — matching the --json-errors block's own "detect early,
|
|
4626
|
+
// before any flag parsing that can fire error()" rationale above. This also
|
|
4627
|
+
// memoizes the resolved version into the shared contract-version cell so a
|
|
4628
|
+
// later terminateNow()/runMain() call projects against it correctly.
|
|
4629
|
+
//
|
|
4630
|
+
// The argv splice must happen here too, otherwise the dispatcher below sees
|
|
4631
|
+
// "--exit-contract=<v>" as an unknown command when the flag is given in
|
|
4632
|
+
// LEADING position (argv[0] is what the dispatcher treats as the command
|
|
4633
|
+
// name). Splice EVERY occurrence, not just the first — findExitContractFlag
|
|
4634
|
+
// only consults the first match, so a stray second token would otherwise
|
|
4635
|
+
// survive into the dispatcher and reproduce the same "Unknown command".
|
|
4636
|
+
resolveContractVersion({ argv: process.argv, env: process.env });
|
|
4637
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
4638
|
+
if (typeof args[i] === 'string' && args[i].startsWith('--exit-contract=')) {
|
|
4639
|
+
args.splice(i, 1);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
// #2351: run-with-timeout bounds a spawned command's wall clock portably
|
|
4644
|
+
// (coreutils-independent). It MUST intercept HERE, before the remaining
|
|
4645
|
+
// flag parsing below — the wrapped command's argv is opaque and may itself
|
|
4646
|
+
// contain --raw / --cwd / --pick that this dispatcher would otherwise
|
|
4647
|
+
// consume. (--json-errors / --exit-contract are handled above this block,
|
|
4648
|
+
// not below, precisely so they keep working with run-with-timeout.)
|
|
4649
|
+
{
|
|
4650
|
+
let rwt = args;
|
|
4651
|
+
if (rwt[0] === 'query') rwt = rwt.slice(1);
|
|
4652
|
+
if (rwt[0] === 'run-with-timeout') {
|
|
4653
|
+
// Return the child's exit code; runMain() maps it to process.exitCode.
|
|
4654
|
+
return runWithTimeout(rwt.slice(1));
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4108
4658
|
// Optional cwd override for sandboxed subagents running outside project root.
|
|
4109
4659
|
let cwd = process.cwd();
|
|
4110
4660
|
const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
|
|
@@ -4125,6 +4675,39 @@ async function main() {
|
|
|
4125
4675
|
error(`Invalid --cwd: ${cwd}`, ERROR_REASON.USAGE);
|
|
4126
4676
|
}
|
|
4127
4677
|
|
|
4678
|
+
// #3881: --project-dir <path> is a documented (docs/CONFIGURATION.md,
|
|
4679
|
+
// "Project-Root Resolution in Multi-Repo Workspaces") explicit override of
|
|
4680
|
+
// the project root. It is idempotent under findProjectRoot's ancestor
|
|
4681
|
+
// walk-up — i.e. it short-circuits the walk-up rather than seeding it —
|
|
4682
|
+
// so it MUST be validated and applied here, before findProjectRoot ever
|
|
4683
|
+
// runs, and its result must skip that call entirely below. A relative
|
|
4684
|
+
// value resolves against process.cwd(), matching --cwd's own resolution.
|
|
4685
|
+
let projectDirExplicit = false;
|
|
4686
|
+
const projectDirEqArg = args.find(arg => arg.startsWith('--project-dir='));
|
|
4687
|
+
const projectDirIdx = args.indexOf('--project-dir');
|
|
4688
|
+
let projectDirValue;
|
|
4689
|
+
if (projectDirEqArg) {
|
|
4690
|
+
projectDirValue = projectDirEqArg.slice('--project-dir='.length).trim();
|
|
4691
|
+
if (!projectDirValue) error('Missing value for --project-dir', ERROR_REASON.USAGE);
|
|
4692
|
+
args.splice(args.indexOf(projectDirEqArg), 1);
|
|
4693
|
+
} else if (projectDirIdx !== -1) {
|
|
4694
|
+
projectDirValue = args[projectDirIdx + 1];
|
|
4695
|
+
if (!projectDirValue || projectDirValue.startsWith('--')) error('Missing value for --project-dir', ERROR_REASON.USAGE);
|
|
4696
|
+
args.splice(projectDirIdx, 2);
|
|
4697
|
+
}
|
|
4698
|
+
if (projectDirValue !== undefined) {
|
|
4699
|
+
const resolvedProjectDir = path.resolve(projectDirValue);
|
|
4700
|
+
if (!fs.existsSync(resolvedProjectDir) || !fs.statSync(resolvedProjectDir).isDirectory()) {
|
|
4701
|
+
error(`Invalid --project-dir: ${resolvedProjectDir} (path does not exist or is not a directory)`, ERROR_REASON.USAGE);
|
|
4702
|
+
}
|
|
4703
|
+
const resolvedProjectDirPlanning = path.join(resolvedProjectDir, '.planning');
|
|
4704
|
+
if (!fs.existsSync(resolvedProjectDirPlanning) || !fs.statSync(resolvedProjectDirPlanning).isDirectory()) {
|
|
4705
|
+
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);
|
|
4706
|
+
}
|
|
4707
|
+
cwd = resolvedProjectDir;
|
|
4708
|
+
projectDirExplicit = true;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4128
4711
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
4129
4712
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
4130
4713
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
@@ -4237,24 +4820,45 @@ async function main() {
|
|
|
4237
4820
|
}
|
|
4238
4821
|
}
|
|
4239
4822
|
|
|
4240
|
-
|
|
4823
|
+
// #3881: an explicit --project-dir already IS the resolved project root
|
|
4824
|
+
// (validated above) — findProjectRoot's ancestor walk-up must not run
|
|
4825
|
+
// over it, per docs/CONFIGURATION.md's documented idempotence.
|
|
4826
|
+
if (!projectDirExplicit && !SKIP_ROOT_RESOLUTION.has(command)) {
|
|
4241
4827
|
cwd = findProjectRoot(cwd);
|
|
4242
4828
|
}
|
|
4243
4829
|
|
|
4244
4830
|
// When --pick is active, capture stdout and extract the requested field.
|
|
4831
|
+
// ADR-3473 §8.4 (#3365, #3358): an absent field or non-JSON command output
|
|
4832
|
+
// is a failure ("I could not answer"), never a demotion to an empty answer
|
|
4833
|
+
// at exit 0. `resolveAtFileOutput` MUST run before JSON.parse — @file:
|
|
4834
|
+
// payloads (io.cjs output() writes these for JSON > 50KB) are not
|
|
4835
|
+
// themselves JSON text, so resolving late would make every large result a
|
|
4836
|
+
// false "output was not JSON" (negative space N8).
|
|
4245
4837
|
if (pickField) {
|
|
4246
4838
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
4247
4839
|
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
4248
4840
|
});
|
|
4249
4841
|
const resolved = resolveAtFileOutput(captured);
|
|
4842
|
+
let obj;
|
|
4250
4843
|
try {
|
|
4251
|
-
|
|
4252
|
-
const value = extractField(obj, pickField);
|
|
4253
|
-
const result = value === null || value === undefined ? '' : String(value);
|
|
4254
|
-
fs.writeSync(1, result);
|
|
4844
|
+
obj = JSON.parse(resolved);
|
|
4255
4845
|
} catch {
|
|
4256
|
-
|
|
4846
|
+
error(`--pick ${formatDiagnosticToken(pickField)}: command output was not JSON`, ERROR_REASON.PICK_OUTPUT_NOT_JSON);
|
|
4847
|
+
return;
|
|
4257
4848
|
}
|
|
4849
|
+
const { found, value } = extractField(obj, pickField);
|
|
4850
|
+
if (!found) {
|
|
4851
|
+
const rootDescription = isPlainRecord(obj)
|
|
4852
|
+
? `available top-level keys: ${Object.keys(obj).map(formatKeyForDiagnosticList).join(', ') || '(none)'}`
|
|
4853
|
+
: `the command's output is a JSON ${describeJsonRootType(obj)}, not an object with that field`;
|
|
4854
|
+
error(`--pick ${formatDiagnosticToken(pickField)}: field not found; ${rootDescription}`, ERROR_REASON.PICK_FIELD_ABSENT);
|
|
4855
|
+
return;
|
|
4856
|
+
}
|
|
4857
|
+
// N1/N2: `null` and `''` are answers, not failures — an absent field
|
|
4858
|
+
// above already exited non-zero, so reaching here means the field EXISTS
|
|
4859
|
+
// and this is its real value (including `0` and `false`, #3365).
|
|
4860
|
+
const result = value === null || value === undefined ? '' : String(value);
|
|
4861
|
+
fs.writeSync(1, result);
|
|
4258
4862
|
return;
|
|
4259
4863
|
}
|
|
4260
4864
|
|
|
@@ -4316,27 +4920,68 @@ function resolveAtFileOutput(captured) {
|
|
|
4316
4920
|
return fs.readFileSync(captured.slice(6), 'utf-8');
|
|
4317
4921
|
}
|
|
4318
4922
|
|
|
4923
|
+
// A plain object root/intermediate value — everything else (null, an array,
|
|
4924
|
+
// a number, a string, a boolean) is treated as non-object for NAMED-key
|
|
4925
|
+
// lookup purposes (#3365 / #3358, ADR-3473 §8.4): only bracket notation may
|
|
4926
|
+
// reach into an array.
|
|
4927
|
+
function isPlainRecord(v) {
|
|
4928
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
// Describes the JSON root's shape for a --pick "field not found" message
|
|
4932
|
+
// when the root is NOT a plain object (so listing "top-level keys" would be
|
|
4933
|
+
// meaningless).
|
|
4934
|
+
function describeJsonRootType(v) {
|
|
4935
|
+
if (Array.isArray(v)) return 'array';
|
|
4936
|
+
if (v === null) return 'null';
|
|
4937
|
+
return typeof v;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
// A command's JSON output can be a USER-authored document (e.g. `frontmatter
|
|
4941
|
+
// get`), so its top-level keys are untrusted the same way an argv token is.
|
|
4942
|
+
// `formatDiagnosticToken` (io.cjs) is the shared escape (see its JSDoc for
|
|
4943
|
+
// why `error()` cannot do this itself); this thin wrapper reuses that exact
|
|
4944
|
+
// escaping but strips the surrounding quotes JSON.stringify adds, so a key
|
|
4945
|
+
// list reads as "a, b, c" rather than the noisier "\"a\", \"b\", \"c\"" while
|
|
4946
|
+
// a key containing \n/\r/\t/other C0 bytes still cannot forge a second
|
|
4947
|
+
// stderr "Error:" line or span more than one line.
|
|
4948
|
+
function formatKeyForDiagnosticList(key) {
|
|
4949
|
+
return formatDiagnosticToken(key).slice(1, -1);
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4319
4952
|
/**
|
|
4320
4953
|
* Extract a field from an object using dot-notation and bracket syntax.
|
|
4321
4954
|
* Supports: 'field', 'parent.child', 'arr[-1]', 'arr[0]'
|
|
4955
|
+
*
|
|
4956
|
+
* Returns a discriminated `{ found, value }` rather than a bare value so a
|
|
4957
|
+
* caller can distinguish "the field exists and is null/''/0/false" (an
|
|
4958
|
+
* ANSWER, exit 0) from "no such field" (an absence, exit non-zero) — #3365.
|
|
4959
|
+
* Reports NOT-FOUND for: a missing key; a dotted path that dies partway; an
|
|
4960
|
+
* array index out of range (after negative-index normalization); a bracket
|
|
4961
|
+
* applied to a non-array; and any key lookup against a non-object (null, a
|
|
4962
|
+
* number, a string, a boolean, or an array root).
|
|
4322
4963
|
*/
|
|
4323
4964
|
function extractField(obj, fieldPath) {
|
|
4324
4965
|
const parts = fieldPath.split('.');
|
|
4325
4966
|
let current = obj;
|
|
4326
4967
|
for (const part of parts) {
|
|
4327
|
-
if (current === null || current === undefined) return undefined;
|
|
4328
4968
|
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
|
|
4329
4969
|
if (bracketMatch) {
|
|
4330
4970
|
const key = bracketMatch[1];
|
|
4331
4971
|
const index = parseInt(bracketMatch[2], 10);
|
|
4332
|
-
current
|
|
4333
|
-
|
|
4334
|
-
|
|
4972
|
+
if (!isPlainRecord(current)) return { found: false, value: undefined };
|
|
4973
|
+
const arr = current[key];
|
|
4974
|
+
if (!Array.isArray(arr)) return { found: false, value: undefined };
|
|
4975
|
+
const resolvedIndex = index < 0 ? arr.length + index : index;
|
|
4976
|
+
if (resolvedIndex < 0 || resolvedIndex >= arr.length) return { found: false, value: undefined };
|
|
4977
|
+
current = arr[resolvedIndex];
|
|
4335
4978
|
} else {
|
|
4979
|
+
if (!isPlainRecord(current)) return { found: false, value: undefined };
|
|
4980
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) return { found: false, value: undefined };
|
|
4336
4981
|
current = current[part];
|
|
4337
4982
|
}
|
|
4338
4983
|
}
|
|
4339
|
-
return current;
|
|
4984
|
+
return { found: true, value: current };
|
|
4340
4985
|
}
|
|
4341
4986
|
|
|
4342
4987
|
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
@@ -4405,5 +5050,21 @@ module.exports = {
|
|
|
4405
5050
|
// #3275: exported for tests — the shared PATH+PATHEXT resolver behind
|
|
4406
5051
|
// review-lane invoke's `deps.spawn` / `deps.hasBinary` seams.
|
|
4407
5052
|
resolveSpawnBinary,
|
|
5053
|
+
// #3714 follow-up: exported for tests — the dispatch model-pin VALUE
|
|
5054
|
+
// policy (charset accept/render parity, max-length boundary, leading-char
|
|
5055
|
+
// anchor) is otherwise unreachable from outside the dispatchOverlayCapabilityCommand closure.
|
|
5056
|
+
resolveDispatchModelPin,
|
|
5057
|
+
MODEL_ID_CHARSET_RE,
|
|
5058
|
+
// The shared character-class body both MODEL_ID_CHARSET_RE and
|
|
5059
|
+
// MODEL_ID_SANITIZE_STRIP_RE are derived from — exported so a test can
|
|
5060
|
+
// assert its own expected charset literal EQUALS this value, making a
|
|
5061
|
+
// silent widening of the production body fail the test instead of only
|
|
5062
|
+
// the (unexported) regexes built from it.
|
|
5063
|
+
MODEL_ID_CHARSET_BODY,
|
|
5064
|
+
// Non-global companion of the internal g-flagged sanitize regex — see the
|
|
5065
|
+
// comment at its definition for why the g-flagged instance is never
|
|
5066
|
+
// exported.
|
|
5067
|
+
MODEL_ID_SANITIZE_STRIP_RE,
|
|
5068
|
+
MODEL_ID_MAX_LENGTH,
|
|
4408
5069
|
};
|
|
4409
5070
|
|