@opengsd/gsd-core 1.14.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-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- 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 +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- 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 +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- 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 +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- 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 +8 -0
- 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/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- 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/model-profiles.md +1 -1
- 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 +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- 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 +3 -3
- 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 +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- 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.md +3 -3
- 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 +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- 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/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 +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- 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 +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- 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 +2 -2
- 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 +2 -2
- 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 +7 -7
- 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 +1 -1
- 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 +24 -7
- 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 +113 -13
- 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 +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- 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 +4 -4
- 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 +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- 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 +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* state signal-resume Remove WAITING.json signal
|
|
20
20
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
21
21
|
* find-phase <phase> Find phase directory by number
|
|
22
|
+
* select-revert-commits (--phase P | --plan NN-MM) --range R
|
|
23
|
+
* List commits within R whose DECLARED conventional-commit
|
|
24
|
+
* scope identifies P or NN-MM (exact match; phase mode also
|
|
25
|
+
* accepts a NN-MM-scoped commit). Used by /gsd:undo (#4661).
|
|
22
26
|
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
|
23
27
|
* commit-docs-guard enable|disable Opt-in .git/hooks/pre-commit guard
|
|
24
28
|
* that refuses a commit staging
|
|
@@ -60,7 +64,7 @@
|
|
|
60
64
|
* roadmap update-plan-progress <N> Update progress table row from disk (PLAN vs SUMMARY counts)
|
|
61
65
|
* roadmap annotate-dependencies <N> Add wave dependency notes + cross-cutting constraints to ROADMAP.md
|
|
62
66
|
* roadmap validate Validate phase ID convention compliance
|
|
63
|
-
* roadmap upgrade [--apply] --convention milestone-prefixed Migrate phase IDs
|
|
67
|
+
* roadmap upgrade [--apply] --convention <milestone-prefixed|bracket> Migrate phase IDs (dry-run by default)
|
|
64
68
|
*
|
|
65
69
|
* Requirements Operations:
|
|
66
70
|
* requirements mark-complete <ids> Mark requirement IDs as complete in REQUIREMENTS.md
|
|
@@ -949,6 +953,219 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
949
953
|
if (!handled) phase.cmdFindPhase(cwd, args[1], raw);
|
|
950
954
|
}
|
|
951
955
|
|
|
956
|
+
/**
|
|
957
|
+
* #4906 Phase 5 (issue #4661, absorbed from the original bug report):
|
|
958
|
+
* `gsd-tools.cjs query select-revert-commits --phase <id>|--plan <id>
|
|
959
|
+
* --range <git-revision-range>`.
|
|
960
|
+
*
|
|
961
|
+
* Replaces the two `git log --oneline | grep -E ...` pipelines
|
|
962
|
+
* `gsd-core/workflows/undo.md` used to run for `/gsd:undo --phase`/`--plan`
|
|
963
|
+
* selection. `--range` is the ALREADY-COMPUTED `$UNDO_RANGE` the workflow's
|
|
964
|
+
* own phase-directory anchor logic derives — this subcommand changes only
|
|
965
|
+
* the subject-matching step, never the ranging behavior.
|
|
966
|
+
*
|
|
967
|
+
* Validates `--phase`/`--range <id>` via `validatePhaseNumber`
|
|
968
|
+
* (src/security.cts) BEFORE running any git command — an invalid id is
|
|
969
|
+
* refused here, never reaching git or the selection regex (closes bug
|
|
970
|
+
* class 1 of #4661 by construction). A `--plan NN-MM` id is two
|
|
971
|
+
* phase-number-shaped segments joined by the FIRST `-`: `validatePhaseNumber`'s
|
|
972
|
+
* bracket-style alternative requires a leading letter, so a plain numeric
|
|
973
|
+
* plan id never matches it whole — each segment is validated on its own
|
|
974
|
+
* instead of forking a second grammar.
|
|
975
|
+
*
|
|
976
|
+
* Selection itself is delegated to `selectCommitsByDeclaredScope`
|
|
977
|
+
* (src/undo-commit-selection.cts), which parses each commit subject
|
|
978
|
+
* through the SAME anchored conventional-commit header regex the
|
|
979
|
+
* changelog/PR-title gate uses and compares the declared scope to the
|
|
980
|
+
* validated id by exact string equality — no regex is ever built from
|
|
981
|
+
* user input.
|
|
982
|
+
*
|
|
983
|
+
* Output: JSON `{selected, classified}` by default; with `--raw`, one
|
|
984
|
+
* `<full-sha> <subject>` line per selected commit (preserves the visual
|
|
985
|
+
* shape `git log --oneline` produced, so the workflow's downstream
|
|
986
|
+
* confirm-screen display, >50 truncation count, and `git revert
|
|
987
|
+
* --no-commit` loop over COMMITS are unchanged).
|
|
988
|
+
*/
|
|
989
|
+
function routeSelectRevertCommits({ args, cwd, raw, error, invokingCwd }) {
|
|
990
|
+
// #4906/#4465 review fix: `cwd` here has already been remapped to the
|
|
991
|
+
// MAIN worktree root by `resolveMainWorktreeCwd` (main(), applied
|
|
992
|
+
// BLANKET to every command before dispatch — correct for a router that
|
|
993
|
+
// reads `.planning/`, e.g. `find-phase`, but wrong for this one). This
|
|
994
|
+
// command reads NO `.planning/` content — it only runs `git log` — and
|
|
995
|
+
// that git operation must see the CALLER's own worktree: its HEAD, its
|
|
996
|
+
// branch, its reachable commit range. Using the remapped `cwd` instead
|
|
997
|
+
// silently resolves `--range` against a DIFFERENT repository checkout
|
|
998
|
+
// (reproduced: from a linked worktree with no local `.planning/`, a
|
|
999
|
+
// `--range HEAD~3..HEAD` that succeeds when run directly in that
|
|
1000
|
+
// worktree fails with "ambiguous argument" through this command,
|
|
1001
|
+
// because it silently ran against main's shorter HEAD instead —
|
|
1002
|
+
// tests/undo-commit-selection-4465.test.cjs, "linked worktree: the main
|
|
1003
|
+
// worktree's planning is the same repository, and is not refused").
|
|
1004
|
+
// `invokingCwd` (threaded from main() through dispatchHostCommand) is
|
|
1005
|
+
// the pre-remap value; falling back to `cwd` keeps direct callers that
|
|
1006
|
+
// omit it (e.g. `dispatchHostCommand` invoked without the new field)
|
|
1007
|
+
// working exactly as before rather than crashing on `undefined`.
|
|
1008
|
+
const gitCwd = invokingCwd || cwd;
|
|
1009
|
+
const { validatePhaseNumber } = require('./lib/security.cjs');
|
|
1010
|
+
const { normalizePhaseName } = require('./lib/phase-id.cjs');
|
|
1011
|
+
const { selectCommitsByDeclaredScope } = require('./lib/undo-commit-selection.cjs');
|
|
1012
|
+
const { execGit } = require('./lib/shell-command-projection.cjs');
|
|
1013
|
+
|
|
1014
|
+
// #4906 Phase 5 review fix: the RETIRED phase-mode grep's `0*` prefix
|
|
1015
|
+
// (`\(0*${TARGET_PHASE}(-[0-9]+)?\):`) tolerated an UNPADDED user-supplied
|
|
1016
|
+
// phase number against a zero-padded commit scope — `--phase 3` matched
|
|
1017
|
+
// `feat(03-01): ...` because `0*3` matches the literal text `03`.
|
|
1018
|
+
// `selectCommitsByDeclaredScope` compares by exact string equality, and
|
|
1019
|
+
// `gsd-core/workflows/undo.md`'s parse_arguments step passes `TARGET_PHASE`
|
|
1020
|
+
// through with NO normalization, so without this, `--phase 3` would
|
|
1021
|
+
// silently select nothing against real (zero-padded) commit scopes — a
|
|
1022
|
+
// behavior regression, not just a documented known-limit. Zero-pad via
|
|
1023
|
+
// `normalizePhaseName` (src/phase-id.cts), the canonical owner of this
|
|
1024
|
+
// exact operation (confirmed: `normalizePhaseName('3') === '03'`).
|
|
1025
|
+
//
|
|
1026
|
+
// GUARDED to digit-first ids only: `normalizePhaseName` also runs
|
|
1027
|
+
// `stripProjectCodePrefix`, which strips a leading `LETTERS-`-shaped
|
|
1028
|
+
// prefix UNCONDITIONALLY — `normalizePhaseName('PROJ-42') === '42'`,
|
|
1029
|
+
// discarding the "PROJ-" entirely (confirmed by direct call). That is
|
|
1030
|
+
// exactly the shape `validatePhaseNumber`'s bracket-style alternative
|
|
1031
|
+
// legitimately accepts as a phase id in ITS OWN right (`PROJ-42`,
|
|
1032
|
+
// `AUTH-101` — see tests/security.test.cjs). Running a letter-first id
|
|
1033
|
+
// through it would silently widen the match (a wildcard-style
|
|
1034
|
+
// over-match reintroducing bug classes 1/2). The old grep's `0*` was
|
|
1035
|
+
// never meaningful for a letter-first target anyway — `0*` before a
|
|
1036
|
+
// non-'0' character matches zero repetitions, a no-op — so a
|
|
1037
|
+
// letter-first id needs neither padding nor this normalization.
|
|
1038
|
+
// `PROJECT_CODE_PREFIX_STRIP_RE_I` (`src/phase-id.cts:33`) itself
|
|
1039
|
+
// requires `^[A-Z]`, so it can never fire on a digit-first id — the
|
|
1040
|
+
// guard below is therefore exactly the safe/unsafe boundary.
|
|
1041
|
+
const normalizePaddedPhase = (id) => (/^\d/.test(id) ? normalizePhaseName(id) : id);
|
|
1042
|
+
|
|
1043
|
+
const parsed = parseNamedArgsOrExit(
|
|
1044
|
+
args,
|
|
1045
|
+
{ valueFlags: ['phase', 'plan', 'range'], positionals: 1 },
|
|
1046
|
+
error,
|
|
1047
|
+
);
|
|
1048
|
+
const rawPhase = parsed.phase;
|
|
1049
|
+
const rawPlan = parsed.plan;
|
|
1050
|
+
const range = parsed.range;
|
|
1051
|
+
|
|
1052
|
+
if ((rawPhase && rawPlan) || (!rawPhase && !rawPlan)) {
|
|
1053
|
+
error('select-revert-commits requires exactly one of --phase <id> or --plan <id>', ERROR_REASON.USAGE);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (!range) {
|
|
1057
|
+
error('select-revert-commits requires --range <git-revision-range>', ERROR_REASON.USAGE);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
// Belt-and-suspenders alongside the --end-of-options guard below (mirrors
|
|
1061
|
+
// src/git-base-branch.cts's isSafeRevisionRef posture for the same class
|
|
1062
|
+
// of argument): refuse an option-shaped range before it ever reaches git.
|
|
1063
|
+
if (typeof range !== 'string' || range.startsWith('-')) {
|
|
1064
|
+
error(`Invalid --range: ${JSON.stringify(range)}`, ERROR_REASON.USAGE);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
let mode;
|
|
1069
|
+
let normalizedId;
|
|
1070
|
+
if (rawPhase) {
|
|
1071
|
+
mode = 'phase';
|
|
1072
|
+
const check = validatePhaseNumber(rawPhase);
|
|
1073
|
+
if (!check.valid) {
|
|
1074
|
+
error(`Invalid --phase: ${check.error}`, ERROR_REASON.USAGE);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
normalizedId = normalizePaddedPhase(check.normalized);
|
|
1078
|
+
} else {
|
|
1079
|
+
mode = 'plan';
|
|
1080
|
+
// Split on the FIRST `-` only, giving exactly two segments (phase,
|
|
1081
|
+
// plan). This is deliberately NOT a general N-segment composite-id
|
|
1082
|
+
// parser: checked against docs/reference/plan-md.md (frontmatter
|
|
1083
|
+
// `plan` field + `.planning/phases/<NN>-<slug>/<NN>-<PP>-PLAN.md`
|
|
1084
|
+
// layout) and every `--plan` usage in gsd-core/workflows/*.md, the only
|
|
1085
|
+
// plan-id shape this repo documents or emits is the plain two-segment
|
|
1086
|
+
// `NN-MM`. A THREE-segment purely-numeric shape (`NN-MM-PP`) does exist
|
|
1087
|
+
// elsewhere in the codebase, but as a MILESTONE-phase-plan composite
|
|
1088
|
+
// under the (already-deprecated-forward) `milestone-prefixed`
|
|
1089
|
+
// `phase_id_convention` — a different id space than phase-plan, which
|
|
1090
|
+
// `docs/adr/612-bracket-phase-id-convention.md:15` itself documents as
|
|
1091
|
+
// having "no deterministic parse" once a token carries both a
|
|
1092
|
+
// milestone-joined phase AND a plan (the exact ambiguity that
|
|
1093
|
+
// motivated bracket's own `[PROJECT.MM] PP-PP` grammar). No function in
|
|
1094
|
+
// `src/phase-id.cts` validates the phase-PLAN grammar end-to-end
|
|
1095
|
+
// either: `parsePhaseId` explicitly REJECTS a bare `NN-MM` token by
|
|
1096
|
+
// design (its own doc comment lists `02-04` as a rejected "ambiguous /
|
|
1097
|
+
// bare token"), and `getPhaseDirFromPhaseId`'s N-segment dash grammar
|
|
1098
|
+
// is a MILESTONE-phase(-subphase) directory-name constructor — a
|
|
1099
|
+
// structurally similar but semantically different id space (first
|
|
1100
|
+
// segment = milestone, not phase) whose return value doesn't map back
|
|
1101
|
+
// to (phase, plan) anyway. Given no suitable whole-string validator
|
|
1102
|
+
// exists and no real usage needs more than two segments, a `NN-MM-PP`
|
|
1103
|
+
// id is refused today (see
|
|
1104
|
+
// tests/undo-commit-selection.test.cjs "refuses a 3-segment plan id"),
|
|
1105
|
+
// not silently mis-parsed — extending this to N segments is a product
|
|
1106
|
+
// decision for whichever future issue actually needs it. The same
|
|
1107
|
+
// "only the plain two-segment shape is supported" standard applies to
|
|
1108
|
+
// a bracket-style PHASE segment here too: `--plan PROJ-42-01` splits
|
|
1109
|
+
// on the first `-` into phase segment `PROJ` (no internal dash) and
|
|
1110
|
+
// plan segment `42-01`, and `validatePhaseNumber`'s bracket branch
|
|
1111
|
+
// requires the internal dash on ITS side (`PROJ-42`, not bare `PROJ`),
|
|
1112
|
+
// so the phase segment fails and the whole id is refused — bracket-style
|
|
1113
|
+
// ids are supported for `--phase` only, never as a `--plan` segment (no
|
|
1114
|
+
// real usage found; see tests/undo-commit-selection.test.cjs "bracket-
|
|
1115
|
+
// style phase segment in --plan is refused").
|
|
1116
|
+
const dashIdx = rawPlan.indexOf('-');
|
|
1117
|
+
if (dashIdx === -1) {
|
|
1118
|
+
error(`Invalid --plan: "${rawPlan}" (expected NN-MM)`, ERROR_REASON.USAGE);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const phasePart = rawPlan.slice(0, dashIdx);
|
|
1122
|
+
const planPart = rawPlan.slice(dashIdx + 1);
|
|
1123
|
+
const phaseCheck = validatePhaseNumber(phasePart);
|
|
1124
|
+
if (!phaseCheck.valid) {
|
|
1125
|
+
error(`Invalid --plan: "${rawPlan}" (phase segment: ${phaseCheck.error})`, ERROR_REASON.USAGE);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const planCheck = validatePhaseNumber(planPart);
|
|
1129
|
+
if (!planCheck.valid) {
|
|
1130
|
+
error(`Invalid --plan: "${rawPlan}" (plan segment: ${planCheck.error})`, ERROR_REASON.USAGE);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
normalizedId = `${normalizePaddedPhase(phaseCheck.normalized)}-${normalizePaddedPhase(planCheck.normalized)}`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const GIT_LOG_TIMEOUT_MS = 15000;
|
|
1137
|
+
const gitResult = execGit(
|
|
1138
|
+
['log', '--format=%H%x00%s', '--no-merges', '--no-decorate', '--end-of-options', range],
|
|
1139
|
+
{ cwd: gitCwd, timeout: GIT_LOG_TIMEOUT_MS },
|
|
1140
|
+
);
|
|
1141
|
+
if (gitResult.exitCode !== 0) {
|
|
1142
|
+
// `execGit` (src/shell-command-projection.cts) exposes `timedOut` on
|
|
1143
|
+
// its SpawnResultOutput — thread it through so a hung `git log` reads
|
|
1144
|
+
// distinctly from an ordinary non-zero exit with no stderr, which
|
|
1145
|
+
// otherwise both surfaced as the same unhelpful "(no stderr)" message.
|
|
1146
|
+
const gitFailureDetail = gitResult.timedOut
|
|
1147
|
+
? `git log timed out after ${GIT_LOG_TIMEOUT_MS}ms`
|
|
1148
|
+
: `git log failed: ${gitResult.stderr || '(no stderr)'}`;
|
|
1149
|
+
error(
|
|
1150
|
+
`select-revert-commits: ${gitFailureDetail} for range "${range}"`,
|
|
1151
|
+
ERROR_REASON.USAGE,
|
|
1152
|
+
);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const commits = (gitResult.stdout ? gitResult.stdout.split('\n') : [])
|
|
1157
|
+
.filter((line) => line.length > 0)
|
|
1158
|
+
.map((line) => {
|
|
1159
|
+
const nulIdx = line.indexOf('\0');
|
|
1160
|
+
return nulIdx === -1 ? null : { sha: line.slice(0, nulIdx), subject: line.slice(nulIdx + 1) };
|
|
1161
|
+
})
|
|
1162
|
+
.filter((c) => c !== null);
|
|
1163
|
+
|
|
1164
|
+
const result = selectCommitsByDeclaredScope(commits, normalizedId, mode);
|
|
1165
|
+
const rawLines = result.selected.map((c) => `${c.sha} ${c.subject}`).join('\n');
|
|
1166
|
+
output(result, raw, rawLines);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
952
1169
|
function routeCommit({ args, cwd, raw, error }) {
|
|
953
1170
|
const amend = args.includes('--amend');
|
|
954
1171
|
const noVerify = args.includes('--no-verify');
|
|
@@ -1183,7 +1400,18 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1183
1400
|
// First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
|
|
1184
1401
|
if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
|
|
1185
1402
|
}
|
|
1186
|
-
|
|
1403
|
+
// A retired runtime id now THROWS rather than resolving (#4709 AC#1).
|
|
1404
|
+
// Map it to the same clean single-line error routeSkillsRoot emits for
|
|
1405
|
+
// an unknown runtime — a CLI must not answer a bad flag value with a
|
|
1406
|
+
// stack trace. The thrown message already names the successor and the
|
|
1407
|
+
// retiring issue, so it is surfaced verbatim.
|
|
1408
|
+
let filename;
|
|
1409
|
+
try {
|
|
1410
|
+
filename = getProjectInstructionFile(pifRuntime);
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
if (err && err.code === 'GSD_RETIRED_RUNTIME') error(err.message);
|
|
1413
|
+
throw err;
|
|
1414
|
+
}
|
|
1187
1415
|
process.stdout.write(filename + '\n');
|
|
1188
1416
|
}
|
|
1189
1417
|
|
|
@@ -1252,9 +1480,21 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1252
1480
|
// claims. `--directory` wins outright when given explicitly;
|
|
1253
1481
|
// otherwise a supplied `--quick-id` + `--slug` pair derives the
|
|
1254
1482
|
// canonical permalink the same way `workflows/quick.md` renders it.
|
|
1483
|
+
// #4906 Phase 3 (#4958, evidence #4736): `--status` is the OPTIONAL
|
|
1484
|
+
// widening that closes the last gap between this CLI and
|
|
1485
|
+
// `workflows/quick.md`'s own Step 7c row shapes. `appendQuickTaskRow`
|
|
1486
|
+
// (markdown-table.cjs) has always accepted `status` — the schema's
|
|
1487
|
+
// `Status` column exists precisely for `$VALIDATE_MODE` runs — but
|
|
1488
|
+
// nothing on this CLI surface could set it, so quick.md's
|
|
1489
|
+
// VALIDATE_MODE branch had no escaping-safe path and kept
|
|
1490
|
+
// interpolating `${DESCRIPTION}` into raw markdown (#4736 D1: an
|
|
1491
|
+
// unescaped `|` in a task description permanently rags the table).
|
|
1492
|
+
// Omitted, `status` stays undefined and `appendQuickTaskRow` falls
|
|
1493
|
+
// back to its own `'—'` default — unchanged for every existing
|
|
1494
|
+
// caller (fast.md, quick.md's non-VALIDATE_MODE branch).
|
|
1255
1495
|
const qtaParsed = parseNamedArgsOrExit(
|
|
1256
1496
|
qtaArgs,
|
|
1257
|
-
{ valueFlags: ['task', 'quick-id', 'slug', 'directory'], positionals: 'rest' },
|
|
1497
|
+
{ valueFlags: ['task', 'quick-id', 'slug', 'directory', 'status'], positionals: 'rest' },
|
|
1258
1498
|
error,
|
|
1259
1499
|
);
|
|
1260
1500
|
const qtaTask = qtaParsed.task || args[1];
|
|
@@ -1265,6 +1505,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1265
1505
|
const qtaSlug = qtaParsed['slug'] || undefined;
|
|
1266
1506
|
const qtaDirectory = qtaParsed['directory']
|
|
1267
1507
|
|| (qtaQuickId && qtaSlug ? `[${qtaQuickId}-${qtaSlug}](./quick/${qtaQuickId}-${qtaSlug}/)` : undefined);
|
|
1508
|
+
const qtaStatus = qtaParsed['status'] || undefined;
|
|
1268
1509
|
|
|
1269
1510
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
1270
1511
|
if (!fs.existsSync(statePath)) {
|
|
@@ -1302,6 +1543,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
1302
1543
|
description: qtaTask,
|
|
1303
1544
|
date,
|
|
1304
1545
|
commit,
|
|
1546
|
+
status: qtaStatus,
|
|
1305
1547
|
quickId: qtaQuickId,
|
|
1306
1548
|
directory: qtaDirectory,
|
|
1307
1549
|
});
|
|
@@ -3006,6 +3248,12 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3006
3248
|
worktreeSafety.cmdWorktreeRecordAgent(cwd, args.slice(2));
|
|
3007
3249
|
} else if (subcommand === 'reap-orphans') {
|
|
3008
3250
|
worktreeSafety.cmdWorktreeReapOrphans(cwd);
|
|
3251
|
+
} else if (subcommand === 'worker-record') {
|
|
3252
|
+
worktreeSafety.cmdWorktreeWorkerRecord(cwd, args.slice(2));
|
|
3253
|
+
} else if (subcommand === 'worker-status') {
|
|
3254
|
+
worktreeSafety.cmdWorktreeWorkerStatus(cwd, args.slice(2));
|
|
3255
|
+
} else if (subcommand === 'worker-complete') {
|
|
3256
|
+
worktreeSafety.cmdWorktreeWorkerComplete(cwd, args.slice(2));
|
|
3009
3257
|
} else if (subcommand === 'base-check') {
|
|
3010
3258
|
require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
|
|
3011
3259
|
} else if (subcommand === 'set-baseref') {
|
|
@@ -3013,7 +3261,7 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
|
|
3013
3261
|
} else if (subcommand === 'create') {
|
|
3014
3262
|
worktreeSafety.cmdWorktreeCreate(cwd, args.slice(2));
|
|
3015
3263
|
} else {
|
|
3016
|
-
error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3264
|
+
error('Unknown worktree subcommand. Available: cleanup-wave, record-agent, reap-orphans, base-check, set-baseref, create, worker-record, worker-status, worker-complete', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
3017
3265
|
}
|
|
3018
3266
|
}
|
|
3019
3267
|
|
|
@@ -4407,6 +4655,8 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4407
4655
|
'smart-entry': routeSmartEntry,
|
|
4408
4656
|
'check': routeCheck,
|
|
4409
4657
|
'find-phase': routeFindPhase,
|
|
4658
|
+
// #4906 Phase 5 (#4661): commit-scope selection for /gsd:undo --phase/--plan.
|
|
4659
|
+
'select-revert-commits': routeSelectRevertCommits,
|
|
4410
4660
|
'commit': routeCommit,
|
|
4411
4661
|
'check-commit': routeCheckCommit,
|
|
4412
4662
|
'commit-docs-guard': routeCommitDocsGuard,
|
|
@@ -4484,7 +4734,7 @@ const HOST_COMMAND_ROUTERS = {
|
|
|
4484
4734
|
// through. Prototype-pollution-safe: own-property lookup rejects
|
|
4485
4735
|
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
|
4486
4736
|
// dispatchCapabilityCommand).
|
|
4487
|
-
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext }) {
|
|
4737
|
+
async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd }) {
|
|
4488
4738
|
if (
|
|
4489
4739
|
command === '__proto__' ||
|
|
4490
4740
|
command === 'constructor' ||
|
|
@@ -4499,7 +4749,13 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
4499
4749
|
if (typeof router !== 'function') return false;
|
|
4500
4750
|
// `await` so async host routers (e.g. capability's install/upgrade ops)
|
|
4501
4751
|
// complete before runCommand returns; sync routers pass through unchanged.
|
|
4502
|
-
|
|
4752
|
+
// `invokingCwd` (#4906/#4465): the pre-worktree-remap cwd, for the rare
|
|
4753
|
+
// router (currently only `select-revert-commits`) whose own git operation
|
|
4754
|
+
// must see the INVOKING worktree's HEAD, never the main-worktree remap
|
|
4755
|
+
// every other router legitimately wants via `cwd`. Optional — callers/tests
|
|
4756
|
+
// that omit it (every existing one) leave routers that don't read it
|
|
4757
|
+
// byte-unaffected.
|
|
4758
|
+
await router({ args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd });
|
|
4503
4759
|
return true; // consumed — don't emit "Unknown command"
|
|
4504
4760
|
}
|
|
4505
4761
|
|
|
@@ -4532,7 +4788,7 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
|
|
4532
4788
|
// keep working. No shell is spawned (argv array) — no injection surface beyond
|
|
4533
4789
|
// the old `timeout … bash -c "$CMD"`.
|
|
4534
4790
|
function runWithTimeout(argv) {
|
|
4535
|
-
const { spawn } = require('node:child_process');
|
|
4791
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4536
4792
|
const os = require('node:os');
|
|
4537
4793
|
|
|
4538
4794
|
const USAGE = 'Usage: gsd_run run-with-timeout <seconds> [--] <command> [args...]';
|
|
@@ -4557,9 +4813,11 @@ function runWithTimeout(argv) {
|
|
|
4557
4813
|
|
|
4558
4814
|
const isWin = process.platform === 'win32';
|
|
4559
4815
|
// Detached (own process group) on POSIX so a timeout can reap the WHOLE tree —
|
|
4560
|
-
// a bare child.kill() misses grandchildren (e.g. a test runner's workers)
|
|
4561
|
-
//
|
|
4562
|
-
//
|
|
4816
|
+
// a bare child.kill() misses grandchildren (e.g. a test runner's workers).
|
|
4817
|
+
// Windows has no POSIX process groups and process.kill(-pid) is unsupported
|
|
4818
|
+
// there, so EVERY killTree attempt on Windows tree-kills via
|
|
4819
|
+
// `taskkill /PID <pid> /T /F` while the root is alive (see killTree) — by the
|
|
4820
|
+
// time the direct child exits, its descendants are already orphaned.
|
|
4563
4821
|
const detached = !isWin && secs > 0;
|
|
4564
4822
|
const spawnFailureCode = (err) =>
|
|
4565
4823
|
(err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
|
|
@@ -4583,9 +4841,26 @@ function runWithTimeout(argv) {
|
|
|
4583
4841
|
// cap's process-group kill (the wrapped child escapes reap → exit 124 never
|
|
4584
4842
|
// fires) and risks cmd.exe mis-parsing an arg like `-e "setTimeout(()=>{})"`.
|
|
4585
4843
|
// Only .cmd/.bat are the CVE-2024-27980 EINVAL cases that require mediation.
|
|
4844
|
+
// #4797: the mediation is NOT hand-rolled here — the private `/d /s /c <cmd>
|
|
4845
|
+
// ...args` copy broke on any shim path containing a space (Node quotes the
|
|
4846
|
+
// argv token; `/s` strips the FIRST and LAST quote of the /c string, so
|
|
4847
|
+
// cmd.exe took the pre-space fragment as the program). projectSpawnInvocation
|
|
4848
|
+
// (the declared single owner, #3411/#3617) wraps the WHOLE command line in
|
|
4849
|
+
// one extra quote pair with windowsVerbatimArguments — the shape that
|
|
4850
|
+
// survives spaces. One behavior delta, accepted: the seam DECLINES mediation
|
|
4851
|
+
// when the target or an arg carries CR/LF (the old block mediated anyway) —
|
|
4852
|
+
// the unmediated spawn of a .cmd then fails EINVAL, loud, at the catch below.
|
|
4586
4853
|
const winShim = isWin && /\.(cmd|bat)$/i.test(path.basename(cmd));
|
|
4587
|
-
|
|
4588
|
-
|
|
4854
|
+
let spawnCmd = cmd;
|
|
4855
|
+
let spawnArgs = cmdArgs;
|
|
4856
|
+
let spawnOpts;
|
|
4857
|
+
if (winShim) {
|
|
4858
|
+
const { projectSpawnInvocation } = require('./lib/shell-command-projection.cjs');
|
|
4859
|
+
const inv = projectSpawnInvocation(cmd, cmdArgs);
|
|
4860
|
+
spawnCmd = inv.command;
|
|
4861
|
+
spawnArgs = inv.args;
|
|
4862
|
+
if (inv.windowsVerbatimArguments) spawnOpts = { windowsVerbatimArguments: true };
|
|
4863
|
+
}
|
|
4589
4864
|
// Node's setTimeout delay is a 32-bit signed ms int; a larger value silently
|
|
4590
4865
|
// clamps to 1ms → a spurious immediate timeout. Cap the budget (~24.8 days).
|
|
4591
4866
|
const timerMs = Math.min(Math.round(secs * 1000), 2 ** 31 - 1);
|
|
@@ -4596,11 +4871,13 @@ function runWithTimeout(argv) {
|
|
|
4596
4871
|
return new Promise((resolve) => {
|
|
4597
4872
|
let child;
|
|
4598
4873
|
try {
|
|
4599
|
-
// #2667: on win32 `.cmd`/`.bat
|
|
4600
|
-
//
|
|
4601
|
-
//
|
|
4602
|
-
//
|
|
4603
|
-
|
|
4874
|
+
// #2667: on win32 `.cmd`/`.bat` shims, spawn cmd.exe with an explicit
|
|
4875
|
+
// argv ARRAY rather than the shim directly — preserves the array-only,
|
|
4876
|
+
// no-shell-string argv contract. #4797: the exact argv shape (quote
|
|
4877
|
+
// wrapping, verbatim arguments) is projected by projectSpawnInvocation —
|
|
4878
|
+
// see the block above. `detached` is always false on win32, so it never
|
|
4879
|
+
// co-occurs with the cmd.exe mediation.
|
|
4880
|
+
child = spawn(spawnCmd, spawnArgs, { stdio: 'inherit', detached, ...spawnOpts });
|
|
4604
4881
|
} catch (err) {
|
|
4605
4882
|
process.stderr.write(`run-with-timeout: ${cmd}: ${err && err.message ? err.message : 'failed to start'}\n`);
|
|
4606
4883
|
resolve(spawnFailureCode(err));
|
|
@@ -4612,6 +4889,27 @@ function runWithTimeout(argv) {
|
|
|
4612
4889
|
if (detached && child.pid) {
|
|
4613
4890
|
try { process.kill(-child.pid, signal); return; } catch { /* group already gone */ }
|
|
4614
4891
|
}
|
|
4892
|
+
if (isWin && child.pid) {
|
|
4893
|
+
// #4601: Windows has no POSIX process groups, so the tree kill rides
|
|
4894
|
+
// on `taskkill /T`, which walks the child's descendants the way
|
|
4895
|
+
// `process.kill(-pid)` reaches a POSIX group — this is what bounds
|
|
4896
|
+
// the wall clock when the direct child mediates (cmd.exe /c shim) or
|
|
4897
|
+
// spawns its own children. Deliberately NOT gated on the SIGKILL
|
|
4898
|
+
// stage: child.kill on Windows is TerminateProcess regardless of
|
|
4899
|
+
// signal, so by the time the direct child exits its descendants are
|
|
4900
|
+
// orphaned and no taskkill can reach them — the tree kill must ride
|
|
4901
|
+
// the FIRST attempt, while the root is still alive. /F is required:
|
|
4902
|
+
// without it taskkill posts WM_CLOSE, which a headless CLI never
|
|
4903
|
+
// pumps. Spawned as an argv array per the no-shell-for-argv-array
|
|
4904
|
+
// contract, and bounded — a non-zero/absent status means taskkill
|
|
4905
|
+
// lost a race with an exiting process, and we fall through to the
|
|
4906
|
+
// direct kill so the attempt is never weaker than before.
|
|
4907
|
+
const reap = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
4908
|
+
encoding: 'utf8',
|
|
4909
|
+
timeout: 15000, // taskkill /T is sub-second in practice; bounded so a wedged taskkill can't hang the gate
|
|
4910
|
+
});
|
|
4911
|
+
if (reap.status === 0) return;
|
|
4912
|
+
}
|
|
4615
4913
|
child.kill(signal);
|
|
4616
4914
|
} catch { /* already exited */ }
|
|
4617
4915
|
};
|
|
@@ -4694,7 +4992,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
|
|
|
4694
4992
|
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, planning, profile-questionnaire, ' +
|
|
4695
4993
|
'profile-sample, progress, project-instruction-file, prompt-budget, quick-batch, quick-tasks-append, quick-tasks-migrate, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, runtime-identity, scaffold, smart-entry, state, ' +
|
|
4696
4994
|
'config-set-model-profile, dispatch-capacity, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
|
|
4697
|
-
'resolve-execution, review-lane, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
4995
|
+
'resolve-execution, review-lane, select-revert-commits, skill-manifest, skills-root, stamp-codebase-map, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
|
|
4698
4996
|
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
|
4699
4997
|
'Global flags:\n' +
|
|
4700
4998
|
' --raw Emit raw output without post-processing\n' +
|
|
@@ -4916,10 +5214,25 @@ async function main() {
|
|
|
4916
5214
|
cwd = resolvedProjectDir;
|
|
4917
5215
|
projectDirExplicit = true;
|
|
4918
5216
|
}
|
|
5217
|
+
// #4894: verification derives its root from a phase directory, not `cwd`, so
|
|
5218
|
+
// hand it the validated explicit root. Always (re)set — `null` when the flag is
|
|
5219
|
+
// absent — so a prior in-process main() call can never leak an override.
|
|
5220
|
+
projectRoot.setExplicitProjectRoot(projectDirExplicit ? cwd : null);
|
|
4919
5221
|
|
|
4920
5222
|
// Resolve worktree root: in a linked worktree, .planning/ lives in the main worktree.
|
|
4921
5223
|
// However, in monorepo worktrees where the subdirectory itself owns .planning/,
|
|
4922
5224
|
// skip worktree resolution — the CWD is already the correct project root.
|
|
5225
|
+
//
|
|
5226
|
+
// #4906/#4465: this remap is a BLANKET rewrite applied to every command, before
|
|
5227
|
+
// `command` is even known — most commands need it (they read `.planning/`, which
|
|
5228
|
+
// in a linked worktree lives only in the main one). A command whose own git
|
|
5229
|
+
// operation must run against the INVOKING worktree's OWN HEAD/branch (never the
|
|
5230
|
+
// main worktree's) needs the PRE-remap value instead — preserved here so it can be
|
|
5231
|
+
// threaded through dispatchHostCommand as `invokingCwd` (see routeSelectRevertCommits,
|
|
5232
|
+
// the first consumer: its `git log` must see the caller's own reachable history, not
|
|
5233
|
+
// main's, or a linked-worktree revert range silently resolves against the wrong repo
|
|
5234
|
+
// state — reproduced in tests/undo-commit-selection-4465.test.cjs's linked-worktree case).
|
|
5235
|
+
const preWorktreeRemapCwd = cwd;
|
|
4923
5236
|
cwd = resolveMainWorktreeCwd(cwd);
|
|
4924
5237
|
|
|
4925
5238
|
// Optional workstream override for parallel milestone work.
|
|
@@ -5045,7 +5358,7 @@ async function main() {
|
|
|
5045
5358
|
// false "output was not JSON" (negative space N8).
|
|
5046
5359
|
if (pickField) {
|
|
5047
5360
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
5048
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
5361
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5049
5362
|
});
|
|
5050
5363
|
const resolved = resolveAtFileOutput(captured);
|
|
5051
5364
|
let obj;
|
|
@@ -5077,7 +5390,7 @@ async function main() {
|
|
|
5077
5390
|
// every workflow to have a bash-specific `if [[ "$INIT" == @file:* ]]` check
|
|
5078
5391
|
// that breaks on PowerShell and other non-bash shells.
|
|
5079
5392
|
const captured = await captureStdoutSyncWrites(async () => {
|
|
5080
|
-
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext);
|
|
5393
|
+
await runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext, preWorktreeRemapCwd);
|
|
5081
5394
|
});
|
|
5082
5395
|
fs.writeSync(1, resolveAtFileOutput(captured));
|
|
5083
5396
|
}
|
|
@@ -5193,7 +5506,7 @@ function extractField(obj, fieldPath) {
|
|
|
5193
5506
|
return { found: true, value: current };
|
|
5194
5507
|
}
|
|
5195
5508
|
|
|
5196
|
-
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
|
|
5509
|
+
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null, invokingCwd) {
|
|
5197
5510
|
switch (command) {
|
|
5198
5511
|
|
|
5199
5512
|
default: {
|
|
@@ -5214,7 +5527,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
5214
5527
|
// commands (state, …) routed via their `route*Command` router instead of
|
|
5215
5528
|
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
|
5216
5529
|
// before the unknown-command error.
|
|
5217
|
-
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext })) break;
|
|
5530
|
+
if (await dispatchHostCommand({ command, args, cwd, raw, error, defaultValue, workstreamContext, invokingCwd })) break;
|
|
5218
5531
|
|
|
5219
5532
|
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
|
5220
5533
|
// above split it so `command` here is the head ("foo"). Use
|
|
@@ -52,6 +52,7 @@ const CANONICAL_HEADERS = {
|
|
|
52
52
|
'recommendation',
|
|
53
53
|
'strategy',
|
|
54
54
|
'decision outcome',
|
|
55
|
+
'locked decisions',
|
|
55
56
|
],
|
|
56
57
|
considered_options: [
|
|
57
58
|
'considered options',
|
|
@@ -190,7 +191,8 @@ function normalizeAdrHeader(raw) {
|
|
|
190
191
|
.toLowerCase()
|
|
191
192
|
.replace(/[\s:._-]+/g, ' ')
|
|
192
193
|
.replace(/[^\w\s]/g, '')
|
|
193
|
-
.trim()
|
|
194
|
+
.trim()
|
|
195
|
+
.replace(/^\d+(?:\s+\d+)*\s+/, '');
|
|
194
196
|
}
|
|
195
197
|
// Normalized synonym index (audit M7). classifyHeader receives an ALREADY-normalized
|
|
196
198
|
// header (via normalizeAdrHeader), but historically compared it against the RAW synonym
|