@opengsd/gsd-core 1.10.0 → 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 +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- 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 +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.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 +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -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 +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- 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 +575 -101
- 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 +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- 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 +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +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-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- 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 +134 -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-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- 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 +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- 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 +56 -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/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- 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/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -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 +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- 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 +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- 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 +24 -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 +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- 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/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- 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/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- 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.md +2 -1
- 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 +31 -6
- 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 +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- 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/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- 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/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- 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 +77 -2
- 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 +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- 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 +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- 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/changeset/lint.cjs +60 -5
- 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-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- 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 +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -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 +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- 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 +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +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 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for the command-alias drift surface.
|
|
5
|
+
*
|
|
6
|
+
* Two parallel surfaces consume this: `scripts/check-alias-drift.cjs`, which
|
|
7
|
+
* validates the built alias artifacts, and `.githooks/pre-commit`, which decides
|
|
8
|
+
* when to run that validation. Until #2725 the hook's watched-path list had
|
|
9
|
+
* drifted from the checker's family table until every pattern named either the
|
|
10
|
+
* retired `sdk/` tree (ADR-0174) or a gitignored build output, so the guard was
|
|
11
|
+
* silently inert. Both surfaces derive from this list now, and
|
|
12
|
+
* `tests/precommit-alias-drift-hook.test.cjs` fails if they diverge again.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Alias families the drift check validates. `router` is the module basename,
|
|
17
|
+
* shared by the `src/<router>.cts` source and the `gsd-core/bin/lib/<router>.cjs`
|
|
18
|
+
* artifact it compiles to.
|
|
19
|
+
*/
|
|
20
|
+
const FAMILIES = Object.freeze([
|
|
21
|
+
{ commandAliases: 'STATE_COMMAND_ALIASES', subcommands: 'STATE_SUBCOMMANDS', router: 'state-command-router' },
|
|
22
|
+
{ commandAliases: 'VERIFY_COMMAND_ALIASES', subcommands: 'VERIFY_SUBCOMMANDS', router: 'verify-command-router' },
|
|
23
|
+
{ commandAliases: 'INIT_COMMAND_ALIASES', subcommands: 'INIT_SUBCOMMANDS', router: 'init-command-router' },
|
|
24
|
+
{ commandAliases: 'PHASE_COMMAND_ALIASES', subcommands: 'PHASE_SUBCOMMANDS', router: 'phase-command-router' },
|
|
25
|
+
{ commandAliases: 'PHASES_COMMAND_ALIASES', subcommands: 'PHASES_SUBCOMMANDS', router: 'phases-command-router' },
|
|
26
|
+
{ commandAliases: 'VALIDATE_COMMAND_ALIASES', subcommands: 'VALIDATE_SUBCOMMANDS', router: 'validate-command-router' },
|
|
27
|
+
{ commandAliases: 'ROADMAP_COMMAND_ALIASES', subcommands: 'ROADMAP_SUBCOMMANDS', router: 'roadmap-command-router' },
|
|
28
|
+
{ commandAliases: 'EVAL_COMMAND_ALIASES', subcommands: 'EVAL_SUBCOMMANDS', router: 'eval-command-router' },
|
|
29
|
+
].map(Object.freeze));
|
|
30
|
+
|
|
31
|
+
/** Basename of the generated alias table every family is checked against. */
|
|
32
|
+
const ALIAS_TABLE = 'command-aliases';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Repo-relative TRACKED sources whose edit should re-run the drift check.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately the `src/*.cts` sources, never the `gsd-core/bin/lib/*.cjs`
|
|
38
|
+
* build outputs they compile to: those are gitignored, so
|
|
39
|
+
* `git diff --cached --name-only` can never list them and a pre-commit guard
|
|
40
|
+
* keyed on them can never fire (#2725).
|
|
41
|
+
*/
|
|
42
|
+
function stagedSourcePaths() {
|
|
43
|
+
return [`src/${ALIAS_TABLE}.cts`, ...FAMILIES.map((family) => `src/${family.router}.cts`)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { FAMILIES, ALIAS_TABLE, stagedSourcePaths };
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared anti-divergence scanner machinery (epic #3180, ADR-3180 Decision 4).
|
|
6
|
+
*
|
|
7
|
+
* Extracted from `scripts/lint-plan-count-drift.cjs` (#3183) so that
|
|
8
|
+
* `scripts/lint-milestone-window-drift.cjs` (#3184) and any future
|
|
9
|
+
* `scripts/lint-<derivation>-drift.cjs` guard consume ONE tree-walk /
|
|
10
|
+
* root-confinement / literal-tokenizer / sanitizer implementation instead of
|
|
11
|
+
* each copying it verbatim — the exact generative-fix-divergence class this
|
|
12
|
+
* epic exists to remove, now applied to the guards themselves (see
|
|
13
|
+
* `.gsd/phase/refactor-3184-milestone-window-single-owner/40-design.md`,
|
|
14
|
+
* "Rejected: let the new drift guard copy Phase 1's tree-walk /
|
|
15
|
+
* root-confinement / sanitizer").
|
|
16
|
+
*
|
|
17
|
+
* `lint-plan-count-drift.cjs` re-exports every symbol it exported before this
|
|
18
|
+
* extraction (`readRegexLiteralAt`, `MAX_REGEX_LITERAL_LEN`, `isInsideRoot`,
|
|
19
|
+
* `sanitizeForReport`), so `tests/plan-count-single-owner.test.cjs` — the
|
|
20
|
+
* ReDoS and root-confinement regression net for this exact machinery —
|
|
21
|
+
* continues to pass unchanged and is the regression net for this move too.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
// Longest regex literal this scanner will consider, in characters. Real
|
|
28
|
+
// plan/summary filename filters (and milestone-window regex literals) are far
|
|
29
|
+
// shorter; the bound is what keeps the scan linear. The tokenizer restarts at
|
|
30
|
+
// every `/` on the line (so that a literal preceded by a stray unpaired `/`
|
|
31
|
+
// is still found, matching the previous regex's "find anywhere" behaviour),
|
|
32
|
+
// which without a per-literal bound would be quadratic on a pathological
|
|
33
|
+
// line. With it the whole-line cost is O(n * MAX_REGEX_LITERAL_LEN) with no
|
|
34
|
+
// backtracking at all.
|
|
35
|
+
const MAX_REGEX_LITERAL_LEN = 400;
|
|
36
|
+
|
|
37
|
+
// Directory names this scanner never descends into or reports out of —
|
|
38
|
+
// `.git` (repo internals, e.g. a persisted CI token in `.git/config`),
|
|
39
|
+
// `node_modules` (thousands of third-party files, none of them authored
|
|
40
|
+
// source), `dist` (build output). Named once and used at BOTH skip sites
|
|
41
|
+
// below: the cheap `entry.name` fast path in `walk`, and the resolved-path
|
|
42
|
+
// component check in `isUnderSkippedDir` — a symlink whose OWN name is not
|
|
43
|
+
// in this set but whose target resolves through a directory that IS (e.g.
|
|
44
|
+
// `src/g -> ../.git`, `src/nm -> ../node_modules`) must still be skipped, or
|
|
45
|
+
// the name-only check is a trivial bypass.
|
|
46
|
+
const SKIP_DIR_NAMES = new Set(['node_modules', 'dist', '.git']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read the JS regex literal starting at `line[start]` (which must be `/`).
|
|
50
|
+
* Returns `{ text, end }` — `text` includes the delimiters and any trailing
|
|
51
|
+
* flags, `end` is the index one past the literal — or null if no literal
|
|
52
|
+
* closes within MAX_REGEX_LITERAL_LEN characters.
|
|
53
|
+
*
|
|
54
|
+
* Single left-to-right pass, no backtracking. It models the two constructs a
|
|
55
|
+
* backtracking pattern gets wrong, which is why this is a tokenizer and not a
|
|
56
|
+
* regex:
|
|
57
|
+
* - `\x` escapes consume BOTH characters, so an escaped `\/` never
|
|
58
|
+
* terminates the literal;
|
|
59
|
+
* - inside a `[...]` character class a bare `/` does NOT terminate, so
|
|
60
|
+
* `/PLAN[\\/].*\.md$/` is one literal rather than two fragments. The
|
|
61
|
+
* previous regex silently MISSED every re-derivation using a
|
|
62
|
+
* cross-platform path-separator class for exactly this reason.
|
|
63
|
+
*/
|
|
64
|
+
function readRegexLiteralAt(line, start) {
|
|
65
|
+
if (line[start] !== '/') return null;
|
|
66
|
+
const limit = Math.min(line.length, start + MAX_REGEX_LITERAL_LEN);
|
|
67
|
+
let inClass = false;
|
|
68
|
+
for (let i = start + 1; i < limit; i++) {
|
|
69
|
+
const ch = line[i];
|
|
70
|
+
if (ch === '\\') {
|
|
71
|
+
i++; // escape consumes the next character, whatever it is
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === '\r' || ch === '\n') return null; // a literal cannot span lines
|
|
75
|
+
if (ch === '[') {
|
|
76
|
+
inClass = true;
|
|
77
|
+
} else if (ch === ']') {
|
|
78
|
+
inClass = false;
|
|
79
|
+
} else if (ch === '/' && !inClass) {
|
|
80
|
+
// Trailing flags are bounded by the SAME `limit` as the literal body
|
|
81
|
+
// itself (not `line.length`) — a literal followed by an unbounded run
|
|
82
|
+
// of lowercase letters must not make `text` grow past
|
|
83
|
+
// MAX_REGEX_LITERAL_LEN either.
|
|
84
|
+
let end = i + 1;
|
|
85
|
+
while (end < limit && line[end] >= 'a' && line[end] <= 'z') end++;
|
|
86
|
+
return { text: line.slice(start, end), end };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Symlinks report `isDirectory()`/`isFile()` as false on the Dirent from
|
|
93
|
+
// `readdirSync`, so a symlinked `.cts` (or a symlinked directory containing
|
|
94
|
+
// one) was previously invisible to this scanner — an evasion of a guard
|
|
95
|
+
// whose stated design principle (ADR-3180 Decision 4a) is whole-repo
|
|
96
|
+
// discovery with no allowlist. Resolve each entry with `fs.statSync` (which
|
|
97
|
+
// follows symlinks) to classify it, skipping broken links. `ctx.visitedRealDirs`
|
|
98
|
+
// guards against a symlink cycle sending `walk` into infinite recursion.
|
|
99
|
+
//
|
|
100
|
+
// Every sibling drift guard in `scripts/` that does NOT import this module
|
|
101
|
+
// (`lint-phase-id-drift.cjs`, `lint-package-identity-drift.cjs`,
|
|
102
|
+
// `lint-portable-timeout.cjs`, `lint-test-file-count.cjs`,
|
|
103
|
+
// `lint-allow-test-rule-refs.cjs`) uses the `Dirent` classification straight
|
|
104
|
+
// off `readdirSync` and does NOT follow symlinks at all. This scanner follows
|
|
105
|
+
// them so a symlinked source file cannot evade ADR-3180 Decision 4a's
|
|
106
|
+
// whole-repo discovery — root confinement (`isInsideRoot` below) is the price
|
|
107
|
+
// of doing so: without it, a symlink planted anywhere under a scan directory
|
|
108
|
+
// could walk this scanner out to read and report arbitrary files elsewhere on
|
|
109
|
+
// disk.
|
|
110
|
+
//
|
|
111
|
+
// DIRECTORY vs FILE symlinks are confined to two DIFFERENT roots, tracked as
|
|
112
|
+
// `ctx.scanDirRoot` (the realpath of the current top-level scan-dir entry,
|
|
113
|
+
// e.g. `<realRoot>/src`) vs `ctx.realRoot` (the whole repo):
|
|
114
|
+
// - a DIRECTORY symlink is descended ONLY if its resolved realpath is
|
|
115
|
+
// inside `ctx.scanDirRoot` — NOT merely inside `ctx.realRoot`. Without
|
|
116
|
+
// this, `src/up -> ..` (or `-> <realRoot>`) resolves inside the repo
|
|
117
|
+
// root and `walk` descends the ENTIRE repo, reporting violations under
|
|
118
|
+
// paths like `tests/not-src.cts` or `docs/other.cts` — files the caller's
|
|
119
|
+
// scan-dir list scopes it OUT of. This is a deliberate, fail-CLOSED
|
|
120
|
+
// trade-off: a directory symlink pointing elsewhere INSIDE the repo (but
|
|
121
|
+
// outside the scan directory) is simply not followed. The alternative —
|
|
122
|
+
// descending it — is exactly the whole-repo sweep this rule exists to
|
|
123
|
+
// prevent, and a fork PR could use that sweep to redden `lint:ci` on
|
|
124
|
+
// files this guard was never meant to read. The narrower rule is worth
|
|
125
|
+
// more than the missed edge case.
|
|
126
|
+
// - a FILE symlink is still scanned if its resolved realpath is inside
|
|
127
|
+
// `ctx.realRoot` (the whole repo, not just the scan directory) — this is
|
|
128
|
+
// what keeps `src/alias.cts -> vendor/real.cts` covered (test (f)): an
|
|
129
|
+
// aliased file genuinely is part of the compiled surface even when its
|
|
130
|
+
// real target lives outside `src/`, and it is still reported under its
|
|
131
|
+
// canonical (real) path.
|
|
132
|
+
//
|
|
133
|
+
// A resolved path is inside a root only if it IS that root or begins with
|
|
134
|
+
// root + separator — a plain `startsWith(root)` would also accept a sibling
|
|
135
|
+
// directory whose name merely starts with the root's name (`/repo-evil`).
|
|
136
|
+
function isInsideRoot(realPath, realRoot) {
|
|
137
|
+
return realPath === realRoot || realPath.startsWith(realRoot + path.sep);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// True when `realPath` (already confirmed inside `realRoot` by `isInsideRoot`)
|
|
141
|
+
// resolves THROUGH a skip-list directory anywhere along its path relative to
|
|
142
|
+
// the root — not just when `realPath` itself IS one. This is what closes the
|
|
143
|
+
// symlink bypass the `entry.name` fast path alone cannot: `walk` tests
|
|
144
|
+
// `entry.name` (the symlink's OWN name in its parent directory), but a
|
|
145
|
+
// symlink named something innocuous can still RESOLVE into `.git` /
|
|
146
|
+
// `node_modules` / `dist` (`src/g -> ../.git`, `src/leak.cts ->
|
|
147
|
+
// ../.git/config`, `src/nm -> ../node_modules`) — `isInsideRoot` alone admits
|
|
148
|
+
// all three, because every one of those real paths is still under the root.
|
|
149
|
+
function isUnderSkippedDir(realPath, realRoot) {
|
|
150
|
+
const rel = path.relative(realRoot, realPath);
|
|
151
|
+
return rel.split(path.sep).some((segment) => SKIP_DIR_NAMES.has(segment));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// `ctx.scanExt` is a `Set` of file extensions (e.g. `.cts`/`.ts`/`.mts`) the
|
|
155
|
+
// caller wants reported — threaded through `ctx` rather than as a positional
|
|
156
|
+
// parameter so recursive `walk(full, acc, ctx)` calls stay unchanged.
|
|
157
|
+
function walk(dir, acc, ctx) {
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
161
|
+
} catch {
|
|
162
|
+
return acc;
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const full = path.join(dir, entry.name);
|
|
166
|
+
if (SKIP_DIR_NAMES.has(entry.name)) continue; // cheap fast path
|
|
167
|
+
let stat;
|
|
168
|
+
try {
|
|
169
|
+
stat = entry.isSymbolicLink() ? fs.statSync(full) : entry;
|
|
170
|
+
} catch {
|
|
171
|
+
continue; // broken symlink target
|
|
172
|
+
}
|
|
173
|
+
let realPath;
|
|
174
|
+
try {
|
|
175
|
+
realPath = fs.realpathSync(full);
|
|
176
|
+
} catch {
|
|
177
|
+
continue; // broken symlink target (race, or a link stat() followed but realpath cannot)
|
|
178
|
+
}
|
|
179
|
+
if (stat.isDirectory()) {
|
|
180
|
+
// Directories (symlinked or real) are confined to the CURRENT scan
|
|
181
|
+
// directory root, not merely the repo root — see the comment above
|
|
182
|
+
// `isInsideRoot` for why (`src/up -> ..` whole-repo sweep).
|
|
183
|
+
if (!isInsideRoot(realPath, ctx.scanDirRoot)) continue;
|
|
184
|
+
if (isUnderSkippedDir(realPath, ctx.realRoot)) continue; // symlink resolves through a skipped dir
|
|
185
|
+
if (ctx.visitedRealDirs.has(realPath)) continue; // symlink cycle guard
|
|
186
|
+
ctx.visitedRealDirs.add(realPath);
|
|
187
|
+
walk(full, acc, ctx);
|
|
188
|
+
} else if (stat.isFile() && ctx.scanExt.has(path.extname(entry.name))) {
|
|
189
|
+
// Files are confined to the whole repo root — a symlinked FILE whose
|
|
190
|
+
// real target lives outside the scan directory but inside the repo
|
|
191
|
+
// (e.g. `src/alias.cts -> vendor/real.cts`) is still part of the
|
|
192
|
+
// compiled surface and must be scanned.
|
|
193
|
+
if (!isInsideRoot(realPath, ctx.realRoot)) continue;
|
|
194
|
+
if (isUnderSkippedDir(realPath, ctx.realRoot)) continue; // symlink resolves through a skipped dir
|
|
195
|
+
if (ctx.visitedRealFiles.has(realPath)) continue; // two symlinks, same real file
|
|
196
|
+
ctx.visitedRealFiles.add(realPath);
|
|
197
|
+
acc.push(realPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return acc;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whole-repo tree-walk driver shared by every `lint-<derivation>-drift.cjs`
|
|
205
|
+
* guard. Resolves `root`, walks each of `scanDirs` filtered to `scanExt`
|
|
206
|
+
* (symlink-following, root-confined, cycle-guarded — see `walk` above), reads
|
|
207
|
+
* each discovered file, and calls `onFile(relPath, text)` for it — `relPath`
|
|
208
|
+
* is repo-relative and already the file's canonical (real) path, so a
|
|
209
|
+
* per-file exemption keyed on `relPath` matches consistently regardless of
|
|
210
|
+
* which symlink reached it.
|
|
211
|
+
*
|
|
212
|
+
* `onFile` returns an array of violation objects (or an empty array / null /
|
|
213
|
+
* undefined for "no violations in this file"); `scanTree` flattens them all
|
|
214
|
+
* into one returned array. Pure I/O orchestration — detection logic lives
|
|
215
|
+
* entirely in the caller's `onFile`.
|
|
216
|
+
*/
|
|
217
|
+
function scanTree({ root, scanDirs, scanExt, onFile }) {
|
|
218
|
+
const violations = [];
|
|
219
|
+
let realRoot;
|
|
220
|
+
try {
|
|
221
|
+
realRoot = fs.realpathSync(root);
|
|
222
|
+
} catch {
|
|
223
|
+
return violations; // root itself does not exist / is unreadable
|
|
224
|
+
}
|
|
225
|
+
for (const dir of scanDirs) {
|
|
226
|
+
const scanDirPath = path.join(root, dir);
|
|
227
|
+
let scanDirRoot;
|
|
228
|
+
try {
|
|
229
|
+
scanDirRoot = fs.realpathSync(scanDirPath);
|
|
230
|
+
} catch {
|
|
231
|
+
continue; // scan directory itself does not exist / is unreadable
|
|
232
|
+
}
|
|
233
|
+
const ctx = { realRoot, scanDirRoot, scanExt, visitedRealDirs: new Set(), visitedRealFiles: new Set() };
|
|
234
|
+
for (const file of walk(scanDirPath, [], ctx)) {
|
|
235
|
+
const rel = path.relative(realRoot, file);
|
|
236
|
+
let text;
|
|
237
|
+
try {
|
|
238
|
+
text = fs.readFileSync(file, 'utf8');
|
|
239
|
+
} catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const found = onFile(rel, text);
|
|
243
|
+
if (found && found.length > 0) violations.push(...found);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return violations;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// A reported fragment AND a reported file path are both attacker-controlled
|
|
250
|
+
// source text on a fork PR (a repo can legally track a filename containing
|
|
251
|
+
// control bytes, so the path is exactly as attacker-controlled as the
|
|
252
|
+
// fragment), and both are written straight to a CI log. Replace C0/C1
|
|
253
|
+
// control bytes (ANSI escapes included) with a visible \xNN, AND the
|
|
254
|
+
// non-Latin-1 formatting/bidi/line-separator codepoints below with \uNNNN, so
|
|
255
|
+
// a crafted literal or filename cannot rewrite the terminal rendering of the
|
|
256
|
+
// report or hide/reorder its own text:
|
|
257
|
+
// - U+200B-U+200F: zero-width space/joiners and directional marks
|
|
258
|
+
// - U+2028/U+2029: Unicode LINE SEPARATOR / PARAGRAPH SEPARATOR (line
|
|
259
|
+
// breaks a `\n`-only log scan would not catch)
|
|
260
|
+
// - U+202A-U+202E: bidi embedding/override controls (RLO etc.)
|
|
261
|
+
// - U+2066-U+2069: bidi isolate controls
|
|
262
|
+
function sanitizeForReport(text) {
|
|
263
|
+
return text
|
|
264
|
+
// eslint-disable-next-line no-control-regex -- the control range IS the target
|
|
265
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, (c) => '\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'))
|
|
266
|
+
.replace(/[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
SKIP_DIR_NAMES,
|
|
271
|
+
isInsideRoot,
|
|
272
|
+
isUnderSkippedDir,
|
|
273
|
+
walk,
|
|
274
|
+
readRegexLiteralAt,
|
|
275
|
+
MAX_REGEX_LITERAL_LEN,
|
|
276
|
+
sanitizeForReport,
|
|
277
|
+
scanTree,
|
|
278
|
+
};
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"tests/agent-classification-parity.test.cjs :: source-text-is-the-product",
|
|
3
3
|
"tests/agent-frontmatter.test.cjs :: source-text-is-the-product",
|
|
4
4
|
"tests/agent-required-reading-consistency.test.cjs :: source-text-is-the-product",
|
|
5
|
-
"tests/agent-size-budget.test.cjs :: source-text-is-the-product",
|
|
6
5
|
"tests/agent-skills-awareness.test.cjs :: source-text-is-the-product",
|
|
7
6
|
"tests/ai-evals.test.cjs :: source-text-is-the-product",
|
|
8
7
|
"tests/analyze-dependencies.test.cjs :: source-text-is-the-product",
|
|
@@ -16,9 +15,7 @@
|
|
|
16
15
|
"tests/autonomous-to-flag.test.cjs :: source-text-is-the-product",
|
|
17
16
|
"tests/chain-flag-plan-phase.test.cjs :: source-text-is-the-product",
|
|
18
17
|
"tests/changeset-cli.test.cjs :: source-text-is-the-product",
|
|
19
|
-
"tests/check-update-config-dir.test.cjs :: structural-regression-guard",
|
|
20
18
|
"tests/claude-md.test.cjs :: source-text-is-the-product",
|
|
21
|
-
"tests/claude-skills-migration.test.cjs :: source-text-is-the-product",
|
|
22
19
|
"tests/cleanup-branch-pruning.test.cjs :: source-text-is-the-product",
|
|
23
20
|
"tests/cline-install.test.cjs :: source-text-is-the-product",
|
|
24
21
|
"tests/cline-support.test.cjs :: source-text-is-the-product",
|
|
@@ -30,7 +27,6 @@
|
|
|
30
27
|
"tests/codex-config.test.cjs :: source-text-is-the-product",
|
|
31
28
|
"tests/command-contract.test.cjs :: source-text-is-the-product",
|
|
32
29
|
"tests/commands.test.cjs :: source-text-is-the-product",
|
|
33
|
-
"tests/concurrency-safety.test.cjs :: source-text-is-the-product",
|
|
34
30
|
"tests/config-field-docs.test.cjs :: docs-parity",
|
|
35
31
|
"tests/context-enrichment.test.cjs :: source-text-is-the-product",
|
|
36
32
|
"tests/contributor-standards.test.cjs :: source-text-is-the-product",
|
|
@@ -38,9 +34,7 @@
|
|
|
38
34
|
"tests/cursor-hooks.test.cjs :: source-text-is-the-product",
|
|
39
35
|
"tests/cursor-reviewer.test.cjs :: source-text-is-the-product",
|
|
40
36
|
"tests/debug-session-management.test.cjs :: source-text-is-the-product",
|
|
41
|
-
"tests/discuss-all-flag.test.cjs :: source-text-is-the-product",
|
|
42
37
|
"tests/discuss-checkpoint.test.cjs :: source-text-is-the-product",
|
|
43
|
-
"tests/discuss-mode.test.cjs :: structural-implementation-guard",
|
|
44
38
|
"tests/discuss-phase-power.test.cjs :: source-text-is-the-product",
|
|
45
39
|
"tests/docs-parity-live-registry.test.cjs :: source-text-is-the-product",
|
|
46
40
|
"tests/drift-detection.test.cjs :: source-text-is-the-product",
|
|
@@ -48,9 +42,6 @@
|
|
|
48
42
|
"tests/edge-probe-planner-contract.test.cjs :: source-text-is-the-product",
|
|
49
43
|
"tests/edge-probe-spec-phase-contract.test.cjs :: source-text-is-the-product",
|
|
50
44
|
"tests/edit-phase.test.cjs :: source-text-is-the-product",
|
|
51
|
-
"tests/eslint-rules.test.cjs :: <source-grep reason> must still error",
|
|
52
|
-
"tests/eslint-rules.test.cjs :: pending migration",
|
|
53
|
-
"tests/eslint-rules.test.cjs :: source-text-is-the-product",
|
|
54
45
|
"tests/execute-phase-active-flags.test.cjs :: source-text-is-the-product",
|
|
55
46
|
"tests/execute-phase-step-5-5-deviation-doc.test.cjs :: source-text-is-the-product",
|
|
56
47
|
"tests/execute-phase-wave.test.cjs :: source-text-is-the-product",
|
|
@@ -65,26 +56,16 @@
|
|
|
65
56
|
"tests/gsd-researcher-app-aware.test.cjs :: source-text-is-the-product",
|
|
66
57
|
"tests/gsd-researcher-flow-diagram.test.cjs :: source-text-is-the-product",
|
|
67
58
|
"tests/gsd-settings-advanced.test.cjs :: source-text-is-the-product",
|
|
68
|
-
"tests/
|
|
59
|
+
"tests/helpers/live-command-registry.cjs :: source-text-is-the-product",
|
|
69
60
|
"tests/hermes-skills-migration.test.cjs :: source-text-is-the-product",
|
|
70
61
|
"tests/import-command.test.cjs :: source-text-is-the-product",
|
|
71
62
|
"tests/ingest-docs.test.cjs :: source-text-is-the-product",
|
|
72
63
|
"tests/inline-plan-threshold.test.cjs :: source-text-is-the-product",
|
|
73
|
-
"tests/install-minimal-hooks.test.cjs :: source-text-is-the-product",
|
|
74
64
|
"tests/install-nested-layout.test.cjs :: source-text-is-the-product",
|
|
75
65
|
"tests/install-runtime-artifacts.test.cjs :: source-text-is-the-product",
|
|
76
|
-
"tests/install.test.cjs :: source-text-is-the-product",
|
|
77
66
|
"tests/intel.test.cjs :: source-text-is-the-product",
|
|
78
67
|
"tests/inventory-headings-countfree.test.cjs :: source-text-is-the-product",
|
|
79
68
|
"tests/ios-scaffold-safety.test.cjs :: source-text-is-the-product",
|
|
80
|
-
"tests/issue-2639-codex-toml-neutralization.test.cjs :: source-text-is-the-product",
|
|
81
|
-
"tests/issue-429-comment-text-gate.test.cjs :: source-text-is-the-product",
|
|
82
|
-
"tests/issue-498-update-backup-runtime-dir.test.cjs :: source-text-is-the-product",
|
|
83
|
-
"tests/issue-57-runtime-install-no-drift.test.cjs :: structural-regression-guard",
|
|
84
|
-
"tests/issue-607-installer-dry-run.install.test.cjs :: integration-test-input",
|
|
85
|
-
"tests/issue-607-legacy-cleanup.test.cjs :: integration-test-input",
|
|
86
|
-
"tests/issue-787-cline-hooks-agents.test.cjs :: source-text-is-the-product",
|
|
87
|
-
"tests/issue-815-update-next-channel.test.cjs :: source-text-is-the-product",
|
|
88
69
|
"tests/locking-bugs-1909-1916-1925-1927.test.cjs :: architectural-invariant",
|
|
89
70
|
"tests/mcp-tool-inheritance.test.cjs :: source-text-is-the-product",
|
|
90
71
|
"tests/milestone-summary.test.cjs :: source-text-is-the-product",
|
|
@@ -93,13 +74,10 @@
|
|
|
93
74
|
"tests/next-safety-gates.test.cjs :: source-text-is-the-product",
|
|
94
75
|
"tests/next-up-clear-order.test.cjs :: source-text-is-the-product",
|
|
95
76
|
"tests/no-hardcoded-home-gsd-tools.test.cjs :: source-text-is-the-product",
|
|
96
|
-
"tests/opencode-permissions.test.cjs :: architectural-invariant",
|
|
97
|
-
"tests/orphan-worktree-detection.test.cjs :: architectural-invariant",
|
|
98
77
|
"tests/orphaned-hooks.test.cjs :: structural-regression-guard",
|
|
99
78
|
"tests/package-legitimacy-gate.test.cjs :: source-text-is-the-product",
|
|
100
79
|
"tests/parallel-dependent-plans.test.cjs :: source-text-is-the-product",
|
|
101
80
|
"tests/path-replacement.test.cjs :: source-text-is-the-product",
|
|
102
|
-
"tests/phase-dependency-levels.test.cjs :: source-text-is-the-product",
|
|
103
81
|
"tests/phase.test.cjs :: source-text-is-the-product",
|
|
104
82
|
"tests/phase6-capability-docs.test.cjs :: source-text-is-the-product",
|
|
105
83
|
"tests/phase6-capstone-conformance.test.cjs :: source-text-is-the-product",
|
|
@@ -117,7 +95,6 @@
|
|
|
117
95
|
"tests/profile-output.test.cjs :: source-text-is-the-product",
|
|
118
96
|
"tests/progress-forensic.test.cjs :: source-text-is-the-product",
|
|
119
97
|
"tests/prompt-thinning.test.cjs :: source-text-is-the-product",
|
|
120
|
-
"tests/quick-session-management.test.cjs :: source-text-is-the-product",
|
|
121
98
|
"tests/qwen-skills-migration.test.cjs :: source-text-is-the-product",
|
|
122
99
|
"tests/read-guard.test.cjs :: source-text-is-the-product",
|
|
123
100
|
"tests/reapply-patches.test.cjs :: source-text-is-the-product",
|
|
@@ -126,7 +103,6 @@
|
|
|
126
103
|
"tests/release-tarball-smoke-workflow.test.cjs :: source-text-is-the-product",
|
|
127
104
|
"tests/release-tarball-smoke.install.test.cjs :: integration-test-input",
|
|
128
105
|
"tests/research-agent-profiles.test.cjs :: source-text-is-the-product",
|
|
129
|
-
"tests/review-default-reviewers-workflow.test.cjs :: source-text-is-the-product",
|
|
130
106
|
"tests/roadmap.test.cjs :: source-text-is-the-product",
|
|
131
107
|
"tests/runtime-launcher-parity.test.cjs :: structural-regression-guard",
|
|
132
108
|
"tests/runtime-name-policy.test.cjs :: source-text-is-the-product",
|
|
@@ -144,7 +120,6 @@
|
|
|
144
120
|
"tests/subagent-timeout.test.cjs :: source-text-is-the-product",
|
|
145
121
|
"tests/template.test.cjs :: source-text-is-the-product",
|
|
146
122
|
"tests/thinking-partner.test.cjs :: source-text-is-the-product",
|
|
147
|
-
"tests/thread-session-management.test.cjs :: source-text-is-the-product",
|
|
148
123
|
"tests/ultraplan-phase.test.cjs :: source-text-is-the-product",
|
|
149
124
|
"tests/verify-health.test.cjs :: source-text-is-the-product",
|
|
150
125
|
"tests/verify-test-quality.test.cjs :: source-text-is-the-product",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canary-version-leak lint (DEFECT.CANARY-VERSION-LEAK, CONTEXT.md).
|
|
6
|
+
*
|
|
7
|
+
* `package.json` `.version` on `main` must never carry a `-canary.<N>`
|
|
8
|
+
* suffix — that suffix is a dev-branch/prerelease marker. Nothing published
|
|
9
|
+
* depends on the string at runtime, but every consumer of the version
|
|
10
|
+
* metadata (release flow, install banners, statusline) surfaces the
|
|
11
|
+
* dev-channel label as if it were the shipped release. The 2026-05-16 audit
|
|
12
|
+
* found `origin/main` at `"version": "1.50.0-canary.0"`, landed by a fix PR
|
|
13
|
+
* that accidentally carried a version bump from a dev-branch base (commit
|
|
14
|
+
* 2d32ad82, #3206).
|
|
15
|
+
*
|
|
16
|
+
* Modeled on scripts/lint-package-identity-drift.cjs / scripts/lint-table-schema-drift.cjs:
|
|
17
|
+
* a standalone node script (not a node:test), exit 0 clean / exit 1 + message
|
|
18
|
+
* on a leaked canary version. Wired to run only for PRs targeting `main`
|
|
19
|
+
* (.github/workflows/version-gate.yml) — a canary version is expected and
|
|
20
|
+
* harmless on every other branch.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const CANARY_RE = /-canary\.\d+/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pure: does this version string carry a `-canary.<N>` suffix?
|
|
30
|
+
* @param {string} version
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isCanaryVersion(version) {
|
|
34
|
+
return typeof version === 'string' && CANARY_RE.test(version);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read `<root>/package.json` and return its `.version`, or null if the file
|
|
39
|
+
* is missing/unreadable/unparsable.
|
|
40
|
+
* @param {string} root
|
|
41
|
+
* @returns {string|null}
|
|
42
|
+
*/
|
|
43
|
+
function readPackageVersion(root) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
|
|
46
|
+
const pkg = JSON.parse(raw);
|
|
47
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
const root = path.join(__dirname, '..');
|
|
55
|
+
const version = readPackageVersion(root);
|
|
56
|
+
if (version == null) {
|
|
57
|
+
process.stderr.write('canary-version-leak: could not read/parse package.json .version\n');
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!isCanaryVersion(version)) {
|
|
62
|
+
process.stdout.write(`ok canary-version-leak: package.json version '${version}' carries no -canary.<N> suffix\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
process.stderr.write(`canary-version-leak: package.json version '${version}' carries a -canary.<N> suffix (DEFECT.CANARY-VERSION-LEAK).\n`);
|
|
66
|
+
process.stderr.write('A -canary.<N> version must never land on main. Reset .version to the canonical\n');
|
|
67
|
+
process.stderr.write('pre-canary stable before merging — see CONTEXT.md DEFECT.CANARY-VERSION-LEAK.\n');
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (require.main === module) main();
|
|
72
|
+
|
|
73
|
+
module.exports = { isCanaryVersion, readPackageVersion, CANARY_RE };
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* 3. allowed-tools: block present, non-empty, all entries from CANONICAL_TOOLS
|
|
10
10
|
* 4. execution_context @-refs: every @-reference resolves to an existing file on disk
|
|
11
11
|
* 5. execution_context @-refs: each appears on its own line (no trailing prose)
|
|
12
|
+
* 6. every gsd-core/workflows/*.md file is reachable from at least one
|
|
13
|
+
* commands/agents/skills loader, transitively through gsd-core/**
|
|
14
|
+
* (repo-level check, runs once — not per command file)
|
|
12
15
|
*
|
|
13
16
|
* Exit 0 = clean. Exit 1 = violations (with diagnostics).
|
|
14
17
|
*/
|
|
@@ -18,7 +21,17 @@
|
|
|
18
21
|
const fs = require('fs');
|
|
19
22
|
const path = require('path');
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
function resolveRoot(argv) {
|
|
25
|
+
const idx = argv.indexOf('--root');
|
|
26
|
+
if (idx === -1) return path.join(__dirname, '..');
|
|
27
|
+
const value = argv[idx + 1];
|
|
28
|
+
if (!value) {
|
|
29
|
+
throw new Error('lint-command-contract: --root requires a directory argument');
|
|
30
|
+
}
|
|
31
|
+
return path.resolve(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ROOT = resolveRoot(process.argv.slice(2));
|
|
22
35
|
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
|
|
23
36
|
const GSD_ROOT = path.join(ROOT, 'gsd-core');
|
|
24
37
|
|
|
@@ -26,10 +39,57 @@ const {
|
|
|
26
39
|
CANONICAL_TOOLS,
|
|
27
40
|
parseFrontmatter,
|
|
28
41
|
executionContextRefs: extractExecutionContextRefs,
|
|
42
|
+
unreachableWorkflows,
|
|
29
43
|
} = require('./command-contract-helpers.cjs');
|
|
30
44
|
|
|
31
45
|
const { runMain } = require('./lib/cli-exit.cjs');
|
|
32
46
|
|
|
47
|
+
// ─── rule 6: repo-level workflow reachability ─────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function walkMarkdownFiles(dir, acc) {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return acc;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
walkMarkdownFiles(full, acc);
|
|
60
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
61
|
+
acc.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return acc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toGsdRelative(absPath) {
|
|
68
|
+
return path.relative(GSD_ROOT, absPath).split(path.sep).join('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkWorkflowReachability() {
|
|
72
|
+
const loaderFiles = [
|
|
73
|
+
...walkMarkdownFiles(path.join(ROOT, 'commands'), []),
|
|
74
|
+
...walkMarkdownFiles(path.join(ROOT, 'agents'), []),
|
|
75
|
+
...walkMarkdownFiles(path.join(ROOT, 'skills'), []),
|
|
76
|
+
];
|
|
77
|
+
const loaderContents = loaderFiles.map(f => fs.readFileSync(f, 'utf-8'));
|
|
78
|
+
|
|
79
|
+
const gsdFileAbsPaths = walkMarkdownFiles(GSD_ROOT, []);
|
|
80
|
+
const gsdFiles = new Map();
|
|
81
|
+
for (const abs of gsdFileAbsPaths) {
|
|
82
|
+
gsdFiles.set(toGsdRelative(abs), fs.readFileSync(abs, 'utf-8'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const workflowAbsPaths = walkMarkdownFiles(path.join(GSD_ROOT, 'workflows'), []);
|
|
86
|
+
const workflowPaths = workflowAbsPaths.map(toGsdRelative);
|
|
87
|
+
|
|
88
|
+
const unreachable = unreachableWorkflows(loaderContents, gsdFiles, workflowPaths);
|
|
89
|
+
|
|
90
|
+
return { workflowCount: workflowPaths.length, unreachable };
|
|
91
|
+
}
|
|
92
|
+
|
|
33
93
|
// ─── check one file ───────────────────────────────────────────────────────────
|
|
34
94
|
|
|
35
95
|
function check(filePath) {
|
|
@@ -88,27 +148,50 @@ function main() {
|
|
|
88
148
|
.map(f => path.join(COMMANDS_DIR, f));
|
|
89
149
|
|
|
90
150
|
const results = commandFiles.map(check).filter(Boolean);
|
|
151
|
+
const { workflowCount, unreachable } = checkWorkflowReachability();
|
|
152
|
+
|
|
153
|
+
let ok = true;
|
|
91
154
|
|
|
92
155
|
if (results.length === 0) {
|
|
93
156
|
console.log(
|
|
94
157
|
`ok lint-command-contract: ${commandFiles.length} command files checked, 0 violations`,
|
|
95
158
|
);
|
|
96
|
-
|
|
159
|
+
} else {
|
|
160
|
+
ok = false;
|
|
161
|
+
const total = results.reduce((n, r) => n + r.violations.length, 0);
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
`\nERROR lint-command-contract: ${total} violation(s) across ${results.length} file(s)\n\n`,
|
|
164
|
+
);
|
|
165
|
+
for (const r of results) {
|
|
166
|
+
process.stderr.write(` ${r.file}\n`);
|
|
167
|
+
for (const v of r.violations) {
|
|
168
|
+
process.stderr.write(` - ${v}\n`);
|
|
169
|
+
}
|
|
170
|
+
process.stderr.write('\n');
|
|
171
|
+
}
|
|
172
|
+
process.stderr.write('See docs/adr/0002-command-contract-validation-module.md for the contract spec.\n\n');
|
|
97
173
|
}
|
|
98
174
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
175
|
+
if (unreachable.length === 0) {
|
|
176
|
+
console.log(
|
|
177
|
+
`ok lint-command-contract: ${workflowCount} workflow files, ${workflowCount} reachable, 0 unreachable`,
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
ok = false;
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
`\nERROR lint-command-contract: ${unreachable.length} unreachable workflow file(s) of ${workflowCount}\n\n`,
|
|
183
|
+
);
|
|
184
|
+
for (const p of unreachable) {
|
|
185
|
+
process.stderr.write(` gsd-core/${p}\n`);
|
|
107
186
|
}
|
|
108
|
-
process.stderr.write(
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
'\nEach file above ships to every runtime but is never referenced by any command,\n' +
|
|
189
|
+
'agent, or skill loader (directly or transitively). Either wire it to a loader\n' +
|
|
190
|
+
'or delete it — removing a command must sweep its orphaned workflow.\n\n',
|
|
191
|
+
);
|
|
109
192
|
}
|
|
110
|
-
|
|
111
|
-
return 1;
|
|
193
|
+
|
|
194
|
+
return ok ? 0 : 1;
|
|
112
195
|
}
|
|
113
196
|
|
|
114
197
|
runMain(main);
|