@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
|
@@ -61,4 +61,906 @@ function executionContextRefs(content) {
|
|
|
61
61
|
return refs;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* workflowPathRefs(content)
|
|
66
|
+
*
|
|
67
|
+
* Locates every gsd-core-relative workflow path referenced in a markdown
|
|
68
|
+
* string, whether the reference is an eager @-include (already covered by
|
|
69
|
+
* executionContextRefs) or a *lazy* path mentioned only in prose/code — a
|
|
70
|
+
* path a command reads on demand via Read/Bash rather than an @-inclusion
|
|
71
|
+
* the harness inlines automatically. Both kinds are load-bearing: the
|
|
72
|
+
* progressive-disclosure split (#717) deliberately keeps most workflow
|
|
73
|
+
* content out of the eager path so the common case stays cheap, but that
|
|
74
|
+
* means a command naming a workflow only in prose is invisible to
|
|
75
|
+
* executionContextRefs even though the runtime still needs the file to
|
|
76
|
+
* exist. Recognizes three reference shapes:
|
|
77
|
+
*
|
|
78
|
+
* A. Any path whose segments include `workflows/`, optionally preceded by
|
|
79
|
+
* an eager `@`, a home-dir prefix (`~/` or `$HOME/`), `.claude/`, and/or
|
|
80
|
+
* `gsd-core/` — e.g. `@~/.claude/gsd-core/workflows/scan.md`,
|
|
81
|
+
* `gsd-core/workflows/x.md`, or a bare `workflows/x.md`.
|
|
82
|
+
* B. Same as A but without the eager `@` — a lazy reference read on
|
|
83
|
+
* demand rather than inlined at load time.
|
|
84
|
+
* C. Parent-relative sub-file paths with no `workflows/` prefix at all —
|
|
85
|
+
* `execute-phase/steps/post-merge-gate.md` — implicitly rooted under
|
|
86
|
+
* `workflows/` because that's the only place `steps/`, `modes/`, and
|
|
87
|
+
* `templates/` subdirectories live.
|
|
88
|
+
*
|
|
89
|
+
* Traversal segments (`..`) are dropped rather than surfaced: this resolver
|
|
90
|
+
* only ever reports paths under `workflows/`, never something a `..` could
|
|
91
|
+
* walk outside of it. Results are de-duplicated, first-seen order preserved.
|
|
92
|
+
*
|
|
93
|
+
* Both regexes anchor `\.md` with a trailing `(?![A-Za-z0-9_])` negative
|
|
94
|
+
* lookahead so a longer extension (`.mdx`, `.md5`) is rejected outright
|
|
95
|
+
* rather than silently truncated into a plausible-looking `.md` path.
|
|
96
|
+
*/
|
|
97
|
+
function workflowPathRefs(content) {
|
|
98
|
+
const refs = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
|
|
101
|
+
function addRef(normalized) {
|
|
102
|
+
if (normalized.split('/').includes('..')) return;
|
|
103
|
+
if (seen.has(normalized)) return;
|
|
104
|
+
seen.add(normalized);
|
|
105
|
+
refs.push(normalized);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const shapeARe = /@?(?:(?:~|\$HOME)\/)?(?:\.claude\/)?(?:gsd-core\/)?workflows\/[A-Za-z0-9._/-]+\.md(?![A-Za-z0-9_])/g;
|
|
109
|
+
let m;
|
|
110
|
+
while ((m = shapeARe.exec(content)) !== null) {
|
|
111
|
+
const normalized = m[0]
|
|
112
|
+
.replace(/^@/, '')
|
|
113
|
+
.replace(/^(?:~|\$HOME)\//, '')
|
|
114
|
+
.replace(/^\.claude\//, '')
|
|
115
|
+
.replace(/^gsd-core\//, '');
|
|
116
|
+
addRef(normalized);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const shapeCRe = /(?:^|[\s`("'>])([A-Za-z0-9._-]+\/(?:steps|modes|templates)\/[A-Za-z0-9._-]+\.md(?![A-Za-z0-9_]))/gm;
|
|
120
|
+
while ((m = shapeCRe.exec(content)) !== null) {
|
|
121
|
+
addRef('workflows/' + m[1]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return refs;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* unreachableWorkflows(loaderContents, gsdFiles, workflowPaths)
|
|
129
|
+
*
|
|
130
|
+
* Computes reachability over the gsd-core file graph and reports which
|
|
131
|
+
* `workflowPaths` are never reached, starting only from `loaderContents`
|
|
132
|
+
* (commands/agents/skills — the files a runtime actually loads) and walking
|
|
133
|
+
* `workflowPathRefs` edges transitively through `gsdFiles`.
|
|
134
|
+
*
|
|
135
|
+
* The seed set is deliberately restricted to loaders and never includes a
|
|
136
|
+
* workflow's own content. Seeding from workflows too would let two failure
|
|
137
|
+
* modes hide: a workflow that only references itself would satisfy its own
|
|
138
|
+
* reachability, and a pair of workflows that reference only each other would
|
|
139
|
+
* form an island that looks connected from the inside but that no command,
|
|
140
|
+
* agent, or skill ever actually opens. Both are orphans in every sense that
|
|
141
|
+
* matters — nothing external can reach them — and both must be reported.
|
|
142
|
+
* Requiring every path to originate at a loader is what makes "reachable"
|
|
143
|
+
* mean "a runtime can actually get here," not merely "something points to
|
|
144
|
+
* it."
|
|
145
|
+
*
|
|
146
|
+
* `gsdFiles` covers all of `gsd-core/**`, not just `workflows/`, because a
|
|
147
|
+
* `references/` or `templates/` file can itself name a workflow path and
|
|
148
|
+
* needs to be walked through to propagate reachability — restricting the map
|
|
149
|
+
* to `workflows/` would silently break any chain that passes through a
|
|
150
|
+
* non-workflow file.
|
|
151
|
+
*
|
|
152
|
+
* `visited` guards the walk against reference cycles (including the
|
|
153
|
+
* mutual/self cases above) so traversal always terminates.
|
|
154
|
+
*/
|
|
155
|
+
function unreachableWorkflows(loaderContents, gsdFiles, workflowPaths) {
|
|
156
|
+
const visited = new Set();
|
|
157
|
+
const queue = [];
|
|
158
|
+
|
|
159
|
+
for (const content of loaderContents) {
|
|
160
|
+
for (const ref of workflowPathRefs(content)) queue.push(ref);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
while (queue.length > 0) {
|
|
164
|
+
const p = queue.pop();
|
|
165
|
+
if (visited.has(p)) continue;
|
|
166
|
+
visited.add(p);
|
|
167
|
+
if (gsdFiles.has(p)) {
|
|
168
|
+
for (const ref of workflowPathRefs(gsdFiles.get(p))) queue.push(ref);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return workflowPaths.filter(p => !visited.has(p));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* splitOutsideDelimiter(str, delimiter)
|
|
177
|
+
*
|
|
178
|
+
* Splits `str` on `delimiter` characters that fall outside any backtick
|
|
179
|
+
* span AND outside any parenthetical span. A backtick toggles an "inside a
|
|
180
|
+
* code span" flag; `(` / `)` track nesting depth. Delimiters seen while
|
|
181
|
+
* inside either construct are treated as literal content rather than split
|
|
182
|
+
* points. Used both for comma-splitting a completion-markers cell and for
|
|
183
|
+
* pipe-splitting a table row, so a marker annotation
|
|
184
|
+
* (`(unconsumed: draft presentation, approved interactively)`) or a path
|
|
185
|
+
* that happens to contain the delimiter inside backticks or parens is never
|
|
186
|
+
* torn in half.
|
|
187
|
+
*/
|
|
188
|
+
function splitOutsideDelimiter(str, delimiter) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
let cur = '';
|
|
191
|
+
let inBacktick = false;
|
|
192
|
+
let parenDepth = 0;
|
|
193
|
+
for (const ch of str) {
|
|
194
|
+
if (ch === '`') inBacktick = !inBacktick;
|
|
195
|
+
if (!inBacktick) {
|
|
196
|
+
if (ch === '(') parenDepth += 1;
|
|
197
|
+
else if (ch === ')' && parenDepth > 0) parenDepth -= 1;
|
|
198
|
+
}
|
|
199
|
+
if (ch === delimiter && !inBacktick && parenDepth === 0) {
|
|
200
|
+
parts.push(cur);
|
|
201
|
+
cur = '';
|
|
202
|
+
} else {
|
|
203
|
+
cur += ch;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
parts.push(cur);
|
|
207
|
+
return parts;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function splitTableRow(line) {
|
|
211
|
+
let s = line.trim();
|
|
212
|
+
if (s.startsWith('|')) s = s.slice(1);
|
|
213
|
+
if (s.endsWith('|')) s = s.slice(0, -1);
|
|
214
|
+
return splitOutsideDelimiter(s, '|').map(c => c.trim());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isSeparatorRow(line) {
|
|
218
|
+
const t = line.trim();
|
|
219
|
+
return t.length > 0 && /^[|:\- ]+$/.test(t) && t.includes('-');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* parseCompletionMarkersCell(value)
|
|
224
|
+
*
|
|
225
|
+
* Parses one `Completion Markers` table cell into `{markers, notes}`.
|
|
226
|
+
* Entries are comma-separated OUTSIDE backticks (so a marker's own text
|
|
227
|
+
* never leaks a split point). Each entry is stripped of a trailing
|
|
228
|
+
* parenthetical annotation (`(title case)`, `(non-standard)`, ...) — that
|
|
229
|
+
* annotation is captured into `notes` rather than the marker text — then
|
|
230
|
+
* stripped of surrounding backticks and a leading `## ` heading prefix. A
|
|
231
|
+
* bare `No marker` entry (post-annotation-strip) contributes nothing to
|
|
232
|
+
* `markers`; its parenthetical still lands in `notes`.
|
|
233
|
+
*/
|
|
234
|
+
function parseCompletionMarkersCell(value) {
|
|
235
|
+
const markers = [];
|
|
236
|
+
const notes = [];
|
|
237
|
+
const unconsumedMarkers = [];
|
|
238
|
+
if (!value) return { markers, notes, unconsumedMarkers };
|
|
239
|
+
const entries = splitOutsideDelimiter(value, ',');
|
|
240
|
+
for (const rawEntry of entries) {
|
|
241
|
+
const entry = rawEntry.trim();
|
|
242
|
+
if (entry === '') continue;
|
|
243
|
+
let markerPortion = entry;
|
|
244
|
+
let annotation = null;
|
|
245
|
+
const noteMatch = entry.match(/^(.*?)\s*\(([^)]*)\)\s*$/);
|
|
246
|
+
if (noteMatch) {
|
|
247
|
+
markerPortion = noteMatch[1].trim();
|
|
248
|
+
annotation = noteMatch[2].trim();
|
|
249
|
+
notes.push(annotation);
|
|
250
|
+
}
|
|
251
|
+
markerPortion = markerPortion.replace(/`/g, '').trim();
|
|
252
|
+
markerPortion = markerPortion.replace(/^#{1,6}\s+/, '').trim();
|
|
253
|
+
if (markerPortion === '' || markerPortion === 'No marker') continue;
|
|
254
|
+
// An `(unconsumed: <reason>)` annotation declares a marker that is
|
|
255
|
+
// emitted deliberately but matched by no consumer (e.g. a user-facing
|
|
256
|
+
// presentation format). It stays in the declared+emitted contract and in
|
|
257
|
+
// case-collision scope; only the consumer requirement is waived.
|
|
258
|
+
if (annotation !== null && /^unconsumed\b/i.test(annotation)) {
|
|
259
|
+
unconsumedMarkers.push(markerPortion);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
markers.push(markerPortion);
|
|
263
|
+
}
|
|
264
|
+
return { markers, notes, unconsumedMarkers };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* extractMarkers(content, knownMarkers)
|
|
269
|
+
*
|
|
270
|
+
* Scans a markdown string line by line for H1-H6 headings, and reports
|
|
271
|
+
* whether each heading sits inside a fenced code block. A heading counts as
|
|
272
|
+
* a marker only when its captured text EXACTLY matches (case-sensitively)
|
|
273
|
+
* one of `knownMarkers` — the declared vocabulary from the agent-contracts
|
|
274
|
+
* registry, supplied by the caller. There is deliberately no shape
|
|
275
|
+
* heuristic (e.g. "ends in COMPLETE") here: the registry already declares
|
|
276
|
+
* every marker string, several of which do NOT end in COMPLETE (`##
|
|
277
|
+
* ISSUES FOUND`, `## ROADMAP CREATED`, `## CHECKPOINT REACHED`, `##
|
|
278
|
+
* ESCALATE`, ...), so guessing the shape both over-matches ordinary prose
|
|
279
|
+
* headings that happen to look marker-shaped and under-matches every real
|
|
280
|
+
* marker that doesn't end in the word COMPLETE. When `knownMarkers` is
|
|
281
|
+
* omitted or empty, `markers` is always `[]` — this function never falls
|
|
282
|
+
* back to guessing.
|
|
283
|
+
*
|
|
284
|
+
* `candidates` separately reports in-fence headings that are NOT in
|
|
285
|
+
* `knownMarkers` but still look marker-shaped (`/^[A-Z][A-Z0-9 _:'\/-]*$/`,
|
|
286
|
+
* no backticks, <= 60 chars) — these feed the `emitted_marker_not_declared`
|
|
287
|
+
* check. They are reported as candidates, never silently treated as
|
|
288
|
+
* markers.
|
|
289
|
+
*
|
|
290
|
+
* Fence tracking is a single boolean, not a stack: a line whose trimmed
|
|
291
|
+
* form starts with a run of 3+ backticks or tildes toggles it, but only a
|
|
292
|
+
* closing run of the SAME character and AT LEAST the same length actually
|
|
293
|
+
* closes an open fence — a shorter or differently-charred run nested inside
|
|
294
|
+
* (e.g. a 3-backtick span shown inside a 4-backtick outer fence) is inert
|
|
295
|
+
* and leaves the outer fence open. `unclosedFence` reports when the file
|
|
296
|
+
* ends without ever closing the last-opened fence, so callers can tell a
|
|
297
|
+
* truncated/malformed file from one that's actually well-formed.
|
|
298
|
+
*/
|
|
299
|
+
function extractMarkers(content, knownMarkers) {
|
|
300
|
+
const markers = [];
|
|
301
|
+
const candidates = [];
|
|
302
|
+
if (content == null || typeof content !== 'string' || content.trim() === '') {
|
|
303
|
+
return { markers, candidates, unclosedFence: false };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const known = new Set(knownMarkers || []);
|
|
307
|
+
const lines = content.split(/\r?\n/);
|
|
308
|
+
const headingRe = /^(#{1,6})\s+(\S.*?)\s*$/;
|
|
309
|
+
const candidateShapeRe = /^[A-Z][A-Z0-9 _:'/-]*$/;
|
|
310
|
+
|
|
311
|
+
let inFence = false;
|
|
312
|
+
let fenceChar = null;
|
|
313
|
+
let fenceLen = 0;
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < lines.length; i++) {
|
|
316
|
+
const line = lines[i];
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
const stateForThisLine = inFence;
|
|
319
|
+
|
|
320
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
321
|
+
if (fenceMatch) {
|
|
322
|
+
const runChar = fenceMatch[1][0];
|
|
323
|
+
const runLen = fenceMatch[1].length;
|
|
324
|
+
if (!inFence) {
|
|
325
|
+
inFence = true;
|
|
326
|
+
fenceChar = runChar;
|
|
327
|
+
fenceLen = runLen;
|
|
328
|
+
} else if (runChar === fenceChar && runLen >= fenceLen) {
|
|
329
|
+
inFence = false;
|
|
330
|
+
fenceChar = null;
|
|
331
|
+
fenceLen = 0;
|
|
332
|
+
}
|
|
333
|
+
// else: nested/mismatched run — does not close the outer fence.
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const hm = line.match(headingRe);
|
|
337
|
+
if (hm) {
|
|
338
|
+
const text = hm[2].trim();
|
|
339
|
+
if (known.has(text)) {
|
|
340
|
+
markers.push({ marker: text, inFence: stateForThisLine, line: i + 1 });
|
|
341
|
+
} else if (stateForThisLine && text.length <= 60 && candidateShapeRe.test(text)) {
|
|
342
|
+
candidates.push({ marker: text, inFence: stateForThisLine, line: i + 1 });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { markers, candidates, unclosedFence: inFence };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* parseAgentContracts(markdown)
|
|
352
|
+
*
|
|
353
|
+
* Parses the `## Agent Registry` pipe table in
|
|
354
|
+
* gsd-core/references/agent-contracts.md into structured rows, keyed by
|
|
355
|
+
* lowercased, underscore-joined column headers (`Completion Markers` ->
|
|
356
|
+
* `completion_markers`). Column set is read from the header row itself, so
|
|
357
|
+
* the table can grow a `Consumed By` / `Kind` column later without this
|
|
358
|
+
* parser needing an update — rows simply gain the corresponding key.
|
|
359
|
+
*
|
|
360
|
+
* The `completion_markers` column gets special handling via
|
|
361
|
+
* parseCompletionMarkersCell: it is never a plain string on the returned
|
|
362
|
+
* row, always the parsed `{markers}` array (with any parenthetical
|
|
363
|
+
* annotation split out into a row-level `notes` array instead).
|
|
364
|
+
*
|
|
365
|
+
* Malformed rows (wrong cell count for the header, or an empty agent name)
|
|
366
|
+
* are reported in `errors` as `{line, reason}` and excluded from `rows`
|
|
367
|
+
* rather than thrown.
|
|
368
|
+
*/
|
|
369
|
+
function parseAgentContracts(markdown) {
|
|
370
|
+
const rows = [];
|
|
371
|
+
const errors = [];
|
|
372
|
+
if (markdown == null || typeof markdown !== 'string') return { rows, errors };
|
|
373
|
+
|
|
374
|
+
const lines = markdown.split(/\r?\n/);
|
|
375
|
+
const registryIdx = lines.findIndex(l => /^##\s+Agent Registry\s*$/.test(l.trim()));
|
|
376
|
+
if (registryIdx === -1) return { rows, errors };
|
|
377
|
+
|
|
378
|
+
let i = registryIdx + 1;
|
|
379
|
+
while (i < lines.length && lines[i].trim() === '') i++;
|
|
380
|
+
if (i >= lines.length || !lines[i].trim().startsWith('|')) return { rows, errors };
|
|
381
|
+
|
|
382
|
+
const columnKeys = splitTableRow(lines[i]).map(h =>
|
|
383
|
+
h.trim().toLowerCase().replace(/\s+/g, '_')
|
|
384
|
+
);
|
|
385
|
+
i++;
|
|
386
|
+
if (i < lines.length && isSeparatorRow(lines[i])) i++;
|
|
387
|
+
|
|
388
|
+
for (; i < lines.length; i++) {
|
|
389
|
+
const raw = lines[i];
|
|
390
|
+
if (!raw.trim().startsWith('|')) break;
|
|
391
|
+
|
|
392
|
+
const lineNum = i + 1;
|
|
393
|
+
const cells = splitTableRow(raw);
|
|
394
|
+
if (cells.length !== columnKeys.length) {
|
|
395
|
+
errors.push({
|
|
396
|
+
line: lineNum,
|
|
397
|
+
reason: `column count mismatch: expected ${columnKeys.length}, got ${cells.length}`,
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const row = {};
|
|
403
|
+
for (let j = 0; j < columnKeys.length; j++) {
|
|
404
|
+
const key = columnKeys[j];
|
|
405
|
+
const value = cells[j];
|
|
406
|
+
if (key === 'completion_markers') {
|
|
407
|
+
const parsed = parseCompletionMarkersCell(value);
|
|
408
|
+
row.completion_markers = parsed.markers;
|
|
409
|
+
row.notes = parsed.notes;
|
|
410
|
+
row.unconsumed_markers = parsed.unconsumedMarkers;
|
|
411
|
+
} else {
|
|
412
|
+
row[key] = value;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!row.agent) {
|
|
417
|
+
errors.push({ line: lineNum, reason: 'empty agent name' });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
rows.push(row);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { rows, errors };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const CONTRACT_KINDS = new Set(['sentinel-match', 'artifact+query', 'structured-return']);
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* contractViolations({registry, producerMarkers, candidateMarkers, consumerTexts})
|
|
431
|
+
*
|
|
432
|
+
* Cross-checks the agent-contracts registry (from parseAgentContracts)
|
|
433
|
+
* against what agent files actually emit in-fence (from extractMarkers) and
|
|
434
|
+
* what every workflow/command/agent file actually consumes, exact-case.
|
|
435
|
+
* Rows are evaluated independently; `registry`, `producerMarkers`,
|
|
436
|
+
* `candidateMarkers`, and `consumerTexts` default to empty when omitted so a
|
|
437
|
+
* partial call never throws.
|
|
438
|
+
*
|
|
439
|
+
* `emitted_marker_not_declared` is driven by `candidateMarkers` (agent ->
|
|
440
|
+
* [in-fence heading text]) rather than `producerMarkers` -- a heading only
|
|
441
|
+
* lands in `producerMarkers` once it already matches a KNOWN registry
|
|
442
|
+
* marker (see extractMarkers), so it can never be "emitted but undeclared";
|
|
443
|
+
* `candidateMarkers` is exactly the set of marker-shaped in-fence headings
|
|
444
|
+
* extractMarkers could NOT resolve against the known vocabulary.
|
|
445
|
+
*
|
|
446
|
+
* A row declaring `kind: 'artifact+query'` or `'structured-return'` opts
|
|
447
|
+
* out of the sentinel-marker contract entirely — its only possible
|
|
448
|
+
* violation is `vestigial_marker` (it still emits an in-fence marker
|
|
449
|
+
* despite the row saying completion isn't detected that way). Every other
|
|
450
|
+
* row (kind `sentinel-match`, or the column simply absent/empty) is held to
|
|
451
|
+
* the full contract: declared markers must be emitted, emitted markers must
|
|
452
|
+
* be declared, and — for sentinel-match rows specifically — every declared
|
|
453
|
+
* marker must have at least one EXACT-CASE consumer somewhere in
|
|
454
|
+
* `consumerTexts`, excluding the producing agent's own file (an agent can
|
|
455
|
+
* never satisfy its own marker, even when the same map entry also serves as
|
|
456
|
+
* a consumer of some OTHER agent's marker). A case-insensitive-only match
|
|
457
|
+
* is reported (`case_only_match`), never treated as satisfying the
|
|
458
|
+
* contract. `case_collision` is computed once across the whole registry,
|
|
459
|
+
* independent of kind, since two markers differing only by case is a
|
|
460
|
+
* registry-wide authoring hazard.
|
|
461
|
+
*/
|
|
462
|
+
function contractViolations({ registry, producerMarkers, candidateMarkers, consumerTexts } = {}) {
|
|
463
|
+
const violations = [];
|
|
464
|
+
try {
|
|
465
|
+
const rows = Array.isArray(registry) ? registry : [];
|
|
466
|
+
const producers = producerMarkers instanceof Map ? producerMarkers : new Map();
|
|
467
|
+
const candidatesByAgent = candidateMarkers instanceof Map ? candidateMarkers : new Map();
|
|
468
|
+
const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
|
|
469
|
+
|
|
470
|
+
// case_collision — global, across every declared marker in the registry,
|
|
471
|
+
// including `(unconsumed: …)` entries: a case-variant of an unconsumed
|
|
472
|
+
// marker is still an authoring hazard even though no consumer matches it.
|
|
473
|
+
const byLower = new Map();
|
|
474
|
+
for (const row of rows) {
|
|
475
|
+
for (const m of [...(row.completion_markers || []), ...(row.unconsumed_markers || [])]) {
|
|
476
|
+
const lower = m.toLowerCase();
|
|
477
|
+
if (!byLower.has(lower)) byLower.set(lower, new Set());
|
|
478
|
+
byLower.get(lower).add(m);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (const row of rows) {
|
|
482
|
+
for (const m of [...(row.completion_markers || []), ...(row.unconsumed_markers || [])]) {
|
|
483
|
+
const variants = byLower.get(m.toLowerCase());
|
|
484
|
+
if (variants && variants.size > 1) {
|
|
485
|
+
const others = [...variants].filter(v => v !== m);
|
|
486
|
+
violations.push({
|
|
487
|
+
kind: 'case_collision',
|
|
488
|
+
agent: row.agent,
|
|
489
|
+
marker: m,
|
|
490
|
+
detail: `case-insensitive collision with: ${others.join(', ')}`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// roster coverage — the registry is the declaration of record for every
|
|
497
|
+
// agents/*.md file: an agent with no row, two rows for one agent, or a
|
|
498
|
+
// row naming an agent that does not exist are all contract drift. The
|
|
499
|
+
// roster is the producer map's key set; guards on map size keep empty
|
|
500
|
+
// inputs inert rather than spuriously "missing".
|
|
501
|
+
const seenAgents = new Set();
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
const agent = row.agent;
|
|
504
|
+
if (seenAgents.has(agent)) {
|
|
505
|
+
violations.push({
|
|
506
|
+
kind: 'duplicate_registry_row',
|
|
507
|
+
agent,
|
|
508
|
+
marker: null,
|
|
509
|
+
detail: `agent "${agent}" has more than one registry row`,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
seenAgents.add(agent);
|
|
513
|
+
if (producers.size > 0 && !producers.has(agent)) {
|
|
514
|
+
violations.push({
|
|
515
|
+
kind: 'unknown_producer',
|
|
516
|
+
agent,
|
|
517
|
+
marker: null,
|
|
518
|
+
detail: `registry row for "${agent}" but no agents/${agent}.md exists`,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const agent of producers.keys()) {
|
|
523
|
+
if (!seenAgents.has(agent)) {
|
|
524
|
+
violations.push({
|
|
525
|
+
kind: 'agent_without_contract',
|
|
526
|
+
agent,
|
|
527
|
+
marker: null,
|
|
528
|
+
detail: `agents/${agent}.md exists but has no registry row in agent-contracts.md`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const row of rows) {
|
|
534
|
+
const agent = row.agent;
|
|
535
|
+
const declared = row.completion_markers || [];
|
|
536
|
+
const unconsumed = row.unconsumed_markers || [];
|
|
537
|
+
const declaredAll = [...declared, ...unconsumed];
|
|
538
|
+
// Deduped: the same marker emitted on several lines/files is one
|
|
539
|
+
// emission fact, never several violations.
|
|
540
|
+
const produced = [...new Set(producers.get(agent) || [])];
|
|
541
|
+
const kind = row.kind;
|
|
542
|
+
const kindProvided = kind !== undefined && kind !== null && kind !== '';
|
|
543
|
+
|
|
544
|
+
// A row naming a nonexistent agent file: unknown_producer (above) is
|
|
545
|
+
// the violation; per-marker noise on top of it hides nothing.
|
|
546
|
+
if (producers.size > 0 && !producers.has(agent)) continue;
|
|
547
|
+
|
|
548
|
+
// unknown_consumer — the `Consumed by` column feeds readTagViolations,
|
|
549
|
+
// so a file-shaped entry that resolves to nothing would make that arm a
|
|
550
|
+
// lint over fiction. Only checked when a consumer corpus was supplied.
|
|
551
|
+
for (const p of parseConsumedByCell(row.consumed_by)) {
|
|
552
|
+
if (consumers.size > 0 && !consumers.has(p)) {
|
|
553
|
+
violations.push({
|
|
554
|
+
kind: 'unknown_consumer',
|
|
555
|
+
agent,
|
|
556
|
+
marker: null,
|
|
557
|
+
detail: `Consumed by names "${p}" but no such file exists`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (kindProvided && !CONTRACT_KINDS.has(kind)) {
|
|
563
|
+
violations.push({
|
|
564
|
+
kind: 'unknown_kind',
|
|
565
|
+
agent,
|
|
566
|
+
marker: null,
|
|
567
|
+
detail: `row declares unknown kind "${kind}"`,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const isArtifactOrStructured =
|
|
572
|
+
kindProvided && (kind === 'artifact+query' || kind === 'structured-return');
|
|
573
|
+
|
|
574
|
+
// declaredAll includes unconsumed entries: an `(unconsumed: …)` marker
|
|
575
|
+
// must still be emitted — the exemption waives the consumer
|
|
576
|
+
// requirement only, never the declared↔emitted agreement (Marker
|
|
577
|
+
// Rule 7). This runs for EVERY row kind.
|
|
578
|
+
for (const m of declaredAll) {
|
|
579
|
+
if (!produced.includes(m)) {
|
|
580
|
+
violations.push({
|
|
581
|
+
kind: 'declared_marker_not_emitted',
|
|
582
|
+
agent,
|
|
583
|
+
marker: m,
|
|
584
|
+
detail: `declared in registry but not emitted in-fence by ${agent}`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (isArtifactOrStructured) {
|
|
590
|
+
// An `(unconsumed: …)` annotation on an artifact/structured row is a
|
|
591
|
+
// recorded decision to keep emitting a marker the completion route
|
|
592
|
+
// doesn't need (Marker Rule 2's intentional title-case markers) —
|
|
593
|
+
// exempt from vestigial_marker. Every OTHER emitted marker on such a
|
|
594
|
+
// row is reported ONCE, as vestigial_marker: the candidate check
|
|
595
|
+
// below is skipped for these rows because emitted_marker_not_declared
|
|
596
|
+
// on top of vestigial_marker double-reports the same fact, and the
|
|
597
|
+
// consumer contract cannot apply to a row whose completion is not
|
|
598
|
+
// detected by marker matching at all.
|
|
599
|
+
const unconsumedSet = new Set(unconsumed);
|
|
600
|
+
for (const m of produced) {
|
|
601
|
+
if (unconsumedSet.has(m)) continue;
|
|
602
|
+
violations.push({
|
|
603
|
+
kind: 'vestigial_marker',
|
|
604
|
+
agent,
|
|
605
|
+
marker: m,
|
|
606
|
+
detail: `row kind "${kind}" expects no marker, but agent still emits "${m}" in-fence`,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Deduped per marker: "emitted but undeclared" is a fact about the
|
|
613
|
+
// marker, and a caller-supplied list carrying the same marker twice
|
|
614
|
+
// (two template lines, or agent file + @-included reference) is one
|
|
615
|
+
// violation, not two.
|
|
616
|
+
const candidates = [...new Set(candidatesByAgent.get(agent) || [])];
|
|
617
|
+
for (const m of candidates) {
|
|
618
|
+
if (!declaredAll.includes(m)) {
|
|
619
|
+
violations.push({
|
|
620
|
+
kind: 'emitted_marker_not_declared',
|
|
621
|
+
agent,
|
|
622
|
+
marker: m,
|
|
623
|
+
detail: `emitted in-fence by ${agent} but absent from its registry row`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const isSentinelMatchKind = !kindProvided || kind === 'sentinel-match';
|
|
629
|
+
if (isSentinelMatchKind) {
|
|
630
|
+
// Only markers the agent actually emits participate in the consumer
|
|
631
|
+
// contract — a declared_marker_not_emitted finding already covers a
|
|
632
|
+
// phantom declaration, and stacking no_consumer on top of it is
|
|
633
|
+
// double-reporting the same dead row.
|
|
634
|
+
const declaredConsumerPaths = parseConsumedByCell(row.consumed_by);
|
|
635
|
+
for (const m of declared) {
|
|
636
|
+
if (!produced.includes(m)) continue;
|
|
637
|
+
let exactMatch = false;
|
|
638
|
+
let caseInsensitiveMatch = false;
|
|
639
|
+
let declaredConsumerMatch = false;
|
|
640
|
+
for (const [consumerId, text] of consumers.entries()) {
|
|
641
|
+
// consumerTexts is keyed by repo-relative path — an agent's own
|
|
642
|
+
// file is 'agents/<name>.md'. The producer's own template can
|
|
643
|
+
// never satisfy its marker.
|
|
644
|
+
if (consumerId === `agents/${agent}.md`) continue;
|
|
645
|
+
if (typeof text !== 'string') continue;
|
|
646
|
+
if (text.includes(m)) {
|
|
647
|
+
exactMatch = true;
|
|
648
|
+
if (declaredConsumerPaths.includes(consumerId)) declaredConsumerMatch = true;
|
|
649
|
+
} else if (text.toLowerCase().includes(m.toLowerCase())) {
|
|
650
|
+
caseInsensitiveMatch = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!exactMatch) {
|
|
654
|
+
if (caseInsensitiveMatch) {
|
|
655
|
+
violations.push({
|
|
656
|
+
kind: 'case_only_match',
|
|
657
|
+
agent,
|
|
658
|
+
marker: m,
|
|
659
|
+
detail: `no exact-case consumer for "${m}"; case-insensitive match found only`,
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
violations.push({
|
|
663
|
+
kind: 'no_consumer',
|
|
664
|
+
agent,
|
|
665
|
+
marker: m,
|
|
666
|
+
detail: `no consumer text contains "${m}" (exact case)`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
} else if (
|
|
670
|
+
declaredConsumerPaths.length > 0 &&
|
|
671
|
+
!declaredConsumerMatch
|
|
672
|
+
) {
|
|
673
|
+
// The marker IS consumed somewhere, but by none of the consumers
|
|
674
|
+
// the row cites — the `Consumed by` cell is documentation the
|
|
675
|
+
// read-tag arm and humans both navigate by, so a cell naming
|
|
676
|
+
// files that never match is registry drift even when the corpus
|
|
677
|
+
// at large happens to contain the token.
|
|
678
|
+
violations.push({
|
|
679
|
+
kind: 'declared_consumer_no_match',
|
|
680
|
+
agent,
|
|
681
|
+
marker: m,
|
|
682
|
+
detail: `consumed somewhere, but none of the row's Consumed by entries ( ${declaredConsumerPaths.join(', ')} ) contain "${m}"`,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} catch (e) {
|
|
689
|
+
// A drift gate must fail RED, never green: an internal error here must
|
|
690
|
+
// surface, not be swallowed into '0 violations'.
|
|
691
|
+
e.message = `contractViolations (internal failure — failing red): ${e.message}`;
|
|
692
|
+
throw e;
|
|
693
|
+
}
|
|
694
|
+
return violations;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* parseConsumedByCell(value)
|
|
699
|
+
*
|
|
700
|
+
* Extracts every file-shaped path from a `Consumed by` table cell. Cell
|
|
701
|
+
* entries are comma-separated outside backticks, and an entry may carry
|
|
702
|
+
* several backtick-wrapped tokens (a path, a command, a marker string) in
|
|
703
|
+
* prose — so every backtick span is inspected and only tokens that look like
|
|
704
|
+
* repo-relative markdown paths (`^[\w][\w./-]*\.md$` — no spaces, no glob
|
|
705
|
+
* stars) are returned. A glob (`*-VERIFICATION.md`) or a command
|
|
706
|
+
* (`gsd_run query verification.status`) describes the consumption mechanism
|
|
707
|
+
* in prose; it is not a file the check can open, and is ignored.
|
|
708
|
+
*/
|
|
709
|
+
function parseConsumedByCell(value) {
|
|
710
|
+
if (!value || typeof value !== 'string') return [];
|
|
711
|
+
const paths = [];
|
|
712
|
+
const re = /`([^`]+)`/g;
|
|
713
|
+
let m;
|
|
714
|
+
while ((m = re.exec(value)) !== null) {
|
|
715
|
+
const token = m[1].trim();
|
|
716
|
+
if (!/^[\w][\w./-]*\.md$/.test(token) || token.includes('*')) continue;
|
|
717
|
+
// Traversal segments are dropped, never surfaced — the same rule
|
|
718
|
+
// workflowPathRefs applies to its refs: this resolver only ever reports
|
|
719
|
+
// paths inside the repo, never something a `..` could walk outside of
|
|
720
|
+
// (the driver reads these paths under --root, so an interior `..` would
|
|
721
|
+
// be an out-of-root file read).
|
|
722
|
+
if (token.split('/').includes('..')) continue;
|
|
723
|
+
paths.push(token);
|
|
724
|
+
}
|
|
725
|
+
return paths;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* readTagViolations({registry, agentTexts, consumerTexts})
|
|
730
|
+
*
|
|
731
|
+
* The read-tag arm — F8 one layer up. Two invariants:
|
|
732
|
+
*
|
|
733
|
+
* legacy_read_tag — no `<files_to_read>` may survive anywhere in the
|
|
734
|
+
* consumer corpus (workflows/, commands/, agents/). #3423 standardized on
|
|
735
|
+
* `<required_reading>` and retired the old vocabulary via a test; this
|
|
736
|
+
* folds that assertion into the gate itself.
|
|
737
|
+
*
|
|
738
|
+
* read_tag_gate_missing — for every registry row, every file-shaped
|
|
739
|
+
* `Consumed by` consumer that itself emits `<required_reading>` requires
|
|
740
|
+
* the producing agent's file to reference `<required_reading>`. A spawner
|
|
741
|
+
* that sends a required-reading block to an agent whose instructions never
|
|
742
|
+
* mention the gate is exactly the F8 shape: the emit side is correct, the
|
|
743
|
+
* gate side never fires — and #3423's per-agent test cannot see it,
|
|
744
|
+
* because an agent carrying no reading instruction at all is skipped by
|
|
745
|
+
* it. The registry supplies the producer↔consumer pair, so this check
|
|
746
|
+
* needs no spawn-site parsing heuristics.
|
|
747
|
+
*/
|
|
748
|
+
function readTagViolations({ registry, agentTexts, consumerTexts } = {}) {
|
|
749
|
+
const violations = [];
|
|
750
|
+
try {
|
|
751
|
+
const rows = Array.isArray(registry) ? registry : [];
|
|
752
|
+
const agents = agentTexts instanceof Map ? agentTexts : new Map();
|
|
753
|
+
const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
|
|
754
|
+
|
|
755
|
+
for (const [file, text] of consumers.entries()) {
|
|
756
|
+
if (typeof text === 'string' && text.includes('<files_to_read>')) {
|
|
757
|
+
violations.push({
|
|
758
|
+
kind: 'legacy_read_tag',
|
|
759
|
+
agent: null,
|
|
760
|
+
marker: null,
|
|
761
|
+
detail: `${file} still contains the retired <files_to_read> tag`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
for (const row of rows) {
|
|
767
|
+
const agent = row.agent;
|
|
768
|
+
const agentText = agents.get(agent);
|
|
769
|
+
if (typeof agentText !== 'string') continue; // unknown_producer reports this
|
|
770
|
+
if (agentText.includes('<required_reading>')) continue;
|
|
771
|
+
for (const p of parseConsumedByCell(row.consumed_by)) {
|
|
772
|
+
const text = consumers.get(p);
|
|
773
|
+
if (typeof text === 'string' && text.includes('<required_reading>')) {
|
|
774
|
+
violations.push({
|
|
775
|
+
kind: 'read_tag_gate_missing',
|
|
776
|
+
agent,
|
|
777
|
+
marker: null,
|
|
778
|
+
detail: `consumer ${p} emits <required_reading> but agents/${agent}.md never references the gate`,
|
|
779
|
+
});
|
|
780
|
+
break; // one violation per agent is the fact; more is noise
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
// A drift gate must fail RED, never green: an internal error here must
|
|
786
|
+
// surface, not be swallowed into '0 violations'.
|
|
787
|
+
e.message = `readTagViolations (internal failure — failing red): ${e.message}`;
|
|
788
|
+
throw e;
|
|
789
|
+
}
|
|
790
|
+
return violations;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Tokens that look like completion markers but are NOT agent contracts, and
|
|
795
|
+
* therefore exempt from the reverse-direction check. Each entry carries its
|
|
796
|
+
* justification inline; a test locks the set's membership so it cannot grow
|
|
797
|
+
* silently.
|
|
798
|
+
*/
|
|
799
|
+
const NON_AGENT_TOKENS = new Set([
|
|
800
|
+
// Display headings the /gsd:graphify command itself renders from
|
|
801
|
+
// `gsd_run graphify build` CLI output — tool output shown to the user,
|
|
802
|
+
// not an agent return any spawner dispatches on.
|
|
803
|
+
'GRAPHIFY BUILD COMPLETE',
|
|
804
|
+
'GRAPHIFY BUILD FAILED',
|
|
805
|
+
]);
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* unmatchedConsumerTokens({consumerTexts, vocabulary})
|
|
809
|
+
*
|
|
810
|
+
* The reverse direction of the registry contract: a consumer that matches a
|
|
811
|
+
* `## TOKEN` string no producer emits is dispatch-on-phantom — F9's shape
|
|
812
|
+
* (a sanctioned return that no dispatch branch ever keyed on), seen from
|
|
813
|
+
* the consumer side. Scans ONLY workflow/command files (the dispatch
|
|
814
|
+
* surfaces); `agents/` is excluded because an agent mentioning a token in
|
|
815
|
+
* its own file is a self-description, not a spawner's match instruction —
|
|
816
|
+
* the forward direction already validates agent-to-agent consumption.
|
|
817
|
+
*
|
|
818
|
+
* A token is extracted when it appears quoted (`## X`, "## X", '## X') —
|
|
819
|
+
* the shape a match/dispatch instruction actually writes — is ALL-CAPS
|
|
820
|
+
* marker-shaped (the same candidate shape extractMarkers uses, ≤60 chars),
|
|
821
|
+
* is absent from the declared vocabulary, and is not exempt via
|
|
822
|
+
* NON_AGENT_TOKENS.
|
|
823
|
+
*/
|
|
824
|
+
function unmatchedConsumerTokens({ consumerTexts, vocabulary } = {}) {
|
|
825
|
+
const violations = [];
|
|
826
|
+
try {
|
|
827
|
+
const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
|
|
828
|
+
const known = vocabulary instanceof Set ? vocabulary : new Set(knownMarkersArray(vocabulary));
|
|
829
|
+
const shapeRe = /^[A-Z][A-Z0-9 _:'/-]*$/;
|
|
830
|
+
const tokenRe = /[`'"]## ([^`'"\n]+)[`'"]/g;
|
|
831
|
+
|
|
832
|
+
const reported = new Set();
|
|
833
|
+
for (const [file, text] of consumers.entries()) {
|
|
834
|
+
if (!file.startsWith('gsd-core/workflows/') && !file.startsWith('commands/')) continue;
|
|
835
|
+
if (typeof text !== 'string') continue;
|
|
836
|
+
tokenRe.lastIndex = 0;
|
|
837
|
+
let m;
|
|
838
|
+
while ((m = tokenRe.exec(text)) !== null) {
|
|
839
|
+
const token = m[1].trim();
|
|
840
|
+
if (known.has(token)) continue;
|
|
841
|
+
if (NON_AGENT_TOKENS.has(token)) continue;
|
|
842
|
+
if (token.length > 60 || !shapeRe.test(token)) continue;
|
|
843
|
+
const key = `${file}\0${token}`;
|
|
844
|
+
if (reported.has(key)) continue;
|
|
845
|
+
reported.add(key);
|
|
846
|
+
violations.push({
|
|
847
|
+
kind: 'unmatched_consumer_token',
|
|
848
|
+
agent: null,
|
|
849
|
+
marker: token,
|
|
850
|
+
detail: `${file} matches \`## ${token}\` but no agent declares or emits it`,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
} catch (e) {
|
|
855
|
+
// A drift gate must fail RED, never green: an internal error here must
|
|
856
|
+
// surface, not be swallowed into '0 violations'.
|
|
857
|
+
e.message = `unmatchedConsumerTokens (internal failure — failing red): ${e.message}`;
|
|
858
|
+
throw e;
|
|
859
|
+
}
|
|
860
|
+
return violations;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function knownMarkersArray(vocabulary) {
|
|
864
|
+
// vocabulary may arrive as an array (tests) — normalize; the Set branch
|
|
865
|
+
// above handles the driver's Set.
|
|
866
|
+
return Array.isArray(vocabulary) ? vocabulary : [];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* The frozen violation-kind vocabulary — one entry per finding the check
|
|
871
|
+
* can emit. Tests lock this set and the driver's REMEDIES parity, so a new
|
|
872
|
+
* kind is three coordinated changes: enum + remedy + test, the same
|
|
873
|
+
* discipline verify-reapply-patches' REASON enum follows.
|
|
874
|
+
*/
|
|
875
|
+
const VIOLATION_KINDS = Object.freeze({
|
|
876
|
+
PARSE_ERROR: 'parse_error',
|
|
877
|
+
UNCLOSED_FENCE: 'unclosed_fence',
|
|
878
|
+
DECLARED_MARKER_NOT_EMITTED: 'declared_marker_not_emitted',
|
|
879
|
+
EMITTED_MARKER_NOT_DECLARED: 'emitted_marker_not_declared',
|
|
880
|
+
CASE_COLLISION: 'case_collision',
|
|
881
|
+
CASE_ONLY_MATCH: 'case_only_match',
|
|
882
|
+
NO_CONSUMER: 'no_consumer',
|
|
883
|
+
DECLARED_CONSUMER_NO_MATCH: 'declared_consumer_no_match',
|
|
884
|
+
UNKNOWN_KIND: 'unknown_kind',
|
|
885
|
+
VESTIGIAL_MARKER: 'vestigial_marker',
|
|
886
|
+
AGENT_WITHOUT_CONTRACT: 'agent_without_contract',
|
|
887
|
+
DUPLICATE_REGISTRY_ROW: 'duplicate_registry_row',
|
|
888
|
+
UNKNOWN_PRODUCER: 'unknown_producer',
|
|
889
|
+
UNKNOWN_CONSUMER: 'unknown_consumer',
|
|
890
|
+
LEGACY_READ_TAG: 'legacy_read_tag',
|
|
891
|
+
READ_TAG_GATE_MISSING: 'read_tag_gate_missing',
|
|
892
|
+
UNMATCHED_CONSUMER_TOKEN: 'unmatched_consumer_token',
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* The remedy shown per violation kind — one entry per VIOLATION_KINDS value.
|
|
897
|
+
* The parity test locks REMEDIES keys against VIOLATION_KINDS values, so a
|
|
898
|
+
* new kind cannot ship without its remedy.
|
|
899
|
+
*/
|
|
900
|
+
const REMEDIES = Object.freeze({
|
|
901
|
+
parse_error: 'fix the malformed row in gsd-core/references/agent-contracts.md',
|
|
902
|
+
unclosed_fence: 'close the unterminated code fence in this agent file',
|
|
903
|
+
declared_marker_not_emitted:
|
|
904
|
+
'remove the marker from the registry row, or emit it as an in-fence example in the agent file',
|
|
905
|
+
emitted_marker_not_declared:
|
|
906
|
+
"add this marker to the agent's Completion Markers cell in the registry table",
|
|
907
|
+
case_collision: 'rename one of the colliding markers so they no longer differ only by case',
|
|
908
|
+
case_only_match:
|
|
909
|
+
'fix the consumer file to match this marker exact-case, or update the marker/registry to match the consumer',
|
|
910
|
+
no_consumer:
|
|
911
|
+
'add an exact-case consumer for this marker, or reclassify the row Kind to artifact+query/structured-return',
|
|
912
|
+
declared_consumer_no_match:
|
|
913
|
+
"the marker is consumed, but not by any file the row's Consumed by cell names — fix the cell to name a real consumer",
|
|
914
|
+
unknown_kind: 'set the Kind column to one of sentinel-match, artifact+query, structured-return',
|
|
915
|
+
vestigial_marker:
|
|
916
|
+
"remove this marker heading from the agent file, since the row's Kind says no marker is expected",
|
|
917
|
+
agent_without_contract: 'add a registry row for this agent in agent-contracts.md',
|
|
918
|
+
duplicate_registry_row: 'merge the two rows for this agent into one',
|
|
919
|
+
unknown_producer: 'remove the row, or create the agents/<name>.md file it names',
|
|
920
|
+
unknown_consumer:
|
|
921
|
+
'fix the path in the Consumed by cell, or drop it — only real files the check can open belong there',
|
|
922
|
+
legacy_read_tag:
|
|
923
|
+
'replace <files_to_read> with <required_reading> (#3423 standardized on the gate tag)',
|
|
924
|
+
read_tag_gate_missing:
|
|
925
|
+
"add the <required_reading> MUST-Read gate clause to this agent's instructions (see docs/AGENTS.md)",
|
|
926
|
+
unmatched_consumer_token:
|
|
927
|
+
'declare the producing agent and marker in the registry, or remove the dispatch — nothing emits this token',
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* sanitizeEcho(text)
|
|
932
|
+
*
|
|
933
|
+
* File-derived strings (registry cells, marker tokens, workflow text) are
|
|
934
|
+
* echoed into lint output that AI agents read as trusted instructions —
|
|
935
|
+
* every interpolated field is control-char-stripped and length-capped
|
|
936
|
+
* before it is written, so a malicious repo file cannot make the lint emit
|
|
937
|
+
* instruction-shaped text. Implemented as a code-point filter rather than a
|
|
938
|
+
* regex so no control-character regex literal exists to lint.
|
|
939
|
+
*/
|
|
940
|
+
function sanitizeEcho(text) {
|
|
941
|
+
let out = '';
|
|
942
|
+
for (const ch of String(text)) {
|
|
943
|
+
const code = ch.codePointAt(0);
|
|
944
|
+
if (code <= 0x1f || code === 0x7f) continue;
|
|
945
|
+
out += ch;
|
|
946
|
+
}
|
|
947
|
+
return out.length > 200 ? out.slice(0, 200) : out;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
module.exports = {
|
|
951
|
+
CANONICAL_TOOLS,
|
|
952
|
+
parseFrontmatter,
|
|
953
|
+
executionContextRefs,
|
|
954
|
+
workflowPathRefs,
|
|
955
|
+
unreachableWorkflows,
|
|
956
|
+
extractMarkers,
|
|
957
|
+
parseAgentContracts,
|
|
958
|
+
contractViolations,
|
|
959
|
+
parseConsumedByCell,
|
|
960
|
+
readTagViolations,
|
|
961
|
+
unmatchedConsumerTokens,
|
|
962
|
+
NON_AGENT_TOKENS,
|
|
963
|
+
VIOLATION_KINDS,
|
|
964
|
+
REMEDIES,
|
|
965
|
+
sanitizeEcho,
|
|
966
|
+
};
|