@opengsd/gsd-core 1.10.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 +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- 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-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- 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-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- 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 +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- 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/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -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 +268 -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 +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- 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-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 +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -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-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- 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-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -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 +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- 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/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -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-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -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 +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- 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 +44 -26
- 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 +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- 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 +17 -11
- 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 +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- 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-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -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 +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.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 +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- 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/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -1
- 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 +13 -20
- 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 +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- 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 +4 -3
- 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 +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- 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 +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -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 +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- 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 +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -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 +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- 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 +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- 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 +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- 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 +53 -13
- 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 +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- 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 +2 -3
- 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 +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- 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 +162 -46
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- 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 +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- 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 +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- 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 +162 -46
- 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-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -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 +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/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 +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- 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-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- 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/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-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 +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -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 +471 -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 +488 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- 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/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -210,10 +210,35 @@ const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/;
|
|
|
210
210
|
// hard validation error — fail closed so the capability install/load is rejected loudly.
|
|
211
211
|
const SAFE_HOOK_SCRIPT_RE = /^[A-Za-z0-9._/-]+$/;
|
|
212
212
|
|
|
213
|
+
// #3631 (defense-in-depth, mirrors capability-lifecycle.cts — KEEP BOTH IN SYNC): a declared script
|
|
214
|
+
// path must not point into the space bundleContentHash (capability-consent.cts) excludes from the
|
|
215
|
+
// consent-binding digest. A file whose basename ends `.pyc`/`.pyo` can contain perfectly valid
|
|
216
|
+
// JavaScript and would be executed by `node` regardless of extension, and a `__pycache__`/
|
|
217
|
+
// `.pytest_cache` segment marks a directory whose digest marker is suppressed — so a MANIFEST-DECLARED
|
|
218
|
+
// executable surface must never be able to reach either, or the exclusion becomes reachable from a
|
|
219
|
+
// path an attacker fully controls at declare-time rather than only via post-consent tamper.
|
|
220
|
+
// Regex asymmetry is DELIBERATE, KEEP BOTH RULES IN SYNC WITH capability-lifecycle.cts (byte-identical
|
|
221
|
+
// text, verified by the isSafeHookScriptPath parity test in tests/capability-registry.test.cjs):
|
|
222
|
+
// (i) PYCACHE_SUFFIX_RE is case-INSENSITIVE (`/i`) on purpose — a validator should be STRICTER than
|
|
223
|
+
// the digest it defends, so it rejects `x.PYC` too even though bundleContentHash's own suffix
|
|
224
|
+
// match (hasPycacheFileSuffix, capability-consent.cts) is byte-exact and would still hash it.
|
|
225
|
+
// (ii) The __pycache__/.pytest_cache SEGMENT match is case-SENSITIVE to match the digest's own
|
|
226
|
+
// byte-exact, case-sensitive directory-basename comparison (CPython always writes a lowercase
|
|
227
|
+
// `__pycache__`) — a validator segment match looser than the digest here would reject paths the
|
|
228
|
+
// digest would still hash, which is over-strict in the wrong direction for a defense-in-depth
|
|
229
|
+
// check layered on top of an already-correct digest.
|
|
230
|
+
// (iii) The `[/\\]` backslash alternations in both regexes are defensive/UNREACHABLE in practice:
|
|
231
|
+
// SAFE_HOOK_SCRIPT_RE (above) already rejects any backslash character outright, so a script
|
|
232
|
+
// string containing `\` never reaches either PYCACHE_*_RE check.
|
|
233
|
+
const PYCACHE_SEGMENT_RE = /(?:^|[/\\])(__pycache__|\.pytest_cache)(?:[/\\]|$)/;
|
|
234
|
+
const PYCACHE_SUFFIX_RE = /\.(pyc|pyo)$/i;
|
|
235
|
+
|
|
213
236
|
/**
|
|
214
237
|
* #1460 (R): true when a relative hook-script path is shell-safe (see SAFE_HOOK_SCRIPT_RE).
|
|
215
238
|
* Rejects absolute paths, `..` segments, a leading `-` on any path segment, and any char
|
|
216
|
-
* outside the allowlist (whitespace / shell metacharacters / control / NUL).
|
|
239
|
+
* outside the allowlist (whitespace / shell metacharacters / control / NUL). #3631: also rejects a
|
|
240
|
+
* path with a `__pycache__`/`.pytest_cache` segment or a `.pyc`/`.pyo` basename suffix (defense in
|
|
241
|
+
* depth — keeps a declared executable surface out of the digest-excluded space).
|
|
217
242
|
*/
|
|
218
243
|
function isSafeHookScriptPath(script) {
|
|
219
244
|
if (typeof script !== 'string' || script.length === 0) return false;
|
|
@@ -225,6 +250,8 @@ function isSafeHookScriptPath(script) {
|
|
|
225
250
|
for (const seg of segments) {
|
|
226
251
|
if (seg.startsWith('-')) return false;
|
|
227
252
|
}
|
|
253
|
+
if (PYCACHE_SEGMENT_RE.test(script)) return false;
|
|
254
|
+
if (PYCACHE_SUFFIX_RE.test(path.basename(script))) return false;
|
|
228
255
|
return true;
|
|
229
256
|
}
|
|
230
257
|
|
|
@@ -369,6 +396,11 @@ function validateCapability(cap, folderId) {
|
|
|
369
396
|
errors.push(...validateRuntimeBody(cap));
|
|
370
397
|
// A host that is ALSO a reviewer keeps exactly one manifest (ADR-2782 D1).
|
|
371
398
|
errors.push(...validateReviewerBody(cap));
|
|
399
|
+
// ADR-3646: a runtime capability installs a host CLI, it does not resolve
|
|
400
|
+
// task content — taskContentResolver is feature-only.
|
|
401
|
+
if (cap.taskContentResolver !== undefined) {
|
|
402
|
+
errors.push('role:runtime capability must not have a "taskContentResolver" body (feature-only field)');
|
|
403
|
+
}
|
|
372
404
|
} else if (cap.role === 'reviewer') {
|
|
373
405
|
// ADR-2782 D3 — a lane that is not an install target. No runtime body, no
|
|
374
406
|
// install surface, no runtimeCompat (it surfaces through no host runtime).
|
|
@@ -691,6 +723,118 @@ function validateFeatureBody(cap) {
|
|
|
691
723
|
}
|
|
692
724
|
}
|
|
693
725
|
|
|
726
|
+
// ADR-3646: optional per-task external-tracker content-resolution seam.
|
|
727
|
+
errors.push(...validateTaskContentResolver(cap));
|
|
728
|
+
|
|
729
|
+
return errors;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* ADR-3646 — validate an OPTIONAL `taskContentResolver` body on a `role:
|
|
734
|
+
* "feature"` capability. Absence is never an error (most manifests won't
|
|
735
|
+
* have one); presence is strictly validated.
|
|
736
|
+
*
|
|
737
|
+
* Wrapped in try/catch to degrade any unexpected throw (a hostile Proxy, a
|
|
738
|
+
* throwing getter, etc.) to a single validation error rather than crashing
|
|
739
|
+
* every consumer of loadRegistry, per the #1461 OVL-1 discipline that
|
|
740
|
+
* `validateReviewerBody` follows.
|
|
741
|
+
*
|
|
742
|
+
* @param {object} cap The parsed capability manifest.
|
|
743
|
+
* @returns {string[]} Array of error strings; empty = valid or absent.
|
|
744
|
+
*/
|
|
745
|
+
function validateTaskContentResolver(cap) {
|
|
746
|
+
try {
|
|
747
|
+
return validateTaskContentResolverFields(cap);
|
|
748
|
+
} catch (err) {
|
|
749
|
+
return ['capability taskContentResolver body could not be validated: ' + safeErrorMessage(err)];
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Upper bound for `taskContentResolver.invoke.timeoutMs`, specific to this
|
|
755
|
+
* field only. `isPositiveIntegerMs()` has no ceiling and stays that way — it
|
|
756
|
+
* is shared with the reviewer lane's `timeoutFloorMs` and probe `timeoutMs`,
|
|
757
|
+
* which may legitimately need a longer or unbounded value. Without a ceiling
|
|
758
|
+
* here, a manifest could declare `Number.MAX_SAFE_INTEGER` and let
|
|
759
|
+
* `resolve-content` hang near-indefinitely on a stuck/malicious resolver,
|
|
760
|
+
* defeating the feature's "bounded subprocess" design intent.
|
|
761
|
+
*/
|
|
762
|
+
const TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS = 120000;
|
|
763
|
+
|
|
764
|
+
function validateTaskContentResolverFields(cap) {
|
|
765
|
+
const errors = [];
|
|
766
|
+
if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return errors;
|
|
767
|
+
|
|
768
|
+
const tcr = cap.taskContentResolver;
|
|
769
|
+
if (tcr === undefined) return errors; // optional — never an error to omit
|
|
770
|
+
|
|
771
|
+
const ctx = 'capability "' + (typeof cap.id === 'string' ? cap.id : '(unknown)') + '"';
|
|
772
|
+
|
|
773
|
+
if (typeof tcr !== 'object' || tcr === null || Array.isArray(tcr)) {
|
|
774
|
+
const got = tcr === null ? 'null' : Array.isArray(tcr) ? 'array' : typeof tcr;
|
|
775
|
+
errors.push(
|
|
776
|
+
ctx + ' taskContentResolver must be an object (got: ' + got + '). ' +
|
|
777
|
+
'Omit the key entirely to declare no resolver — an explicit null is not an omission.',
|
|
778
|
+
);
|
|
779
|
+
return errors; // cannot validate fields of a non-object
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── trackerPrefix — same grammar as a capability id ─────────────────────
|
|
783
|
+
if (typeof tcr.trackerPrefix !== 'string' || tcr.trackerPrefix.length === 0 || !KEBAB_RE.test(tcr.trackerPrefix)) {
|
|
784
|
+
errors.push(
|
|
785
|
+
ctx + ' taskContentResolver.trackerPrefix must be a non-empty kebab-case string matching ' +
|
|
786
|
+
String(KEBAB_RE) + ' (got: ' + describeValue(tcr.trackerPrefix) + ')',
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ── invoke ────────────────────────────────────────────────────────────
|
|
791
|
+
const inv = tcr.invoke;
|
|
792
|
+
if (typeof inv !== 'object' || inv === null || Array.isArray(inv)) {
|
|
793
|
+
const got = inv === null ? 'null' : Array.isArray(inv) ? 'array' : typeof inv;
|
|
794
|
+
errors.push(ctx + ' taskContentResolver.invoke must be an object (got: ' + got + ')');
|
|
795
|
+
return errors; // cannot validate sub-fields of a non-object
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (typeof inv.binary !== 'string' || inv.binary.length === 0) {
|
|
799
|
+
errors.push(ctx + ' taskContentResolver.invoke.binary must be a non-empty string');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (!Array.isArray(inv.args)) {
|
|
803
|
+
errors.push(ctx + ' taskContentResolver.invoke.args must be an array of strings');
|
|
804
|
+
} else {
|
|
805
|
+
let hasPlaceholder = false;
|
|
806
|
+
for (const a of inv.args) {
|
|
807
|
+
if (typeof a !== 'string') {
|
|
808
|
+
errors.push(ctx + ' taskContentResolver.invoke.args entries must be strings (got: ' + describeValue(a) + ')');
|
|
809
|
+
} else if (a === '{{id}}') {
|
|
810
|
+
hasPlaceholder = true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (!hasPlaceholder) {
|
|
814
|
+
errors.push(
|
|
815
|
+
ctx + ' taskContentResolver.invoke.args must contain a "{{id}}" placeholder — ' +
|
|
816
|
+
'without it the resolved tracker id could never reach the resolver subprocess',
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (!isPositiveIntegerMs(inv.timeoutMs)) {
|
|
822
|
+
errors.push(
|
|
823
|
+
ctx + ' taskContentResolver.invoke.timeoutMs must be a positive integer of milliseconds — ' +
|
|
824
|
+
'an unbounded resolver call could hang task execution indefinitely ' +
|
|
825
|
+
'(got: ' + describeValue(inv.timeoutMs) + ')',
|
|
826
|
+
);
|
|
827
|
+
} else if (inv.timeoutMs > TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS) {
|
|
828
|
+
// Ceiling specific to this field — `isPositiveIntegerMs()` itself stays
|
|
829
|
+
// unbounded because it is shared with the reviewer lane's
|
|
830
|
+
// `timeoutFloorMs`/probe `timeoutMs`, which have no such ceiling.
|
|
831
|
+
errors.push(
|
|
832
|
+
ctx + ' taskContentResolver.invoke.timeoutMs must not exceed ' +
|
|
833
|
+
TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS + 'ms — an unbounded-in-practice value defeats the ' +
|
|
834
|
+
'"bounded subprocess" design intent (got: ' + inv.timeoutMs + ')',
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
694
838
|
return errors;
|
|
695
839
|
}
|
|
696
840
|
|
|
@@ -727,6 +871,16 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
727
871
|
'convertClaudeAgentToCodexAgent',
|
|
728
872
|
// ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
|
|
729
873
|
'convertClaudeAgentToQwenAgent',
|
|
874
|
+
// #3384 — ZCode agents are Claude-shaped but its dispatcher treats mcp__* tools
|
|
875
|
+
// grants as required MCP servers; this converter strips them at install time.
|
|
876
|
+
'convertClaudeAgentToZcodeAgent',
|
|
877
|
+
// #2875 Part 2 (the agents-bypass closure) — data-driven Hermes branding
|
|
878
|
+
// converter (reads hostBehaviors.brandingRewrites rather than a hardcode),
|
|
879
|
+
// and the kilo/opencode agent converters (shared with those runtimes'
|
|
880
|
+
// commands-kind entries, options-bag signature `(content, {isAgent, modelOverride})`).
|
|
881
|
+
'convertClaudeAgentToHermesAgent',
|
|
882
|
+
'convertClaudeToKiloFrontmatter',
|
|
883
|
+
'convertClaudeToOpencodeFrontmatter',
|
|
730
884
|
]);
|
|
731
885
|
|
|
732
886
|
// C3: Validate role:runtime body
|
|
@@ -745,6 +899,23 @@ const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'non
|
|
|
745
899
|
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
|
|
746
900
|
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
|
|
747
901
|
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
|
|
902
|
+
// #2871 Phase 2 — only `commands` and `skills` are trigger-bearing (a `/gsd-<name>`
|
|
903
|
+
// the USER types). `agents` and `kimi-agents` are a separate dispatch interface
|
|
904
|
+
// point (subagent invocation via `subagent_type`/named dispatch), never a trigger —
|
|
905
|
+
// see 40-design.md's "agents are not trigger-bearing" correction. A narrower set
|
|
906
|
+
// than VALID_ARTIFACT_KIND_NAMES on purpose: an artifact KIND can be agents; a
|
|
907
|
+
// trigger-precedence MEMBER never can.
|
|
908
|
+
const VALID_TRIGGER_PRECEDENCE_KINDS = new Set(['commands', 'skills']);
|
|
909
|
+
// The default runtime.triggerPrecedence (highest priority first) applied when a
|
|
910
|
+
// descriptor omits the axis (see validateRuntimeBody's required-with-default
|
|
911
|
+
// handling below). Not invented: `['skills', 'commands']` is the ordering every
|
|
912
|
+
// in-tree runtime that emits both kinds (from the same trigger stems) wants —
|
|
913
|
+
// claude's local/global collision and the same-scope collision (codebuddy, kilo,
|
|
914
|
+
// opencode, zcode) all resolve to skills winning. claude's shipped descriptor
|
|
915
|
+
// declares this SAME value explicitly, and
|
|
916
|
+
// tests/runtime-artifact-layout-trigger-surface.test.cjs asserts the two agree
|
|
917
|
+
// (a parity assertion — two surfaces reading one rule must not silently drift).
|
|
918
|
+
const DEFAULT_TRIGGER_PRECEDENCE = Object.freeze(['skills', 'commands']);
|
|
748
919
|
const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
|
|
749
920
|
// 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) that
|
|
750
921
|
// are never CLI-installed (no allRuntimes membership, no install flag).
|
|
@@ -851,7 +1022,36 @@ const VALID_LANE_HANDLERS = new Set(['antigravity', 'openai-compatible', 'op
|
|
|
851
1022
|
// BOTH sub-shapes, or from NEITHER, has undefined meaning and fails validation.
|
|
852
1023
|
// The discriminator is explicit rather than inferred from field presence, which
|
|
853
1024
|
// is precisely the ambiguity these two sets exist to detect.
|
|
854
|
-
|
|
1025
|
+
// `env` added by #2483. It belongs in THIS set and not merely in validateSpawnInvoke: an environment
|
|
1026
|
+
// pair has no meaning for a transport that issues an HTTP POST, so a manifest declaring it alongside
|
|
1027
|
+
// `openai-http` fields is the undefined-meaning case the comment above describes. Registering it here
|
|
1028
|
+
// is what makes that case reportable — the openai-http arm rejects exactly the members of this list,
|
|
1029
|
+
// so a field absent from it is silently accepted on the wrong transport. Note `effortChannel` is
|
|
1030
|
+
// deliberately in NEITHER set: D2 defines it for both transports, so it is shared, not spawn-only.
|
|
1031
|
+
const SPAWN_ONLY_INVOKE_FIELDS = ['binary', 'args', 'promptChannel', 'outputChannel', 'outputArg', 'modelArg', 'env'];
|
|
1032
|
+
// Environment names refused outright on a reviewer lane (#2483): each one turns a declared pair into
|
|
1033
|
+
// arbitrary code execution in the spawned child. DEFENCE IN DEPTH, NOT THE BOUNDARY — say so plainly,
|
|
1034
|
+
// because a future reader who mistakes this for the control will under-invest in the one that is.
|
|
1035
|
+
// The boundary is install-time consent: `capability-trust` discloses every declared env pair, shows
|
|
1036
|
+
// its value, and binds it to the consent signature, so an unlisted name is still SEEN before it runs.
|
|
1037
|
+
//
|
|
1038
|
+
// Deliberately incomplete, and it cannot be otherwise: the spawned binary is arbitrary third-party
|
|
1039
|
+
// code, so the exhaustive set is every interpreter's injection variables. What this list buys is that
|
|
1040
|
+
// the highest-confidence, lowest-legitimacy routes cannot be taken quietly. `PATH` is included — it is
|
|
1041
|
+
// the most complete primitive of the set (repoint it at a directory holding a fake binary) and no
|
|
1042
|
+
// shipped reviewer manifest declares it; a lane that needs a specific executable declares an absolute
|
|
1043
|
+
// `invoke.binary` rather than reshaping the child's `PATH`.
|
|
1044
|
+
//
|
|
1045
|
+
// Matched CASE-INSENSITIVELY (members stored uppercase) — Windows environment lookup is
|
|
1046
|
+
// case-insensitive, so an exact-case set is bypassed by declaring `Path` or `node_options`.
|
|
1047
|
+
const DENIED_LANE_ENV_KEYS = new Set([
|
|
1048
|
+
'PATH', 'NODE_OPTIONS', 'NODE_REPL_EXTERNAL_MODULE', 'NODE_PATH',
|
|
1049
|
+
'LD_PRELOAD', 'LD_AUDIT', 'LD_LIBRARY_PATH',
|
|
1050
|
+
'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH',
|
|
1051
|
+
'PYTHONSTARTUP', 'PYTHONPATH', 'BASH_ENV', 'ENV',
|
|
1052
|
+
'PERL5OPT', 'RUBYOPT', 'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'CLASSPATH',
|
|
1053
|
+
'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF',
|
|
1054
|
+
]);
|
|
855
1055
|
// `defaultHost` / `fallbackModel` added by Phase 5b (#2799). Phase 4 federated every
|
|
856
1056
|
// `review.*_host` key with a default of `""`, so the REAL fallback destination and model
|
|
857
1057
|
// (`http://localhost:11434` / `llama3` and friends) existed only inside the bash leg. Once the
|
|
@@ -860,7 +1060,7 @@ const HTTP_ONLY_INVOKE_FIELDS = ['hostConfigKey', 'defaultHost', 'path', 'model
|
|
|
860
1060
|
|
|
861
1061
|
// Feature-only fields are as forbidden on a lane-only capability as on a runtime
|
|
862
1062
|
// one; a `role: "reviewer"` capability owns no artefacts and wires no loop point.
|
|
863
|
-
const FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
|
|
1063
|
+
const FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey', 'taskContentResolver'];
|
|
864
1064
|
|
|
865
1065
|
// GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
|
|
866
1066
|
// Derived from the actual pairings in the 16 real runtime descriptors.
|
|
@@ -1505,6 +1705,16 @@ function validateRuntimeBody(cap) {
|
|
|
1505
1705
|
'runtime.orchestratorExec.promptFlag must be a string or null (got: ' + JSON.stringify(oe.promptFlag) + ')',
|
|
1506
1706
|
);
|
|
1507
1707
|
}
|
|
1708
|
+
|
|
1709
|
+
// modelFlag — optional; string or null (#3714). `null`/absent means the
|
|
1710
|
+
// host offers no per-invocation model override on this exec path; a
|
|
1711
|
+
// string names the flag that pins the executor's model (codex: --model).
|
|
1712
|
+
// Deliberately asymmetric: only codex declares this today.
|
|
1713
|
+
if (oe.modelFlag !== undefined && oe.modelFlag !== null && typeof oe.modelFlag !== 'string') {
|
|
1714
|
+
errors.push(
|
|
1715
|
+
'runtime.orchestratorExec.modelFlag must be a string or null (got: ' + JSON.stringify(oe.modelFlag) + ')',
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1508
1718
|
}
|
|
1509
1719
|
}
|
|
1510
1720
|
|
|
@@ -1557,6 +1767,45 @@ function validateRuntimeBody(cap) {
|
|
|
1557
1767
|
}
|
|
1558
1768
|
}
|
|
1559
1769
|
|
|
1770
|
+
// triggerPrecedence — #2871 Phase 2 amendment to ADR-1016's runtime body.
|
|
1771
|
+
// REQUIRED-WITH-DEFAULT: no other axis in this validator uses this shape —
|
|
1772
|
+
// every axis above either hard-requires the field (throws when absent) or
|
|
1773
|
+
// treats absence as fully unconstrained (effortSurface/isolation: "nothing to
|
|
1774
|
+
// validate" when undefined). This axis does neither: an ABSENT value is
|
|
1775
|
+
// substituted with DEFAULT_TRIGGER_PRECEDENCE and then validated exactly as
|
|
1776
|
+
// if it HAD been supplied, so a third-party capability.json authored before
|
|
1777
|
+
// this phase keeps validating (ADR-894 additive-only / Hyrum's Law, ADR-1244)
|
|
1778
|
+
// while a PRESENT-but-malformed value still fails loudly instead of silently
|
|
1779
|
+
// passing through unchecked.
|
|
1780
|
+
const triggerPrecedenceValue = Object.prototype.hasOwnProperty.call(r, 'triggerPrecedence')
|
|
1781
|
+
? r.triggerPrecedence
|
|
1782
|
+
: DEFAULT_TRIGGER_PRECEDENCE;
|
|
1783
|
+
if (!Array.isArray(triggerPrecedenceValue)) {
|
|
1784
|
+
errors.push(
|
|
1785
|
+
'runtime.triggerPrecedence must be an array of trigger-bearing kind names (' +
|
|
1786
|
+
[...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') + ') (got: ' + JSON.stringify(triggerPrecedenceValue) + ')',
|
|
1787
|
+
);
|
|
1788
|
+
} else if (triggerPrecedenceValue.length === 0) {
|
|
1789
|
+
errors.push('runtime.triggerPrecedence must not be empty');
|
|
1790
|
+
} else {
|
|
1791
|
+
const seenKinds = new Set();
|
|
1792
|
+
for (let i = 0; i < triggerPrecedenceValue.length; i++) {
|
|
1793
|
+
const k = triggerPrecedenceValue[i];
|
|
1794
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
|
|
1795
|
+
errors.push('runtime.triggerPrecedence[' + i + '] "' + k + '" is a reserved name');
|
|
1796
|
+
} else if (typeof k !== 'string' || !VALID_TRIGGER_PRECEDENCE_KINDS.has(k)) {
|
|
1797
|
+
errors.push(
|
|
1798
|
+
'runtime.triggerPrecedence[' + i + '] must be one of: ' + [...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') +
|
|
1799
|
+
' (got: ' + JSON.stringify(k) + ')',
|
|
1800
|
+
);
|
|
1801
|
+
} else if (seenKinds.has(k)) {
|
|
1802
|
+
errors.push('runtime.triggerPrecedence contains a duplicate kind: ' + JSON.stringify(k));
|
|
1803
|
+
} else {
|
|
1804
|
+
seenKinds.add(k);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1560
1809
|
return errors;
|
|
1561
1810
|
}
|
|
1562
1811
|
|
|
@@ -1578,6 +1827,122 @@ const KNOWN_REVIEWER_FIELDS = new Set([
|
|
|
1578
1827
|
'handler',
|
|
1579
1828
|
]);
|
|
1580
1829
|
|
|
1830
|
+
/**
|
|
1831
|
+
* The closed `runtime.hostBehaviors` vocabulary (ADR-1016, closed via #2801).
|
|
1832
|
+
*
|
|
1833
|
+
* ADR-1016's core principle is that every per-runtime difference is a value over
|
|
1834
|
+
* a closed primitive vocabulary, and that a host needing a new shape gets a
|
|
1835
|
+
* reviewed first-party primitive rather than "an open escape hatch in the
|
|
1836
|
+
* descriptor" (§Alternatives #2 rejects exactly that). `hostBehaviors` was the
|
|
1837
|
+
* one hole left in that closure: 59 keys across 18 manifests, 39 of them set by
|
|
1838
|
+
* a single capability, validated by nothing. It was described in the reference
|
|
1839
|
+
* docs as a deliberate open seam sanctioned by ADR-1016 — ADR-1016 does not
|
|
1840
|
+
* mention `hostBehaviors` at all, and its stated principle is the opposite.
|
|
1841
|
+
*
|
|
1842
|
+
* Adding a key here is deliberate and reviewed. That friction IS the decision
|
|
1843
|
+
* (ADR-1016 §Consequences: "the closed vocabulary must grow (reviewed) when a
|
|
1844
|
+
* genuinely new host shape appears — intentional friction, the trust boundary").
|
|
1845
|
+
*
|
|
1846
|
+
* WARNING, never error. Two reasons:
|
|
1847
|
+
* 1. It matches the ADR-2782 D4.3 treatment of an unknown `reviewer` field, so
|
|
1848
|
+
* a manifest built against a newer GSD degrades visibly instead of failing
|
|
1849
|
+
* the build of a repo that merely reads it.
|
|
1850
|
+
* 2. An error would hard-break an out-of-tree descriptor carrying a bespoke
|
|
1851
|
+
* key, with no deprecation window — the exact mistake #2801's own alias
|
|
1852
|
+
* removal spent a full release avoiding. Escalating to an error is a later
|
|
1853
|
+
* step and needs its own window.
|
|
1854
|
+
*
|
|
1855
|
+
* Kept in sorted order, and `tests/reviewer-manifest-body.test.cjs` asserts this
|
|
1856
|
+
* set equals the keys the shipped manifests actually declare, so the list cannot
|
|
1857
|
+
* silently rot away from reality (DEFECT.GENERATIVE-FIX).
|
|
1858
|
+
*/
|
|
1859
|
+
const KNOWN_HOST_BEHAVIORS = new Set([
|
|
1860
|
+
'agentFileExtension',
|
|
1861
|
+
'agentFrontmatterExtensions',
|
|
1862
|
+
'agentManifestStyle',
|
|
1863
|
+
'agentTomlFiles',
|
|
1864
|
+
'attributionConfigResolver',
|
|
1865
|
+
'attributionSource',
|
|
1866
|
+
'authorsCanonicalWorkflow',
|
|
1867
|
+
'brandingRewrites',
|
|
1868
|
+
'cleanupSkillSidecars',
|
|
1869
|
+
'clineRulesSurface',
|
|
1870
|
+
'combinedFamilyInstall',
|
|
1871
|
+
'commandBodyConverter',
|
|
1872
|
+
'doneBannerStyle',
|
|
1873
|
+
'flatCommandDir',
|
|
1874
|
+
'frontmatterDialect',
|
|
1875
|
+
'globalDirResolver',
|
|
1876
|
+
'hookPathStyle',
|
|
1877
|
+
'hooksJsonSurface',
|
|
1878
|
+
'hyphenNameAgentBody',
|
|
1879
|
+
'installsCommandBodiesForWorkflowDelegation',
|
|
1880
|
+
'legacyCommandsGsdCleanup',
|
|
1881
|
+
'legacyCommandsGsdInstallMigration',
|
|
1882
|
+
'legacyCommandsGsdUninstall',
|
|
1883
|
+
'legacyDevinSkillsCleanup',
|
|
1884
|
+
'localCommandsViaRules',
|
|
1885
|
+
'localInstallDeferred',
|
|
1886
|
+
'localInstallStyle',
|
|
1887
|
+
'localTargetIsProjectRoot',
|
|
1888
|
+
'managedHookEvents',
|
|
1889
|
+
'mcpCompanion',
|
|
1890
|
+
'namedSubagentsSupported',
|
|
1891
|
+
'nativeModelAliases',
|
|
1892
|
+
'nativePlugin',
|
|
1893
|
+
'noPathRewrite',
|
|
1894
|
+
'ownsClaudePaths',
|
|
1895
|
+
'permissionsSchema',
|
|
1896
|
+
'pluginOnlyInstall',
|
|
1897
|
+
'projectInstructionFile',
|
|
1898
|
+
'reapplyCommand',
|
|
1899
|
+
'reportCommandsDir',
|
|
1900
|
+
'reportSkillsCount',
|
|
1901
|
+
'retiredArtifacts',
|
|
1902
|
+
'settingsFileByScope',
|
|
1903
|
+
'sharedHooksDirName',
|
|
1904
|
+
'skillFrontmatterVersion',
|
|
1905
|
+
'skillPriorityFrontmatter',
|
|
1906
|
+
'skillsGlobalOnboarding',
|
|
1907
|
+
'skillsManifestPrefix',
|
|
1908
|
+
'skipCodexSkillsManifest',
|
|
1909
|
+
'skipHomePrefixSubstitution',
|
|
1910
|
+
'skipSettingsUi',
|
|
1911
|
+
'skipSharedHooksInstall',
|
|
1912
|
+
'skipUpdateBannerCommand',
|
|
1913
|
+
'soloStageMetadata',
|
|
1914
|
+
'sourceMarkerFile',
|
|
1915
|
+
'tomlConfigInstall',
|
|
1916
|
+
'trackCategoryDescription',
|
|
1917
|
+
'verificationStyle',
|
|
1918
|
+
'writeCategoryDescription',
|
|
1919
|
+
]);
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Frozen reason codes for the non-fatal reviewer diagnostics (ADR-2782 D4.3).
|
|
1923
|
+
*
|
|
1924
|
+
* The IR behind `collectReviewerWarnings`' rendered strings. Tests assert on
|
|
1925
|
+
* these codes; the rendered `message` is operator console output and tests must
|
|
1926
|
+
* not depend on it (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
|
|
1927
|
+
* Outputs"), which is the same split `bin/verify-reapply-patches.cjs` uses.
|
|
1928
|
+
*
|
|
1929
|
+
* Adding a code is THREE coordinated changes: this enum, the emitting site in
|
|
1930
|
+
* `collectReviewerWarningRecordFields`, and the test that locks
|
|
1931
|
+
* `Object.keys(REVIEWER_WARNING).sort()`. That coupling is the point — it stops
|
|
1932
|
+
* the code surface drifting from the test surface.
|
|
1933
|
+
*/
|
|
1934
|
+
const REVIEWER_WARNING = Object.freeze({
|
|
1935
|
+
/** A key inside a `reviewer` body that this GSD version does not know. */
|
|
1936
|
+
UNKNOWN_REVIEWER_FIELD: 'unknown_reviewer_field',
|
|
1937
|
+
/** A `runtime.hostBehaviors` key that was removed from the vocabulary. */
|
|
1938
|
+
REMOVED_HOST_BEHAVIOR: 'removed_host_behavior',
|
|
1939
|
+
/** A `runtime.hostBehaviors` key outside the closed vocabulary. */
|
|
1940
|
+
UNKNOWN_HOST_BEHAVIOR: 'unknown_host_behavior',
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
/** Dotted path of the field removed by ADR-2782 D9 / #2801. */
|
|
1944
|
+
const REMOVED_REVIEWER_CLI_FIELD = 'runtime.hostBehaviors.reviewerCli';
|
|
1945
|
+
|
|
1581
1946
|
const KNOWN_PROBE_FIELDS = new Set(['kind', 'binary', 'needle', 'timeoutMs', 'hostConfigKey', 'path']);
|
|
1582
1947
|
|
|
1583
1948
|
/** A bounded probe timeout must be a finite, positive INTEGER of milliseconds. */
|
|
@@ -1615,6 +1980,41 @@ function describeValue(v) {
|
|
|
1615
1980
|
}
|
|
1616
1981
|
}
|
|
1617
1982
|
|
|
1983
|
+
/**
|
|
1984
|
+
* Ceiling on how many undeclared-key diagnostics one capability may produce.
|
|
1985
|
+
*
|
|
1986
|
+
* The loops below iterate MANIFEST-SUPPLIED keys, and an installed third-party
|
|
1987
|
+
* manifest is bounded only by MANIFEST_MAX_BYTES (8MB). Unbounded, one manifest
|
|
1988
|
+
* of 800k keys yields 800k records and ~139MB of message text, retained for the
|
|
1989
|
+
* registry's lifetime in OverlayMeta.diagnostics. Ten is enough to act on; the
|
|
1990
|
+
* rest are summarized. Mirrors the existing truncation idiom in
|
|
1991
|
+
* `capability-loader.cts` (`crossErrs.slice(0, 3)`).
|
|
1992
|
+
*/
|
|
1993
|
+
const MAX_REPORTED_UNKNOWN_KEYS = 10;
|
|
1994
|
+
|
|
1995
|
+
/** Ceiling on how much of one manifest-supplied key name a diagnostic repeats. */
|
|
1996
|
+
const MAX_REPORTED_KEY_CHARS = 80;
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* Render a manifest-supplied KEY for a diagnostic: control-safe and bounded.
|
|
2000
|
+
*
|
|
2001
|
+
* Key names carry no grammar anywhere — unlike `cap.id`, which `validateCapability`
|
|
2002
|
+
* gates on KEBAB_RE before these diagnostics run — so a key is fully
|
|
2003
|
+
* attacker-controlled text heading for stderr and OverlayMeta.warnings. C0/C1
|
|
2004
|
+
* controls (ESC, CR, LF) become U+FFFD so a key cannot emit terminal escapes or
|
|
2005
|
+
* forge a log line, and the result is clipped so one key cannot carry megabytes
|
|
2006
|
+
* into a retained diagnostic.
|
|
2007
|
+
*
|
|
2008
|
+
* @param {*} key
|
|
2009
|
+
* @returns {string}
|
|
2010
|
+
*/
|
|
2011
|
+
function describeKey(key) {
|
|
2012
|
+
const raw = typeof key === 'string' ? key : String(key);
|
|
2013
|
+
// eslint-disable-next-line no-control-regex
|
|
2014
|
+
const safe = raw.replace(/[\x00-\x1f\x7f-\x9f]/g, '�');
|
|
2015
|
+
return safe.length > MAX_REPORTED_KEY_CHARS ? safe.slice(0, MAX_REPORTED_KEY_CHARS) + '…' : safe;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1618
2018
|
/** Extract a message from an unknown thrown value without throwing again. */
|
|
1619
2019
|
function safeErrorMessage(err) {
|
|
1620
2020
|
try {
|
|
@@ -1661,7 +2061,39 @@ function validateEnumField(ctx, label, value, validSet) {
|
|
|
1661
2061
|
}
|
|
1662
2062
|
|
|
1663
2063
|
/**
|
|
1664
|
-
* Collect NON-FATAL diagnostics for a
|
|
2064
|
+
* Collect NON-FATAL reviewer diagnostics for a capability manifest as TYPED
|
|
2065
|
+
* RECORDS (ADR-2782 D4.3, plus the D9 `hostBehaviors.reviewerCli` removal
|
|
2066
|
+
* notice, #2801).
|
|
2067
|
+
*
|
|
2068
|
+
* This is the IR. `collectReviewerWarnings` below renders it to strings for the
|
|
2069
|
+
* two production consumers; tests assert on these records instead of matching
|
|
2070
|
+
* the rendered prose (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
|
|
2071
|
+
* Outputs").
|
|
2072
|
+
*
|
|
2073
|
+
* Record shape — `code` and `capId` are always present; the rest is per-code:
|
|
2074
|
+
* { code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
|
2075
|
+
* capId, field: 'reviewer.<key>', knownFields: string[], message }
|
|
2076
|
+
* { code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
|
|
2077
|
+
* capId, field: 'runtime.hostBehaviors.reviewerCli',
|
|
2078
|
+
* replacement: 'reviewer', docs: '<how-to path>', message }
|
|
2079
|
+
*
|
|
2080
|
+
* TOTAL: returns an array for ANY input and never throws. It runs on
|
|
2081
|
+
* loadRegistry's ACCEPT path, so a diagnostic that throws would cost the user a
|
|
2082
|
+
* lane that is otherwise perfectly valid (#1461 OVL-1).
|
|
2083
|
+
*
|
|
2084
|
+
* @param {object} cap A capability manifest.
|
|
2085
|
+
* @returns {Array<object>} Warning records; empty when there is nothing to say.
|
|
2086
|
+
*/
|
|
2087
|
+
function collectReviewerWarningRecords(cap) {
|
|
2088
|
+
try {
|
|
2089
|
+
return collectReviewerWarningRecordFields(cap);
|
|
2090
|
+
} catch {
|
|
2091
|
+
return [];
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/**
|
|
2096
|
+
* Render the IR for operator console output.
|
|
1665
2097
|
*
|
|
1666
2098
|
* Kept separate from validateReviewerBody so validateCapability's contract
|
|
1667
2099
|
* (`=> string[]` of ERRORS) is unchanged for its two existing callers. The
|
|
@@ -1669,35 +2101,135 @@ function validateEnumField(ctx, label, value, validSet) {
|
|
|
1669
2101
|
* through OverlayMeta.warnings. A warning written only to a build log nobody
|
|
1670
2102
|
* reads is not a warning (ADR-2782 D4, "Where warnings surface").
|
|
1671
2103
|
*
|
|
2104
|
+
* The `string[]` shape is load-bearing for both consumers and does not change.
|
|
2105
|
+
*
|
|
1672
2106
|
* @param {object} cap A capability manifest.
|
|
1673
2107
|
* @returns {string[]} Warning strings; empty when there is nothing to say.
|
|
1674
2108
|
*/
|
|
1675
2109
|
function collectReviewerWarnings(cap) {
|
|
1676
|
-
|
|
1677
|
-
// called from loadRegistry's accept path, and a diagnostic that throws must
|
|
1678
|
-
// never cost the user a working lane.
|
|
1679
|
-
try {
|
|
1680
|
-
return collectReviewerWarningFields(cap);
|
|
1681
|
-
} catch {
|
|
1682
|
-
return [];
|
|
1683
|
-
}
|
|
2110
|
+
return collectReviewerWarningRecords(cap).map((record) => record.message);
|
|
1684
2111
|
}
|
|
1685
2112
|
|
|
1686
|
-
function
|
|
1687
|
-
const
|
|
1688
|
-
if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return
|
|
1689
|
-
const r = cap.reviewer;
|
|
1690
|
-
if (typeof r !== 'object' || r === null || Array.isArray(r)) return warnings;
|
|
2113
|
+
function collectReviewerWarningRecordFields(cap) {
|
|
2114
|
+
const records = [];
|
|
2115
|
+
if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return records;
|
|
1691
2116
|
|
|
1692
2117
|
const capId = typeof cap.id === 'string' ? cap.id : '(unknown)';
|
|
2118
|
+
|
|
2119
|
+
// ADR-2782 D9 / #2801 — the REMOVED `runtime.hostBehaviors.reviewerCli` alias.
|
|
2120
|
+
//
|
|
2121
|
+
// Emitted BEFORE the `reviewer`-body early-return below, deliberately. The
|
|
2122
|
+
// manifest this notice exists for is the ALIAS-ONLY one, which by definition
|
|
2123
|
+
// carries no body; after that guard the check would fire only for
|
|
2124
|
+
// capabilities that already declare a lane — exactly the set that does not
|
|
2125
|
+
// need telling.
|
|
2126
|
+
//
|
|
2127
|
+
// Presence-based, not `=== true`: the key is unknown at ANY value now, which
|
|
2128
|
+
// is the same rule the unknown-`reviewer.*`-field loop below applies. Own-key
|
|
2129
|
+
// read, so a polluted prototype cannot manufacture this warning on every
|
|
2130
|
+
// otherwise-innocent manifest. This is ONE keyed removal notice, not general
|
|
2131
|
+
// `hostBehaviors` validation — the closed vocabulary below handles that (#2801).
|
|
2132
|
+
const runtimeBody = cap.runtime;
|
|
2133
|
+
if (typeof runtimeBody === 'object' && runtimeBody !== null && !Array.isArray(runtimeBody)) {
|
|
2134
|
+
const hostBehaviors = runtimeBody.hostBehaviors;
|
|
2135
|
+
if (
|
|
2136
|
+
typeof hostBehaviors === 'object'
|
|
2137
|
+
&& hostBehaviors !== null
|
|
2138
|
+
&& !Array.isArray(hostBehaviors)
|
|
2139
|
+
&& Object.prototype.hasOwnProperty.call(hostBehaviors, 'reviewerCli')
|
|
2140
|
+
) {
|
|
2141
|
+
records.push({
|
|
2142
|
+
code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
|
|
2143
|
+
capId,
|
|
2144
|
+
field: REMOVED_REVIEWER_CLI_FIELD,
|
|
2145
|
+
replacement: 'reviewer',
|
|
2146
|
+
docs: 'docs/how-to/ship-a-reviewer-lane.md',
|
|
2147
|
+
message:
|
|
2148
|
+
'⚠ capability "' + capId + '" ' + REMOVED_REVIEWER_CLI_FIELD + ' was removed (ADR-2782 D9) ' +
|
|
2149
|
+
'— ignored, and it contributes no reviewer lane. Declare a `reviewer` body instead; see ' +
|
|
2150
|
+
'docs/how-to/ship-a-reviewer-lane.md',
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// #2801 — the closed `hostBehaviors` vocabulary (ADR-1016). `reviewerCli` is
|
|
2155
|
+
// excluded here: it already drew its own removal notice above, and a second,
|
|
2156
|
+
// generic "unknown key" record for the same key would be noise, not signal.
|
|
2157
|
+
if (typeof hostBehaviors === 'object' && hostBehaviors !== null && !Array.isArray(hostBehaviors)) {
|
|
2158
|
+
let reported = 0;
|
|
2159
|
+
let omitted = 0;
|
|
2160
|
+
for (const key of Object.keys(hostBehaviors)) {
|
|
2161
|
+
if (isReservedName(key) || key === 'reviewerCli' || KNOWN_HOST_BEHAVIORS.has(key)) continue;
|
|
2162
|
+
if (reported >= MAX_REPORTED_UNKNOWN_KEYS) {
|
|
2163
|
+
omitted += 1;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
reported += 1;
|
|
2167
|
+
const safeKey = describeKey(key);
|
|
2168
|
+
records.push({
|
|
2169
|
+
code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
|
|
2170
|
+
capId,
|
|
2171
|
+
field: 'runtime.hostBehaviors.' + safeKey,
|
|
2172
|
+
message:
|
|
2173
|
+
'⚠ capability "' + capId + '" runtime.hostBehaviors.' + safeKey + ' is not a known host behavior ' +
|
|
2174
|
+
'in this GSD version — ignored. Adding one is a reviewed first-party change (ADR-1016).',
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
if (omitted > 0) {
|
|
2178
|
+
records.push({
|
|
2179
|
+
code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
|
|
2180
|
+
capId,
|
|
2181
|
+
field: 'runtime.hostBehaviors',
|
|
2182
|
+
truncated: true,
|
|
2183
|
+
omittedCount: omitted,
|
|
2184
|
+
message:
|
|
2185
|
+
'⚠ capability "' + capId + '" declares ' + omitted + ' further unknown runtime.hostBehaviors ' +
|
|
2186
|
+
'key(s), not listed. A manifest this far outside the vocabulary is likely built for a ' +
|
|
2187
|
+
'different GSD version.',
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const r = cap.reviewer;
|
|
2194
|
+
if (typeof r !== 'object' || r === null || Array.isArray(r)) return records;
|
|
2195
|
+
|
|
2196
|
+
// Same ceiling and the same key sanitization as the hostBehaviors sweep above.
|
|
2197
|
+
// This loop predates #2801 and carried both defects; fixing only the new copy
|
|
2198
|
+
// would leave the identical defect one screen away from its own fix.
|
|
2199
|
+
let reportedFields = 0;
|
|
2200
|
+
let omittedFields = 0;
|
|
1693
2201
|
for (const key of Object.keys(r)) {
|
|
1694
2202
|
if (isReservedName(key) || KNOWN_REVIEWER_FIELDS.has(key)) continue;
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2203
|
+
if (reportedFields >= MAX_REPORTED_UNKNOWN_KEYS) {
|
|
2204
|
+
omittedFields += 1;
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
reportedFields += 1;
|
|
2208
|
+
const safeKey = describeKey(key);
|
|
2209
|
+
records.push({
|
|
2210
|
+
code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
|
2211
|
+
capId,
|
|
2212
|
+
field: 'reviewer.' + safeKey,
|
|
2213
|
+
knownFields: [...KNOWN_REVIEWER_FIELDS],
|
|
2214
|
+
message:
|
|
2215
|
+
'⚠ capability "' + capId + '" reviewer.' + safeKey + ' is not a known reviewer field ' +
|
|
2216
|
+
'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if (omittedFields > 0) {
|
|
2220
|
+
records.push({
|
|
2221
|
+
code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
|
|
2222
|
+
capId,
|
|
2223
|
+
field: 'reviewer',
|
|
2224
|
+
knownFields: [...KNOWN_REVIEWER_FIELDS],
|
|
2225
|
+
truncated: true,
|
|
2226
|
+
omittedCount: omittedFields,
|
|
2227
|
+
message:
|
|
2228
|
+
'⚠ capability "' + capId + '" declares ' + omittedFields + ' further unknown reviewer field(s), ' +
|
|
2229
|
+
'not listed.',
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
return records;
|
|
1701
2233
|
}
|
|
1702
2234
|
|
|
1703
2235
|
/**
|
|
@@ -2046,6 +2578,66 @@ function validateSpawnInvoke(ctx, invoke) {
|
|
|
2046
2578
|
|
|
2047
2579
|
errors.push(...validateEnumField(ctx, 'reviewer.invoke.effortChannel', invoke.effortChannel, VALID_LANE_EFFORT_CHANNELS));
|
|
2048
2580
|
|
|
2581
|
+
// `env` — per-invocation environment pairs (#2483). OPTIONAL, unlike every field above: only a lane
|
|
2582
|
+
// that needs to shape its child's environment declares it, and absent is the common case. Validated
|
|
2583
|
+
// when present, because `resolveLanePlan` DROPS a non-string value rather than coercing it — so an
|
|
2584
|
+
// unvalidated manifest declares a pair that silently never reaches the spawn, which is the failure
|
|
2585
|
+
// mode a shipped env-guard can least afford. Keys are held to the portable POSIX
|
|
2586
|
+
// environment-name grammar, which is a POLICY — not a claim about what an environment can physically
|
|
2587
|
+
// hold. Measured: of the names refused below only NUL is actually rejected by `spawnSync`; `=`, a
|
|
2588
|
+
// leading digit, a dash and a space are all carried through to the child (an `A=B` key arrives as
|
|
2589
|
+
// the raw entry `A=B=value`, and reads back via `process.env['A=B']`). They are refused because a
|
|
2590
|
+
// name outside the grammar is not portably addressable by the program meant to read it.
|
|
2591
|
+
if (invoke.env !== undefined) {
|
|
2592
|
+
if (typeof invoke.env !== 'object' || invoke.env === null || Array.isArray(invoke.env)) {
|
|
2593
|
+
errors.push(
|
|
2594
|
+
ctx + ' reviewer.invoke.env must be an object of environment name/value pairs ' +
|
|
2595
|
+
'(got: ' + describeValue(invoke.env) + ')',
|
|
2596
|
+
);
|
|
2597
|
+
} else {
|
|
2598
|
+
for (const key of Object.keys(invoke.env)) {
|
|
2599
|
+
// `__proto__` passes the grammar below and IS a real own key once a manifest is JSON-parsed,
|
|
2600
|
+
// but assigning it onto a plain accumulator goes through the inherited `__proto__` SETTER
|
|
2601
|
+
// instead of creating an own property. For the string values this field permits the setter is
|
|
2602
|
+
// a no-op — the prototype is not even changed — so the pair would validate and then simply
|
|
2603
|
+
// vanish before the spawn: the declared-but-never-delivered failure this block exists to catch.
|
|
2604
|
+
// Refused by name, because the grammar cannot see it.
|
|
2605
|
+
//
|
|
2606
|
+
// Only `__proto__` needs this. Sibling reserved-name guards in this file reject
|
|
2607
|
+
// `constructor`/`prototype` alongside it, but those guard bracket LOOKUPS that resolve
|
|
2608
|
+
// prototype members; here the read is `Object.keys` + an own-value read, and `constructor`
|
|
2609
|
+
// assigns as an ordinary own key. Refusing it too would reject a name the spawn could carry.
|
|
2610
|
+
// CASE-INSENSITIVE, and that is not pedantry: Windows environment lookups are
|
|
2611
|
+
// case-insensitive, so `Path` / `node_options` reach the child as `PATH` / `NODE_OPTIONS`
|
|
2612
|
+
// and an exact-case set is bypassed by changing one letter. The grammar above already
|
|
2613
|
+
// constrains keys to ASCII, so a plain uppercase fold is sufficient here.
|
|
2614
|
+
if (DENIED_LANE_ENV_KEYS.has(key.toUpperCase())) {
|
|
2615
|
+
errors.push(
|
|
2616
|
+
ctx + ' reviewer.invoke.env key "' + key + '" is not permitted ' +
|
|
2617
|
+
'(it makes the spawned reviewer run code of the manifest\'s choosing; ' +
|
|
2618
|
+
'declare an absolute `invoke.binary` instead of reshaping the child\'s environment)',
|
|
2619
|
+
);
|
|
2620
|
+
} else if (key === '__proto__') {
|
|
2621
|
+
errors.push(
|
|
2622
|
+
ctx + ' reviewer.invoke.env key "__proto__" is not permitted ' +
|
|
2623
|
+
'(it is silently dropped when the spawn plan is assembled, so it would never reach the child)',
|
|
2624
|
+
);
|
|
2625
|
+
} else if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
2626
|
+
errors.push(
|
|
2627
|
+
ctx + ' reviewer.invoke.env key ' + describeValue(key) +
|
|
2628
|
+
' is not a valid environment variable name',
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
if (typeof invoke.env[key] !== 'string') {
|
|
2632
|
+
errors.push(
|
|
2633
|
+
ctx + ' reviewer.invoke.env.' + key + ' must be a string ' +
|
|
2634
|
+
'(got: ' + describeValue(invoke.env[key]) + ')',
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2049
2641
|
return errors;
|
|
2050
2642
|
}
|
|
2051
2643
|
|
|
@@ -2709,6 +3301,11 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
|
|
|
2709
3301
|
const laneSlugClaims = new Map(); // slug → capId[]
|
|
2710
3302
|
const laneFlagClaims = new Map(); // flag → capId[]
|
|
2711
3303
|
const laneSectionClaims = new Map(); // reviewsSection → capId[]
|
|
3304
|
+
// ADR-3646: task-content resolver tracker-prefix uniqueness across the
|
|
3305
|
+
// MERGED first-party ∪ overlay set, mirroring the reviewer-lane collision
|
|
3306
|
+
// pattern above — two resolvers claiming the same prefix would make
|
|
3307
|
+
// `execute:task` dispatch ambiguous (which capability's resolver runs?).
|
|
3308
|
+
const trackerPrefixClaims = new Map(); // trackerPrefix → capId[]
|
|
2712
3309
|
|
|
2713
3310
|
// Claims are ACCUMULATED and reported after the sweep, never reported on the
|
|
2714
3311
|
// second claimant. Reporting pairwise-on-collision looks equivalent and is not:
|
|
@@ -2729,6 +3326,15 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
|
|
|
2729
3326
|
};
|
|
2730
3327
|
|
|
2731
3328
|
for (const [capId, cap] of capMap) {
|
|
3329
|
+
// ADR-3646: a MALFORMED taskContentResolver body was already reported by
|
|
3330
|
+
// validateCapability — do not double-report; only claim well-shaped
|
|
3331
|
+
// bodies. Independent of the reviewer-lane checks below, so it runs even
|
|
3332
|
+
// for capabilities that carry no `reviewer` body at all.
|
|
3333
|
+
const tcr = cap.taskContentResolver;
|
|
3334
|
+
if (typeof tcr === 'object' && tcr !== null && !Array.isArray(tcr)) {
|
|
3335
|
+
claim(trackerPrefixClaims, tcr.trackerPrefix, capId);
|
|
3336
|
+
}
|
|
3337
|
+
|
|
2732
3338
|
const r = cap.reviewer;
|
|
2733
3339
|
// A capability with no lane contributes to no uniqueness set. A MALFORMED
|
|
2734
3340
|
// body was already reported by validateCapability — do not double-report.
|
|
@@ -2750,6 +3356,7 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
|
|
|
2750
3356
|
[laneSlugClaims, 'slug'],
|
|
2751
3357
|
[laneFlagClaims, 'flag'],
|
|
2752
3358
|
[laneSectionClaims, 'reviewsSection'],
|
|
3359
|
+
[trackerPrefixClaims, 'taskContentResolver.trackerPrefix'],
|
|
2753
3360
|
]) {
|
|
2754
3361
|
for (const [key, claimants] of claims) {
|
|
2755
3362
|
if (claimants.length < 2) continue;
|
|
@@ -2970,26 +3577,55 @@ function topoSortContributions(entries) {
|
|
|
2970
3577
|
|
|
2971
3578
|
// ─── Gen-time wired guard ─────────────────────────────────────────────────────
|
|
2972
3579
|
|
|
3580
|
+
/**
|
|
3581
|
+
* Hook group (capability.json array name) → hook kind (dispatch discriminator).
|
|
3582
|
+
* Single source of truth for both validateHooksWired and the scanner-parity
|
|
3583
|
+
* test in tests/capability-registry.test.cjs (#3606).
|
|
3584
|
+
*/
|
|
3585
|
+
const HOOK_GROUP_KINDS = Object.freeze({
|
|
3586
|
+
steps: 'step',
|
|
3587
|
+
contributions: 'contribution',
|
|
3588
|
+
gates: 'gate',
|
|
3589
|
+
});
|
|
3590
|
+
|
|
2973
3591
|
/**
|
|
2974
3592
|
* Validate that every hook point declared by a capability has a corresponding
|
|
2975
|
-
* `loop render-hooks <point>` call site in one of the host-loop workflow files
|
|
3593
|
+
* `loop render-hooks <point>` call site in one of the host-loop workflow files,
|
|
3594
|
+
* AND — #3606 — that the call site's dispatch text covers the hook's KIND.
|
|
3595
|
+
*
|
|
3596
|
+
* A call site proves hooks are rendered, not dispatched: a consumer that
|
|
3597
|
+
* iterates only `kind == "gate"` (or narrows `kind == "step"` to one
|
|
3598
|
+
* `ref.skill`) silently drops every other registered kind — a capability can
|
|
3599
|
+
* be wired, enabled, resolved active, and never run. See
|
|
3600
|
+
* gsd-core/references/loop-hook-dispatch.md ("A point whose workflow
|
|
3601
|
+
* hand-rolls one kind does not implement this contract").
|
|
2976
3602
|
*
|
|
2977
3603
|
* Only valid loop points (in VALID_LOOP_POINTS) are checked here. Invalid points
|
|
2978
3604
|
* are already caught by validateStep/validateContribution/validateGate — do not
|
|
2979
3605
|
* double-report.
|
|
2980
3606
|
*
|
|
3607
|
+
* KNOWN LIMITATION (#3606): coverage is the UNION across all call sites for a
|
|
3608
|
+
* point in the five STEP_WORKFLOWS host files. Consumers outside that universe
|
|
3609
|
+
* (quick.md, autonomous.md, code-review*.md, audit-milestone.md,
|
|
3610
|
+
* secure-phase.md, validate-phase.md) are not per-file checked — a narrowed
|
|
3611
|
+
* consumer there passes as long as one host file covers the point. Per-file
|
|
3612
|
+
* coverage maps are the tightening path.
|
|
3613
|
+
*
|
|
2981
3614
|
* @param {object} cap Validated capability object.
|
|
2982
|
-
* @param {Set<string
|
|
2983
|
-
*
|
|
3615
|
+
* @param {Map<string, Set<string>>} wiredKinds Per point, the hook kinds the
|
|
3616
|
+
* host workflows' call-site dispatch text covers (getWiredKinds). A point
|
|
3617
|
+
* absent from the map is unwired.
|
|
3618
|
+
* @returns {string[]} Array of error strings; empty means all points
|
|
3619
|
+
* are wired and every registered kind is covered.
|
|
2984
3620
|
*/
|
|
2985
|
-
function validateHooksWired(cap,
|
|
3621
|
+
function validateHooksWired(cap, wiredKinds) {
|
|
2986
3622
|
const errors = [];
|
|
2987
3623
|
const capId = cap.id || '(unknown)';
|
|
2988
3624
|
|
|
2989
|
-
function checkPoint(point, groupName, idx) {
|
|
3625
|
+
function checkPoint(point, groupName, kind, idx) {
|
|
2990
3626
|
// Only flag valid points that are unwired — invalid points are schema-validator's job.
|
|
2991
3627
|
if (!VALID_LOOP_POINTS.has(point)) return;
|
|
2992
|
-
if (!
|
|
3628
|
+
if (!wiredKinds.has(point)) {
|
|
2993
3629
|
errors.push(
|
|
2994
3630
|
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
|
|
2995
3631
|
'" is declared but not wired in any host-loop workflow ' +
|
|
@@ -2997,20 +3633,34 @@ function validateHooksWired(cap, wiredSet) {
|
|
|
2997
3633
|
'Wire the call site in the host workflow ' +
|
|
2998
3634
|
'(see scripts/gen-loop-host-contract.cjs STEP_WORKFLOWS) or remove the hook.',
|
|
2999
3635
|
);
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
const covered = wiredKinds.get(point);
|
|
3639
|
+
if (covered.size === 0) {
|
|
3640
|
+
errors.push(
|
|
3641
|
+
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
|
|
3642
|
+
'" has `loop render-hooks ' + point + '` call site(s), but their dispatch text covers NO ' +
|
|
3643
|
+
'hook kind — every consumer is narrowed to specific hooks. Dispatch every registered kind ' +
|
|
3644
|
+
'per gsd-core/references/loop-hook-dispatch.md.',
|
|
3645
|
+
);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
if (!covered.has(kind)) {
|
|
3649
|
+
errors.push(
|
|
3650
|
+
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
|
|
3651
|
+
'" registers a ' + kind + ' hook, but the host call site\'s dispatch text never ' +
|
|
3652
|
+
'covers `kind == "' + kind + '"` (it covers: ' + [...covered].sort().join(', ') + '). ' +
|
|
3653
|
+
'A hand-rolled single-kind consumer silently never dispatches the other kinds — ' +
|
|
3654
|
+
'dispatch every registered kind per gsd-core/references/loop-hook-dispatch.md.',
|
|
3655
|
+
);
|
|
3000
3656
|
}
|
|
3001
3657
|
}
|
|
3002
3658
|
|
|
3003
|
-
for (
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
const hook = cap.contributions[i];
|
|
3009
|
-
if (hook.point !== undefined) checkPoint(hook.point, 'contributions', i);
|
|
3010
|
-
}
|
|
3011
|
-
for (let i = 0; i < (cap.gates || []).length; i++) {
|
|
3012
|
-
const hook = cap.gates[i];
|
|
3013
|
-
if (hook.point !== undefined) checkPoint(hook.point, 'gates', i);
|
|
3659
|
+
for (const [group, kind] of Object.entries(HOOK_GROUP_KINDS)) {
|
|
3660
|
+
for (let i = 0; i < (cap[group] || []).length; i++) {
|
|
3661
|
+
const hook = cap[group][i];
|
|
3662
|
+
if (hook.point !== undefined) checkPoint(hook.point, group, kind, i);
|
|
3663
|
+
}
|
|
3014
3664
|
}
|
|
3015
3665
|
|
|
3016
3666
|
return errors;
|
|
@@ -3155,6 +3805,8 @@ module.exports = {
|
|
|
3155
3805
|
VALID_SANDBOX_TIERS,
|
|
3156
3806
|
VALID_ARTIFACT_KIND_NAMES,
|
|
3157
3807
|
VALID_ARTIFACT_NESTINGS,
|
|
3808
|
+
VALID_TRIGGER_PRECEDENCE_KINDS,
|
|
3809
|
+
DEFAULT_TRIGGER_PRECEDENCE,
|
|
3158
3810
|
FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME,
|
|
3159
3811
|
// ADR-2782 D1/D2/D3/D6/D7/D8 — reviewer lane body
|
|
3160
3812
|
FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER,
|
|
@@ -3170,8 +3822,14 @@ module.exports = {
|
|
|
3170
3822
|
VALID_EVIDENCE_CLASSES,
|
|
3171
3823
|
VALID_LANE_HANDLERS,
|
|
3172
3824
|
KNOWN_REVIEWER_FIELDS,
|
|
3825
|
+
KNOWN_HOST_BEHAVIORS,
|
|
3826
|
+
MAX_REPORTED_UNKNOWN_KEYS,
|
|
3827
|
+
MAX_REPORTED_KEY_CHARS,
|
|
3173
3828
|
validateReviewerBody,
|
|
3174
3829
|
collectReviewerWarnings,
|
|
3830
|
+
collectReviewerWarningRecords,
|
|
3831
|
+
REVIEWER_WARNING,
|
|
3832
|
+
REMOVED_REVIEWER_CLI_FIELD,
|
|
3175
3833
|
VALID_INSTALL_SURFACES,
|
|
3176
3834
|
VALID_PERMISSION_WRITERS,
|
|
3177
3835
|
VALID_EXTENDED_HOOK_EVENTS,
|
|
@@ -3208,6 +3866,7 @@ module.exports = {
|
|
|
3208
3866
|
validateCommandEntry,
|
|
3209
3867
|
validateRuntimeCompat,
|
|
3210
3868
|
validateFeatureBody,
|
|
3869
|
+
validateTaskContentResolver,
|
|
3211
3870
|
validateConfigHome,
|
|
3212
3871
|
validateArtifactKindEntry,
|
|
3213
3872
|
validateArtifactLayout,
|
|
@@ -3226,6 +3885,7 @@ module.exports = {
|
|
|
3226
3885
|
topoSortSteps,
|
|
3227
3886
|
topoSortContributions,
|
|
3228
3887
|
validateHooksWired,
|
|
3888
|
+
HOOK_GROUP_KINDS,
|
|
3229
3889
|
validateConfigSliceEntry,
|
|
3230
3890
|
classifyCrossErrors,
|
|
3231
3891
|
runConfigFormatParityGate,
|