@opengsd/gsd-core 1.13.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- 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 +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -26,12 +26,24 @@
|
|
|
26
26
|
// .env, .secrets, and .env.<suffix> — EXCEPT .env.example / .env.sample /
|
|
27
27
|
// .env.template / .env.dist, which are the non-secret templates GSD's own
|
|
28
28
|
// phase prompt tells executors to read.
|
|
29
|
-
// Stated cost:
|
|
30
|
-
//
|
|
29
|
+
// Stated cost (#4580): the exemption matches the token's FINAL EXTENSION,
|
|
30
|
+
// not the whole name, so the trusted set is `.env.<anything>.example` /
|
|
31
|
+
// `.sample` / `.template` / `.dist` — an unbounded family, not four fixed
|
|
32
|
+
// names. A real secret named `.env.prod-real-secrets.example` is NOT
|
|
33
|
+
// protected, and renaming any secret to end in one of those four
|
|
34
|
+
// extensions bypasses the guard across Read, Grep and Bash alike. This is
|
|
35
|
+
// the deliberate cost of #4580, which fixed the prior whole-name
|
|
36
|
+
// comparison wrongly refusing committed, secret-free templates like
|
|
37
|
+
// `.env.local.example`.
|
|
31
38
|
// A token containing `:` is also tested on the part after its LAST `:`,
|
|
32
39
|
// so `git show HEAD:.env`, `origin/main:config/.env` and `C:\proj\.env`
|
|
33
|
-
// are caught without git-specific parsing.
|
|
34
|
-
// commit message `fix: .env parsing` yields
|
|
40
|
+
// are caught without git-specific parsing. Leading/interior whitespace is
|
|
41
|
+
// still NOT trimmed: the commit message `fix: .env parsing` yields
|
|
42
|
+
// ` .env parsing`, which is prose, not a name. TRAILING dots and spaces ARE
|
|
43
|
+
// stripped from the basename before classification (`.env.`, `.env..`,
|
|
44
|
+
// `.env `, `.env. ` all normalize to `.env`), because Win32 strips trailing
|
|
45
|
+
// dots and spaces from each path component, so these are aliases for the
|
|
46
|
+
// same on-disk file, not distinct names.
|
|
35
47
|
//
|
|
36
48
|
// Bash analysis is a two-pass token scan, not a shell:
|
|
37
49
|
// pass 1 tokenizes with quote state, comments, redirect operators (with fd
|
|
@@ -74,7 +86,12 @@
|
|
|
74
86
|
// on plain commands, without arming the compound-`cd` prompt". Writes to
|
|
75
87
|
// secret files are out of scope (Write/Edit were never gated). Commands
|
|
76
88
|
// over 1 MiB are denied outright (`command-too-large`) rather than
|
|
77
|
-
// scanned partially or waved through.
|
|
89
|
+
// scanned partially or waved through. (#4639 adds one more, by design: the
|
|
90
|
+
// value of `--env-file` under a container runtime is exempt, so the
|
|
91
|
+
// container's own command can print the interpolated environment
|
|
92
|
+
// (`alpine printenv`, `docker compose config`) — the same exposure class
|
|
93
|
+
// as the pre-existing volume-mount gap (`-v .env:/s`); the flag's value
|
|
94
|
+
// itself is a name, never contents.)
|
|
78
95
|
//
|
|
79
96
|
// Triggers on: Read, Grep, Bash tool calls (Kimi: ReadFile, Grep, Shell)
|
|
80
97
|
// Action: BLOCK (decision: 'block', exit 2) — codes secret-read |
|
|
@@ -86,6 +103,7 @@
|
|
|
86
103
|
'use strict';
|
|
87
104
|
|
|
88
105
|
const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
|
|
106
|
+
const { finalExtension, normalizeWindowsBasename, lastSegment } = require('./lib/filename-classification.js');
|
|
89
107
|
|
|
90
108
|
// Fail open on a hook-internal error (see header). Declared ONCE so the
|
|
91
109
|
// outer catch states its policy explicitly (#3911).
|
|
@@ -114,6 +132,15 @@ const NON_READING_COMMANDS = new Set([
|
|
|
114
132
|
'basename', 'dirname', 'realpath', 'file', 'echo', 'printf',
|
|
115
133
|
]);
|
|
116
134
|
|
|
135
|
+
// #4639: container runtimes take `--env-file <file>` — the runtime opens the
|
|
136
|
+
// file itself, interpolates it into the container environment, and returns
|
|
137
|
+
// nothing to the agent, so the flag's VALUE is a name, never contents. The
|
|
138
|
+
// same category NON_READING_COMMANDS encodes, expressed as a flag value. Only
|
|
139
|
+
// the flag's value under these runtimes is exempt; every other operand in the
|
|
140
|
+
// segment is still checked, so the carve-out cannot launder a read.
|
|
141
|
+
const CONTAINER_RUNTIMES = new Set(['docker', 'docker-compose', 'podman', 'nerdctl']);
|
|
142
|
+
const ENV_FILE_FLAG_RE = /^--env-file(=|$)/;
|
|
143
|
+
|
|
117
144
|
// Shell interpreters that run a script from `-c`, a file operand, or stdin
|
|
118
145
|
// (heredoc / here-string / piped `echo`|`printf`). `su` is here for its `-c`
|
|
119
146
|
// form (`su [user] -c 'cmd'`); a bare `su user` resolves to file mode, which
|
|
@@ -148,21 +175,18 @@ const GLOB_PROBES = [
|
|
|
148
175
|
// ---------------------------------------------------------------------------
|
|
149
176
|
|
|
150
177
|
function isSecretBasename(name) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
// Win32 strips trailing dots/spaces per path component, so `.env.`,
|
|
179
|
+
// `.env ` etc. resolve to the real `.env` on Windows — normalize FIRST so
|
|
180
|
+
// those aliases can't bypass classification.
|
|
181
|
+
const n = normalizeWindowsBasename(name);
|
|
182
|
+
if (n === '.env' || n === '.secrets') return true;
|
|
183
|
+
if (n.startsWith('.env.')) {
|
|
184
|
+
const suffix = n.slice('.env.'.length);
|
|
185
|
+
return suffix !== '' && !NON_SECRET_ENV_SUFFIXES.has(finalExtension(suffix).toLowerCase());
|
|
155
186
|
}
|
|
156
187
|
return false;
|
|
157
188
|
}
|
|
158
189
|
|
|
159
|
-
// Last `/`- or `\`-separated segment, ignoring trailing separators.
|
|
160
|
-
function lastSegment(tok) {
|
|
161
|
-
const s = tok.replace(/[\\/]+$/, '');
|
|
162
|
-
const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
|
|
163
|
-
return i === -1 ? s : s.slice(i + 1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
190
|
// True when the token's basename — or the basename of the part after its
|
|
167
191
|
// last `:` (git `<ref>:<path>`, Windows drive) — is a secret name. Folded to
|
|
168
192
|
// lower case once at the top so `.ENV` / `.Secrets` match on the
|
|
@@ -247,7 +271,19 @@ function globAltSelectsSecret(alt) {
|
|
|
247
271
|
if (/^[*?]+$/.test(alt)) return false; // pure wildcard: equivalent to no glob
|
|
248
272
|
const wild = alt.search(/[*?[]/);
|
|
249
273
|
const lit = wild === -1 ? alt : alt.slice(0, wild);
|
|
250
|
-
|
|
274
|
+
// #4580: when there is no wildcard, `alt` (== `lit`) is a WHOLE literal
|
|
275
|
+
// filename, so classify it exactly the same way Read/Bash do (by its
|
|
276
|
+
// FINAL extension, via isSecretBasename) rather than by a `.env.`-prefix
|
|
277
|
+
// heuristic — that heuristic mis-blocked multi-dot templates like
|
|
278
|
+
// `.env.local.example`. When a wildcard IS present, `lit` is only a
|
|
279
|
+
// PARTIAL literal prefix (`.env.local.exam*` can still select
|
|
280
|
+
// `.env.local`), which cannot be classified exactly, so the original
|
|
281
|
+
// conservative prefix rule stays.
|
|
282
|
+
if (wild === -1) {
|
|
283
|
+
if (isSecretBasename(lit)) return true;
|
|
284
|
+
} else if (lit.startsWith('.env.')) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
251
287
|
if (lit !== '' && ('.env.'.startsWith(lit) || '.secrets'.startsWith(lit))) return true;
|
|
252
288
|
let re;
|
|
253
289
|
try {
|
|
@@ -260,10 +296,14 @@ function globAltSelectsSecret(alt) {
|
|
|
260
296
|
|
|
261
297
|
// Returns null (allowed), 'secret-read', or 'glob-too-complex'.
|
|
262
298
|
function classifyGrepGlob(glob) {
|
|
263
|
-
|
|
299
|
+
// Segment via the SAME `lastSegment` helper Read/Bash use (namesSecret),
|
|
300
|
+
// rather than a hand-rolled forward-slash-only split — the two used to
|
|
301
|
+
// diverge on a backslash-bearing glob (`config\.env`), which `lastSegment`
|
|
302
|
+
// reduces to `.env` but a `/`-only split left untouched, letting it escape
|
|
303
|
+
// this arm's predicate while Read/Bash still blocked it.
|
|
264
304
|
// Case-fold the last segment (GLOB_PROBES are lower case) so `.ENV*` and
|
|
265
305
|
// `*.ENV` select the secret namespace on case-insensitive filesystems.
|
|
266
|
-
const segment = (
|
|
306
|
+
const segment = lastSegment(glob).toLowerCase();
|
|
267
307
|
const alts = expandBraces(segment);
|
|
268
308
|
if (alts === null) return 'glob-too-complex';
|
|
269
309
|
return alts.some(globAltSelectsSecret) ? 'secret-read' : null;
|
|
@@ -814,7 +854,19 @@ function findSecretRead(command, depth) {
|
|
|
814
854
|
|
|
815
855
|
if (NON_READING_COMMANDS.has(base)) continue;
|
|
816
856
|
|
|
857
|
+
// #4639: exempt ONLY the value of `--env-file`, and only when the
|
|
858
|
+
// segment's command word is a container runtime. A bare `--env-file`
|
|
859
|
+
// consumes the next operand; `--env-file=<value>` is a single word.
|
|
860
|
+
const envFileExempt = CONTAINER_RUNTIMES.has(base);
|
|
861
|
+
let skipNext = false;
|
|
817
862
|
for (const w of operands) {
|
|
863
|
+
if (envFileExempt) {
|
|
864
|
+
if (skipNext) { skipNext = false; continue; }
|
|
865
|
+
if (ENV_FILE_FLAG_RE.test(w.text)) {
|
|
866
|
+
if (!w.text.includes('=')) skipNext = true; // bare flag consumes the next operand
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
818
870
|
if (namesSecret(normalizeOperand(w.text))) return w.text;
|
|
819
871
|
}
|
|
820
872
|
}
|
package/hooks/gsd-statusline.js
CHANGED
|
@@ -42,6 +42,10 @@ if (require.main === module) {
|
|
|
42
42
|
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
43
43
|
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
|
44
44
|
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
|
45
|
+
const {
|
|
46
|
+
renderBracketPhaseDisplay,
|
|
47
|
+
renderBracketMilestoneDisplay,
|
|
48
|
+
} = require('../gsd-core/bin/lib/phase-id-display.cjs');
|
|
45
49
|
// #2850: reuse the existing workstream resolution seams rather than
|
|
46
50
|
// re-implementing CLI>env>store precedence or path construction inline.
|
|
47
51
|
// peekActiveWorkstream is the read-only sibling of the store-tier lookup
|
|
@@ -359,16 +363,26 @@ function renderProgressBar(percent) {
|
|
|
359
363
|
* Progress bar is opt-in: appended to the milestone segment only when
|
|
360
364
|
* progress.percent is present in frontmatter; absent → empty string.
|
|
361
365
|
*/
|
|
362
|
-
function formatGsdState(s) {
|
|
366
|
+
function formatGsdState(s, opts = {}) {
|
|
363
367
|
// #2850: workstream mode with nothing resolvable — an observable signal,
|
|
364
368
|
// never silent emptiness (distinguishes from "GSD isn't installed here").
|
|
365
369
|
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
|
366
370
|
|
|
367
371
|
const parts = [];
|
|
372
|
+
const bracket = opts.convention === 'bracket';
|
|
373
|
+
const bracketMilestone = bracket
|
|
374
|
+
? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
|
|
375
|
+
: null;
|
|
376
|
+
const phaseLabel = (phase) => {
|
|
377
|
+
const display = bracket
|
|
378
|
+
? renderBracketPhaseDisplay(s.milestone, phase, opts.projectCode)
|
|
379
|
+
: null;
|
|
380
|
+
return display ?? `Phase ${phase}`;
|
|
381
|
+
};
|
|
368
382
|
|
|
369
383
|
// Milestone segment: version + name + (opt-in) progress bar
|
|
370
384
|
if (s.milestone || s.milestoneName) {
|
|
371
|
-
const ver = s.milestone
|
|
385
|
+
const ver = bracketMilestone ?? s.milestone ?? '';
|
|
372
386
|
const name = (s.milestoneName && s.milestoneName !== 'milestone') ? s.milestoneName : '';
|
|
373
387
|
const bar = renderProgressBar(s.percent);
|
|
374
388
|
const pieces = [ver, name, bar].filter(Boolean);
|
|
@@ -384,7 +398,8 @@ function formatGsdState(s) {
|
|
|
384
398
|
// stage = whichever lifecycle status was written by the orchestrator
|
|
385
399
|
// (discussing / planning / executing / verifying)
|
|
386
400
|
const stage = s.status || '';
|
|
387
|
-
|
|
401
|
+
const phase = phaseLabel(s.activePhase);
|
|
402
|
+
parts.push(stage ? `${phase} ${stage}` : phase);
|
|
388
403
|
} else if (s.nextAction && phasesStr) {
|
|
389
404
|
// Scene 2: idle + a recommended next command is visible to the user.
|
|
390
405
|
// Surfaces "what to run next" without the user opening STATE.md.
|
|
@@ -402,9 +417,16 @@ function formatGsdState(s) {
|
|
|
402
417
|
// earlier so no existing project's status-line changes shape.
|
|
403
418
|
if (s.status) parts.push(s.status);
|
|
404
419
|
if (s.phaseNum && s.phaseTotal) {
|
|
420
|
+
const bracketPhase = bracket
|
|
421
|
+
? renderBracketPhaseDisplay(s.milestone, s.phaseNum, opts.projectCode)
|
|
422
|
+
: null;
|
|
405
423
|
const phase = s.phaseName
|
|
406
|
-
?
|
|
407
|
-
|
|
424
|
+
? bracketPhase
|
|
425
|
+
? `${bracketPhase} ${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
|
|
426
|
+
: `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
|
|
427
|
+
: bracketPhase
|
|
428
|
+
? `${bracketPhase} (${s.phaseNum}/${s.phaseTotal})`
|
|
429
|
+
: `ph ${s.phaseNum}/${s.phaseTotal}`;
|
|
408
430
|
parts.push(phase);
|
|
409
431
|
}
|
|
410
432
|
}
|
|
@@ -449,13 +471,17 @@ function contextTokenSuffix(currentUsage) {
|
|
|
449
471
|
// --- Compact state format (opt-in) ---------------------------------------------
|
|
450
472
|
|
|
451
473
|
/**
|
|
452
|
-
* Collapse GSD's
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
474
|
+
* Collapse GSD's status value to a single keyword, built on the canonical
|
|
475
|
+
* normalizer (#2162 approval condition): normalizeStateStatus() in
|
|
476
|
+
* state-document.cjs owns the status vocabulary (discussing / planning /
|
|
477
|
+
* executing / verifying / completed / paused) so the two can't drift.
|
|
478
|
+
* #4186: the normalizer recognizes the DECLARED vocabulary by anchored
|
|
479
|
+
* whole-field match — vocabulary values (the state writer persists tokens)
|
|
480
|
+
* collapse to their keyword; free-text narratives are no longer
|
|
481
|
+
* keyword-guessed from substrings (a `.planning/` mention in non-English
|
|
482
|
+
* prose used to render `planning`), and pass through unrecognized to the
|
|
483
|
+
* first-word fallback below. "paused" — the canonical stuck state — is
|
|
484
|
+
* uppercased to PAUSED, the one state worth shouting about. The fallback is
|
|
459
485
|
* capped at 16 chars so a rogue STATE.md can't blow up the line.
|
|
460
486
|
* Returns null for empty input.
|
|
461
487
|
*/
|
|
@@ -486,17 +512,24 @@ function shortGsdStatus(status) {
|
|
|
486
512
|
* default format — and collapses narrative statuses via shortGsdStatus().
|
|
487
513
|
* The default "full" format is untouched.
|
|
488
514
|
*/
|
|
489
|
-
function formatGsdStateCompact(s) {
|
|
515
|
+
function formatGsdStateCompact(s, opts = {}) {
|
|
490
516
|
// #2850: mirrors formatGsdState's observable "nothing resolvable" signal.
|
|
491
517
|
if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
|
|
492
518
|
|
|
493
519
|
const parts = [];
|
|
520
|
+
const bracket = opts.convention === 'bracket';
|
|
521
|
+
const bracketMilestone = bracket
|
|
522
|
+
? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
|
|
523
|
+
: null;
|
|
494
524
|
|
|
495
|
-
if (s.milestone) parts.push(s.milestone);
|
|
525
|
+
if (s.milestone) parts.push(bracketMilestone ?? s.milestone);
|
|
496
526
|
|
|
497
527
|
const phaseId = s.activePhase || s.phaseNum;
|
|
498
528
|
if (phaseId) {
|
|
499
|
-
|
|
529
|
+
const bracketPhase = bracket
|
|
530
|
+
? renderBracketPhaseDisplay(s.milestone, phaseId, opts.projectCode)
|
|
531
|
+
: null;
|
|
532
|
+
parts.push(bracketPhase ?? (s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`));
|
|
500
533
|
}
|
|
501
534
|
|
|
502
535
|
// Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
|
|
@@ -743,7 +776,7 @@ function formatStateFreshness(fresh) {
|
|
|
743
776
|
* entry points — this collapses both onto one resolver.
|
|
744
777
|
*
|
|
745
778
|
* @param {object} cfg — parsed .planning/config.json (readGsdConfig())
|
|
746
|
-
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean }}
|
|
779
|
+
* @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean, convention: string|null, projectCode: string|null }}
|
|
747
780
|
*/
|
|
748
781
|
function resolveStatuslineOptions(cfg) {
|
|
749
782
|
const showLastCommand = getConfigValue(cfg, 'statusline.show_last_command') === true;
|
|
@@ -755,7 +788,22 @@ function resolveStatuslineOptions(cfg) {
|
|
|
755
788
|
const stateFormat = getConfigValue(cfg, 'statusline.state_format') === 'compact' ? 'compact' : 'full';
|
|
756
789
|
const showGit = getConfigValue(cfg, 'statusline.show_git') === true;
|
|
757
790
|
const showStateFreshness = getConfigValue(cfg, 'statusline.show_state_freshness') === true;
|
|
758
|
-
|
|
791
|
+
const convention = getConfigValue(cfg, 'phase_id_convention') === 'bracket'
|
|
792
|
+
? 'bracket'
|
|
793
|
+
: null;
|
|
794
|
+
const projectCodeValue = getConfigValue(cfg, 'project_code');
|
|
795
|
+
const projectCode = typeof projectCodeValue === 'string' && projectCodeValue !== ''
|
|
796
|
+
? projectCodeValue
|
|
797
|
+
: null;
|
|
798
|
+
return {
|
|
799
|
+
showLastCommand,
|
|
800
|
+
position,
|
|
801
|
+
stateFormat,
|
|
802
|
+
showGit,
|
|
803
|
+
showStateFreshness,
|
|
804
|
+
convention,
|
|
805
|
+
projectCode,
|
|
806
|
+
};
|
|
759
807
|
}
|
|
760
808
|
|
|
761
809
|
// --- stdin ------------------------------------------------------------------
|
|
@@ -930,7 +978,9 @@ function runStatusline() {
|
|
|
930
978
|
// freshness git spawn here would spend a subprocess on discarded output.
|
|
931
979
|
if (!task) {
|
|
932
980
|
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
933
|
-
gsdStateStr = options.stateFormat === 'compact'
|
|
981
|
+
gsdStateStr = options.stateFormat === 'compact'
|
|
982
|
+
? formatGsdStateCompact(state, options)
|
|
983
|
+
: formatGsdState(state, options);
|
|
934
984
|
}
|
|
935
985
|
|
|
936
986
|
// Output
|
|
@@ -1038,6 +1088,7 @@ module.exports = {
|
|
|
1038
1088
|
STATE_HEAD_ADVISORY_COMMITS, isValidStateHeadStamp,
|
|
1039
1089
|
readStateHeadCommits, parseRevListCounts, deriveStateFreshness,
|
|
1040
1090
|
formatStateFreshness, resolveStatuslineOptions,
|
|
1091
|
+
renderBracketPhaseDisplay, renderBracketMilestoneDisplay,
|
|
1041
1092
|
};
|
|
1042
1093
|
|
|
1043
1094
|
/**
|
|
@@ -1055,7 +1106,15 @@ function renderStatusline(data) {
|
|
|
1055
1106
|
// key from reaching only one of them.
|
|
1056
1107
|
let lastCmdSuffix = '';
|
|
1057
1108
|
let gitSuffix = '';
|
|
1058
|
-
let options = {
|
|
1109
|
+
let options = {
|
|
1110
|
+
showLastCommand: false,
|
|
1111
|
+
position: 'end',
|
|
1112
|
+
stateFormat: 'full',
|
|
1113
|
+
showGit: false,
|
|
1114
|
+
showStateFreshness: false,
|
|
1115
|
+
convention: null,
|
|
1116
|
+
projectCode: null,
|
|
1117
|
+
};
|
|
1059
1118
|
try {
|
|
1060
1119
|
const cfg = readGsdConfig(dir);
|
|
1061
1120
|
options = resolveStatuslineOptions(cfg);
|
|
@@ -1071,7 +1130,9 @@ function renderStatusline(data) {
|
|
|
1071
1130
|
} catch (e) { /* swallow */ }
|
|
1072
1131
|
|
|
1073
1132
|
const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
|
|
1074
|
-
const gsdStateStr = options.stateFormat === 'compact'
|
|
1133
|
+
const gsdStateStr = options.stateFormat === 'compact'
|
|
1134
|
+
? formatGsdStateCompact(state, options)
|
|
1135
|
+
: formatGsdState(state, options);
|
|
1075
1136
|
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
|
1076
1137
|
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position: options.position });
|
|
1077
1138
|
}
|
|
@@ -16,6 +16,21 @@ set -euo pipefail
|
|
|
16
16
|
# Idempotent and failure-proof by construction: unset vars expand to "" (a
|
|
17
17
|
# no-op rm -f target), and `|| true` guarantees the trap itself never changes
|
|
18
18
|
# the script's exit status.
|
|
19
|
+
# Subprocess exit statuses, pre-initialised so they can never be inherited from
|
|
20
|
+
# the ambient environment. Each is captured as `... || VAR=$?`, which assigns
|
|
21
|
+
# ONLY on the failure branch; on success the variable keeps whatever it already
|
|
22
|
+
# held, and `${VAR:-0}` defaults only when unset or empty. So an EXPORTED
|
|
23
|
+
# CONFIG_STATUS / CMD_STATUS / CLASSIFY_STATUS — from a CI wrapper, a .envrc, or
|
|
24
|
+
# another hook — survived into the success path and was read as "the subprocess
|
|
25
|
+
# failed". Measured: `CLASSIFY_STATUS=3 git commit -m "nope: bad"` printed
|
|
26
|
+
# "validator disabled for this call" and exited 0, silently accepting a
|
|
27
|
+
# non-conforming commit. Same for CONFIG_STATUS and CMD_STATUS. Found by the
|
|
28
|
+
# security review of #4429; the gate is fail-open by design on a genuine
|
|
29
|
+
# subprocess failure (#3838), which is exactly what made this bypass quiet.
|
|
30
|
+
CONFIG_STATUS=0
|
|
31
|
+
CMD_STATUS=0
|
|
32
|
+
CLASSIFY_STATUS=0
|
|
33
|
+
|
|
19
34
|
ENABLED_ERR=""
|
|
20
35
|
CMD_ERR=""
|
|
21
36
|
CLASSIFY_ERR=""
|
|
@@ -61,6 +76,12 @@ if [ -f .planning/config.json ]; then
|
|
|
61
76
|
process.exit(3);
|
|
62
77
|
}
|
|
63
78
|
" 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
|
|
79
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
80
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
81
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
82
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
83
|
+
# `CONFIG_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
84
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
64
85
|
CONFIG_STATUS=${CONFIG_STATUS:-0}
|
|
65
86
|
if [ "$CONFIG_STATUS" != "0" ]; then
|
|
66
87
|
# Could not determine the opt-in flag at all (node missing, JSON parse
|
|
@@ -69,7 +90,11 @@ if [ -f .planning/config.json ]; then
|
|
|
69
90
|
echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
|
|
70
91
|
exit 0
|
|
71
92
|
fi
|
|
72
|
-
|
|
93
|
+
# Pure parameter expansion, not `printf ... | head -1`: same SIGPIPE race
|
|
94
|
+
# class as the SUBJECT extraction below (`echo "$MSG" | head -1`) — CONFIG_OUT
|
|
95
|
+
# is multi-line whenever extra commit types are configured, and `head -1`
|
|
96
|
+
# closing early can SIGPIPE `printf` under `set -euo pipefail`.
|
|
97
|
+
ENABLED="${CONFIG_OUT%%$'\n'*}"
|
|
73
98
|
if [ "$ENABLED" != "1" ]; then exit 0; fi
|
|
74
99
|
# Remaining lines (if any) are the sanitized, deduped configured commit
|
|
75
100
|
# types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
|
|
@@ -99,6 +124,12 @@ CMD=$(echo "$INPUT" | node -e "
|
|
|
99
124
|
}
|
|
100
125
|
});
|
|
101
126
|
" 2>"$CMD_ERR") || CMD_STATUS=$?
|
|
127
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
128
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
129
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
130
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
131
|
+
# `CMD_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
132
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
102
133
|
CMD_STATUS=${CMD_STATUS:-0}
|
|
103
134
|
if [ "$CMD_STATUS" != "0" ]; then
|
|
104
135
|
# Could not extract tool_input.command at all (node missing, malformed
|
|
@@ -123,6 +154,12 @@ GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
|
|
|
123
154
|
process.exit(3);
|
|
124
155
|
}
|
|
125
156
|
" "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
|
|
157
|
+
# Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
|
|
158
|
+
# assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
|
|
159
|
+
# already held — and an EXPORTED variable of this name is inherited from the
|
|
160
|
+
# ambient environment. `${VAR:-0}` defaults only when unset/empty, so
|
|
161
|
+
# `CLASSIFY_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
|
|
162
|
+
# silently accepting a non-conforming commit. Found by review of #4429.
|
|
126
163
|
CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
|
|
127
164
|
if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
|
|
128
165
|
# 0 = is a git commit (validate below); 1 = genuinely not a git commit
|
|
@@ -214,7 +251,24 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
214
251
|
# the message — the window a guard must use when the token it scans for
|
|
215
252
|
# is also legal English inside a commit message, but may legally appear
|
|
216
253
|
# on EITHER side of the message on the command line.
|
|
217
|
-
|
|
254
|
+
# Indexed, not searched (#4492). `${CMD#*"$MSG_MATCH"}` is quadratic in
|
|
255
|
+
# the message: bash walks every prefix length and compares the whole
|
|
256
|
+
# matched literal at each one, and MSG_MATCH is BASH_REMATCH[0] — the
|
|
257
|
+
# entire `-m "..."` — so the cost grows with the thing being scanned.
|
|
258
|
+
# Measured on the path EVERY commit takes (conforming and non-conforming
|
|
259
|
+
# cost the same): 10.0 s at a 64 KB message, 22.0 s at 96 KB, 30.2 s at
|
|
260
|
+
# 112 KB. Sizes stop there deliberately — a single argument above Linux's
|
|
261
|
+
# MAX_ARG_STRLEN (131072 on a 4 KB-page kernel) never reaches this code
|
|
262
|
+
# at all, because execve fails and the hook fails open, so a larger
|
|
263
|
+
# "measurement" would be timing the wrong thing.
|
|
264
|
+
#
|
|
265
|
+
# MSG_PREFIX above has already located the match, so the suffix is
|
|
266
|
+
# arithmetic rather than a search: skip the prefix and the match. This
|
|
267
|
+
# removes the quadratic SEARCH; the expansion still counts characters and
|
|
268
|
+
# materialises a substring, so it is linear in the command, not O(1).
|
|
269
|
+
# Same first-occurrence assumption both expansions here always made —
|
|
270
|
+
# MSG_MATCH is a literal substring of CMD by construction.
|
|
271
|
+
MSG_SUFFIX="${CMD:$(( ${#MSG_PREFIX} + ${#MSG_MATCH} ))}"
|
|
218
272
|
# LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
|
|
219
273
|
# `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
|
|
220
274
|
# spread an invocation over lines, and every guard below reads a newline in
|
|
@@ -521,13 +575,22 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
521
575
|
SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
|
|
522
576
|
const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
|
|
523
577
|
process.stdout.write(resolveCommitSubject(process.env.MSG));
|
|
524
|
-
" 2>/dev/null) || SUBJECT
|
|
578
|
+
" 2>/dev/null) || SUBJECT="${MSG%%$'\n'*}"
|
|
525
579
|
else
|
|
526
|
-
|
|
580
|
+
# Pure parameter expansion, not `echo "$MSG" | head -1`: that pipeline
|
|
581
|
+
# raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
|
|
582
|
+
# (the common case) — `head -1` can close its read end as soon as it
|
|
583
|
+
# has the first line, and if `echo`'s write lands after that close,
|
|
584
|
+
# `echo` dies with signal 13 (exit 141), which is NOT suppressed by
|
|
585
|
+
# `set -e` and aborted the whole hook intermittently (observed in
|
|
586
|
+
# tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). Zero
|
|
587
|
+
# subprocesses here means zero pipe/race surface. Equivalent to
|
|
588
|
+
# `head -1` for single-line, multi-line, and trailing-newline input.
|
|
589
|
+
SUBJECT="${MSG%%$'\n'*}"
|
|
527
590
|
fi
|
|
528
591
|
# Single source of truth for the accepted commit-type list (#3811): the
|
|
529
592
|
# 10 built-ins plus whatever passed the safe-token filter above. Both the
|
|
530
|
-
#
|
|
593
|
+
# membership test and the human-readable error text below are derived
|
|
531
594
|
# from this ONE array — no hand-synced second copy.
|
|
532
595
|
#
|
|
533
596
|
# The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
|
|
@@ -538,7 +601,6 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
538
601
|
# /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
|
|
539
602
|
# nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
|
|
540
603
|
COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
|
|
541
|
-
COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
|
|
542
604
|
COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
|
|
543
605
|
COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
|
|
544
606
|
# Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
|
|
@@ -550,8 +612,35 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
|
|
|
550
612
|
# or `\`.
|
|
551
613
|
COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
|
|
552
614
|
COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
|
|
553
|
-
# Validate Conventional Commits format
|
|
554
|
-
|
|
615
|
+
# Validate Conventional Commits format.
|
|
616
|
+
#
|
|
617
|
+
# #4429: do NOT build `^(type1|type2|...)` out of COMMIT_TYPES. That
|
|
618
|
+
# alternation grows with the CONFIGURED list, and how large a pattern can be
|
|
619
|
+
# compiled is a property of the platform's regex engine. bash 3.2.57 / BSD
|
|
620
|
+
# libc (macOS, this file's stated target) caps it at 64 KiB - bisected: a
|
|
621
|
+
# 65504-byte alternation compiles, 65515 fails. bash 5.2 / glibc has no
|
|
622
|
+
# reachable cap, so this half never bit Linux. Past a cap `[[ =~ ]]`
|
|
623
|
+
# returns 2, and `if !` cannot tell a COMPILE ERROR from "the subject does
|
|
624
|
+
# not conform" - so a valid `feat(auth): ...` was blocked with
|
|
625
|
+
# CONVENTIONAL_COMMITS_VIOLATION while `feat` sat in its own valid_types.
|
|
626
|
+
#
|
|
627
|
+
# Match the SHAPE with a fixed-size pattern, then test membership against
|
|
628
|
+
# the array. The regex no longer depends on how many types are configured,
|
|
629
|
+
# and the loop adds no subprocess or pipe (the #4429 hazard this file
|
|
630
|
+
# already avoids elsewhere). The character class is exactly the safe-token
|
|
631
|
+
# filter `^[a-z][a-z0-9-]*$` applied above, so it captures every type that
|
|
632
|
+
# can legally reach COMMIT_TYPES and no token that cannot.
|
|
633
|
+
SUBJECT_TYPE=''
|
|
634
|
+
if [[ "$SUBJECT" =~ ^([a-z][a-z0-9-]*)(\(.+\))?:[[:space:]].+ ]]; then
|
|
635
|
+
SUBJECT_TYPE="${BASH_REMATCH[1]}"
|
|
636
|
+
fi
|
|
637
|
+
COMMIT_TYPE_OK=0
|
|
638
|
+
if [ -n "$SUBJECT_TYPE" ]; then
|
|
639
|
+
for _known_type in "${COMMIT_TYPES[@]}"; do
|
|
640
|
+
if [ "$_known_type" = "$SUBJECT_TYPE" ]; then COMMIT_TYPE_OK=1; break; fi
|
|
641
|
+
done
|
|
642
|
+
fi
|
|
643
|
+
if [ "$COMMIT_TYPE_OK" -ne 1 ]; then
|
|
555
644
|
# Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
|
|
556
645
|
# #3811). Tests assert on the stable code string and the typed array;
|
|
557
646
|
# the reason is the human-readable copy, never grepped by tests.
|
|
@@ -163,17 +163,37 @@ process.stdin.on('end', () => {
|
|
|
163
163
|
// returns a path containing .git/worktrees/ as a component.
|
|
164
164
|
// In the main repo or a submodule it returns .git (or a path without /worktrees/).
|
|
165
165
|
// This approach works even when cwd is a subdirectory of the worktree.
|
|
166
|
-
|
|
166
|
+
// Combined into one spawn — git rev-parse accepts multiple query flags in
|
|
167
|
+
// one invocation and prints one line of output per flag, in the exact
|
|
168
|
+
// order given, reducing this guard's worst-case subprocess count under
|
|
169
|
+
// CI/load contention (three spawns collapse into one). `--abbrev-ref HEAD`
|
|
170
|
+
// is used instead of `symbolic-ref --short HEAD` because it is combinable
|
|
171
|
+
// (a single `rev-parse` call) and behaviorally equivalent for this guard's
|
|
172
|
+
// branch-acceptance check, including on detached HEAD: `--abbrev-ref`
|
|
173
|
+
// returns the literal string `HEAD` there (exit 0), which the acceptance
|
|
174
|
+
// regex below also rejects — the same guard outcome as symbolic-ref's
|
|
175
|
+
// exit-128/empty-stdout failure. Do not change any timeout value as part
|
|
176
|
+
// of this change, only the spawn count.
|
|
177
|
+
const combinedResult = git(['rev-parse', '--git-dir', '--abbrev-ref', 'HEAD', '--show-toplevel'], cwd);
|
|
167
178
|
// #3911: a timeout/spawn-failure result is indistinguishable from a clean
|
|
168
179
|
// "not a git repo" answer by status/stdout alone — reportIfUndetermined
|
|
169
180
|
// is a no-op on a genuine negative and only fires the diagnostic when the
|
|
170
181
|
// probe itself could not run. The allow() below is UNCHANGED either way.
|
|
171
|
-
reportIfUndetermined(
|
|
172
|
-
|
|
182
|
+
reportIfUndetermined(
|
|
183
|
+
'gsd-worktree-path-guard',
|
|
184
|
+
'git rev-parse --git-dir --abbrev-ref HEAD --show-toplevel',
|
|
185
|
+
combinedResult
|
|
186
|
+
);
|
|
187
|
+
if (combinedResult.status !== 0 || !combinedResult.stdout) {
|
|
173
188
|
allow(undefined); // not a git repo — pass through
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
const
|
|
191
|
+
const combinedLines = combinedResult.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
|
192
|
+
if (combinedLines.length < 3) {
|
|
193
|
+
allow(undefined); // malformed/short output — can't determine root, fail open
|
|
194
|
+
}
|
|
195
|
+
const [gitDir, branch, wtTopRaw] = combinedLines;
|
|
196
|
+
|
|
177
197
|
// A linked worktree's --git-dir contains .git/worktrees/ as a path component
|
|
178
198
|
const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
|
|
179
199
|
if (!isLinkedWorktree) {
|
|
@@ -186,23 +206,14 @@ process.stdin.on('end', () => {
|
|
|
186
206
|
// created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
|
|
187
207
|
// on the user's own branch, so the guard must be a no-op there. Detached HEAD
|
|
188
208
|
// / error → not GSD-managed → no-op.
|
|
189
|
-
const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
|
|
190
|
-
reportIfUndetermined('gsd-worktree-path-guard', 'git symbolic-ref --short HEAD', branchResult);
|
|
191
|
-
const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
|
|
192
209
|
// #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
|
|
193
210
|
if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
|
|
194
211
|
allow(undefined); // not a GSD-managed executor worktree — no-op
|
|
195
212
|
}
|
|
196
213
|
|
|
197
|
-
//
|
|
214
|
+
// wtTopRaw: the raw --show-toplevel output for the worktree (cwd).
|
|
198
215
|
// We keep it raw (not path.resolve'd) to compare directly with the
|
|
199
216
|
// file's toplevel — same git binary, same format, no normalization needed.
|
|
200
|
-
const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
|
|
201
|
-
reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (worktree cwd)', wtTopResult);
|
|
202
|
-
if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
|
|
203
|
-
allow(undefined); // can't determine root — fail open
|
|
204
|
-
}
|
|
205
|
-
const wtTopRaw = wtTopResult.stdout.trim();
|
|
206
217
|
|
|
207
218
|
// #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
|
|
208
219
|
// non-string through — `[]` and `{}` are truthy, so they survived the
|