@opengsd/gsd-core 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -9
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +15 -55
- package/agents/gsd-planner.md +6 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +160 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +3 -2
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +3 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +34 -16
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +134 -1
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +122 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
* it imports `stripBOM`/`scanTomlLines` from here and its regression suite
|
|
15
15
|
* (`tests/agent-install-check.test.cjs`) is the proof.
|
|
16
16
|
*
|
|
17
|
+
* #3897 rung 3 amendment: `deriveCodexSandboxMode`/`CODEX_SANDBOX_HOLDS`/
|
|
18
|
+
* `validateCodexSandboxHolds` also live here now (moved from `bin/install.js`).
|
|
19
|
+
* That IS a policy (which `sandbox_mode` a role's tool contract derives), a
|
|
20
|
+
* narrow exception to this module's "document model, not policy" charter above
|
|
21
|
+
* — made because `bin/install.js` cannot be the shared owner: requiring it for
|
|
22
|
+
* its side effect on `require()` (the CLI banner print) corrupts every
|
|
23
|
+
* stdout-JSON caller (`agent-install-check.cts`'s `checkCodexSandboxPosture`).
|
|
24
|
+
* This module was already the single fs/path-free-parsing home both callers
|
|
25
|
+
* shared; `fs`/`path` are imported below ONLY for `validateCodexSandboxHolds`'s
|
|
26
|
+
* roster check — still node builtins only, no third-party or bin/lib dependency.
|
|
27
|
+
*
|
|
17
28
|
* ── The reconciliation (40-design.md) ──────────────────────────────────────
|
|
18
29
|
*
|
|
19
30
|
* Phase 2's reader and this phase's writer disagree on how to handle an
|
|
@@ -31,8 +42,11 @@
|
|
|
31
42
|
* One block-range detector, two call sites, two policies — never two detectors
|
|
32
43
|
* that could silently drift from each other.
|
|
33
44
|
*/
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
34
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.PARSE_REASON = void 0;
|
|
49
|
+
exports.CODEX_SANDBOX_HOLDS = exports.PARSE_REASON = void 0;
|
|
36
50
|
exports.stripBOM = stripBOM;
|
|
37
51
|
exports.unquoteTomlValue = unquoteTomlValue;
|
|
38
52
|
exports.findDeveloperInstructionsBlockRange = findDeveloperInstructionsBlockRange;
|
|
@@ -41,6 +55,13 @@ exports.parseCodexAgentToml = parseCodexAgentToml;
|
|
|
41
55
|
exports.renderCodexAgentToml = renderCodexAgentToml;
|
|
42
56
|
exports.stripModel = stripModel;
|
|
43
57
|
exports.stripReasoningEffort = stripReasoningEffort;
|
|
58
|
+
exports.normalizeSandboxIdentity = normalizeSandboxIdentity;
|
|
59
|
+
exports.isSandboxHeld = isSandboxHeld;
|
|
60
|
+
exports.extractToolsValue = extractToolsValue;
|
|
61
|
+
exports.deriveCodexSandboxMode = deriveCodexSandboxMode;
|
|
62
|
+
exports.validateCodexSandboxHolds = validateCodexSandboxHolds;
|
|
63
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
64
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
65
|
/** Frozen reason enum for a failed {@link parseCodexAgentToml}. */
|
|
45
66
|
exports.PARSE_REASON = Object.freeze({
|
|
46
67
|
UNTERMINATED_BLOCK: 'unterminated_block',
|
|
@@ -108,11 +129,15 @@ function findDeveloperInstructionsBlockRange(lines) {
|
|
|
108
129
|
// lenient reader, boolean-only for reasoning effort) and parseCodexAgentToml
|
|
109
130
|
// (the strict writer, which also needs the effort's value and both keys' line
|
|
110
131
|
// indices so stripModel/stripReasoningEffort can remove exactly one line).
|
|
132
|
+
// `sandbox_mode` (#3897 rung 4 MINOR finding 2) rides the same block-aware
|
|
133
|
+
// pass as `model`/`model_reasoning_effort` — one scanner, never a second,
|
|
134
|
+
// naive whole-file regex that could match prose inside the block.
|
|
111
135
|
function scanHeaderLines(lines, blockStart, blockEnd) {
|
|
112
136
|
let model = null;
|
|
113
137
|
let modelLineIndex = null;
|
|
114
138
|
let reasoningEffort = null;
|
|
115
139
|
let reasoningEffortLineIndex = null;
|
|
140
|
+
let sandboxMode = null;
|
|
116
141
|
for (let i = 0; i < lines.length; i++) {
|
|
117
142
|
if (blockStart !== -1 && i >= blockStart && i <= blockEnd)
|
|
118
143
|
continue;
|
|
@@ -132,8 +157,11 @@ function scanHeaderLines(lines, blockStart, blockEnd) {
|
|
|
132
157
|
reasoningEffort = unquoteTomlValue(rawValue);
|
|
133
158
|
reasoningEffortLineIndex = i;
|
|
134
159
|
}
|
|
160
|
+
else if (key === 'sandbox_mode') {
|
|
161
|
+
sandboxMode = unquoteTomlValue(rawValue);
|
|
162
|
+
}
|
|
135
163
|
}
|
|
136
|
-
return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex };
|
|
164
|
+
return { model, modelLineIndex, reasoningEffort, reasoningEffortLineIndex, sandboxMode };
|
|
137
165
|
}
|
|
138
166
|
/**
|
|
139
167
|
* The LENIENT reader entry point (Phase 2, moved verbatim in behavior). Never
|
|
@@ -144,8 +172,8 @@ function scanHeaderLines(lines, blockStart, blockEnd) {
|
|
|
144
172
|
function scanTomlLines(content) {
|
|
145
173
|
const lines = content.split(/\r?\n/);
|
|
146
174
|
const { start, end } = findDeveloperInstructionsBlockRange(lines);
|
|
147
|
-
const { model, reasoningEffort } = scanHeaderLines(lines, start, end);
|
|
148
|
-
return { model, hasReasoningEffort: reasoningEffort !== null };
|
|
175
|
+
const { model, reasoningEffort, sandboxMode } = scanHeaderLines(lines, start, end);
|
|
176
|
+
return { model, hasReasoningEffort: reasoningEffort !== null, sandboxMode };
|
|
149
177
|
}
|
|
150
178
|
// Splits `content` into `{lines, terminators}` where `terminators[i]` is the
|
|
151
179
|
// terminator that FOLLOWS `lines[i]` (`'\r\n'`, `'\r'`, `'\n'`, or `''` for a
|
|
@@ -327,3 +355,381 @@ function stripReasoningEffort(doc) {
|
|
|
327
355
|
return doc;
|
|
328
356
|
return removeLine(doc, doc.reasoningEffortLineIndex, 'reasoningEffort');
|
|
329
357
|
}
|
|
358
|
+
// ── Codex sandbox_mode derivation (#3897 rung 3, ADR-3473 §8.3) ────────────
|
|
359
|
+
//
|
|
360
|
+
// Moved here from `bin/install.js` (fix for the CAUSE A regression this rung
|
|
361
|
+
// introduced): `agent-install-check.cts`'s `checkCodexSandboxPosture` used to
|
|
362
|
+
// lazily `require(bin/install.js)` to reach this derivation — but requiring
|
|
363
|
+
// `bin/install.js` runs its whole top-level script, including the ASCII
|
|
364
|
+
// banner print to stdout, which corrupted every stdout-JSON caller downstream
|
|
365
|
+
// of `checkCodexSandboxPosture` (`gsd-tools validate agents`). This module is
|
|
366
|
+
// a genuine leaf with no top-level side effects, so both `bin/install.js` and
|
|
367
|
+
// `agent-install-check.cts` import the derivation from here instead — ONE
|
|
368
|
+
// owner, no second predicate (routing `src/` through `bin/install.js` was
|
|
369
|
+
// backwards layering to begin with).
|
|
370
|
+
//
|
|
371
|
+
// This module does NOT parse frontmatter (there is no third copy of that
|
|
372
|
+
// extraction here — two already exist, `bin/install.js` and
|
|
373
|
+
// `runtime-artifact-conversion.cts`). `deriveCodexSandboxMode` below takes
|
|
374
|
+
// the already-resolved `tools:` value as a plain string parameter: every
|
|
375
|
+
// caller already has it (or the raw frontmatter to pull it from) in hand
|
|
376
|
+
// before calling in, so this module stays a pure predicate over data
|
|
377
|
+
// supplied by the caller — never a document reader itself. This also means
|
|
378
|
+
// the module never needs the full YAML-backed `frontmatter.cts` engine
|
|
379
|
+
// (vendored js-yaml + anchor/alias refusal + comment-channel plumbing, built
|
|
380
|
+
// for a much broader contract than a single `tools:` line lookup) — it has
|
|
381
|
+
// no frontmatter-parsing need at all anymore.
|
|
382
|
+
/**
|
|
383
|
+
* The 17 roles measured as widening under derivation (declare Write/Edit,
|
|
384
|
+
* never in the pre-#3897 `CODEX_AGENT_SANDBOX` map, so the old
|
|
385
|
+
* `|| 'read-only'` fallback silently under-granted them). Pinned to
|
|
386
|
+
* `read-only` pending the open question of whether Codex enforces
|
|
387
|
+
* `sandbox_mode` or treats it as advisory (HALT.md). This list is CLOSED and
|
|
388
|
+
* SHRINK-ONLY: a new writing role never lands here (S6, T26); it is validated
|
|
389
|
+
* against the live tool contract every time it is consulted
|
|
390
|
+
* ({@link _deriveCodexSandboxModeFromTools}) and against the real
|
|
391
|
+
* `agents/` roster by a dedicated test (`tests/codex-config.test.cjs` T24/T25)
|
|
392
|
+
* so a stale or orphaned entry fails loudly instead of being silently
|
|
393
|
+
* honored forever.
|
|
394
|
+
*
|
|
395
|
+
* `gsd-nyquist-auditor` is the 17th entry, added by the list-form `tools:`
|
|
396
|
+
* parse fix (#3897 follow-up): its `tools:` frontmatter uses YAML block-list
|
|
397
|
+
* form (`tools:` + indented `- Item` lines) and declares both Write and
|
|
398
|
+
* Edit, but the original `extractToolsLine`/`extractFrontmatterField`
|
|
399
|
+
* readers only ever saw the first list item (`- Read`) and derived
|
|
400
|
+
* `read-only` by accident, not by design. {@link extractToolsValue} now
|
|
401
|
+
* parses the list correctly, so this role genuinely derives
|
|
402
|
+
* `workspace-write` from its tool contract — HALT.md's original 16-role
|
|
403
|
+
* count measured against the pre-fix (single-line) readers and undercounted
|
|
404
|
+
* this role. It is held here for the same reason as the other 16: pending
|
|
405
|
+
* Codex's `sandbox_mode` enforcement decision, not because the derivation is
|
|
406
|
+
* wrong.
|
|
407
|
+
*/
|
|
408
|
+
exports.CODEX_SANDBOX_HOLDS = Object.freeze({
|
|
409
|
+
'gsd-ai-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
410
|
+
'gsd-code-fixer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
411
|
+
'gsd-code-reviewer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
412
|
+
'gsd-debug-session-manager': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
413
|
+
'gsd-doc-classifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
414
|
+
'gsd-doc-synthesizer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
415
|
+
'gsd-doc-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
416
|
+
'gsd-doc-writer': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
417
|
+
'gsd-dom-verifier': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
418
|
+
'gsd-domain-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
419
|
+
'gsd-eval-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
420
|
+
'gsd-eval-planner': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
421
|
+
'gsd-intel-updater': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
422
|
+
'gsd-pattern-mapper': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
423
|
+
'gsd-ui-auditor': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
424
|
+
'gsd-ui-researcher': 'declares Write/Edit; pending Codex sandbox_mode enforcement decision',
|
|
425
|
+
'gsd-nyquist-auditor': 'declares Write/Edit (YAML list-form tools:, surfaced by the list-form parse fix); pending Codex sandbox_mode enforcement decision',
|
|
426
|
+
});
|
|
427
|
+
// True iff a `tools:` frontmatter value declares Write or Edit as a whole
|
|
428
|
+
// token (never a substring match, so a hypothetical "Edith"-named tool could
|
|
429
|
+
// never collide). Single predicate owner for both the emitter
|
|
430
|
+
// ({@link _deriveCodexSandboxModeFromTools}) and the posture check
|
|
431
|
+
// (`agent-install-check.cts`'s `checkCodexSandboxPosture`, via
|
|
432
|
+
// {@link deriveCodexSandboxMode}).
|
|
433
|
+
//
|
|
434
|
+
// #3897 security review F5: a negation form — `All tools except Agent,
|
|
435
|
+
// Write, Edit` — used to derive `workspace-write` because the plain
|
|
436
|
+
// comma-tokenizer below saw the literal token `Write` and never noticed it
|
|
437
|
+
// was named as an EXCLUSION, not a grant. That is fail-OPEN: the value's
|
|
438
|
+
// stated meaning is "everything except these", so a `Write`/`Edit` name
|
|
439
|
+
// after `except` means the role does NOT get them. No shipped roster agent
|
|
440
|
+
// uses this form today, but a wrong answer here widens silently, so it is
|
|
441
|
+
// handled: once the literal word `except` (case-insensitive) appears
|
|
442
|
+
// anywhere in the value, this is a "<grant> except <exclusions>" statement
|
|
443
|
+
// and only the tokens AFTER `except` decide the verdict — `Write`/`Edit`
|
|
444
|
+
// declare broader iff `except` is ABSENT, or present but does not name them.
|
|
445
|
+
function _codexToolsDeclareWriteOrEdit(toolsRaw) {
|
|
446
|
+
const raw = String(toolsRaw || '');
|
|
447
|
+
const exceptIndex = raw.search(/\bexcept\b/i);
|
|
448
|
+
if (exceptIndex !== -1) {
|
|
449
|
+
const exclusionsText = raw.slice(exceptIndex).replace(/^\S*\s*except\b/i, '');
|
|
450
|
+
const exclusions = exclusionsText.split(',').map((t) => t.trim()).filter(Boolean);
|
|
451
|
+
const excludesWriteOrEdit = exclusions.includes('Write') || exclusions.includes('Edit');
|
|
452
|
+
return !excludesWriteOrEdit;
|
|
453
|
+
}
|
|
454
|
+
const tokens = raw.split(',').map((t) => t.trim());
|
|
455
|
+
return tokens.includes('Write') || tokens.includes('Edit');
|
|
456
|
+
}
|
|
457
|
+
// #3897 security review F1/F3: control-character/whitespace class used by
|
|
458
|
+
// {@link normalizeSandboxIdentity} to trim more than plain ASCII whitespace
|
|
459
|
+
// from BOTH edges of a candidate identity before it is compared against the
|
|
460
|
+
// hold set. A bare .trim() only strips the ASCII whitespace \s already
|
|
461
|
+
// covers; the hold bypass this closes (a trailing space, \n/\r, or NBSP
|
|
462
|
+
// after `gsd-doc-writer`) is exactly a value that survives .trim() untouched.
|
|
463
|
+
// Spelled entirely as \u escapes (never a literal invisible codepoint in
|
|
464
|
+
// the source), matching this module's own BOM_CHAR convention above: NBSP
|
|
465
|
+
// (U+00A0), the zero-width space/non-joiner/joiner + LTR/RTL marks
|
|
466
|
+
// (U+200B-U+200F), the BOM/ZWNBSP (U+FEFF), and the full C0 control range
|
|
467
|
+
// + DEL (U+0000-U+001F, U+007F), alongside ASCII whitespace (\s).
|
|
468
|
+
const SANDBOX_IDENTITY_EDGE_RE = new RegExp('^[\\s\\u0000-\\u001f\\u007f\\u00a0\\u200b-\\u200f\\ufeff]+' +
|
|
469
|
+
'|[\\s\\u0000-\\u001f\\u007f\\u00a0\\u200b-\\u200f\\ufeff]+$', 'g');
|
|
470
|
+
// Anything outside this set, AFTER normalization, is "suspicious" — see
|
|
471
|
+
// {@link isSandboxHeld}'s docblock for why fail-closed (never enumerate
|
|
472
|
+
// confusables) is the only tractable answer here.
|
|
473
|
+
const SANDBOX_IDENTITY_SUSPICIOUS_RE = /[^a-z0-9._-]/;
|
|
474
|
+
/**
|
|
475
|
+
* Canonicalizes a single candidate sandbox-hold identity (a source filename
|
|
476
|
+
* stem OR a frontmatter-derived `name:` value) into the same lowercase ASCII
|
|
477
|
+
* shape {@link CODEX_SANDBOX_HOLDS}'s keys are written in, so both candidate
|
|
478
|
+
* identities for one emitted artifact (see {@link isSandboxHeld}) can be
|
|
479
|
+
* compared against the hold set on equal footing.
|
|
480
|
+
*
|
|
481
|
+
* #3897 security review F1/F3 — pipeline, in this exact order:
|
|
482
|
+
* 1. basename it (strip through the LAST `/` or `\`) — a value carrying a
|
|
483
|
+
* path separator (`../agents/x`, `./x`) is reduced to its final segment
|
|
484
|
+
* before anything else runs, so a path-traversal-shaped candidate can
|
|
485
|
+
* never dodge the hold lookup by hiding behind a directory prefix.
|
|
486
|
+
* 2. trim ASCII whitespace AND the NBSP/zero-width/control-char class
|
|
487
|
+
* ({@link SANDBOX_IDENTITY_EDGE_RE}) from both ends.
|
|
488
|
+
* 3. strip trailing dots (`gsd-doc-writer.` collapses to the same key).
|
|
489
|
+
* 4. `String.prototype.normalize('NFKC')` — canonicalizes compatibility
|
|
490
|
+
* variants (e.g. fullwidth `g` U+FF47 folds to ASCII `g`) to their
|
|
491
|
+
* standard form so a widened-by-Unicode-lookalike name collapses onto
|
|
492
|
+
* the real held key instead of merely looking similar to it.
|
|
493
|
+
* 5. lowercase — ASCII recasing already matched the hold set before this
|
|
494
|
+
* rung; this keeps that property after the above steps.
|
|
495
|
+
*
|
|
496
|
+
* Returns `null` (never throws) for a non-string input or when the fully
|
|
497
|
+
* normalized result is empty, so callers can treat `null` as "not a real
|
|
498
|
+
* identity, cannot be held, but see the `suspicious` companion signal".
|
|
499
|
+
*/
|
|
500
|
+
function normalizeSandboxIdentity(raw) {
|
|
501
|
+
if (typeof raw !== 'string')
|
|
502
|
+
return null;
|
|
503
|
+
const lastSlash = Math.max(raw.lastIndexOf('/'), raw.lastIndexOf('\\'));
|
|
504
|
+
let value = lastSlash === -1 ? raw : raw.slice(lastSlash + 1);
|
|
505
|
+
value = value.replace(SANDBOX_IDENTITY_EDGE_RE, '');
|
|
506
|
+
value = value.replace(/\.+$/, '');
|
|
507
|
+
value = value.normalize('NFKC');
|
|
508
|
+
value = value.toLowerCase();
|
|
509
|
+
return value === '' ? null : value;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* `held` is true iff ANY candidate identity, once normalized, names a real
|
|
513
|
+
* {@link CODEX_SANDBOX_HOLDS} entry. `suspicious` is true iff any candidate,
|
|
514
|
+
* after normalization, still contains a character outside `[a-z0-9._-]`.
|
|
515
|
+
*
|
|
516
|
+
* #3897 security review F1 (the blocker): the sandbox decision must be safe
|
|
517
|
+
* for the ARTIFACT IT LANDS ON, not for whichever single identity happens to
|
|
518
|
+
* be handy at the call site. `bin/install.js`'s emit loop has TWO candidate
|
|
519
|
+
* identities for one `.toml` artifact — the source filename stem (what the
|
|
520
|
+
* hold lookup used to be keyed on) and the frontmatter `name:` value (what
|
|
521
|
+
* the OUTPUT PATH is actually keyed on) — and an attacker who controls
|
|
522
|
+
* frontmatter can make them disagree (rename the file, or plant a sibling
|
|
523
|
+
* file whose `name:` collides with a held role). Deciding over only one of
|
|
524
|
+
* the two and applying the result to whichever path the OTHER one names is
|
|
525
|
+
* exactly the regression this closes: this predicate takes every candidate
|
|
526
|
+
* that can influence either the derivation or the emitted path and takes the
|
|
527
|
+
* MOST RESTRICTIVE answer — `held` if any one of them is held.
|
|
528
|
+
*
|
|
529
|
+
* #3897 security review F3: `suspicious` is the fail-CLOSED half of this
|
|
530
|
+
* predicate, and it is deliberately not an attempt to enumerate Unicode
|
|
531
|
+
* confusables (Turkish dotted/dotless I, fullwidth forms, NFD combining
|
|
532
|
+
* marks, ...). Every one of the 35 shipped roster files is pure ASCII
|
|
533
|
+
* (`gsd-[a-z-]+`), so a non-ASCII-after-normalization identity cannot be a
|
|
534
|
+
* legitimate shipped role — flagging it `suspicious` (which
|
|
535
|
+
* {@link deriveCodexSandboxMode} turns into `read-only`) has zero false
|
|
536
|
+
* positives against real content, and refuses to widen on anything this
|
|
537
|
+
* module does not recognize instead of chasing an open-ended confusables
|
|
538
|
+
* list that will always be one codepoint behind the next lookalike.
|
|
539
|
+
*/
|
|
540
|
+
function isSandboxHeld(...candidates) {
|
|
541
|
+
let held = false;
|
|
542
|
+
let suspicious = false;
|
|
543
|
+
for (const candidate of candidates) {
|
|
544
|
+
const values = Array.isArray(candidate) ? candidate : [candidate];
|
|
545
|
+
for (const value of values) {
|
|
546
|
+
const normalized = normalizeSandboxIdentity(value);
|
|
547
|
+
if (normalized === null)
|
|
548
|
+
continue;
|
|
549
|
+
if (Object.prototype.hasOwnProperty.call(exports.CODEX_SANDBOX_HOLDS, normalized)) {
|
|
550
|
+
held = true;
|
|
551
|
+
}
|
|
552
|
+
if (SANDBOX_IDENTITY_SUSPICIOUS_RE.test(normalized)) {
|
|
553
|
+
suspicious = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return { held, suspicious };
|
|
558
|
+
}
|
|
559
|
+
// #3897 CAUSE A fix: a single-purpose `tools:`-VALUE reader, NOT a general
|
|
560
|
+
// frontmatter parser (`extractFrontmatterAndBody`/`extractFrontmatterField`
|
|
561
|
+
// were deliberately deleted from this module's ancestor — see the module
|
|
562
|
+
// header's "document model, not policy" charter). This module is the one
|
|
563
|
+
// genuinely side-effect-free leaf `agent-install-check.cts`'s
|
|
564
|
+
// `checkCodexSandboxPosture` already imports from for
|
|
565
|
+
// {@link deriveCodexSandboxMode}; giving it this reader too avoids importing
|
|
566
|
+
// `runtime-artifact-conversion.cjs` there just to pull one frontmatter field
|
|
567
|
+
// — that module's own dependency chain (`command-roster.cjs` →
|
|
568
|
+
// `scripts/fix-slash-commands.cjs`, a dev-only repo script) does not resolve
|
|
569
|
+
// from an installed tree, which is exactly what broke
|
|
570
|
+
// `tests/agent-install-check.test.cjs` against a synthetic install dir.
|
|
571
|
+
// Scoped to ONLY the `tools:` key: finds the leading `---`-delimited
|
|
572
|
+
// frontmatter block and returns its `tools:` value (quotes stripped), or
|
|
573
|
+
// `''` when there is no frontmatter block or no `tools:` key at all.
|
|
574
|
+
//
|
|
575
|
+
// #3897 rung 4/list-form fix: handles BOTH shapes a `tools:` key can take —
|
|
576
|
+
// (a) inline: `tools: Read, Write, Edit` on the same line as the key, and
|
|
577
|
+
// (b) YAML block-list form: `tools:` alone, followed by indented `- Item`
|
|
578
|
+
// lines (`agents/gsd-nyquist-auditor.md`, `agents/gsd-security-auditor.md`
|
|
579
|
+
// are the only two roster files using this shape today). The former
|
|
580
|
+
// implementation was a single `/^tools:\s*(.+)$/m` regex — `\s*` swallows the
|
|
581
|
+
// newline after a bare `tools:` key, so it silently matched into the FIRST
|
|
582
|
+
// list item's own line and returned just `"- Read"`, reading a real
|
|
583
|
+
// Write/Edit DECLARATION as an absence. List items are joined with `, ` so
|
|
584
|
+
// the result feeds {@link _codexToolsDeclareWriteOrEdit}'s comma-tokenizer
|
|
585
|
+
// unchanged. The list terminates at the first line that is not an indented
|
|
586
|
+
// `- Item` (a new frontmatter key, `---`, a blank line, or EOF) — it never
|
|
587
|
+
// grows into a general YAML parser (that boundary is deliberate, see above).
|
|
588
|
+
//
|
|
589
|
+
// #3897 security review F4: TOTAL for any input, not just a well-formed
|
|
590
|
+
// string — a Buffer, `undefined`, or `null` returns `undefined` rather than
|
|
591
|
+
// throwing on `.startsWith`, matching this module's "never throws" charter
|
|
592
|
+
// (see {@link deriveCodexSandboxMode}'s own totality note).
|
|
593
|
+
function extractToolsValue(agentContent) {
|
|
594
|
+
if (typeof agentContent !== 'string')
|
|
595
|
+
return undefined;
|
|
596
|
+
if (!agentContent.startsWith('---'))
|
|
597
|
+
return '';
|
|
598
|
+
const endIndex = agentContent.indexOf('---', 3);
|
|
599
|
+
if (endIndex === -1)
|
|
600
|
+
return '';
|
|
601
|
+
const frontmatter = agentContent.substring(3, endIndex);
|
|
602
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
603
|
+
const toolsLineIndex = lines.findIndex((line) => /^tools:/.test(line));
|
|
604
|
+
if (toolsLineIndex === -1)
|
|
605
|
+
return '';
|
|
606
|
+
const inlineMatch = lines[toolsLineIndex].match(/^tools:[ \t]*(\S.*)$/);
|
|
607
|
+
if (inlineMatch) {
|
|
608
|
+
return inlineMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
609
|
+
}
|
|
610
|
+
// Bare `tools:` key (nothing but optional trailing whitespace on its own
|
|
611
|
+
// line) — read the YAML block-list form that follows: indented `- Item`
|
|
612
|
+
// lines, one item per line, stopping at the first line that is not one.
|
|
613
|
+
const items = [];
|
|
614
|
+
for (let i = toolsLineIndex + 1; i < lines.length; i++) {
|
|
615
|
+
const itemMatch = lines[i].match(/^[ \t]+-[ \t]*(.+)$/);
|
|
616
|
+
if (!itemMatch)
|
|
617
|
+
break;
|
|
618
|
+
items.push(itemMatch[1].trim().replace(/^['"]|['"]$/g, ''));
|
|
619
|
+
}
|
|
620
|
+
return items.join(', ');
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The single owner of `sandbox_mode` derivation: `workspace-write` iff the
|
|
624
|
+
* role's own frontmatter `tools:` declares Write/Edit, UNLESS the role is
|
|
625
|
+
* held ({@link CODEX_SANDBOX_HOLDS}) at `read-only`.
|
|
626
|
+
*
|
|
627
|
+
* #3897 CAUSE C fix: this function is TOTAL — it never throws, for any
|
|
628
|
+
* (identity, toolsValue) pair. It used to throw when a held role's CONTENT
|
|
629
|
+
* (whatever the caller handed it) did not derive broader than its pin, on
|
|
630
|
+
* the theory that a stale hold should "fail loudly at the point the
|
|
631
|
+
* derivation runs". That reasoning does not survive contact with this
|
|
632
|
+
* function's actual contract: it is a pure predicate over whatever content
|
|
633
|
+
* the caller supplies, and tests legitimately pass synthetic fixtures for
|
|
634
|
+
* held role names, so the throw fired on arbitrary input, not just the real
|
|
635
|
+
* roster. It is also redundant even for real input — if a held role's
|
|
636
|
+
* content does not derive broader, applying the hold pins `read-only` and
|
|
637
|
+
* derivation would return `read-only` anyway; the hold is a no-op, so there
|
|
638
|
+
* is nothing to fail about at derivation time. The STALENESS invariant (S4)
|
|
639
|
+
* is a property of the real `agents/` roster, not of an arbitrary call's
|
|
640
|
+
* input, so it belongs — and stays enforced — only at the roster level: see
|
|
641
|
+
* {@link validateCodexSandboxHolds} and `tests/codex-config.test.cjs`
|
|
642
|
+
* T24/T25, which assert it against the real roster directly.
|
|
643
|
+
*
|
|
644
|
+
* `agentName` here MUST be every identity the content's own frontmatter
|
|
645
|
+
* could disagree with — i.e. the source `.md` FILENAME stem AND (when the
|
|
646
|
+
* caller has one) the frontmatter-derived `name:` display value, passed
|
|
647
|
+
* together as an array. Accepting only one of the two and applying the
|
|
648
|
+
* result to an ARTIFACT keyed on the other is the #3897 security review F1
|
|
649
|
+
* regression this function closes: `bin/install.js`'s emit loop decides
|
|
650
|
+
* `sandbox_mode` for the filename stem but writes the `.toml` at a path keyed
|
|
651
|
+
* on `name`, so a renamed file (stem drifts, `name:` stays pinned) or a
|
|
652
|
+
* sibling file whose `name:` collides with a held role could make the two
|
|
653
|
+
* identities disagree and land a held role's own artifact with
|
|
654
|
+
* `workspace-write`. See {@link isSandboxHeld} for the most-restrictive-wins
|
|
655
|
+
* rule across every candidate identity.
|
|
656
|
+
*
|
|
657
|
+
* `toolsRaw` is the already-resolved `tools:` frontmatter VALUE (e.g.
|
|
658
|
+
* `"Read, Write, Edit"`), not the frontmatter block or the full agent
|
|
659
|
+
* content — see {@link deriveCodexSandboxMode}'s doc for why.
|
|
660
|
+
*/
|
|
661
|
+
function _deriveCodexSandboxModeFromTools(agentName, toolsRaw) {
|
|
662
|
+
const derivesBroader = _codexToolsDeclareWriteOrEdit(toolsRaw || '');
|
|
663
|
+
const { held, suspicious } = isSandboxHeld(agentName);
|
|
664
|
+
if (held || suspicious) {
|
|
665
|
+
// A held (or unrecognizable/"suspicious", per F3) identity always pins
|
|
666
|
+
// read-only, whether or not its supplied content still derives broader
|
|
667
|
+
// (see #3897 CAUSE C note above the docstring for this function's
|
|
668
|
+
// caller-facing counterpart): if it no longer derives broader, the pin is
|
|
669
|
+
// a no-op and there is nothing to fail about here — the staleness
|
|
670
|
+
// invariant lives at the roster level instead
|
|
671
|
+
// ({@link validateCodexSandboxHolds}).
|
|
672
|
+
return 'read-only';
|
|
673
|
+
}
|
|
674
|
+
return derivesBroader ? 'workspace-write' : 'read-only';
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Exported form for external callers (`checkCodexSandboxPosture`,
|
|
678
|
+
* `generateCodexAgentToml`). Takes the already-resolved `tools:` frontmatter
|
|
679
|
+
* VALUE, not raw agent content or a frontmatter slice — this module does no
|
|
680
|
+
* frontmatter parsing at all (there is no third copy of that extraction
|
|
681
|
+
* here; see the module-header note above `CODEX_SANDBOX_HOLDS`). Both
|
|
682
|
+
* callers already have (or can trivially get) `tools:` in hand via this
|
|
683
|
+
* module's own {@link extractToolsValue} — the single shared extractor both
|
|
684
|
+
* `bin/install.js`'s `generateCodexAgentToml` and `agent-install-check.cts`'s
|
|
685
|
+
* `checkCodexSandboxPosture` route through (#3897 CAUSE A fix, and the
|
|
686
|
+
* #3897 list-form parse fix's Fix 3: `bin/install.js` used to read `tools:`
|
|
687
|
+
* via its own private `extractFrontmatterField`, a second single-line-only
|
|
688
|
+
* copy that disagreed with this module's reader on YAML block-list form —
|
|
689
|
+
* NOT `runtime-artifact-conversion.cts`'s general-purpose extractors either;
|
|
690
|
+
* that module's dependency chain does not resolve from an installed tree) —
|
|
691
|
+
* so this stays a pure predicate over data the caller already holds, never a
|
|
692
|
+
* document reader.
|
|
693
|
+
*
|
|
694
|
+
* `agentName` is one or more HOLD-LOOKUP IDENTITIES: pass the agent's
|
|
695
|
+
* canonical source filename stem (e.g. `gsd-doc-writer` for
|
|
696
|
+
* `agents/gsd-doc-writer.md`) as a plain string for the single-identity call
|
|
697
|
+
* sites this signature always supported, OR an array of every identity that
|
|
698
|
+
* can influence the emitted artifact (filename stem AND frontmatter `name:`)
|
|
699
|
+
* when the caller — like `bin/install.js`'s per-agent emit loop — has both
|
|
700
|
+
* (see the F1 security note on {@link _deriveCodexSandboxModeFromTools}
|
|
701
|
+
* above). TOTAL for any value here, including `undefined`, `null`, `[]`, and
|
|
702
|
+
* an array containing non-string members — none of those throw; they simply
|
|
703
|
+
* contribute nothing to the hold/suspicious check (see {@link isSandboxHeld}
|
|
704
|
+
* / {@link normalizeSandboxIdentity}).
|
|
705
|
+
*/
|
|
706
|
+
function deriveCodexSandboxMode(agentName, toolsRaw) {
|
|
707
|
+
return _deriveCodexSandboxModeFromTools(agentName, toolsRaw || '');
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* (S5) Every {@link CODEX_SANDBOX_HOLDS} key must still name a real
|
|
711
|
+
* `<agentsSrcDir>/<role>.md`. A hold for a role that no longer exists is
|
|
712
|
+
* stale and must fail loudly, not be silently ignored — else the hold list
|
|
713
|
+
* only ever grows/rots instead of shrinking to zero.
|
|
714
|
+
*
|
|
715
|
+
* NOT called from the install runtime path ({@link deriveCodexSandboxMode} /
|
|
716
|
+
* `bin/install.js`'s `installCodexConfig`): the shrink-to-zero invariant it
|
|
717
|
+
* checks is a REPO invariant about the canonical roster in `agents/`, not a
|
|
718
|
+
* property of whatever directory an install happens to read from — a
|
|
719
|
+
* partial/synthetic install source (a test fixture, a `--config-dir`
|
|
720
|
+
* subset) legitimately contains only a few agents, and a hold whose role is
|
|
721
|
+
* simply absent from THAT source dir must be inert, not fatal. The invariant
|
|
722
|
+
* is enforced instead as a test over the real `agents/` roster
|
|
723
|
+
* (`tests/codex-config.test.cjs` T24/T25). This function is kept, exported,
|
|
724
|
+
* for any caller that specifically wants to validate the CANONICAL roster
|
|
725
|
+
* (pass `agents/` itself, never an arbitrary install source).
|
|
726
|
+
*/
|
|
727
|
+
function validateCodexSandboxHolds(agentsSrcDir) {
|
|
728
|
+
for (const role of Object.keys(exports.CODEX_SANDBOX_HOLDS)) {
|
|
729
|
+
const agentFile = node_path_1.default.join(agentsSrcDir, `${role}.md`);
|
|
730
|
+
if (!node_fs_1.default.existsSync(agentFile)) {
|
|
731
|
+
throw new Error(`CODEX_SANDBOX_HOLDS: stale hold for "${role}" — no ${agentFile} exists. The hold list is ` +
|
|
732
|
+
'closed and shrink-only (ADR-3473 §8.3 / HALT.md); remove this entry.');
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
@@ -2,41 +2,171 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Command Argument Projection Module (ADR-457 build-at-publish: the
|
|
4
4
|
* hand-written bin/lib/command-arg-projection.cjs collapsed to a TypeScript
|
|
5
|
-
* source of truth).
|
|
6
|
-
*
|
|
5
|
+
* source of truth).
|
|
6
|
+
*
|
|
7
|
+
* ADR-3473 §8.4 ("failure is a value"): `parseNamedArgs` is now strict and
|
|
8
|
+
* returns a `Result` instead of silently accepting unrecognized or stray
|
|
9
|
+
* positional tokens. See .gsd/phase/feat-3884-failure-is-a-value/40-design.md
|
|
10
|
+
* for the full behavior table (A1-A18) and negative-space section (N1-N8).
|
|
7
11
|
*
|
|
8
12
|
* Shared helpers for command-family adapters to project argv tokens into
|
|
9
13
|
* typed named values and multi-word segments.
|
|
10
14
|
*/
|
|
11
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.isFlagToken = isFlagToken;
|
|
12
17
|
exports.parseNamedArgs = parseNamedArgs;
|
|
18
|
+
exports.parseNamedArgsOrExit = parseNamedArgsOrExit;
|
|
13
19
|
exports.parseMultiwordArg = parseMultiwordArg;
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const io = require("./io.cjs");
|
|
22
|
+
const { ERROR_REASON, formatDiagnosticToken } = io;
|
|
23
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
function isPlainObject(v) {
|
|
25
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
|
+
}
|
|
14
27
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
28
|
+
* ADR-3473 Decision 2: both ends of this seam are gsd-core's own source, so a
|
|
29
|
+
* malformed spec (a stale call site still using the legacy
|
|
30
|
+
* `parseNamedArgs(args, valueFlags, booleanFlags)` shape, or a missing spec
|
|
31
|
+
* entirely) is an internal invariant violation, not user input — it throws
|
|
32
|
+
* loudly instead of destructuring `undefined` off a Result.
|
|
33
|
+
*/
|
|
34
|
+
function assertValidSpec(spec) {
|
|
35
|
+
if (!isPlainObject(spec)) {
|
|
36
|
+
throw new TypeError('parseNamedArgs: spec must be an object of shape ' +
|
|
37
|
+
'{ valueFlags?: string[], booleanFlags?: string[], positionals: number | "rest" }. ' +
|
|
38
|
+
'Received a missing, array, or non-object value — this is the legacy ' +
|
|
39
|
+
'parseNamedArgs(args, valueFlags, booleanFlags) call shape, retired by ADR-3473 §8.4.');
|
|
40
|
+
}
|
|
41
|
+
const positionals = spec.positionals;
|
|
42
|
+
const positionalsValid = positionals === 'rest' ||
|
|
43
|
+
(typeof positionals === 'number' && Number.isInteger(positionals) && positionals >= 0);
|
|
44
|
+
if (!positionalsValid) {
|
|
45
|
+
throw new TypeError('parseNamedArgs: spec.positionals must be a non-negative integer or the literal "rest" ' +
|
|
46
|
+
`— received ${JSON.stringify(positionals)}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Single predicate reused for both extraction (a value beginning with a
|
|
50
|
+
// single `-` is a value, not a flag) and validation (negative space N5).
|
|
51
|
+
// Exported for init-command-router's #3865 --phase alias normalization,
|
|
52
|
+
// which needs the same flag-shape test before deciding whether args[2] is a
|
|
53
|
+
// phase positional or a flag token.
|
|
54
|
+
function isFlagToken(tok) {
|
|
55
|
+
return tok.startsWith('--');
|
|
56
|
+
}
|
|
57
|
+
// ─── parseNamedArgs ───────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Project argv tokens into typed named values, then strictly validate every
|
|
60
|
+
* token past the caller's declared positional boundary.
|
|
61
|
+
*
|
|
62
|
+
* Extraction (unchanged semantics, #312): first occurrence wins; a value
|
|
63
|
+
* flag whose next token is absent or starts with `--` yields `null` (this is
|
|
64
|
+
* NOT a validation error — see the value-flag branch below); boolean flags
|
|
65
|
+
* and optional-value flags (`optionalValueFlags`, #2932's `--wave` shape) are
|
|
66
|
+
* presence tests. Kept as a single first-index Map so the flag loops don't
|
|
67
|
+
* each re-scan argv — O(argv + flags) instead of O(flags * argv).
|
|
68
|
+
*
|
|
69
|
+
* Validation (skipped entirely when `positionals === 'rest'`): a single
|
|
70
|
+
* left-to-right cursor walk from `spec.positionals`, per the design doc's
|
|
71
|
+
* Kernighan's Law note — debuggable over clever, never a set-difference.
|
|
18
72
|
*/
|
|
19
|
-
function parseNamedArgs(args,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
73
|
+
function parseNamedArgs(args, spec) {
|
|
74
|
+
assertValidSpec(spec);
|
|
75
|
+
const valueFlags = spec.valueFlags ?? [];
|
|
76
|
+
const booleanFlags = spec.booleanFlags ?? [];
|
|
77
|
+
const optionalValueFlags = spec.optionalValueFlags ?? [];
|
|
23
78
|
const firstIndex = new Map();
|
|
24
79
|
for (let i = 0; i < args.length; i++) {
|
|
25
80
|
if (!firstIndex.has(args[i]))
|
|
26
81
|
firstIndex.set(args[i], i);
|
|
27
82
|
}
|
|
28
|
-
const
|
|
83
|
+
const data = {};
|
|
29
84
|
for (const flag of valueFlags) {
|
|
30
85
|
const idx = firstIndex.has(`--${flag}`) ? firstIndex.get(`--${flag}`) : -1;
|
|
31
|
-
|
|
32
|
-
idx !== -1 && args[idx + 1] !== undefined && !args[idx + 1]
|
|
86
|
+
data[flag] =
|
|
87
|
+
idx !== -1 && args[idx + 1] !== undefined && !isFlagToken(args[idx + 1])
|
|
33
88
|
? args[idx + 1]
|
|
34
89
|
: null;
|
|
35
90
|
}
|
|
36
91
|
for (const flag of booleanFlags) {
|
|
37
|
-
|
|
92
|
+
data[flag] = firstIndex.has(`--${flag}`);
|
|
93
|
+
}
|
|
94
|
+
// Optional-value flags (#2932: `--wave`-shaped) — presence-only, exactly
|
|
95
|
+
// like booleanFlags. The value (if any) is deliberately not surfaced here;
|
|
96
|
+
// see the NamedArgSpec.optionalValueFlags JSDoc.
|
|
97
|
+
for (const flag of optionalValueFlags) {
|
|
98
|
+
data[flag] = firstIndex.has(`--${flag}`);
|
|
99
|
+
}
|
|
100
|
+
if (spec.positionals === 'rest') {
|
|
101
|
+
return { ok: true, data };
|
|
102
|
+
}
|
|
103
|
+
const valueFlagSet = new Set(valueFlags);
|
|
104
|
+
const booleanFlagSet = new Set(booleanFlags);
|
|
105
|
+
const optionalValueFlagSet = new Set(optionalValueFlags);
|
|
106
|
+
const flagList = [
|
|
107
|
+
...valueFlags.map((f) => `--${f} <value>`),
|
|
108
|
+
...booleanFlags.map((f) => `--${f}`),
|
|
109
|
+
...optionalValueFlags.map((f) => `--${f} [value]`),
|
|
110
|
+
];
|
|
111
|
+
let i = spec.positionals;
|
|
112
|
+
while (i < args.length) {
|
|
113
|
+
const tok = args[i];
|
|
114
|
+
if (isFlagToken(tok)) {
|
|
115
|
+
const name = tok.slice(2);
|
|
116
|
+
if (valueFlagSet.has(name)) {
|
|
117
|
+
// A value flag whose next token is absent or flag-shaped resolves to
|
|
118
|
+
// `null` in `data` (see the extraction loop above) — this is NOT an
|
|
119
|
+
// error (#3180/behavior-lock: emptyPrdValueIsFalsyAndTreatedAsAbsent,
|
|
120
|
+
// section-manifest-init-facts.test.cjs "flag-shaped value"). Advance
|
|
121
|
+
// by 1 so the following flag token is validated on its own merits on
|
|
122
|
+
// the next iteration.
|
|
123
|
+
const next = args[i + 1];
|
|
124
|
+
i += next !== undefined && !isFlagToken(next) ? 2 : 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (booleanFlagSet.has(name)) {
|
|
128
|
+
i += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (optionalValueFlagSet.has(name)) {
|
|
132
|
+
const next = args[i + 1];
|
|
133
|
+
i += next !== undefined && !isFlagToken(next) ? 2 : 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const reason = flagList.length > 0
|
|
137
|
+
? `unknown flag ${formatDiagnosticToken(tok)}; accepted: ${flagList.join(', ')}`
|
|
138
|
+
: `unknown flag ${formatDiagnosticToken(tok)}; this command accepts no flags`;
|
|
139
|
+
return { ok: false, kind: 'InvalidArgs', arg: tok, reason, exitReason: ERROR_REASON.USAGE };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
kind: 'InvalidArgs',
|
|
144
|
+
arg: tok,
|
|
145
|
+
reason: `unexpected positional argument ${formatDiagnosticToken(tok)}`,
|
|
146
|
+
exitReason: ERROR_REASON.USAGE,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, data };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Thin projection over `parseNamedArgs`: on `ok:false` it calls
|
|
153
|
+
* `fail(result.reason, result.exitReason)` and then throws.
|
|
154
|
+
*
|
|
155
|
+
* The trailing throw exists for two reasons: (1) TypeScript's control-flow
|
|
156
|
+
* analysis needs a `never`-returning path so callers can destructure the
|
|
157
|
+
* return value without a null check; (2) it is a fail-closed backstop — the
|
|
158
|
+
* `fail` callbacks in this repo are `never`-returning at runtime (`io.error`
|
|
159
|
+
* calls `process.exit(1)`) but are typed `void`, so if a caller ever passes a
|
|
160
|
+
* `fail` that actually returns, this still halts instead of falling through
|
|
161
|
+
* with a half-built `ParsedNamedArgs`.
|
|
162
|
+
*/
|
|
163
|
+
function parseNamedArgsOrExit(args, spec, fail) {
|
|
164
|
+
const result = parseNamedArgs(args, spec);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
fail(result.reason, result.exitReason);
|
|
167
|
+
throw new Error(`parseNamedArgsOrExit: fail() returned instead of exiting (arg: ${result.arg})`);
|
|
38
168
|
}
|
|
39
|
-
return result;
|
|
169
|
+
return result.data;
|
|
40
170
|
}
|
|
41
171
|
/**
|
|
42
172
|
* Collect all tokens after --flag until the next --flag or end of args.
|