@opengsd/gsd-core 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +44 -26
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +17 -11
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -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 +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +13 -20
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- 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 +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +53 -13
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +162 -46
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +162 -46
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +471 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +488 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -23,21 +23,47 @@ const clock_cjs_1 = require("./clock.cjs");
|
|
|
23
23
|
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
24
24
|
const write_set_cjs_1 = require("./write-set.cjs");
|
|
25
25
|
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
26
|
+
const security_cjs_1 = require("./security.cjs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- audit.cjs is an export= CommonJS module
|
|
28
|
+
const auditMod = require("./audit.cjs");
|
|
29
|
+
const { resolveQuickTaskSummaryFile } = auditMod;
|
|
26
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
31
|
const ioMod = require("./io.cjs");
|
|
28
32
|
const { output, error } = ioMod;
|
|
29
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const cliExitMod = require("./cli-exit.cjs");
|
|
35
|
+
const { ExitError } = cliExitMod;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const stateContract = require("./state-contract.cjs");
|
|
38
|
+
const { publishStateContract } = stateContract;
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
40
|
const phaseIdMod = require("./phase-id.cjs");
|
|
31
|
-
const {
|
|
41
|
+
const { normalizePhaseName, matchPhaseDirs, PHASE_NUMBER_TOKEN_SOURCE, isSentinelPhaseId, isSentinelPhaseDir } = phaseIdMod;
|
|
42
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
32
43
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
44
|
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
34
|
-
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo } = roadmapParserMod;
|
|
45
|
+
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo, sliceMilestoneWindow, } = roadmapParserMod;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
48
|
+
const { SCOPE } = planningScopeMod;
|
|
35
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
50
|
const coreUtilsMod = require("./core-utils.cjs");
|
|
37
|
-
const { extractOneLinerFromBody } = coreUtilsMod;
|
|
51
|
+
const { extractOneLinerFromBody, countMatchedSummaries } = coreUtilsMod;
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
53
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
54
|
+
const { scanPhasePlans } = planScanMod;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
56
|
+
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
57
|
+
const { listMilestonePhaseDirs } = phaseLocatorMod;
|
|
38
58
|
const { planningPaths } = planningWorkspace;
|
|
39
59
|
const { extractFrontmatter } = frontmatterMod;
|
|
40
|
-
|
|
60
|
+
// ADR-3408 §8.3 / #3469: `writeStateMd` gets sync and NO preservation — the
|
|
61
|
+
// same #3374-shaped exposure the milestone-complete write used to carry (a
|
|
62
|
+
// stale body value silently clobbering fresher frontmatter, with no
|
|
63
|
+
// divergence signal). Routed through the single write-seam composition
|
|
64
|
+
// (`syncAndPreserveStateMd`) instead, under `withStateLock` — see
|
|
65
|
+
// `cmdMilestoneComplete`'s own STATE.md-update block for the full rationale.
|
|
66
|
+
const { syncAndPreserveStateMd, withStateLock, readModifyWriteStateMd } = stateMod;
|
|
41
67
|
// #2288 security: a milestone version label becomes a filesystem directory
|
|
42
68
|
// component (`milestones/<label>-phases/`) into which phase directories are
|
|
43
69
|
// MOVED. Any label used as a path segment must be a safe version token —
|
|
@@ -131,7 +157,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
131
157
|
// table_unmatched; this only adds the structured ADR-2143 shape on top).
|
|
132
158
|
const writeSet = [];
|
|
133
159
|
for (const reqId of reqIds) {
|
|
134
|
-
const reqEscaped = escapeRegex(reqId);
|
|
160
|
+
const reqEscaped = (0, pattern_cjs_1.escapeRegex)(reqId);
|
|
135
161
|
// Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
136
162
|
// Use replace() + compare to avoid the test()+replace() global regex
|
|
137
163
|
// lastIndex bug where test() advances state and replace() misses matches.
|
|
@@ -298,17 +324,20 @@ function cmdRequirementsReadyIds(cwd, args, raw) {
|
|
|
298
324
|
return;
|
|
299
325
|
}
|
|
300
326
|
const planAbsPath = node_path_1.default.resolve(cwd, planPathArg);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
327
|
+
// #3183: `planPathArg` may point at a root plan (`<phaseDir>/<n>-PLAN.md`)
|
|
328
|
+
// or a nested plan (`<phaseDir>/plans/PLAN-<n>.md`, #3139 layout) —
|
|
329
|
+
// scanPhasePlans always operates on the PHASE dir, so a nested plan needs
|
|
330
|
+
// one extra `dirname` to reach it, and its planFiles-relative identity
|
|
331
|
+
// carries the `plans/` prefix scanPhasePlans itself applies.
|
|
332
|
+
const isNestedPlanPath = node_path_1.default.basename(node_path_1.default.dirname(planAbsPath)) === 'plans';
|
|
333
|
+
const phaseDir = isNestedPlanPath ? node_path_1.default.dirname(node_path_1.default.dirname(planAbsPath)) : node_path_1.default.dirname(planAbsPath);
|
|
334
|
+
const currentRelative = isNestedPlanPath ? `plans/${node_path_1.default.basename(planAbsPath)}` : node_path_1.default.basename(planAbsPath);
|
|
335
|
+
// #3183: canonical plan/summary sets (root+nested, superseded-excluded)
|
|
336
|
+
// from the single owner, rather than a root-only hand-rolled readdirSync
|
|
337
|
+
// filter — a superseded sibling that still declares reqId with no SUMMARY
|
|
338
|
+
// used to block the ID forever (false-block); it is now excluded upstream.
|
|
339
|
+
const phaseScan = scanPhasePlans(phaseDir);
|
|
340
|
+
const siblingPlanFiles = phaseScan.planFiles.filter((f) => f !== currentRelative);
|
|
312
341
|
const parseFrontmatterReqIds = (content, sourcePath) => {
|
|
313
342
|
const fm = extractFrontmatter(content, sourcePath);
|
|
314
343
|
const fmReq = fm.requirements;
|
|
@@ -341,9 +370,11 @@ function cmdRequirementsReadyIds(cwd, args, raw) {
|
|
|
341
370
|
if (!siblingDeclaresId)
|
|
342
371
|
continue;
|
|
343
372
|
// Sibling declares the SAME ID — it must have finished (produced a
|
|
344
|
-
// SUMMARY) before this ID is ready to mark Complete.
|
|
345
|
-
|
|
346
|
-
|
|
373
|
+
// SUMMARY) before this ID is ready to mark Complete. Canonical pairing
|
|
374
|
+
// via countMatchedSummaries (root+nested, all three naming forms)
|
|
375
|
+
// instead of a bespoke -PLAN.md→-SUMMARY.md regex swap.
|
|
376
|
+
const siblingHasSummary = countMatchedSummaries([siblingFile], phaseScan.summaryFiles) > 0;
|
|
377
|
+
if (!siblingHasSummary) {
|
|
347
378
|
blockedBySibling = true;
|
|
348
379
|
break;
|
|
349
380
|
}
|
|
@@ -388,7 +419,7 @@ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
|
|
|
388
419
|
const reverted = [];
|
|
389
420
|
const unchanged = [];
|
|
390
421
|
for (const reqId of reqIds) {
|
|
391
|
-
const reqEscaped = escapeRegex(reqId);
|
|
422
|
+
const reqEscaped = (0, pattern_cjs_1.escapeRegex)(reqId);
|
|
392
423
|
let idReverted = false;
|
|
393
424
|
// Surface 1 — checkbox: - [x] **REQ-ID** -> - [ ] **REQ-ID**
|
|
394
425
|
const checkboxPattern = new RegExp(`(-\\s*\\[)x(\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
|
@@ -424,9 +455,35 @@ function cmdRequirementsRevertPhase(cwd, reqIdsRaw, raw) {
|
|
|
424
455
|
}
|
|
425
456
|
output({ reverted, unchanged, total: reqIds.length }, raw, `${reverted.length}/${reqIds.length} requirement(s) reverted from Complete`);
|
|
426
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* #2142 (code-review FIX 4): the single owned "should the Quick Tasks
|
|
460
|
+
* Completed table be reset, and is a reset failure worth a warning" decision
|
|
461
|
+
* — shared by `cmdMilestoneComplete` (which folds this into its own
|
|
462
|
+
* `withStateLock` transform, since it already holds that lock for the
|
|
463
|
+
* closure-transition write happening in the same block) and `cmdQuickArchive`
|
|
464
|
+
* (which routes through `readModifyWriteStateMd`'s own transform instead, per
|
|
465
|
+
* the lock-reentrancy note on that function). Only the WRITE mechanics
|
|
466
|
+
* differ between the two callers — the decision itself ("skip a
|
|
467
|
+
* `QUICK_TASKS_SECTION_ABSENT` result silently; surface any other failure")
|
|
468
|
+
* was previously duplicated verbatim at both call sites.
|
|
469
|
+
*
|
|
470
|
+
* Never throws: a reset failure degrades to returning `content` unchanged
|
|
471
|
+
* with a non-null `warning`, mirroring both callers' pre-existing
|
|
472
|
+
* "liberal but visible" posture.
|
|
473
|
+
*/
|
|
474
|
+
function applyQuickTasksReset(content) {
|
|
475
|
+
const resetResult = (0, markdown_table_cjs_1.resetQuickTaskRows)(content);
|
|
476
|
+
if (resetResult.ok) {
|
|
477
|
+
return { content: resetResult.value.content, warning: null };
|
|
478
|
+
}
|
|
479
|
+
if (resetResult.reason !== markdown_table_cjs_1.QUICK_TASKS_SECTION_ABSENT) {
|
|
480
|
+
return { content, warning: { field: 'quick_tasks_table', reason: resetResult.reason } };
|
|
481
|
+
}
|
|
482
|
+
return { content, warning: null };
|
|
483
|
+
}
|
|
427
484
|
function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
428
485
|
if (!version) {
|
|
429
|
-
error('version required for milestone complete (e.g., v1.0)');
|
|
486
|
+
error('version required for milestone complete (e.g., v1.0) — and --confirm to mutate');
|
|
430
487
|
}
|
|
431
488
|
// #2288 security: `version` is a CLI positional that is interpolated into
|
|
432
489
|
// multiple filesystem sinks below — `path.join(archiveDir, `${version}-ROADMAP.md`)`,
|
|
@@ -437,6 +494,23 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
437
494
|
if (!ARCHIVE_VERSION_LABEL_RE.test(version)) {
|
|
438
495
|
error(`milestone complete: version "${version}" is invalid — a milestone version label may contain only letters, digits, '.', '-' and '_', and must not contain path separators or "..".`);
|
|
439
496
|
}
|
|
497
|
+
// #3726: confirmation gate — refuse before ANY read of the tree beyond the
|
|
498
|
+
// arg checks above, so an unconfirmed invocation is a guaranteed no-op on
|
|
499
|
+
// disk. The threat model is the NEVER_VALID_FLAGS one (gsd-tools.cjs): a
|
|
500
|
+
// caller supplies a token it believes is inert and a destructive operation
|
|
501
|
+
// proceeds unchecked — here the caller-side belief was that the `query`
|
|
502
|
+
// meta-prefix implies a read, and `query milestone.complete <v>` archived
|
|
503
|
+
// the milestone with no confirmation. The prefix is an intentional
|
|
504
|
+
// invocation-compatibility mechanism, not a permission boundary, so the
|
|
505
|
+
// gate lives on the destructive command itself and covers every invocation
|
|
506
|
+
// path. --dry-run needs no confirmation (it mutates nothing and is the
|
|
507
|
+
// recommended first step); --force does NOT imply it (see
|
|
508
|
+
// MilestoneCompleteOptions.confirm).
|
|
509
|
+
if (!options.dryRun && !options.confirm) {
|
|
510
|
+
error(`milestone complete is irreversible: it archives ROADMAP.md and REQUIREMENTS.md, MOVES every phase ` +
|
|
511
|
+
`directory for ${version} into .planning/milestones/, and rewrites STATE.md. ` +
|
|
512
|
+
`Nothing has been changed. Re-run with --confirm to proceed, or --dry-run to preview exactly what would move.`);
|
|
513
|
+
}
|
|
440
514
|
const roadmapPath = planningPaths(cwd).roadmap;
|
|
441
515
|
const reqPath = planningPaths(cwd).requirements;
|
|
442
516
|
const statePath = planningPaths(cwd).state;
|
|
@@ -450,17 +524,56 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
450
524
|
const phasesDir = planningPaths(cwd).phases;
|
|
451
525
|
const today = clock_cjs_1.realClock.localToday();
|
|
452
526
|
const milestoneName = options.name || version;
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
527
|
+
// ADR-3408 §8.5 / #3469: "liberal but visible" — when the write-seam
|
|
528
|
+
// composition's preservation stage restores a curated frontmatter value
|
|
529
|
+
// over a disagreeing freshly-derived one, that divergence is surfaced
|
|
530
|
+
// here rather than silently absorbed (the direct answer to #3374's
|
|
531
|
+
// `warnings: []`). Structured (field + reason), not prose, so a caller can
|
|
532
|
+
// assert on the value rather than regex a rendered message.
|
|
533
|
+
//
|
|
534
|
+
// Named `preservation_warnings`, NOT `warnings`: `cmdPhaseComplete` already
|
|
535
|
+
// exposes a sibling field called `warnings` typed as prose `string[]`. Reusing
|
|
536
|
+
// that name here for a structured `{field, reason}[]` shape would be the
|
|
537
|
+
// "Generative Fix Divergence" anti-pattern — two sibling state commands
|
|
538
|
+
// sharing one field name with different element types. `warnings` stays
|
|
539
|
+
// one meaning (prose) repo-wide; this is a distinct, machine-assertable
|
|
540
|
+
// signal for ADR-3408 §8.5's "preservation is visible" rule. Check
|
|
541
|
+
// `preservation_warnings.length` rather than a companion `has_warnings`
|
|
542
|
+
// flag — that flag existed only to mirror `cmdPhaseComplete`'s channel,
|
|
543
|
+
// which this field intentionally does not claim to be.
|
|
544
|
+
const preservationWarnings = [];
|
|
457
545
|
// Scope stats and accomplishments to only the phases belonging to the
|
|
458
546
|
// current milestone's ROADMAP. Uses the shared filter from roadmap-parser.cjs
|
|
459
547
|
// (same logic used by cmdPhasesList and other callers).
|
|
548
|
+
// #3184 review finding: this scope computation + refusal MUST run BEFORE
|
|
549
|
+
// `platformEnsureDir(archiveDir)` below — a refused run (scope not COMPLETE,
|
|
550
|
+
// no --force) must be a true no-op on disk, and creating the archive
|
|
551
|
+
// directory first left an empty directory behind even on refusal.
|
|
460
552
|
const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
|
|
461
553
|
if (isDirInMilestone.missingExplicitVersion) {
|
|
462
554
|
error(`no phases found for milestone ${version} in ROADMAP.md`);
|
|
463
555
|
}
|
|
556
|
+
// #3184/#3166: `milestone complete` is the ONE-WAY-DOOR consumer of the
|
|
557
|
+
// milestone window (ROADMAP/REQUIREMENTS archived, phase directories
|
|
558
|
+
// MOVED). #3166 is specifically the TRUNCATED case: the milestone's
|
|
559
|
+
// heading IS found but its section closes before the phase region, and the
|
|
560
|
+
// phase filter degrades to pass-all (see getMilestonePhaseFilter above) —
|
|
561
|
+
// silently archiving every phase directory on disk. UNREADABLE (no
|
|
562
|
+
// ROADMAP.md at all) and UNSCOPED (no section for this version) are
|
|
563
|
+
// pre-existing, legitimately-handled states — `missingExplicitVersion`
|
|
564
|
+
// above already errors where that matters, and a missing ROADMAP.md has
|
|
565
|
+
// its own documented graceful path — so only TRUNCATED is refused here.
|
|
566
|
+
// The read-path consumers keep the pass-all degrade for every scope
|
|
567
|
+
// (ADR-3180 Decision 3's Rejected section: deny-all there would trade one
|
|
568
|
+
// silent wrong answer for another); this write path refuses on TRUNCATED
|
|
569
|
+
// alone, positioned before `platformEnsureDir` so a refusal stays a no-op
|
|
570
|
+
// on disk.
|
|
571
|
+
if (isDirInMilestone.scope === SCOPE.TRUNCATED && !options.force) {
|
|
572
|
+
error(`Cannot mark milestone complete: the ROADMAP window for "${version}" is truncated ` +
|
|
573
|
+
`(the milestone heading was found but its section ends before reaching any phase ` +
|
|
574
|
+
`entries, even though the ROADMAP has phase entries elsewhere), so phase scoping ` +
|
|
575
|
+
`cannot be trusted for this destructive operation. Re-run with --force to override.`);
|
|
576
|
+
}
|
|
464
577
|
// Guard: prevent marking complete when ROADMAP still lists phases that have
|
|
465
578
|
// no directory on disk (disk_status: no_directory). This catches the case
|
|
466
579
|
// where the active milestone was erroneously marked complete before phases
|
|
@@ -514,7 +627,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
514
627
|
`running the unstarted-phase guard against the ROADMAP scoped for "${version}" anyway.\n`);
|
|
515
628
|
}
|
|
516
629
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
517
|
-
|
|
630
|
+
// #3184/#2946: scope the unstarted-phase guard to the same `version`
|
|
631
|
+
// window `getMilestonePhaseFilter` used above, NOT to
|
|
632
|
+
// extractCurrentMilestone's own STATE.md-derived window — those two
|
|
633
|
+
// can disagree (that disagreement is exactly what the WARNING above
|
|
634
|
+
// detects), and scoping this guard to the wrong window under-detects
|
|
635
|
+
// unstarted phases on the destructive completion path. Calls the same
|
|
636
|
+
// sliceMilestoneWindow owner getMilestonePhaseFilter's versionOverride
|
|
637
|
+
// branch calls (a prior pass here re-composed locate+select+section-end
|
|
638
|
+
// locally, which review caught as a second, disagreeing derivation of
|
|
639
|
+
// the same window — ADR-3180 Decision 4(c)); falls back to
|
|
640
|
+
// extractCurrentMilestone's whole-document result only for the
|
|
641
|
+
// free-form (no versioned milestones anywhere) shape, where both
|
|
642
|
+
// windows converge to the same value regardless of which version drove
|
|
643
|
+
// the lookup.
|
|
644
|
+
const scopedContent = sliceMilestoneWindow(roadmapContent, version) ?? extractCurrentMilestone(roadmapContent, cwd);
|
|
518
645
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
519
646
|
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
520
647
|
const noDirectoryPhases = [];
|
|
@@ -537,15 +664,15 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
537
664
|
// milestone completion. Mirrors the engine-wide sentinel convention
|
|
538
665
|
// (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
|
|
539
666
|
// the #1445 /^999/ progress filters). (#1580)
|
|
540
|
-
|
|
541
|
-
if (
|
|
667
|
+
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this local check already covered both 0 and 999; now delegates to the single canonical owner.
|
|
668
|
+
if (isSentinelPhaseId(phaseNum))
|
|
542
669
|
continue;
|
|
543
670
|
const normalized = normalizePhaseName(phaseNum);
|
|
544
671
|
// A phase has disk_status: 'no_directory' when no phase directory
|
|
545
|
-
// with a matching token exists on disk. Use the same
|
|
546
|
-
//
|
|
547
|
-
// (2.1) and letter-suffix (12A) phase IDs.
|
|
548
|
-
const hasDirectory = phaseDirEntries
|
|
672
|
+
// with a matching token exists on disk. Use the same matchPhaseDirs
|
|
673
|
+
// owner that roadmap.analyze uses to avoid false positives on decimal
|
|
674
|
+
// (2.1) and letter-suffix (12A) phase IDs. (#2528)
|
|
675
|
+
const hasDirectory = matchPhaseDirs(phaseDirEntries, normalized).matches.length > 0;
|
|
549
676
|
if (!hasDirectory) {
|
|
550
677
|
noDirectoryPhases.push(phaseNum);
|
|
551
678
|
}
|
|
@@ -556,6 +683,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
556
683
|
}
|
|
557
684
|
}
|
|
558
685
|
catch (e) {
|
|
686
|
+
// ADR-3889: error() now throws ExitError (carries no message) instead
|
|
687
|
+
// of calling process.exit() directly, so it can no longer be detected
|
|
688
|
+
// by sniffing e.message — an ExitError from our own guard above must be
|
|
689
|
+
// re-thrown UNCONDITIONALLY, before any message inspection, or the
|
|
690
|
+
// guard silently stops blocking milestone completion.
|
|
691
|
+
if (e instanceof ExitError)
|
|
692
|
+
throw e;
|
|
559
693
|
// If the error came from our guard, re-throw it; otherwise skip silently.
|
|
560
694
|
const message = e instanceof Error ? e.message : String(e);
|
|
561
695
|
if (message && message.startsWith('Cannot mark milestone complete:'))
|
|
@@ -568,20 +702,29 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
568
702
|
let totalPlans = 0;
|
|
569
703
|
let totalTasks = 0;
|
|
570
704
|
const accomplishments = [];
|
|
705
|
+
// #3597 (ADR-3180 Decision 2): SINGLE resolution of "which phase
|
|
706
|
+
// directories belong to the current milestone" AND the SCOPE discriminator
|
|
707
|
+
// that resolution came from — shared verbatim by the read-only stats loop
|
|
708
|
+
// immediately below, the --dry-run preview, and the real archive pass, so
|
|
709
|
+
// none of the three can ever disagree. `listMilestonePhaseDirs` never
|
|
710
|
+
// throws (its own doc comment), so this is safe to call unguarded ahead of
|
|
711
|
+
// the try/catch that scopes the stats roll-up below.
|
|
712
|
+
//
|
|
713
|
+
// The stats loop's own ENUMERATION behavior is intentionally left
|
|
714
|
+
// unaffected by a non-COMPLETE scope — it reports what is actually on
|
|
715
|
+
// disk, same as before #3597. Only the destructive archive pass (and its
|
|
716
|
+
// --dry-run preview) refuses to act on a non-COMPLETE (non-answer) scope;
|
|
717
|
+
// see the guard built from `milestonePhaseScope` further down.
|
|
718
|
+
const { value: milestonePhaseDirs, scope: milestonePhaseScope } = listMilestonePhaseDirs(phasesDir, { cwd, versionOverride: version });
|
|
571
719
|
try {
|
|
572
|
-
const
|
|
573
|
-
const dirs = entries
|
|
574
|
-
.filter((e) => e.isDirectory())
|
|
575
|
-
.map((e) => e.name)
|
|
576
|
-
.sort();
|
|
577
|
-
for (const dir of dirs) {
|
|
578
|
-
if (!isDirInMilestone(dir))
|
|
579
|
-
continue;
|
|
720
|
+
for (const dir of milestonePhaseDirs) {
|
|
580
721
|
phaseCount++;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
722
|
+
// #3183: canonical plan/summary sets (root+nested, superseded-excluded)
|
|
723
|
+
// from the single owner, rather than a root-only hand-rolled readdirSync
|
|
724
|
+
// filter.
|
|
725
|
+
const phaseScan = scanPhasePlans(node_path_1.default.join(phasesDir, dir));
|
|
726
|
+
const summaries = phaseScan.summaryFiles;
|
|
727
|
+
totalPlans += phaseScan.planCount;
|
|
585
728
|
// Extract one-liners from summaries
|
|
586
729
|
for (const s of summaries) {
|
|
587
730
|
try {
|
|
@@ -621,21 +764,49 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
621
764
|
* were created). Degrades stats to phaseCount/totalPlans/totalTasks=0,
|
|
622
765
|
* accomplishments=[] rather than crash `milestone complete`. */
|
|
623
766
|
}
|
|
767
|
+
// #3597 (ADR-3180 Decision 2): `SCOPE.UNREADABLE` is the ONE classification
|
|
768
|
+
// where `getMilestonePhaseFilter` throws (no workstream ROADMAP of its
|
|
769
|
+
// own), leaves `milestonePhaseNums` empty, and the window degrades to a
|
|
770
|
+
// pass-all fallback — that fallback is what silently WIDENS the archive
|
|
771
|
+
// set past the single-derivation guarantee this block exists to protect
|
|
772
|
+
// (confirmed empirically: a workstream with phase dirs but no workstream
|
|
773
|
+
// ROADMAP.md reports UNREADABLE and used to enumerate every directory on
|
|
774
|
+
// disk). `SCOPE.TRUNCATED` already refuses the WHOLE command above.
|
|
775
|
+
// `SCOPE.UNSCOPED` is a DIFFERENT, pre-existing classification — e.g. a
|
|
776
|
+
// root project with no milestone asserted in STATE.md — whose
|
|
777
|
+
// `listMilestonePhaseDirs` resolution is a real (non-degraded) answer, and
|
|
778
|
+
// its rollover archive behavior predates this branch and must not change.
|
|
779
|
+
// The guard therefore refuses ONLY on UNREADABLE, not on "not COMPLETE" —
|
|
780
|
+
// widening to every non-COMPLETE scope was itself a regression (a root
|
|
781
|
+
// project with no active workstream resolves UNSCOPED, and refusing to
|
|
782
|
+
// archive there broke the ordinary `milestone complete` -> `phases clear`
|
|
783
|
+
// rollover). Computed once here from the single
|
|
784
|
+
// `milestonePhaseDirs`/`milestonePhaseScope` resolution above, so the
|
|
785
|
+
// --dry-run preview below and the real archive pass further down can never
|
|
786
|
+
// disagree about whether (or what) to archive.
|
|
787
|
+
const phasesArchiveSkippedForScope = options.archivePhases !== false && milestonePhaseScope === SCOPE.UNREADABLE;
|
|
788
|
+
const phasesArchiveSkipReason = phasesArchiveSkippedForScope
|
|
789
|
+
? `milestone window scope is "${milestonePhaseScope}" — refusing to archive phase directories until the window can be resolved (ADR-3180)`
|
|
790
|
+
: null;
|
|
624
791
|
// #2118: --dry-run preview — compute what WOULD happen without mutating.
|
|
625
792
|
// The stats above are read-only; all mutations start at the archive section below.
|
|
626
793
|
if (options.dryRun) {
|
|
627
794
|
const phaseDirsToArchive = [];
|
|
628
|
-
if (options.archivePhases !== false) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
catch { /* phasesDir missing — nothing to archive */ }
|
|
795
|
+
if (options.archivePhases !== false && !phasesArchiveSkippedForScope) {
|
|
796
|
+
// #3185 (ADR-3180 Decision 1) / #3597: same single routed derivation as
|
|
797
|
+
// the stats loop above — the dry-run preview must list exactly what
|
|
798
|
+
// the real archive pass below would move, including refusing to list
|
|
799
|
+
// anything when the window scope is not COMPLETE.
|
|
800
|
+
phaseDirsToArchive.push(...milestonePhaseDirs);
|
|
638
801
|
}
|
|
802
|
+
// #2142 MAJOR 5 (review): dry-run preview of quick-task archival —
|
|
803
|
+
// read-only, routed through the SAME `listQuickTaskDirsForArchive`
|
|
804
|
+
// selection `archiveQuickTaskDirectories` uses for real (directory
|
|
805
|
+
// entries only, `requireSafePath`-guarded, sorted) so this preview can
|
|
806
|
+
// never disagree with what a real run actually archives. Absent
|
|
807
|
+
// --archive-quick this stays `[]` and nothing on disk is touched either
|
|
808
|
+
// way (dry-run always returns before any mutation below).
|
|
809
|
+
const quickDirsToArchive = options.archiveQuick ? listQuickTaskDirsForArchive(cwd) : [];
|
|
639
810
|
const dryRunResult = {
|
|
640
811
|
dry_run: true,
|
|
641
812
|
version,
|
|
@@ -653,6 +824,9 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
653
824
|
? { source: node_path_1.default.relative(cwd, node_path_1.default.join(planningBase, `${version}-MILESTONE-AUDIT.md`)).split(node_path_1.default.sep).join('/'), target: node_path_1.default.relative(cwd, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)).split(node_path_1.default.sep).join('/') }
|
|
654
825
|
: null,
|
|
655
826
|
phases: phaseDirsToArchive,
|
|
827
|
+
phases_archive_skipped: phasesArchiveSkippedForScope,
|
|
828
|
+
phases_archive_skip_reason: phasesArchiveSkipReason,
|
|
829
|
+
quick: quickDirsToArchive,
|
|
656
830
|
},
|
|
657
831
|
would_update: {
|
|
658
832
|
milestones_md: node_path_1.default.relative(cwd, milestonesPath).split(node_path_1.default.sep).join('/'),
|
|
@@ -662,6 +836,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
662
836
|
output(dryRunResult, raw);
|
|
663
837
|
return;
|
|
664
838
|
}
|
|
839
|
+
// Ensure archive directory exists. Deliberately placed AFTER the dry-run
|
|
840
|
+
// early return and every refusal/guard above (missingExplicitVersion, the
|
|
841
|
+
// scope refusal, the unstarted-phase guard) — #3184 review finding: this
|
|
842
|
+
// used to run before those checks, so a refused run still left an empty
|
|
843
|
+
// archive directory behind. Reaching this point means the run is
|
|
844
|
+
// committed to mutating.
|
|
845
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(archiveDir);
|
|
665
846
|
// Archive ROADMAP.md
|
|
666
847
|
if (node_fs_1.default.existsSync(roadmapPath)) {
|
|
667
848
|
const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
@@ -687,6 +868,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
687
868
|
// Create/append MILESTONES.md entry
|
|
688
869
|
const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
|
|
689
870
|
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
|
|
871
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract — the result
|
|
872
|
+
// below used to report `milestones_updated: true` hardcoded, never
|
|
873
|
+
// consulting whether the MILESTONES.md write actually changed anything.
|
|
874
|
+
// Captured before the write branches below so the after-comparison reports
|
|
875
|
+
// a real content diff instead of an assumed one.
|
|
876
|
+
const milestonesBefore = node_fs_1.default.existsSync(milestonesPath) ? node_fs_1.default.readFileSync(milestonesPath, 'utf-8') : null;
|
|
690
877
|
if (node_fs_1.default.existsSync(milestonesPath)) {
|
|
691
878
|
const existing = node_fs_1.default.readFileSync(milestonesPath, 'utf-8');
|
|
692
879
|
if (!existing.trim()) {
|
|
@@ -695,7 +882,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
695
882
|
}
|
|
696
883
|
else {
|
|
697
884
|
// Insert after the header line(s) for reverse chronological order (newest first)
|
|
698
|
-
|
|
885
|
+
// #3415: empirically verified linear-time up to 5MB adversarial input (worst-case
|
|
886
|
+
// no-newline-at-all forcing full [^\r\n]* backtrack: 0.11ms@10KB -> 6.9ms@5MB).
|
|
887
|
+
// Non-global, `^`-anchored (no /m) so this is a single match attempt at position 0
|
|
888
|
+
// only — never rescanned at every offset — with no nested repeated group, so it
|
|
889
|
+
// cannot exhibit the #2128-class catastrophic backtracking.
|
|
890
|
+
// eslint-disable-next-line local/no-unbounded-quantifier -- single ^-anchored non-global attempt at pos 0, measured linear to 5MB, no nested quantifier
|
|
891
|
+
const headerMatch = existing.match(/^(#{1,3}\s+[^\r\n]*\r?\n(?:\r?\n)?)/);
|
|
699
892
|
if (headerMatch) {
|
|
700
893
|
const header = headerMatch[1];
|
|
701
894
|
const rest = existing.slice(header.length);
|
|
@@ -710,26 +903,169 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
710
903
|
else {
|
|
711
904
|
(0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
|
|
712
905
|
}
|
|
906
|
+
// #3685: real content diff, not the hardcoded `true` this used to report —
|
|
907
|
+
// see the `milestonesBefore` capture above.
|
|
908
|
+
const milestonesAfter = node_fs_1.default.existsSync(milestonesPath) ? node_fs_1.default.readFileSync(milestonesPath, 'utf-8') : null;
|
|
909
|
+
const milestonesUpdated = milestonesAfter !== milestonesBefore;
|
|
910
|
+
// #2142 BLOCKER 2 (review): opt-in quick-task archival. This call MUST sit
|
|
911
|
+
// immediately adjacent to the STATE.md write block directly below it, with
|
|
912
|
+
// NO unguarded IO in between (unlike the ROADMAP/REQUIREMENTS/audit/
|
|
913
|
+
// MILESTONES.md writes above, none of which are wrapped in a try/catch).
|
|
914
|
+
// If the move ran earlier — e.g. right after `platformEnsureDir(archiveDir)`
|
|
915
|
+
// — and any one of those unguarded writes then threw, the quick-task
|
|
916
|
+
// directories would already be gone from `.planning/quick/` while the
|
|
917
|
+
// STATE.md Quick Tasks table reset (which lives inside `withStateLock`
|
|
918
|
+
// immediately below) would never be reached. That is precisely the
|
|
919
|
+
// STATE-vs-disk drift #2142 exists to eliminate: a table still describing
|
|
920
|
+
// directories that no longer exist. Keeping the move and the reset
|
|
921
|
+
// adjacent — separated only by this comment, never by IO that can throw —
|
|
922
|
+
// means either both happen or (if the move itself throws) neither does.
|
|
923
|
+
// `archiveQuick` is opt-in (default OFF); absent the flag this is `null`
|
|
924
|
+
// and every downstream read of it degrades to "no quick archival happened".
|
|
925
|
+
const quickArchiveResult = options.archiveQuick
|
|
926
|
+
? archiveQuickTaskDirectories(cwd, version)
|
|
927
|
+
: null;
|
|
713
928
|
// Update STATE.md — keep frontmatter/body semantically aligned after closure.
|
|
714
929
|
// ADR-1769 Phase 5: dispatches to the STATE.md Transition Module. The closure
|
|
715
930
|
// write (Status, Last Activity, Last Activity Description, Current Position
|
|
716
931
|
// reset, Operator Next Steps reset) is the pure `milestoneCompleteCore` in
|
|
717
932
|
// src/state-transition.cts, backed by the field-classification table. The
|
|
718
933
|
// runtime-specific next-milestone slash command is resolved here and injected
|
|
719
|
-
// via the intent so the core stays pure.
|
|
720
|
-
//
|
|
934
|
+
// via the intent so the core stays pure.
|
|
935
|
+
//
|
|
936
|
+
// ADR-3408 §8.3 / #3469: this used to write via `writeStateMd`, which gets
|
|
937
|
+
// sync and NO preservation — the identical shape #3374 reported for
|
|
938
|
+
// `phase.complete` (a stale body value silently clobbering fresher
|
|
939
|
+
// frontmatter). Routed through the single write-seam composition
|
|
940
|
+
// (`syncAndPreserveStateMd`) instead, under the same lock discipline
|
|
941
|
+
// `cmdPhaseComplete`'s atomic-commit adapter already uses: `withStateLock`
|
|
942
|
+
// wraps read + transform + sync + preserve + write so the read this
|
|
943
|
+
// transaction bases its transform on cannot be raced by a concurrent
|
|
944
|
+
// writer (closing a pre-existing TOCTOU gap `writeStateMd`'s own internal
|
|
945
|
+
// lock never covered, since the read used to happen before any lock was
|
|
946
|
+
// taken). `resync: true` mirrors `cmdPhaseComplete`'s posture (progress
|
|
947
|
+
// recomputed from disk; only the preserve-when-unchanged deltas apply) —
|
|
948
|
+
// milestone completion is the same kind of lifecycle transition.
|
|
949
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract — this used to
|
|
950
|
+
// report `state_updated: fs.existsSync(statePath)`, true even on a no-op
|
|
951
|
+
// transaction. Declared here beside `stateUpdated`'s sibling flags and
|
|
952
|
+
// defaulted to `false` so the "STATE.md absent" case keeps today's answer
|
|
953
|
+
// (existsSync also returns false there) reached via a real content
|
|
954
|
+
// comparison instead.
|
|
955
|
+
let stateUpdated = false;
|
|
721
956
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
957
|
+
withStateLock(statePath, () => {
|
|
958
|
+
const originalStateContent = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
959
|
+
const result = (0, state_transition_cjs_1.transitionCore)(originalStateContent, {
|
|
960
|
+
kind: 'milestoneComplete',
|
|
961
|
+
version,
|
|
962
|
+
nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
|
|
963
|
+
}, { clock: clock_cjs_1.realClock, sourcePath: statePath });
|
|
964
|
+
const divergedFields = [];
|
|
965
|
+
// #2111 (found by #3471 review): `milestoneCompleteCore` never declares
|
|
966
|
+
// `current_phase`/`current_phase_name` among the fields it touches — but
|
|
967
|
+
// its ## Current Position reset REWRITES the `Phase:` prose line to a
|
|
968
|
+
// closure message ("Milestone vX.Y complete"), which is not a number.
|
|
969
|
+
// That is an unavoidable side effect of the wholesale section reset
|
|
970
|
+
// `resetSectionVerbatim` performs, not an intent to change the phase.
|
|
971
|
+
// Downstream, `current_phase`/`current_phase_name` are
|
|
972
|
+
// `preserve-when-unchanged` rows: the #1230 delta heuristic sees the
|
|
973
|
+
// body source go from a real value to unparseable and — correctly, per
|
|
974
|
+
// ADR-3408 §8.5 Row 2 — lets the derived (empty) value win, discarding
|
|
975
|
+
// the curated phase entirely. §8.5 Row 2 governs a genuine mid-write
|
|
976
|
+
// body edit (e.g. `state.patch` deleting the Phase line); milestone
|
|
977
|
+
// closure is a different shape — the transition never intended to
|
|
978
|
+
// touch these fields at all. Re-assert them via `authoritativeFm` (the
|
|
979
|
+
// same #2736 intent-first mechanism `beginPhaseCore`/`completePhaseCore`
|
|
980
|
+
// already use to freeze a field the transition resolved out-of-band),
|
|
981
|
+
// so the closure-message side effect cannot clobber the last real
|
|
982
|
+
// phase. Scoped to non-empty strings only, mirroring #2736's own guard.
|
|
983
|
+
const authoritativeFm = {};
|
|
984
|
+
const preFm = extractFrontmatter(originalStateContent, statePath);
|
|
985
|
+
const preCurrentPhase = preFm['current_phase'];
|
|
986
|
+
const preCurrentPhaseName = preFm['current_phase_name'];
|
|
987
|
+
if (typeof preCurrentPhase === 'string' && preCurrentPhase.trim().length > 0) {
|
|
988
|
+
authoritativeFm['current_phase'] = preCurrentPhase;
|
|
989
|
+
}
|
|
990
|
+
if (typeof preCurrentPhaseName === 'string' && preCurrentPhaseName.trim().length > 0) {
|
|
991
|
+
authoritativeFm['current_phase_name'] = preCurrentPhaseName;
|
|
992
|
+
}
|
|
993
|
+
// #2142: fold the Quick Tasks table reset into this SAME
|
|
994
|
+
// `withStateLock` transform — no second lock acquisition, no second
|
|
995
|
+
// `syncAndPreserveStateMd`/`platformWriteSync` pass. Only applied when
|
|
996
|
+
// quick archival actually MOVED something (never when the flag was
|
|
997
|
+
// absent, and never for a mere dry-run preview, which never reaches
|
|
998
|
+
// here at all). A refused reset degrades to leaving the content
|
|
999
|
+
// untouched and never fails milestone completion, but the two refusal
|
|
1000
|
+
// shapes are NOT equally noteworthy (design doc §40, behavior table
|
|
1001
|
+
// row 5): an ABSENT "Quick Tasks Completed" section is the normal,
|
|
1002
|
+
// common case — the section is created lazily by
|
|
1003
|
+
// `gsd-core/workflows/quick.md` Step 7b, not by
|
|
1004
|
+
// `gsd-core/templates/state.md`, so most projects simply don't have
|
|
1005
|
+
// one — and is silently skipped (compared via the shared
|
|
1006
|
+
// `QUICK_TASKS_SECTION_ABSENT` sentinel, never by matching on the
|
|
1007
|
+
// free-form reason string). A section that EXISTS but couldn't be
|
|
1008
|
+
// reset (unparseable table, or columns matching neither registered
|
|
1009
|
+
// QuickTasks variant) is a genuine anomaly and IS surfaced via
|
|
1010
|
+
// `preservationWarnings`, the same "liberal but visible" posture the
|
|
1011
|
+
// rest of this block already uses for a disagreeing derived STATE.md
|
|
1012
|
+
// value.
|
|
1013
|
+
let quickTasksResetContent = result.content;
|
|
1014
|
+
if (quickArchiveResult && quickArchiveResult.archived > 0) {
|
|
1015
|
+
const { content: resetContent, warning } = applyQuickTasksReset(quickTasksResetContent);
|
|
1016
|
+
quickTasksResetContent = resetContent;
|
|
1017
|
+
if (warning)
|
|
1018
|
+
preservationWarnings.push(warning);
|
|
1019
|
+
}
|
|
1020
|
+
const finalContent = syncAndPreserveStateMd(originalStateContent, quickTasksResetContent, statePath, cwd, {
|
|
1021
|
+
resync: true,
|
|
1022
|
+
authoritativeFm: Object.keys(authoritativeFm).length > 0 ? authoritativeFm : undefined,
|
|
1023
|
+
divergedFields,
|
|
1024
|
+
});
|
|
1025
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(statePath, finalContent);
|
|
1026
|
+
// #3685 / #3691: compare NORMALIZED bytes, not the pre-normalize
|
|
1027
|
+
// `finalContent` string, against the pre-normalize `originalStateContent`
|
|
1028
|
+
// read above. `platformWriteSync` runs Markdown normalization (blank-line
|
|
1029
|
+
// insertion around headings/fences/lists) before persisting — the
|
|
1030
|
+
// transition core (`transitionCore`'s `## Current Position` section
|
|
1031
|
+
// reset) regenerates that section fresh on every call, including on a
|
|
1032
|
+
// genuine no-op re-run, and its raw un-normalized output differs from
|
|
1033
|
+
// the already-normalized on-disk original even though the write
|
|
1034
|
+
// converges to byte-identical content. Comparing pre-normalize strings
|
|
1035
|
+
// (mirroring cmdPhaseComplete's shape verbatim) was verified live to
|
|
1036
|
+
// report `true` on three consecutive byte-identical writes.
|
|
1037
|
+
// `contentChangedAfterNormalize` runs BOTH sides through the exact same
|
|
1038
|
+
// normalizer `platformWriteSync` used to persist (no extra disk I/O,
|
|
1039
|
+
// and immune by construction to this ordering artifact) — this used to
|
|
1040
|
+
// re-read the file to get the same answer; #3691 hoisted that seam so
|
|
1041
|
+
// this site, `updateRoadmapAfterPhaseRemoval`, and `cmdPhaseComplete`'s
|
|
1042
|
+
// roadmap/state/requirements flags all agree by construction.
|
|
1043
|
+
stateUpdated = (0, shell_command_projection_cjs_1.contentChangedAfterNormalize)(statePath, originalStateContent, finalContent);
|
|
1044
|
+
for (const field of divergedFields) {
|
|
1045
|
+
preservationWarnings.push({ field, reason: 'preserved-over-disagreeing-derived' });
|
|
1046
|
+
}
|
|
1047
|
+
// The authoritativeFm re-assert above (unlike a delta-based restore) is
|
|
1048
|
+
// invisible to `divergedFields` — #2736's re-assert runs after that
|
|
1049
|
+
// diff — so surface it explicitly here for "liberal but visible".
|
|
1050
|
+
for (const field of Object.keys(authoritativeFm)) {
|
|
1051
|
+
if (!divergedFields.includes(field)) {
|
|
1052
|
+
preservationWarnings.push({ field, reason: 'preserved-over-disagreeing-derived' });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
728
1056
|
}
|
|
729
1057
|
// Archive phase directories if requested
|
|
730
1058
|
let phasesArchived = false;
|
|
731
1059
|
// #1871: archive phase dirs by default on milestone complete (opt out via --no-archive-phases).
|
|
732
|
-
|
|
1060
|
+
// #3597: `phasesArchiveSkippedForScope` (computed once, above, from the SAME
|
|
1061
|
+
// `milestonePhaseDirs`/`milestonePhaseScope` resolution the --dry-run preview
|
|
1062
|
+
// consumed) refuses this destructive rename loop entirely when the window
|
|
1063
|
+
// scope is UNREADABLE — the one scope whose resolution degrades to a
|
|
1064
|
+
// pass-all fallback (ADR-3180). UNSCOPED/TRUNCATED are real, pre-existing
|
|
1065
|
+
// answers and archive exactly as they did before this branch.
|
|
1066
|
+
// `phasesArchived` stays false and the refusal is surfaced on `result` below;
|
|
1067
|
+
// nothing on disk moves, and `phaseArchiveDir` is never even created.
|
|
1068
|
+
if (options.archivePhases !== false && !phasesArchiveSkippedForScope) {
|
|
733
1069
|
// #2245 audit (was ERROR-HIDING): retryRenameSync moves one phase dir at a
|
|
734
1070
|
// time — a mid-loop failure (e.g. the Nth rename) used to leave
|
|
735
1071
|
// `phasesArchived` at its `false` default even though the first N-1 dirs
|
|
@@ -741,11 +1077,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
741
1077
|
try {
|
|
742
1078
|
const phaseArchiveDir = node_path_1.default.join(archiveDir, `${version}-phases`);
|
|
743
1079
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(phaseArchiveDir);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1080
|
+
// #3185 (ADR-3180 Decision 1) / #3597: same single routed derivation as
|
|
1081
|
+
// the stats loop and the --dry-run preview above — only the CURRENT
|
|
1082
|
+
// milestone's phase directories move, never a sentinel or an
|
|
1083
|
+
// out-of-window directory left for a later milestone, and never a
|
|
1084
|
+
// pass-all degrade from a non-COMPLETE scope (refused above).
|
|
1085
|
+
for (const dir of milestonePhaseDirs) {
|
|
749
1086
|
(0, shell_command_projection_cjs_1.retryRenameSync)(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
|
|
750
1087
|
archivedCount++;
|
|
751
1088
|
}
|
|
@@ -772,11 +1109,36 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
772
1109
|
requirements: node_fs_1.default.existsSync(node_path_1.default.join(archiveDir, `${version}-REQUIREMENTS.md`)),
|
|
773
1110
|
audit: node_fs_1.default.existsSync(node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
|
|
774
1111
|
phases: phasesArchived,
|
|
1112
|
+
// #3597: machine-readable refusal signal — distinguishes "nothing to
|
|
1113
|
+
// archive because the milestone genuinely has no phase directories"
|
|
1114
|
+
// (phases: false, phases_archive_skipped: false) from "refused to
|
|
1115
|
+
// archive because the milestone window scope was not COMPLETE"
|
|
1116
|
+
// (phases: false, phases_archive_skipped: true, with a reason).
|
|
1117
|
+
phases_archive_skipped: phasesArchiveSkippedForScope,
|
|
1118
|
+
phases_archive_skip_reason: phasesArchiveSkipReason,
|
|
1119
|
+
quick: !!quickArchiveResult && quickArchiveResult.archived > 0,
|
|
775
1120
|
},
|
|
776
|
-
|
|
777
|
-
|
|
1121
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract — both flags
|
|
1122
|
+
// now report a real before/after content diff instead of the previous
|
|
1123
|
+
// hardcoded `true` (milestones_updated) / bare fs.existsSync (state_updated).
|
|
1124
|
+
milestones_updated: milestonesUpdated,
|
|
1125
|
+
state_updated: stateUpdated,
|
|
1126
|
+
preservation_warnings: preservationWarnings,
|
|
778
1127
|
};
|
|
779
1128
|
output(result, raw);
|
|
1129
|
+
// #3227 (design doc §40 row 26 / "Not-corruption" rule): a refreshed
|
|
1130
|
+
// state.json `updated_at` must always mean something on disk actually
|
|
1131
|
+
// moved. This site is unconditional because every reachable path either
|
|
1132
|
+
// exits via `error()` (process.exit — refusals like a truncated milestone
|
|
1133
|
+
// window, an unstarted phase, or an invalid version never reach here) or
|
|
1134
|
+
// returns early on `--dry-run` (before any mutation, see the `dry_run:
|
|
1135
|
+
// true` branch above) — the only way execution reaches this line is after
|
|
1136
|
+
// the unconditional MILESTONES.md `platformWriteSync` a few lines above,
|
|
1137
|
+
// which always runs (new file, empty file, or append) once the run is
|
|
1138
|
+
// committed to mutating. Best-effort — cannot throw, cannot change this
|
|
1139
|
+
// command's exit code or output. publishStateContract resolves the
|
|
1140
|
+
// workstream planning root itself via planningPaths.
|
|
1141
|
+
publishStateContract(cwd);
|
|
780
1142
|
}
|
|
781
1143
|
function cmdPhasesClear(cwd, raw, args) {
|
|
782
1144
|
const phasesDir = planningPaths(cwd).phases;
|
|
@@ -813,7 +1175,17 @@ function cmdPhasesClear(cwd, raw, args) {
|
|
|
813
1175
|
let cleared = 0;
|
|
814
1176
|
if (node_fs_1.default.existsSync(phasesDir)) {
|
|
815
1177
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
816
|
-
|
|
1178
|
+
// #3185 (ADR-3180 Decision 1): this carried the FIFTH copy of the
|
|
1179
|
+
// sentinel rule and its THIRD regex variant — `/^999(?:\.|$)/` — which
|
|
1180
|
+
// excluded 999 but NOT 0. Because this is the DESTRUCTIVE path, that
|
|
1181
|
+
// divergence meant a `0-*` directory `roadmap analyze` preserves as a
|
|
1182
|
+
// sentinel was DELETED here. Routed through the canonical predicate so
|
|
1183
|
+
// every reader of "is this a sentinel phase" agrees by construction.
|
|
1184
|
+
// #3639: the DIR-AWARE recognizer — the convention-less id predicate
|
|
1185
|
+
// never saw bracket sentinel dirs (GSD.999-07-icebox), so they were
|
|
1186
|
+
// counted for deletion here while the disk guards (post-#3639) preserve
|
|
1187
|
+
// them; the destructive path must not be the one blind reader left.
|
|
1188
|
+
const dirs = entries.filter((e) => e.isDirectory() && !isSentinelPhaseDir(e.name));
|
|
817
1189
|
if (dirs.length > 0 && !confirm) {
|
|
818
1190
|
error(`phases clear would delete ${dirs.length} phase director${dirs.length === 1 ? 'y' : 'ies'}. ` +
|
|
819
1191
|
`Pass --confirm to proceed.`);
|
|
@@ -897,7 +1269,13 @@ function archivePhaseDirectories(cwd, phasesDir, dirs, archiveVersionOverride =
|
|
|
897
1269
|
let archiveVersion = safeOverride;
|
|
898
1270
|
if (!archiveVersion) {
|
|
899
1271
|
try {
|
|
900
|
-
|
|
1272
|
+
// #3216 (ADR-3180 §7.2 Decision): getMilestoneInfo's version becomes a
|
|
1273
|
+
// DIRECTORY NAME below — only a COMPLETE scope's identity is trustworthy
|
|
1274
|
+
// enough to act on destructively. On any other scope, treat the version
|
|
1275
|
+
// as unavailable so control falls through to the dated-label fallback,
|
|
1276
|
+
// same as an unreadable ROADMAP/STATE.
|
|
1277
|
+
const info = getMilestoneInfo(cwd);
|
|
1278
|
+
const liveVersion = info.scope === SCOPE.COMPLETE ? (info.value?.version ?? null) : null;
|
|
901
1279
|
// Defense in depth (#2288 security): getMilestoneInfo reads STATE.md's
|
|
902
1280
|
// `milestone:` field, which is unvalidated file content. Only accept it
|
|
903
1281
|
// as a path component if it is a safe version label; a crafted value
|
|
@@ -928,10 +1306,401 @@ function archivePhaseDirectories(cwd, phasesDir, dirs, archiveVersionOverride =
|
|
|
928
1306
|
}
|
|
929
1307
|
return { archiveDir: archivePhasesDir, archived };
|
|
930
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* #2142 BLOCKER 1 (review): escape one directory-name span for insertion as
|
|
1311
|
+
* markdown LINK TEXT (`[...]`) — a directory name containing a literal `|`,
|
|
1312
|
+
* `[` or `]` must not be able to break the enclosing markdown. `mkdirSync`
|
|
1313
|
+
* accepts an embedded newline in a directory name on POSIX (and `isDirectory()`
|
|
1314
|
+
* still reports true for it), so an unescaped newline would let attacker-
|
|
1315
|
+
* controlled content — including a markdown HEADING — land verbatim in the
|
|
1316
|
+
* generated README.md, an indirect prompt-injection vector for any agent
|
|
1317
|
+
* workflow step that later reads that file. Mirrors `escapeCell`'s exact
|
|
1318
|
+
* convention (markdown-table.cts `escapeCell`): collapse `\r?\n+` to a single
|
|
1319
|
+
* space FIRST (so a newline can never re-enter the output as a line break),
|
|
1320
|
+
* THEN escape the escape char itself (before the rest, so a literal backslash
|
|
1321
|
+
* in the name is never mistaken for part of an escape sequence this function
|
|
1322
|
+
* introduces), THEN the markdown-syntax characters.
|
|
1323
|
+
*/
|
|
1324
|
+
function escapeMarkdownLinkText(text) {
|
|
1325
|
+
return text
|
|
1326
|
+
.replace(/\r?\n+/g, ' ')
|
|
1327
|
+
.replace(/\\/g, '\\\\')
|
|
1328
|
+
.replace(/\|/g, '\\|')
|
|
1329
|
+
.replace(/\[/g, '\\[')
|
|
1330
|
+
.replace(/\]/g, '\\]');
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* #2142 BLOCKER 1 (review): encode one path span for insertion as a markdown
|
|
1334
|
+
* link DESTINATION (`(...)`) — `relSummary` is built from a directory name
|
|
1335
|
+
* that may legally contain a space, a `(`/`)`, or a control character
|
|
1336
|
+
* (including an embedded newline) on POSIX. Per CommonMark, an unbracketed
|
|
1337
|
+
* link destination terminates at the first ASCII space/control character and
|
|
1338
|
+
* requires parens to be balanced or escaped — any of those would truncate or
|
|
1339
|
+
* corrupt the link, or let attacker-controlled content spill out of the
|
|
1340
|
+
* `(...)` span into the surrounding markdown (the same indirect
|
|
1341
|
+
* prompt-injection vector `escapeMarkdownLinkText` guards the link TEXT
|
|
1342
|
+
* against). Percent-encodes just the unsafe set (space, `(`, `)`, and C0
|
|
1343
|
+
* control chars incl. `\r`/`\n`, plus DEL) rather than switching to the
|
|
1344
|
+
* angle-bracket `<...>` destination form — percent-encoding is reversible (a
|
|
1345
|
+
* markdown viewer resolving the link still reaches the right file) and does
|
|
1346
|
+
* not introduce a new pair of syntax characters (`<`/`>`) that would in turn
|
|
1347
|
+
* need their own escaping.
|
|
1348
|
+
*/
|
|
1349
|
+
function encodeMarkdownLinkTarget(target) {
|
|
1350
|
+
return target.replace(/[\x00-\x1f\x7f ()]/g, (ch) => `%${ch.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`);
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* #2142 (code-review FIX 2): PURE builder — scans `archiveQuickDir` and
|
|
1354
|
+
* resolves each entry's summary link, but performs NO IO beyond the read
|
|
1355
|
+
* scan itself; never writes. Split out of the former `writeQuickArchiveReadme`
|
|
1356
|
+
* so tests can assert on the returned structured IR (`entries`) instead of
|
|
1357
|
+
* substring-matching rendered markdown (CONTRIBUTING.md "Prohibited: Raw Text
|
|
1358
|
+
* Matching on Test Outputs" — a generated archive index is a "Rendered file",
|
|
1359
|
+
* which requires a pure builder returning IR, not the `.md`-IS-the-runtime-
|
|
1360
|
+
* artifact exemption).
|
|
1361
|
+
*
|
|
1362
|
+
* (re)generates an index of every quick-task directory PHYSICALLY PRESENT in
|
|
1363
|
+
* the archive, built by scanning the ARCHIVE directory on disk. Deliberately
|
|
1364
|
+
* NOT built from STATE.md's Quick Tasks table (the issue evidenced that table
|
|
1365
|
+
* drifting — 53 rows against 49 dirs, ~22 rows pointing at absent dirs, 18
|
|
1366
|
+
* dirs missing from the table — the filesystem is the only source of truth)
|
|
1367
|
+
* and NOT from the pre-move source list either, so a RE-RUN's index includes
|
|
1368
|
+
* entries a PRIOR run already archived, not just this run's (design row 11).
|
|
1369
|
+
*
|
|
1370
|
+
* Each entry's summary link is resolved via `resolveQuickTaskSummaryFile`
|
|
1371
|
+
* (audit.cts) — the SAME rule `scanQuickTasks` uses to read a task's record
|
|
1372
|
+
* — imported rather than re-derived, so the read and write paths can never
|
|
1373
|
+
* disagree about which file is a task's summary. A task WITHOUT a summary is
|
|
1374
|
+
* still listed, just without a link — never omitted (an omission would
|
|
1375
|
+
* under-report the index, which is worse than an unlinked entry).
|
|
1376
|
+
*
|
|
1377
|
+
* Entries are sorted for deterministic output. A directory name containing
|
|
1378
|
+
* `|`, `[`, `]` or an embedded newline is neutralized via
|
|
1379
|
+
* `escapeMarkdownLinkText` (link TEXT) so it cannot break the generated
|
|
1380
|
+
* markdown or inject a heading — applied here, at build time, so `entries`
|
|
1381
|
+
* itself already carries the injection-safe name (the regression test
|
|
1382
|
+
* asserts on THIS, not on rendered output). The destination is separately
|
|
1383
|
+
* encoded via `encodeMarkdownLinkTarget` (link TARGET) at RENDER time, so a
|
|
1384
|
+
* space/paren/control char in the name cannot truncate or corrupt the
|
|
1385
|
+
* `(...)` span. The summary path is normalized to POSIX
|
|
1386
|
+
* (`.split(path.sep).join('/')`) so the link is stable across platforms.
|
|
1387
|
+
*
|
|
1388
|
+
* Throws when `archiveQuickDir` is unreadable — the caller (`writeQuickArchiveReadme`)
|
|
1389
|
+
* is the best-effort boundary, not this builder.
|
|
1390
|
+
*/
|
|
1391
|
+
function buildQuickArchiveIndex(archiveQuickDir) {
|
|
1392
|
+
const dirEntries = node_fs_1.default.readdirSync(archiveQuickDir, { withFileTypes: true });
|
|
1393
|
+
const dirNames = dirEntries
|
|
1394
|
+
.filter((e) => e.isDirectory())
|
|
1395
|
+
.map((e) => e.name)
|
|
1396
|
+
.sort();
|
|
1397
|
+
const entries = dirNames.map((dirName) => {
|
|
1398
|
+
const taskDir = node_path_1.default.join(archiveQuickDir, dirName);
|
|
1399
|
+
const summaryPath = resolveQuickTaskSummaryFile(taskDir, dirName);
|
|
1400
|
+
const escapedName = escapeMarkdownLinkText(dirName);
|
|
1401
|
+
if (summaryPath) {
|
|
1402
|
+
const relSummary = node_path_1.default.relative(archiveQuickDir, summaryPath).split(node_path_1.default.sep).join('/');
|
|
1403
|
+
return { name: escapedName, summary: relSummary };
|
|
1404
|
+
}
|
|
1405
|
+
// No summary file — list the directory, but never link into it (there
|
|
1406
|
+
// is nothing to point at). See indexListsTaskWithoutSummaryWithoutLink.
|
|
1407
|
+
return { name: escapedName, summary: null };
|
|
1408
|
+
});
|
|
1409
|
+
return {
|
|
1410
|
+
entries,
|
|
1411
|
+
render() {
|
|
1412
|
+
const lines = ['# Archived Quick Tasks', ''];
|
|
1413
|
+
for (const entry of entries) {
|
|
1414
|
+
if (entry.summary !== null) {
|
|
1415
|
+
lines.push(`- [${entry.name}](${encodeMarkdownLinkTarget(entry.summary)})`);
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
lines.push(`- ${entry.name}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
lines.push('');
|
|
1422
|
+
return lines.join('\n');
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* #2142: thin writer — calls `buildQuickArchiveIndex` and writes its
|
|
1428
|
+
* `render()` output to `<archiveQuickDir>/README.md`. No-ops (writes
|
|
1429
|
+
* nothing) when `archiveQuickDir` is unreadable — this is a best-effort
|
|
1430
|
+
* index, not a gate on milestone completion. #2142 MAJOR 4 (review): the
|
|
1431
|
+
* whole body is wrapped in a try/catch so a failure of the WRITE itself
|
|
1432
|
+
* (read-only archive dir, full disk, or a quick-task directory literally
|
|
1433
|
+
* named `README.md` colliding with the file being written) degrades the same
|
|
1434
|
+
* way — this function genuinely cannot throw, matching its own
|
|
1435
|
+
* "best-effort, not a gate" contract; the directories are already safely
|
|
1436
|
+
* archived by the time this runs.
|
|
1437
|
+
*/
|
|
1438
|
+
function writeQuickArchiveReadme(archiveQuickDir) {
|
|
1439
|
+
try {
|
|
1440
|
+
const index = buildQuickArchiveIndex(archiveQuickDir);
|
|
1441
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(archiveQuickDir, 'README.md'), index.render());
|
|
1442
|
+
}
|
|
1443
|
+
catch {
|
|
1444
|
+
/* best-effort (#2142 MAJOR 4): a read-only archive dir, a full disk, or a
|
|
1445
|
+
* quick-task directory literally named `README.md` colliding with the
|
|
1446
|
+
* file this function writes must never crash `milestone complete` —
|
|
1447
|
+
* the quick-task directories are already safely archived on disk by the
|
|
1448
|
+
* time this index-generation step runs. */
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* #2142 MAJOR 5 (review): the single owned selection rule for "which
|
|
1453
|
+
* directories under `.planning/quick/` would/will move" — directory entries
|
|
1454
|
+
* only (symlinks are excluded here, per the MAJOR 3 note above), each
|
|
1455
|
+
* additionally guarded with `requireSafePath` (the same guard
|
|
1456
|
+
* `scanQuickTasks`/`archiveQuickTaskDirectories` use), sorted for
|
|
1457
|
+
* deterministic output. Extracted so `cmdMilestoneComplete`'s dry-run
|
|
1458
|
+
* preview, `cmdQuickArchive`'s dry-run preview, and the REAL selection inside
|
|
1459
|
+
* `archiveQuickTaskDirectories` all call this ONE function instead of each
|
|
1460
|
+
* re-deriving the rule — the "Generative Fix Divergence" anti-pattern this
|
|
1461
|
+
* repo explicitly guards against (a prior version of this code had the rule
|
|
1462
|
+
* written three times, and only the real-run copy applied `requireSafePath`,
|
|
1463
|
+
* so a dry-run preview could list a directory the real run would silently
|
|
1464
|
+
* skip).
|
|
1465
|
+
*/
|
|
1466
|
+
function listQuickTaskDirsForArchive(cwd) {
|
|
1467
|
+
const planningBase = planningPaths(cwd).planning;
|
|
1468
|
+
const quickDir = planningPaths(cwd).quick;
|
|
1469
|
+
let sourceEntries;
|
|
1470
|
+
try {
|
|
1471
|
+
sourceEntries = node_fs_1.default.readdirSync(quickDir, { withFileTypes: true });
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
// .planning/quick absent or unreadable — nothing to select.
|
|
1475
|
+
return [];
|
|
1476
|
+
}
|
|
1477
|
+
const names = [];
|
|
1478
|
+
for (const entry of sourceEntries) {
|
|
1479
|
+
if (!entry.isDirectory())
|
|
1480
|
+
continue; // excludes symlinks too — see MAJOR 3 note above
|
|
1481
|
+
try {
|
|
1482
|
+
(0, security_cjs_1.requireSafePath)(node_path_1.default.join(quickDir, entry.name), planningBase, 'quick task dir', { allowAbsolute: true });
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
continue; // symlink/escape attempt — never a candidate, in preview OR real run
|
|
1486
|
+
}
|
|
1487
|
+
names.push(entry.name);
|
|
1488
|
+
}
|
|
1489
|
+
return names.sort();
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* #2142: move each DIRECTORY entry under `.planning/quick/` into
|
|
1493
|
+
* `milestones/<version>-quick/` (collision-safe), then (re)write that
|
|
1494
|
+
* archive directory's README.md index. Sibling of `archivePhaseDirectories`
|
|
1495
|
+
* — extracted rather than inlined into `cmdMilestoneComplete` (already
|
|
1496
|
+
* cyclomatic 61) — mirroring its collision-safe destination-suffix loop,
|
|
1497
|
+
* `retryRenameSync`, and `platformEnsureDir` usage.
|
|
1498
|
+
*
|
|
1499
|
+
* `version` is ALREADY validated by `ARCHIVE_VERSION_LABEL_RE` at
|
|
1500
|
+
* `cmdMilestoneComplete`'s entry — this helper does not re-validate it, and
|
|
1501
|
+
* must only ever be called after that guard has run.
|
|
1502
|
+
*
|
|
1503
|
+
* #2142 MAJOR 3 (review): a symlink under `.planning/quick/` — even one that
|
|
1504
|
+
* targets a directory — is excluded by the `dirEntries` filter below
|
|
1505
|
+
* (`fs.Dirent.isDirectory()` returns FALSE for a symlink, regardless of what
|
|
1506
|
+
* it points at), so it is never a candidate `entry` in the first place and
|
|
1507
|
+
* `requireSafePath` below never runs against it. `requireSafePath` is
|
|
1508
|
+
* retained here as defense-in-depth for the NON-symlink path (a real
|
|
1509
|
+
* directory entry whose resolved path still needs re-validating against
|
|
1510
|
+
* `planningBase`) — the SAME guard `scanQuickTasks` (audit.cts) uses — so an
|
|
1511
|
+
* entry that fails it is skipped, never archived, never counted. See the
|
|
1512
|
+
* symlink regression tests in tests/milestone-archive.test.cjs
|
|
1513
|
+
* (`symlinkEscapeIsNeverArchivedByMilestoneComplete` /
|
|
1514
|
+
* `symlinkEscapeIsNeverArchivedByQuickArchive`) for a fixture proving neither
|
|
1515
|
+
* the symlink nor its external target is ever moved or altered — added
|
|
1516
|
+
* specifically so a future change to this filter cannot silently reopen the
|
|
1517
|
+
* escape with nothing to catch it.
|
|
1518
|
+
*
|
|
1519
|
+
* No-op (returns `{archived: 0, entries: []}`, creates NOTHING on disk) when
|
|
1520
|
+
* `.planning/quick/` does not exist or contains zero DIRECTORY entries — a
|
|
1521
|
+
* stray file with no sibling directory is neither an empty-dir case nor an
|
|
1522
|
+
* archive case.
|
|
1523
|
+
*
|
|
1524
|
+
* A mid-loop rename failure (or a failure to create the archive directory
|
|
1525
|
+
* itself) does not crash `milestone complete` — it degrades to whatever
|
|
1526
|
+
* `archived`/`entries` had already accumulated before the failure, mirroring
|
|
1527
|
+
* the `archivedCount` finally-pattern `cmdMilestoneComplete`'s own phase
|
|
1528
|
+
* archival uses a few hundred lines above (so a partial archive reports the
|
|
1529
|
+
* TRUE count, never a false `0`/`false`).
|
|
1530
|
+
*/
|
|
1531
|
+
function archiveQuickTaskDirectories(cwd, version) {
|
|
1532
|
+
const planningBase = planningPaths(cwd).planning;
|
|
1533
|
+
const quickDir = planningPaths(cwd).quick;
|
|
1534
|
+
const archiveQuickDir = node_path_1.default.join(planningBase, 'milestones', `${version}-quick`);
|
|
1535
|
+
// #2142 MAJOR 5 (review): dirNames is the SAME selection
|
|
1536
|
+
// `listQuickTaskDirsForArchive` hands to both dry-run previews — this is
|
|
1537
|
+
// the real run, so it cannot disagree with what a preview reported.
|
|
1538
|
+
const dirNames = listQuickTaskDirsForArchive(cwd);
|
|
1539
|
+
if (dirNames.length === 0) {
|
|
1540
|
+
// Boundary 0 (#2142): zero (safe) directory entries (empty dir, only
|
|
1541
|
+
// stray files, or every entry excluded by the selection rule) must not
|
|
1542
|
+
// create the archive directory. Also covers `.planning/quick/` being
|
|
1543
|
+
// absent/unreadable — `listQuickTaskDirsForArchive` degrades to `[]`.
|
|
1544
|
+
return { archiveDir: archiveQuickDir, archived: 0, entries: [] };
|
|
1545
|
+
}
|
|
1546
|
+
let archived = 0;
|
|
1547
|
+
const entries = [];
|
|
1548
|
+
try {
|
|
1549
|
+
(0, shell_command_projection_cjs_1.platformEnsureDir)(archiveQuickDir);
|
|
1550
|
+
for (const name of dirNames) {
|
|
1551
|
+
const src = node_path_1.default.join(quickDir, name);
|
|
1552
|
+
let safeSrc;
|
|
1553
|
+
try {
|
|
1554
|
+
// Re-validated here (not just trusted from the selection above) as
|
|
1555
|
+
// TOCTOU defense-in-depth: `listQuickTaskDirsForArchive` and this
|
|
1556
|
+
// rename are two separate filesystem observations, and an entry
|
|
1557
|
+
// that was a safe real directory at selection time could in theory
|
|
1558
|
+
// be swapped for a symlink before this loop reaches it.
|
|
1559
|
+
safeSrc = (0, security_cjs_1.requireSafePath)(src, planningBase, 'quick task dir', { allowAbsolute: true });
|
|
1560
|
+
}
|
|
1561
|
+
catch {
|
|
1562
|
+
continue; // symlink/escape attempt — skip, not archived
|
|
1563
|
+
}
|
|
1564
|
+
// Collision-safe: if a same-named archive entry exists (re-run), suffix it.
|
|
1565
|
+
let dest = node_path_1.default.join(archiveQuickDir, name);
|
|
1566
|
+
let destName = name;
|
|
1567
|
+
let n = 1;
|
|
1568
|
+
while (node_fs_1.default.existsSync(dest)) {
|
|
1569
|
+
destName = `${name}.${n++}`;
|
|
1570
|
+
dest = node_path_1.default.join(archiveQuickDir, destName);
|
|
1571
|
+
}
|
|
1572
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(safeSrc, dest);
|
|
1573
|
+
archived++;
|
|
1574
|
+
entries.push(destName);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
/* best-effort: platformEnsureDir failed, or the rename loop failed
|
|
1579
|
+
* partway — `archived`/`entries` above already reflect exactly what
|
|
1580
|
+
* succeeded before the failure (accumulated incrementally, never lost
|
|
1581
|
+
* with the swallowed exception — mirrors the archivedCount pattern at
|
|
1582
|
+
* cmdMilestoneComplete's phase-archival block). */
|
|
1583
|
+
}
|
|
1584
|
+
// Regenerate the README from whatever is ACTUALLY on disk now — covers
|
|
1585
|
+
// both a clean full archive and a degraded partial one, and (on a re-run)
|
|
1586
|
+
// includes entries a PRIOR run already archived. Skipped only when the
|
|
1587
|
+
// archive directory itself was never created (ensureDir failed above).
|
|
1588
|
+
if (node_fs_1.default.existsSync(archiveQuickDir)) {
|
|
1589
|
+
writeQuickArchiveReadme(archiveQuickDir);
|
|
1590
|
+
}
|
|
1591
|
+
return { archiveDir: archiveQuickDir, archived, entries };
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* #2142 escalation: `milestone.archive-quick` (CLI: `milestone archive-quick`,
|
|
1595
|
+
* renamed from the original `quick.archive` per code-review FIX 1 — folded
|
|
1596
|
+
* under the existing `milestone` namespace rather than adding a new top-level
|
|
1597
|
+
* command) — the narrow archival helper the issue's own "Scope of changes"
|
|
1598
|
+
* anticipated ("a `quick.archive`-style routine"), for callers (chiefly
|
|
1599
|
+
* `gsd-core/workflows/cleanup.md`) that need to sweep
|
|
1600
|
+
* `.planning/quick/*` WITHOUT the full `milestone complete` close-out.
|
|
1601
|
+
*
|
|
1602
|
+
* `milestone complete --archive-quick` cannot be reused for this: it
|
|
1603
|
+
* hard-errors via `missingExplicitVersion` for an already-completed
|
|
1604
|
+
* milestone (no `### Phase N:` headings left in its ROADMAP window),
|
|
1605
|
+
* re-archives ROADMAP.md over the very snapshot cleanup depends on, and
|
|
1606
|
+
* appends a duplicate MILESTONES.md entry on every re-run.
|
|
1607
|
+
*
|
|
1608
|
+
* This command performs ONLY the two things `archiveQuickTaskDirectories`
|
|
1609
|
+
* already does (move `.planning/quick/*` dirs into
|
|
1610
|
+
* `milestones/<version>-quick/` + (re)write that archive's README index —
|
|
1611
|
+
* the SAME helper `cmdMilestoneComplete` calls, so the two entry points can
|
|
1612
|
+
* never diverge on step 1) plus a Quick Tasks Completed table reset. It
|
|
1613
|
+
* NEVER touches ROADMAP.md, REQUIREMENTS.md, or MILESTONES.md, and runs
|
|
1614
|
+
* NEITHER the unstarted-phase guard NOR the milestone-window/TRUNCATED
|
|
1615
|
+
* refusal — those remain `milestone complete`'s alone.
|
|
1616
|
+
*
|
|
1617
|
+
* #2142 MAJOR 6 (review): the STATE.md write now routes through
|
|
1618
|
+
* `readModifyWriteStateMd` — the same owned read-transform-write composition
|
|
1619
|
+
* `gsd-tools.cjs`'s `quick-tasks-append` handler uses (ADR-3408 §8.3 / #3469:
|
|
1620
|
+
* "the single owned composition ... so the composition cannot diverge"). A
|
|
1621
|
+
* prior version of this function called `platformWriteSync` directly with
|
|
1622
|
+
* the reset result, bypassing `syncAndPreserveStateMd` entirely — the exact
|
|
1623
|
+
* bypass shape that ADR closed. Per `src/state.cts:3289-3330`,
|
|
1624
|
+
* `readModifyWriteStateMd` ALREADY acquires its own exclusive lock
|
|
1625
|
+
* (`acquireStateLock`/`releaseStateLock`, a real `O_CREAT|O_EXCL` file lock,
|
|
1626
|
+
* not reentrant) across its own read -> transform -> write cycle — so, unlike
|
|
1627
|
+
* `cmdMilestoneComplete` (which folds the table reset into its own
|
|
1628
|
+
* pre-existing `withStateLock` transform because it ALSO needs that lock for
|
|
1629
|
+
* the closure-transition write happening in the same block), this function
|
|
1630
|
+
* must NOT wrap the call in its own `withStateLock`: doing so would acquire
|
|
1631
|
+
* the same lock file twice in the same process, and the second acquire would
|
|
1632
|
+
* spin against a lock this same call already holds until it times out.
|
|
1633
|
+
*/
|
|
1634
|
+
function cmdQuickArchive(cwd, version, options, raw) {
|
|
1635
|
+
if (!version) {
|
|
1636
|
+
error('version required for milestone.archive-quick (e.g., v1.0)');
|
|
1637
|
+
}
|
|
1638
|
+
// #2288-class security: `version` becomes a filesystem directory component
|
|
1639
|
+
// (`milestones/<version>-quick/`) that directories are MOVED into — same
|
|
1640
|
+
// guard + wording shape `cmdMilestoneComplete` uses for its own version arg.
|
|
1641
|
+
if (!ARCHIVE_VERSION_LABEL_RE.test(version)) {
|
|
1642
|
+
error(`milestone.archive-quick: version "${version}" is invalid — a milestone version label may contain only letters, digits, '.', '-' and '_', and must not contain path separators or "..".`);
|
|
1643
|
+
}
|
|
1644
|
+
const statePath = planningPaths(cwd).state;
|
|
1645
|
+
const planningBase = planningPaths(cwd).planning;
|
|
1646
|
+
const toPosixRel = (p) => node_path_1.default.relative(cwd, p).split(node_path_1.default.sep).join('/');
|
|
1647
|
+
// --dry-run: preview only, mutates nothing. #2142 MAJOR 5 (review): routed
|
|
1648
|
+
// through the SAME `listQuickTaskDirsForArchive` selection
|
|
1649
|
+
// `cmdMilestoneComplete`'s own dry-run preview and the real
|
|
1650
|
+
// `archiveQuickTaskDirectories` both use, so all three can never disagree.
|
|
1651
|
+
if (options.dryRun) {
|
|
1652
|
+
const quickDirsToArchive = listQuickTaskDirsForArchive(cwd);
|
|
1653
|
+
output({
|
|
1654
|
+
dry_run: true,
|
|
1655
|
+
version,
|
|
1656
|
+
would_archive: quickDirsToArchive,
|
|
1657
|
+
archive_dir: toPosixRel(node_path_1.default.join(planningBase, 'milestones', `${version}-quick`)),
|
|
1658
|
+
}, raw);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const quickArchiveResult = archiveQuickTaskDirectories(cwd, version);
|
|
1662
|
+
const warnings = [];
|
|
1663
|
+
let stateUpdated = false;
|
|
1664
|
+
// Same silent/surfaced rule `cmdMilestoneComplete` applies: only attempt
|
|
1665
|
+
// the reset when something actually moved, and treat the
|
|
1666
|
+
// QUICK_TASKS_SECTION_ABSENT sentinel as a silent no-op (the section is
|
|
1667
|
+
// created lazily by quick.md Step 7b and absent from templates/state.md,
|
|
1668
|
+
// so absence is the common case, not an anomaly). Any other reset failure
|
|
1669
|
+
// is surfaced via `warnings`, never thrown.
|
|
1670
|
+
//
|
|
1671
|
+
// #2142 MAJOR 6 (review): routed through `readModifyWriteStateMd` (see the
|
|
1672
|
+
// docstring above) instead of a bare `platformWriteSync` — the transform
|
|
1673
|
+
// returns the ORIGINAL content unchanged whenever the reset did not apply
|
|
1674
|
+
// (sentinel-absent or a genuine failure), so `readModifyWriteStateMd`'s own
|
|
1675
|
+
// no-op guard (#948, state.cts:3304) skips the write and its `false`
|
|
1676
|
+
// return accurately reports "nothing was written" — the same "state_updated
|
|
1677
|
+
// must report accurately" contract the prior direct-write version upheld.
|
|
1678
|
+
if (quickArchiveResult.archived > 0 && node_fs_1.default.existsSync(statePath)) {
|
|
1679
|
+
let resetWarning = null;
|
|
1680
|
+
stateUpdated = readModifyWriteStateMd(statePath, (content) => {
|
|
1681
|
+
const { content: nextContent, warning } = applyQuickTasksReset(content);
|
|
1682
|
+
resetWarning = warning;
|
|
1683
|
+
return nextContent;
|
|
1684
|
+
}, cwd);
|
|
1685
|
+
if (resetWarning) {
|
|
1686
|
+
warnings.push(resetWarning);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
output({
|
|
1690
|
+
version,
|
|
1691
|
+
archived: quickArchiveResult.archived,
|
|
1692
|
+
entries: quickArchiveResult.entries,
|
|
1693
|
+
archive_dir: toPosixRel(quickArchiveResult.archiveDir),
|
|
1694
|
+
state_updated: stateUpdated,
|
|
1695
|
+
warnings,
|
|
1696
|
+
}, raw, `${quickArchiveResult.archived} quick task director${quickArchiveResult.archived === 1 ? 'y' : 'ies'} archived`);
|
|
1697
|
+
}
|
|
931
1698
|
module.exports = {
|
|
932
1699
|
cmdRequirementsMarkComplete,
|
|
933
1700
|
cmdRequirementsReadyIds,
|
|
934
1701
|
cmdRequirementsRevertPhase,
|
|
935
1702
|
cmdMilestoneComplete,
|
|
936
1703
|
cmdPhasesClear,
|
|
1704
|
+
cmdQuickArchive,
|
|
1705
|
+
buildQuickArchiveIndex,
|
|
937
1706
|
};
|