@opengsd/gsd-core 1.17.0 → 1.19.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 +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +3 -3
- package/agents/gsd-code-fixer.md +5 -4
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +460 -105
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +124 -18
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +41 -8
- package/gsd-core/bin/lib/capability-registry.cjs +162 -74
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +20 -11
- package/gsd-core/bin/lib/capability-validator.cjs +23 -4
- package/gsd-core/bin/lib/commands.cjs +5 -1
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +34 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +93 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +21 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +19 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +114 -18
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +76 -5
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -7
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +43 -1
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-id.cjs +21 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
- package/gsd-core/bin/lib/phase.cjs +235 -37
- package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
- package/gsd-core/bin/lib/roadmap.cjs +89 -30
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +428 -16
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +123 -16
- package/gsd-core/bin/lib/smart-entry.cjs +37 -16
- package/gsd-core/bin/lib/state-contract.cjs +29 -9
- package/gsd-core/bin/lib/state-document.cjs +47 -5
- package/gsd-core/bin/lib/state-transition.cjs +150 -34
- package/gsd-core/bin/lib/state.cjs +140 -88
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +7 -28
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +38 -1
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +14 -3
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +5 -4
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +66 -8
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +64 -157
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +34 -6
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +44 -5
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +11 -11
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +19 -8
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +21 -3
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +7 -3
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +46 -11
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +6 -2
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +7 -3
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +54 -86
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +23 -8
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +5 -1
- package/gsd-core/workflows/review.md +22 -15
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +4 -3
- package/gsd-core/workflows/ship.md +16 -14
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +15 -6
- package/hooks/dist/gsd-agent-isolation-guard.js +14 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -3
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +244 -53
- package/hooks/dist/gsd-statusline.js +35 -2
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -1
- package/hooks/dist/gsd-worktree-path-guard.js +28 -1
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +14 -2
- package/hooks/gsd-cursor-subagent-start.js +8 -3
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +244 -53
- package/hooks/gsd-statusline.js +35 -2
- package/hooks/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-windsurf-pre-write.js +28 -1
- package/hooks/gsd-workflow-guard.js +1 -1
- package/hooks/gsd-worktree-path-guard.js +28 -1
- package/hooks/hooks.json +1 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -6
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +769 -0
- package/pi/package-agents/gsd-code-reviewer.md +402 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +894 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +51 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/build-hooks.js +4 -0
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +19 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +41 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/release-tarball-smoke.cjs +1 -0
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -56,7 +56,9 @@
|
|
|
56
56
|
// EOF)"` — is never operand-checked, so prose mentioning `.env` is safe.
|
|
57
57
|
// pass 2 groups tokens by segment and evaluates each on its own:
|
|
58
58
|
// input redirect targets (`<`, `N<`) are always checked; the command word is
|
|
59
|
-
// located past
|
|
59
|
+
// located past wrapper prefixes AND their own options (`sudo -u U`,
|
|
60
|
+
// `env -i VAR=x`, `timeout -s KILL 5`, `nice -n 5`, `stdbuf -oL`, `exec`;
|
|
61
|
+
// see WRAPPERS, #5627); a closed set of
|
|
60
62
|
// NON-READING commands (test/[/ls/stat/rm/touch/echo/…) exempts that
|
|
61
63
|
// segment's operands — `[ -f .env ]` and `ls .env*` are existence checks
|
|
62
64
|
// GSD's own agents run — while `cp`/`mv`/`ln`/`git` are deliberately NOT
|
|
@@ -67,6 +69,8 @@
|
|
|
67
69
|
// `git check-ignore` / `git ls-files` / `git rm --cached`, and the
|
|
68
70
|
// destination of a one-source `cp`/`mv` (#4856). The #4856 carve-outs
|
|
69
71
|
// fail closed: an option they do not list withdraws them from the segment.
|
|
72
|
+
// A git global option the walker does not know also keeps the #5045 `-c`
|
|
73
|
+
// scan running over every later word (see walkGitGlobals).
|
|
70
74
|
// A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
|
|
71
75
|
// whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
|
|
72
76
|
// here-string, or a pipe from a knowable `echo`/`printf` source
|
|
@@ -88,7 +92,9 @@
|
|
|
88
92
|
// (`cat .e*`), interpreter one-liners (`python -c "open('.env')"`), a piped
|
|
89
93
|
// script from a non-echo source (`cat gen.sh | bash`, `curl … | sh`), reads
|
|
90
94
|
// inside scripts the agent executes, and `glob: '*'` reaching a
|
|
91
|
-
// NON-gitignored `.env
|
|
95
|
+
// NON-gitignored `.env`, and a wrapper (or wrapper option taking a
|
|
96
|
+
// separate value) the WRAPPERS table does not list, or `env -S 'script'` (the split string is skipped as
|
|
97
|
+
// the option's value). The promise is "no looser than the retired rules
|
|
92
98
|
// on plain commands, without arming the compound-`cd` prompt". Writes to
|
|
93
99
|
// secret files are out of scope (Write/Edit were never gated). Commands
|
|
94
100
|
// over 1 MiB are denied outright (`command-too-large`) rather than
|
|
@@ -146,8 +152,29 @@ const MAX_GLOB_ALTERNATIVES = 64;
|
|
|
146
152
|
const NON_SECRET_ENV_SUFFIXES = new Set(['example', 'sample', 'template', 'dist']);
|
|
147
153
|
|
|
148
154
|
// Command-prefix wrappers to look through when locating the command word at
|
|
149
|
-
// the head of a segment (
|
|
150
|
-
|
|
155
|
+
// the head of a segment (#5627). Each wrapper runs the words that follow its
|
|
156
|
+
// own options, so the table records what resolveCommand must step over:
|
|
157
|
+
// short letters of short options that take a value (`-n 5`, `-n5`)
|
|
158
|
+
// long long options that take a SEPARATE value word (`--adjustment 5`);
|
|
159
|
+
// `--opt=value` is one word and needs no entry
|
|
160
|
+
// assigns leading `VAR=value` words belong to the wrapper (env)
|
|
161
|
+
// operands positional words after the options (timeout's DURATION)
|
|
162
|
+
// Taken from sudo(8), doas(1), env(1), nice(1), time(1), timeout(1),
|
|
163
|
+
// stdbuf(1), setsid(1) and bash's `command`/`exec`. A wrong entry fails open
|
|
164
|
+
// (the option's value becomes the command word), so keep it to the man pages.
|
|
165
|
+
const WRAPPERS = new Map([
|
|
166
|
+
['sudo', { short: 'CDghpRrTtUu', long: ['--close-from', '--chdir', '--group', '--host', '--prompt', '--chroot', '--role', '--command-timeout', '--type', '--other-user', '--user'] }],
|
|
167
|
+
['doas', { short: 'Cu' }],
|
|
168
|
+
['env', { short: 'uCSaP', long: ['--unset', '--chdir', '--split-string', '--argv0'], assigns: true }],
|
|
169
|
+
['nice', { short: 'n', long: ['--adjustment'] }],
|
|
170
|
+
['nohup', {}],
|
|
171
|
+
['command', {}],
|
|
172
|
+
['time', { short: 'fo', long: ['--format', '--output'] }],
|
|
173
|
+
['timeout', { short: 'sk', long: ['--signal', '--kill-after'], operands: 1 }],
|
|
174
|
+
['stdbuf', { short: 'ioe', long: ['--input', '--output', '--error'] }],
|
|
175
|
+
['setsid', {}],
|
|
176
|
+
['exec', { short: 'a' }],
|
|
177
|
+
]);
|
|
151
178
|
|
|
152
179
|
// Commands whose ordinary operands are file NAMES, never file CONTENTS. A
|
|
153
180
|
// closed set on purpose: anything not listed is assumed to read.
|
|
@@ -192,14 +219,29 @@ const GIT_PATHSPEC_SUBCOMMANDS = new Map([
|
|
|
192
219
|
}],
|
|
193
220
|
]);
|
|
194
221
|
|
|
195
|
-
// git's global options (
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
222
|
+
// git's global options (git.c handle_options, git 2.49) — the ONE vocabulary
|
|
223
|
+
// both the #4856 pathspec exemption and the #5045 -c scan walk, via
|
|
224
|
+
// walkGitGlobals. A value option consumes the next word; the long options in
|
|
225
|
+
// GIT_GLOBAL_EQUALS_FORM_OPTIONS also take it attached as `--opt=value`.
|
|
226
|
+
// `-C`, `-c` and the hidden `--shallow-file` accept only the separate word
|
|
227
|
+
// (git rejects `--shallow-file=x` and `-c<name>=<value>`), and
|
|
228
|
+
// `--exec-path` runs a subcommand only as `--exec-path=<path>` (bare, it
|
|
229
|
+
// prints the path and exits). Any other option fails closed for both
|
|
230
|
+
// consumers: its arity is unknown, so its value could be read as the
|
|
231
|
+
// subcommand (`git -C ls-files show HEAD:.env` runs `show`). The pathspec
|
|
232
|
+
// exemption is withdrawn, and the -c scan keeps collecting every later
|
|
233
|
+
// `-c <value>` pair instead of stopping.
|
|
234
|
+
const GIT_GLOBAL_VALUE_OPTIONS = new Set([
|
|
235
|
+
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source',
|
|
236
|
+
'--shallow-file',
|
|
237
|
+
]);
|
|
238
|
+
const GIT_GLOBAL_EQUALS_FORM_OPTIONS = new Set([
|
|
239
|
+
'--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--exec-path',
|
|
240
|
+
]);
|
|
200
241
|
const GIT_GLOBAL_FLAGS = new Set([
|
|
201
242
|
'-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--no-lazy-fetch',
|
|
202
243
|
'--no-optional-locks', '--no-advice', '--bare',
|
|
244
|
+
'--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs',
|
|
203
245
|
]);
|
|
204
246
|
|
|
205
247
|
// #4856: `cp`/`mv` write their destination and never print it, so a secret
|
|
@@ -388,6 +430,20 @@ function classifyGrepGlob(glob) {
|
|
|
388
430
|
// Bash command scan — pass 1: tokenizer
|
|
389
431
|
// ---------------------------------------------------------------------------
|
|
390
432
|
|
|
433
|
+
// Which reading of a heredoc the scan is making (#5513). The guard scans a command that
|
|
434
|
+
// opens a heredoc under every reading below and denies if any of them finds a secret read
|
|
435
|
+
// (see findSecretReadUnderOtherReadings), so a change of reading can add a denial but
|
|
436
|
+
// never remove one.
|
|
437
|
+
// - heredocBodiesSkippedInSpans: bash 5 skips the bodies of heredocs opened inside a
|
|
438
|
+
// `$( )` span when it looks for the closing `)`; bash 3.2.57 (still `/bin/bash` on
|
|
439
|
+
// macOS) does not, so a `)` on a body line closes the substitution and the rest of
|
|
440
|
+
// that line runs as the OUTER command.
|
|
441
|
+
// - heredocTagsAsWords: an unquoted tag is a bash word (a CR and a substitution belong
|
|
442
|
+
// to it). false is the earlier reading, where the tag stopped at any whitespace and at
|
|
443
|
+
// `(`: kept as a reading so nothing the guard denied before is allowed now.
|
|
444
|
+
let heredocBodiesSkippedInSpans = true;
|
|
445
|
+
let heredocTagsAsWords = true;
|
|
446
|
+
|
|
391
447
|
// Index of the `)` closing a `$(` / `<(` / `>(` opened just before `i`, or
|
|
392
448
|
// str.length when unterminated. Quote- and heredoc-aware so a `)` inside a
|
|
393
449
|
// quoted string or a heredoc body never closes the span early.
|
|
@@ -422,7 +478,7 @@ function findParenClose(str, i) {
|
|
|
422
478
|
i = j === -1 ? str.length : j + 1;
|
|
423
479
|
continue;
|
|
424
480
|
}
|
|
425
|
-
if (ch === '<' && str[i + 1] === '<' && str[i + 2] !== '<') {
|
|
481
|
+
if (heredocBodiesSkippedInSpans && ch === '<' && str[i + 1] === '<' && str[i + 2] !== '<') {
|
|
426
482
|
const tag = readHeredocTag(str, i + 2);
|
|
427
483
|
heredocTags.push(tag);
|
|
428
484
|
i = tag.end;
|
|
@@ -458,7 +514,34 @@ function readHeredocTag(str, i) {
|
|
|
458
514
|
i = j === -1 ? str.length : j + 1;
|
|
459
515
|
} else {
|
|
460
516
|
if (str[i] === '\\') { quoted = true; i++; }
|
|
461
|
-
|
|
517
|
+
// #5513: an unquoted tag is a bash WORD. It ends only at a metacharacter (space, tab,
|
|
518
|
+
// newline, `;&|<>()`): a carriage return is not one, so `EOF` + CR is the tag and the
|
|
519
|
+
// CRLF terminator line matches it; and a substitution inside the word belongs to it,
|
|
520
|
+
// taken literally (`E$(x)F` waits for the line `E$(x)F`).
|
|
521
|
+
while (i < str.length && !(heredocTagsAsWords ? /[ \t\n;&|<>()]/ : /[\s;&|<>()]/).test(str[i])) {
|
|
522
|
+
if (!heredocTagsAsWords) { tag += str[i++]; continue; }
|
|
523
|
+
if (str[i] === '$' && str[i + 1] === '(') {
|
|
524
|
+
const e = findParenClose(str, i + 2);
|
|
525
|
+
tag += str.slice(i, e + 1);
|
|
526
|
+
i = e + 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (str[i] === '$' && str[i + 1] === '{') {
|
|
530
|
+
const j = str.indexOf('}', i);
|
|
531
|
+
const e = j === -1 ? str.length - 1 : j;
|
|
532
|
+
tag += str.slice(i, e + 1);
|
|
533
|
+
i = e + 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (str[i] === '`') {
|
|
537
|
+
const j = str.indexOf('`', i + 1);
|
|
538
|
+
const e = j === -1 ? str.length - 1 : j;
|
|
539
|
+
tag += str.slice(i, e + 1);
|
|
540
|
+
i = e + 1;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
tag += str[i++];
|
|
544
|
+
}
|
|
462
545
|
}
|
|
463
546
|
return { tag, quoted, stripTabs, end: i };
|
|
464
547
|
}
|
|
@@ -604,6 +687,7 @@ function tokenize(str) {
|
|
|
604
687
|
hasWord = true;
|
|
605
688
|
const e = findParenClose(str, i + 2);
|
|
606
689
|
nested.push(str.slice(i + 2, e));
|
|
690
|
+
if (expectTag && heredocTagsAsWords) buf += str.slice(i, e + 1); // #5513: the tag is taken literally
|
|
607
691
|
i = e + 1;
|
|
608
692
|
continue;
|
|
609
693
|
}
|
|
@@ -622,6 +706,7 @@ function tokenize(str) {
|
|
|
622
706
|
const j = str.indexOf('`', i + 1);
|
|
623
707
|
const e = j === -1 ? str.length : j;
|
|
624
708
|
nested.push(str.slice(i + 1, e));
|
|
709
|
+
if (expectTag && heredocTagsAsWords) buf += str.slice(i, e + 1); // #5513: the tag is taken literally
|
|
625
710
|
i = e + 1;
|
|
626
711
|
continue;
|
|
627
712
|
}
|
|
@@ -656,6 +741,14 @@ function tokenize(str) {
|
|
|
656
741
|
continue;
|
|
657
742
|
}
|
|
658
743
|
|
|
744
|
+
// #5513: a CR is not a bash metacharacter, so inside a heredoc tag it is part of the word
|
|
745
|
+
// (a CRLF command's terminator line is `EOF` + CR).
|
|
746
|
+
if (ch === '\r' && expectTag && hasWord && heredocTagsAsWords) {
|
|
747
|
+
buf += ch;
|
|
748
|
+
i++;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
|
|
659
752
|
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
660
753
|
flush();
|
|
661
754
|
i++;
|
|
@@ -752,17 +845,33 @@ function tokenize(str) {
|
|
|
752
845
|
// Bash command scan — pass 2: per-segment evaluation
|
|
753
846
|
// ---------------------------------------------------------------------------
|
|
754
847
|
|
|
755
|
-
//
|
|
756
|
-
|
|
848
|
+
// Option characters of a bundled short cluster (`-2nif`).
|
|
849
|
+
const CLUSTER_FLAG_CHAR_RE = /[A-Za-z0-9]/;
|
|
850
|
+
|
|
851
|
+
// True when an operand word names a secret file. `@file` (curl -d) and
|
|
852
|
+
// `--flag=value` are stripped to the value. In a single-dash word
|
|
853
|
+
// (`/^-[^-]./`, so `-2if.env` counts too) the value of a bundled cluster
|
|
854
|
+
// starts right after its run of option characters (#5046): `-if.env` holds
|
|
855
|
+
// `.env`, `-nf=.env` holds `.env` after the `=`. That value is checked as
|
|
856
|
+
// well as the historical two-character strip, so the verdict is never looser
|
|
857
|
+
// than base and costs a fixed number of `namesSecret` calls (at most three,
|
|
858
|
+
// whatever the word's length). The option run also swallows a value's leading
|
|
859
|
+
// letters and digits, so `-fapp.env` is denied although `cat app.env` is not:
|
|
860
|
+
// a fail-safe false positive, pinned in tests.
|
|
861
|
+
function operandNamesSecret(text) {
|
|
757
862
|
let v = text;
|
|
758
863
|
if (v.startsWith('@')) v = v.slice(1);
|
|
759
864
|
if (v.startsWith('--')) {
|
|
760
865
|
const eq = v.indexOf('=');
|
|
761
866
|
if (eq !== -1) v = v.slice(eq + 1);
|
|
762
|
-
|
|
763
|
-
v = v.slice(2);
|
|
867
|
+
return namesSecret(v);
|
|
764
868
|
}
|
|
765
|
-
return v;
|
|
869
|
+
if (!/^-[^-]./.test(v)) return namesSecret(v);
|
|
870
|
+
if (namesSecret(v.slice(2))) return true;
|
|
871
|
+
let end = 1;
|
|
872
|
+
while (end < v.length && CLUSTER_FLAG_CHAR_RE.test(v[end])) end++;
|
|
873
|
+
if (end > 2 && namesSecret(v.slice(end))) return true;
|
|
874
|
+
return v[end] === '=' && namesSecret(v.slice(end + 1));
|
|
766
875
|
}
|
|
767
876
|
|
|
768
877
|
const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
@@ -772,23 +881,83 @@ const DASH_C_RE = /^-[A-Za-z]*c$/;
|
|
|
772
881
|
|
|
773
882
|
const GROUPING_SEPS = new Set(['(', ')', '{', '}']);
|
|
774
883
|
|
|
884
|
+
// How many words the option `word` occupies for wrapper `spec`: 2 when it
|
|
885
|
+
// takes a separate value word, else 1.
|
|
886
|
+
function wrapperOptionWidth(word, spec) {
|
|
887
|
+
if (word.startsWith('--')) {
|
|
888
|
+
if (word.includes('=')) return 1;
|
|
889
|
+
// getopt_long accepts any unambiguous prefix (`--adj 5`).
|
|
890
|
+
return word.length > 2 && (spec.long || []).some((l) => l.startsWith(word)) ? 2 : 1;
|
|
891
|
+
}
|
|
892
|
+
const valued = spec.short || '';
|
|
893
|
+
for (let i = 1; i < word.length; i++) {
|
|
894
|
+
if (valued.includes(word[i])) return i === word.length - 1 ? 2 : 1; // `-n5` carries its value
|
|
895
|
+
}
|
|
896
|
+
return 1;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Index of the first word after a wrapper's own options, assignments and
|
|
900
|
+
// positional operands (`-n 5`, `-i A=1`, `-s KILL 5`, `--`).
|
|
901
|
+
function skipWrapperArgs(words, start, spec) {
|
|
902
|
+
let idx = start;
|
|
903
|
+
let optionsEnded = false;
|
|
904
|
+
while (idx < words.length) {
|
|
905
|
+
const text = words[idx].text;
|
|
906
|
+
if (spec.assigns && ASSIGNMENT_RE.test(text)) {
|
|
907
|
+
idx++;
|
|
908
|
+
} else if (!optionsEnded && text === '--') {
|
|
909
|
+
optionsEnded = true;
|
|
910
|
+
idx++;
|
|
911
|
+
} else if (!optionsEnded && text.length > 1 && text.startsWith('-')) {
|
|
912
|
+
idx += wrapperOptionWidth(text, spec);
|
|
913
|
+
} else {
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!spec.operands) return idx;
|
|
918
|
+
idx = Math.min(idx + spec.operands, words.length);
|
|
919
|
+
// `timeout 5 -- cmd`: some getopt builds still accept `--` after DURATION.
|
|
920
|
+
return idx < words.length && words[idx].text === '--' ? idx + 1 : idx;
|
|
921
|
+
}
|
|
922
|
+
|
|
775
923
|
// Command base + operands after leading `VAR=val` assignments and prefix
|
|
776
|
-
// wrappers (`sudo`, `env VAR=x`,
|
|
924
|
+
// wrappers with their own options (`sudo -u root`, `env -i VAR=x`,
|
|
925
|
+
// `timeout -s KILL 5`, `nice -n 5`, …), or null when nothing but prefixes remain.
|
|
777
926
|
function resolveCommand(words) {
|
|
778
927
|
let idx = 0;
|
|
779
928
|
while (idx < words.length && ASSIGNMENT_RE.test(words[idx].text)) idx++;
|
|
780
929
|
while (idx < words.length) {
|
|
781
|
-
const
|
|
782
|
-
if (!
|
|
783
|
-
idx
|
|
784
|
-
if (base === 'env') {
|
|
785
|
-
while (idx < words.length && ASSIGNMENT_RE.test(words[idx].text)) idx++;
|
|
786
|
-
}
|
|
930
|
+
const spec = WRAPPERS.get(lastSegment(words[idx].text).toLowerCase());
|
|
931
|
+
if (!spec) break;
|
|
932
|
+
idx = skipWrapperArgs(words, idx + 1, spec);
|
|
787
933
|
}
|
|
788
934
|
if (idx >= words.length) return null;
|
|
789
935
|
return { base: lastSegment(words[idx].text).toLowerCase(), operands: words.slice(idx + 1) };
|
|
790
936
|
}
|
|
791
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Rescan only the command-valued -c keys selected by #5045. This deliberately
|
|
940
|
+
* does not resolve config files, --config-env/GIT_CONFIG_* values, or other
|
|
941
|
+
* executing keys (diff drivers, filters, credential helpers, ssh, gpg).
|
|
942
|
+
* Those are documented gaps, not a promise of complete Git config analysis.
|
|
943
|
+
* Global option values and subcommand arguments are data, not more -c flags.
|
|
944
|
+
*/
|
|
945
|
+
function scanGitConfig(operands, depth) {
|
|
946
|
+
for (const config of walkGitGlobals(operands).configs) {
|
|
947
|
+
const eq = config.indexOf('=');
|
|
948
|
+
if (eq === -1) continue;
|
|
949
|
+
const key = config.slice(0, eq).toLowerCase();
|
|
950
|
+
const value = config.slice(eq + 1);
|
|
951
|
+
let script;
|
|
952
|
+
if (key.startsWith('alias.') && value.startsWith('!')) script = value.slice(1);
|
|
953
|
+
else if (key === 'diff.external') script = value;
|
|
954
|
+
else continue;
|
|
955
|
+
const hit = findSecretRead(script, depth + 1);
|
|
956
|
+
if (hit) return hit;
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
792
961
|
// Operand indices the segment's command consumes as a NAME, never as
|
|
793
962
|
// CONTENTS (#4639, #4856). Only these positions skip the operand check;
|
|
794
963
|
// every other operand is still checked, so a carve-out cannot launder a read
|
|
@@ -820,26 +989,38 @@ function isListedFlag(text, flags) {
|
|
|
820
989
|
return /^-[A-Za-z]+$/.test(text) && [...text.slice(1)].every((ch) => flags.has(`-${ch}`));
|
|
821
990
|
}
|
|
822
991
|
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
992
|
+
// Walks git's global options (see GIT_GLOBAL_VALUE_OPTIONS). `sub` is the
|
|
993
|
+
// subcommand's index, or -1 when an unlisted option (or the end of the words)
|
|
994
|
+
// comes first; `configs` holds every `-c` value seen on the way, in order.
|
|
995
|
+
// Past an unlisted option the subcommand cannot be located, so `configs`
|
|
996
|
+
// conservatively takes the word after every later `-c` as well: a read
|
|
997
|
+
// still needs a command-valued key, so a harmless value stays allowed.
|
|
998
|
+
function walkGitGlobals(operands) {
|
|
999
|
+
const configs = [];
|
|
826
1000
|
for (let k = 0; k < operands.length; k++) {
|
|
827
1001
|
const t = operands[k].text;
|
|
828
|
-
if (!t.startsWith('-')) return k;
|
|
829
|
-
if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
|
|
1002
|
+
if (!t.startsWith('-')) return { sub: k, configs };
|
|
1003
|
+
if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
|
|
1004
|
+
if (t === '-c' && k + 1 < operands.length) configs.push(operands[k + 1].text);
|
|
1005
|
+
k++;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
830
1008
|
if (GIT_GLOBAL_FLAGS.has(t)) continue;
|
|
831
1009
|
const eq = t.indexOf('=');
|
|
832
|
-
|
|
833
|
-
|
|
1010
|
+
if (t.startsWith('--') && eq !== -1 && GIT_GLOBAL_EQUALS_FORM_OPTIONS.has(t.slice(0, eq))) continue;
|
|
1011
|
+
for (let j = k + 1; j + 1 < operands.length; j++) {
|
|
1012
|
+
if (operands[j].text === '-c') configs.push(operands[++j].text);
|
|
1013
|
+
}
|
|
1014
|
+
return { sub: -1, configs };
|
|
834
1015
|
}
|
|
835
|
-
return -1;
|
|
1016
|
+
return { sub: -1, configs };
|
|
836
1017
|
}
|
|
837
1018
|
|
|
838
1019
|
// #4856: the pathspec operands of a GIT_PATHSPEC_SUBCOMMANDS invocation. git
|
|
839
1020
|
// permutes options, so one may follow a pathspec; after `--` every word is a
|
|
840
1021
|
// pathspec. The subcommand is matched case-sensitively, as git looks it up.
|
|
841
1022
|
function gitPathspecIndices(operands) {
|
|
842
|
-
const sub =
|
|
1023
|
+
const { sub } = walkGitGlobals(operands);
|
|
843
1024
|
const spec = sub === -1 ? undefined : GIT_PATHSPEC_SUBCOMMANDS.get(operands[sub].text);
|
|
844
1025
|
if (!spec) return new Set();
|
|
845
1026
|
const pathspecs = new Set();
|
|
@@ -910,6 +1091,26 @@ function precedingOp(s, bySeg, sepAfter) {
|
|
|
910
1091
|
return { op, prevSeg: p };
|
|
911
1092
|
}
|
|
912
1093
|
|
|
1094
|
+
// #5513: the same scan under the other readings of a heredoc: bash 3.2.57's `$( )` span
|
|
1095
|
+
// (bodies not skipped) and the earlier tag reading (tag ends at any whitespace). Only a
|
|
1096
|
+
// command that opens a heredoc can read differently, so the extra passes are skipped for
|
|
1097
|
+
// every other command.
|
|
1098
|
+
function findSecretReadUnderOtherReadings(command) {
|
|
1099
|
+
if (!command.includes('<<')) return null;
|
|
1100
|
+
for (const [bodiesSkipped, tagsAsWords] of [[false, true], [true, false]]) {
|
|
1101
|
+
heredocBodiesSkippedInSpans = bodiesSkipped;
|
|
1102
|
+
heredocTagsAsWords = tagsAsWords;
|
|
1103
|
+
try {
|
|
1104
|
+
const hit = findSecretRead(command, 0);
|
|
1105
|
+
if (hit) return hit;
|
|
1106
|
+
} finally {
|
|
1107
|
+
heredocBodiesSkippedInSpans = true;
|
|
1108
|
+
heredocTagsAsWords = true;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
913
1114
|
// Returns the offending token text, or null.
|
|
914
1115
|
function findSecretRead(command, depth) {
|
|
915
1116
|
const { tokens, nested } = tokenize(command);
|
|
@@ -988,7 +1189,7 @@ function findSecretRead(command, depth) {
|
|
|
988
1189
|
const hit = findSecretRead(src, depth + 1);
|
|
989
1190
|
if (hit) return hit;
|
|
990
1191
|
}
|
|
991
|
-
} else if (
|
|
1192
|
+
} else if (operandNamesSecret(w.text)) return w.text;
|
|
992
1193
|
}
|
|
993
1194
|
continue;
|
|
994
1195
|
}
|
|
@@ -1007,12 +1208,18 @@ function findSecretRead(command, depth) {
|
|
|
1007
1208
|
// `bash .env` (file mode) is caught by the operand check below.
|
|
1008
1209
|
}
|
|
1009
1210
|
|
|
1211
|
+
// Git -c can carry a shell script instead of a file-name operand (#5045).
|
|
1212
|
+
if ((base === 'git' || base === 'git.exe') && depth < MAX_NESTING_DEPTH) {
|
|
1213
|
+
const hit = scanGitConfig(operands, depth);
|
|
1214
|
+
if (hit) return hit;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1010
1217
|
if (NON_READING_COMMANDS.has(base)) continue;
|
|
1011
1218
|
|
|
1012
1219
|
const nameOnly = nameOnlyOperandIndices(base, operands);
|
|
1013
1220
|
for (let k = 0; k < operands.length; k++) {
|
|
1014
1221
|
if (nameOnly.has(k)) continue;
|
|
1015
|
-
if (
|
|
1222
|
+
if (operandNamesSecret(operands[k].text)) return operands[k].text;
|
|
1016
1223
|
}
|
|
1017
1224
|
}
|
|
1018
1225
|
return null;
|
|
@@ -1030,21 +1237,6 @@ function isDebuggerAgent(value) {
|
|
|
1030
1237
|
return value.slice(value.lastIndexOf(':') + 1) === DEBUGGER_AGENT_NAME;
|
|
1031
1238
|
}
|
|
1032
1239
|
|
|
1033
|
-
// `timeout [flags] DURATION cmd …` is not in CMD_PREFIXES because its first
|
|
1034
|
-
// operand is a duration, not the command. Returns the command that follows it.
|
|
1035
|
-
function unwrapTimeout(cmd) {
|
|
1036
|
-
let { base, operands } = cmd;
|
|
1037
|
-
while (base === 'timeout') {
|
|
1038
|
-
let k = 0;
|
|
1039
|
-
while (k < operands.length && operands[k].text.startsWith('-')) k++;
|
|
1040
|
-
k++; // the DURATION operand
|
|
1041
|
-
const rest = resolveCommand(operands.slice(k));
|
|
1042
|
-
if (!rest) return null;
|
|
1043
|
-
({ base, operands } = rest);
|
|
1044
|
-
}
|
|
1045
|
-
return { base, operands };
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
1240
|
// Returns the `--agent` value text of a debugger launch, or null.
|
|
1049
1241
|
function findDebuggerCliSpawn(command, depth) {
|
|
1050
1242
|
const { tokens, nested } = tokenize(command);
|
|
@@ -1076,8 +1268,7 @@ function findDebuggerCliSpawn(command, depth) {
|
|
|
1076
1268
|
}
|
|
1077
1269
|
if (!words.length) continue;
|
|
1078
1270
|
|
|
1079
|
-
const
|
|
1080
|
-
const cmd = first && unwrapTimeout(first);
|
|
1271
|
+
const cmd = resolveCommand(words);
|
|
1081
1272
|
if (!cmd) continue;
|
|
1082
1273
|
const { base, operands } = cmd;
|
|
1083
1274
|
|
|
@@ -1190,7 +1381,7 @@ function scanXargsPipe(operands, s, bySeg, sepAfter, depth) {
|
|
|
1190
1381
|
const prevCmd = resolveCommand((bySeg.get(prevSeg) || []).filter((t) => t.kind === 'word'));
|
|
1191
1382
|
if (!prevCmd) return null;
|
|
1192
1383
|
for (const w of prevCmd.operands) {
|
|
1193
|
-
if (
|
|
1384
|
+
if (operandNamesSecret(w.text)) return w.text;
|
|
1194
1385
|
}
|
|
1195
1386
|
return null;
|
|
1196
1387
|
}
|
|
@@ -1365,7 +1556,7 @@ process.stdin.on('end', () => {
|
|
|
1365
1556
|
const command = typeof data.tool_input.command === 'string' ? data.tool_input.command : '';
|
|
1366
1557
|
if (command === '') allow(undefined);
|
|
1367
1558
|
if (command.length > MAX_COMMAND_LENGTH) emitBlock('command-too-large', tool, '');
|
|
1368
|
-
const hit = findSecretRead(command, 0);
|
|
1559
|
+
const hit = findSecretRead(command, 0) || findSecretReadUnderOtherReadings(command);
|
|
1369
1560
|
if (hit !== null) emitBlock('secret-read', tool, hit);
|
|
1370
1561
|
const spawn = findDebuggerCliSpawn(command, 0);
|
|
1371
1562
|
if (spawn !== null) emitBlock('debugger-cli-spawn', tool, spawn);
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -889,8 +889,40 @@ function compactModelName(name) {
|
|
|
889
889
|
// are a fixed array, and the workspace dir is passed via -C. Fails silently
|
|
890
890
|
// (segment absent) outside a repo, without git, or on timeout.
|
|
891
891
|
|
|
892
|
+
// #5048: bound for the `git status` spawn below. With GIT_OPTIONAL_LOCKS=0 the
|
|
893
|
+
// spawn no longer writes the refreshed index back, so what is left is reading
|
|
894
|
+
// the index and stat-ing the tracked paths. The bound keeps a hung network
|
|
895
|
+
// filesystem from stalling a render for long while leaving room for a cold stat
|
|
896
|
+
// cache on a large repo. The figure was chosen, not measured: time
|
|
897
|
+
// `git status --porcelain=v2 --branch` on a large tree with a cold cache before
|
|
898
|
+
// changing it. Exported so the test can pin the bound instead of restating the
|
|
899
|
+
// literal (which is how the constant and its assertion drifted before).
|
|
892
900
|
const GIT_STATUS_TIMEOUT_MS = 1500;
|
|
893
901
|
|
|
902
|
+
/**
|
|
903
|
+
* #5048: env for the read-only git spawn in this file.
|
|
904
|
+
*
|
|
905
|
+
* By default `git status` refreshes the index and takes an *optional*
|
|
906
|
+
* `.git/index.lock` to write the refreshed copy back. That is a write the
|
|
907
|
+
* caller never asked for, and it turns a pure read into a contender for the
|
|
908
|
+
* lock a real `git add` / `git commit` needs — a read-only statusline render
|
|
909
|
+
* can then make a concurrent commit fail with
|
|
910
|
+
* `Unable to create '.git/index.lock': File exists`. The statusline spawns
|
|
911
|
+
* git on every render, so the lock is re-created continuously.
|
|
912
|
+
*
|
|
913
|
+
* The env block itself is owned by src/read-only-git-env.cts and shared with
|
|
914
|
+
* every other read-only seam (`execGit` in src/shell-command-projection.cts,
|
|
915
|
+
* `readGitSignals` in src/smart-entry.cts, `gitExec` in
|
|
916
|
+
* src/pristine-baseline.cts, the two blocking guards). This hook reads it per
|
|
917
|
+
* call rather than snapshotting at load, so a later `process.env` mutation is
|
|
918
|
+
* visible and the seams behave identically.
|
|
919
|
+
*
|
|
920
|
+
* Deliberately NOT applied to the `git rev-list` spawn in
|
|
921
|
+
* readStateHeadCommits(): that walks the commit graph and never opens the
|
|
922
|
+
* index, so the variable would be a no-op there.
|
|
923
|
+
*/
|
|
924
|
+
const { readOnlyGitEnv } = require('../gsd-core/bin/lib/read-only-git-env.cjs');
|
|
925
|
+
|
|
894
926
|
/**
|
|
895
927
|
* Run `git status --porcelain=v2 --branch` in dir.
|
|
896
928
|
* Returns raw stdout, or null when git is missing, dir isn't a repo, or the
|
|
@@ -902,7 +934,7 @@ function readGitStatus(dir) {
|
|
|
902
934
|
// or untracked files; overflow still degrades safely to segment-absent via
|
|
903
935
|
// the catch below.
|
|
904
936
|
return childProcess.execFileSync('git', ['-C', dir, 'status', '--porcelain=v2', '--branch'],
|
|
905
|
-
{ encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
937
|
+
{ encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, env: readOnlyGitEnv() });
|
|
906
938
|
} catch (e) {
|
|
907
939
|
return null;
|
|
908
940
|
}
|
|
@@ -995,7 +1027,7 @@ function isValidStateHeadStamp(raw) {
|
|
|
995
1027
|
*/
|
|
996
1028
|
function readStateHeadCommits(root, stamp) {
|
|
997
1029
|
try {
|
|
998
|
-
return childProcess.execFileSync('git',
|
|
1030
|
+
return childProcess.execFileSync('git', // allow-index-refresh: rev-list walks the commit graph and never opens the index
|
|
999
1031
|
['-C', root, 'rev-list', '--left-right', '--count', `${stamp}...HEAD`],
|
|
1000
1032
|
{ encoding: 'utf8', timeout: STATE_FRESHNESS_GIT_TIMEOUT_MS,
|
|
1001
1033
|
stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
@@ -1396,6 +1428,7 @@ module.exports = {
|
|
|
1396
1428
|
isAutoCompactDisabled, AUTO_COMPACT_DISABLE_ENV_KEYS, AUTO_COMPACT_ENV_TRUTHY,
|
|
1397
1429
|
resolveAutoCompactThreshold, contextMeter, SETTINGS_FILE_MAX_BYTES,
|
|
1398
1430
|
parseAutoCompactWindowEnv, canonicalModelKey, isAutoCompactWindowValue,
|
|
1431
|
+
GIT_STATUS_TIMEOUT_MS,
|
|
1399
1432
|
};
|
|
1400
1433
|
|
|
1401
1434
|
/**
|
|
@@ -39,6 +39,27 @@ cleanup_temp_files() {
|
|
|
39
39
|
}
|
|
40
40
|
trap cleanup_temp_files EXIT
|
|
41
41
|
|
|
42
|
+
# The first line of $1 (everything before the first newline), matched BYTE-wise.
|
|
43
|
+
#
|
|
44
|
+
# `${1%%$'\n'*}` is a longest-suffix glob match, and on bash 3.2 (macOS's
|
|
45
|
+
# /bin/bash) in a multibyte locale such as en_US.UTF-8 it costs ~3.5 s on a
|
|
46
|
+
# 112KB value (#5473, measured inside the macOS conformance job: the same hook
|
|
47
|
+
# on the same 114750-byte command took ~5.6 s under LC_ALL=en_US.UTF-8 and
|
|
48
|
+
# ~0.55 s under LC_ALL=C, and a trace put 3.5 s of it on this expansion). A
|
|
49
|
+
# newline is the same single byte in every locale, so matching it byte-wise
|
|
50
|
+
# changes no result.
|
|
51
|
+
#
|
|
52
|
+
# ONLY call this inside a command substitution, `SUBJECT=$(gsd_first_line "$MSG")`:
|
|
53
|
+
# the substitution's subshell is what keeps LC_ALL=C from leaking into the rest
|
|
54
|
+
# of the hook, whose regexes and length arithmetic keep the caller's locale. A
|
|
55
|
+
# substitution is a fork with no pipe between two processes, so it keeps the
|
|
56
|
+
# property the pure-expansion form was chosen for (no `head -1` SIGPIPE race
|
|
57
|
+
# under `set -euo pipefail`, #4429); printf is a builtin, so no exec either.
|
|
58
|
+
gsd_first_line() {
|
|
59
|
+
LC_ALL=C
|
|
60
|
+
printf '%s' "${1%%$'\n'*}"
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
# The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
|
|
43
64
|
# review finding: this was previously hand-typed a second time inside the
|
|
44
65
|
# node -e script below, a generative-fix-divergence risk per CLAUDE.md's
|
|
@@ -575,18 +596,19 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
575
596
|
SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
|
|
576
597
|
const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
|
|
577
598
|
process.stdout.write(resolveCommitSubject(process.env.MSG));
|
|
578
|
-
" 2>/dev/null) || SUBJECT
|
|
599
|
+
" 2>/dev/null) || SUBJECT=$(gsd_first_line "$MSG")
|
|
579
600
|
else
|
|
580
|
-
# Pure parameter expansion, not
|
|
601
|
+
# Pure parameter expansion (in gsd_first_line), not
|
|
602
|
+
# `echo "$MSG" | head -1`: that pipeline
|
|
581
603
|
# raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
|
|
582
604
|
# (the common case) — `head -1` can close its read end as soon as it
|
|
583
605
|
# has the first line, and if `echo`'s write lands after that close,
|
|
584
606
|
# `echo` dies with signal 13 (exit 141), which is NOT suppressed by
|
|
585
607
|
# `set -e` and aborted the whole hook intermittently (observed in
|
|
586
|
-
# tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case).
|
|
587
|
-
#
|
|
608
|
+
# tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). No pipe
|
|
609
|
+
# means no race surface. Equivalent to
|
|
588
610
|
# `head -1` for single-line, multi-line, and trailing-newline input.
|
|
589
|
-
SUBJECT
|
|
611
|
+
SUBJECT=$(gsd_first_line "$MSG")
|
|
590
612
|
fi
|
|
591
613
|
# Single source of truth for the accepted commit-type list (#3811): the
|
|
592
614
|
# 10 built-ins plus whatever passed the safe-token filter above. Both the
|
|
@@ -49,8 +49,35 @@ const { reportIfUndetermined, BLOCKING_GUARD_PROBE_TIMEOUT_MS } = require('./lib
|
|
|
49
49
|
// timeout does. The guard keeps its fail-open posture either way.
|
|
50
50
|
const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: BLOCKING_GUARD_PROBE_TIMEOUT_MS, windowsHide: true };
|
|
51
51
|
|
|
52
|
+
// #5048: GIT_OPTIONAL_LOCKS=0 on every git call from this hook. `git rev-parse`
|
|
53
|
+
// never opens the index, so this is inert for today's callers; it is set anyway
|
|
54
|
+
// so the read-only invariant has no holes if a `status` / `diff` caller is ever
|
|
55
|
+
// added here. git refreshes the index and takes an *optional*
|
|
56
|
+
// `.git/index.lock` on some reads, and this hook runs on every tool call, so a
|
|
57
|
+
// read here can otherwise fail a concurrent `git add` / `git commit` with
|
|
58
|
+
// `Unable to create '.git/index.lock': File exists`.
|
|
59
|
+
//
|
|
60
|
+
// The env block is owned by src/read-only-git-env.cts and shared with every
|
|
61
|
+
// other read-only seam (built artifact, not a sibling hooks/-tree file — same
|
|
62
|
+
// rule hooks/lib/git-cmd.js follows for token-scanner). It is read per spawn,
|
|
63
|
+
// not snapshotted at load, so the two guards behave identically.
|
|
64
|
+
// Parity: tests/git-optional-locks-parity.test.cjs.
|
|
65
|
+
// A raw plugin-marketplace or git-clone install never ran `npm run build:lib`
|
|
66
|
+
// (#3582), so the compiled owner can be absent. This guard only runs
|
|
67
|
+
// `git rev-parse`, which never opens the index, so the unmodified environment
|
|
68
|
+
// loses nothing; requiring the artifact unguarded would throw at load and the
|
|
69
|
+
// containment guard would silently stop running.
|
|
70
|
+
let readOnlyGitEnv = () => process.env;
|
|
71
|
+
try {
|
|
72
|
+
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
|
73
|
+
ensureRuntimeBuild();
|
|
74
|
+
({ readOnlyGitEnv } = require('../gsd-core/bin/lib/read-only-git-env.cjs'));
|
|
75
|
+
} catch {
|
|
76
|
+
// artifact absent and not buildable: keep the inert fallback above
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
function git(args, cwd) {
|
|
53
|
-
return spawnSync('git', args, { ...SPAWNOPT, cwd });
|
|
80
|
+
return spawnSync('git', args, { ...SPAWNOPT, cwd, env: readOnlyGitEnv() });
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
// Walk up from `start` to find the nearest existing DIRECTORY (not merely an
|
|
@@ -81,7 +81,7 @@ function forceGitAddCwds(command, defaultCwd) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function currentBranch(cwd) {
|
|
84
|
-
const result = spawnSync('git', ['branch', '--show-current'], {
|
|
84
|
+
const result = spawnSync('git', ['branch', '--show-current'], { // allow-index-refresh: reads HEAD only, never opens the index
|
|
85
85
|
cwd,
|
|
86
86
|
encoding: 'utf8',
|
|
87
87
|
stdio: ['ignore', 'pipe', 'ignore'],
|