@opengsd/gsd-core 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +17 -9
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +15 -55
- package/agents/gsd-planner.md +6 -4
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +609 -134
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/quick.md +8 -4
- package/gsd-core/bin/gsd-tools.cjs +567 -51
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +336 -95
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-validator.cjs +205 -18
- package/gsd-core/bin/lib/check-command-router.cjs +145 -5
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +543 -44
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +118 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +84 -7
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/frontmatter.cjs +840 -305
- package/gsd-core/bin/lib/gap-checker.cjs +27 -3
- package/gsd-core/bin/lib/git-base-branch.cjs +174 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +6 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +120 -41
- package/gsd-core/bin/lib/install-engine.cjs +68 -3
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -1
- package/gsd-core/bin/lib/install-profiles.cjs +78 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +160 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-table.cjs +52 -4
- package/gsd-core/bin/lib/milestone.cjs +90 -5
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +70 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +405 -84
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +18 -14
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -0
- package/gsd-core/bin/lib/probe-core.cjs +4 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +9 -9
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +79 -16
- package/gsd-core/bin/lib/roadmap.cjs +74 -19
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +96 -8
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +287 -55
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +71 -8
- package/gsd-core/bin/lib/smart-entry.cjs +12 -22
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +186 -0
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +517 -101
- package/gsd-core/bin/lib/state.cjs +946 -163
- package/gsd-core/bin/lib/surface.cjs +10 -2
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +1376 -125
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification.cjs +14 -1
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +95 -40
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +177 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +3 -2
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +1 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +37 -8
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +2 -2
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/phase-prompt.md +3 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -9
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +10 -26
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +121 -40
- package/gsd-core/workflows/complete-milestone.md +15 -10
- package/gsd-core/workflows/debug.md +5 -3
- package/gsd-core/workflows/diagnose-issues.md +12 -6
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +3 -7
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -2
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -9
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +113 -11
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +22 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +38 -54
- package/gsd-core/workflows/execute-plan.md +17 -12
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +2 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +3 -6
- package/gsd-core/workflows/import.md +14 -11
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +44 -11
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +2 -2
- package/gsd-core/workflows/new-milestone.md +9 -21
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +12 -26
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +121 -42
- package/gsd-core/workflows/plan-review-convergence.md +46 -9
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +27 -12
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -3
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -4
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +20 -29
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +193 -15
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +2 -2
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +3 -5
- package/gsd-core/workflows/ship.md +12 -6
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +23 -1
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +1 -1
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +5 -5
- package/gsd-core/workflows/ui-phase.md +10 -16
- package/gsd-core/workflows/ui-review.md +6 -10
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +6 -10
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +57 -18
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +16 -7
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +17 -8
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +215 -26
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +34 -16
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +16 -7
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +17 -8
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +215 -26
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +34 -16
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +12 -7
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +5 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +45 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +134 -1
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +21 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +122 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/prompt-injection-scan.sh +75 -14
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -23,6 +23,9 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
|
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
24
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
25
25
|
const { isCapabilityActive } = capabilityStateMod;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const ioMod = require("./io.cjs");
|
|
28
|
+
const { formatDiagnosticToken } = ioMod;
|
|
26
29
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
27
30
|
const INTEL_DIR = '.planning/intel';
|
|
28
31
|
const INTEL_FILES = {
|
|
@@ -32,6 +35,16 @@ const INTEL_FILES = {
|
|
|
32
35
|
arch: 'arch-decisions.json',
|
|
33
36
|
stack: 'stack.json',
|
|
34
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* ADR-3473 §8.5 / #3885: recursion bound for the intel JSON search walk.
|
|
40
|
+
* Restored from the retired SDK lineage (`sdk/src/query/intel.ts` at `11918dcc3^`),
|
|
41
|
+
* lost in the ADR-0174 consolidation. Unlike the original, hitting the ceiling is
|
|
42
|
+
* NOT reported as a silent "no match" — the walk that stops early sets a
|
|
43
|
+
* `truncated` flag threaded back up to `intelQuery`'s result (Decision 4: a
|
|
44
|
+
* routine that discards an input says so). The bound is on DEPTH only; breadth
|
|
45
|
+
* (sibling count at any given depth) is unaffected.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_JSON_SEARCH_DEPTH = 48;
|
|
35
48
|
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
36
49
|
/**
|
|
37
50
|
* Ensure the intel directory exists under the given planning dir.
|
|
@@ -71,16 +84,42 @@ function intelFilePath(planningDir, filename) {
|
|
|
71
84
|
}
|
|
72
85
|
/**
|
|
73
86
|
* Safely read and parse a JSON intel file.
|
|
74
|
-
*
|
|
87
|
+
*
|
|
88
|
+
* Returns null for THREE distinct on-disk states, only one of which is a
|
|
89
|
+
* "quiet" case (#3885, ADR-3473 §8.5):
|
|
90
|
+
* - ABSENT (ENOENT, via platformReadSync returning null): silent — not
|
|
91
|
+
* every project has every intel file, and callers already loop over
|
|
92
|
+
* the full INTEL_FILES set expecting misses. Never pushed to `errors`.
|
|
93
|
+
* - UNREADABLE (EACCES/EIO/... — platformReadSync rethrows anything that
|
|
94
|
+
* isn't ENOENT): surfaced, naming the file, when `errors` is supplied.
|
|
95
|
+
* - MALFORMED (JSON.parse throws on a file that WAS read successfully):
|
|
96
|
+
* also surfaced, naming the file — a corrupt intel file used to read
|
|
97
|
+
* identically to "no matches", which is the same defect one layer down.
|
|
98
|
+
*
|
|
99
|
+
* `errors` is an optional accumulator so callers that want to thread the
|
|
100
|
+
* outcome into their own result shape can pass an array and read it back
|
|
101
|
+
* after the call; callers that omit it keep the prior fold-to-null shape.
|
|
75
102
|
*/
|
|
76
|
-
function safeReadJson(filePath) {
|
|
103
|
+
function safeReadJson(filePath, errors) {
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (errors) {
|
|
110
|
+
errors.push(`Could not read ${formatDiagnosticToken(filePath)}: ${formatDiagnosticToken(err?.message ?? String(err))}`);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (raw === null)
|
|
115
|
+
return null; // ENOENT — genuinely absent, not an error.
|
|
77
116
|
try {
|
|
78
|
-
const raw = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
|
|
79
|
-
if (raw === null)
|
|
80
|
-
return null;
|
|
81
117
|
return JSON.parse(raw);
|
|
82
118
|
}
|
|
83
|
-
catch {
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (errors) {
|
|
121
|
+
errors.push(`Could not parse ${formatDiagnosticToken(filePath)}: ${formatDiagnosticToken(err?.message ?? String(err))}`);
|
|
122
|
+
}
|
|
84
123
|
return null;
|
|
85
124
|
}
|
|
86
125
|
}
|
|
@@ -101,16 +140,17 @@ function hashFile(filePath) {
|
|
|
101
140
|
}
|
|
102
141
|
/**
|
|
103
142
|
* Search for a term (case-insensitive) in a JSON object's keys and string values.
|
|
104
|
-
* Returns
|
|
143
|
+
* Returns matching entries plus whether the walk hit MAX_JSON_SEARCH_DEPTH.
|
|
105
144
|
*/
|
|
106
145
|
function searchJsonEntries(data, term) {
|
|
107
146
|
if (!data || typeof data !== 'object')
|
|
108
|
-
return [];
|
|
147
|
+
return { matches: [], truncated: false };
|
|
109
148
|
const entries = data.entries || data;
|
|
110
149
|
if (!entries || typeof entries !== 'object')
|
|
111
|
-
return [];
|
|
150
|
+
return { matches: [], truncated: false };
|
|
112
151
|
const lowerTerm = term.toLowerCase();
|
|
113
152
|
const matches = [];
|
|
153
|
+
const state = { truncated: false };
|
|
114
154
|
for (const [key, value] of Object.entries(entries)) {
|
|
115
155
|
if (key === '_meta')
|
|
116
156
|
continue;
|
|
@@ -119,25 +159,40 @@ function searchJsonEntries(data, term) {
|
|
|
119
159
|
matches.push({ key, value });
|
|
120
160
|
continue;
|
|
121
161
|
}
|
|
122
|
-
// Check string value match (recursive for objects)
|
|
123
|
-
if (matchesInValue(value, lowerTerm)) {
|
|
162
|
+
// Check string value match (recursive for objects/arrays, bounded by depth)
|
|
163
|
+
if (matchesInValue(value, lowerTerm, 0, state)) {
|
|
124
164
|
matches.push({ key, value });
|
|
125
165
|
}
|
|
126
166
|
}
|
|
127
|
-
return matches;
|
|
167
|
+
return { matches, truncated: state.truncated };
|
|
128
168
|
}
|
|
129
169
|
/**
|
|
130
170
|
* Recursively check if a term appears in any string value.
|
|
171
|
+
*
|
|
172
|
+
* `depth` counts container unwraps already performed (starts at 0 for the
|
|
173
|
+
* entry's own value). Strings never fail the ceiling check themselves — only
|
|
174
|
+
* a container (object/array) refuses to recurse one level deeper once
|
|
175
|
+
* `depth > MAX_JSON_SEARCH_DEPTH`, at which point `state.truncated` is set so
|
|
176
|
+
* the caller can report "I stopped looking" rather than a bare "no match".
|
|
177
|
+
* The bound is on nesting depth only, never on sibling breadth.
|
|
131
178
|
*/
|
|
132
|
-
function matchesInValue(value, lowerTerm) {
|
|
179
|
+
function matchesInValue(value, lowerTerm, depth, state) {
|
|
133
180
|
if (typeof value === 'string') {
|
|
134
181
|
return value.toLowerCase().includes(lowerTerm);
|
|
135
182
|
}
|
|
136
183
|
if (Array.isArray(value)) {
|
|
137
|
-
|
|
184
|
+
if (depth > MAX_JSON_SEARCH_DEPTH) {
|
|
185
|
+
state.truncated = true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return value.some(v => matchesInValue(v, lowerTerm, depth + 1, state));
|
|
138
189
|
}
|
|
139
190
|
if (value && typeof value === 'object') {
|
|
140
|
-
|
|
191
|
+
if (depth > MAX_JSON_SEARCH_DEPTH) {
|
|
192
|
+
state.truncated = true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return Object.values(value).some(v => matchesInValue(v, lowerTerm, depth + 1, state));
|
|
141
196
|
}
|
|
142
197
|
return false;
|
|
143
198
|
}
|
|
@@ -150,19 +205,23 @@ function intelQuery(term, planningDir) {
|
|
|
150
205
|
return disabledResponse();
|
|
151
206
|
const matches = [];
|
|
152
207
|
let total = 0;
|
|
208
|
+
let truncated = false;
|
|
209
|
+
const readErrors = [];
|
|
153
210
|
// Search all JSON intel files
|
|
154
211
|
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
|
|
155
212
|
const filePath = intelFilePath(planningDir, filename);
|
|
156
|
-
const data = safeReadJson(filePath);
|
|
213
|
+
const data = safeReadJson(filePath, readErrors);
|
|
157
214
|
if (!data)
|
|
158
215
|
continue;
|
|
159
|
-
const found = searchJsonEntries(data, term);
|
|
216
|
+
const { matches: found, truncated: fileTruncated } = searchJsonEntries(data, term);
|
|
217
|
+
if (fileTruncated)
|
|
218
|
+
truncated = true;
|
|
160
219
|
if (found.length > 0) {
|
|
161
220
|
matches.push({ source: filename, entries: found });
|
|
162
221
|
total += found.length;
|
|
163
222
|
}
|
|
164
223
|
}
|
|
165
|
-
return { matches, term, total };
|
|
224
|
+
return { matches, term, total, truncated, read_errors: readErrors };
|
|
166
225
|
}
|
|
167
226
|
/**
|
|
168
227
|
* Report status and staleness of each intel file.
|
|
@@ -179,13 +238,14 @@ function intelStatus(planningDir) {
|
|
|
179
238
|
const filePath = intelFilePath(planningDir, filename);
|
|
180
239
|
const exists = node_fs_1.default.existsSync(filePath);
|
|
181
240
|
if (!exists) {
|
|
182
|
-
files[filename] = { exists: false, updated_at: null, stale: true };
|
|
241
|
+
files[filename] = { exists: false, updated_at: null, stale: true, read_error: null };
|
|
183
242
|
overallStale = true;
|
|
184
243
|
continue;
|
|
185
244
|
}
|
|
186
245
|
let updatedAt = null;
|
|
187
246
|
// All intel files are JSON — read _meta.updated_at
|
|
188
|
-
const
|
|
247
|
+
const readErrors = [];
|
|
248
|
+
const data = safeReadJson(filePath, readErrors);
|
|
189
249
|
if (data && data._meta && data._meta.updated_at) {
|
|
190
250
|
updatedAt = data._meta.updated_at;
|
|
191
251
|
}
|
|
@@ -196,7 +256,7 @@ function intelStatus(planningDir) {
|
|
|
196
256
|
}
|
|
197
257
|
if (stale)
|
|
198
258
|
overallStale = true;
|
|
199
|
-
files[filename] = { exists: true, updated_at: updatedAt, stale };
|
|
259
|
+
files[filename] = { exists: true, updated_at: updatedAt, stale, read_error: readErrors[0] ?? null };
|
|
200
260
|
}
|
|
201
261
|
return { files, overall_stale: overallStale };
|
|
202
262
|
}
|
|
@@ -207,9 +267,15 @@ function intelDiff(planningDir) {
|
|
|
207
267
|
if (!isIntelCapabilityActive(planningDir))
|
|
208
268
|
return disabledResponse();
|
|
209
269
|
const snapshotPath = intelFilePath(planningDir, '.last-refresh.json');
|
|
210
|
-
const
|
|
270
|
+
const readErrors = [];
|
|
271
|
+
const snapshot = safeReadJson(snapshotPath, readErrors);
|
|
211
272
|
if (!snapshot) {
|
|
212
|
-
|
|
273
|
+
// #3885 (ADR-3473 §8.5): `no_baseline: true` stays true for BOTH a
|
|
274
|
+
// genuinely-never-snapshotted project (ENOENT, read_error stays null)
|
|
275
|
+
// AND a present-but-unreadable/malformed snapshot — collapsing those
|
|
276
|
+
// two into a bare `no_baseline: true` would manufacture "you've never
|
|
277
|
+
// run a refresh" out of a read failure. `read_error` names which one.
|
|
278
|
+
return { no_baseline: true, read_error: readErrors[0] ?? null };
|
|
213
279
|
}
|
|
214
280
|
const prevHashes = snapshot.hashes || {};
|
|
215
281
|
const changed = [];
|
|
@@ -374,7 +440,9 @@ function intelApiSurface(planningDir) {
|
|
|
374
440
|
const intelPath = ensureIntelDir(planningDir);
|
|
375
441
|
const apiMapPath = node_path_1.default.join(intelPath, INTEL_FILES.apis);
|
|
376
442
|
const outputPath = node_path_1.default.join(intelPath, 'API-SURFACE.md');
|
|
377
|
-
const
|
|
443
|
+
const readErrors = [];
|
|
444
|
+
const data = safeReadJson(apiMapPath, readErrors);
|
|
445
|
+
const readError = readErrors[0] ?? null;
|
|
378
446
|
const entries = (data && data.entries && typeof data.entries === 'object')
|
|
379
447
|
? Object.entries(data.entries)
|
|
380
448
|
: [];
|
|
@@ -392,7 +460,14 @@ function intelApiSurface(planningDir) {
|
|
|
392
460
|
lines.push('> Generated from `.planning/intel/api-map.json`. Do not edit by hand.');
|
|
393
461
|
lines.push('');
|
|
394
462
|
if (symbolCount === 0) {
|
|
395
|
-
|
|
463
|
+
if (readError) {
|
|
464
|
+
// #3885: a read/parse failure is NOT "not yet populated" — say so,
|
|
465
|
+
// rather than manufacturing the wrong reason for the empty surface.
|
|
466
|
+
lines.push(`> **Incomplete:** ${readError}`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
lines.push('> **Incomplete:** api-map.json has no entries (intel extraction is regex/JS-only or not yet populated).');
|
|
470
|
+
}
|
|
396
471
|
lines.push('> Treat absence here as "unknown", not "does not exist".');
|
|
397
472
|
lines.push('');
|
|
398
473
|
}
|
|
@@ -414,7 +489,7 @@ function intelApiSurface(planningDir) {
|
|
|
414
489
|
}
|
|
415
490
|
}
|
|
416
491
|
(0, shell_command_projection_cjs_1.platformWriteSync)(outputPath, lines.join('\n'));
|
|
417
|
-
return { written: outputPath, symbolCount, stale };
|
|
492
|
+
return { written: outputPath, symbolCount, stale, read_error: readError };
|
|
418
493
|
}
|
|
419
494
|
/**
|
|
420
495
|
* Patch _meta.updated_at in a JSON intel file to the current timestamp.
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -15,6 +15,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
15
15
|
const node_os_1 = __importDefault(require("node:os"));
|
|
16
16
|
const node_path_1 = __importDefault(require("node:path"));
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const cliExitModule = require("./cli-exit.cjs");
|
|
20
|
+
const { setJsonErrorMode, getJsonErrorMode, EXIT_ENVELOPE_REASON, ExitError, setPendingOutcome, projectOutcome, getContractVersion, } = cliExitModule;
|
|
18
21
|
// ─── Temp-file helpers (needed by output()) ──────────────────────────────────
|
|
19
22
|
/**
|
|
20
23
|
* Dedicated GSD temp directory: path.join(os.tmpdir(), 'gsd').
|
|
@@ -134,7 +137,48 @@ function writeAllSync(fd, data) {
|
|
|
134
137
|
function serializeForOutput(result) {
|
|
135
138
|
return JSON.stringify(result, null, 2);
|
|
136
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* A payload-carried error, per ADR-2980's own definition (#3912, ADR-3889
|
|
142
|
+
* §4): `result` is an object carrying a SERIALIZABLE `error` property, in
|
|
143
|
+
* ANY key order — `{ found: false, error }` counts exactly the same as
|
|
144
|
+
* `{ error, found: false }`. The discriminator is a serializable error
|
|
145
|
+
* value, NOT mere key presence: `result.error`'s own truthiness is
|
|
146
|
+
* irrelevant (falsy `0`/`null`/`''` all count), and neither is `result`'s
|
|
147
|
+
* prototype (a plain object literal is all any call site here ever
|
|
148
|
+
* passes) — but `error: undefined` does NOT count, because
|
|
149
|
+
* `JSON.stringify` (the exact serializer `serializeForOutput` uses to
|
|
150
|
+
* build the payload the caller actually receives) drops an object
|
|
151
|
+
* property whose value is `undefined` entirely. A payload built as
|
|
152
|
+
* `{ found: false, error: undefined }` therefore reaches the wire as
|
|
153
|
+
* `{"found":false}` — no error at all — and recording DEGRADED for it
|
|
154
|
+
* would be a false verdict: exit 80 under v2 for output the user sees as
|
|
155
|
+
* clean. `hasOwnProperty` alone is not enough to answer "does this payload
|
|
156
|
+
* declare an error"; it must also survive `JSON.stringify`.
|
|
157
|
+
*/
|
|
158
|
+
function isPayloadCarriedError(result) {
|
|
159
|
+
return typeof result === 'object' && result !== null
|
|
160
|
+
&& Object.prototype.hasOwnProperty.call(result, 'error')
|
|
161
|
+
&& result.error !== undefined;
|
|
162
|
+
}
|
|
137
163
|
function output(result, raw, rawValue) {
|
|
164
|
+
// #3912 (ADR-3889 §4): a payload-carried error declares DEGRADED into the
|
|
165
|
+
// pending-outcome cell runMain reads. This is the ONLY new thing output()
|
|
166
|
+
// does — it still just writes fd 1 and returns; the exit code stays
|
|
167
|
+
// whatever it already was under v1 (DEGRADED projects to 0), and nothing
|
|
168
|
+
// here touches process.exitCode directly.
|
|
169
|
+
//
|
|
170
|
+
// LAST-WRITE-WINS (review fix): a clean (non-error-shaped) payload CLEARS
|
|
171
|
+
// the cell rather than leaving a prior degraded declaration in place. A
|
|
172
|
+
// handler that calls output() more than once per invocation — a
|
|
173
|
+
// diagnostic error payload followed by a clean final payload — must have
|
|
174
|
+
// its LATEST declaration win, not its first: the cell reflects "is a
|
|
175
|
+
// degraded outcome pending right now", not "was one ever declared".
|
|
176
|
+
if (isPayloadCarriedError(result)) {
|
|
177
|
+
setPendingOutcome('DEGRADED');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
setPendingOutcome(undefined);
|
|
181
|
+
}
|
|
138
182
|
let data;
|
|
139
183
|
if (raw && rawValue !== undefined) {
|
|
140
184
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
@@ -178,7 +222,7 @@ const ERROR_REASON = Object.freeze({
|
|
|
178
222
|
CONFIG_PARSE_FAILED: 'config_parse_failed',
|
|
179
223
|
CONFIG_INVALID_KEY: 'config_invalid_key',
|
|
180
224
|
// SDK / gsd-tools dispatch
|
|
181
|
-
SDK_FAIL_FAST:
|
|
225
|
+
SDK_FAIL_FAST: EXIT_ENVELOPE_REASON,
|
|
182
226
|
SDK_UNKNOWN_COMMAND: 'sdk_unknown_command',
|
|
183
227
|
SDK_MISSING_ARG: 'sdk_missing_arg',
|
|
184
228
|
// workflow / phase
|
|
@@ -195,6 +239,10 @@ const ERROR_REASON = Object.freeze({
|
|
|
195
239
|
// graphify
|
|
196
240
|
GRAPHIFY_NO_GRAPH: 'graphify_no_graph',
|
|
197
241
|
GRAPHIFY_INVALID_QUERY: 'graphify_invalid_query',
|
|
242
|
+
// estimate-calibrate (#3882, ADR-3473 §8.2): the phases directory exists
|
|
243
|
+
// but could not be read — a NON-answer, distinct from a project that
|
|
244
|
+
// genuinely has zero completed phases yet.
|
|
245
|
+
ESTIMATE_PHASES_UNREADABLE: 'estimate_phases_unreadable',
|
|
198
246
|
// hooks
|
|
199
247
|
HOOKS_OPT_OUT: 'hooks_opt_out',
|
|
200
248
|
// commit-docs-guard (#3588)
|
|
@@ -203,22 +251,17 @@ const ERROR_REASON = Object.freeze({
|
|
|
203
251
|
COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
|
|
204
252
|
// security-scan
|
|
205
253
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
254
|
+
// --pick (#3365 / #3358, ADR-3473 §8.4): an absent field or non-JSON
|
|
255
|
+
// command output is a failure, never a demotion to an empty answer at
|
|
256
|
+
// exit 0. See .gsd/phase/feat-3884-failure-is-a-value/40-design.md.
|
|
257
|
+
PICK_FIELD_ABSENT: 'pick_field_absent',
|
|
258
|
+
PICK_OUTPUT_NOT_JSON: 'pick_output_not_json',
|
|
206
259
|
// generic
|
|
207
260
|
USAGE: 'usage',
|
|
208
261
|
UNKNOWN: 'unknown',
|
|
209
262
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
* instead of plain "Error: <message>" text. Set by gsd-tools.cjs when the
|
|
213
|
-
* CLI is invoked with `--json-errors`. Tests opt in to typed-IR error
|
|
214
|
-
* assertions by passing that flag and parsing the JSON.
|
|
215
|
-
*
|
|
216
|
-
* Default off so existing callers and human operators keep their plain-text
|
|
217
|
-
* diagnostics. The structured form is opt-in for tooling and tests (#2974).
|
|
218
|
-
*/
|
|
219
|
-
let _jsonErrorMode = false;
|
|
220
|
-
function setJsonErrorMode(v) { _jsonErrorMode = !!v; }
|
|
221
|
-
function getJsonErrorMode() { return _jsonErrorMode; }
|
|
263
|
+
// setJsonErrorMode / getJsonErrorMode now live in cli-exit.cts (imported above)
|
|
264
|
+
// and are re-exported here for the callers that already import them from io.
|
|
222
265
|
/**
|
|
223
266
|
* Emit an error and exit. When the second argument is provided it must be
|
|
224
267
|
* a value from ERROR_REASON; tests can assert on `result.reason`. When the
|
|
@@ -233,15 +276,116 @@ function getJsonErrorMode() { return _jsonErrorMode; }
|
|
|
233
276
|
* human-readable text. Ignored entirely in plain-text mode — the human
|
|
234
277
|
* message is the only thing an operator sees there.
|
|
235
278
|
*/
|
|
279
|
+
/**
|
|
280
|
+
* Render an UNTRUSTED string for embedding inside a human-readable,
|
|
281
|
+
* plain-text diagnostic (the `'Error: ' + message` line `error()` writes in
|
|
282
|
+
* non-JSON mode).
|
|
283
|
+
*
|
|
284
|
+
* WHY THIS EXISTS AND WHY `error()` DOES NOT DO IT ITSELF: `error()`
|
|
285
|
+
* deliberately writes its `message` argument verbatim — several callers in
|
|
286
|
+
* this repo intentionally emit multi-line diagnostics (e.g. the phase-gate
|
|
287
|
+
* messages), and `error()` has no way to distinguish a legitimate multi-line
|
|
288
|
+
* message from a hostile one, so it must not mangle newlines generically.
|
|
289
|
+
* That means any UNTRUSTED substring a caller interpolates into `message`
|
|
290
|
+
* (an argv token, a JSON key/value read back from a command's own output,
|
|
291
|
+
* etc.) can smuggle its own `\n` and forge a second `Error: ` line on
|
|
292
|
+
* stderr — a caller that parses stderr line-by-line would then see a second,
|
|
293
|
+
* attacker-authored error. Every call site that interpolates untrusted data
|
|
294
|
+
* into a diagnostic MUST pass that substring through this function first;
|
|
295
|
+
* `error()` itself stays a dumb, faithful writer.
|
|
296
|
+
*
|
|
297
|
+
* `JSON.stringify` is the primitive: it wraps the value in quotes and
|
|
298
|
+
* escapes control characters (`\n`, `\r`, `\t`, and the rest of the C0
|
|
299
|
+
* range, plus the quote character itself), so the result can never span
|
|
300
|
+
* more than one line or introduce an unescaped `"`. Callers embedding the
|
|
301
|
+
* result MUST NOT add their own surrounding quotes — that would
|
|
302
|
+
* double-quote it.
|
|
303
|
+
*/
|
|
304
|
+
function formatDiagnosticToken(value) {
|
|
305
|
+
return JSON.stringify(value);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Map an ERROR_REASON wire value onto a declared outcome name (#3912,
|
|
309
|
+
* ADR-3889 §4). Closed over the 25-member enum: every reason gets an
|
|
310
|
+
* explicit entry below, so a 26th member added without a mapping falls
|
|
311
|
+
* through to the `?? 'FAIL'` default rather than silently mis-projecting —
|
|
312
|
+
* and tests/A1 iterates `Object.values(ERROR_REASON)`, so that default is
|
|
313
|
+
* exactly what makes an unmapped addition visible instead of invisible.
|
|
314
|
+
*
|
|
315
|
+
* This function's result is ONLY consulted under v2 (see `error()` below) —
|
|
316
|
+
* it is deliberately never routed through `projectOutcome` under v1, which
|
|
317
|
+
* is what keeps the v1 pin intact (`projectOutcome` treats registered names
|
|
318
|
+
* as version-invariant, so e.g. USAGE would otherwise become 64 today).
|
|
319
|
+
*
|
|
320
|
+
* Each non-FAIL choice below is justified inline; `UNKNOWN` and anything
|
|
321
|
+
* with no clearly better fit stays `FAIL` — the honest default the design
|
|
322
|
+
* calls for, not a guess dressed up as a specific outcome.
|
|
323
|
+
*/
|
|
324
|
+
const REASON_TO_OUTCOME = Object.freeze({
|
|
325
|
+
// Bad argv/subcommand/argument — the caller, not the run, is at fault.
|
|
326
|
+
[ERROR_REASON.CONFIG_INVALID_KEY]: 'USAGE',
|
|
327
|
+
[ERROR_REASON.SDK_UNKNOWN_COMMAND]: 'USAGE',
|
|
328
|
+
[ERROR_REASON.SDK_MISSING_ARG]: 'USAGE',
|
|
329
|
+
[ERROR_REASON.GRAPHIFY_INVALID_QUERY]: 'USAGE',
|
|
330
|
+
[ERROR_REASON.USAGE]: 'USAGE',
|
|
331
|
+
// A specific, named thing does not exist / nothing was there to find —
|
|
332
|
+
// genuine, known emptiness rather than a broken prerequisite.
|
|
333
|
+
[ERROR_REASON.CONFIG_KEY_NOT_FOUND]: 'NO_INPUT',
|
|
334
|
+
[ERROR_REASON.SUMMARY_NO_PLANNING]: 'NO_INPUT',
|
|
335
|
+
[ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE]: 'NO_INPUT',
|
|
336
|
+
// A prerequisite is absent, unreadable, or otherwise not in a state the
|
|
337
|
+
// run could proceed from — distinct from NO_INPUT's genuine emptiness.
|
|
338
|
+
[ERROR_REASON.CONFIG_NO_FILE]: 'UNAVAILABLE',
|
|
339
|
+
[ERROR_REASON.CONFIG_PARSE_FAILED]: 'UNAVAILABLE',
|
|
340
|
+
[ERROR_REASON.PHASE_NOT_FOUND]: 'UNAVAILABLE',
|
|
341
|
+
[ERROR_REASON.PHASE_VERIFICATION_INCOMPLETE]: 'UNAVAILABLE',
|
|
342
|
+
[ERROR_REASON.PHASE_PLAN_COVERAGE_INCOMPLETE]: 'UNAVAILABLE',
|
|
343
|
+
// Its own docstring: "a marker exists but didn't resolve" — a broken
|
|
344
|
+
// prerequisite, not the "no marker anywhere" emptiness NONE_ACTIVE covers.
|
|
345
|
+
[ERROR_REASON.WORKSTREAM_MODE_MARKER_UNRESOLVED]: 'UNAVAILABLE',
|
|
346
|
+
[ERROR_REASON.GRAPHIFY_NO_GRAPH]: 'UNAVAILABLE',
|
|
347
|
+
// Its own docstring: "a NON-answer, distinct from a project that
|
|
348
|
+
// genuinely has zero completed phases yet" — UNAVAILABLE, not NO_INPUT.
|
|
349
|
+
[ERROR_REASON.ESTIMATE_PHASES_UNREADABLE]: 'UNAVAILABLE',
|
|
350
|
+
[ERROR_REASON.COMMIT_DOCS_GUARD_NOT_A_REPO]: 'UNAVAILABLE',
|
|
351
|
+
[ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK]: 'UNAVAILABLE',
|
|
352
|
+
[ERROR_REASON.COMMIT_DOCS_GUARD_HOOKS_PATH_SET]: 'UNAVAILABLE',
|
|
353
|
+
// Its own docstring: "an absent field or non-JSON command output is a
|
|
354
|
+
// failure, never a demotion to an empty answer" — the field/output was
|
|
355
|
+
// supposed to be there and was not; a prerequisite of the query failed.
|
|
356
|
+
[ERROR_REASON.PICK_FIELD_ABSENT]: 'UNAVAILABLE',
|
|
357
|
+
[ERROR_REASON.PICK_OUTPUT_NOT_JSON]: 'UNAVAILABLE',
|
|
358
|
+
// Self-failure: the run itself broke, not its inputs.
|
|
359
|
+
[ERROR_REASON.SDK_FAIL_FAST]: 'INTERNAL',
|
|
360
|
+
[ERROR_REASON.SECURITY_SCAN_FAILED]: 'INTERNAL',
|
|
361
|
+
// No clearly better fit — the honest default, per design.
|
|
362
|
+
[ERROR_REASON.HOOKS_OPT_OUT]: 'FAIL',
|
|
363
|
+
[ERROR_REASON.UNKNOWN]: 'FAIL',
|
|
364
|
+
});
|
|
365
|
+
function outcomeForReason(reason) {
|
|
366
|
+
return REASON_TO_OUTCOME[reason] ?? 'FAIL';
|
|
367
|
+
}
|
|
236
368
|
function error(message, reason = ERROR_REASON.UNKNOWN, extra) {
|
|
237
|
-
if (
|
|
369
|
+
if (getJsonErrorMode()) {
|
|
238
370
|
const payload = JSON.stringify({ ok: false, reason, message, ...(extra || {}) }) + '\n';
|
|
239
371
|
writeAllSync(2, payload);
|
|
240
372
|
}
|
|
241
373
|
else {
|
|
242
374
|
writeAllSync(2, 'Error: ' + message + '\n');
|
|
243
375
|
}
|
|
244
|
-
|
|
376
|
+
// #3912 (ADR-3889 §4): the declaration is version-gated HERE, not inside
|
|
377
|
+
// projectOutcome — registered names are version-invariant there, so
|
|
378
|
+
// routing every reason through it unconditionally would change v1 exit
|
|
379
|
+
// codes today (e.g. USAGE -> 64) and break the pin. Under v1 the exit
|
|
380
|
+
// stays ExitError(1) unconditionally, byte-identical to every prior
|
|
381
|
+
// release; only v2 projects the declared outcome through the registry.
|
|
382
|
+
if (getContractVersion() === 'v2') {
|
|
383
|
+
throw new ExitError(projectOutcome(outcomeForReason(reason), 'v2'));
|
|
384
|
+
}
|
|
385
|
+
// No message passed to ExitError: the stderr write above is already done,
|
|
386
|
+
// byte-identical to the prior process.exit(1) behavior, and ExitError with
|
|
387
|
+
// no message means runMain's catch adds nothing further to stderr.
|
|
388
|
+
throw new ExitError(1);
|
|
245
389
|
}
|
|
246
390
|
module.exports = {
|
|
247
391
|
GSD_TEMP_DIR,
|
|
@@ -253,4 +397,5 @@ module.exports = {
|
|
|
253
397
|
setJsonErrorMode,
|
|
254
398
|
getJsonErrorMode,
|
|
255
399
|
error,
|
|
400
|
+
formatDiagnosticToken,
|
|
256
401
|
};
|
|
@@ -21,6 +21,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
23
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
+
const phaseLocator = require("./phase-locator.cjs");
|
|
24
26
|
const node_os_1 = __importDefault(require("node:os"));
|
|
25
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
28
|
const ioMod = require("./io.cjs");
|
|
@@ -145,9 +147,55 @@ function learningsDelete(id, opts) {
|
|
|
145
147
|
node_fs_1.default.unlinkSync(filePath);
|
|
146
148
|
return true;
|
|
147
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* #3683 — resolve which learnings artifact a copy reads. The extractor writes
|
|
152
|
+
* phase-scoped `{PHASE_DIR}/{PADDED}-LEARNINGS.md`; the legacy shape is a
|
|
153
|
+
* project-root `LEARNINGS.md`. The MOST RECENT phase-scoped artifact wins (a
|
|
154
|
+
* gated completion copies the phase it just wrote); the project-root file is
|
|
155
|
+
* the fallback when no phase artifact exists. Returns null when neither shape
|
|
156
|
+
* is present.
|
|
157
|
+
*/
|
|
158
|
+
function resolveLearningsSource(planningDir) {
|
|
159
|
+
let best = null;
|
|
160
|
+
const phasesDir = node_path_1.default.join(planningDir, 'phases');
|
|
161
|
+
// Phase enumeration routes through the sanctioned seam (the #3185 drift
|
|
162
|
+
// guard rejects ad-hoc re-derivations): no cwd → every phase dir in the
|
|
163
|
+
// tree, sentinel dirs excluded, unreadable phases dir already degraded to
|
|
164
|
+
// an empty list by the locator itself.
|
|
165
|
+
const { value: phaseDirNames } = phaseLocator.listMilestonePhaseDirs(phasesDir);
|
|
166
|
+
for (const entry of phaseDirNames) {
|
|
167
|
+
const phaseDir = node_path_1.default.join(phasesDir, entry);
|
|
168
|
+
let files;
|
|
169
|
+
try {
|
|
170
|
+
files = node_fs_1.default.readdirSync(phaseDir);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// An unreadable phase dir (ACL, removal race) must not crash the copy —
|
|
174
|
+
// skip it and keep scanning.
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const f of files) {
|
|
178
|
+
if (!/-LEARNINGS\.md$/i.test(f))
|
|
179
|
+
continue;
|
|
180
|
+
const p = node_path_1.default.join(phaseDir, f);
|
|
181
|
+
try {
|
|
182
|
+
const m = node_fs_1.default.statSync(p).mtimeMs;
|
|
183
|
+
if (best === null || m > best.m)
|
|
184
|
+
best = { p, m };
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (best !== null)
|
|
192
|
+
return best.p;
|
|
193
|
+
const rootPath = node_path_1.default.join(planningDir, 'LEARNINGS.md');
|
|
194
|
+
return node_fs_1.default.existsSync(rootPath) ? rootPath : null;
|
|
195
|
+
}
|
|
148
196
|
function learningsCopyFromProject(planningDir, opts) {
|
|
149
|
-
const learningsPath =
|
|
150
|
-
if (
|
|
197
|
+
const learningsPath = resolveLearningsSource(planningDir);
|
|
198
|
+
if (learningsPath === null) {
|
|
151
199
|
return { total: 0, created: 0, skipped: 0 };
|
|
152
200
|
}
|
|
153
201
|
const content = node_fs_1.default.readFileSync(learningsPath, 'utf-8');
|
|
@@ -167,29 +215,52 @@ function learningsCopyFromProject(planningDir, opts) {
|
|
|
167
215
|
dedupeIndex.set(existing.content_hash, existing.id);
|
|
168
216
|
}
|
|
169
217
|
}
|
|
170
|
-
// Parse markdown: split on ## headings
|
|
218
|
+
// Parse markdown: split on ## category headings.
|
|
219
|
+
// #3683: the real producer (extract-learnings.md write_learnings) writes ##
|
|
220
|
+
// categories containing ### item headings — each item is ONE learning (the
|
|
221
|
+
// store's relevance contract caps injection by count, so category-sized
|
|
222
|
+
// blobs defeat it). A bare ## section with no ### items keeps the legacy
|
|
223
|
+
// single-learning shape.
|
|
171
224
|
const sections = content.split(/^## /m).slice(1); // skip preamble before first ##
|
|
172
225
|
let created = 0;
|
|
173
226
|
let skipped = 0;
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
continue;
|
|
180
|
-
// Extract tags from title (simple: use words as tags)
|
|
181
|
-
const tags = title.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
227
|
+
const writeOne = (title, body, extraTags) => {
|
|
228
|
+
const tags = Array.from(new Set([
|
|
229
|
+
...extraTags,
|
|
230
|
+
...title.toLowerCase().split(/\s+/).filter(w => w.length > 2),
|
|
231
|
+
]));
|
|
182
232
|
const result = learningsWrite({
|
|
183
233
|
source_project: sourceProject,
|
|
184
234
|
learning: body,
|
|
185
235
|
context: title,
|
|
186
236
|
tags,
|
|
187
237
|
}, { ...opts, dedupeIndex });
|
|
188
|
-
if (result.created)
|
|
238
|
+
if (result.created)
|
|
189
239
|
created++;
|
|
190
|
-
|
|
191
|
-
else {
|
|
240
|
+
else
|
|
192
241
|
skipped++;
|
|
242
|
+
};
|
|
243
|
+
for (const section of sections) {
|
|
244
|
+
const lines = section.trim().split('\n');
|
|
245
|
+
const category = lines[0].trim();
|
|
246
|
+
const body = lines.slice(1).join('\n').trim();
|
|
247
|
+
if (!body)
|
|
248
|
+
continue;
|
|
249
|
+
const categoryTag = category.toLowerCase().split(/\s+/)[0] || '';
|
|
250
|
+
// Split into ### items when present; otherwise one learning per section.
|
|
251
|
+
const items = body.split(/^### /m).map(s => s.trim()).filter(s => s.length > 0);
|
|
252
|
+
const hasItemHeadings = /^### /m.test(body);
|
|
253
|
+
if (!hasItemHeadings) {
|
|
254
|
+
writeOne(category, body, categoryTag ? [categoryTag] : []);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
for (const item of items) {
|
|
258
|
+
const itemLines = item.split('\n');
|
|
259
|
+
const title = itemLines[0].trim();
|
|
260
|
+
const itemBody = itemLines.slice(1).join('\n').trim();
|
|
261
|
+
if (!itemBody && !title)
|
|
262
|
+
continue;
|
|
263
|
+
writeOne(title, itemBody || title, categoryTag ? [categoryTag] : []);
|
|
193
264
|
}
|
|
194
265
|
}
|
|
195
266
|
return { total: created + skipped, created, skipped };
|
|
@@ -262,8 +262,14 @@ function planLegacyCleanup(configDirs, opts = {}) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// Legacy shared cache (fixed name from the old package)
|
|
266
|
-
|
|
265
|
+
// Legacy shared cache (fixed name from the old package). #3799: under an
|
|
266
|
+
// explicit configDirs override the cache lives under the SCOPE root(s), not
|
|
267
|
+
// the default home — the override means "clean only what this redirected
|
|
268
|
+
// install owns", and the default home's cache belongs to the live install.
|
|
269
|
+
const cacheRoot = (Array.isArray(opts.configDirs) && opts.configDirs.length > 0)
|
|
270
|
+
? opts.configDirs[0]
|
|
271
|
+
: homeDir;
|
|
272
|
+
const legacyCachePath = path.join(cacheRoot, '.cache', 'gsd', 'gsd-update-check.json');
|
|
267
273
|
try {
|
|
268
274
|
const stat = fsMod.statSync(legacyCachePath);
|
|
269
275
|
if (stat.isFile()) {
|