session-orchestrator 5.1.0 → 5.3.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/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +6 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +7 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +8 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +21 -0
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +7 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +7 -2
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +7 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +7 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +7 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +8 -3
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +22 -0
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +7 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +20 -0
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +6 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +6 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +8 -2
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +21 -0
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +6 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +22 -0
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +6 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +6 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +7 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +21 -0
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +7 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +13 -4
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +197 -0
- package/README.md +11 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +17 -3
- package/docs/baseline.md +12 -6
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +15 -3
- package/docs/components.md +13 -6
- package/docs/events-schema.md +59 -9
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +135 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +106 -11
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +125 -8
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +72 -25
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +211 -23
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +6 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +18 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +119 -28
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +385 -5
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +2 -2
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +171 -15
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +77 -32
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +220 -11
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +408 -33
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +76 -13
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +332 -50
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +122 -91
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +429 -7
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +91 -12
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
- package/scripts/lib/user-invocable-skills.mjs +205 -0
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +329 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +35 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
- package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
- package/scripts/lib/validate/check-unwired-features.mjs +220 -33
- package/scripts/lib/validate/check-validator-registration.mjs +36 -12
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +39 -12
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +109 -39
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +227 -0
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +11 -4
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +80 -11
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +52 -23
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +9 -2
- package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +2 -2
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +21 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +18 -6
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +20 -2
- package/skills/wave-executor/SKILL.md +14 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -37,12 +37,16 @@
|
|
|
37
37
|
* mistaken import away from filing issues nobody asked for.
|
|
38
38
|
*
|
|
39
39
|
* Exports:
|
|
40
|
-
* UX_GRILL_LABELS, buildIssueTitle,
|
|
40
|
+
* UX_GRILL_LABELS, buildIssueTitle, buildUxGrillIssueBody, reconcileFindings
|
|
41
|
+
* buildIssueBody — @deprecated alias of buildUxGrillIssueBody (5.2.0 name),
|
|
42
|
+
* removed in 6.0.0
|
|
41
43
|
*/
|
|
42
44
|
|
|
43
45
|
import {
|
|
44
46
|
createFinding as defaultCreateFinding,
|
|
47
|
+
fingerprintSentinelLine,
|
|
45
48
|
listExistingFindings as defaultListExisting,
|
|
49
|
+
neutralizeFingerprintSentinel,
|
|
46
50
|
triageDecision,
|
|
47
51
|
updateFinding as defaultUpdateFinding,
|
|
48
52
|
} from '../test-runner/issue-reconcile.mjs';
|
|
@@ -69,31 +73,22 @@ const TITLE_MAX_LENGTH = 100;
|
|
|
69
73
|
const AUTO_SEVERITIES = Object.freeze(['critical', 'high']);
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Neutralise
|
|
76
|
+
* Neutralise fingerprint-sentinel literals (#388 SEC-IR-MED-1) and strip the
|
|
77
|
+
* characters `createFinding` rejects at the argv boundary, so a multi-line page
|
|
78
|
+
* title in a finding message cannot turn into a VALIDATION failure for the whole
|
|
79
|
+
* reconcile pass.
|
|
73
80
|
*
|
|
74
|
-
* `issue-reconcile.mjs`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
81
|
+
* The neutralisation is the SHARED one from `issue-reconcile.mjs` (#1339 P6): a
|
|
82
|
+
* body built HERE and handed to `createFinding` is not sanitised by anything
|
|
83
|
+
* else, so a finding `message` echoing page text could otherwise forge the
|
|
84
|
+
* authoritative `**Fingerprint:**` line and make the next run's
|
|
78
85
|
* `triageDecision` dedup against the wrong issue.
|
|
79
86
|
*
|
|
80
87
|
* @param {unknown} text
|
|
81
88
|
* @returns {string}
|
|
82
89
|
*/
|
|
83
|
-
function sanitizeSentinel(text) {
|
|
84
|
-
return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Strip the characters `createFinding` rejects at the argv boundary, so a
|
|
89
|
-
* multi-line page title in a finding message cannot turn into a VALIDATION
|
|
90
|
-
* failure for the whole reconcile pass.
|
|
91
|
-
*
|
|
92
|
-
* @param {unknown} text
|
|
93
|
-
* @returns {string}
|
|
94
|
-
*/
|
|
95
90
|
function oneLine(text) {
|
|
96
|
-
return
|
|
91
|
+
return neutralizeFingerprintSentinel(text).replace(/[\n\r\0]+/g, ' ').trim();
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
/**
|
|
@@ -144,7 +139,7 @@ export function buildIssueTitle(finding) {
|
|
|
144
139
|
* @param {string} [context.rubricHash]
|
|
145
140
|
* @returns {string}
|
|
146
141
|
*/
|
|
147
|
-
export function
|
|
142
|
+
export function buildUxGrillIssueBody(finding, { runId, rubricHash } = {}) {
|
|
148
143
|
const evidence = finding?.evidence ?? {};
|
|
149
144
|
const evidenceLines = Object.entries(evidence)
|
|
150
145
|
.filter(([, value]) => typeof value === 'string' || Array.isArray(value))
|
|
@@ -153,7 +148,7 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
|
|
|
153
148
|
return [
|
|
154
149
|
oneLine(finding?.message) || `${oneLine(finding?.checkId)} at ${oneLine(finding?.locator)}`,
|
|
155
150
|
'',
|
|
156
|
-
|
|
151
|
+
fingerprintSentinelLine(oneLine(finding?.fingerprint)),
|
|
157
152
|
`**Severity:** ${oneLine(finding?.severity)}`,
|
|
158
153
|
`**Check:** ${oneLine(finding?.checkId)}`,
|
|
159
154
|
`**Locator:** \`${oneLine(finding?.locator)}\``,
|
|
@@ -168,6 +163,37 @@ export function buildIssueBody(finding, { runId, rubricHash } = {}) {
|
|
|
168
163
|
.join('\n');
|
|
169
164
|
}
|
|
170
165
|
|
|
166
|
+
/** One stderr line per process for the deprecated alias below, not one per call. */
|
|
167
|
+
let buildIssueBodyDeprecationWarned = false;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deprecated alias of {@link buildUxGrillIssueBody}, shipped under this name in
|
|
171
|
+
* 5.2.0.
|
|
172
|
+
*
|
|
173
|
+
* @deprecated since 5.3.0 — use buildUxGrillIssueBody; removed in 6.0.0.
|
|
174
|
+
* Kept one minor cycle (`.claude/rules/development.md` § Package Lifecycle &
|
|
175
|
+
* Versioning) because `package.json` carries no `exports` map: every packed
|
|
176
|
+
* `scripts/lib/` export is a public deep-import, and a consumer calling this
|
|
177
|
+
* name would otherwise get a runtime TypeError, not a compile error. It
|
|
178
|
+
* delegates to the LOCAL ux-grill body builder — never to the unrelated
|
|
179
|
+
* `buildIssueBody(finding, fp)` of `test-runner/issue-reconcile.mjs`, whose
|
|
180
|
+
* finding shape and second argument differ.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} finding - a `makeFinding` record
|
|
183
|
+
* @param {object} [context] - same as {@link buildUxGrillIssueBody}
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function buildIssueBody(finding, context) {
|
|
187
|
+
if (!buildIssueBodyDeprecationWarned) {
|
|
188
|
+
buildIssueBodyDeprecationWarned = true;
|
|
189
|
+
console.warn(
|
|
190
|
+
'[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since 5.3.0 ' +
|
|
191
|
+
'and will be removed in 6.0.0 — use buildUxGrillIssueBody instead.',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return buildUxGrillIssueBody(finding, context);
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
/**
|
|
172
198
|
* Collect every string in a finding's `evidence` that looks like an absolute
|
|
173
199
|
* host path (POSIX `/…` or Windows `C:\…`).
|
|
@@ -296,7 +322,7 @@ export async function reconcileFindings({
|
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
const title = buildIssueTitle(finding);
|
|
299
|
-
const body =
|
|
325
|
+
const body = buildUxGrillIssueBody(finding, { runId, rubricHash });
|
|
300
326
|
const decision = triageDecision({ fingerprint: finding?.fingerprint, title }, candidates);
|
|
301
327
|
|
|
302
328
|
if (decision.action === 'ignore') {
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
* inode, or byte-identical).
|
|
12
12
|
* B. root `plugin.json` — the agent-plugins.org 1.0.0 manifest; its `version`
|
|
13
13
|
* must equal `package.json`'s and the two existing plugin manifests'.
|
|
14
|
-
* C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per
|
|
15
|
-
*
|
|
14
|
+
* C. `.agents/skills/<name>/SKILL.md` — one spec-legal mirror per skill or
|
|
15
|
+
* command, no orphans, agentskills.io field list only, description ≤ 1024.
|
|
16
16
|
*
|
|
17
17
|
* INDEPENDENCE (load-bearing). This checker deliberately imports NOTHING from
|
|
18
18
|
* `scripts/generate-agents-skills.mjs` and re-derives the spec field list, the
|
|
19
19
|
* 1024 cap and the alias rule from the spec, not from the generator. A
|
|
20
20
|
* generator-vs-generator check only proves the generator is self-consistent —
|
|
21
21
|
* that is how the Cursor `argument-hint` defect survived. The oracles here are
|
|
22
|
-
* the repository's own filesystem (`skills/`, `CLAUDE.md`,
|
|
23
|
-
* the published field list.
|
|
22
|
+
* the repository's own filesystem (`skills/`, `commands/`, `CLAUDE.md`,
|
|
23
|
+
* `package.json`) and the published field list.
|
|
24
24
|
*
|
|
25
25
|
* Usage: node scripts/lib/validate/check-agents-skills.mjs <plugin-root>
|
|
26
26
|
* Exit codes: 0 — all checks passed · 1 — one or more failures.
|
|
@@ -195,6 +195,7 @@ console.log('');
|
|
|
195
195
|
console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
196
196
|
{
|
|
197
197
|
const skillsDir = join(pluginRoot, 'skills');
|
|
198
|
+
const commandsDir = join(pluginRoot, 'commands');
|
|
198
199
|
const mirrorDir = join(pluginRoot, '.agents', 'skills');
|
|
199
200
|
|
|
200
201
|
const sourceSkills = existsSync(skillsDir) && statSync(skillsDir).isDirectory()
|
|
@@ -204,12 +205,21 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
204
205
|
.filter((n) => existsSync(join(skillsDir, n, 'SKILL.md')))
|
|
205
206
|
.sort()
|
|
206
207
|
: [];
|
|
208
|
+
const sources = new Map(sourceSkills.map((name) => [name, `skills/${name}/SKILL.md`]));
|
|
209
|
+
if (existsSync(commandsDir) && statSync(commandsDir).isDirectory()) {
|
|
210
|
+
for (const entry of readdirSync(commandsDir, { withFileTypes: true })) {
|
|
211
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
|
|
212
|
+
// Commands own public names when an internal skill has the same name.
|
|
213
|
+
sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const sourceNames = [...sources.keys()].sort();
|
|
207
217
|
|
|
208
218
|
if (!existsSync(mirrorDir)) {
|
|
209
|
-
if (
|
|
210
|
-
pass('no
|
|
219
|
+
if (sourceNames.length === 0) {
|
|
220
|
+
pass('no skill or command sources and no .agents/skills/ — nothing to mirror');
|
|
211
221
|
} else {
|
|
212
|
-
fail(`.agents/skills/ is missing but ${
|
|
222
|
+
fail(`.agents/skills/ is missing but ${sourceNames.length} source entry(s) exist `
|
|
213
223
|
+ '(remedy: node scripts/generate-agents-skills.mjs)');
|
|
214
224
|
}
|
|
215
225
|
} else {
|
|
@@ -218,12 +228,12 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
218
228
|
.map((e) => e.name)
|
|
219
229
|
.sort();
|
|
220
230
|
|
|
221
|
-
const missing =
|
|
222
|
-
const orphans = mirrored.filter((n) => !
|
|
223
|
-
if (missing.length === 0) pass(`every source
|
|
224
|
-
else fail(`${missing.length} source
|
|
231
|
+
const missing = sourceNames.filter((n) => !mirrored.includes(n));
|
|
232
|
+
const orphans = mirrored.filter((n) => !sources.has(n));
|
|
233
|
+
if (missing.length === 0) pass(`every source entry has a mirror (${sourceNames.length})`);
|
|
234
|
+
else fail(`${missing.length} source entry(s) have no mirror: ${missing.join(', ')}`);
|
|
225
235
|
if (orphans.length === 0) pass('no orphan mirrors');
|
|
226
|
-
else fail(`${orphans.length} orphan mirror(s) with no source
|
|
236
|
+
else fail(`${orphans.length} orphan mirror(s) with no skill or command source: ${orphans.join(', ')}`);
|
|
227
237
|
|
|
228
238
|
let fmFail = 0;
|
|
229
239
|
let keyFail = 0;
|
|
@@ -303,8 +313,9 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
303
313
|
}
|
|
304
314
|
}
|
|
305
315
|
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
const canonical = sources.get(name);
|
|
317
|
+
if (canonical && !body.includes(canonical)) {
|
|
318
|
+
fail(`.agents/skills/${name}/SKILL.md body does not cite the canonical ${canonical} `
|
|
308
319
|
+ '— the mirror is a pointer, and a pointer with no target is dead weight');
|
|
309
320
|
pointerFail += 1;
|
|
310
321
|
}
|
|
@@ -314,7 +325,7 @@ console.log('--- Check C: .agents/skills/ portable mirror ---');
|
|
|
314
325
|
if (keyFail === 0 && mirrored.length > 0) pass('all mirrors use only agentskills.io spec fields');
|
|
315
326
|
if (descFail === 0 && mirrored.length > 0) pass(`all mirror descriptions are non-empty and ≤ ${DESCRIPTION_MAX} chars`);
|
|
316
327
|
if (sizeFail === 0 && mirrored.length > 0) pass(`all mirrors ≤ ${MIRROR_MAX_BYTES} bytes (total ${totalBytes} bytes)`);
|
|
317
|
-
if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical
|
|
328
|
+
if (pointerFail === 0 && mirrored.length > 0) pass('all mirrors cite their canonical source');
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
import { execFileSync } from 'node:child_process';
|
|
47
47
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
|
-
import { pathToFileURL } from 'node:url';
|
|
50
49
|
import { HISTORICAL_GUARD_BANNER } from '../historical-guard.mjs';
|
|
50
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
51
51
|
|
|
52
52
|
/** Directories whose markdown quotes the banner as coordinator-facing prose. */
|
|
53
53
|
const SCAN_DIRS = Object.freeze(['skills', 'commands']);
|
|
@@ -365,7 +365,7 @@ export function runCheckBannerParity(pluginRoot) {
|
|
|
365
365
|
return inspection.toolError ? 2 : 1;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
const isMain =
|
|
368
|
+
const isMain =isMainModule(import.meta.url);
|
|
369
369
|
if (isMain) {
|
|
370
370
|
const pluginRoot = process.argv[2];
|
|
371
371
|
if (!pluginRoot) {
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
8
|
import { join, relative } from 'node:path';
|
|
9
|
-
import { pathToFileURL } from 'node:url';
|
|
10
9
|
|
|
11
10
|
import yaml from 'js-yaml';
|
|
12
11
|
|
|
13
12
|
import { CURSOR_TO_CANONICAL_EVENT } from '../cursor-hook-bridge.mjs';
|
|
13
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
14
14
|
|
|
15
15
|
/** agentskills.io caps a skill/command description at 1024 characters. */
|
|
16
16
|
const DESCRIPTION_MAX = 1024;
|
|
@@ -177,6 +177,7 @@ function runCli(pluginRoot) {
|
|
|
177
177
|
} else {
|
|
178
178
|
const detail = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
|
|
179
179
|
fail(`Cursor adapter wrappers are stale${detail ? `: ${detail}` : ''}`);
|
|
180
|
+
console.log(' Remedy: node scripts/generate-cursor-adapter.mjs');
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -259,6 +260,6 @@ function runCli(pluginRoot) {
|
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
// Direct invocation only — importing this module must not run the CLI.
|
|
262
|
-
if (
|
|
263
|
+
if (isMainModule(import.meta.url)) {
|
|
263
264
|
runCli(process.argv[2]);
|
|
264
265
|
}
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
// or process.exit — the isMain guard at the bottom is the only side-effecting
|
|
23
23
|
// path. The Quality wave imports runCheckDeadBridge directly for unit tests.
|
|
24
24
|
|
|
25
|
-
import { pathToFileURL } from 'node:url';
|
|
26
25
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
27
26
|
import path from 'node:path';
|
|
28
27
|
import { DETECTORS } from './dead-bridge-detectors.mjs';
|
|
29
28
|
import * as corpus from './dead-bridge-corpus.mjs';
|
|
30
29
|
import { listRepoFiles } from './repo-files.mjs';
|
|
30
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
31
31
|
|
|
32
32
|
const DEFAULT_EXTS = ['.mjs', '.md'];
|
|
33
33
|
|
|
@@ -191,7 +191,7 @@ export function runCheckDeadBridge(pluginRoot) {
|
|
|
191
191
|
|
|
192
192
|
// CLI entry — only when executed directly, never on import (keeps the exports
|
|
193
193
|
// safe to import from tests without triggering process.exit).
|
|
194
|
-
const isMain =
|
|
194
|
+
const isMain =isMainModule(import.meta.url);
|
|
195
195
|
if (isMain) {
|
|
196
196
|
const pluginRoot = process.argv[2];
|
|
197
197
|
if (!pluginRoot) {
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
import { readFileSync } from 'node:fs';
|
|
66
66
|
import path from 'node:path';
|
|
67
67
|
import { spawnSync } from 'node:child_process';
|
|
68
|
-
import { pathToFileURL } from 'node:url';
|
|
69
68
|
import { listRepoFiles } from './repo-files.mjs';
|
|
70
69
|
import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
|
|
70
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
71
71
|
|
|
72
72
|
/** Directories walked for documentation. Mirrors check-vcs-repo-flag.mjs. */
|
|
73
73
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -461,7 +461,7 @@ export function runCheckDocCliCommands(pluginRoot) {
|
|
|
461
461
|
return 0;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
const isMain =
|
|
464
|
+
const isMain =isMainModule(import.meta.url);
|
|
465
465
|
if (isMain) {
|
|
466
466
|
const args = process.argv.slice(2).filter((arg) => arg !== '--json');
|
|
467
467
|
const root = path.resolve(args[0] || process.cwd());
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-entry-guard.mjs — census of SYMLINK-FRAGILE ESM entry guards.
|
|
4
|
+
*
|
|
5
|
+
* ## The defect class (#1371)
|
|
6
|
+
*
|
|
7
|
+
* A module that runs `main()` only when invoked as a CLI compares "the module I
|
|
8
|
+
* am" against "the script Node was told to run". The two are not the same kind
|
|
9
|
+
* of path: Node resolves `import.meta.url` to the **realpath**, while
|
|
10
|
+
* `process.argv[1]` is the path **as typed**. With a symlink anywhere in the
|
|
11
|
+
* invocation path — a `node_modules/.bin/` shim, a `~/bin` convenience link, a
|
|
12
|
+
* plugin directory symlinked into `~/.claude/plugins/`, or `/tmp` itself (a
|
|
13
|
+
* symlink to `/private/tmp` on macOS) — the comparison is false.
|
|
14
|
+
*
|
|
15
|
+
* The failure mode is the worst shape a defect can have: `main()` never runs,
|
|
16
|
+
* nothing is printed, and the process exits 0. Every caller reads that as
|
|
17
|
+
* success.
|
|
18
|
+
*
|
|
19
|
+
* The fix is `isMainModule()` from `scripts/lib/is-main-module.mjs`, which
|
|
20
|
+
* realpaths BOTH sides. This check is the recurrence guard: it fails the build
|
|
21
|
+
* when a new hand-written comparison appears.
|
|
22
|
+
*
|
|
23
|
+
* ## Oracle
|
|
24
|
+
*
|
|
25
|
+
* Over every tracked `.mjs` under `scripts/` + `hooks/` (via `git ls-files`,
|
|
26
|
+
* so an untracked scratch file is never a finding):
|
|
27
|
+
*
|
|
28
|
+
* 1. strip comments (line comments, trailing `//`, and block comments),
|
|
29
|
+
* 2. split the remainder into statement fragments on `;{}`,
|
|
30
|
+
* 3. inside each fragment mentioning `process.argv[1]`, apply two oracles:
|
|
31
|
+
* a. **comparison form** — take every `===` / `!==` comparison and its two
|
|
32
|
+
* operands (bounded to the adjacent `&&` / `||` segment) and FAIL when
|
|
33
|
+
* one operand mentions `process.argv[1]` and the other mentions
|
|
34
|
+
* `import.meta.url` or `__filename`;
|
|
35
|
+
* b. **bare-basename form** — FAIL on `process.argv[1].endsWith('x.mjs')`
|
|
36
|
+
* (plus the `?.`, `String(...)` and `(argv[1] || '')` receiver
|
|
37
|
+
* variants) against a `.mjs`/`.js`/`.cjs` string literal. This form has
|
|
38
|
+
* no comparison operator at all, so oracle (a) never saw it.
|
|
39
|
+
* 4. In both cases, a fragment wrapping either side in a `realpath*` call is
|
|
40
|
+
* the safe idiom and must not be flagged.
|
|
41
|
+
*
|
|
42
|
+
* Both oracles are anchored on a SYNTAX shape, which is what keeps prose out of
|
|
43
|
+
* the census: `scripts/validate-plugin.mjs`,
|
|
44
|
+
* `scripts/lib/vault-consolidate-fs.mjs`, `hooks/post-edit-import-probe.mjs`
|
|
45
|
+
* and this file's own header all MENTION a broken idiom inside a comment and
|
|
46
|
+
* are excluded by step 1 without needing an allowlist entry.
|
|
47
|
+
* `scripts/lib/baseline-archetypes.mjs` (`await import(process.argv[1])`) is a
|
|
48
|
+
* deliberate cross-process trick that is neither a comparison nor a basename
|
|
49
|
+
* test — out of scope for this check.
|
|
50
|
+
*
|
|
51
|
+
* `scripts/lib/ecosystem-wizard.mjs` (bare `argv[1].endsWith(...)`) and
|
|
52
|
+
* `scripts/lib/fetch-baseline.mjs` (a hybrid `===`-plus-`endsWith` guard, which
|
|
53
|
+
* needed the ALLOWLIST below) were the two hand-written guards this check was
|
|
54
|
+
* built to catch (#1371); both were fixed to `isMainModule()` in #1378. Oracle
|
|
55
|
+
* (b) was added afterwards because reverting the FIRST of them was caught by
|
|
56
|
+
* nothing: the bare form was outside oracle (a) by construction, and neither
|
|
57
|
+
* file's symlink smoke test reproduced its own regression (a same-named file
|
|
58
|
+
* behind a symlinked DIRECTORY still ends with the basename). The allowlist is
|
|
59
|
+
* empty — kept as a frozen mechanism (see its docblock) rather than deleted,
|
|
60
|
+
* since the next hand-written exception is exactly what would re-populate it.
|
|
61
|
+
*
|
|
62
|
+
* `scripts/lib/is-main-module.mjs` itself is excluded: it is the fix, and its
|
|
63
|
+
* docblock quotes every variant it replaces.
|
|
64
|
+
*
|
|
65
|
+
* ## Mode: BLOCKING
|
|
66
|
+
*
|
|
67
|
+
* Unlike the WARN-only censuses in validate-plugin (`check-unwired-features`,
|
|
68
|
+
* `check-learning-provenance`, `check-vcs-repo-flag`), this one blocks. It can
|
|
69
|
+
* do so honestly because the backlog was drained to zero in the same change
|
|
70
|
+
* that introduced it — a blocking gate is only dishonest when it is red on
|
|
71
|
+
* arrival for work nobody intends to do.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
import { execFileSync } from 'node:child_process';
|
|
75
|
+
import { readFileSync } from 'node:fs';
|
|
76
|
+
import path from 'node:path';
|
|
77
|
+
|
|
78
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
79
|
+
import { maskSource } from '../js-mask.mjs';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Files whose `process.argv[1]` comparison the oracle flags but which are NOT
|
|
83
|
+
* the defect class. Every entry carries the reason it is exempt; a stale entry
|
|
84
|
+
* (one the oracle no longer flags) is itself reported, so this list cannot rot
|
|
85
|
+
* into a silent suppression.
|
|
86
|
+
*
|
|
87
|
+
* Empty since #1378: the one entry this list ever carried
|
|
88
|
+
* (`scripts/lib/fetch-baseline.mjs`, whose hybrid guard's `endsWith(basename)`
|
|
89
|
+
* fallback made it non-fragile) was removed when that file was fixed to
|
|
90
|
+
* `isMainModule()` instead of being kept as a hand-verified exception. The
|
|
91
|
+
* mechanism is retained rather than deleted: the stale-entry check below only
|
|
92
|
+
* fires for an entry that is PRESENT and no longer flagged, so an empty object
|
|
93
|
+
* never trips it, and a future hand-written exception can still be recorded
|
|
94
|
+
* here.
|
|
95
|
+
*
|
|
96
|
+
* @type {Record<string, string>}
|
|
97
|
+
*/
|
|
98
|
+
const ALLOWLIST = {};
|
|
99
|
+
|
|
100
|
+
/** The one module allowed to contain every broken variant: it documents them. */
|
|
101
|
+
const SELF_EXEMPT = 'scripts/lib/is-main-module.mjs';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Remove comments while preserving character offsets (so line numbers survive).
|
|
105
|
+
*
|
|
106
|
+
* A thin wrapper over the shared lexer `maskSource` in `keepLiterals` mode:
|
|
107
|
+
* string, template and regex literals are skipped INTACT. That is load-bearing
|
|
108
|
+
* twice over — the V1 idiom \`file://${process.argv[1]}\` carries a `//` that a
|
|
109
|
+
* naive scanner reads as a line comment, and a regex literal must be recognised
|
|
110
|
+
* as one (#1383: a `/` the lexer read as a division let the regex body's `/*`
|
|
111
|
+
* open a block comment that blanked the guard below it — a fail-open miss, for
|
|
112
|
+
* a quote-bearing regex, a `/[/*]/` character class, and a regex after a keyword
|
|
113
|
+
* alike).
|
|
114
|
+
*
|
|
115
|
+
* @param {string} src module source
|
|
116
|
+
* @returns {string} same length, comment bytes replaced by spaces
|
|
117
|
+
*/
|
|
118
|
+
export function stripComments(src) {
|
|
119
|
+
return maskSource(src, { keepLiterals: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A `.endsWith('<something>.mjs')` call with a module-basename-shaped literal.
|
|
124
|
+
* Scanned rather than matched in one shot so the RECEIVER can be judged from the
|
|
125
|
+
* text preceding each hit — a `.endsWith()` on anything other than
|
|
126
|
+
* `process.argv[1]` is not this defect class.
|
|
127
|
+
*/
|
|
128
|
+
const ENDS_WITH_LITERAL = /endsWith\(\s*(['"`])([^'"`]*)\1\s*\)/g;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The receiver chain, whitespace-stripped, that makes an `.endsWith()` call a
|
|
132
|
+
* test on the invocation path. Deliberately unanchored at the START so an outer
|
|
133
|
+
* wrapper (`String(...)`, `path.basename(...)`) still matches; anchored at the
|
|
134
|
+
* END so only the call immediately downstream of `process.argv[1]` counts.
|
|
135
|
+
*/
|
|
136
|
+
const ARGV_RECEIVER_TAIL = /process\.argv\[1\](?:\|\|''|\|\|"")?\)*\??\.$/;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Findings for one module body.
|
|
140
|
+
*
|
|
141
|
+
* `kind` distinguishes the two oracles: `comparison` is the
|
|
142
|
+
* `argv[1] === import.meta.url` family, `basename` the bare
|
|
143
|
+
* `argv[1].endsWith('x.mjs')` family. Both are symlink-fragile; only the remedy
|
|
144
|
+
* wording differs.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} src module source
|
|
147
|
+
* @returns {Array<{line: number, text: string, kind: 'comparison'|'basename'}>} one per fragile guard
|
|
148
|
+
*/
|
|
149
|
+
export function findFragileGuards(src) {
|
|
150
|
+
// Template substitutions are rewritten `${x}` → `$(x)` BEFORE the fragment
|
|
151
|
+
// split: the V1 idiom `import.meta.url === \`file://${process.argv[1]}\`` puts
|
|
152
|
+
// braces INSIDE the comparison, so splitting on `{}` would tear the statement
|
|
153
|
+
// in two and the most common variant of the defect class (12 of 50 files at
|
|
154
|
+
// ca214376) would never be seen. Length is preserved so line numbers survive.
|
|
155
|
+
const stripped = stripComments(src).replace(/\$\{([^{}]*)\}/g, (_m, inner) => `$(${inner})`);
|
|
156
|
+
const findings = [];
|
|
157
|
+
let cursor = 0;
|
|
158
|
+
for (const fragment of stripped.split(/[;{}]/)) {
|
|
159
|
+
const fragStart = cursor;
|
|
160
|
+
cursor += fragment.length + 1;
|
|
161
|
+
if (!fragment.includes('process.argv[1]')) continue;
|
|
162
|
+
|
|
163
|
+
/** @type {(offset: number) => number} 1-based line of an offset in `fragment` */
|
|
164
|
+
const lineAt = (offset) =>
|
|
165
|
+
stripped.slice(0, Math.min(fragStart + offset, stripped.length)).split('\n').length;
|
|
166
|
+
|
|
167
|
+
if (!/import\.meta\.url|__filename/.test(fragment)) {
|
|
168
|
+
// Oracle (b): no self-path mention at all, so the comparison oracle below
|
|
169
|
+
// cannot see this fragment. The bare basename test lives exactly here.
|
|
170
|
+
if (!/realpath/i.test(fragment)) {
|
|
171
|
+
ENDS_WITH_LITERAL.lastIndex = 0;
|
|
172
|
+
let m;
|
|
173
|
+
while ((m = ENDS_WITH_LITERAL.exec(fragment)) !== null) {
|
|
174
|
+
if (!/\.(?:mjs|js|cjs)$/.test(m[2])) continue;
|
|
175
|
+
const receiver = fragment.slice(0, m.index).replace(/\s+/g, '');
|
|
176
|
+
if (!ARGV_RECEIVER_TAIL.test(receiver)) continue;
|
|
177
|
+
findings.push({
|
|
178
|
+
line: lineAt(m.index),
|
|
179
|
+
text: `process.argv[1]…${m[0]}`,
|
|
180
|
+
kind: 'basename',
|
|
181
|
+
});
|
|
182
|
+
break; // one finding per statement is enough to act on
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const parts = fragment.split(/===|!==/);
|
|
189
|
+
for (let k = 0; k < parts.length - 1; k++) {
|
|
190
|
+
const left = parts[k].split(/&&|\|\|/).pop() ?? '';
|
|
191
|
+
const right = parts[k + 1].split(/&&|\|\|/)[0] ?? '';
|
|
192
|
+
const mentionsArgv = /process\.argv\[1\]/.test(left) || /process\.argv\[1\]/.test(right);
|
|
193
|
+
const mentionsSelf =
|
|
194
|
+
/import\.meta\.url|__filename/.test(left) || /import\.meta\.url|__filename/.test(right);
|
|
195
|
+
if (!mentionsArgv || !mentionsSelf) continue;
|
|
196
|
+
if (/realpath/i.test(left) || /realpath/i.test(right)) continue; // the safe idiom
|
|
197
|
+
const line = lineAt(parts.slice(0, k + 1).join('===').length || 0);
|
|
198
|
+
findings.push({
|
|
199
|
+
line,
|
|
200
|
+
text: `${left.trim()} === ${right.trim()}`.replace(/\s+/g, ' '),
|
|
201
|
+
kind: 'comparison',
|
|
202
|
+
});
|
|
203
|
+
break; // one finding per statement is enough to act on
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return findings;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Tracked `.mjs` files under `scripts/` and `hooks/`.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} repoRoot absolute repo root
|
|
213
|
+
* @returns {string[]} repo-relative POSIX paths
|
|
214
|
+
*/
|
|
215
|
+
function trackedModules(repoRoot) {
|
|
216
|
+
// Pathspecs are DIRECTORIES, not `scripts/**/*.mjs`: git's wildmatch requires
|
|
217
|
+
// `**` to consume at least one path component, so a `scripts/**/*.mjs` spec
|
|
218
|
+
// silently drops every top-level `scripts/*.mjs` AND all of `hooks/*.mjs` —
|
|
219
|
+
// measured 2026-09-16: 61 files matched vs 176 actually tracked. The extension
|
|
220
|
+
// filter therefore lives here, where it cannot be wrong about that.
|
|
221
|
+
//
|
|
222
|
+
// Re-measured 2026-09-17 (the 61/176 pair no longer reproduces at any scope I
|
|
223
|
+
// could reconstruct, so it is kept as-dated rather than edited):
|
|
224
|
+
// `git ls-files -- 'scripts/**/*.mjs' 'hooks/**/*.mjs' | wc -l` → 422 against
|
|
225
|
+
// `git ls-files -- scripts hooks | grep -c '\.mjs$'` → 508. Direction of the
|
|
226
|
+
// trap is unchanged; only the magnitude moved.
|
|
227
|
+
const out = execFileSync('git', ['ls-files', '--', 'scripts', 'hooks'], {
|
|
228
|
+
cwd: repoRoot,
|
|
229
|
+
encoding: 'utf8',
|
|
230
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
231
|
+
});
|
|
232
|
+
return out
|
|
233
|
+
.split('\n')
|
|
234
|
+
.map((l) => l.trim())
|
|
235
|
+
.filter((l) => l.endsWith('.mjs'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Run the census.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} repoRoot absolute repo root
|
|
242
|
+
* @returns {Promise<number>} 0 clean · 1 findings · 2 tool error
|
|
243
|
+
*/
|
|
244
|
+
export async function runCheckEntryGuard(repoRoot) {
|
|
245
|
+
let files;
|
|
246
|
+
try {
|
|
247
|
+
files = trackedModules(repoRoot);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.log(` FAIL: cannot enumerate tracked modules: ${err.message}`);
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log('Results: 0 passed, 1 failed');
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let failed = 0;
|
|
256
|
+
const flagged = new Set();
|
|
257
|
+
for (const rel of files) {
|
|
258
|
+
if (rel === SELF_EXEMPT) continue;
|
|
259
|
+
let body;
|
|
260
|
+
try {
|
|
261
|
+
body = readFileSync(path.join(repoRoot, rel), 'utf8');
|
|
262
|
+
} catch {
|
|
263
|
+
continue; // deleted between ls-files and read — not this check's business
|
|
264
|
+
}
|
|
265
|
+
const findings = findFragileGuards(body);
|
|
266
|
+
if (findings.length === 0) continue;
|
|
267
|
+
flagged.add(rel);
|
|
268
|
+
if (rel in ALLOWLIST) continue;
|
|
269
|
+
for (const f of findings) {
|
|
270
|
+
const why =
|
|
271
|
+
f.kind === 'basename'
|
|
272
|
+
? 'a basename test answers "was a file with this name run?", never "was THIS ' +
|
|
273
|
+
'module run" — it is true for any same-named file (a sibling copy, a ' +
|
|
274
|
+
'vendored duplicate, a fixture) and resolves no path at all, so main() ' +
|
|
275
|
+
'fires or stays silent for reasons unrelated to identity'
|
|
276
|
+
: '`import.meta.url` is the realpath, `process.argv[1]` is the path as typed, ' +
|
|
277
|
+
'so the guard is false under any symlinked invocation and main() silently ' +
|
|
278
|
+
'never runs (exit 0, no output)';
|
|
279
|
+
console.log(
|
|
280
|
+
` FAIL: ${rel}:${f.line} — symlink-fragile entry guard \`${f.text}\`: ${why}. ` +
|
|
281
|
+
'Use `isMainModule(import.meta.url)` from scripts/lib/is-main-module.mjs',
|
|
282
|
+
);
|
|
283
|
+
failed++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const censused = new Set(files);
|
|
288
|
+
for (const [rel, reason] of Object.entries(ALLOWLIST)) {
|
|
289
|
+
if (flagged.has(rel)) continue;
|
|
290
|
+
// Not present at all (a fixture repo, a fork that deleted the file) is not
|
|
291
|
+
// rot — only an entry whose file IS censused and no longer flagged is.
|
|
292
|
+
if (!censused.has(rel)) continue;
|
|
293
|
+
console.log(
|
|
294
|
+
` FAIL: ${rel} — stale allowlist entry: the oracle no longer flags this file, ` +
|
|
295
|
+
`so the exemption (“${reason}”) suppresses nothing. Remove it.`,
|
|
296
|
+
);
|
|
297
|
+
failed++;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (failed === 0) {
|
|
301
|
+
console.log(
|
|
302
|
+
` PASS: censused ${files.length} tracked module(s) under scripts/ + hooks/ — ` +
|
|
303
|
+
`0 symlink-fragile entry guard(s), ${Object.keys(ALLOWLIST).length} allowlisted`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(`Results: ${failed === 0 ? 1 : 0} passed, ${failed} failed`);
|
|
308
|
+
return failed === 0 ? 0 : 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isMainModule(import.meta.url)) {
|
|
312
|
+
const argv = process.argv.slice(2);
|
|
313
|
+
const usage =
|
|
314
|
+
'Usage: check-entry-guard.mjs [<repo-root>]\n' +
|
|
315
|
+
'Exit: 0 clean · 1 symlink-fragile entry guard(s) found · 2 tool error';
|
|
316
|
+
if (argv.includes('--help')) {
|
|
317
|
+
console.log(usage);
|
|
318
|
+
process.exitCode = 0;
|
|
319
|
+
} else {
|
|
320
|
+
const positional = argv.filter((a) => !a.startsWith('--'));
|
|
321
|
+
const unknown = argv.filter((a) => a.startsWith('--') && a !== '--help');
|
|
322
|
+
if (unknown.length > 0) {
|
|
323
|
+
console.error(`Unknown flag(s): ${unknown.join(', ')}\n${usage}`);
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
} else {
|
|
326
|
+
process.exitCode = await runCheckEntryGuard(path.resolve(positional[0] ?? process.cwd()));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import { pathToFileURL } from 'node:url';
|
|
18
17
|
import { parse } from '@babel/parser';
|
|
18
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
19
19
|
|
|
20
20
|
const HOOKS_RELATIVE_DIR = 'hooks';
|
|
21
21
|
const MODULE_RELATIVE_PREFIX = 'scripts/lib';
|
|
@@ -1137,7 +1137,7 @@ export function runCheckGuardRequiresParity(pluginRoot) {
|
|
|
1137
1137
|
return inspection.toolError ? 2 : 1;
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
const isMain =
|
|
1140
|
+
const isMain =isMainModule(import.meta.url);
|
|
1141
1141
|
if (isMain) {
|
|
1142
1142
|
const pluginRoot = process.argv[2];
|
|
1143
1143
|
if (!pluginRoot) {
|