session-orchestrator 5.2.0 → 5.4.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 +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -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 +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- 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 +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- 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 +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- 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 +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- 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 +5 -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 +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -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 +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/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 +5 -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/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +9 -30
- package/CHANGELOG.md +206 -0
- package/README.md +13 -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 +7 -3
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +22 -7
- package/docs/ci-setup.md +57 -4
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +12 -10
- package/docs/events-schema.md +65 -15
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +84 -17
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +154 -12
- package/docs/session-config-template.md +41 -3
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +87 -6
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- 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-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +101 -11
- package/hooks/on-stop.mjs +170 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -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 +202 -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 +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
- package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +792 -74
- package/hooks/subagent-telemetry.mjs +111 -38
- 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 +3 -3
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +7 -2
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +6 -2
- package/rules/opt-in-stack/backend-data.md +10 -2
- package/rules/opt-in-stack/backend.md +10 -4
- package/rules/opt-in-stack/frontend.md +7 -2
- package/rules/opt-in-stack/security-web.md +10 -4
- package/rules/opt-in-stack/swift.md +6 -2
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +70 -53
- package/scripts/backfill-abandoned-sessions.mjs +118 -16
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +371 -31
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- 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/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +18 -2
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +19 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +163 -0
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +85 -10
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- 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 +545 -7
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +40 -5
- package/scripts/lib/gates/gate-full.mjs +35 -5
- package/scripts/lib/gates/gate-helpers.mjs +336 -23
- package/scripts/lib/gates/gate-incremental.mjs +49 -23
- package/scripts/lib/gates/gate-per-file.mjs +42 -8
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +208 -8
- package/scripts/lib/io.mjs +42 -8
- 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/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +316 -0
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +950 -0
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +152 -33
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- 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/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +141 -74
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +56 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +118 -4
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/sh-quote.mjs +29 -0
- 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/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- 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 +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- 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 +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +210 -84
- 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-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +306 -0
- package/scripts/release.mjs +40 -8
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +357 -197
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +16 -4
- package/scripts/validate-plugin.mjs +19 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +81 -47
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +47 -18
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- 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/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +3 -3
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +32 -60
- package/skills/session-plan/SKILL.md +17 -8
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +9 -5
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
112
112
|
import path from 'node:path';
|
|
113
113
|
import { isMainModule } from '../is-main-module.mjs';
|
|
114
|
+
import { maskSource } from '../js-mask.mjs';
|
|
114
115
|
|
|
115
116
|
/** Only this directory is scanned — a mutation in `scripts/` is production intent. */
|
|
116
117
|
const SCAN_DIR = 'tests';
|
|
@@ -487,43 +488,24 @@ export function insideStringLiteral(body, index) {
|
|
|
487
488
|
* "no cwd" measured as a cwd. `inCommentLine` cannot serve here: it answers a
|
|
488
489
|
* question about a whole LINE, while this one runs inside a call expression.
|
|
489
490
|
*
|
|
491
|
+
* Delegates to the shared lexer {@link maskSource} in `keepLiterals` mode
|
|
492
|
+
* (#1388). The hand-rolled walker this replaced had no REGEX-LITERAL branch,
|
|
493
|
+
* so a call tail carrying one — measured 2026-09-18:
|
|
494
|
+
* `wrapperHasCwd(", undefined, { env: /\/*$/.source, cwd: dir }")` — read the
|
|
495
|
+
* regex's `/*` as a block-comment opener, swallowed the rest of the tail and
|
|
496
|
+
* returned `false`: a false alarm from this check.
|
|
497
|
+
*
|
|
498
|
+
* One behavioural difference the callers must honour: `maskSource` blanks a
|
|
499
|
+
* comment to SPACES (length-preserving) where this function used to collapse
|
|
500
|
+
* it to a single space. {@link wrapperHasCwd} judges its second positional
|
|
501
|
+
* textually against `second !== ''`, so that difference was checked rather
|
|
502
|
+
* than assumed — see the measurement recorded at that test.
|
|
503
|
+
*
|
|
490
504
|
* @param {string} text
|
|
491
|
-
* @returns {string} same length
|
|
505
|
+
* @returns {string} same length, comment bytes blanked to spaces
|
|
492
506
|
*/
|
|
493
507
|
export function stripComments(text) {
|
|
494
|
-
|
|
495
|
-
/** @type {string|null} */
|
|
496
|
-
let quote = null;
|
|
497
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
498
|
-
const ch = text[i];
|
|
499
|
-
if (quote !== null) {
|
|
500
|
-
out += ch;
|
|
501
|
-
if (ch === '\\') {
|
|
502
|
-
out += text[i + 1] ?? '';
|
|
503
|
-
i += 1;
|
|
504
|
-
} else if (ch === quote) quote = null;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
if (ch === "'" || ch === '"' || ch === '`') {
|
|
508
|
-
quote = ch;
|
|
509
|
-
out += ch;
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
if (ch === '/' && text[i + 1] === '*') {
|
|
513
|
-
const end = text.indexOf('*/', i + 2);
|
|
514
|
-
i = end === -1 ? text.length : end + 1;
|
|
515
|
-
out += ' ';
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
if (ch === '/' && text[i + 1] === '/') {
|
|
519
|
-
const end = text.indexOf('\n', i);
|
|
520
|
-
i = end === -1 ? text.length : end - 1;
|
|
521
|
-
out += ' ';
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
out += ch;
|
|
525
|
-
}
|
|
526
|
-
return out;
|
|
508
|
+
return maskSource(text, { keepLiterals: true });
|
|
527
509
|
}
|
|
528
510
|
|
|
529
511
|
/**
|
|
@@ -549,6 +531,14 @@ export function wrapperHasCwd(tail) {
|
|
|
549
531
|
const clean = stripComments(tail);
|
|
550
532
|
const m = /^\s*,\s*/.exec(clean);
|
|
551
533
|
if (m) {
|
|
534
|
+
// NO `.trim()` here, and that is measured rather than assumed (#1388): the
|
|
535
|
+
// shared lexer blanks a comment to SPACES where the local stripper used to
|
|
536
|
+
// delete it, so a comment-only tail LOOKS like it would arrive as truthy
|
|
537
|
+
// whitespace. It cannot — the `\s*` above is greedy and consumes every
|
|
538
|
+
// blanked byte, leaving `second === ''`. Measured 2026-09-18 over
|
|
539
|
+
// `, /* no cwd */`, `, /* a */ /* b */`, `,\n // note\n`,
|
|
540
|
+
// `, /* x */ undefined`, `, undefined /* y */`: all `false`, `, /* c */ dir`
|
|
541
|
+
// → `true`. A `.trim()` would be dead code; the case is pinned by a test.
|
|
552
542
|
const second = clean.slice(m[0].length);
|
|
553
543
|
if (second !== '' && !/^(?:undefined|null)\b/.test(second) && !/^,/.test(second)) return true;
|
|
554
544
|
}
|
|
@@ -93,7 +93,8 @@
|
|
|
93
93
|
* pure input→output date function is out of scope by
|
|
94
94
|
* construction rather than by exception list.
|
|
95
95
|
*
|
|
96
|
-
* NOT flagged: blocks that control the clock (`now
|
|
96
|
+
* NOT flagged: blocks that control the clock (`now:`, `{ now }`
|
|
97
|
+
* or a positional `now` — wider than the seam proof, #1478 —
|
|
97
98
|
* `vi.useFakeTimers` / `vi.setSystemTime`); date literals in
|
|
98
99
|
* INPUT position (only `.toBe`/`.toEqual`/`.toStrictEqual`
|
|
99
100
|
* expected values are read), which leaves the passthrough
|
|
@@ -335,18 +336,68 @@ const keyLiteralRe = (key) => new RegExp(`["'\\\\]+${key}["'\\\\]+\\s*:`);
|
|
|
335
336
|
* An absolute date pinned as the EXPECTED value of an equality assertion.
|
|
336
337
|
* Input-position date literals (`created_at: '2026-06-21T…'`) do not match —
|
|
337
338
|
* that is what keeps the passthrough class (input date === output date) out.
|
|
339
|
+
*
|
|
340
|
+
* CEILING (#1473): a fixed INPUT date that a production TTL ages out later is
|
|
341
|
+
* not flagged either, on purpose. Measured 2026-09-29 with a shim that moves
|
|
342
|
+
* `Date.now()` and `new Date()` forward in every vitest worker: the 34 test
|
|
343
|
+
* files (plus one fixture module) holding a fixed `created_at: '2026-05…09'`
|
|
344
|
+
* (1,211 tests) stayed green at +120, +365 and +3,650 days, so a rule keyed on
|
|
345
|
+
* fixed input dates would have reported false positives only. The one real
|
|
346
|
+
* bomb of that shape, the phase-skip fixture before 6670149c, was green at
|
|
347
|
+
* -60 days and red at +0 and +120 days under the same shim — and
|
|
348
|
+
* scanClockBombs() reaches that file since #1476 (alias imports are read) but
|
|
349
|
+
* stays silent on it: the file hands no clock argument to anything. REVISIT when a
|
|
350
|
+
* second fixed-input-date bomb lands on main: the class then recurs, and a
|
|
351
|
+
* clock-shift run of the suite is the check that finds it without guessing.
|
|
338
352
|
*/
|
|
339
353
|
const DATE_EXPECTATION =
|
|
340
354
|
/\.(?:toBe|toEqual|toStrictEqual)\(\s*(['"`])(\d{4}-\d{2}-\d{2}(?:[T ][^'"`]*)?)\1\s*\)/;
|
|
341
355
|
|
|
342
|
-
/**
|
|
343
|
-
const
|
|
356
|
+
/** A clock-named token not read off an object (`Date.now`, `opts.now`) — shared head of the two below. */
|
|
357
|
+
const CLOCK_NAME = String.raw`(?<![.\w$])(?:now|nowMs|nowIso|clock|currentDate)\s*`;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Pass 1, the seam PROOF: a NAMED clock key (`{ now: … }`) — only that shape says
|
|
361
|
+
* the callee's API takes a clock. Every line it matches also matches
|
|
362
|
+
* CONTROL_CLOCK_ARG, so a block that proves a seam is never flagged itself.
|
|
363
|
+
*/
|
|
364
|
+
const SEAM_CLOCK_ARG = new RegExp(`${CLOCK_NAME}:`);
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Pass 2, is this block clock-CONTROLLED: any clock handover. Property shorthand
|
|
368
|
+
* (`{ now }`, `{ repoRoot, now }`) and a positional `now` count too; `Date.now()`
|
|
369
|
+
* and `const now =` do not. Wider is the safe direction here — it can only
|
|
370
|
+
* remove findings.
|
|
371
|
+
*
|
|
372
|
+
* CEILING (#1478): pass 1 reads only the named form, because the wide form also
|
|
373
|
+
* turns helpers into seams — +20 seamed (file, id) pairs in 15 files
|
|
374
|
+
* (172 → 192, measured 2026-10-01 @ ffc5929f), e.g. `expectDeny` via a
|
|
375
|
+
* `{ manifestMtimeMs: now }` fixture. Cost: a file whose ONLY clock handover is
|
|
376
|
+
* `{ now }` or a positional `now` loses its seam proof and its bombs go unseen.
|
|
377
|
+
* REVISIT on the first B5 finding whose subject is a helper, or the first CI
|
|
378
|
+
* `test-value-bans` B5 finding the ±days clock-shift probe shows to be false.
|
|
379
|
+
*/
|
|
380
|
+
const CONTROL_CLOCK_ARG = new RegExp(String.raw`${CLOCK_NAME}(?::|,|\}|\))`);
|
|
344
381
|
|
|
345
382
|
/** Freezing the global clock — equally valid control, but not a seam PROOF. */
|
|
346
383
|
const FAKE_TIMER = /\b(?:useFakeTimers|setSystemTime|advanceTimersByTime|runAllTimers)\b/;
|
|
347
384
|
|
|
348
|
-
/**
|
|
349
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Vitest aliases that resolve to the repo's own modules — source of truth is
|
|
387
|
+
* `resolve.alias` in vitest.config.mjs (today only `@lib`). CEILING: one entry
|
|
388
|
+
* per alias, kept by hand; REVISIT when vitest.config.mjs gains an alias — the
|
|
389
|
+
* parametrised B5 test reads that config and goes red until the key is listed
|
|
390
|
+
* here. Dynamic imports (`await import('@lib/…')`) are not read in either form.
|
|
391
|
+
*/
|
|
392
|
+
const ALIAS_PREFIXES = ['@lib/'];
|
|
393
|
+
|
|
394
|
+
/** `import { a, b as c } from './rel.mjs'` or `'@lib/x.mjs'` — SUT candidates live behind these. */
|
|
395
|
+
const LOCAL_IMPORT = new RegExp(
|
|
396
|
+
String.raw`import\s+([^;]+?)\s+from\s+['"]((?:\.|` +
|
|
397
|
+
ALIAS_PREFIXES.map((p) => p.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|') +
|
|
398
|
+
String.raw`)[^'"]+)['"]`,
|
|
399
|
+
'g',
|
|
400
|
+
);
|
|
350
401
|
|
|
351
402
|
// ---------------------------------------------------------------------------
|
|
352
403
|
// File enumeration
|
|
@@ -588,17 +639,17 @@ function testBlocks(lines) {
|
|
|
588
639
|
}
|
|
589
640
|
|
|
590
641
|
/**
|
|
591
|
-
* Identifiers this test file imports from the repo's OWN modules (relative
|
|
592
|
-
* specifiers). These are the subject-under-test candidates for B5;
|
|
593
|
-
* (`vitest`) and stdlib (`node:*`) imports are structurally excluded
|
|
594
|
-
* their specifiers
|
|
642
|
+
* Identifiers this test file imports from the repo's OWN modules (relative or
|
|
643
|
+
* vitest-alias specifiers). These are the subject-under-test candidates for B5;
|
|
644
|
+
* framework (`vitest`) and stdlib (`node:*`) imports are structurally excluded
|
|
645
|
+
* because their specifiers match neither form.
|
|
595
646
|
* @param {string} content
|
|
596
647
|
* @returns {string[]}
|
|
597
648
|
*/
|
|
598
649
|
function importedLocalIdentifiers(content) {
|
|
599
650
|
/** @type {Set<string>} */
|
|
600
651
|
const ids = new Set();
|
|
601
|
-
for (const m of content.matchAll(
|
|
652
|
+
for (const m of content.matchAll(LOCAL_IMPORT)) {
|
|
602
653
|
const clause = m[1];
|
|
603
654
|
// `{ a, b as c }` → c ; `x` / `* as ns` → x / ns
|
|
604
655
|
for (const part of clause.replace(/[{}]/g, ',').split(',')) {
|
|
@@ -623,8 +674,9 @@ function callsIdentifier(lines, id) {
|
|
|
623
674
|
*
|
|
624
675
|
* Two passes over the file's `it`/`test` blocks:
|
|
625
676
|
* 1. PROVE the seam — an imported id called from a block that also hands over
|
|
626
|
-
*
|
|
627
|
-
* `now` parameter because the function reads the clock on its
|
|
677
|
+
* a NAMED clock key (SEAM_CLOCK_ARG) is clock-seamed. A public API only
|
|
678
|
+
* grows a `now` parameter because the function reads the clock on its
|
|
679
|
+
* main path.
|
|
628
680
|
* 2. FLAG — in blocks with NO clock control at all, any equality assertion
|
|
629
681
|
* pinning an absolute date against such a subject is a time bomb.
|
|
630
682
|
*
|
|
@@ -648,7 +700,8 @@ function scanClockBombs(relPath, content, lines) {
|
|
|
648
700
|
return {
|
|
649
701
|
start,
|
|
650
702
|
body,
|
|
651
|
-
|
|
703
|
+
provesSeam: live.some((l) => SEAM_CLOCK_ARG.test(l)),
|
|
704
|
+
hasClockArg: live.some((l) => CONTROL_CLOCK_ARG.test(l)),
|
|
652
705
|
hasFakeTimer: live.some((l) => FAKE_TIMER.test(l)),
|
|
653
706
|
};
|
|
654
707
|
});
|
|
@@ -656,19 +709,39 @@ function scanClockBombs(relPath, content, lines) {
|
|
|
656
709
|
/** @type {Set<string>} */
|
|
657
710
|
const seamed = new Set();
|
|
658
711
|
for (const b of blocks) {
|
|
659
|
-
if (!b.
|
|
712
|
+
if (!b.provesSeam) continue;
|
|
660
713
|
for (const id of sutIds) if (callsIdentifier(b.body, id)) seamed.add(id);
|
|
661
714
|
}
|
|
662
715
|
if (seamed.size === 0) return findings;
|
|
663
716
|
|
|
717
|
+
// Where an echo may come from: the finding's own block, or the setup every
|
|
718
|
+
// block shares (helpers, constants, beforeEach). Never a sibling test — its
|
|
719
|
+
// assertions, titles and clock literals say nothing about this block's inputs.
|
|
720
|
+
// A block without its own `});` (a one-line it) runs on to the next sibling
|
|
721
|
+
// and takes the setup lines in between out of sharedSetup — that direction can
|
|
722
|
+
// only add a finding, never hide one (1.4% of blocks, measured 2026-09-30).
|
|
723
|
+
/** @type {Set<number>} */
|
|
724
|
+
const inBlock = new Set();
|
|
725
|
+
for (const b of blocks) for (let q = b.start; q < b.start + b.body.length; q++) inBlock.add(q);
|
|
726
|
+
const sharedSetup = lines.filter((_, q) => !inBlock.has(q));
|
|
727
|
+
|
|
664
728
|
for (const b of blocks) {
|
|
665
729
|
if (b.hasClockArg || b.hasFakeTimer) continue;
|
|
666
730
|
const subject = [...seamed].find((id) => callsIdentifier(b.body, id));
|
|
667
731
|
if (!subject) continue;
|
|
732
|
+
const echoSources = [...b.body, ...sharedSetup].filter(
|
|
733
|
+
(l) => !isCommentLine(l) && !DATE_EXPECTATION.test(l),
|
|
734
|
+
);
|
|
668
735
|
b.body.forEach((line, k) => {
|
|
669
736
|
if (isCommentLine(line)) return;
|
|
670
737
|
const m = DATE_EXPECTATION.exec(line);
|
|
671
738
|
if (!m) return;
|
|
739
|
+
// Echo: the pinned value is also an input of this block or its shared setup,
|
|
740
|
+
// not a clock reading (the CEILING above keeps fixed INPUT dates unflagged on
|
|
741
|
+
// purpose). CEILING: a real bomb whose date also stands in its own block or
|
|
742
|
+
// the shared setup (its title, a trailing comment, a helper fixture) is
|
|
743
|
+
// swallowed; REVISIT when a clock-shift run finds a bomb this scan missed.
|
|
744
|
+
if (echoSources.some((l) => l.includes(m[2]))) return;
|
|
672
745
|
findings.push({
|
|
673
746
|
file: relPath,
|
|
674
747
|
line: b.start + k + 1,
|
|
@@ -108,6 +108,13 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
108
108
|
import { spawnSync } from 'node:child_process';
|
|
109
109
|
import path from 'node:path';
|
|
110
110
|
import { isMainModule } from '../is-main-module.mjs';
|
|
111
|
+
import { maskSource } from '../js-mask.mjs';
|
|
112
|
+
|
|
113
|
+
// `maskSource` moved to `scripts/lib/js-mask.mjs` (#1388 — three other modules
|
|
114
|
+
// needed the same lexer). `package.json` carries no `exports` map, so every
|
|
115
|
+
// `scripts/lib/**` path is public API: this re-export keeps the OLD import path
|
|
116
|
+
// working for any consumer that already reaches in here.
|
|
117
|
+
export { maskSource } from '../js-mask.mjs';
|
|
111
118
|
|
|
112
119
|
// ---------------------------------------------------------------------------
|
|
113
120
|
// Constants
|
|
@@ -167,108 +174,6 @@ const NON_CALLEES = new Set([
|
|
|
167
174
|
'await', 'new', 'do', 'else', 'in', 'of', 'delete', 'void', 'yield', 'import',
|
|
168
175
|
]);
|
|
169
176
|
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// Source masking — string/template/comment/regex interiors become spaces
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Blank out the INTERIOR of every string literal, template literal, comment and
|
|
176
|
-
* regex literal, preserving byte offsets and newlines. Delimiters are kept so
|
|
177
|
-
* paren/comma balance and identifier boundaries survive.
|
|
178
|
-
*
|
|
179
|
-
* This is what separates CODE from DATA. A test that embeds fixture source in a
|
|
180
|
-
* template literal (this check's own test file does) must not be scanned as
|
|
181
|
-
* though that fixture were its own body — masking makes that structural instead
|
|
182
|
-
* of relying on a self-exemption marker.
|
|
183
|
-
*
|
|
184
|
-
* @param {string} text
|
|
185
|
-
* @returns {string} same-length masked source
|
|
186
|
-
*/
|
|
187
|
-
export function maskSource(text) {
|
|
188
|
-
const out = text.split('');
|
|
189
|
-
const n = out.length;
|
|
190
|
-
let i = 0;
|
|
191
|
-
// Last significant (non-space) char before `i`, used for the regex/division
|
|
192
|
-
// disambiguation below.
|
|
193
|
-
let prev = '';
|
|
194
|
-
|
|
195
|
-
const blank = (from, to) => {
|
|
196
|
-
for (let k = from; k < to && k < n; k++) if (out[k] !== '\n') out[k] = ' ';
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
while (i < n) {
|
|
200
|
-
const c = text[i];
|
|
201
|
-
const next = text[i + 1];
|
|
202
|
-
|
|
203
|
-
// Line comment.
|
|
204
|
-
if (c === '/' && next === '/') {
|
|
205
|
-
let j = i;
|
|
206
|
-
while (j < n && text[j] !== '\n') j++;
|
|
207
|
-
blank(i, j);
|
|
208
|
-
i = j;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
// Block comment.
|
|
212
|
-
if (c === '/' && next === '*') {
|
|
213
|
-
let j = i + 2;
|
|
214
|
-
while (j < n && !(text[j] === '*' && text[j + 1] === '/')) j++;
|
|
215
|
-
blank(i, Math.min(j + 2, n));
|
|
216
|
-
i = Math.min(j + 2, n);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
// Single/double-quoted string — keep the quotes, blank the interior.
|
|
220
|
-
if (c === "'" || c === '"') {
|
|
221
|
-
let j = i + 1;
|
|
222
|
-
while (j < n && text[j] !== c) {
|
|
223
|
-
if (text[j] === '\\') j++;
|
|
224
|
-
if (text[j] === '\n') break; // unterminated — bail at EOL
|
|
225
|
-
j++;
|
|
226
|
-
}
|
|
227
|
-
blank(i + 1, j);
|
|
228
|
-
i = Math.min(j + 1, n);
|
|
229
|
-
prev = c;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
// Template literal — blank everything including `${…}` substitutions.
|
|
233
|
-
if (c === '`') {
|
|
234
|
-
let j = i + 1;
|
|
235
|
-
while (j < n && text[j] !== '`') {
|
|
236
|
-
if (text[j] === '\\') j++;
|
|
237
|
-
j++;
|
|
238
|
-
}
|
|
239
|
-
blank(i + 1, j);
|
|
240
|
-
i = Math.min(j + 1, n);
|
|
241
|
-
prev = c;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
// Regex literal — only when the previous significant char cannot end an
|
|
245
|
-
// expression, AND a closing unescaped `/` exists on the SAME line. The
|
|
246
|
-
// same-line bound keeps a misread division from swallowing the file.
|
|
247
|
-
if (c === '/' && (prev === '' || '(,=:[!&|?{};+-*%~^<>'.includes(prev))) {
|
|
248
|
-
let j = i + 1;
|
|
249
|
-
let inClass = false;
|
|
250
|
-
let closed = -1;
|
|
251
|
-
while (j < n && text[j] !== '\n') {
|
|
252
|
-
if (text[j] === '\\') { j += 2; continue; }
|
|
253
|
-
if (text[j] === '[') inClass = true;
|
|
254
|
-
else if (text[j] === ']') inClass = false;
|
|
255
|
-
else if (text[j] === '/' && !inClass) { closed = j; break; }
|
|
256
|
-
j++;
|
|
257
|
-
}
|
|
258
|
-
if (closed > i) {
|
|
259
|
-
blank(i + 1, closed);
|
|
260
|
-
i = closed + 1;
|
|
261
|
-
prev = '/';
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!/\s/.test(c)) prev = c;
|
|
267
|
-
i++;
|
|
268
|
-
}
|
|
269
|
-
return out.join('');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
177
|
// ---------------------------------------------------------------------------
|
|
273
178
|
// Tiny source scanners (offset-aligned against the masked text)
|
|
274
179
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +53,30 @@
|
|
|
53
53
|
* a rule that only cites live instances loses its explanation the moment it works.
|
|
54
54
|
* If you need a CURRENT S2 hit, run the checker; do not assume this one.
|
|
55
55
|
*
|
|
56
|
+
* S6 `parser-only-config-key` — the key IS read, but the ONLY file that
|
|
57
|
+
* reads it is its own block parser
|
|
58
|
+
* `scripts/lib/config/<root>.mjs`.
|
|
59
|
+
*
|
|
60
|
+
* S6 is the mirror image of S2, and it exists because S1 counts the PARSER as a
|
|
61
|
+
* consumer: `CONSUMER_DIRS` is `scripts/` + `hooks/`, and every block parser
|
|
62
|
+
* lives under `scripts/lib/config/`. A key its parser resolves and nothing else
|
|
63
|
+
* reads is `compact-nudge` wearing a value object. Measured 2026-09-22 on this
|
|
64
|
+
* tree: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window` both passed
|
|
65
|
+
* S1 on `scripts/lib/config/reaper.mjs` alone while `scripts/lib/orphan-reaper.mjs`
|
|
66
|
+
* used same-named HARDCODED defaults and never saw the parsed value.
|
|
67
|
+
*
|
|
68
|
+
* Why the signal is this narrow — one read file, and it is the key's OWN parser.
|
|
69
|
+
* The blunt rule ("the parser layer never counts") was measured first and
|
|
70
|
+
* REJECTED: it flips 102 of 263 declared keys to unwired, because the ordinary
|
|
71
|
+
* shape here is a consumer that reads the camelCase FIELD (`cfg.driftCheck.mode`)
|
|
72
|
+
* while this census matches the kebab TOKEN. 102 of 263 is the broken instrument
|
|
73
|
+
* `.claude/rules/host-resources.md` HR-101 forbids. The narrow rule reports 32 —
|
|
74
|
+
* a backlog, printed as an aggregate like S4 and enumerated under `--list`.
|
|
75
|
+
*
|
|
76
|
+
* S6 carries no camelCase fallback ON PURPOSE: adding one re-hides both reaper
|
|
77
|
+
* keys, whose camelCase names exist in `orphan-reaper/defaults.mjs` as that module's own
|
|
78
|
+
* defaults. A name is not a read.
|
|
79
|
+
*
|
|
56
80
|
* S2 applies to TOP-LEVEL keys only — a nested key reaches code through its
|
|
57
81
|
* parent — and its premise is structural: every Session Config key has to pass
|
|
58
82
|
* through the parser layer to become a value. Measured 2026-08-08: 84 of 89
|
|
@@ -393,6 +417,29 @@ const PARSER_PATHS = Object.freeze([
|
|
|
393
417
|
path.join('scripts', 'parse-config.mjs'),
|
|
394
418
|
]);
|
|
395
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The per-block parser directory: `scripts/lib/config/<block>.mjs` turns ONE
|
|
422
|
+
* Session Config block's YAML into a value object and does nothing else.
|
|
423
|
+
*
|
|
424
|
+
* Signal S6 below is about this directory specifically, which is why it is not
|
|
425
|
+
* folded into {@link PARSER_PATHS}.
|
|
426
|
+
*/
|
|
427
|
+
const BLOCK_PARSER_DIR = path.join('scripts', 'lib', 'config');
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The key's OWN block parser, by convention `scripts/lib/config/<root>.mjs`.
|
|
431
|
+
*
|
|
432
|
+
* Deliberately an exact name match and not "anything under the parser dir": a
|
|
433
|
+
* sibling block's parser reading the key is still a second file, and S6 only
|
|
434
|
+
* fires when there is exactly ONE reader and it is the key's own.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} root top-level key of the block
|
|
437
|
+
* @returns {string} plugin-root-relative path
|
|
438
|
+
*/
|
|
439
|
+
function ownBlockParserPath(root) {
|
|
440
|
+
return path.join(BLOCK_PARSER_DIR, `${root}.mjs`);
|
|
441
|
+
}
|
|
442
|
+
|
|
396
443
|
/**
|
|
397
444
|
* Declared-but-unread keys accepted on purpose. Key = full dotted path (S1/S2)
|
|
398
445
|
* or module path relative to the plugin root (S4), value = REASON naming the
|
|
@@ -420,7 +467,8 @@ const ALLOWLIST = Object.freeze({
|
|
|
420
467
|
|
|
421
468
|
/**
|
|
422
469
|
* @typedef {{
|
|
423
|
-
* kind: 'unwired-config-key' | 'parser-orphan-config-key' | '
|
|
470
|
+
* kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'parser-only-config-key'
|
|
471
|
+
* | 'allowlist-missing-reason'
|
|
424
472
|
* | 'allowlist-stale' | 'orphaned-prose-module' | 'unreachable-library-module'
|
|
425
473
|
* | 'coordinator-invoked-module' | 'hand-keyed-learning-subject'
|
|
426
474
|
* | 'tool-error',
|
|
@@ -883,7 +931,9 @@ function mentionedModuleTokens(lines) {
|
|
|
883
931
|
* switched off. Revisit if a real module-resolver (import-specifier resolution
|
|
884
932
|
* relative to the importing file) becomes cheap, or if a collided basename is
|
|
885
933
|
* ever confirmed to mask a true positive. The `coordinator-invoked-module`
|
|
886
|
-
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter
|
|
934
|
+
* DOWNGRADE is exempt, and since #1293 so is the CLUSTER-ROOT filter (since
|
|
935
|
+
* #1298 its predicate also drives the "drags N" count, which had collapsed
|
|
936
|
+
* two dragged `index.mjs` into one): in both
|
|
887
937
|
* a colliding basename must be named with its `dirname/base` suffix, because
|
|
888
938
|
* those matches move a module OUT of the reportable class and would otherwise
|
|
889
939
|
* hide a true unreachable sibling. Measured cost of leaving the root filter
|
|
@@ -1014,24 +1064,41 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
1014
1064
|
const basenameCount = new Map();
|
|
1015
1065
|
for (const module of modules) basenameCount.set(module.base, (basenameCount.get(module.base) ?? 0) + 1);
|
|
1016
1066
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1067
|
+
// One `dirname/base` matcher per unreachable module whose basename collides,
|
|
1068
|
+
// built once: `references` below runs per (mentioner, target) pair.
|
|
1069
|
+
/** @type {Map<string, RegExp>} */
|
|
1070
|
+
const qualifiedMatchers = new Map();
|
|
1071
|
+
for (const module of unreachable) {
|
|
1072
|
+
if ((basenameCount.get(module.base) ?? 0) < 2) continue;
|
|
1073
|
+
qualifiedMatchers.set(module.relative, tokenMatcher(module.relative.split(path.sep).slice(-2).join('/')));
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Does `mentioner` reference the unreachable module `target`? A bare-basename
|
|
1077
|
+
* mention only counts when the basename is UNIQUE (see the census above).
|
|
1078
|
+
* When it collides, the mentioning module must name the `dirname/base` form
|
|
1079
|
+
* in its body — otherwise ONE unreachable module mentioning bare `index.mjs`
|
|
1080
|
+
* references EVERY differently-pathed `index.mjs` at once (#1293:
|
|
1081
|
+
* `locks/index.mjs` + `worktree/index.mjs` were masked as roots by a third
|
|
1082
|
+
* unreachable module until that module was deleted for an unrelated reason).
|
|
1083
|
+
*
|
|
1084
|
+
* The ONE predicate for both consumers — the cluster-root filter and the drag
|
|
1085
|
+
* count of the finding message — so the two cannot disagree about who
|
|
1086
|
+
* references whom. They did (#1298): the drag count compared bare basenames,
|
|
1087
|
+
* so a root importing `a/index.mjs` AND `b/index.mjs` read "drags 1".
|
|
1088
|
+
*
|
|
1089
|
+
* @param {(typeof modules)[number]} mentioner
|
|
1090
|
+
* @param {(typeof modules)[number]} target an `unreachable` module
|
|
1091
|
+
* @returns {boolean}
|
|
1092
|
+
*/
|
|
1093
|
+
const references = (mentioner, target) => {
|
|
1094
|
+
if (!mentioner.mentions.has(target.base)) return false;
|
|
1095
|
+
const qualifiedRe = qualifiedMatchers.get(target.relative);
|
|
1096
|
+
return qualifiedRe === undefined || qualifiedRe.test(mentioner.rawBody);
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const roots = unreachable.filter(
|
|
1100
|
+
(module) => !unreachable.some((other) => other.relative !== module.relative && references(other, module)),
|
|
1101
|
+
);
|
|
1035
1102
|
|
|
1036
1103
|
// A pure `export *` barrel enters the population above so it can HEAD its own
|
|
1037
1104
|
// cluster, but reporting it needs one more condition. S3 already exempts this
|
|
@@ -1106,9 +1173,10 @@ export function collectUnreachableLibraryModules(pluginRoot) {
|
|
|
1106
1173
|
'architecture. Re-check only if that instruction is ever removed',
|
|
1107
1174
|
});
|
|
1108
1175
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
)
|
|
1176
|
+
// Counted per MODULE through the root filter's own predicate, never per
|
|
1177
|
+
// basename token: `mentions` is a Set of basenames, so two dragged
|
|
1178
|
+
// `index.mjs` files would collapse into one (#1298).
|
|
1179
|
+
const dragged = unreachable.filter((other) => other.relative !== module.relative && references(module, other));
|
|
1112
1180
|
const tail = dragged.length > 0 ? `, and drags ${dragged.length} further unreachable module(s)` : '';
|
|
1113
1181
|
// A pure barrel reports its re-export surface; naming "0 symbol(s)" there
|
|
1114
1182
|
// would read as a checker bug rather than as the barrel it is.
|
|
@@ -1259,6 +1327,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1259
1327
|
orphanedModules: 0,
|
|
1260
1328
|
unreachableModules: 0,
|
|
1261
1329
|
coordinatorInvokedModules: 0,
|
|
1330
|
+
parserOnly: 0,
|
|
1262
1331
|
handKeyedSubjects: 0,
|
|
1263
1332
|
judgedSubjects: 0,
|
|
1264
1333
|
},
|
|
@@ -1318,7 +1387,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1318
1387
|
|
|
1319
1388
|
for (const key of [...declared.keys.keys()].sort()) {
|
|
1320
1389
|
const meta = /** @type {DeclaredKey} */ (declared.keys.get(key));
|
|
1321
|
-
const { code, comment } = countReadSites(meta, corpus);
|
|
1390
|
+
const { code, comment, files: readFiles } = countReadSites(meta, corpus);
|
|
1322
1391
|
|
|
1323
1392
|
/** @type {Finding | null} */
|
|
1324
1393
|
let issue = null;
|
|
@@ -1331,6 +1400,21 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1331
1400
|
`declared in ${meta.sources.join(' + ')} but no .mjs under ${CONSUMER_DIRS.join('/ or ')}/ ` +
|
|
1332
1401
|
`reads it${commentNote} — wire it, delete it, or allowlist it with a reason`,
|
|
1333
1402
|
};
|
|
1403
|
+
} else if (readFiles.length === 1 && readFiles[0] === ownBlockParserPath(meta.root)) {
|
|
1404
|
+
// S6 — the ONLY file that reads this key is the parser that produced it.
|
|
1405
|
+
// A parser is not a consumer: it turns YAML into a field nobody then
|
|
1406
|
+
// reads, which is `compact-nudge` wearing a value object. Measured
|
|
1407
|
+
// 2026-09-22: `reaper.max-hook-latency-ms` and `reaper.false-alarm-window`
|
|
1408
|
+
// both passed S1 on the strength of `scripts/lib/config/reaper.mjs`
|
|
1409
|
+
// alone, while `orphan-reaper.mjs` used its OWN hardcoded defaults of the
|
|
1410
|
+
// same name and never received the parsed value.
|
|
1411
|
+
issue = {
|
|
1412
|
+
kind: 'parser-only-config-key',
|
|
1413
|
+
key,
|
|
1414
|
+
message:
|
|
1415
|
+
`read only by its own block parser (${readFiles[0]}) — a parser is not a consumer; ` +
|
|
1416
|
+
'nothing outside the config layer reads the parsed value',
|
|
1417
|
+
};
|
|
1334
1418
|
} else if (key === meta.root && !tokenMatcher(key).test(parserBody)) {
|
|
1335
1419
|
issue = {
|
|
1336
1420
|
kind: 'parser-orphan-config-key',
|
|
@@ -1356,7 +1440,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
|
|
|
1356
1440
|
continue;
|
|
1357
1441
|
}
|
|
1358
1442
|
|
|
1359
|
-
result.summary.
|
|
1443
|
+
if (issue.kind === 'parser-only-config-key') result.summary.parserOnly += 1;
|
|
1444
|
+
else result.summary.unwired += 1;
|
|
1360
1445
|
findings.push(issue);
|
|
1361
1446
|
}
|
|
1362
1447
|
|
|
@@ -1426,6 +1511,7 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1426
1511
|
consumerFiles,
|
|
1427
1512
|
unwired,
|
|
1428
1513
|
allowlisted,
|
|
1514
|
+
parserOnly,
|
|
1429
1515
|
orphanedModules,
|
|
1430
1516
|
unreachableModules,
|
|
1431
1517
|
coordinatorInvokedModules,
|
|
@@ -1445,8 +1531,16 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1445
1531
|
// architecture — an instruction surface tells an LLM to call the module.
|
|
1446
1532
|
// Measured 2026-09-07: 46 of the 52 findings the single S4 class carried.
|
|
1447
1533
|
// Printing 46 WARN lines for the design is the broken instrument HR-101 forbids.
|
|
1448
|
-
|
|
1534
|
+
//
|
|
1535
|
+
// `parser-only-config-key` (S6) is deferred on the S4 terms too: 32 findings
|
|
1536
|
+
// on the live tree is a backlog to work down, not a per-run alarm.
|
|
1537
|
+
const DEFERRED = Object.freeze([
|
|
1538
|
+
'unreachable-library-module',
|
|
1539
|
+
'coordinator-invoked-module',
|
|
1540
|
+
'parser-only-config-key',
|
|
1541
|
+
]);
|
|
1449
1542
|
const s4 = inspection.findings.filter((item) => item.kind === 'unreachable-library-module');
|
|
1543
|
+
const s6 = inspection.findings.filter((item) => item.kind === 'parser-only-config-key');
|
|
1450
1544
|
for (const item of inspection.findings) {
|
|
1451
1545
|
if (!list && DEFERRED.includes(item.kind)) continue;
|
|
1452
1546
|
console.log(` WARN: [${item.kind}] ${item.key} — ${item.message}`);
|
|
@@ -1465,10 +1559,19 @@ export function runCheckUnwiredFeatures(pluginRoot, { list = false } = {}) {
|
|
|
1465
1559
|
);
|
|
1466
1560
|
}
|
|
1467
1561
|
|
|
1562
|
+
if (!list && s6.length > 0) {
|
|
1563
|
+
console.log(
|
|
1564
|
+
` WARN: [parser-only-config-key] ${s6.length} declared key(s) whose only reader is their own ` +
|
|
1565
|
+
`block parser — e.g. ${s6.slice(0, 3).map((item) => item.key).join(', ')}. ` +
|
|
1566
|
+
'Re-run with --list for the full census.',
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1468
1570
|
console.log(
|
|
1469
1571
|
` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
|
|
1470
1572
|
`against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
|
|
1471
|
-
`${
|
|
1573
|
+
`${parserOnly} parser-only, ${orphanedModules} prose-orphaned module(s), ` +
|
|
1574
|
+
`${unreachableModules} unreachable module(s), ` +
|
|
1472
1575
|
`${coordinatorInvokedModules} coordinator-invoked module(s), ${handKeyedSubjects} hand-keyed ` +
|
|
1473
1576
|
`learning subject(s) of ${judgedSubjects} judged`,
|
|
1474
1577
|
);
|