@opengsd/gsd-core 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -9
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +15 -55
- package/agents/gsd-planner.md +6 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +160 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +3 -2
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +3 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +34 -16
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +134 -1
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +122 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -21,9 +21,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
};
|
|
22
22
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
24
|
+
const node_child_process_1 = require("node:child_process");
|
|
24
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
|
|
25
26
|
const ioMod = require("./io.cjs");
|
|
26
|
-
const { output, error, ERROR_REASON } = ioMod;
|
|
27
|
+
const { output, error, ERROR_REASON, formatDiagnosticToken } = ioMod;
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const stateContract = require("./state-contract.cjs");
|
|
30
|
+
const { publishStateContract } = stateContract;
|
|
27
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
28
32
|
const configLoaderMod = require("./config-loader.cjs");
|
|
29
33
|
const { loadConfig } = configLoaderMod;
|
|
@@ -34,7 +38,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
34
38
|
// generative-fix divergence CLAUDE.md warns about. Collapsed onto core-utils'
|
|
35
39
|
// copy, which was already the leaf owner, so there is no second surface left to
|
|
36
40
|
// drift and no parity test needed to police one.
|
|
37
|
-
const { toPosixPath, generateSlugInternal, readSubdirectories, extractCanonicalPlanId, findUnsummarizedPlans, } = coreUtilsMod;
|
|
41
|
+
const { toPosixPath, generateSlugInternal, readSubdirectories, extractCanonicalPlanId, findUnsummarizedPlans, normalizeLineEndings, } = coreUtilsMod;
|
|
38
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
39
43
|
const phaseIdMod = require("./phase-id.cjs");
|
|
40
44
|
const { normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, matchPhaseDirs, isSentinelPhaseId, scopeToPhase, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, } = phaseIdMod;
|
|
@@ -74,6 +78,9 @@ const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted, isSu
|
|
|
74
78
|
const { planningDir, withPlanningLock, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, } = planningWorkspace;
|
|
75
79
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- milestone-lock.cjs is an export= CommonJS module
|
|
76
80
|
const milestoneLockMod = require("./milestone-lock.cjs");
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
82
|
+
const planDocumentMod = require("./plan-document.cjs");
|
|
83
|
+
const { parsePlanDocument, planIdFromFile } = planDocumentMod;
|
|
77
84
|
const { extractFrontmatter } = frontmatterMod;
|
|
78
85
|
const { readModifyWriteStateMd, stateExtractField, stateReplaceField, syncAndPreserveStateMd, withStateLock, updatePerformanceMetricsSection, } = stateMod;
|
|
79
86
|
// Any .md file with PLAN anywhere in the basename — diagnostic net
|
|
@@ -504,23 +511,73 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
504
511
|
}
|
|
505
512
|
output(notFound, raw, '');
|
|
506
513
|
}
|
|
507
|
-
function extractObjective(content) {
|
|
508
|
-
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
509
|
-
return m ? m[1].trim() : null;
|
|
510
|
-
}
|
|
511
514
|
/**
|
|
512
515
|
* Resolve a raw `depends_on` token to the `RawPlan.id` it refers to
|
|
513
|
-
* (case-folded exact match, falling back to canonical-id matching
|
|
516
|
+
* (case-folded exact match, falling back to canonical-id matching, falling
|
|
517
|
+
* back to the in-phase short-form plan number — #3897 rung 4). Returns
|
|
514
518
|
* `null` when the token does not resolve to any plan in this phase (a typo
|
|
515
519
|
* or a cross-phase reference) — every call site treats that as "ignore this
|
|
516
520
|
* edge", never a throw. Shared by `computeDependencyLevels`'s DAG-edge
|
|
517
|
-
* resolution
|
|
518
|
-
*
|
|
519
|
-
*
|
|
521
|
+
* resolution and (#2830) the halt-propagation node resolution, so the two can
|
|
522
|
+
* never disagree about which token resolves to which plan. NOT used by the
|
|
523
|
+
* `depends_on` display mapping (#3785/N3) — that stays a passthrough by
|
|
524
|
+
* design; see the comment at its call site.
|
|
525
|
+
*
|
|
526
|
+
* `shortFormToId` (#3897 rung 4, ADR-3473 §8.9) is the third tier, consulted
|
|
527
|
+
* only when neither `planMap` nor `canonicalToId` resolves the token. It is
|
|
528
|
+
* optional so any caller that has not been threaded through yet (there are
|
|
529
|
+
* none left in this file) degrades to the pre-#3897 two-tier behavior rather
|
|
530
|
+
* than throwing on a missing argument.
|
|
520
531
|
*/
|
|
521
|
-
function resolveDependencyId(dep, planMap, canonicalToId) {
|
|
532
|
+
function resolveDependencyId(dep, planMap, canonicalToId, shortFormToId) {
|
|
522
533
|
const lower = dep.toLowerCase();
|
|
523
|
-
|
|
534
|
+
if (planMap.has(lower))
|
|
535
|
+
return planMap.get(lower).id;
|
|
536
|
+
if (canonicalToId.has(lower))
|
|
537
|
+
return canonicalToId.get(lower);
|
|
538
|
+
return shortFormToId?.get(lower) ?? null;
|
|
539
|
+
}
|
|
540
|
+
// #3897 rung 4 (ADR-3473 §8.9) — builds the third depends_on resolution tier:
|
|
541
|
+
// a map from an in-phase BARE PLAN NUMBER (e.g. "01") to the plan id whose
|
|
542
|
+
// canonical id ends with that number. Recovered from the retired SDK lineage
|
|
543
|
+
// (sdk/src/query/phase.ts at 11918dcc3^) with ONE deliberate narrowing: the
|
|
544
|
+
// lost implementation indexed ANY trailing dash-segment of a canonical id,
|
|
545
|
+
// with no constraint that the segment be a plan NUMBER — so a phase
|
|
546
|
+
// containing both `09-FIX-auth-PLAN.md` and `09-GAP-auth-PLAN.md` (canonical
|
|
547
|
+
// id `09-FIX-auth`, trailing segment "auth") would silently bind
|
|
548
|
+
// `depends_on: ["auth"]` to whichever sorted first, fabricating a
|
|
549
|
+
// wave-affecting DAG edge with ZERO warning — a mis-resolved edge, which is
|
|
550
|
+
// worse than a dropped one (found in isolated correctness review, #3897).
|
|
551
|
+
// `docs/reference/plan-md.md` already documents this tier as resolving "the
|
|
552
|
+
// bare plan number", so requiring `/^\d+$/` on the trailing segment is a
|
|
553
|
+
// strict narrowing onto the tier's OWN documented contract, not a behavior
|
|
554
|
+
// change for any legitimate input. Do NOT restore the unconstrained
|
|
555
|
+
// lastDash-slice "to match the recovered original" — the original was wrong
|
|
556
|
+
// here; this rung deliberately departs from it in this one respect, and only
|
|
557
|
+
// this one. Everything else — the `lastDash` bound, first-write-wins,
|
|
558
|
+
// lowercasing — is kept exactly as recovered:
|
|
559
|
+
// - first write wins, deterministic because rawPlans is passed in sorted
|
|
560
|
+
// plan-file order (D4/T44) and this loop iterates in that same order;
|
|
561
|
+
// - a canonical id with no dash (`lastDash === -1` or `lastDash === 0`,
|
|
562
|
+
// e.g. "24" or "-01") or a trailing dash (`lastDash === canonical.length
|
|
563
|
+
// - 1`, e.g. "09-") is never indexed (D5).
|
|
564
|
+
// Exported so callers can build this map once and so tests assert against
|
|
565
|
+
// this REAL implementation rather than a hand-rolled copy that could
|
|
566
|
+
// silently disagree with it after a future change here (CLAUDE.md's
|
|
567
|
+
// generative-fix-divergence rule).
|
|
568
|
+
function buildShortFormToId(rawPlans) {
|
|
569
|
+
const shortFormToId = new Map();
|
|
570
|
+
for (const p of rawPlans) {
|
|
571
|
+
const canonical = extractCanonicalPlanId(p.id);
|
|
572
|
+
const lastDash = canonical.lastIndexOf('-');
|
|
573
|
+
if (lastDash > 0 && lastDash < canonical.length - 1) {
|
|
574
|
+
const shortForm = canonical.slice(lastDash + 1).toLowerCase();
|
|
575
|
+
if (/^\d+$/.test(shortForm) && !shortFormToId.has(shortForm)) {
|
|
576
|
+
shortFormToId.set(shortForm, p.id);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return shortFormToId;
|
|
524
581
|
}
|
|
525
582
|
// O(V + E). Assigns each in-phase plan its longest-path topological level over the
|
|
526
583
|
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number,
|
|
@@ -528,19 +585,32 @@ function resolveDependencyId(dep, planMap, canonicalToId) {
|
|
|
528
585
|
// the exact dequeue order this pass already produces — a valid topological order — passed to
|
|
529
586
|
// computeHaltPropagation as `precomputedOrder` so halt propagation does not re-run Kahn's
|
|
530
587
|
// algorithm a second time over the same graph.
|
|
531
|
-
|
|
588
|
+
//
|
|
589
|
+
// `shortFormToId` (#3897 rung 4, optional — see resolveDependencyId) is threaded through so a
|
|
590
|
+
// bare in-phase plan-number token (`depends_on: ["01"]`) resolves as a real DAG edge instead of
|
|
591
|
+
// being dropped and silently collapsing the dependent plan to wave 1 (D3).
|
|
592
|
+
function computeDependencyLevels(rawPlans, planMap, canonicalToId, shortFormToId) {
|
|
532
593
|
const level = new Map();
|
|
533
594
|
const inDeg = new Map();
|
|
534
595
|
const adj = new Map();
|
|
596
|
+
// #3427 / ADR-3473 §8.5: a depends_on token that resolves via NONE of the
|
|
597
|
+
// three tiers (planMap, canonicalToId, shortFormToId) is a dropped edge.
|
|
598
|
+
// Naming it here (rather than silently `continue`-ing past it) lets
|
|
599
|
+
// cmdPhasePlanIndex surface the token's own warning instead of
|
|
600
|
+
// manufacturing a wave-mismatch verdict from the resulting damaged graph
|
|
601
|
+
// (#3427).
|
|
602
|
+
const unresolved = [];
|
|
535
603
|
for (const p of rawPlans) {
|
|
536
604
|
if (!inDeg.has(p.id))
|
|
537
605
|
inDeg.set(p.id, 0);
|
|
538
606
|
if (!adj.has(p.id))
|
|
539
607
|
adj.set(p.id, []);
|
|
540
608
|
for (const dep of p.dependsOn) {
|
|
541
|
-
const resolvedDep = resolveDependencyId(dep, planMap, canonicalToId);
|
|
542
|
-
if (!resolvedDep)
|
|
609
|
+
const resolvedDep = resolveDependencyId(dep, planMap, canonicalToId, shortFormToId);
|
|
610
|
+
if (!resolvedDep) {
|
|
611
|
+
unresolved.push({ plan: p.id, token: String(dep) });
|
|
543
612
|
continue;
|
|
613
|
+
}
|
|
544
614
|
if (!adj.has(resolvedDep))
|
|
545
615
|
adj.set(resolvedDep, []);
|
|
546
616
|
adj.get(resolvedDep).push(p.id);
|
|
@@ -573,7 +643,7 @@ function computeDependencyLevels(rawPlans, planMap, canonicalToId) {
|
|
|
573
643
|
}
|
|
574
644
|
}
|
|
575
645
|
}
|
|
576
|
-
return { level, visited, order: queue };
|
|
646
|
+
return { level, visited, order: queue, unresolved };
|
|
577
647
|
}
|
|
578
648
|
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
579
649
|
if (!phase) {
|
|
@@ -677,46 +747,14 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
677
747
|
// ── Pass 1: parse each plan file ─────────────────────────────────────────
|
|
678
748
|
const rawPlans = [];
|
|
679
749
|
for (const planFile of planFiles) {
|
|
680
|
-
const planId = planFile
|
|
750
|
+
const planId = planIdFromFile(planFile);
|
|
681
751
|
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
682
752
|
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
const parsedWave = parseInt(fm['wave'], 10);
|
|
689
|
-
const declaredWave = Number.isNaN(parsedWave) ? null : parsedWave;
|
|
690
|
-
let dependsOn = [];
|
|
691
|
-
const fmDeps = fm['depends_on'];
|
|
692
|
-
if (Array.isArray(fmDeps)) {
|
|
693
|
-
dependsOn = fmDeps.map(String);
|
|
694
|
-
}
|
|
695
|
-
else if (typeof fmDeps === 'string' && fmDeps.trim() !== '') {
|
|
696
|
-
dependsOn = [fmDeps];
|
|
697
|
-
}
|
|
698
|
-
let autonomous = true;
|
|
699
|
-
if (fm['autonomous'] !== undefined) {
|
|
700
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue comparison
|
|
701
|
-
autonomous = fm['autonomous'] === 'true' || String(fm['autonomous']) === 'true';
|
|
702
|
-
}
|
|
703
|
-
let filesModified = [];
|
|
704
|
-
const fmFiles = fm['files_modified'] || fm['files-modified'];
|
|
705
|
-
if (fmFiles) {
|
|
706
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar-to-string
|
|
707
|
-
filesModified = Array.isArray(fmFiles) ? fmFiles.map(String) : [String(fmFiles)];
|
|
708
|
-
}
|
|
709
|
-
// #1689: optional per-plan specialist executor hint. Read verbatim here; the
|
|
710
|
-
// orchestrator resolves it against the active runtime's agent dir at dispatch
|
|
711
|
-
// time (execute-phase.md -> `gsd_run query resolve-agent`), falling back to
|
|
712
|
-
// gsd-executor when the field is unset or the named agent does not resolve.
|
|
713
|
-
let agentHint = null;
|
|
714
|
-
const fmAgentHint = fm['agent_hint'];
|
|
715
|
-
if (fmAgentHint !== undefined) {
|
|
716
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar-to-string
|
|
717
|
-
const hintStr = String(fmAgentHint).trim();
|
|
718
|
-
agentHint = hintStr !== '' ? hintStr : null;
|
|
719
|
-
}
|
|
753
|
+
// #2790: plan-body parsing is owned by the shared Plan Document Module, so
|
|
754
|
+
// this command and the read-only `planning.inspect` query cannot drift on
|
|
755
|
+
// what a plan document says. planPath is still passed so a truncated
|
|
756
|
+
// PLAN.md names the file in the #1882 diagnostic.
|
|
757
|
+
const planDoc = parsePlanDocument(content, planPath);
|
|
720
758
|
const hasSummary = !unsummarizedPlanFiles.has(planFile);
|
|
721
759
|
// #2830: a plan can have a SUMMARY (hasSummary=true) and still be halted —
|
|
722
760
|
// a designed stop still writes a completion record, just one whose status
|
|
@@ -728,13 +766,14 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
728
766
|
: false;
|
|
729
767
|
rawPlans.push({
|
|
730
768
|
id: planId,
|
|
731
|
-
declaredWave,
|
|
732
|
-
dependsOn,
|
|
733
|
-
autonomous,
|
|
734
|
-
objective:
|
|
735
|
-
filesModified,
|
|
736
|
-
|
|
737
|
-
|
|
769
|
+
declaredWave: planDoc.declaredWave,
|
|
770
|
+
dependsOn: planDoc.dependsOn,
|
|
771
|
+
autonomous: planDoc.autonomous,
|
|
772
|
+
objective: planDoc.objective,
|
|
773
|
+
filesModified: planDoc.filesModified,
|
|
774
|
+
filesDeleted: planDoc.filesDeleted,
|
|
775
|
+
agentHint: planDoc.agentHint,
|
|
776
|
+
taskCount: planDoc.taskCount,
|
|
738
777
|
hasSummary,
|
|
739
778
|
halted,
|
|
740
779
|
});
|
|
@@ -752,7 +791,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
752
791
|
}
|
|
753
792
|
const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
|
|
754
793
|
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
|
|
755
|
-
|
|
794
|
+
// #3897 rung 4 (ADR-3473 §8.9) — the third depends_on resolution tier.
|
|
795
|
+
// Resolves a bare in-phase plan-number short form (e.g. "01") to its owning
|
|
796
|
+
// plan id. In-phase only by construction (T49): the map is built from THIS
|
|
797
|
+
// phase's rawPlans alone, so a short form colliding with a different
|
|
798
|
+
// phase's plan can never be a candidate. See {@link buildShortFormToId}'s
|
|
799
|
+
// own comment for the numeric-only narrowing this rung applies on top of
|
|
800
|
+
// the recovered SDK-lineage algorithm.
|
|
801
|
+
const shortFormToId = buildShortFormToId(rawPlans);
|
|
802
|
+
const { level, visited, order, unresolved } = computeDependencyLevels(rawPlans, planMap, canonicalToId, shortFormToId);
|
|
756
803
|
if (visited < rawPlans.length) {
|
|
757
804
|
const cycleNodes = rawPlans.filter((p) => !level.has(p.id)).map((p) => p.id);
|
|
758
805
|
error(`depends_on cycle detected in phase ${normalized} — cycle involves: ${cycleNodes.join(', ')}`);
|
|
@@ -766,7 +813,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
766
813
|
const haltNodes = rawPlans.map((p) => ({
|
|
767
814
|
id: p.id,
|
|
768
815
|
resolvedDependsOn: p.dependsOn
|
|
769
|
-
.map((dep) => resolveDependencyId(String(dep), planMap, canonicalToId))
|
|
816
|
+
.map((dep) => resolveDependencyId(String(dep), planMap, canonicalToId, shortFormToId))
|
|
770
817
|
.filter((id) => id !== null),
|
|
771
818
|
halted: p.halted,
|
|
772
819
|
}));
|
|
@@ -780,6 +827,17 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
780
827
|
const runnable = [];
|
|
781
828
|
let hasCheckpoints = false;
|
|
782
829
|
const warnings = [];
|
|
830
|
+
// #3427 / ADR-3473 §8.5: name every dropped depends_on edge (plan AND
|
|
831
|
+
// token) rather than letting it silently collapse the plan to a DAG root.
|
|
832
|
+
// A plan with at least one unresolved token gets ITS OWN warning here and
|
|
833
|
+
// the wave-mismatch verdict below is suppressed for that plan ONLY — a
|
|
834
|
+
// plan with no dropped edges and a genuinely wrong `wave:` still warns
|
|
835
|
+
// (N3, D6, T25).
|
|
836
|
+
const plansWithUnresolvedTokens = new Set();
|
|
837
|
+
for (const { plan, token } of unresolved) {
|
|
838
|
+
plansWithUnresolvedTokens.add(plan);
|
|
839
|
+
warnings.push(`Plan ${plan}: depends_on token ${formatDiagnosticToken(token)} does not resolve to any plan in this phase — edge dropped, wave placement for this plan may be unreliable`);
|
|
840
|
+
}
|
|
783
841
|
for (const rawPlan of rawPlans) {
|
|
784
842
|
if (!rawPlan.autonomous) {
|
|
785
843
|
hasCheckpoints = true;
|
|
@@ -796,7 +854,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
796
854
|
}
|
|
797
855
|
const computedWave = (level.get(rawPlan.id) ?? 0) + levelOffset;
|
|
798
856
|
const effectiveWave = computedWave;
|
|
799
|
-
|
|
857
|
+
// #3427 (D5/N3): suppress the wave-mismatch verdict for a plan that has
|
|
858
|
+
// at least one unresolved depends_on token — its own dropped-edge
|
|
859
|
+
// warning above already explains the degraded wave placement, so the
|
|
860
|
+
// mismatch here would blame the author for a DAG the tool itself
|
|
861
|
+
// couldn't build. A plan with NO unresolved tokens still gets a genuine
|
|
862
|
+
// mismatch reported (N3, T25) — the suppression is per-plan, never blanket.
|
|
863
|
+
if (rawPlan.declaredWave !== null &&
|
|
864
|
+
rawPlan.declaredWave !== computedWave &&
|
|
865
|
+
!plansWithUnresolvedTokens.has(rawPlan.id)) {
|
|
800
866
|
warnings.push(`Plan ${rawPlan.id}: declared wave: ${rawPlan.declaredWave} but depends_on DAG places it in wave ${computedWave}`);
|
|
801
867
|
}
|
|
802
868
|
const plan = {
|
|
@@ -816,6 +882,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
816
882
|
autonomous: rawPlan.autonomous,
|
|
817
883
|
objective: rawPlan.objective,
|
|
818
884
|
files_modified: rawPlan.filesModified,
|
|
885
|
+
files_deleted: rawPlan.filesDeleted,
|
|
819
886
|
agent_hint: rawPlan.agentHint,
|
|
820
887
|
task_count: rawPlan.taskCount,
|
|
821
888
|
has_summary: rawPlan.hasSummary,
|
|
@@ -912,6 +979,75 @@ function assertDescriptionPreservesMilestoneScope(description, command) {
|
|
|
912
979
|
`Rewrite the line so it is not a level 1-3 "#" heading carrying a milestone marker ` +
|
|
913
980
|
`(a vN.N version token, a ✅/📋/🚧/🔄 marker, or the word "Milestone").`);
|
|
914
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* #3849 — widen "used phase numbers" beyond this checkout. Every sibling git
|
|
984
|
+
* worktree carries its own `.planning/` on its own branch, so a phase minted
|
|
985
|
+
* there is invisible to the cwd-scoped sources (headers, bullets, on-disk
|
|
986
|
+
* dirs). Scan each sibling's phase-directory names (cheap — dir names alone
|
|
987
|
+
* caught the real incident) and its WHOLE ROADMAP.md headers (a row can exist
|
|
988
|
+
* before any directory does; milestone-scoping is wrong here because a number
|
|
989
|
+
* used under any milestone on another branch is still taken).
|
|
990
|
+
*
|
|
991
|
+
* Widen, never refuse: a missing `.planning/`, an unreadable sibling, a
|
|
992
|
+
* non-git cwd, or an unavailable git binary each leave `used` untouched —
|
|
993
|
+
* allocation then behaves exactly as it did before this horizon existed.
|
|
994
|
+
* Sentinels reuse the canonical `isSentinelPhaseId`; the dir pattern is the
|
|
995
|
+
* same one the on-disk scan uses, so decimal sub-phases (`411.1-foo`) are
|
|
996
|
+
* correctly not integers.
|
|
997
|
+
*/
|
|
998
|
+
function collectSiblingWorktreePhaseNums(cwd, used) {
|
|
999
|
+
let porcelain;
|
|
1000
|
+
try {
|
|
1001
|
+
porcelain = (0, node_child_process_1.execFileSync)('git', ['worktree', 'list', '--porcelain'], {
|
|
1002
|
+
cwd,
|
|
1003
|
+
encoding: 'utf-8',
|
|
1004
|
+
// Same subprocess band as the other git call sites (smart-entry, check-command-router):
|
|
1005
|
+
// inside the 5-30s git window, hidden console window on Windows, bounded buffer.
|
|
1006
|
+
timeout: 10_000,
|
|
1007
|
+
windowsHide: true,
|
|
1008
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
return; // not a git repo / git unavailable — unchanged behavior
|
|
1013
|
+
}
|
|
1014
|
+
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
1015
|
+
// Same header shape the allocators scan locally (#1729 tag tolerance).
|
|
1016
|
+
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1017
|
+
for (const line of porcelain.split('\n')) {
|
|
1018
|
+
if (!line.startsWith('worktree '))
|
|
1019
|
+
continue;
|
|
1020
|
+
const wt = line.slice('worktree '.length).trim();
|
|
1021
|
+
if (!wt || node_path_1.default.resolve(wt) === node_path_1.default.resolve(cwd))
|
|
1022
|
+
continue;
|
|
1023
|
+
try {
|
|
1024
|
+
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(wt, '.planning', 'phases'))) {
|
|
1025
|
+
const match = entry.match(dirNumPattern);
|
|
1026
|
+
if (!match)
|
|
1027
|
+
continue;
|
|
1028
|
+
const num = parseInt(match[1], 10);
|
|
1029
|
+
if (!isSentinelPhaseId(num))
|
|
1030
|
+
used.add(num);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
/* worktree has no .planning — normal, contributes nothing */
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(wt, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1038
|
+
let m;
|
|
1039
|
+
headerPattern.lastIndex = 0;
|
|
1040
|
+
while ((m = headerPattern.exec(content)) !== null) {
|
|
1041
|
+
const num = parseInt(m[1], 10);
|
|
1042
|
+
if (!isSentinelPhaseId(num))
|
|
1043
|
+
used.add(num);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
/* no roadmap in that worktree — normal, contributes nothing */
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
915
1051
|
function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
916
1052
|
if (!description) {
|
|
917
1053
|
error('description required for phase add');
|
|
@@ -980,6 +1116,9 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
980
1116
|
// section headers, roadmap bullets, AND on-disk dirs above is what prevents the
|
|
981
1117
|
// #1229 collision (a bullet-only Phase N is now counted), so max+1 cannot reuse
|
|
982
1118
|
// an existing number.
|
|
1119
|
+
// 4) Sibling git worktrees (#3849) — same max+1, wider horizon: a number
|
|
1120
|
+
// taken on another branch is still taken.
|
|
1121
|
+
collectSiblingWorktreePhaseNums(cwd, usedPhaseNums);
|
|
983
1122
|
const maxUsed = usedPhaseNums.size > 0 ? Math.max(...usedPhaseNums) : 0;
|
|
984
1123
|
_newPhaseId = maxUsed + 1;
|
|
985
1124
|
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
@@ -1009,6 +1148,18 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
1009
1148
|
if (titleWarning)
|
|
1010
1149
|
result['warning'] = titleWarning;
|
|
1011
1150
|
output(result, raw, result['padded']);
|
|
1151
|
+
// #3227 (design doc §40 row 26 / "Not-corruption" rule): every
|
|
1152
|
+
// `publishStateContract` call site in this file is audited so a refreshed
|
|
1153
|
+
// state.json `updated_at` always means something on disk actually moved —
|
|
1154
|
+
// a stale-but-refreshed timestamp is worse than no refresh, because it
|
|
1155
|
+
// reads as fresh to a downstream watcher. This site is unconditional
|
|
1156
|
+
// because every reachable path either exits via `error()` (process.exit,
|
|
1157
|
+
// never reaches here) or falls through to the unconditional
|
|
1158
|
+
// `platformEnsureDir`/`platformWriteSync` pair above that always creates
|
|
1159
|
+
// the phase directory and rewrites ROADMAP.md — there is no code path that
|
|
1160
|
+
// reaches this line without having just written to disk. Best-effort —
|
|
1161
|
+
// cannot throw, cannot change this command's exit code or output.
|
|
1162
|
+
publishStateContract(cwd);
|
|
1012
1163
|
}
|
|
1013
1164
|
function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
1014
1165
|
if (!Array.isArray(descriptions) || descriptions.length === 0) {
|
|
@@ -1032,8 +1183,12 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1032
1183
|
const content = extractCurrentMilestone(rawContent, cwd);
|
|
1033
1184
|
let maxPhase = 0;
|
|
1034
1185
|
if (config.phase_naming !== 'custom') {
|
|
1186
|
+
// Same three cwd-scoped sources as cmdPhaseAdd (#1229): headers, roadmap
|
|
1187
|
+
// bullets, on-disk dirs. The bullet scan was missing here — a bullet-only
|
|
1188
|
+
// `Phase N` row was invisible to batch allocation (#3849 secondary).
|
|
1035
1189
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1036
1190
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*(?:\s*\([^)\n]{0,200}\))?:/gi;
|
|
1191
|
+
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]{0,200}\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
1037
1192
|
let m;
|
|
1038
1193
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
1039
1194
|
const num = parseInt(m[1], 10);
|
|
@@ -1043,6 +1198,13 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1043
1198
|
if (num > maxPhase)
|
|
1044
1199
|
maxPhase = num;
|
|
1045
1200
|
}
|
|
1201
|
+
while ((m = bulletPattern.exec(content)) !== null) {
|
|
1202
|
+
const num = parseInt(m[1], 10);
|
|
1203
|
+
if (isSentinelPhaseId(num))
|
|
1204
|
+
continue;
|
|
1205
|
+
if (num > maxPhase)
|
|
1206
|
+
maxPhase = num;
|
|
1207
|
+
}
|
|
1046
1208
|
const phasesOnDisk = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1047
1209
|
if (node_fs_1.default.existsSync(phasesOnDisk)) {
|
|
1048
1210
|
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
@@ -1058,6 +1220,13 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1058
1220
|
maxPhase = num;
|
|
1059
1221
|
}
|
|
1060
1222
|
}
|
|
1223
|
+
// 4) Sibling git worktrees (#3849) — same max+1, wider horizon.
|
|
1224
|
+
const siblingNums = new Set();
|
|
1225
|
+
collectSiblingWorktreePhaseNums(cwd, siblingNums);
|
|
1226
|
+
for (const num of siblingNums) {
|
|
1227
|
+
if (num > maxPhase)
|
|
1228
|
+
maxPhase = num;
|
|
1229
|
+
}
|
|
1061
1230
|
}
|
|
1062
1231
|
const added = [];
|
|
1063
1232
|
for (const description of descriptions) {
|
|
@@ -1095,6 +1264,11 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
|
|
1095
1264
|
return added;
|
|
1096
1265
|
});
|
|
1097
1266
|
output({ phases: results, count: results.length }, raw);
|
|
1267
|
+
// #3227: unconditional here because `platformWriteSync(roadmapPath, rawContent)`
|
|
1268
|
+
// above always rewrites ROADMAP.md for every description in the batch before
|
|
1269
|
+
// this line is reached; the only refusal path is the `error('ROADMAP.md not
|
|
1270
|
+
// found')` above, which terminates the process and never reaches here.
|
|
1271
|
+
publishStateContract(cwd);
|
|
1098
1272
|
}
|
|
1099
1273
|
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
1100
1274
|
if (!afterPhase || !description) {
|
|
@@ -1242,6 +1416,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
1242
1416
|
directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
1243
1417
|
};
|
|
1244
1418
|
output(result, raw, decimalPhase);
|
|
1419
|
+
// #3227: unconditional here because `platformWriteSync(roadmapPath, updatedContent)`
|
|
1420
|
+
// above always rewrites ROADMAP.md with the inserted phase before this line is
|
|
1421
|
+
// reached; every refusal along the way (bad args, missing ROADMAP.md, unresolved
|
|
1422
|
+
// target bullet/header) exits via `error()`, which terminates the process.
|
|
1423
|
+
publishStateContract(cwd);
|
|
1245
1424
|
}
|
|
1246
1425
|
function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
1247
1426
|
const renamedDirs = [];
|
|
@@ -1455,9 +1634,17 @@ function findDataRowLine(sectionText, dataRowIndex) {
|
|
|
1455
1634
|
}
|
|
1456
1635
|
return null;
|
|
1457
1636
|
}
|
|
1637
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract — the caller
|
|
1638
|
+
// (cmdPhaseRemove) used to report `roadmap_updated: true` unconditionally,
|
|
1639
|
+
// hardcoded regardless of whether this transform actually changed
|
|
1640
|
+
// ROADMAP.md's content. Returning a real before/after comparison here lets
|
|
1641
|
+
// the caller report accurately, the same fix #3685 applied to
|
|
1642
|
+
// `cmdPhaseComplete` and #2640/#2974 already applied to this same function's
|
|
1643
|
+
// sibling `stateUpdated` flag a few lines below in `cmdPhaseRemove`.
|
|
1458
1644
|
function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt, cwd) {
|
|
1459
|
-
withPlanningLock(cwd, () => {
|
|
1460
|
-
|
|
1645
|
+
return withPlanningLock(cwd, () => {
|
|
1646
|
+
const originalContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
1647
|
+
let content = originalContent;
|
|
1461
1648
|
const escaped = (0, pattern_cjs_1.escapeRegex)(targetPhase);
|
|
1462
1649
|
// #3572: ROADMAP headings and rows carry the normalized (zero-padded) form
|
|
1463
1650
|
// of a decimal id — `phase insert 1` writes `### Phase 01.1:` while the
|
|
@@ -1615,6 +1802,14 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
|
|
1615
1802
|
content = content.replace(/(Depends on:\*\*\s*Phase\s+)(\d+(?:\.\d+)?)\b/gi, (_match, prefix, num) => `${prefix}${decrementRoadmapPhaseToken(num, removedInt)}`);
|
|
1616
1803
|
}
|
|
1617
1804
|
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, content);
|
|
1805
|
+
// #3685 / #3691: compare NORMALIZED bytes (what platformWriteSync actually
|
|
1806
|
+
// persists), not the raw pre-normalize `content` string, against the raw
|
|
1807
|
+
// pre-mutation `originalContent` read above — a raw `!==` here reports a
|
|
1808
|
+
// false `true` whenever this transform's regenerated output takes a
|
|
1809
|
+
// different-but-equivalent shape than the already-normalized on-disk
|
|
1810
|
+
// original (same normalization-order artifact #3685 fixed at
|
|
1811
|
+
// cmdMilestoneComplete; see contentChangedAfterNormalize's own doc).
|
|
1812
|
+
return (0, shell_command_projection_cjs_1.contentChangedAfterNormalize)(roadmapPath, originalContent, content);
|
|
1618
1813
|
});
|
|
1619
1814
|
}
|
|
1620
1815
|
/**
|
|
@@ -1718,7 +1913,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
1718
1913
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1719
1914
|
error(`Failed to renumber phase directories after removing phase ${targetPhase}: ${msg}`);
|
|
1720
1915
|
}
|
|
1721
|
-
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
1916
|
+
const roadmapUpdated = updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
1722
1917
|
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
1723
1918
|
let stateUpdated = false;
|
|
1724
1919
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
@@ -1791,10 +1986,27 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
1791
1986
|
renamed_directories: renamedDirs,
|
|
1792
1987
|
renamed_files: renamedFiles,
|
|
1793
1988
|
renamed_file_collisions: renamedFileCollisions,
|
|
1794
|
-
|
|
1989
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract — true only
|
|
1990
|
+
// when updateRoadmapAfterPhaseRemoval's content diff detected a real
|
|
1991
|
+
// change, not hardcoded regardless of whether ROADMAP.md's content
|
|
1992
|
+
// actually changed.
|
|
1993
|
+
roadmap_updated: roadmapUpdated,
|
|
1795
1994
|
state_updated: stateUpdated,
|
|
1796
1995
|
}, raw);
|
|
1996
|
+
// #3227: unconditional here because `updateRoadmapAfterPhaseRemoval` above
|
|
1997
|
+
// always rewrites ROADMAP.md before this line is reached; every refusal path
|
|
1998
|
+
// (bad target, missing ROADMAP.md, --force-required, renumber failure) exits
|
|
1999
|
+
// via `error()`, and the ambiguous-match case exits via an earlier `return`
|
|
2000
|
+
// before any file is touched.
|
|
2001
|
+
publishStateContract(cwd);
|
|
1797
2002
|
}
|
|
2003
|
+
/**
|
|
2004
|
+
* #3227: returns the count of writes actually applied (entries whose
|
|
2005
|
+
* `before` differed from `after` and were therefore written to disk) — the
|
|
2006
|
+
* caller (`cmdPhaseComplete`) uses this as its publish-gate signal, since a
|
|
2007
|
+
* re-run against an already-completed phase can produce a `writes[]` array
|
|
2008
|
+
* where every entry is byte-identical to what's already on disk.
|
|
2009
|
+
*/
|
|
1798
2010
|
function writePlanningFileSet(writes) {
|
|
1799
2011
|
const applied = [];
|
|
1800
2012
|
try {
|
|
@@ -1824,6 +2036,7 @@ function writePlanningFileSet(writes) {
|
|
|
1824
2036
|
}
|
|
1825
2037
|
throw err;
|
|
1826
2038
|
}
|
|
2039
|
+
return applied.length;
|
|
1827
2040
|
}
|
|
1828
2041
|
function phaseDisplayNameFromRoadmap(roadmapContent, phaseNum) {
|
|
1829
2042
|
if (!roadmapContent || !phaseNum)
|
|
@@ -1890,6 +2103,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1890
2103
|
? phaseInfo['summaries'].length
|
|
1891
2104
|
: 0;
|
|
1892
2105
|
let requirementsUpdated = false;
|
|
2106
|
+
// #3685: mirror requirementsUpdated's diff-tracking contract at the
|
|
2107
|
+
// writes.push({filePath, before, after}) sites below, rather than
|
|
2108
|
+
// reporting via fs.existsSync (which is true whenever the file merely
|
|
2109
|
+
// exists, not when the transaction actually wrote a change).
|
|
2110
|
+
let roadmapUpdated = false;
|
|
2111
|
+
let stateUpdated = false;
|
|
1893
2112
|
const warnings = [];
|
|
1894
2113
|
// ADR-3408 §8.5 / D2 (#3374): "liberal but visible" — when the write-seam
|
|
1895
2114
|
// composition's preservation stage restores a curated frontmatter value
|
|
@@ -1994,7 +2213,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
1994
2213
|
}
|
|
1995
2214
|
for (const file of scopeToPhase(phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md')), phaseFullDirBaseName)) {
|
|
1996
2215
|
const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
|
|
1997
|
-
|
|
2216
|
+
// #3707-CR follow-up MINOR: normalize line endings at this read boundary
|
|
2217
|
+
// (same fix as src/verification.cts's readVerificationStatus) so a
|
|
2218
|
+
// lone-CR VERIFICATION.md's `---\r...\r---` frontmatter fence still
|
|
2219
|
+
// matches extractFrontmatter's byte-0 check instead of silently
|
|
2220
|
+
// dropping the human_needed/gaps_found advisory warning below.
|
|
2221
|
+
const content = normalizeLineEndings(node_fs_1.default.readFileSync(verificationFilePath, 'utf-8'));
|
|
1998
2222
|
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
|
|
1999
2223
|
// from historical metadata in the file body (e.g. `previous_status: gaps_found`).
|
|
2000
2224
|
// A full-text regex like /status: gaps_found/ matches the substring inside
|
|
@@ -2060,6 +2284,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2060
2284
|
// warnings[] entry below (same parity pattern as
|
|
2061
2285
|
// verification_stale_check_indeterminate).
|
|
2062
2286
|
let milestoneConflict = null;
|
|
2287
|
+
// #3227: set inside `runPhaseCompleteTransaction` below from
|
|
2288
|
+
// `writePlanningFileSet`'s applied-count return — the transaction always
|
|
2289
|
+
// RUNS (verification passed, the lock was taken, `writes[]` was built),
|
|
2290
|
+
// but a re-run against a phase whose ROADMAP/STATE bytes already reflect
|
|
2291
|
+
// completion produces a `writes[]` where every entry is byte-identical to
|
|
2292
|
+
// disk, so `writePlanningFileSet` applies none of them. That must not
|
|
2293
|
+
// still refresh state.json's `updated_at` (design doc §40 row 26).
|
|
2294
|
+
let anyPlanningWrite = false;
|
|
2063
2295
|
const verificationBlocked = withPlanningLock(cwd, () => {
|
|
2064
2296
|
// #3311: completing a phase while a live milestone claim (phase + session)
|
|
2065
2297
|
// holds a DIFFERENT phase means two sessions are working two phases against
|
|
@@ -2251,6 +2483,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2251
2483
|
before: originalRoadmapContent,
|
|
2252
2484
|
after: roadmapContent,
|
|
2253
2485
|
});
|
|
2486
|
+
// #3685 / #3691: normalize both sides before comparing — see
|
|
2487
|
+
// contentChangedAfterNormalize's doc (shell-command-projection.cts).
|
|
2488
|
+
// A raw `!==` here false-positives whenever this phase-complete
|
|
2489
|
+
// roadmap mutation regenerates a section in a different-but-
|
|
2490
|
+
// equivalent raw shape than the already-normalized on-disk original.
|
|
2491
|
+
roadmapUpdated = (0, shell_command_projection_cjs_1.contentChangedAfterNormalize)(roadmapPath, originalRoadmapContent, roadmapContent);
|
|
2254
2492
|
const reqPath = node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
2255
2493
|
if (node_fs_1.default.existsSync(reqPath)) {
|
|
2256
2494
|
const phaseEsc = phaseMarkdownRegexSource(phaseNum);
|
|
@@ -2537,9 +2775,43 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2537
2775
|
// diff-tracking pattern used for the ROADMAP write above. A phase
|
|
2538
2776
|
// whose citations match nothing (ghost REQ-IDs only) must report
|
|
2539
2777
|
// `false`, not a bare "the file was present" `true`.
|
|
2540
|
-
|
|
2778
|
+
// #3685 / #3691: normalize both sides before comparing — same
|
|
2779
|
+
// false-positive shape as the sibling roadmapUpdated/stateUpdated
|
|
2780
|
+
// flags in this same transaction; all three must agree by
|
|
2781
|
+
// construction (see contentChangedAfterNormalize's doc).
|
|
2782
|
+
requirementsUpdated = (0, shell_command_projection_cjs_1.contentChangedAfterNormalize)(reqPath, originalReqContent, reqContent);
|
|
2541
2783
|
}
|
|
2542
2784
|
}
|
|
2785
|
+
// #3701 — the ROADMAP decides WHICH phase is next; the disk decides only HOW it
|
|
2786
|
+
// is spelled. Both scans select the numerically lowest phase above N.
|
|
2787
|
+
//
|
|
2788
|
+
// Both scans below are unchanged in what they match; what changed is that
|
|
2789
|
+
// the roadmap is no longer gated behind "the disk found nothing". It used
|
|
2790
|
+
// to be (`if (isLastPhase && roadmapContent !== null)`), which made a wrong
|
|
2791
|
+
// disk answer uncorrectable: phase directories are created lazily, but
|
|
2792
|
+
// `phase insert` scaffolds an inserted phase's directory immediately, so an
|
|
2793
|
+
// inserted decimal is routinely the ONLY directory above N and outranked
|
|
2794
|
+
// every phase preceding it in the roadmap. Observed: roadmap `1, 2, 02.1,
|
|
2795
|
+
// 3` with directories for 01 and 02.1 only reported `next_phase: "02.1"`
|
|
2796
|
+
// after completing 1 — and PERSISTED it to STATE.md — while
|
|
2797
|
+
// `roadmap.analyze` correctly said `2`.
|
|
2798
|
+
//
|
|
2799
|
+
// #3581 fixed exactly this at `init.progress` and named the rule: "the
|
|
2800
|
+
// frontier is ROADMAP ORDER, not artifact presence". This call site was not
|
|
2801
|
+
// in that change's scope.
|
|
2802
|
+
//
|
|
2803
|
+
// Why the disk scan survives, rather than being replaced:
|
|
2804
|
+
// 1. It is the only resolver when there is no ROADMAP.md, or when its
|
|
2805
|
+
// phase rows do not parse.
|
|
2806
|
+
// 2. When both agree, it carries the SPELLING the output has always used
|
|
2807
|
+
// — the zero-padded directory token and the on-disk slug (`02`/`beta`),
|
|
2808
|
+
// where the roadmap would give `2` and a slugified title. Promoting the
|
|
2809
|
+
// roadmap without this would silently change the reported value on
|
|
2810
|
+
// every aligned project, which is the majority case.
|
|
2811
|
+
let diskNextNum = null;
|
|
2812
|
+
let diskNextName = null;
|
|
2813
|
+
let roadmapNextNum = null;
|
|
2814
|
+
let roadmapNextName = null;
|
|
2543
2815
|
try {
|
|
2544
2816
|
// #3185 (ADR-3180 Decision 1): "which phase directories belong to
|
|
2545
2817
|
// the CURRENT milestone" — routed through the canonical owner
|
|
@@ -2555,11 +2827,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2555
2827
|
// #3185: canonical sentinel predicate (SENTINEL_RANGES [0,999]) — this was a local 999-only literal that admitted Phase 0.
|
|
2556
2828
|
if (isSentinelPhaseId(dm[1]))
|
|
2557
2829
|
continue;
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2830
|
+
// Numeric MINIMUM above N, not "first encountered". `listMilestonePhaseDirs`
|
|
2831
|
+
// does sort by `comparePhaseNum`, so a `break` on the first hit happens to be
|
|
2832
|
+
// correct today — but that makes this scan's correctness depend on an
|
|
2833
|
+
// upstream sort nothing here states. Selecting the minimum explicitly costs
|
|
2834
|
+
// one comparison and removes the hidden coupling.
|
|
2835
|
+
if (comparePhaseNum(dm[1], phaseNum) > 0
|
|
2836
|
+
&& (diskNextNum === null || comparePhaseNum(dm[1], diskNextNum) < 0)) {
|
|
2837
|
+
diskNextNum = dm[1];
|
|
2838
|
+
diskNextName = dm[2] || null;
|
|
2563
2839
|
}
|
|
2564
2840
|
}
|
|
2565
2841
|
}
|
|
@@ -2573,7 +2849,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2573
2849
|
* derives the same information independently from ROADMAP.md content
|
|
2574
2850
|
* — not a silent data-loss path. */
|
|
2575
2851
|
}
|
|
2576
|
-
if (
|
|
2852
|
+
if (roadmapContent !== null) {
|
|
2577
2853
|
try {
|
|
2578
2854
|
const roadmapForPhases = extractCurrentMilestone(roadmapContent, cwd);
|
|
2579
2855
|
// #1591: match BOTH heading-style phases (`### Phase N:`) AND
|
|
@@ -2599,15 +2875,28 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2599
2875
|
// stage 2's heading scan must not advance into backlog headings either.
|
|
2600
2876
|
if (isSentinelPhaseId(pm[1]))
|
|
2601
2877
|
continue;
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2878
|
+
// #3701 review: the numeric MINIMUM above N, not the first row above N in
|
|
2879
|
+
// DOCUMENT order. This scan walks raw roadmap text, and one global regex
|
|
2880
|
+
// sweeps both the `## Phases` checklist and the `## Phase Details`
|
|
2881
|
+
// headings, so "first match" is a statement about where a line sits in the
|
|
2882
|
+
// file — not about which phase comes next.
|
|
2883
|
+
//
|
|
2884
|
+
// It mattered only once this scan started deciding the answer. Before, it
|
|
2885
|
+
// ran solely when the disk scan found nothing; now it outranks the disk, so
|
|
2886
|
+
// a roadmap listing rows out of numeric sequence (`1, 3, 2`) reported
|
|
2887
|
+
// `next_phase: 3` and PERSISTED it, skipping Phase 2 — on an input the
|
|
2888
|
+
// pre-#3701 code got right, because the disk scan is numerically sorted.
|
|
2889
|
+
// Phase NUMBERS define sequence here, exactly as `comparePhaseNum` does for
|
|
2890
|
+
// the disk scan and for #2028's lowest-outstanding override; the roadmap
|
|
2891
|
+
// defines which phases EXIST and which milestone they belong to.
|
|
2892
|
+
if (comparePhaseNum(pm[1], phaseNum) > 0
|
|
2893
|
+
&& (roadmapNextNum === null || comparePhaseNum(pm[1], roadmapNextNum) < 0)) {
|
|
2894
|
+
roadmapNextNum = pm[1];
|
|
2895
|
+
roadmapNextName = pm[2]
|
|
2605
2896
|
.replace(/\(INSERTED\)/i, '')
|
|
2606
2897
|
.trim()
|
|
2607
2898
|
.toLowerCase()
|
|
2608
2899
|
.replace(/\s+/g, '-');
|
|
2609
|
-
isLastPhase = false;
|
|
2610
|
-
break;
|
|
2611
2900
|
}
|
|
2612
2901
|
}
|
|
2613
2902
|
}
|
|
@@ -2618,6 +2907,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2618
2907
|
* regardless and provides a further, independent override. */
|
|
2619
2908
|
}
|
|
2620
2909
|
}
|
|
2910
|
+
// Resolve. The roadmap wins on identity; the disk wins on spelling when it
|
|
2911
|
+
// is talking about the same phase.
|
|
2912
|
+
if (roadmapNextNum !== null) {
|
|
2913
|
+
// Same comparator both scans already use to order phases, so "the disk
|
|
2914
|
+
// and the roadmap mean the same phase" cannot drift from "N is above the
|
|
2915
|
+
// one just completed". `02` and `2` compare equal, which is the whole
|
|
2916
|
+
// point — they are the same phase spelled two ways.
|
|
2917
|
+
const diskAgrees = diskNextNum !== null && comparePhaseNum(diskNextNum, roadmapNextNum) === 0;
|
|
2918
|
+
nextPhaseNum = diskAgrees ? diskNextNum : roadmapNextNum;
|
|
2919
|
+
nextPhaseName = diskAgrees ? diskNextName : roadmapNextName;
|
|
2920
|
+
isLastPhase = false;
|
|
2921
|
+
}
|
|
2922
|
+
else if (diskNextNum !== null) {
|
|
2923
|
+
// No usable roadmap (absent, unreadable, or no parseable phase rows) —
|
|
2924
|
+
// the disk is all there is. Unchanged from the pre-#3701 behaviour.
|
|
2925
|
+
nextPhaseNum = diskNextNum;
|
|
2926
|
+
nextPhaseName = diskNextName;
|
|
2927
|
+
isLastPhase = false;
|
|
2928
|
+
}
|
|
2621
2929
|
// #2028: don't stamp "All phases complete" when a LOWER-numbered phase is
|
|
2622
2930
|
// still outstanding. The two blocks above only clear isLastPhase when a
|
|
2623
2931
|
// HIGHER-numbered phase exists, so completing the numerically-highest phase
|
|
@@ -2767,8 +3075,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2767
3075
|
preservationWarnings.push({ field, reason: 'preserved-over-disagreeing-derived' });
|
|
2768
3076
|
}
|
|
2769
3077
|
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
|
3078
|
+
// #3685 / #3691: normalize both sides before comparing (same
|
|
3079
|
+
// transitionCore-regenerated-section artifact cmdMilestoneComplete
|
|
3080
|
+
// hit — see contentChangedAfterNormalize's doc). Reported "not
|
|
3081
|
+
// exposed" by a previous agent; the reviewer disproved that by
|
|
3082
|
+
// inspection and this branch closes it.
|
|
3083
|
+
stateUpdated = (0, shell_command_projection_cjs_1.contentChangedAfterNormalize)(statePath, originalStateContent, stateContent);
|
|
2770
3084
|
}
|
|
2771
|
-
writePlanningFileSet(writes);
|
|
3085
|
+
anyPlanningWrite = writePlanningFileSet(writes) > 0;
|
|
2772
3086
|
};
|
|
2773
3087
|
if (node_fs_1.default.existsSync(statePath)) {
|
|
2774
3088
|
withStateLock(statePath, runPhaseCompleteTransaction);
|
|
@@ -2826,8 +3140,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2826
3140
|
next_phase_name: nextPhaseName,
|
|
2827
3141
|
is_last_phase: isLastPhase,
|
|
2828
3142
|
date: today,
|
|
2829
|
-
roadmap_updated:
|
|
2830
|
-
state_updated:
|
|
3143
|
+
roadmap_updated: roadmapUpdated,
|
|
3144
|
+
state_updated: stateUpdated,
|
|
2831
3145
|
requirements_updated: requirementsUpdated,
|
|
2832
3146
|
auto_pruned: autoPruned,
|
|
2833
3147
|
warnings,
|
|
@@ -2837,6 +3151,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
2837
3151
|
preservation_warnings: preservationWarnings,
|
|
2838
3152
|
};
|
|
2839
3153
|
output(result, raw);
|
|
3154
|
+
// #3227: gate on `anyPlanningWrite` (whether `writePlanningFileSet`
|
|
3155
|
+
// actually wrote anything), not on reaching this line — reaching here only
|
|
3156
|
+
// means verification passed and the transaction ran, not that ROADMAP.md
|
|
3157
|
+
// or STATE.md bytes changed (see the `anyPlanningWrite` declaration above).
|
|
3158
|
+
if (anyPlanningWrite)
|
|
3159
|
+
publishStateContract(cwd);
|
|
2840
3160
|
}
|
|
2841
3161
|
function cmdPhaseUatPassed(cwd, phaseNum, raw, opts = {}) {
|
|
2842
3162
|
if (!phaseNum) {
|
|
@@ -2893,4 +3213,5 @@ module.exports = {
|
|
|
2893
3213
|
cmdPhaseUatPassed,
|
|
2894
3214
|
cmdPhaseListPlans,
|
|
2895
3215
|
computeDependencyLevels,
|
|
3216
|
+
buildShortFormToId,
|
|
2896
3217
|
};
|