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
|
@@ -93,6 +93,7 @@ export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
|
|
|
93
93
|
'fleet_self_declared',
|
|
94
94
|
'session_record',
|
|
95
95
|
'session_profile',
|
|
96
|
+
'ledger_complete',
|
|
96
97
|
]);
|
|
97
98
|
|
|
98
99
|
/**
|
|
@@ -117,6 +118,29 @@ const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAG
|
|
|
117
118
|
*/
|
|
118
119
|
export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
|
|
119
120
|
|
|
121
|
+
/*
|
|
122
|
+
* `ledger_complete` — a SECOND, ORTHOGONAL axis beside `session_record`
|
|
123
|
+
* (GitLab #1416), deliberately NOT a fourth provenance token.
|
|
124
|
+
*
|
|
125
|
+
* `deriveSessionFromEvents` (telemetry/sync.mjs) returns a three-state verdict on
|
|
126
|
+
* the events ledger it read FROM: `true` (every rotation archive present),
|
|
127
|
+
* `false` (at least one measured gap — a deleted/missing archive), and `null`
|
|
128
|
+
* (no verdict was produced at all: the read threw, or there was no source). That
|
|
129
|
+
* verdict decided nothing on the wire, so a derived record reconstructed from a
|
|
130
|
+
* holey ledger was indistinguishable from one reconstructed from a whole one.
|
|
131
|
+
*
|
|
132
|
+
* Why a boolean and not a `derived-partial` token: `SESSION_RECORD_SOURCES` is a
|
|
133
|
+
* frozen enum mirrored in prose and in the server's `raw_json` consumers, and
|
|
134
|
+
* completeness is a property of the SOURCE READ, not of WHICH source was used —
|
|
135
|
+
* widening the provenance enum would make the two facts inseparable and force
|
|
136
|
+
* every reader of `session_record === 'derived'` to learn a second spelling.
|
|
137
|
+
*
|
|
138
|
+
* THE THIRD STATE IS OMISSION: only a real boolean is written. `null` (nothing
|
|
139
|
+
* measured) omits the key, exactly like `session_profile` — "we did not look" is
|
|
140
|
+
* never sent as a measured `false`. On the `ledger` branch the field is not set
|
|
141
|
+
* at ALL: that path never reads the events ledger, so it has no verdict to give.
|
|
142
|
+
*/
|
|
143
|
+
|
|
120
144
|
/** Exact duration-bucket tokens (ASCII, stable wire values). */
|
|
121
145
|
export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
|
|
122
146
|
|
|
@@ -541,6 +565,7 @@ export function buildUsagePing({
|
|
|
541
565
|
consentState,
|
|
542
566
|
sessionRecordSource,
|
|
543
567
|
sessionProfile,
|
|
568
|
+
ledgerComplete,
|
|
544
569
|
} = {}) {
|
|
545
570
|
const session = isPlainObject(sessionRecord) ? sessionRecord : {};
|
|
546
571
|
const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
|
|
@@ -637,6 +662,11 @@ export function buildUsagePing({
|
|
|
637
662
|
session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
|
|
638
663
|
? sessionRecordSource
|
|
639
664
|
: 'absent',
|
|
665
|
+
// ONLY a real boolean travels (#1416). `null`/`undefined` — the read never
|
|
666
|
+
// happened, or this path never read the events ledger at all — OMITS the
|
|
667
|
+
// key, the same absent-is-not-empty contract `session_profile` uses above.
|
|
668
|
+
// Writing `false` there would turn "we did not look" into a measured gap.
|
|
669
|
+
...(typeof ledgerComplete === 'boolean' ? { ledger_complete: ledgerComplete } : {}),
|
|
640
670
|
skills: filterRosterNames(skillNames, rosterSkills),
|
|
641
671
|
commands: filterRosterNames(commandNames, rosterCommands),
|
|
642
672
|
};
|
|
@@ -41,6 +41,7 @@ import { ensureAnonId } from './anon-id.mjs';
|
|
|
41
41
|
import { peekAll, enqueue, clear, queueStats } from './queue.mjs';
|
|
42
42
|
import { loadOwnerConfig } from '../owner-yaml.mjs';
|
|
43
43
|
import { readJsonlFile } from '../io.mjs';
|
|
44
|
+
import { readEventsWithRotations } from '../events.mjs';
|
|
44
45
|
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
45
46
|
import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
|
|
46
47
|
import { readSessionProfile } from '../state-md.mjs';
|
|
@@ -338,12 +339,54 @@ export function readSessionProfileForMetricsDir(metricsDir) {
|
|
|
338
339
|
* Never throws.
|
|
339
340
|
*
|
|
340
341
|
* @param {string} metricsDir
|
|
341
|
-
* @returns {{ session: object, source: 'derived'|'absent'
|
|
342
|
+
* @returns {{ session: object, source: 'derived'|'absent',
|
|
343
|
+
* ledger_complete: boolean|null, ledger_gaps: object[] }}
|
|
344
|
+
* `ledger_complete` is `null` — never `false` — when the read produced no
|
|
345
|
+
* verdict at all: an undetermined ledger is not a proven gap. TWO paths reach
|
|
346
|
+
* that third state (#1423): this function throwing, and the reader finding NO
|
|
347
|
+
* source (no active file, no archive, no legacy ring) — in the latter case
|
|
348
|
+
* `events: []` is the absence of a ledger, not a measured quiet window.
|
|
349
|
+
* `ledger_complete === false` additionally writes ONE stderr line naming the
|
|
350
|
+
* gap kinds, because a verdict nothing surfaces is a verdict nobody acts on.
|
|
342
351
|
*/
|
|
343
352
|
export function deriveSessionFromEvents(metricsDir) {
|
|
344
353
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
// #1407: read ACROSS rotation boundaries. This reconstruction is the ONLY
|
|
355
|
+
// source of session facts whenever `sessions.jsonl` has no usable record,
|
|
356
|
+
// and its sibling window is `DAILY_FLUSH_MS` (24 h) — so a rotation between
|
|
357
|
+
// a session's start and its flush silently truncated it.
|
|
358
|
+
//
|
|
359
|
+
// CEILING (BV-004): the reader loads the active file AND every archive
|
|
360
|
+
// fully into memory — up to ~60 MB transient at the default
|
|
361
|
+
// `max-size-mb: 10` / `max-backups: 5`. Measured 2026-09-20 A/B in ONE
|
|
362
|
+
// process on exactly that worst case (60 MB / 6 sources / 293,994 records,
|
|
363
|
+
// host loadavg 11.2): median 25.5 ms → 215.8 ms, i.e. +190 ms, which is
|
|
364
|
+
// 6.4 % of this path's `POST_TIMEOUT_MS` (3000). Acceptable for a
|
|
365
|
+
// once-per-flush cold read. Revisit if `max-size-mb` is raised past ~100,
|
|
366
|
+
// if `max-backups` grows, or if the flush budget drops below ~1 s.
|
|
367
|
+
const ledger = readEventsWithRotations(undefined, {
|
|
368
|
+
filePath: path.join(metricsDir, 'events.jsonl'),
|
|
369
|
+
});
|
|
370
|
+
const events = ledger.events;
|
|
371
|
+
// A missing archive is a GAP, never an empty window (#1407 AC-3). Carried
|
|
372
|
+
// on EVERY return below, including the empty ones — that is precisely where
|
|
373
|
+
// a deleted archive and a quiet period are otherwise indistinguishable.
|
|
374
|
+
const ledgerVerdict = { ledger_complete: ledger.complete, ledger_gaps: ledger.gaps };
|
|
375
|
+
// Visibility (#1423): the field had ZERO production consumers — a measured
|
|
376
|
+
// gap reached nobody. ONE stderr line, at the single point the verdict is
|
|
377
|
+
// formed, so every return below is covered by construction. Diagnostics on
|
|
378
|
+
// stderr, never stdout (cli-design.md); no new event, because the gap is a
|
|
379
|
+
// property of the LEDGER and an event about it would land in the same file.
|
|
380
|
+
if (ledger.complete === false) {
|
|
381
|
+
const kinds = [...new Set((ledger.gaps ?? []).map((g) => g?.kind ?? 'unknown'))].sort();
|
|
382
|
+
process.stderr.write(
|
|
383
|
+
`⚠ telemetry: events ledger INCOMPLETE — ${ledger.gaps.length} gap(s) ` +
|
|
384
|
+
`[${kinds.join(', ')}]; the session facts below are reconstructed from a PARTIAL read\n`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
388
|
+
return { session: {}, source: 'absent', ...ledgerVerdict };
|
|
389
|
+
}
|
|
347
390
|
|
|
348
391
|
let startedAt = null;
|
|
349
392
|
let sessionType = null;
|
|
@@ -412,7 +455,9 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
412
455
|
profile = undatedShapeProfile;
|
|
413
456
|
}
|
|
414
457
|
|
|
415
|
-
if (startedAt === null && sessionType === null && profile === null)
|
|
458
|
+
if (startedAt === null && sessionType === null && profile === null) {
|
|
459
|
+
return { session: {}, source: 'absent', ...ledgerVerdict };
|
|
460
|
+
}
|
|
416
461
|
|
|
417
462
|
const session = {};
|
|
418
463
|
if (sessionType !== null) session.session_type = sessionType;
|
|
@@ -422,9 +467,11 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
422
467
|
// omit-never-fabricate contract session-close-backfill.mjs uses (#914 R1).
|
|
423
468
|
if (lastTs !== null && startedAt !== null && lastTs >= startedAt) session.completed_at = lastTs;
|
|
424
469
|
|
|
425
|
-
return { session, source: 'derived' };
|
|
470
|
+
return { session, source: 'derived', ...ledgerVerdict };
|
|
426
471
|
} catch {
|
|
427
|
-
|
|
472
|
+
// No verdict was ever produced — `null`, not `false`: a measurement that
|
|
473
|
+
// did not happen is not a measured zero.
|
|
474
|
+
return { session: {}, source: 'absent', ledger_complete: null, ledger_gaps: [] };
|
|
428
475
|
}
|
|
429
476
|
}
|
|
430
477
|
|
|
@@ -483,6 +530,12 @@ export function buildBatch({
|
|
|
483
530
|
let sessionForPing;
|
|
484
531
|
/** @type {'ledger'|'derived'|'absent'} */
|
|
485
532
|
let sessionRecordSource = 'ledger';
|
|
533
|
+
// #1416: the events-ledger completeness verdict, carried to the ping as its
|
|
534
|
+
// own optional boolean. Stays `null` on the LEDGER branch by construction —
|
|
535
|
+
// that branch never reads events.jsonl, so it has nothing to report, and
|
|
536
|
+
// `buildUsagePing` omits the key for anything that is not a real boolean.
|
|
537
|
+
/** @type {boolean|null} */
|
|
538
|
+
let ledgerComplete = null;
|
|
486
539
|
if (sessionRecord && typeof sessionRecord.started_at === 'string' && !Number.isNaN(Date.parse(sessionRecord.started_at))) {
|
|
487
540
|
const startMs = Date.parse(sessionRecord.started_at);
|
|
488
541
|
windowInvocations = invocations.filter((rec) => {
|
|
@@ -500,6 +553,7 @@ export function buildBatch({
|
|
|
500
553
|
const derived = deriveSessionFromEvents(dir);
|
|
501
554
|
sessionForPing = derived.session;
|
|
502
555
|
sessionRecordSource = derived.source;
|
|
556
|
+
ledgerComplete = derived.ledger_complete;
|
|
503
557
|
|
|
504
558
|
const derivedStartMs = Date.parse(sessionForPing.started_at);
|
|
505
559
|
const cutoff = (Number.isNaN(Date.parse(nowIso)) ? Date.now() : Date.parse(nowIso)) - DAILY_FLUSH_MS;
|
|
@@ -522,6 +576,7 @@ export function buildBatch({
|
|
|
522
576
|
consentState,
|
|
523
577
|
sessionRecordSource,
|
|
524
578
|
sessionProfile: readSessionProfileForMetricsDir(dir),
|
|
579
|
+
ledgerComplete,
|
|
525
580
|
});
|
|
526
581
|
|
|
527
582
|
const target = statePath || TELEMETRY_JSON_PATH;
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
* @module scripts/lib/telemetry-flush-health-banner
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { existsSync
|
|
34
|
+
import { existsSync } from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
|
+
import { readTailWindow } from './tail-window.mjs';
|
|
38
|
+
|
|
37
39
|
/**
|
|
38
40
|
* Bytes of `events.jsonl` read from the END of the file.
|
|
39
41
|
*
|
|
@@ -82,31 +84,11 @@ function sanitizeReason(reason) {
|
|
|
82
84
|
* @returns {{text: string} | {missing: true} | {error: string}}
|
|
83
85
|
*/
|
|
84
86
|
function readTail(file) {
|
|
85
|
-
let fd;
|
|
86
87
|
try {
|
|
87
|
-
|
|
88
|
-
const { size } = fstatSync(fd);
|
|
89
|
-
const length = Math.min(size, TAIL_BYTES);
|
|
90
|
-
const start = size - length;
|
|
91
|
-
const buf = Buffer.allocUnsafe(length);
|
|
92
|
-
let read = 0;
|
|
93
|
-
while (read < length) {
|
|
94
|
-
const n = readSync(fd, buf, read, length - read, start + read);
|
|
95
|
-
if (n <= 0) break;
|
|
96
|
-
read += n;
|
|
97
|
-
}
|
|
98
|
-
return { text: buf.subarray(0, read).toString('utf8') };
|
|
88
|
+
return { text: readTailWindow(file, TAIL_BYTES).text };
|
|
99
89
|
} catch (err) {
|
|
100
90
|
if (err?.code === 'ENOENT') return { missing: true };
|
|
101
91
|
return { error: typeof err?.code === 'string' ? err.code : 'EUNKNOWN' };
|
|
102
|
-
} finally {
|
|
103
|
-
if (fd !== undefined) {
|
|
104
|
-
try {
|
|
105
|
-
closeSync(fd);
|
|
106
|
-
} catch {
|
|
107
|
-
/* best effort */
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
92
|
}
|
|
111
93
|
}
|
|
112
94
|
|
|
@@ -22,10 +22,17 @@
|
|
|
22
22
|
* triageDecision(finding, candidates)
|
|
23
23
|
* → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
|
|
24
24
|
*
|
|
25
|
+
* buildIssueBody(finding, fp) → string
|
|
26
|
+
*
|
|
27
|
+
* neutralizeFingerprintSentinel(text) → string
|
|
28
|
+
* fingerprintSentinelLine(fp) → string
|
|
29
|
+
* The one shared sentinel writer + neutraliser, also used by
|
|
30
|
+
* ux-grill/reconcile.mjs (#1339 P6).
|
|
31
|
+
*
|
|
25
32
|
* ReconcileError
|
|
26
33
|
*
|
|
27
34
|
* Security:
|
|
28
|
-
* - #388 (SEC-IR-MED-1): sentinel injection hardening —
|
|
35
|
+
* - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
|
|
29
36
|
* strips **Fingerprint:** literals from free-text fields before they are
|
|
30
37
|
* embedded in the body (before the authoritative sentinel line is appended).
|
|
31
38
|
* - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
|
|
@@ -126,39 +133,64 @@ function validateFinding(finding) {
|
|
|
126
133
|
// ---------------------------------------------------------------------------
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
136
|
+
* Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
|
|
137
|
+
* to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
|
|
138
|
+
* {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
|
|
139
|
+
* through it (#1339 P6), so a hardening added here reaches both producers.
|
|
133
140
|
*
|
|
134
|
-
* Context: glab parses issue bodies as Markdown. If
|
|
141
|
+
* Context: glab parses issue bodies as Markdown. If a free-text field contained
|
|
135
142
|
* `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
|
|
136
143
|
* listExistingFindings() would match the spoofed value instead of the real one,
|
|
137
144
|
* bypassing dedup. Replacing the literal prevents the attack without altering
|
|
138
|
-
* the semantic meaning of the
|
|
145
|
+
* the semantic meaning of the text.
|
|
146
|
+
*
|
|
147
|
+
* @param {unknown} text - coerced with `String(text ?? '')`
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function neutralizeFingerprintSentinel(text) {
|
|
151
|
+
// gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
|
|
152
|
+
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
153
|
+
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
154
|
+
return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The authoritative dedup sentinel line, in the exact shape
|
|
159
|
+
* {@link extractFingerprintFromBody} reads. The caller passes an
|
|
160
|
+
* already-trusted value; this function does not sanitise it.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} fp - 16-char hex fingerprint
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function fingerprintSentinelLine(fp) {
|
|
166
|
+
return `**Fingerprint:** \`${fp}\``;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize free-text recommendation fields before embedding in the issue body
|
|
171
|
+
* (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
|
|
172
|
+
* string, anything else returned unchanged (a `null` description is dropped by
|
|
173
|
+
* buildIssueBody's line filter rather than rendered as an empty line).
|
|
139
174
|
*
|
|
140
175
|
* @param {string|undefined|null} text
|
|
141
176
|
* @returns {string|undefined|null}
|
|
142
177
|
*/
|
|
143
178
|
function sanitizeRecommendation(text) {
|
|
144
179
|
if (!text || typeof text !== 'string') return text;
|
|
145
|
-
|
|
146
|
-
// are also neutralized. Sanitizer is intentionally broader than the case-sensitive
|
|
147
|
-
// extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
|
|
148
|
-
return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
|
|
180
|
+
return neutralizeFingerprintSentinel(text);
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
/**
|
|
152
184
|
* Render untrusted text as the content of ONE inline Markdown code span:
|
|
153
|
-
* sentinel literals neutralised
|
|
154
|
-
*
|
|
155
|
-
*
|
|
185
|
+
* sentinel literals neutralised, line breaks and NUL folded to a space,
|
|
186
|
+
* backticks turned into `'` — so a page-controlled locator can neither close
|
|
187
|
+
* its code span nor start a new body line.
|
|
156
188
|
*
|
|
157
189
|
* @param {unknown} text
|
|
158
190
|
* @returns {string}
|
|
159
191
|
*/
|
|
160
192
|
function codeSpanContent(text) {
|
|
161
|
-
return
|
|
193
|
+
return neutralizeFingerprintSentinel(text)
|
|
162
194
|
.replace(/[\r\n\0]+/g, ' ')
|
|
163
195
|
.replace(/`/g, "'");
|
|
164
196
|
}
|
|
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
|
|
|
218
250
|
'',
|
|
219
251
|
safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
|
|
220
252
|
'',
|
|
221
|
-
|
|
253
|
+
fingerprintSentinelLine(fp),
|
|
222
254
|
`**Severity:** ${finding.severity}`,
|
|
223
255
|
`**Check:** ${finding.checkId}`,
|
|
224
256
|
`**Locator:** \`${codeSpanContent(finding.locator)}\``,
|
|
@@ -76,9 +76,9 @@
|
|
|
76
76
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
77
77
|
import { join, isAbsolute, relative, resolve } from 'node:path';
|
|
78
78
|
import { execFileSync } from 'node:child_process';
|
|
79
|
-
import { fileURLToPath } from 'node:url';
|
|
80
79
|
|
|
81
80
|
import { writeStdoutLineSync } from './io.mjs';
|
|
81
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
82
82
|
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
84
|
// The definition — the part that must never be re-derived by hand
|
|
@@ -475,10 +475,6 @@ function main() {
|
|
|
475
475
|
process.exit(args.check && !result.withinCorridor ? 1 : 0);
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
const isMain =
|
|
479
|
-
typeof process !== 'undefined' &&
|
|
480
|
-
process.argv[1] !== null &&
|
|
481
|
-
process.argv[1] !== undefined &&
|
|
482
|
-
resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
478
|
+
const isMain =isMainModule(import.meta.url);
|
|
483
479
|
|
|
484
480
|
if (isMain) main();
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* scripts/lib/tmux-layout/telemetry-stats.mjs
|
|
3
3
|
*
|
|
4
4
|
* Aggregator stub for /tmux-layout telemetry promotion gate (#563).
|
|
5
|
-
* Reads
|
|
5
|
+
* Reads the events ledger — active file AND every rotated archive (#1407) —
|
|
6
|
+
* and computes:
|
|
6
7
|
* - invocation count (total + per layout)
|
|
7
8
|
* - completion rate (completed / invoked) — promotion gate threshold 80%
|
|
8
9
|
* - top-K degradation reasons
|
|
@@ -13,25 +14,68 @@
|
|
|
13
14
|
* - CLI: `node scripts/lib/tmux-layout/telemetry-stats.mjs` (emits JSON to stdout)
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { readEventsWithRotations } from '../events.mjs';
|
|
19
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
17
20
|
|
|
18
21
|
const EVENTS_PATH = '.orchestrator/metrics/events.jsonl';
|
|
19
22
|
|
|
23
|
+
/** Every event this module reports on carries this prefix. */
|
|
24
|
+
const TMUX_EVENT_PREFIX = 'tmux-layout.';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read the tmux-layout events ACROSS rotation boundaries, together with the
|
|
28
|
+
* honesty verdict of that read.
|
|
29
|
+
*
|
|
30
|
+
* WHY THE ENVELOPE (#1407 acceptance criterion 3): the stats below are an
|
|
31
|
+
* ALL-TIME rate. Reading only the active file silently redefines "all time" as
|
|
32
|
+
* "since the last rotation", and a genuinely missing archive would then look
|
|
33
|
+
* identical to a quiet week. `complete === false` says which of the two it is;
|
|
34
|
+
* the CLI prints it and never computes over a partial set in silence.
|
|
35
|
+
*
|
|
36
|
+
* THREE states (#1423): `complete === null` is a THIRD reading — no source
|
|
37
|
+
* existed at all, so `invocations: 0` is UNMEASURED, not a measured zero, and
|
|
38
|
+
* `meetsPromotionGate: false` below rests on nothing. The CLI names that case
|
|
39
|
+
* separately; folding it into `false` would report a gap nobody found, folding
|
|
40
|
+
* it into `true` would report a measurement nobody took.
|
|
41
|
+
*
|
|
42
|
+
* CEILING (BV-004): `readEventsWithRotations` loads the active file and every
|
|
43
|
+
* archive fully into memory — up to ~60 MB transient at the default
|
|
44
|
+
* `max-size-mb: 10` / `max-backups: 5`. Acceptable here because this is a COLD
|
|
45
|
+
* ops path: a hand-run CLI / promotion-gate check, never a hook and never on
|
|
46
|
+
* the session path. Revisit if this module gains a hot-path caller or if
|
|
47
|
+
* `max-size-mb` is raised past ~100.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
|
|
50
|
+
* @returns {{events: Array<object>, complete: boolean|null, gaps: Array<object>,
|
|
51
|
+
* notices: Array<object>}}
|
|
52
|
+
*/
|
|
53
|
+
export function readTmuxEventsEnvelope(eventsPath = EVENTS_PATH) {
|
|
54
|
+
const { events, gaps, complete, notices } = readEventsWithRotations(undefined, {
|
|
55
|
+
filePath: eventsPath,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
events: events.filter(
|
|
59
|
+
(rec) => rec && typeof rec.event === 'string' && rec.event.startsWith(TMUX_EVENT_PREFIX),
|
|
60
|
+
),
|
|
61
|
+
complete,
|
|
62
|
+
gaps,
|
|
63
|
+
notices: notices ?? [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
/**
|
|
21
|
-
* Read events
|
|
68
|
+
* Read the events ledger and return tmux-layout-related events.
|
|
69
|
+
*
|
|
70
|
+
* Thin array-returning view of {@link readTmuxEventsEnvelope} — the shape every
|
|
71
|
+
* existing caller expects. Use the envelope when the completeness of the read
|
|
72
|
+
* matters to the answer.
|
|
22
73
|
*
|
|
23
74
|
* @param {string} [eventsPath=.orchestrator/metrics/events.jsonl]
|
|
24
75
|
* @returns {Array<object>} parsed event records (filtered to tmux-layout.* events)
|
|
25
76
|
*/
|
|
26
77
|
export function readTmuxEvents(eventsPath = EVENTS_PATH) {
|
|
27
|
-
|
|
28
|
-
const raw = readFileSync(eventsPath, 'utf-8');
|
|
29
|
-
const lines = raw.split('\n').filter((l) => l.trim().length > 0);
|
|
30
|
-
return lines
|
|
31
|
-
.map((l) => {
|
|
32
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
33
|
-
})
|
|
34
|
-
.filter((rec) => rec && typeof rec.event === 'string' && rec.event.startsWith('tmux-layout.'));
|
|
78
|
+
return readTmuxEventsEnvelope(eventsPath).events;
|
|
35
79
|
}
|
|
36
80
|
|
|
37
81
|
/**
|
|
@@ -93,8 +137,24 @@ export function computeStats(events) {
|
|
|
93
137
|
}
|
|
94
138
|
|
|
95
139
|
// CLI entry-point — emit stats as JSON to stdout when run directly
|
|
96
|
-
if (import.meta.url
|
|
97
|
-
const events =
|
|
140
|
+
if (isMainModule(import.meta.url)) {
|
|
141
|
+
const { events, complete, gaps } = readTmuxEventsEnvelope();
|
|
98
142
|
const stats = computeStats(events);
|
|
99
|
-
|
|
143
|
+
if (complete === null) {
|
|
144
|
+
// NOT MEASURED — a different sentence from "incomplete" (#1423): no active
|
|
145
|
+
// file and no archive means the zeros below were never observed.
|
|
146
|
+
console.error(
|
|
147
|
+
'WARN: no events source was readable (no active file, no archive) — the stats below are UNMEASURED, not zero',
|
|
148
|
+
);
|
|
149
|
+
} else if (!complete) {
|
|
150
|
+
// A gap, never an empty window: diagnostics on stderr (cli-design.md),
|
|
151
|
+
// the machine-readable verdict in the JSON below.
|
|
152
|
+
const detail = gaps
|
|
153
|
+
.map((g) => `${g.kind}:${path.basename(String(g.archived_as ?? 'unknown'))}`)
|
|
154
|
+
.join(', ');
|
|
155
|
+
console.error(
|
|
156
|
+
`WARN: events ledger incomplete — the all-time rate below is computed over a PARTIAL set (${gaps.length} gap(s): ${detail})`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
console.log(JSON.stringify({ ...stats, ledgerComplete: complete, ledgerGaps: gaps }, null, 2));
|
|
100
160
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/user-invocable-skills.mjs
|
|
3
|
+
*
|
|
4
|
+
* ONE definition of "this skill is an operator-facing slash command".
|
|
5
|
+
*
|
|
6
|
+
* Since the #1370 command→skill fold (2026-09-16) `commands/` holds only the two
|
|
7
|
+
* names that cannot be a skill (`session`, `templates-ack` — see
|
|
8
|
+
* `tests/commands/headless-bare-command-availability.test.mjs` for the measured
|
|
9
|
+
* mechanism). Every other slash command IS a skill, marked by an EXPLICIT
|
|
10
|
+
* `user-invocable: true` in its SKILL.md frontmatter. Consumers that used to
|
|
11
|
+
* count or link `commands/*.md` therefore need the union:
|
|
12
|
+
*
|
|
13
|
+
* commands/*.md ∪ skills with explicit `user-invocable: true`
|
|
14
|
+
*
|
|
15
|
+
* This module exists so that union has one implementation rather than one per
|
|
16
|
+
* consumer (site-numbers tile, sunset walker linkage, the guard tests). It is
|
|
17
|
+
* deliberately tiny and dependency-free: `scripts/lib/sunset/walker.mjs` imports it,
|
|
18
|
+
* and the walker sits in the hook import graph, which must load WITHOUT
|
|
19
|
+
* node_modules (GH#62/#63) — so no `js-yaml` here; the frontmatter parser is the
|
|
20
|
+
* repo's own `parseAgentFrontmatter`, already in that graph.
|
|
21
|
+
*
|
|
22
|
+
* PARSED, never line-matched: `.claude/rules/test-hygiene.md` records that a
|
|
23
|
+
* line-regex frontmatter reader is blind to unparseable YAML and mis-measures
|
|
24
|
+
* block scalars. A SKILL.md whose frontmatter does not parse is treated as NOT
|
|
25
|
+
* user-invocable — fail-closed, and loud where it matters because such a file
|
|
26
|
+
* already fails `check-skills`/`validate-plugin`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { parseAgentFrontmatter } from './agent-frontmatter.mjs';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* YAML 1.1 boolean lookalikes that YAML 1.2 / js-yaml's CORE_SCHEMA (what
|
|
35
|
+
* `generate-codex-skills` parses with) reads as PLAIN STRINGS, not as `true`.
|
|
36
|
+
* They are therefore NOT user-invocable here either — but silently demoting a
|
|
37
|
+
* skill on one of them is the #1370 defect class, so each one gets a WARN.
|
|
38
|
+
*/
|
|
39
|
+
const TRUTHY_LOOKALIKE = /^(?:yes|y|on|t|1)$/i;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Strip a trailing `#` comment, honouring quotes: `true # note` is the same
|
|
43
|
+
* declaration as `true`, but `"a # b"` carries the hash as content.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function stripTrailingComment(text) {
|
|
49
|
+
let quote = null;
|
|
50
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
51
|
+
const ch = text[i];
|
|
52
|
+
if (quote) {
|
|
53
|
+
if (ch === quote) quote = null;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"' || ch === "'") {
|
|
57
|
+
quote = ch;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// A `#` opens a YAML comment only at the start or after whitespace.
|
|
61
|
+
if (ch === '#' && (i === 0 || /\s/.test(text[i - 1]))) return text.slice(0, i);
|
|
62
|
+
}
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalise a raw frontmatter scalar to the token a YAML parser would see:
|
|
68
|
+
* trim, drop a trailing comment, drop one layer of matching surrounding quotes.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} text
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function normaliseScalar(text) {
|
|
74
|
+
let out = stripTrailingComment(text).trim();
|
|
75
|
+
if (out.length >= 2 && (out[0] === '"' || out[0] === "'") && out[out.length - 1] === out[0]) {
|
|
76
|
+
out = out.slice(1, -1).trim();
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* THE one normaliser for `user-invocable` — the marker "this skill is an
|
|
83
|
+
* operator-facing slash command". Every generator (Cursor, Pi, Codex) and every
|
|
84
|
+
* counter (site-numbers tile, drift-check `command-count`, the guard tests) must
|
|
85
|
+
* route through this function; four private re-implementations disagreeing on a
|
|
86
|
+
* quoted value is the defect this replaces.
|
|
87
|
+
*
|
|
88
|
+
* Explicit-true only. `undefined` (flag absent) is NOT user-invocable: the whole
|
|
89
|
+
* point of the marker is that it is written down, so a skill that forgot it is a
|
|
90
|
+
* defect to surface, not a default to guess.
|
|
91
|
+
*
|
|
92
|
+
* Accepted: the boolean `true`, and a string that normalises to `true`
|
|
93
|
+
* case-insensitively — so `"true"`, `'true'`, `True`, `true # note` and
|
|
94
|
+
* `true ` (trailing space, invisible in an editor) all read as the same
|
|
95
|
+
* declaration. Trailing-whitespace tolerance is load-bearing rather than
|
|
96
|
+
* cosmetic: without it a stray space silently demotes a skill out of the
|
|
97
|
+
* generated adapters with no diagnostic anywhere.
|
|
98
|
+
*
|
|
99
|
+
* REJECTED: `yes` / `y` / `on` / `t` / `1`. Those are YAML 1.1 booleans; under
|
|
100
|
+
* YAML 1.2 — which is what js-yaml's CORE_SCHEMA in `generate-codex-skills`
|
|
101
|
+
* applies — they are plain strings, and this predicate follows the parser rather
|
|
102
|
+
* than inventing a fifth dialect. Because the demotion is the surprising half,
|
|
103
|
+
* each one emits exactly one stderr WARN naming the file.
|
|
104
|
+
*
|
|
105
|
+
* The same normalisation governs the SIBLING boolean marker
|
|
106
|
+
* `disable-model-invocation` (the Cursor and Codex adapters read it with this
|
|
107
|
+
* predicate). The WARN therefore names the key it was asked about rather than a
|
|
108
|
+
* hard-coded `user-invocable`: measured 2026-09-18, a `disable-model-invocation:
|
|
109
|
+
* yes` demotion reported `user-invocable: "yes"`, sending the operator to a
|
|
110
|
+
* line that does not exist in that file.
|
|
111
|
+
*
|
|
112
|
+
* The NOUN follows the key too (#1388 P10). Naming the key while hard-coding
|
|
113
|
+
* "is NOT a slash-command marker" told a `disable-model-invocation: yes` author
|
|
114
|
+
* that his value failed to be something it never was — a correct key with a
|
|
115
|
+
* wrong subject reads as a broken diagnostic. The default phrase is unchanged
|
|
116
|
+
* for `user-invocable`, so the four existing call sites see identical text.
|
|
117
|
+
*
|
|
118
|
+
* @param {unknown} value the raw frontmatter value
|
|
119
|
+
* @param {string} [file] path named in the WARN when a truthy-looking value is demoted
|
|
120
|
+
* @param {string} [key] frontmatter key named in the WARN (default `user-invocable`)
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
const MARKER_NOUN = Object.freeze({
|
|
124
|
+
'user-invocable': 'a slash-command marker',
|
|
125
|
+
'disable-model-invocation': 'a model-invocation opt-out marker',
|
|
126
|
+
});
|
|
127
|
+
const DEFAULT_MARKER_NOUN = 'a boolean marker';
|
|
128
|
+
|
|
129
|
+
export function isUserInvocableValue(value, file, key = 'user-invocable') {
|
|
130
|
+
if (value === true) return true;
|
|
131
|
+
if (typeof value !== 'string') return false;
|
|
132
|
+
const token = normaliseScalar(value);
|
|
133
|
+
if (token.toLowerCase() === 'true') return true;
|
|
134
|
+
if (TRUTHY_LOOKALIKE.test(token)) {
|
|
135
|
+
const where = file ? `${file}: ` : '';
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
`WARN ${where}${key}: ${JSON.stringify(value)} is NOT ${MARKER_NOUN[key] ?? DEFAULT_MARKER_NOUN} — `
|
|
138
|
+
+ `only \`true\` is (YAML 1.2 reads yes/on/1 as strings). Write \`${key}: true\`.\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a SKILL.md's YAML frontmatter. Returns `null` when there is no
|
|
146
|
+
* frontmatter block or it does not parse.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} content
|
|
149
|
+
* @returns {Record<string, unknown>|null}
|
|
150
|
+
*/
|
|
151
|
+
export function parseSkillFrontmatter(content) {
|
|
152
|
+
if (typeof content !== 'string') return null;
|
|
153
|
+
const parsed = parseAgentFrontmatter(content);
|
|
154
|
+
return parsed.ok ? parsed.frontmatter : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Names of skills carrying an explicit `user-invocable: true`, sorted.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} repoRoot plugin root (the directory holding `skills/`)
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
export function userInvocableSkills(repoRoot) {
|
|
164
|
+
const dir = path.join(repoRoot, 'skills');
|
|
165
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
166
|
+
const names = [];
|
|
167
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
168
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
169
|
+
const file = path.join(dir, entry.name, 'SKILL.md');
|
|
170
|
+
if (!existsSync(file) || !statSync(file).isFile()) continue;
|
|
171
|
+
let fm;
|
|
172
|
+
try {
|
|
173
|
+
fm = parseSkillFrontmatter(readFileSync(file, 'utf8'));
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (fm && isUserInvocableValue(fm['user-invocable'], file)) names.push(entry.name);
|
|
178
|
+
}
|
|
179
|
+
return names.sort();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Names of `commands/*.md` files (without the extension), sorted. */
|
|
183
|
+
export function commandFileNames(repoRoot) {
|
|
184
|
+
const dir = path.join(repoRoot, 'commands');
|
|
185
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
186
|
+
return readdirSync(dir)
|
|
187
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
|
|
188
|
+
.map((f) => f.replace(/\.md$/, ''))
|
|
189
|
+
.sort();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The product's "slash commands" set: `commands/*.md` ∪ explicitly
|
|
194
|
+
* user-invocable skills, deduplicated by NAME.
|
|
195
|
+
*
|
|
196
|
+
* Dedup is load-bearing rather than defensive: a name present as BOTH is the
|
|
197
|
+
* picker-duplicate bug (the body runs, and the entry lists twice), so it must
|
|
198
|
+
* count once here and go red in the guard test — never inflate the number.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} repoRoot
|
|
201
|
+
* @returns {string[]} sorted unique names
|
|
202
|
+
*/
|
|
203
|
+
export function slashCommandNames(repoRoot) {
|
|
204
|
+
return [...new Set([...commandFileNames(repoRoot), ...userInvocableSkills(repoRoot)])].sort();
|
|
205
|
+
}
|