@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,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration: retire pi's legacy `<piConfigDir>/hooks/` directory
|
|
4
|
+
* after GSD's shared hook bundle moved to `<piConfigDir>/gsd-hooks/` (#3023).
|
|
5
|
+
*
|
|
6
|
+
* What old artifact is being retired?
|
|
7
|
+
* `hooks/` (and its `hooks/lib/` subdirectory) at the pi config root. pi
|
|
8
|
+
* reserves that exact name as its own deprecated extension directory and
|
|
9
|
+
* warns on every startup whenever it exists — pi's
|
|
10
|
+
* `checkDeprecatedExtensionDirs()` fires on mere PATH EXISTENCE, not on the
|
|
11
|
+
* directory having contents (unlike the sibling `tools/` check, which does
|
|
12
|
+
* `readdir` first). GSD used to install its shared hook bundle at exactly
|
|
13
|
+
* that reserved path, so every pi install carried the warning permanently.
|
|
14
|
+
* The fix moved the install target to `gsd-hooks/`
|
|
15
|
+
* (`hostBehaviors.sharedHooksDirName`), but an EXISTING install that
|
|
16
|
+
* upgrades still has the old `hooks/` tree sitting on disk — nothing
|
|
17
|
+
* removes it on its own, so the warning would persist forever without this
|
|
18
|
+
* migration.
|
|
19
|
+
*
|
|
20
|
+
* How do we prove it is GSD-owned?
|
|
21
|
+
* Per file, by manifest membership — the same `classifyArtifact()` check
|
|
22
|
+
* every other migration in this directory uses. Pre-#3023 pi installs
|
|
23
|
+
* record the shared hook bundle under `hooks/…` keys in
|
|
24
|
+
* `gsd-file-manifest.json`; the new install target writes `gsd-hooks/…`
|
|
25
|
+
* keys instead, which this migration structurally never sees because it
|
|
26
|
+
* only ever walks the `hooks/` subtree.
|
|
27
|
+
*
|
|
28
|
+
* What happens if the user modified it?
|
|
29
|
+
* `backup-and-remove` instead of `remove-managed`, so a locally patched
|
|
30
|
+
* hook script is recoverable from the backup rather than silently
|
|
31
|
+
* destroyed — mirrors migration 006.
|
|
32
|
+
*
|
|
33
|
+
* What happens to files the manifest never recorded?
|
|
34
|
+
* Nothing. An unmanifested file under `hooks/` (classification `unknown`)
|
|
35
|
+
* is left exactly where it is, and — because its presence keeps the
|
|
36
|
+
* directory non-empty — it also keeps the directory itself from being
|
|
37
|
+
* retired. That is a deliberate consequence of directory removal being
|
|
38
|
+
* gated on emptiness, not a special case.
|
|
39
|
+
*
|
|
40
|
+
* What happens to the directory itself?
|
|
41
|
+
* `hooks/lib/` and then `hooks/` each get a `remove-empty-dir` action (see
|
|
42
|
+
* `evaluateRemoveEmptyDir` in `../installer-migrations.cts`). That action
|
|
43
|
+
* only ever calls `fs.rmdirSync` — never a recursive removal — and
|
|
44
|
+
* re-checks emptiness immediately before doing so, so a directory that
|
|
45
|
+
* still holds anything (an unmanifested file, or a file-level action that
|
|
46
|
+
* failed to apply) is left in place rather than assumed empty. Actions are
|
|
47
|
+
* emitted deepest-first (`hooks/lib` before `hooks`) so the parent has a
|
|
48
|
+
* chance to become empty in the same pass.
|
|
49
|
+
*
|
|
50
|
+
* What happens if it is missing?
|
|
51
|
+
* No actions. A fresh post-#3023 pi install never creates `hooks/` at all,
|
|
52
|
+
* and an already-migrated install has nothing left to retire — both plan
|
|
53
|
+
* empty, so the migration is idempotent.
|
|
54
|
+
*
|
|
55
|
+
* What runtime and scope does it affect?
|
|
56
|
+
* pi only, global and local. No other runtime's install is affected:
|
|
57
|
+
* `hostBehaviors.sharedHooksDirName` defaults to `'hooks'` for every other
|
|
58
|
+
* runtime, and none of them reserve that name the way pi does, so a
|
|
59
|
+
* claude/kimi/opencode/etc. `hooks/` directory is a live, in-use install
|
|
60
|
+
* surface that must never be touched here. The runtime check is the FIRST
|
|
61
|
+
* thing `plan()` does, ahead of even checking whether the directory exists,
|
|
62
|
+
* as defense in depth beyond the `runtimes: ['pi']` record-level filter the
|
|
63
|
+
* framework itself already enforces.
|
|
64
|
+
*
|
|
65
|
+
* Is the action safe in non-interactive install?
|
|
66
|
+
* Yes. Every emitted action type (`remove-managed`, `backup-and-remove`,
|
|
67
|
+
* `remove-empty-dir`) is non-interactive and journaled; none requires a
|
|
68
|
+
* user choice, and unknown files never produce an action.
|
|
69
|
+
*
|
|
70
|
+
* See docs/installer-migrations.md#shipped-migrations, the pi row of
|
|
71
|
+
* docs/installer-migrations.md#runtime-configuration-contract-registry, and
|
|
72
|
+
* the 2026-08-07 amendment to docs/adr/0008-installer-migration-module.md.
|
|
73
|
+
*/
|
|
74
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
75
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
|
+
};
|
|
77
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
78
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
79
|
+
/** pi's reserved (and, pre-#3023, GSD-populated) legacy hook directory name. */
|
|
80
|
+
const HOOKS_DIR = 'hooks';
|
|
81
|
+
const FILE_REASON = "pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and GSD's shared " +
|
|
82
|
+
'hook bundle now installs at gsd-hooks/ instead (#3023), so the legacy files are superseded';
|
|
83
|
+
const FILE_OWNERSHIP_EVIDENCE = 'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in gsd-file-manifest.json; ' +
|
|
84
|
+
'the new install target is gsd-hooks/…, which this migration never touches because it only walks the ' +
|
|
85
|
+
'hooks/ subtree';
|
|
86
|
+
const DIR_REASON = "pi's checkDeprecatedExtensionDirs() warns on hooks/'s mere existence, not its contents (#3023); the " +
|
|
87
|
+
'reserved container is retired once every GSD-owned entry inside it is gone';
|
|
88
|
+
const DIR_OWNERSHIP_EVIDENCE = 'hooks/ and hooks/lib/ are GSD-installed container directories under the pi config root (the pre-#3023 ' +
|
|
89
|
+
'default of hostBehaviors.sharedHooksDirName); removal is gated on emptiness by the shared ' +
|
|
90
|
+
'remove-empty-dir action, so a directory that still holds an unmanifested user file — or any file-level ' +
|
|
91
|
+
'action that failed to apply — is left in place rather than assumed empty';
|
|
92
|
+
/**
|
|
93
|
+
* Recursively collect files and directories under `relDir`, never following a
|
|
94
|
+
* symlink (whether it names a file or a directory) and never emitting a path
|
|
95
|
+
* that resolves outside `baseResolved`. Mirrors the traversal guard in
|
|
96
|
+
* migration 003 (`walkLegacyFiles`).
|
|
97
|
+
*/
|
|
98
|
+
function walkPiHooksTree(root, relDir, baseResolved, files, dirs) {
|
|
99
|
+
const dir = node_path_1.default.join(root, relDir);
|
|
100
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
// Never follow a symlink into or through: it must not be traversed,
|
|
103
|
+
// hashed, or removed, regardless of what it points at.
|
|
104
|
+
if (entry.isSymbolicLink())
|
|
105
|
+
continue;
|
|
106
|
+
const relPath = node_path_1.default.posix.join(relDir, entry.name);
|
|
107
|
+
const resolved = node_path_1.default.resolve(root, relPath);
|
|
108
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
|
|
109
|
+
continue;
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
dirs.push(relPath);
|
|
112
|
+
walkPiHooksTree(root, relPath, baseResolved, files, dirs);
|
|
113
|
+
}
|
|
114
|
+
else if (entry.isFile()) {
|
|
115
|
+
files.push(relPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const migration = {
|
|
120
|
+
id: '2026-08-07-pi-retire-reserved-hooks-dir',
|
|
121
|
+
title: "Retire pi's reserved hooks/ directory",
|
|
122
|
+
description: 'Remove manifest-managed files under <piConfigDir>/hooks/ and, once empty, the directory itself ' +
|
|
123
|
+
'(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at gsd-hooks/ instead. pi ' +
|
|
124
|
+
'reserves hooks/ as its own deprecated extension directory and warns on every startup while it exists (#3023).',
|
|
125
|
+
introducedIn: '1.9.2',
|
|
126
|
+
runtimes: ['pi'],
|
|
127
|
+
scopes: ['global', 'local'],
|
|
128
|
+
destructive: true,
|
|
129
|
+
plan: (ctx) => {
|
|
130
|
+
// Defense in depth ahead of the framework's own runtimes filter: a
|
|
131
|
+
// claude/kimi/opencode/etc. hooks/ directory is a live install surface,
|
|
132
|
+
// never a retirement target.
|
|
133
|
+
if (ctx.runtime !== 'pi')
|
|
134
|
+
return [];
|
|
135
|
+
const hooksRoot = node_path_1.default.join(ctx.configDir, HOOKS_DIR);
|
|
136
|
+
let rootLstat;
|
|
137
|
+
try {
|
|
138
|
+
rootLstat = node_fs_1.default.lstatSync(hooksRoot);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return []; // absent -> nothing to retire, idempotent
|
|
142
|
+
}
|
|
143
|
+
// Never follow a symlinked hooks/ root: walking through it could plan
|
|
144
|
+
// actions against paths outside the pi config directory entirely.
|
|
145
|
+
if (rootLstat.isSymbolicLink())
|
|
146
|
+
return [];
|
|
147
|
+
if (!rootLstat.isDirectory())
|
|
148
|
+
return [];
|
|
149
|
+
const baseResolved = node_path_1.default.resolve(ctx.configDir);
|
|
150
|
+
const files = [];
|
|
151
|
+
const dirs = [];
|
|
152
|
+
try {
|
|
153
|
+
walkPiHooksTree(ctx.configDir, HOOKS_DIR, baseResolved, files, dirs);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Unreadable directory: nothing safe to plan.
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const actions = [];
|
|
160
|
+
for (const relPath of files) {
|
|
161
|
+
const { classification } = ctx.classifyArtifact(relPath);
|
|
162
|
+
if (classification === 'managed-pristine') {
|
|
163
|
+
actions.push({ type: 'remove-managed', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
|
|
164
|
+
}
|
|
165
|
+
else if (classification === 'managed-modified') {
|
|
166
|
+
actions.push({ type: 'backup-and-remove', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
|
|
167
|
+
}
|
|
168
|
+
// 'unknown' (user-added, not manifest-recorded): no action, preserved.
|
|
169
|
+
// 'missing' / 'managed-missing': impossible here — relPath was just
|
|
170
|
+
// discovered by walking the live filesystem, so it currently exists.
|
|
171
|
+
}
|
|
172
|
+
// Deepest directories first, so a child has already been evaluated (and
|
|
173
|
+
// possibly removed) before its parent's own emptiness is re-checked by
|
|
174
|
+
// the executor. `hooks/` itself is appended last, unconditionally: the
|
|
175
|
+
// executor's own emptiness re-check is what actually decides whether it
|
|
176
|
+
// goes, not this ordering — this ordering only gives it the chance to.
|
|
177
|
+
const orderedDirs = [...dirs].sort((a, b) => b.split('/').length - a.split('/').length);
|
|
178
|
+
orderedDirs.push(HOOKS_DIR);
|
|
179
|
+
for (const relPath of orderedDirs) {
|
|
180
|
+
actions.push({
|
|
181
|
+
type: 'remove-empty-dir',
|
|
182
|
+
relPath,
|
|
183
|
+
reason: DIR_REASON,
|
|
184
|
+
ownershipEvidence: DIR_OWNERSHIP_EVIDENCE,
|
|
185
|
+
// Declared, not derived: classifyArtifact() hashes file contents via
|
|
186
|
+
// sha256File(), which throws EISDIR against a directory path. These
|
|
187
|
+
// relPaths name directories, so classification is stated directly
|
|
188
|
+
// (never 'unknown', so the planner's unknown-classification block
|
|
189
|
+
// never fires for them) rather than routed through the file
|
|
190
|
+
// classifier.
|
|
191
|
+
classification: 'managed-pristine',
|
|
192
|
+
originalHash: null,
|
|
193
|
+
currentHash: null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return actions;
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
module.exports = migration;
|
|
@@ -16,32 +16,133 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
16
16
|
const installer_migration_authoring_cjs_1 = require("./installer-migration-authoring.cjs");
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
18
|
const clock_cjs_1 = require("./clock.cjs");
|
|
19
|
+
const install_scope_cjs_1 = require("./install-scope.cjs");
|
|
20
|
+
// #2874 (ADR-58 cleanup phase): this file is the ~1200-line migration
|
|
21
|
+
// plan/apply/rollback/lock/journal engine — almost none of it is on the
|
|
22
|
+
// installRuntimeArtifacts call tree. Only `readInstallManifest` and
|
|
23
|
+
// `classifyArtifact` are reached (via install-engine.cts's
|
|
24
|
+
// _migrateLegacyOpencodeCommandDir and retired-artifact-cleanup.cts's
|
|
25
|
+
// pruneRetiredRuntimeArtifacts), so only those two entry points — plus their
|
|
26
|
+
// shared `readJsonIfPresent` helper and `classifyArtifact`'s `sha256File`
|
|
27
|
+
// hashing helper — are routed through the injectable seam. Everything else
|
|
28
|
+
// in this file (locking, journal, apply/rollback, migration discovery)
|
|
29
|
+
// keeps using real `fs` directly: it is not reachable from
|
|
30
|
+
// installRuntimeArtifacts, so routing it would grow this seam past what
|
|
31
|
+
// AC2 actually requires. See install-fs-adapter.cts's module doc.
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
+
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
34
|
+
const { installFs } = installFsAdapter;
|
|
19
35
|
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
|
20
36
|
const INSTALL_STATE_NAME = 'gsd-install-state.json';
|
|
21
37
|
const INSTALL_MIGRATION_LOCK_NAME = 'gsd-install-migration.lock';
|
|
22
38
|
const DEFAULT_MIGRATIONS_DIR = node_path_1.default.join(__dirname, 'installer-migrations');
|
|
23
39
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
24
40
|
const STRICT_JSON = Symbol('strict-json');
|
|
41
|
+
// #2874: routed through installFs()'s openSync/readSync/closeSync trio
|
|
42
|
+
// instead of importing `node:fs` directly, so classifyArtifact — reachable
|
|
43
|
+
// from installRuntimeArtifacts — can be exercised against an injected
|
|
44
|
+
// adapter. This function was briefly converted to a single
|
|
45
|
+
// `installFs().readFileSync` call (buffering the whole file); that broke
|
|
46
|
+
// tests/installer-migrations.test.cjs's "classifies large files without
|
|
47
|
+
// loading the whole file through readFileSync", which monkeypatches real
|
|
48
|
+
// fs.readFileSync to throw for the file under test and asserts hashing still
|
|
49
|
+
// succeeds — an explicit, pre-existing contract that large files must be
|
|
50
|
+
// streamed, not buffered. Restored to the original raw-fd streaming shape,
|
|
51
|
+
// now going through the adapter instead of `node:fs` directly. This is the
|
|
52
|
+
// ONLY call site of sha256File in this file (confirmed by inspection) — no
|
|
53
|
+
// other caller is affected.
|
|
25
54
|
function sha256File(filePath) {
|
|
26
55
|
const hash = node_crypto_1.default.createHash('sha256');
|
|
27
56
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
28
|
-
const fd =
|
|
57
|
+
const fd = installFs().openSync(filePath, 'r');
|
|
29
58
|
try {
|
|
30
59
|
while (true) {
|
|
31
|
-
const bytesRead =
|
|
60
|
+
const bytesRead = installFs().readSync(fd, buffer, 0, buffer.length, null);
|
|
32
61
|
if (bytesRead === 0)
|
|
33
62
|
break;
|
|
34
63
|
hash.update(buffer.subarray(0, bytesRead));
|
|
35
64
|
}
|
|
36
65
|
}
|
|
37
66
|
finally {
|
|
38
|
-
|
|
67
|
+
installFs().closeSync(fd);
|
|
39
68
|
}
|
|
40
69
|
return hash.digest('hex');
|
|
41
70
|
}
|
|
42
71
|
function sha256Text(value) {
|
|
43
72
|
return node_crypto_1.default.createHash('sha256').update(value).digest('hex');
|
|
44
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Evaluate and, if safe, perform a `remove-empty-dir` action against `fullPath`.
|
|
76
|
+
*
|
|
77
|
+
* This is deliberately WEAKER than a recursive directory-removal primitive
|
|
78
|
+
* (which 003's docblock records as an intentional absence in the ADR-0008
|
|
79
|
+
* design): it only ever calls `fs.rmdirSync` — never `fs.rmSync`, never
|
|
80
|
+
* `{ recursive: true }`, never `{ force: true }` — so a non-empty directory
|
|
81
|
+
* fails the underlying syscall rather than being swept. The emptiness check
|
|
82
|
+
* immediately above the call is what turns that failure mode into a
|
|
83
|
+
* deliberate, non-error "left in place" outcome instead of surfacing ENOTEMPTY.
|
|
84
|
+
*
|
|
85
|
+
* Guards, in order:
|
|
86
|
+
* - lstat (not stat): a symlinked directory is refused outright, never
|
|
87
|
+
* followed. A missing target is reported distinctly so callers can tell
|
|
88
|
+
* "nothing was ever there" from "something was there and is left alone".
|
|
89
|
+
* - must actually be a directory (not a file masquerading under the relPath).
|
|
90
|
+
* - containment: the REALPATH of the target must resolve strictly inside the
|
|
91
|
+
* REALPATH of configDir — never equal to it (removing the config root
|
|
92
|
+
* itself is never in scope) and never escaping it (e.g. via an ancestor
|
|
93
|
+
* symlink the lstat check alone would not catch).
|
|
94
|
+
* - emptiness, re-checked here rather than trusted from planning time: a
|
|
95
|
+
* directory that still holds any entry (managed-but-undeleted, unknown,
|
|
96
|
+
* or created between plan and apply) is left in place. This is reported
|
|
97
|
+
* as 'skipped-not-empty', a successful no-op, not a failure.
|
|
98
|
+
*
|
|
99
|
+
* Any unexpected error along the way (EACCES, EBUSY, a race that removes the
|
|
100
|
+
* target between the lstat and the rmdir, etc.) degrades to 'left-in-place'.
|
|
101
|
+
* This action must never throw out of the executor, matching every sibling
|
|
102
|
+
* action type's failure posture.
|
|
103
|
+
*/
|
|
104
|
+
function evaluateRemoveEmptyDir(configDir, fullPath) {
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = node_fs_1.default.lstatSync(fullPath);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return 'missing';
|
|
111
|
+
}
|
|
112
|
+
if (stat.isSymbolicLink())
|
|
113
|
+
return 'left-in-place';
|
|
114
|
+
if (!stat.isDirectory())
|
|
115
|
+
return 'left-in-place';
|
|
116
|
+
let resolvedRoot;
|
|
117
|
+
let resolvedTarget;
|
|
118
|
+
try {
|
|
119
|
+
resolvedRoot = node_fs_1.default.realpathSync(configDir);
|
|
120
|
+
resolvedTarget = node_fs_1.default.realpathSync(fullPath);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return 'left-in-place';
|
|
124
|
+
}
|
|
125
|
+
if (resolvedTarget === resolvedRoot || !resolvedTarget.startsWith(resolvedRoot + node_path_1.default.sep)) {
|
|
126
|
+
// Refuses both "target IS configDir" and "target escaped configDir".
|
|
127
|
+
return 'left-in-place';
|
|
128
|
+
}
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = node_fs_1.default.readdirSync(fullPath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 'left-in-place';
|
|
135
|
+
}
|
|
136
|
+
if (entries.length > 0)
|
|
137
|
+
return 'skipped-not-empty';
|
|
138
|
+
try {
|
|
139
|
+
node_fs_1.default.rmdirSync(fullPath);
|
|
140
|
+
return 'removed';
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return 'left-in-place';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
45
146
|
/**
|
|
46
147
|
* Copy a managed path for the rollback snapshot or the user-facing backup,
|
|
47
148
|
* WITHOUT dereferencing a symlink.
|
|
@@ -60,24 +161,36 @@ function sha256Text(value) {
|
|
|
60
161
|
* surfaces as an apply failure and triggers the normal rollback path, which is
|
|
61
162
|
* the correct outcome — refusing to proceed beats silently copying referent
|
|
62
163
|
* bytes.
|
|
164
|
+
*
|
|
165
|
+
* #2875 (epic #2866 Phase 6): all five fs calls routed through `installFs()`
|
|
166
|
+
* so this primitive can be reused on the routed install path (by
|
|
167
|
+
* user-artifact-staging.cts) without punching a hole through the seam Phase 5
|
|
168
|
+
* built. Every EXISTING caller of this function is on the migration
|
|
169
|
+
* plan/apply/rollback tree, which never wraps a call in `withInstallFs` — the
|
|
170
|
+
* ambient adapter there resolves to real `node:fs` by default, so this
|
|
171
|
+
* routing is behavior-preserving for them (test-matrix D2).
|
|
63
172
|
*/
|
|
64
173
|
function copyPreservingSymlink(srcPath, destPath) {
|
|
65
|
-
if (
|
|
174
|
+
if (installFs().lstatSync(srcPath).isSymbolicLink()) {
|
|
66
175
|
// symlinkSync fails with EEXIST on an occupied path, so clear it first.
|
|
67
176
|
// Scoped to this branch on purpose: the regular-file path below keeps
|
|
68
177
|
// copyFileSync's overwrite-in-place, so a mid-restore failure cannot leave
|
|
69
178
|
// the destination destroyed.
|
|
70
|
-
|
|
71
|
-
|
|
179
|
+
installFs().rmSync(destPath, { force: true });
|
|
180
|
+
installFs().symlinkSync(installFs().readlinkSync(srcPath), destPath);
|
|
72
181
|
return;
|
|
73
182
|
}
|
|
74
|
-
|
|
183
|
+
installFs().copyFileSync(srcPath, destPath);
|
|
75
184
|
}
|
|
185
|
+
// Shared by readInstallManifest (on the installRuntimeArtifacts call tree —
|
|
186
|
+
// routed) and readInstallState/readJson (not on that call tree — the
|
|
187
|
+
// ambient default resolves to real fs for those, unchanged). Routing once
|
|
188
|
+
// here is safe for all three callers.
|
|
76
189
|
function readJsonIfPresent(filePath, fallback) {
|
|
77
|
-
if (!
|
|
190
|
+
if (!installFs().existsSync(filePath))
|
|
78
191
|
return fallback;
|
|
79
192
|
try {
|
|
80
|
-
return JSON.parse(
|
|
193
|
+
return JSON.parse(installFs().readFileSync(filePath, 'utf8'));
|
|
81
194
|
}
|
|
82
195
|
catch (error) {
|
|
83
196
|
if (fallback === STRICT_JSON) {
|
|
@@ -86,17 +199,81 @@ function readJsonIfPresent(filePath, fallback) {
|
|
|
86
199
|
return fallback;
|
|
87
200
|
}
|
|
88
201
|
}
|
|
202
|
+
/** Lowest manifest schema version that records `runtime`/`scope` (#2872). */
|
|
203
|
+
const MANIFEST_SCHEMA_VERSION = 2;
|
|
204
|
+
/**
|
|
205
|
+
* Longest `runtime` string this reader will report. Real runtime ids are
|
|
206
|
+
* registry keys (`claude`, `antigravity`, `kimi-code` — 11 chars at the
|
|
207
|
+
* longest), so this loses nothing legitimate; it exists because the manifest
|
|
208
|
+
* is attacker-influenceable (a project-local one lives inside a repository a
|
|
209
|
+
* user may merely have cloned) and the value reaches a consumer that renders
|
|
210
|
+
* it. Same 64-char convention as `truncatePostureValue`
|
|
211
|
+
* (`agent-install-check.cts`), deliberately, so the subsystem caps reported
|
|
212
|
+
* values one way.
|
|
213
|
+
*/
|
|
214
|
+
const MAX_REPORTED_RUNTIME_LENGTH = 64;
|
|
215
|
+
/**
|
|
216
|
+
* A manifest's `runtime` is reported as a FACT about the file — it is
|
|
217
|
+
* deliberately NOT validated against the capability registry, because an
|
|
218
|
+
* unregistered id is exactly the kind of mismatch the Installed Surface
|
|
219
|
+
* Resolver exists to surface (#2872 design row B8). It is, however, LENGTH
|
|
220
|
+
* bounded: "report the fact" never required "report unbounded bytes".
|
|
221
|
+
*/
|
|
222
|
+
function normalizeReportedRuntime(raw) {
|
|
223
|
+
if (typeof raw !== 'string')
|
|
224
|
+
return null;
|
|
225
|
+
if (raw.trim() === '')
|
|
226
|
+
return null;
|
|
227
|
+
return raw.length > MAX_REPORTED_RUNTIME_LENGTH
|
|
228
|
+
? `${raw.slice(0, MAX_REPORTED_RUNTIME_LENGTH)}…`
|
|
229
|
+
: raw;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Normalize a raw `manifestVersion`. Only a finite integer >= 1 is a version
|
|
233
|
+
* claim; everything else (absent, `"2"`, `0`, `-1`, `2.5`, `NaN`, `Infinity`)
|
|
234
|
+
* reads as `1` — a pre-#2872 manifest. Liberal in what it accepts, but the
|
|
235
|
+
* normalization is a stated value rather than a silent guess: a caller can
|
|
236
|
+
* always tell v1 (`1`) from "no manifest at all" (`null`).
|
|
237
|
+
*/
|
|
238
|
+
function normalizeManifestVersion(raw) {
|
|
239
|
+
if (typeof raw !== 'number')
|
|
240
|
+
return 1;
|
|
241
|
+
if (!Number.isInteger(raw))
|
|
242
|
+
return 1;
|
|
243
|
+
if (raw < 1)
|
|
244
|
+
return 1;
|
|
245
|
+
return raw;
|
|
246
|
+
}
|
|
89
247
|
function readInstallManifest(configDir) {
|
|
90
248
|
const manifest = readJsonIfPresent(node_path_1.default.join(configDir, MANIFEST_NAME), null);
|
|
91
|
-
|
|
92
|
-
|
|
249
|
+
// `typeof [] === 'object'` in JS, so a bare `typeof !== 'object'` guard lets
|
|
250
|
+
// a top-level JSON array (valid JSON, but not the manifest's documented
|
|
251
|
+
// object shape) fall through to the field reads below — `m.manifestVersion`
|
|
252
|
+
// reads `undefined` off an array, which `normalizeManifestVersion` then
|
|
253
|
+
// reports as `1` (a v1 manifest), misclassifying "not an object" as
|
|
254
|
+
// "installed". `Array.isArray` closes that gap explicitly rather than
|
|
255
|
+
// relying on the object-shape checks below to catch it incidentally.
|
|
256
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
257
|
+
return {
|
|
258
|
+
version: null,
|
|
259
|
+
timestamp: null,
|
|
260
|
+
mode: null,
|
|
261
|
+
files: {},
|
|
262
|
+
manifestVersion: null,
|
|
263
|
+
runtime: null,
|
|
264
|
+
scope: null,
|
|
265
|
+
};
|
|
93
266
|
}
|
|
94
267
|
const m = manifest;
|
|
268
|
+
const rawRuntime = m.runtime;
|
|
95
269
|
return {
|
|
96
270
|
version: typeof m.version === 'string' ? m.version : null,
|
|
97
271
|
timestamp: typeof m.timestamp === 'string' ? m.timestamp : null,
|
|
98
272
|
mode: typeof m.mode === 'string' ? m.mode : null,
|
|
99
273
|
files: m.files && typeof m.files === 'object' ? m.files : {},
|
|
274
|
+
manifestVersion: normalizeManifestVersion(m.manifestVersion),
|
|
275
|
+
runtime: normalizeReportedRuntime(rawRuntime),
|
|
276
|
+
scope: (0, install_scope_cjs_1.isInstallScopeId)(m.scope) ? m.scope : null,
|
|
100
277
|
};
|
|
101
278
|
}
|
|
102
279
|
function readInstallState(configDir) {
|
|
@@ -163,7 +340,7 @@ function classifyArtifact(configDir, relPath, manifest) {
|
|
|
163
340
|
const normalized = normalizeRelPath(relPath);
|
|
164
341
|
const originalHash = manifest.files[normalized] || null;
|
|
165
342
|
const fullPath = node_path_1.default.join(configDir, normalized);
|
|
166
|
-
if (!
|
|
343
|
+
if (!installFs().existsSync(fullPath)) {
|
|
167
344
|
return { classification: originalHash ? 'managed-missing' : 'missing', originalHash, currentHash: null };
|
|
168
345
|
}
|
|
169
346
|
const currentHash = sha256File(fullPath);
|
|
@@ -643,7 +820,8 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
643
820
|
action.type !== 'backup-and-remove' &&
|
|
644
821
|
action.type !== 'rewrite-json' &&
|
|
645
822
|
action.type !== 'record-baseline' &&
|
|
646
|
-
action.type !== 'baseline-preserve-user'
|
|
823
|
+
action.type !== 'baseline-preserve-user' &&
|
|
824
|
+
action.type !== 'remove-empty-dir') {
|
|
647
825
|
throw new Error(`unsupported migration action type: ${action.type}`);
|
|
648
826
|
}
|
|
649
827
|
const { normalized, fullPath } = ensureInsideConfig(configDir, action.relPath);
|
|
@@ -655,6 +833,18 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
|
|
|
655
833
|
journal.actions.push(journalAction(action, action.type === 'record-baseline' ? 'recorded' : 'preserved'));
|
|
656
834
|
continue;
|
|
657
835
|
}
|
|
836
|
+
if (action.type === 'remove-empty-dir') {
|
|
837
|
+
// Directory actions never enter the file-copy/rollback machinery below:
|
|
838
|
+
// there is nothing to snapshot-and-restore for a directory node itself
|
|
839
|
+
// (its former CONTENTS were already snapshotted by their own file-level
|
|
840
|
+
// actions before this one runs), and rollback of a removed empty
|
|
841
|
+
// directory is simply re-creating it, which the rollback path below
|
|
842
|
+
// does not model. Non-recursive by construction (evaluateRemoveEmptyDir
|
|
843
|
+
// only ever calls fs.rmdirSync), so there is nothing destructive to undo
|
|
844
|
+
// beyond an mkdir the next install/migration run will happily redo.
|
|
845
|
+
journal.actions.push(journalAction(action, evaluateRemoveEmptyDir(configDir, fullPath)));
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
658
848
|
const rollbackPath = node_path_1.default.join(rollbackRoot, normalized);
|
|
659
849
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(rollbackPath), { recursive: true });
|
|
660
850
|
copyPreservingSymlink(fullPath, rollbackPath);
|
|
@@ -852,7 +1042,10 @@ module.exports = {
|
|
|
852
1042
|
acquireInstallMigrationLock,
|
|
853
1043
|
applyInstallerMigrationPlan,
|
|
854
1044
|
classifyArtifact,
|
|
1045
|
+
copyPreservingSymlink,
|
|
855
1046
|
discoverInstallerMigrations,
|
|
1047
|
+
evaluateRemoveEmptyDir,
|
|
1048
|
+
MANIFEST_SCHEMA_VERSION,
|
|
856
1049
|
migrationChecksum,
|
|
857
1050
|
planInstallerMigrations,
|
|
858
1051
|
readInstallManifest,
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -118,6 +118,22 @@ function writeAllSync(fd, data) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The wire form of a JSON result: the exact bytes `output()` emits for it.
|
|
123
|
+
*
|
|
124
|
+
* Exported because a caller that has to reason about the size of its own
|
|
125
|
+
* response — graphify's `--budget` accounting (#2738) — must measure the string
|
|
126
|
+
* this function produces rather than a second, privately-maintained
|
|
127
|
+
* serialization that can drift from it. One definition, so estimator and
|
|
128
|
+
* emitter cannot disagree about indentation or shape.
|
|
129
|
+
*
|
|
130
|
+
* The `@file:` redirection in `output()` is a transport detail, not a payload
|
|
131
|
+
* change: the caller still consumes these bytes, so these are the right ones to
|
|
132
|
+
* measure.
|
|
133
|
+
*/
|
|
134
|
+
function serializeForOutput(result) {
|
|
135
|
+
return JSON.stringify(result, null, 2);
|
|
136
|
+
}
|
|
121
137
|
function output(result, raw, rawValue) {
|
|
122
138
|
let data;
|
|
123
139
|
if (raw && rawValue !== undefined) {
|
|
@@ -125,7 +141,7 @@ function output(result, raw, rawValue) {
|
|
|
125
141
|
data = String(rawValue);
|
|
126
142
|
}
|
|
127
143
|
else {
|
|
128
|
-
const json =
|
|
144
|
+
const json = serializeForOutput(result);
|
|
129
145
|
// Large payloads exceed Claude Code's Bash tool buffer (~50KB).
|
|
130
146
|
// Write to tmpfile and output the path prefixed with @file: so callers can detect it.
|
|
131
147
|
if (json.length > 50000) {
|
|
@@ -168,12 +184,23 @@ const ERROR_REASON = Object.freeze({
|
|
|
168
184
|
// workflow / phase
|
|
169
185
|
PHASE_NOT_FOUND: 'phase_not_found',
|
|
170
186
|
PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
|
|
187
|
+
PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
|
|
171
188
|
SUMMARY_NO_PLANNING: 'summary_no_planning',
|
|
189
|
+
// #3579: workstream-mode fail-safe guards (init.progress, phase.complete) —
|
|
190
|
+
// distinguishes "no marker/pointer anywhere" from "a marker exists but
|
|
191
|
+
// didn't resolve" so a JSON-error-mode caller can branch on `reason`
|
|
192
|
+
// instead of regexing the human message.
|
|
193
|
+
WORKSTREAM_MODE_NONE_ACTIVE: 'workstream_mode_none_active',
|
|
194
|
+
WORKSTREAM_MODE_MARKER_UNRESOLVED: 'workstream_mode_marker_unresolved',
|
|
172
195
|
// graphify
|
|
173
196
|
GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
|
|
174
197
|
GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
|
|
175
198
|
// hooks
|
|
176
199
|
HOOKS_OPT_OUT: 'hooks_opt_out',
|
|
200
|
+
// commit-docs-guard (#3588)
|
|
201
|
+
COMMIT_DOCS_GUARD_NOT_A_REPO: 'commit_docs_guard_not_a_repo',
|
|
202
|
+
COMMIT_DOCS_GUARD_FOREIGN_HOOK: 'commit_docs_guard_foreign_hook',
|
|
203
|
+
COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
|
|
177
204
|
// security-scan
|
|
178
205
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
179
206
|
// generic
|
|
@@ -198,10 +225,17 @@ function getJsonErrorMode() { return _jsonErrorMode; }
|
|
|
198
225
|
* process is in JSON-error mode, stderr receives `{ ok: false, reason,
|
|
199
226
|
* message }` so callers can parse it; otherwise stderr keeps the plain
|
|
200
227
|
* text form for human operators.
|
|
228
|
+
*
|
|
229
|
+
* `extra` (optional) lets a caller attach additional structured fields
|
|
230
|
+
* (e.g. `{ verification_stale_check_indeterminate: true }`) onto the
|
|
231
|
+
* JSON-error-mode payload, spread alongside `ok`/`reason`/`message`, so a
|
|
232
|
+
* test can assert on the value directly instead of regexing `message`'s
|
|
233
|
+
* human-readable text. Ignored entirely in plain-text mode — the human
|
|
234
|
+
* message is the only thing an operator sees there.
|
|
201
235
|
*/
|
|
202
|
-
function error(message, reason = ERROR_REASON.UNKNOWN) {
|
|
236
|
+
function error(message, reason = ERROR_REASON.UNKNOWN, extra) {
|
|
203
237
|
if (_jsonErrorMode) {
|
|
204
|
-
const payload = JSON.stringify({ ok: false, reason, message }) + '\n';
|
|
238
|
+
const payload = JSON.stringify({ ok: false, reason, message, ...(extra || {}) }) + '\n';
|
|
205
239
|
writeAllSync(2, payload);
|
|
206
240
|
}
|
|
207
241
|
else {
|
|
@@ -214,6 +248,7 @@ module.exports = {
|
|
|
214
248
|
ensureGsdTempDir,
|
|
215
249
|
reapStaleTempFiles,
|
|
216
250
|
output,
|
|
251
|
+
serializeForOutput,
|
|
217
252
|
ERROR_REASON,
|
|
218
253
|
setJsonErrorMode,
|
|
219
254
|
getJsonErrorMode,
|
|
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
14
|
exports.stripFencedCode = stripFencedCode;
|
|
15
15
|
exports.stripInlineCode = stripInlineCode;
|
|
16
16
|
exports.scanInlineCodeSpans = scanInlineCodeSpans;
|
|
17
|
+
exports.scanFencedBlocks = scanFencedBlocks;
|
|
17
18
|
exports.extractFencedBlock = extractFencedBlock;
|
|
18
19
|
exports.tokenizeHeadings = tokenizeHeadings;
|
|
19
20
|
exports.collectSections = collectSections;
|
|
@@ -25,6 +26,7 @@ exports.stripTaggedBlocks = stripTaggedBlocks;
|
|
|
25
26
|
exports.replaceSection = replaceSection;
|
|
26
27
|
exports.withSection = withSection;
|
|
27
28
|
exports.deleteSection = deleteSection;
|
|
29
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
28
30
|
// ─── stripFencedCode ──────────────────────────────────────────────────────────
|
|
29
31
|
/**
|
|
30
32
|
* CommonMark-correct fenced-code-block stripper.
|
|
@@ -206,6 +208,11 @@ function stripInlineCodeLine(line) {
|
|
|
206
208
|
* Tracked duplication (same status as `tokenizeHeadings`'s copy, see its
|
|
207
209
|
* comment above): this is a second independent copy of the fence state
|
|
208
210
|
* machine, pending a T-tier consolidation.
|
|
211
|
+
*
|
|
212
|
+
* Exported so `context-predicates.cts` can consume this seam directly for its
|
|
213
|
+
* line-preserving fenced-line skip detection, instead of carrying a third
|
|
214
|
+
* independent copy of the fence state machine (see that module's doc
|
|
215
|
+
* comment).
|
|
209
216
|
*/
|
|
210
217
|
function scanFencedBlocks(lines) {
|
|
211
218
|
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
@@ -786,7 +793,7 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
|
|
|
786
793
|
* open>` marks the ACTIVE milestone and must be preserved, not stripped (#557).
|
|
787
794
|
*/
|
|
788
795
|
function taggedBlockPattern(tagName, flags, allowAttributes) {
|
|
789
|
-
const esc =
|
|
796
|
+
const esc = (0, pattern_cjs_1.escapeRegex)(tagName);
|
|
790
797
|
const open = allowAttributes ? `<${esc}(?:\\s[^>]{0,1000})?>` : `<${esc}>`;
|
|
791
798
|
const boundary = allowAttributes ? `<${esc}[\\s>]` : `<${esc}>`;
|
|
792
799
|
return new RegExp(`${open}((?:(?!${boundary})[\\s\\S])*?)</${esc}>`, flags);
|