@opengsd/gsd-core 1.9.1 → 1.11.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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +608 -148
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +160 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +33 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anti-divergence drift guard for the completion-RATIO seam
|
|
6
|
+
* (epic #3180, ADR-3180 "Planning Semantic Model Single Owner").
|
|
7
|
+
*
|
|
8
|
+
* `src/phase-lifecycle.cts`'s `clampPercent(completed, total)` /
|
|
9
|
+
* `clampPercentFromFraction(fraction)` are the SINGLE canonical owner of
|
|
10
|
+
* "turn a completed/total pair into an integer completion percentage,
|
|
11
|
+
* clamped to 100". Until just before this guard was added, the identical
|
|
12
|
+
* expression `total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0`
|
|
13
|
+
* was hand-inlined at six call sites across five modules while the owner sat
|
|
14
|
+
* exported and unused by them — the exact ADR-3180 divergence class, in a
|
|
15
|
+
* derivation the epic had not previously named. Those six sites have been
|
|
16
|
+
* migrated onto the owner; this guard is what stops a seventh copy.
|
|
17
|
+
*
|
|
18
|
+
* Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
|
|
19
|
+
* WHOLE `src/` TREE, not by consulting an allowlist of known files — an
|
|
20
|
+
* allowlist only measures re-derivations in files someone remembered to
|
|
21
|
+
* list, and a new call site added anywhere else would sail through silently.
|
|
22
|
+
*
|
|
23
|
+
* DETECTION. A line is a re-derivation when ALL THREE hold, on that ONE
|
|
24
|
+
* source line:
|
|
25
|
+
* (a) it calls one of the `Math.round(`/`Math.floor(`/`Math.trunc(`/
|
|
26
|
+
* `Math.ceil(` rounding family — MATH_ROUND_FAMILY_RE;
|
|
27
|
+
* (b) it SCALES by 100 — a `*` followed by optional whitespace then `100`
|
|
28
|
+
* at a word boundary — SCALE_100_RE;
|
|
29
|
+
* (c) it contains a DIVISION — an identifier/closing-bracket, optional
|
|
30
|
+
* whitespace, `/`, optional whitespace, an identifier/opening-paren —
|
|
31
|
+
* DIVISION_RE — AND that division's index in the line is EARLIER than
|
|
32
|
+
* the index of the `* 100` scale from (b).
|
|
33
|
+
*
|
|
34
|
+
* Clause (c)'s ORDERING requirement is the whole precision of this guard.
|
|
35
|
+
* `(a / b) * 100` — divide FIRST, scale SECOND — is a percentage: the
|
|
36
|
+
* completed/total-derived shape this guard exists to catch. `Math.round(n *
|
|
37
|
+
* 100) / 100` — scale FIRST, divide SECOND — is a completely unrelated
|
|
38
|
+
* idiom (2-decimal-place rounding of an already-fractional value) that
|
|
39
|
+
* happens to share both a rounding call and a `* 100` token; it appears in
|
|
40
|
+
* this repo at `src/eval.cts` and `src/commands.cts` and MUST stay
|
|
41
|
+
* unflagged. Comparing leftmost-match indices (rather than merely testing
|
|
42
|
+
* "does a division exist anywhere on the line") is what tells the two
|
|
43
|
+
* idioms apart: this guard finds the EARLIEST division and the EARLIEST
|
|
44
|
+
* `* 100` scale on the line and requires divIdx < scaleIdx, so a line with a
|
|
45
|
+
* scale-then-divide shape (divIdx > scaleIdx, or no division at all) never
|
|
46
|
+
* matches, regardless of what else is on the line.
|
|
47
|
+
*
|
|
48
|
+
* `Math.floor(Math.random() * 100)` carries (a) and (b) but no division
|
|
49
|
+
* anywhere on the line (DIVISION_RE finds nothing, divIdx === -1) and is
|
|
50
|
+
* correctly excluded by clause (c) alone.
|
|
51
|
+
*
|
|
52
|
+
* `src/context-utilization.cts`'s `Math.min(Math.round(ratio * 100), 100)`
|
|
53
|
+
* is OUT OF SCOPE BY DOMAIN, not by exemption: it scales an
|
|
54
|
+
* ALREADY-COMPUTED fraction (`ratio`, a context-window utilization figure —
|
|
55
|
+
* unrelated to `.planning/` phase/plan completion) and there is no division
|
|
56
|
+
* anywhere on that line either, so clause (c) excludes it the same way as
|
|
57
|
+
* the `Math.random()` case above; it needs no FUNCTION_SCOPED_EXEMPTIONS
|
|
58
|
+
* entry because it was never going to match.
|
|
59
|
+
*
|
|
60
|
+
* Every regex below is small, bounded, and has no nested/overlapping
|
|
61
|
+
* quantifiers — each character class is followed by a fixed literal or a
|
|
62
|
+
* single `\s*` run bounded by the next required literal, so there is
|
|
63
|
+
* nothing for a backtracking engine to explore more than linearly.
|
|
64
|
+
* `npm run lint:ci` runs CodeQL js/redos over this repo; mirrors the
|
|
65
|
+
* ReDoS discipline of `lint-plan-count-drift.cjs` / `lint-milestone-window-drift.cjs`.
|
|
66
|
+
*
|
|
67
|
+
* The tree-walk / root-confinement / sanitizer machinery is SHARED with the
|
|
68
|
+
* sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision 4)
|
|
69
|
+
* — see that module for the `isInsideRoot` case-sensitivity note and the
|
|
70
|
+
* `walk` symlink-confinement rationale. This guard's detection shape needs
|
|
71
|
+
* no regex-LITERAL extraction (unlike the milestone-window guard), so it
|
|
72
|
+
* does not use `readRegexLiteralAt`; the reported fragment is simply the
|
|
73
|
+
* trimmed source line, bounded to MAX_REGEX_LITERAL_LEN characters.
|
|
74
|
+
*
|
|
75
|
+
* KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the
|
|
76
|
+
* sibling drift guards document): a re-derivation whose division and
|
|
77
|
+
* `Math.round`/scale are split across two DIFFERENT lines with no single
|
|
78
|
+
* line carrying all three tokens is not caught by this narrow shape, nor is
|
|
79
|
+
* one routed through a helper that itself performs the division one call
|
|
80
|
+
* away from the rounding. That is left to code review, not this regex.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
const path = require('node:path');
|
|
84
|
+
const driftScan = require('./lib/drift-scan.cjs');
|
|
85
|
+
const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
|
|
86
|
+
|
|
87
|
+
// (a) The `Math.round`/`Math.floor`/`Math.trunc`/`Math.ceil` rounding family,
|
|
88
|
+
// called with an open paren. `\b` before `Math` keeps this from matching
|
|
89
|
+
// inside a longer identifier (e.g. `fooMath.round(` never occurs in this
|
|
90
|
+
// codebase, but the boundary costs nothing and documents intent).
|
|
91
|
+
const MATH_ROUND_FAMILY_RE = /\bMath\.(?:round|floor|trunc|ceil)\(/;
|
|
92
|
+
|
|
93
|
+
// (b) A `* 100` scale — a `*` operator, optional whitespace, then the
|
|
94
|
+
// literal digits `100` at a word boundary (so `*1000` or `*100.5` do not
|
|
95
|
+
// match a bare `100` inside a longer number).
|
|
96
|
+
const SCALE_100_RE = /\*\s*100\b/;
|
|
97
|
+
|
|
98
|
+
// (c) A division: an identifier character/closing-bracket (the end of the
|
|
99
|
+
// numerator expression), optional whitespace, `/`, optional whitespace, an
|
|
100
|
+
// identifier character/opening-paren (the start of the denominator
|
|
101
|
+
// expression). Deliberately does not try to distinguish this from a regex
|
|
102
|
+
// literal or a `//` comment — the detection window is a Math.round-family
|
|
103
|
+
// call on the same line, which neither idiom co-occurs with in practice, and
|
|
104
|
+
// keeping the class small is what keeps the regex non-backtracking.
|
|
105
|
+
const DIVISION_RE = /[A-Za-z0-9_$)\]]\s*\/\s*[A-Za-z0-9_$(]/;
|
|
106
|
+
|
|
107
|
+
// Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
|
|
108
|
+
const SCAN_DIRS = ['src'];
|
|
109
|
+
const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
|
|
110
|
+
|
|
111
|
+
// The canonical owner defines the ratio-to-percent grammar. It is NOT
|
|
112
|
+
// exempt as a whole file (ADR-3180 Decision 4(a) forbids bare file
|
|
113
|
+
// allowlists) — it is scanned like every other file in SCAN_DIRS, and only
|
|
114
|
+
// the two named functions below are exempt, each for a documented reason.
|
|
115
|
+
// An unrelated re-derivation added elsewhere in this same file (including a
|
|
116
|
+
// future one) is still caught.
|
|
117
|
+
const OWNER_FILE = path.join('src', 'phase-lifecycle.cts');
|
|
118
|
+
|
|
119
|
+
// Per ADR-3180 Decision 4(a): function-scoped, not a bare file allowlist.
|
|
120
|
+
// - clampPercentFromFraction: `Math.min(100, Math.round(fraction * 100))`
|
|
121
|
+
// IS the canonical fraction-to-percent kernel this guard exists to
|
|
122
|
+
// protect, not a copy of it — every other caller in the tree is
|
|
123
|
+
// expected to CALL this function rather than re-express its body.
|
|
124
|
+
// - clampPercent: the canonical count-shaped entry point; it delegates to
|
|
125
|
+
// `clampPercentFromFraction(completed / total)` rather than computing
|
|
126
|
+
// `Math.round(...)` itself, so it is exempted for the same reason even
|
|
127
|
+
// though its own line does not currently carry a Math.round-family call.
|
|
128
|
+
const FUNCTION_SCOPED_EXEMPTIONS = new Map([[OWNER_FILE, new Set(['clampPercent', 'clampPercentFromFraction'])]]);
|
|
129
|
+
|
|
130
|
+
// Optional `export ` modifier, matching the sibling guards' convention —
|
|
131
|
+
// only a column-0 top-level `function` declaration updates the
|
|
132
|
+
// current-function tracker.
|
|
133
|
+
const TOP_LEVEL_FUNCTION_RE = /^(?:export\s+)?function\s+([A-Za-z0-9_]+)\s*\(/;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Pure: find every unsanctioned completion-ratio re-derivation in `text`.
|
|
137
|
+
* `relPath` is the repo-relative path, used both to report file:line and to
|
|
138
|
+
* apply the narrow, function-scoped owner exemptions above.
|
|
139
|
+
* Returns [{ line, found }].
|
|
140
|
+
*/
|
|
141
|
+
function findCompletionRatioDrift(text, relPath) {
|
|
142
|
+
const out = [];
|
|
143
|
+
const lines = text.split('\n');
|
|
144
|
+
const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
|
|
145
|
+
let currentFunction = null;
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i];
|
|
148
|
+
const fnMatch = TOP_LEVEL_FUNCTION_RE.exec(line);
|
|
149
|
+
if (fnMatch) currentFunction = fnMatch[1];
|
|
150
|
+
|
|
151
|
+
if (!MATH_ROUND_FAMILY_RE.test(line)) continue;
|
|
152
|
+
const scaleIdx = line.search(SCALE_100_RE);
|
|
153
|
+
if (scaleIdx === -1) continue;
|
|
154
|
+
const divIdx = line.search(DIVISION_RE);
|
|
155
|
+
if (divIdx === -1 || divIdx >= scaleIdx) continue;
|
|
156
|
+
|
|
157
|
+
if (exemptFunctions && exemptFunctions.has(currentFunction)) continue;
|
|
158
|
+
|
|
159
|
+
out.push({ line: i + 1, found: line.trim().slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Scan the authored source tree and return every unsanctioned re-derivation,
|
|
166
|
+
* each annotated with the repo-relative file path.
|
|
167
|
+
*/
|
|
168
|
+
function scanRepo(root) {
|
|
169
|
+
return scanTree({
|
|
170
|
+
root,
|
|
171
|
+
scanDirs: SCAN_DIRS,
|
|
172
|
+
scanExt: SCAN_EXT,
|
|
173
|
+
onFile(rel, text) {
|
|
174
|
+
// `rel` is already the REAL (canonical) path (scanTree resolves
|
|
175
|
+
// symlinks before calling onFile), so this — and
|
|
176
|
+
// FUNCTION_SCOPED_EXEMPTIONS above, also keyed on `rel` — match
|
|
177
|
+
// consistently regardless of which symlink reached the file.
|
|
178
|
+
return findCompletionRatioDrift(text, rel).map((d) => ({ file: rel, ...d }));
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function main() {
|
|
184
|
+
const root = path.join(__dirname, '..');
|
|
185
|
+
const violations = scanRepo(root);
|
|
186
|
+
if (violations.length === 0) {
|
|
187
|
+
process.stdout.write('ok completion-ratio-drift: no unsanctioned completed/total percent re-derivations outside phase-lifecycle.cts\n');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
process.stderr.write('completion-ratio-drift: independent re-derivation(s) of completed/total percent found.\n');
|
|
191
|
+
process.stderr.write('Use src/phase-lifecycle.cjs `clampPercent(completed, total)` (or `clampPercentFromFraction(fraction)`\n');
|
|
192
|
+
process.stderr.write('when you already hold a fraction) instead of re-deriving Math.round((completed / total) * 100):\n');
|
|
193
|
+
for (const d of violations) {
|
|
194
|
+
// `d.file` is exactly as attacker-controlled as `d.found`: a repo can
|
|
195
|
+
// legally track a filename containing control bytes / bidi overrides,
|
|
196
|
+
// and it is a fork-PR-authored value reaching a CI log the same way the
|
|
197
|
+
// matched line text does — sanitize it at the same reporting boundary.
|
|
198
|
+
process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
|
|
199
|
+
}
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (require.main === module) main();
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
findCompletionRatioDrift,
|
|
207
|
+
scanRepo,
|
|
208
|
+
MATH_ROUND_FAMILY_RE,
|
|
209
|
+
SCALE_100_RE,
|
|
210
|
+
DIVISION_RE,
|
|
211
|
+
OWNER_FILE,
|
|
212
|
+
FUNCTION_SCOPED_EXEMPTIONS,
|
|
213
|
+
MAX_REGEX_LITERAL_LEN,
|
|
214
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-default-flip-documentation.cjs — DEFECT.DEFAULT-FLIP-DOCUMENTATION
|
|
6
|
+
* (CONTEXT.md).
|
|
7
|
+
*
|
|
8
|
+
* ## Why
|
|
9
|
+
*
|
|
10
|
+
* A PR flips a config default but doesn't call out the migration semantics
|
|
11
|
+
* (when the new default takes effect; existing configs vs new configs; what
|
|
12
|
+
* the opt-back-in looks like — #3309, the v2 default flip from mid-flight to
|
|
13
|
+
* end-of-phase).
|
|
14
|
+
*
|
|
15
|
+
* ## Scope (deliberately narrower than the full DEFECT.detect clause)
|
|
16
|
+
*
|
|
17
|
+
* The DEFECT text names two surfaces: `CONFIG_DEFAULTS` and
|
|
18
|
+
* `buildNewProjectConfig`. This check covers ONLY the single-source-of-truth
|
|
19
|
+
* defaults manifest, `gsd-core/bin/shared/config-defaults.manifest.json`
|
|
20
|
+
* (what `CONFIG_DEFAULTS` in `src/configuration.cts` / `src/config.cts`
|
|
21
|
+
* actually loads at runtime) — because it is pure JSON, a resolved
|
|
22
|
+
* key→value-map diff between base and head is trivially reliable: no line
|
|
23
|
+
* movement, reordering, or refactor can ever produce a false "value changed"
|
|
24
|
+
* verdict, only an actual value change can.
|
|
25
|
+
*
|
|
26
|
+
* `buildNewProjectConfig`'s `hardcoded` object literal in `src/config.cts`
|
|
27
|
+
* is DELIBERATELY OUT OF SCOPE here. It mixes literal values with
|
|
28
|
+
* environment-derived branches (`hasBraveSearch`, etc.) and spreads of
|
|
29
|
+
* `CONFIG_DEFAULTS.*` — there is no reliable way to compute its *resolved*
|
|
30
|
+
* value map from source text alone without executing the compiled module at
|
|
31
|
+
* both refs, and a line/AST-level diff of that literal would inherit exactly
|
|
32
|
+
* the false-positive risk (a harmless refactor that moves or restructures
|
|
33
|
+
* the literal reads as a "flip") this check exists to avoid. Per the audit's
|
|
34
|
+
* own risk callout, a noisy check here is worse than no check — the
|
|
35
|
+
* `buildNewProjectConfig` half of the DEFECT stays prose-only.
|
|
36
|
+
*
|
|
37
|
+
* ## What this checks
|
|
38
|
+
*
|
|
39
|
+
* If any *value* differs between the base and head resolved manifest
|
|
40
|
+
* key→value maps (additions/removals alone don't count as a "flip" — the
|
|
41
|
+
* symptom is specifically about an EXISTING default changing), fail unless
|
|
42
|
+
* the PR body contains a `## Breaking Changes` (or `# Breaking Changes`)
|
|
43
|
+
* heading.
|
|
44
|
+
*
|
|
45
|
+
* Needs a PR event payload (`GITHUB_EVENT_PATH`) to read the PR body — this
|
|
46
|
+
* is a dedicated-workflow check (like `lint-canary-version-leak.cjs`), not a
|
|
47
|
+
* `lint:ci` member, since a local/push run has no PR body to check against.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const fs = require('node:fs');
|
|
51
|
+
const path = require('node:path');
|
|
52
|
+
const cp = require('node:child_process');
|
|
53
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
54
|
+
|
|
55
|
+
const ROOT = path.join(__dirname, '..');
|
|
56
|
+
const MANIFEST_PATH = path.join('gsd-core', 'bin', 'shared', 'config-defaults.manifest.json');
|
|
57
|
+
const BREAKING_CHANGES_RE = /^#{1,6}\s*Breaking Changes\b/im;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pure: flatten a nested plain-object JSON value into dot-path
|
|
61
|
+
* `{ "a.b.c": value }` leaves. Arrays and primitives are leaves (compared by
|
|
62
|
+
* JSON.stringify equality, never recursed into) so array reordering reads as
|
|
63
|
+
* one value change, not N.
|
|
64
|
+
* @param {unknown} value
|
|
65
|
+
* @param {string} prefix
|
|
66
|
+
* @param {Record<string, unknown>} out
|
|
67
|
+
* @returns {Record<string, unknown>}
|
|
68
|
+
*/
|
|
69
|
+
function flatten(value, prefix = '', out = {}) {
|
|
70
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
71
|
+
for (const [key, v] of Object.entries(value)) {
|
|
72
|
+
flatten(v, prefix ? `${prefix}.${key}` : key, out);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
out[prefix] = value;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pure: given two resolved (already-flattened) key→value maps, return the
|
|
82
|
+
* keys present in BOTH whose value differs. Additions/removals are NOT
|
|
83
|
+
* "flips" — a brand-new default has no prior behavior to contradict.
|
|
84
|
+
* @param {Record<string, unknown>} baseMap
|
|
85
|
+
* @param {Record<string, unknown>} headMap
|
|
86
|
+
* @returns {{ key: string, from: unknown, to: unknown }[]}
|
|
87
|
+
*/
|
|
88
|
+
function findDefaultValueChanges(baseMap, headMap) {
|
|
89
|
+
const changes = [];
|
|
90
|
+
for (const key of Object.keys(baseMap)) {
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(headMap, key)) continue;
|
|
92
|
+
if (JSON.stringify(baseMap[key]) !== JSON.stringify(headMap[key])) {
|
|
93
|
+
changes.push({ key, from: baseMap[key], to: headMap[key] });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return changes;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pure verdict: given the detected default-value changes and the PR body,
|
|
101
|
+
* decide pass/fail.
|
|
102
|
+
* @param {{ key: string, from: unknown, to: unknown }[]} changes
|
|
103
|
+
* @param {string} prBody
|
|
104
|
+
* @returns {{ ok: boolean, changes: object[] }}
|
|
105
|
+
*/
|
|
106
|
+
function evaluateDefaultFlipDoc(changes, prBody) {
|
|
107
|
+
if (changes.length === 0) return { ok: true, changes: [] };
|
|
108
|
+
if (BREAKING_CHANGES_RE.test(prBody || '')) return { ok: true, changes };
|
|
109
|
+
return { ok: false, changes };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Read and JSON.parse the manifest at a given git ref. Returns `{}` when the
|
|
114
|
+
* file doesn't exist at that ref (new file, or ref predates it) — that is
|
|
115
|
+
* not a "flip", it's an addition, and is silently excluded by
|
|
116
|
+
* findDefaultValueChanges's both-sides-present requirement anyway.
|
|
117
|
+
* @param {string} root
|
|
118
|
+
* @param {string} ref
|
|
119
|
+
* @returns {Record<string, unknown>}
|
|
120
|
+
*/
|
|
121
|
+
function readManifestAtRef(root, ref) {
|
|
122
|
+
let raw;
|
|
123
|
+
try {
|
|
124
|
+
raw = cp.execFileSync('git', ['show', `${ref}:${MANIFEST_PATH.split(path.sep).join('/')}`], {
|
|
125
|
+
cwd: root,
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
timeout: 15000,
|
|
128
|
+
});
|
|
129
|
+
} catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(raw);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
throw new ExitError(2, `lint-default-flip-documentation: ${ref}:${MANIFEST_PATH} is not valid JSON: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readPrBody() {
|
|
140
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
141
|
+
if (!eventPath || !fs.existsSync(eventPath)) return null;
|
|
142
|
+
try {
|
|
143
|
+
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
144
|
+
return typeof event.pull_request?.body === 'string' ? event.pull_request.body : '';
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function main() {
|
|
151
|
+
const prBody = readPrBody();
|
|
152
|
+
if (prBody === null) {
|
|
153
|
+
console.log('lint-default-flip-documentation: no PR event payload (not a pull_request run), skipping');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const baseRef = `origin/${process.env.GITHUB_BASE_REF || 'next'}`; // #2988
|
|
158
|
+
const baseMap = flatten(readManifestAtRef(ROOT, baseRef));
|
|
159
|
+
const headMap = flatten(readManifestAtRef(ROOT, 'HEAD'));
|
|
160
|
+
const changes = findDefaultValueChanges(baseMap, headMap);
|
|
161
|
+
const verdict = evaluateDefaultFlipDoc(changes, prBody);
|
|
162
|
+
|
|
163
|
+
if (!verdict.ok) {
|
|
164
|
+
const detail = verdict.changes
|
|
165
|
+
.map((c) => ` ${c.key}: ${JSON.stringify(c.from)} → ${JSON.stringify(c.to)}`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
throw new ExitError(
|
|
168
|
+
1,
|
|
169
|
+
'lint-default-flip-documentation: this PR changes an existing default value in\n'
|
|
170
|
+
+ 'config-defaults.manifest.json (DEFECT.DEFAULT-FLIP-DOCUMENTATION) but the PR body has no\n'
|
|
171
|
+
+ '`## Breaking Changes` section. Add one covering: (a) when the new default takes effect\n'
|
|
172
|
+
+ '(config-set, fresh project, regenerated config), (b) the opt-back-in command\n'
|
|
173
|
+
+ '(`gsd config-set <key> <old-value>`), (c) effect on in-flight artifacts. Changed default(s):\n'
|
|
174
|
+
+ detail,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
console.log(
|
|
178
|
+
changes.length === 0
|
|
179
|
+
? 'ok lint-default-flip-documentation: no default value changed'
|
|
180
|
+
: `ok lint-default-flip-documentation: ${changes.length} default value change(s), PR body documents Breaking Changes`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
flatten,
|
|
186
|
+
findDefaultValueChanges,
|
|
187
|
+
evaluateDefaultFlipDoc,
|
|
188
|
+
readManifestAtRef,
|
|
189
|
+
MANIFEST_PATH,
|
|
190
|
+
BREAKING_CHANGES_RE,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (require.main === module) runMain(main);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-docs-command-form.cjs
|
|
4
|
+
*
|
|
5
|
+
* Enforces the human-facing command form in docs/ (#2903).
|
|
6
|
+
*
|
|
7
|
+
* `/gsd:<cmd>` (and bare `gsd:<cmd>`) is a SOURCE-AUTHORING token: install-time
|
|
8
|
+
* converters (`transformContentToHyphen`, `convertSlashCommandsTo<Runtime>SkillMentions`)
|
|
9
|
+
* rewrite it to `/gsd-<cmd>` per-runtime. It is correct in `commands/gsd/**`,
|
|
10
|
+
* `gsd-core/workflows/**`, and `agents/**` — but docs are never passed through a
|
|
11
|
+
* converter, so a doc telling a reader to type `/gsd:<cmd>` names a command no
|
|
12
|
+
* runtime registers. The real user-facing form is `/gsd-<cmd>`.
|
|
13
|
+
*
|
|
14
|
+
* This guard fails any `docs/**\/*.md` file that still contains `/gsd:<cmd>` or
|
|
15
|
+
* bare `gsd:<cmd>` where `<cmd>` is a real command name (drawn from the
|
|
16
|
+
* `commands/gsd/*.md` roster). It explicitly permits `/gsd-core:<cmd>` (the
|
|
17
|
+
* Claude Code plugin namespace) and any `gsd:<token>` whose `<token>` is not a
|
|
18
|
+
* real command (e.g. the `gsd:section` / `gsd:loop-host` workflow-fragment
|
|
19
|
+
* marker syntax documented in docs/reference/workflow-fragments.md, or
|
|
20
|
+
* `gsd:command-name` used as a placeholder while explaining the Gemini CLI
|
|
21
|
+
* colon-form convention).
|
|
22
|
+
*
|
|
23
|
+
* Exclusions (never checked):
|
|
24
|
+
* - docs/adr/** (historical record)
|
|
25
|
+
* - docs/RELEASE-NOTES-LEGACY.md (maintainer question still open)
|
|
26
|
+
* - commands/gsd/**, gsd-core/workflows/**, agents/** — never touched by this
|
|
27
|
+
* guard at all; the colon form is correct there.
|
|
28
|
+
*
|
|
29
|
+
* Exemption — `name:` frontmatter key citations: source command files
|
|
30
|
+
* (`commands/gsd/*.md`) carry the colon form in their `name:` YAML frontmatter
|
|
31
|
+
* key (e.g. `name: gsd:next`). A doc that quotes that key verbatim — e.g.
|
|
32
|
+
* `` `name: gsd:next` `` or `name: gsd:next` — is citing the real source file,
|
|
33
|
+
* not telling a reader what to type. Rewriting that citation to `gsd-next`
|
|
34
|
+
* would make the doc lie about the source it's quoting, so a `gsd:<cmd>` token
|
|
35
|
+
* immediately preceded by `name:` (optionally with a backtick/whitespace in
|
|
36
|
+
* between) is permitted. This is narrow: it does not exempt `gsd:<cmd>`
|
|
37
|
+
* anywhere else on the line or file, including the reader-facing `/gsd-<cmd>`
|
|
38
|
+
* form that may appear later in the same sentence.
|
|
39
|
+
*
|
|
40
|
+
* Detection is case-insensitive (`/GSD:next`, `Gsd:Next`, etc. are all
|
|
41
|
+
* flagged) since the install-time converters and runtimes treat command names
|
|
42
|
+
* case-insensitively in practice, and a doc typo in casing is still a lie
|
|
43
|
+
* about the real command form.
|
|
44
|
+
*
|
|
45
|
+
* Exit 0 if no violations; exit 1 if any are found (with stderr diagnostics).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
'use strict';
|
|
49
|
+
|
|
50
|
+
const { execFileSync } = require('child_process');
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
54
|
+
|
|
55
|
+
const SELF_PATH = path.resolve(__filename);
|
|
56
|
+
// GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT is used by tests to redirect the guard to
|
|
57
|
+
// a temporary fixture git repo without touching the real working tree.
|
|
58
|
+
const REPO_ROOT = process.env.GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT
|
|
59
|
+
? path.resolve(process.env.GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT)
|
|
60
|
+
: path.resolve(__dirname, '..');
|
|
61
|
+
|
|
62
|
+
const DOCS_PREFIX = 'docs/';
|
|
63
|
+
const ADR_PREFIX = 'docs/adr/';
|
|
64
|
+
const RELEASE_NOTES_LEGACY = 'docs/RELEASE-NOTES-LEGACY.md';
|
|
65
|
+
const COMMANDS_DIR = path.join(REPO_ROOT, 'commands/gsd');
|
|
66
|
+
|
|
67
|
+
// Matches `/gsd:<cmd>` and bare `gsd:<cmd>` (not part of `/gsd-core:<cmd>`,
|
|
68
|
+
// which does not contain the substring `gsd:` — the hyphen breaks it).
|
|
69
|
+
// Case-insensitive so `/GSD:next` / `Gsd:Next` are also caught.
|
|
70
|
+
const COMMAND_FORM_RE = /(^|[^A-Za-z0-9_-])(\/)?gsd:([A-Za-z0-9_-]+)/gi;
|
|
71
|
+
|
|
72
|
+
// A `gsd:<cmd>` token is exempt when it is a citation of a source file's
|
|
73
|
+
// YAML `name:` frontmatter key — i.e. the text immediately before the match
|
|
74
|
+
// (ending exactly where the match begins) is `name:` followed by optional
|
|
75
|
+
// whitespace and/or a backtick. Only applies to the bare (non-`/`) form,
|
|
76
|
+
// since the real frontmatter key never carries a leading slash.
|
|
77
|
+
const NAME_KEY_CITATION_RE = /name:\s*`?\s*$/i;
|
|
78
|
+
|
|
79
|
+
function loadRoster() {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(COMMANDS_DIR);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw new ExitError(1, 'ERROR lint-docs-command-form: could not read commands/gsd: ' + err.message);
|
|
85
|
+
}
|
|
86
|
+
return new Set(
|
|
87
|
+
entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isCheckedDocsFile(relPath) {
|
|
92
|
+
if (!relPath.startsWith(DOCS_PREFIX)) return false;
|
|
93
|
+
if (relPath.startsWith(ADR_PREFIX)) return false;
|
|
94
|
+
if (relPath === RELEASE_NOTES_LEGACY) return false;
|
|
95
|
+
return relPath.endsWith('.md');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pure scan — no fs, no git. Returns violations for a single file's content.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} relPath file path (used only in violation records)
|
|
102
|
+
* @param {string} content file contents
|
|
103
|
+
* @param {Set<string>} roster valid command names
|
|
104
|
+
* @returns {Array<{file:string, line:number, col:number, text:string}>}
|
|
105
|
+
*/
|
|
106
|
+
function scanContent(relPath, content, roster) {
|
|
107
|
+
const violations = [];
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
const line = lines[i];
|
|
111
|
+
COMMAND_FORM_RE.lastIndex = 0;
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = COMMAND_FORM_RE.exec(line)) !== null) {
|
|
114
|
+
const [, pre, slash, cmd] = match;
|
|
115
|
+
if (!roster.has(cmd.toLowerCase())) continue;
|
|
116
|
+
if (!slash) {
|
|
117
|
+
const preContext = line.slice(0, match.index + pre.length);
|
|
118
|
+
if (NAME_KEY_CITATION_RE.test(preContext)) continue;
|
|
119
|
+
}
|
|
120
|
+
const matchedToken = (slash || '') + 'gsd:' + cmd;
|
|
121
|
+
violations.push({
|
|
122
|
+
file: relPath,
|
|
123
|
+
line: i + 1,
|
|
124
|
+
col: match.index + pre.length + 1,
|
|
125
|
+
text: matchedToken,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return violations;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function main() {
|
|
133
|
+
const roster = loadRoster();
|
|
134
|
+
|
|
135
|
+
let trackedFiles;
|
|
136
|
+
try {
|
|
137
|
+
trackedFiles = execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
|
|
138
|
+
.split('\n')
|
|
139
|
+
.map((f) => f.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new ExitError(1, 'ERROR lint-docs-command-form: git ls-files failed: ' + err.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const docsFiles = trackedFiles.filter(isCheckedDocsFile);
|
|
146
|
+
const violations = [];
|
|
147
|
+
|
|
148
|
+
for (const relPath of docsFiles) {
|
|
149
|
+
const fullPath = path.join(REPO_ROOT, relPath);
|
|
150
|
+
if (path.resolve(fullPath) === SELF_PATH) continue;
|
|
151
|
+
|
|
152
|
+
let content;
|
|
153
|
+
try {
|
|
154
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
155
|
+
} catch {
|
|
156
|
+
// Unreadable/deleted files — skip silently.
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
violations.push(...scanContent(relPath, content, roster));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (violations.length === 0) {
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
'ok lint-docs-command-form: ' + docsFiles.length + ' file(s) checked, 0 violations\n',
|
|
166
|
+
);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.stderr.write('\nERROR lint-docs-command-form: ' + violations.length + ' violation(s) found\n\n');
|
|
171
|
+
for (const v of violations) {
|
|
172
|
+
process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — ' + JSON.stringify(v.text) + '\n');
|
|
173
|
+
}
|
|
174
|
+
process.stderr.write('\n');
|
|
175
|
+
process.stderr.write(
|
|
176
|
+
'Fix: docs are never passed through the install-time slash-form converters, so the\n',
|
|
177
|
+
);
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
' colon form names a command no runtime registers. Rewrite to the hyphen form\n',
|
|
180
|
+
);
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
' (`/gsd-<cmd>`), or `/gsd-core:<cmd>` if this is genuinely the Claude Code\n',
|
|
183
|
+
);
|
|
184
|
+
process.stderr.write(' plugin namespace.\n\n');
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (require.main === module) runMain(main);
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
scanContent,
|
|
192
|
+
isCheckedDocsFile,
|
|
193
|
+
loadRoster,
|
|
194
|
+
COMMAND_FORM_RE,
|
|
195
|
+
};
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
const { parseFragment, FRAGMENT_ERROR } = require('./changeset/parse.cjs');
|
|
17
17
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
18
18
|
|
|
19
|
+
// #2988: the repo's integration/default branch — the base every PR targets.
|
|
20
|
+
// Used as the local fallback when GITHUB_BASE_REF is unset (CI sets it).
|
|
21
|
+
const DEFAULT_BASE = 'next';
|
|
22
|
+
|
|
19
23
|
const LINT_REASON = Object.freeze({
|
|
20
24
|
OK_NO_TRIGGERING_FRAGMENTS: 'ok_no_triggering_fragments',
|
|
21
25
|
OK_DOCS_UPDATED: 'ok_docs_updated',
|
|
@@ -149,7 +153,10 @@ function main() {
|
|
|
149
153
|
} catch { /* fall through */ }
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
// #2988: local fallback must match the repo's integration branch (`next`),
|
|
157
|
+
// not the release branch (`main`). CI sets GITHUB_BASE_REF explicitly; the
|
|
158
|
+
// fallback only fires locally, where `next` is the base every PR targets.
|
|
159
|
+
const base = process.env.GITHUB_BASE_REF || DEFAULT_BASE;
|
|
153
160
|
let changedFiles = [];
|
|
154
161
|
try {
|
|
155
162
|
// execFileSync with argv — no shell, so a malicious GITHUB_BASE_REF
|
|
@@ -216,6 +223,7 @@ module.exports = {
|
|
|
216
223
|
OPT_OUT_LABEL,
|
|
217
224
|
TRIGGERING_TYPES,
|
|
218
225
|
FRAGMENT_ERROR,
|
|
226
|
+
DEFAULT_BASE,
|
|
219
227
|
isFragmentPath,
|
|
220
228
|
isDocsFile,
|
|
221
229
|
isExemptFragment,
|