@opengsd/gsd-core 1.11.0 → 1.13.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/.opencode/plugins/gsd-core.js +12 -0
- 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 +78 -42
- 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 +91 -112
- package/agents/gsd-planner.md +20 -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 +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- 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/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- 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-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- 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-aliases.cjs +16 -0
- 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 +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -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 +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- 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/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- 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 +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -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 +195 -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/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- 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-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- 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 +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -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 +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- 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/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- 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 +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- 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 +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- 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 +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- 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/tdd-red-evidence.cjs +133 -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 +2542 -387
- 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/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- 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-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- 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 +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -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/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- 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 +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- 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/mvp-concepts.md +2 -2
- 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/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- 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-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- 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/verifier-evidence-gate.md +160 -0
- 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 +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -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 +29 -5
- 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 +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- 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 +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- 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 +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- 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 +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- 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 +36 -17
- 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 +210 -1
- 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 +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- 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 +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -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 +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -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 +189 -4
- 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/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- 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 +198 -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 +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -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-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -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/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- 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-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- 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
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* ---
|
|
21
21
|
* # Broken Windows Ledger
|
|
22
22
|
* <human-readable prose>
|
|
23
|
-
*
|
|
23
|
+
* ````json
|
|
24
24
|
* [ <entries array, canonical JSON> ]
|
|
25
|
-
*
|
|
25
|
+
* ````
|
|
26
26
|
*
|
|
27
27
|
* Frontmatter holds scalar counts (the FAST path the ship gate reads via jq
|
|
28
28
|
* without parsing JSON). The JSON code block is the AUTHORITATIVE entries
|
|
@@ -56,6 +56,8 @@ exports.markWaived = markWaived;
|
|
|
56
56
|
exports.markFixed = markFixed;
|
|
57
57
|
exports.parseLedger = parseLedger;
|
|
58
58
|
exports.renderLedger = renderLedger;
|
|
59
|
+
exports.renderTable = renderTable;
|
|
60
|
+
exports.extractTableRegion = extractTableRegion;
|
|
59
61
|
exports.cmdWindowsStatus = cmdWindowsStatus;
|
|
60
62
|
exports.cmdWindowsAppend = cmdWindowsAppend;
|
|
61
63
|
exports.cmdWindowsWaive = cmdWindowsWaive;
|
|
@@ -84,6 +86,10 @@ exports.REASON = Object.freeze({
|
|
|
84
86
|
WINDOWS_INVALID_ID: 'windows_invalid_id',
|
|
85
87
|
WINDOWS_APPEND_MISSING_FIELD: 'windows_append_missing_field',
|
|
86
88
|
WINDOWS_USAGE: 'windows_usage',
|
|
89
|
+
// #3689: the rendered markdown table disagreed with the fenced JSON (the
|
|
90
|
+
// sole source of truth) at the pre-write seam — refuse rather than silently
|
|
91
|
+
// reconcile by overwriting the operator's hand-edit or dropping a row.
|
|
92
|
+
WINDOWS_LEDGER_TABLE_DRIFT: 'windows_ledger_table_drift',
|
|
87
93
|
});
|
|
88
94
|
/** Allowed window kinds. Aligned with the issue's enumerated sources. */
|
|
89
95
|
exports.KINDS = Object.freeze([
|
|
@@ -286,6 +292,79 @@ function markFixed(ledger, id, opts = { now: new Date().toISOString() }) {
|
|
|
286
292
|
const JSON_FENCE_OPEN = '````json';
|
|
287
293
|
const JSON_FENCE_CLOSE = '````';
|
|
288
294
|
const FORBIDDEN_BACKTICK_RUN = '````';
|
|
295
|
+
/**
|
|
296
|
+
* #3689: the ledger table's fixed header row literal. `renderTable` emits it
|
|
297
|
+
* on both the empty and non-empty branches; `extractTableRegion` anchors on
|
|
298
|
+
* it to bound the table region. Hoisted to one constant so the two surfaces
|
|
299
|
+
* cannot drift (see "Generative Fix Divergence" — CONTRIBUTING.md).
|
|
300
|
+
*/
|
|
301
|
+
const TABLE_HEADER_LINE = '| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |';
|
|
302
|
+
/**
|
|
303
|
+
* Locate the entries JSON block by CommonMark fence rules rather than a fixed
|
|
304
|
+
* literal width. Both parseJsonBlock (strict) and writeLedgerAtomic's #2893
|
|
305
|
+
* prose preservation (lenient) go through this one function so read tolerance
|
|
306
|
+
* and splice tolerance cannot drift (#3657). A backtick-only line can never
|
|
307
|
+
* occur inside a body: JSON.stringify renders strings single-line-escaped, so
|
|
308
|
+
* an inline run inside a description is never a close-fence candidate.
|
|
309
|
+
*
|
|
310
|
+
* Disambiguation (#3657 security review): an entry description may contain
|
|
311
|
+
* newlines and 3-backtick runs (append validation rejects only 4+ runs), and
|
|
312
|
+
* renderTable renders descriptions into the prose ABOVE the JSON block — so
|
|
313
|
+
* hostile or accidental text can plant a second json fence above the real
|
|
314
|
+
* one. renderLedger always emits the entries block as the FINAL fenced
|
|
315
|
+
* section, so spans are scanned in REVERSE: prefer the latest span whose
|
|
316
|
+
* entries length equals the frontmatter total_count (the real block always
|
|
317
|
+
* satisfies it — parseLedger cross-checks that invariant), else the latest
|
|
318
|
+
* span whose body is a JSON array, else the first span so corrupt bodies keep
|
|
319
|
+
* their fail-closed parse errors. A mirror planted below with identical
|
|
320
|
+
* length and identical entries is indistinguishable by construction — and
|
|
321
|
+
* harmless.
|
|
322
|
+
*/
|
|
323
|
+
function locateJsonBlock(raw, expectedTotal) {
|
|
324
|
+
const spans = [];
|
|
325
|
+
for (const open of raw.matchAll(/^(`{3,})json[ \t]*\r?$/gm)) {
|
|
326
|
+
const width = open[1].length;
|
|
327
|
+
const bodyStart = (open.index ?? 0) + open[0].length;
|
|
328
|
+
for (const close of raw.slice(bodyStart).matchAll(/^(`{3,})[ \t]*\r?$/gm)) {
|
|
329
|
+
if (close[1].length < width)
|
|
330
|
+
continue;
|
|
331
|
+
const bodyEnd = bodyStart + (close.index ?? 0);
|
|
332
|
+
const closeLineEnd = raw.indexOf('\n', bodyEnd);
|
|
333
|
+
spans.push({
|
|
334
|
+
bodyStart,
|
|
335
|
+
bodyEnd,
|
|
336
|
+
afterClose: closeLineEnd === -1 ? raw.length : closeLineEnd + 1,
|
|
337
|
+
});
|
|
338
|
+
break; // CommonMark: the first qualifying close ends this fence block
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (spans.length === 0) {
|
|
342
|
+
const sawOpen = /^(`{3,})json[ \t]*\r?$/m.test(raw);
|
|
343
|
+
return { ok: false, reason: sawOpen ? 'unterminated' : 'missing-open' };
|
|
344
|
+
}
|
|
345
|
+
const parseBody = (s) => {
|
|
346
|
+
try {
|
|
347
|
+
return JSON.parse(raw.slice(s.bodyStart, s.bodyEnd).trim());
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
if (expectedTotal !== undefined) {
|
|
354
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
355
|
+
const body = parseBody(spans[i]);
|
|
356
|
+
if (Array.isArray(body) && body.length === expectedTotal) {
|
|
357
|
+
return { ok: true, span: spans[i] };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
362
|
+
if (Array.isArray(parseBody(spans[i]))) {
|
|
363
|
+
return { ok: true, span: spans[i] };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { ok: true, span: spans[0] };
|
|
367
|
+
}
|
|
289
368
|
/**
|
|
290
369
|
* Minimal strict frontmatter parser for flat scalar keys. Only supports the
|
|
291
370
|
* shape this module emits: `key: <number|string>` per line. Throws on any
|
|
@@ -333,16 +412,14 @@ function parseFrontmatterStrict(raw) {
|
|
|
333
412
|
}
|
|
334
413
|
return out;
|
|
335
414
|
}
|
|
336
|
-
function parseJsonBlock(raw) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (end === -1) {
|
|
343
|
-
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, 'Ledger JSON code block not terminated.');
|
|
415
|
+
function parseJsonBlock(raw, expectedTotal) {
|
|
416
|
+
const span = locateJsonBlock(raw, expectedTotal);
|
|
417
|
+
if (!span.ok) {
|
|
418
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, span.reason === 'missing-open'
|
|
419
|
+
? 'Ledger missing JSON code block for entries.'
|
|
420
|
+
: 'Ledger JSON code block not terminated.');
|
|
344
421
|
}
|
|
345
|
-
const jsonText = raw.slice(
|
|
422
|
+
const jsonText = raw.slice(span.span.bodyStart, span.span.bodyEnd).trim();
|
|
346
423
|
let parsed;
|
|
347
424
|
try {
|
|
348
425
|
parsed = JSON.parse(jsonText);
|
|
@@ -415,7 +492,7 @@ function parseLedger(raw) {
|
|
|
415
492
|
if (typeof fm.last_updated !== 'string') {
|
|
416
493
|
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, `Ledger last_updated must be a string; got ${JSON.stringify(fm.last_updated)}.`);
|
|
417
494
|
}
|
|
418
|
-
const entries = parseJsonBlock(raw);
|
|
495
|
+
const entries = parseJsonBlock(raw, typeof fm.total_count === 'number' ? fm.total_count : undefined);
|
|
419
496
|
const ledger = {
|
|
420
497
|
schema_version: exports.SCHEMA_VERSION,
|
|
421
498
|
open_count: typeof fm.open_count === 'number' ? fm.open_count : 0,
|
|
@@ -464,13 +541,13 @@ function renderLedger(ledger) {
|
|
|
464
541
|
function renderTable(entries) {
|
|
465
542
|
if (entries.length === 0) {
|
|
466
543
|
return [
|
|
467
|
-
|
|
544
|
+
TABLE_HEADER_LINE,
|
|
468
545
|
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
|
|
469
546
|
'| _(none)_ | | | | | _No windows recorded._ | | | | |',
|
|
470
547
|
].join('\n');
|
|
471
548
|
}
|
|
472
549
|
const rows = [
|
|
473
|
-
|
|
550
|
+
TABLE_HEADER_LINE,
|
|
474
551
|
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
|
|
475
552
|
];
|
|
476
553
|
for (const e of entries) {
|
|
@@ -491,6 +568,119 @@ function renderTable(entries) {
|
|
|
491
568
|
}
|
|
492
569
|
return rows.join('\n');
|
|
493
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* #3689: extract the exact markdown table region a rendered ledger emits —
|
|
573
|
+
* the text `renderTable` produced, byte-for-byte — from a raw ledger file.
|
|
574
|
+
* Used by `writeLedgerAtomic`'s drift guard to compare the on-disk table
|
|
575
|
+
* against `renderTable(<on-disk JSON entries>)` without a table parser.
|
|
576
|
+
*
|
|
577
|
+
* Locates the JSON block with the same tolerant `locateJsonBlock` helper the
|
|
578
|
+
* rest of the module uses (#3657), so a formatter-normalized 3-backtick
|
|
579
|
+
* fence still resolves. Everything before the opening fence line, with
|
|
580
|
+
* trailing blank lines dropped, is the candidate region.
|
|
581
|
+
*
|
|
582
|
+
* #3689: the region is bounded by finding the LAST occurrence of the fixed
|
|
583
|
+
* `TABLE_HEADER_LINE` literal (anchored at a line start) within that
|
|
584
|
+
* candidate text, then taking everything from there through its end — NOT
|
|
585
|
+
* by scanning backward for a contiguous run of `|`-prefixed lines. A `|`
|
|
586
|
+
* prefix scan cannot bound the region: `validateDescription` rejects only
|
|
587
|
+
* empty strings and 4-backtick runs, so a description may contain a raw
|
|
588
|
+
* `\n`, and `renderTable`'s `cell()` escapes `\` and `|` but not newlines.
|
|
589
|
+
* Such a description renders a row that physically spans multiple file
|
|
590
|
+
* lines, and the continuation line does not start with `|` — a prefix scan
|
|
591
|
+
* either truncates the table or, when the row's tail is the last pre-fence
|
|
592
|
+
* line, returns null immediately, bricking every subsequent write with
|
|
593
|
+
* `WINDOWS_LEDGER_TABLE_DRIFT` on a ledger nobody hand-edited. Anchoring on
|
|
594
|
+
* the header instead includes any such row whole, so `renderTable`
|
|
595
|
+
* regenerates byte-identical text for it and the drift comparison passes.
|
|
596
|
+
*
|
|
597
|
+
* Returns null when the JSON block cannot be located, or no header line is
|
|
598
|
+
* present.
|
|
599
|
+
*
|
|
600
|
+
* `expectedTotal` (#3689 review finding 2) is threaded straight into
|
|
601
|
+
* `locateJsonBlock` so callers with trailing prose can disambiguate the real
|
|
602
|
+
* ledger block from an unrelated fenced JSON array a user pasted below the
|
|
603
|
+
* closing fence — without it, `locateJsonBlock`'s no-hint fallback picks the
|
|
604
|
+
* LATEST array-shaped span, which is the prose block, not the ledger, and
|
|
605
|
+
* every drift comparison then binds to the wrong table/JSON pairing.
|
|
606
|
+
*/
|
|
607
|
+
function extractTableRegion(raw, expectedTotal) {
|
|
608
|
+
const span = locateJsonBlock(raw, expectedTotal);
|
|
609
|
+
if (!span.ok)
|
|
610
|
+
return null;
|
|
611
|
+
// bodyStart sits right before the newline (or CR) ending the opening fence
|
|
612
|
+
// line; walk back to the start of that line.
|
|
613
|
+
const fenceLineStart = raw.lastIndexOf('\n', span.span.bodyStart - 1) + 1;
|
|
614
|
+
const before = raw.slice(0, fenceLineStart).replace(/\r\n/g, '\n');
|
|
615
|
+
let trimmedEnd = before.length;
|
|
616
|
+
while (trimmedEnd > 0 && before[trimmedEnd - 1] === '\n') {
|
|
617
|
+
trimmedEnd--;
|
|
618
|
+
}
|
|
619
|
+
const candidate = before.slice(0, trimmedEnd);
|
|
620
|
+
// Find the LAST occurrence of TABLE_HEADER_LINE anchored at a line start —
|
|
621
|
+
// a plain string scan rather than a regex, since the module is a leaf
|
|
622
|
+
// (imports only node:fs/node:path) and cannot pull in the shared
|
|
623
|
+
// escapeRegex() helper for a one-off fixed-literal search.
|
|
624
|
+
let headerIndex = -1;
|
|
625
|
+
let searchFrom = candidate.length;
|
|
626
|
+
for (;;) {
|
|
627
|
+
const idx = candidate.lastIndexOf(TABLE_HEADER_LINE, searchFrom);
|
|
628
|
+
if (idx === -1)
|
|
629
|
+
break;
|
|
630
|
+
const atLineStart = idx === 0 || candidate[idx - 1] === '\n';
|
|
631
|
+
const atLineEnd = idx + TABLE_HEADER_LINE.length === candidate.length ||
|
|
632
|
+
candidate[idx + TABLE_HEADER_LINE.length] === '\n';
|
|
633
|
+
if (atLineStart && atLineEnd) {
|
|
634
|
+
headerIndex = idx;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
// #3689: lastIndexOf clamps a negative position into [0, length] per
|
|
638
|
+
// spec, so `searchFrom = -1` would re-search from 0 and re-find the same
|
|
639
|
+
// rejected match at idx===0 forever. Stop explicitly once there is
|
|
640
|
+
// nowhere left to search — this makes the bound strictly decrease each
|
|
641
|
+
// iteration, so the loop terminates within candidate.length steps.
|
|
642
|
+
if (idx === 0)
|
|
643
|
+
break;
|
|
644
|
+
searchFrom = idx - 1;
|
|
645
|
+
}
|
|
646
|
+
if (headerIndex === -1)
|
|
647
|
+
return null;
|
|
648
|
+
return candidate.slice(headerIndex);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* #3689: diff two `renderTable` outputs by row id (the first cell of each
|
|
652
|
+
* data row), skipping the header + separator lines (always exactly two).
|
|
653
|
+
* A row whose line text differs between the two tables, or that is present
|
|
654
|
+
* in only one of them, contributes its id to the result — this is what lets
|
|
655
|
+
* the drift-guard error message name the specific drifted/table-only row(s)
|
|
656
|
+
* rather than just saying "the table disagrees".
|
|
657
|
+
*/
|
|
658
|
+
function diffTableRowIds(expectedTable, actualTable) {
|
|
659
|
+
const rowId = (line) => (line.split('|')[1] ?? '').trim();
|
|
660
|
+
const dataRows = (table) => {
|
|
661
|
+
const lines = table.split('\n');
|
|
662
|
+
const map = new Map();
|
|
663
|
+
for (let i = 2; i < lines.length; i++) {
|
|
664
|
+
const line = lines[i];
|
|
665
|
+
if (!line.startsWith('|'))
|
|
666
|
+
continue;
|
|
667
|
+
map.set(rowId(line), line);
|
|
668
|
+
}
|
|
669
|
+
return map;
|
|
670
|
+
};
|
|
671
|
+
const expectedRows = dataRows(expectedTable);
|
|
672
|
+
const actualRows = dataRows(actualTable);
|
|
673
|
+
const ids = new Set();
|
|
674
|
+
for (const [id, line] of expectedRows) {
|
|
675
|
+
if (actualRows.get(id) !== line)
|
|
676
|
+
ids.add(id);
|
|
677
|
+
}
|
|
678
|
+
for (const id of actualRows.keys()) {
|
|
679
|
+
if (!expectedRows.has(id))
|
|
680
|
+
ids.add(id);
|
|
681
|
+
}
|
|
682
|
+
return Array.from(ids).sort();
|
|
683
|
+
}
|
|
494
684
|
// ─── I/O entry points ──────────────────────────────────────────────────────
|
|
495
685
|
function ledgerPath(cwd) {
|
|
496
686
|
return node_path_1.default.join(cwd, '.planning', exports.LEDGER_FILE_NAME);
|
|
@@ -569,23 +759,111 @@ function writeLedgerAtomic(cwd, ledger) {
|
|
|
569
759
|
// trailing prose that users may have written below the closing fence.
|
|
570
760
|
// Without this, every append/waive/fixed silently destroys that prose.
|
|
571
761
|
let trailingProse = '';
|
|
762
|
+
// #3689: read the pre-image once into `existing` outside the catch, rather
|
|
763
|
+
// than doing every subsequent step inside a bare try/catch, so that a
|
|
764
|
+
// WindowsError thrown by the drift guard below propagates instead of being
|
|
765
|
+
// swallowed by the ENOENT handler meant only for "no ledger yet".
|
|
766
|
+
let existing = null;
|
|
572
767
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// the
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
768
|
+
existing = node_fs_1.default.readFileSync(p, 'utf8');
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
// #1950-H2 / #3689: ENOENT is the only "no ledger yet" case — mirror
|
|
772
|
+
// readLedgerOrNull's discipline exactly. A bare catch here would let
|
|
773
|
+
// EACCES/EIO/ENOTDIR/etc. fall through as "no pre-image", silently
|
|
774
|
+
// skipping BOTH the #2893 prose preservation and the drift guard below
|
|
775
|
+
// and proceeding to overwrite an unreadable file — a guard bypassable by
|
|
776
|
+
// making the pre-image unreadable is not a guard.
|
|
777
|
+
const code = (e && typeof e === 'object' && 'code' in e)
|
|
778
|
+
? String(e.code)
|
|
779
|
+
: '';
|
|
780
|
+
if (code !== 'ENOENT') {
|
|
781
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, `Could not read ledger at ${p} (${code || 'unknown fs error'}): ${e.message}.`);
|
|
585
782
|
}
|
|
783
|
+
// File doesn't exist yet (first write) — no prose to preserve, and
|
|
784
|
+
// nothing on disk to disagree with, so the drift guard below is skipped.
|
|
586
785
|
}
|
|
587
|
-
|
|
588
|
-
//
|
|
786
|
+
if (existing !== null) {
|
|
787
|
+
// #3689 review finding 2 / #3689 bug discovery: both the #2893 prose
|
|
788
|
+
// span AND the drift guard below must disambiguate `locateJsonBlock`
|
|
789
|
+
// against the SAME pre-image ledger block, so this is computed ONCE,
|
|
790
|
+
// hoisted above both uses. expectedTotal MUST be derived from the
|
|
791
|
+
// PRE-IMAGE's own frontmatter (never `ledger.total_count`, which is
|
|
792
|
+
// already post-mutation — e.g. N+1 on an append): #2893 exists precisely
|
|
793
|
+
// because operators may paste prose below the closing fence, and that
|
|
794
|
+
// prose can itself contain a fenced JSON array of a different length.
|
|
795
|
+
// Passing the post-mutation total here (as a since-fixed #3689 review
|
|
796
|
+
// pass once did for the guard alone) makes locateJsonBlock's expectedTotal
|
|
797
|
+
// scan find nothing against the pre-image — no span has N+1 entries yet —
|
|
798
|
+
// so it silently falls through to the no-hint fallback, which binds to
|
|
799
|
+
// the LATEST array-shaped span: the prose block, not the ledger. Left
|
|
800
|
+
// unfixed, that means the #2893 prose-preservation span itself would
|
|
801
|
+
// resolve to the prose fence's `afterClose`, silently dropping
|
|
802
|
+
// everything between the real ledger block and the prose block —
|
|
803
|
+
// including the operator's own prose ABOVE that array — on every
|
|
804
|
+
// append. This is exactly the failure #2893 was written to prevent,
|
|
805
|
+
// reintroduced through the disambiguation hint; it is caught here by
|
|
806
|
+
// deriving the hint from the pre-image, not the post-mutation ledger,
|
|
807
|
+
// for BOTH call sites below. If the pre-image frontmatter cannot be
|
|
808
|
+
// parsed unambiguously, that is itself the ambiguous case — fail closed
|
|
809
|
+
// rather than falling back to the no-hint scan.
|
|
810
|
+
let preImageExpectedTotal;
|
|
811
|
+
try {
|
|
812
|
+
const preFm = parseFrontmatterStrict(existing);
|
|
813
|
+
if (typeof preFm.total_count !== 'number' || !Number.isInteger(preFm.total_count)) {
|
|
814
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, `Ledger frontmatter total_count in ${p} is not an integer; refusing to write — ` +
|
|
815
|
+
'the ledger JSON block cannot be identified unambiguously.');
|
|
816
|
+
}
|
|
817
|
+
preImageExpectedTotal = preFm.total_count;
|
|
818
|
+
}
|
|
819
|
+
catch (e) {
|
|
820
|
+
if (e instanceof WindowsError)
|
|
821
|
+
throw e;
|
|
822
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, `Ledger frontmatter in ${p} could not be parsed (${e.message}); refusing ` +
|
|
823
|
+
'to write — the ledger JSON block cannot be identified unambiguously.');
|
|
824
|
+
}
|
|
825
|
+
// #2893: search for the CLOSING fence starting AFTER the opening fence.
|
|
826
|
+
// The span is located with the same tolerant + disambiguated fence rules
|
|
827
|
+
// parseJsonBlock uses (#3657), so a formatter-normalized 3-backtick ledger
|
|
828
|
+
// keeps its prose too — a literal-width search here would find no block
|
|
829
|
+
// and silently drop everything below the ledger on the next write. The
|
|
830
|
+
// hint passed here is `preImageExpectedTotal` (pre-image derived, see
|
|
831
|
+
// above) — NOT `ledger.total_count` — so this binds to the same span the
|
|
832
|
+
// drift guard below does.
|
|
833
|
+
const span = locateJsonBlock(existing, preImageExpectedTotal);
|
|
834
|
+
if (span.ok) {
|
|
835
|
+
const afterFence = existing.slice(span.span.afterClose);
|
|
836
|
+
// Drop leading newlines; keep the rest as prose.
|
|
837
|
+
trailingProse = afterFence.replace(/^(?:\r?\n)+/, '');
|
|
838
|
+
}
|
|
839
|
+
// #3689 review finding 2: refuse the write if the on-disk table has
|
|
840
|
+
// drifted from the on-disk JSON — the source of truth — BEFORE anything
|
|
841
|
+
// is regenerated. Baseline is the ON-DISK entries, not `ledger` (already
|
|
842
|
+
// the post-mutation state: an appended entry or a changed status);
|
|
843
|
+
// comparing against `ledger` would report drift on every legitimate
|
|
844
|
+
// write.
|
|
845
|
+
const onDiskEntries = parseJsonBlock(existing, preImageExpectedTotal);
|
|
846
|
+
const expectedTable = renderTable(onDiskEntries);
|
|
847
|
+
const actualTable = extractTableRegion(existing, preImageExpectedTotal);
|
|
848
|
+
if (actualTable === null) {
|
|
849
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_TABLE_DRIFT, `Ledger table region could not be located in ${p}; refusing to write. Edit the ` +
|
|
850
|
+
'fenced JSON block directly — the sole source of truth — or delete the corrupted ' +
|
|
851
|
+
'table region and let gsd-tools regenerate it; never hand-edit the rendered table.');
|
|
852
|
+
}
|
|
853
|
+
if (actualTable !== expectedTable) {
|
|
854
|
+
const driftedIds = diffTableRowIds(expectedTable, actualTable);
|
|
855
|
+
// #3689 review finding 3: a header/separator-only drift (e.g. a
|
|
856
|
+
// hand-edited column name or mangled separator) produces no data-row
|
|
857
|
+
// diffs, so driftedIds is empty — naming nothing would read "...for
|
|
858
|
+
// row id(s): .". Say what actually differs instead.
|
|
859
|
+
const driftDescription = driftedIds.length > 0
|
|
860
|
+
? `for row id(s): ${driftedIds.join(', ')}`
|
|
861
|
+
: "in its header or separator row (no data row differs from the expected rendering)";
|
|
862
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_TABLE_DRIFT, `Ledger table in ${p} disagrees with the fenced JSON entries (the sole source of ` +
|
|
863
|
+
`truth) ${driftDescription}. Edit the fenced JSON block ` +
|
|
864
|
+
'directly, or discard the table edit and re-run the command so gsd-tools ' +
|
|
865
|
+
'regenerates the table; never hand-edit the rendered table.');
|
|
866
|
+
}
|
|
589
867
|
}
|
|
590
868
|
const rendered = renderLedger(ledger);
|
|
591
869
|
const content = trailingProse ? `${rendered}${trailingProse}` : rendered;
|
|
@@ -105,9 +105,36 @@ function _resolveActivationValue(dotKey, config, cwd, registry) {
|
|
|
105
105
|
const r = resolveConfigKey(dotKey, { config, cwd, registry });
|
|
106
106
|
return r.found ? Boolean(r.value) : false;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a step's optional `pointFrom` gate (#3661): a step declaring
|
|
110
|
+
* `pointFrom: "<dotted enum key>"` is only active for ITS OWN `point` when the
|
|
111
|
+
* resolved value of that config key equals `point` exactly. This lets a
|
|
112
|
+
* capability register the same logical step at more than one loop point and
|
|
113
|
+
* have config select which registration is live — without teaching the
|
|
114
|
+
* shared `when` grammar an equality operator (`when` stays a plain boolean
|
|
115
|
+
* gate on a dotted key everywhere else).
|
|
116
|
+
*
|
|
117
|
+
* No `pointFrom` → true (unconditional; every existing step is unaffected).
|
|
118
|
+
* Present but not a non-empty string → false (malformed, mirrors `when`).
|
|
119
|
+
* Present and resolved → true only on an exact match against `point`.
|
|
120
|
+
*
|
|
121
|
+
* Single-owner precedence engine: called from both `loop-resolver.cts`
|
|
122
|
+
* (`isActive`) and `capability-state.cts` (`processHooks`) so the two
|
|
123
|
+
* consumers can never diverge on activation (see
|
|
124
|
+
* `tests/capability-precedence-parity.test.cjs`).
|
|
125
|
+
*/
|
|
126
|
+
function _resolvePointGate(pointFrom, point, config, cwd, registry) {
|
|
127
|
+
if (pointFrom === undefined || pointFrom === null)
|
|
128
|
+
return true;
|
|
129
|
+
if (typeof pointFrom !== 'string' || pointFrom.length === 0)
|
|
130
|
+
return false;
|
|
131
|
+
const r = resolveConfigKey(pointFrom, { config, cwd, registry });
|
|
132
|
+
return r.found && r.value === point;
|
|
133
|
+
}
|
|
108
134
|
module.exports = {
|
|
109
135
|
_getNestedConfigValue,
|
|
110
136
|
_readRawConfigKey,
|
|
111
137
|
_resolveActivationValue,
|
|
138
|
+
_resolvePointGate,
|
|
112
139
|
resolveConfigKey,
|
|
113
140
|
};
|
|
@@ -350,10 +350,16 @@ function lockBodyToken(body) {
|
|
|
350
350
|
* and the whole thing is a BOUNDED iterative loop.
|
|
351
351
|
*/
|
|
352
352
|
function acquireLock(lockPath, opts) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
353
|
+
// A genuine failure here (EACCES/ENOSPC/EROFS) MUST surface immediately, matching the #1884
|
|
354
|
+
// fix (0c43d853e / PR #3472) for withPlanningLock's identical shape. The prior
|
|
355
|
+
// `catch { /* best-effort */ }` swallowed it, and the subsequent `fs.openSync(lockPath, 'wx')`
|
|
356
|
+
// below then failed with ENOENT (parent dir missing) — which is NOT 'EEXIST', so the
|
|
357
|
+
// `if (code !== 'EEXIST') return null;` branch laundered a fatal filesystem error into an
|
|
358
|
+
// ordinary "lock unavailable" (null) result, indistinguishable from another live process
|
|
359
|
+
// legitimately holding the lock (#3987). `mkdirSync(recursive:true)` does not throw when the
|
|
360
|
+
// directory already exists, so the normal path (dir already present) is unaffected; only real
|
|
361
|
+
// creation failures propagate.
|
|
362
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(lockPath), { recursive: true });
|
|
357
363
|
const maxAttempts = (opts && Number.isInteger(opts.maxAttempts) && opts.maxAttempts > 0)
|
|
358
364
|
? opts.maxAttempts
|
|
359
365
|
: LOCK_MAX_ATTEMPTS;
|