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
|
@@ -45,6 +45,14 @@
|
|
|
45
45
|
* - `surfaceTopN` (`./learnings/surface.mjs`) — the canonical active-learning
|
|
46
46
|
* filter (confidence > floor, not expired); called with a large cap to get
|
|
47
47
|
* the full active set rather than a top-N slice.
|
|
48
|
+
* - `countReconcileBacklog` (`./reconcile/backlog.mjs`) — the SAME eligibility +
|
|
49
|
+
* materialization partition `/reconcile` applies, imported from the LEAF module
|
|
50
|
+
* rather than from `./reconcile/engine.mjs`. The engine is WRITE-capable (it
|
|
51
|
+
* merges the candidate store) and drags the emitter, renderer and unicode
|
|
52
|
+
* validator behind it: importing the count from there took this probe's static
|
|
53
|
+
* closure from 11 modules / 151,946 B to 19 / 322,897 B (+112 %, measured
|
|
54
|
+
* 2026-09-18), inherited unasked by `maintenance-due-banner.mjs`. A banner must
|
|
55
|
+
* not be able to reach a writer.
|
|
48
56
|
* - `filterEligible` (`./reconcile/eligibility.mjs`) — the SAME type/file_paths
|
|
49
57
|
* allow-list gate the reconcile engine itself uses (deliberately NOT
|
|
50
58
|
* confidence-gated — mirrors `runReconcile`'s own posture, see
|
|
@@ -68,7 +76,7 @@ import { join } from 'node:path';
|
|
|
68
76
|
|
|
69
77
|
import { readLearnings } from './learnings/io.mjs';
|
|
70
78
|
import { surfaceTopN } from './learnings/surface.mjs';
|
|
71
|
-
import {
|
|
79
|
+
import { countReconcileBacklog } from './reconcile/backlog.mjs';
|
|
72
80
|
import { loadCandidates } from './reconcile/idempotency.mjs';
|
|
73
81
|
import { readConfigFile } from './config/io.mjs';
|
|
74
82
|
import { _parseReconcile } from './config/reconcile.mjs';
|
|
@@ -82,9 +90,67 @@ export const NUDGE_MIN_LEARNINGS = 20;
|
|
|
82
90
|
/** Nudge threshold (b): new learnings accrued since the last determinable reconcile run. */
|
|
83
91
|
export const NUDGE_MIN_DELTA = 15;
|
|
84
92
|
|
|
85
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Nudge threshold (c): reconcile BACKLOG — rule-eligible learnings (type + file_paths
|
|
95
|
+
* allow-list, not expired, regardless of confidence) MINUS those already materialized
|
|
96
|
+
* in the idempotency sidecar or a `.claude/rules/` provenance marker. Counting the
|
|
97
|
+
* materialized ones too made the probe un-greenable (#1380: 98 eligible, 35 already
|
|
98
|
+
* materialized, and no `/reconcile` run could ever clear it).
|
|
99
|
+
*/
|
|
86
100
|
export const NUDGE_MIN_ELIGIBLE = 3;
|
|
87
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the `reconcile` Session Config settings this module needs, in ONE read.
|
|
104
|
+
*
|
|
105
|
+
* Both consumers go through here, so the nudge can never judge on a different
|
|
106
|
+
* population than `/reconcile` does: the skill reads `reconcile.min-insight-chars`
|
|
107
|
+
* (default 24) and forwards it to the engine, and so must this probe. Before #1380
|
|
108
|
+
* follow-up the value was only read AFTER the backlog had already been counted, which
|
|
109
|
+
* left the placeholder-insight gate practically OFF for the banner.
|
|
110
|
+
*
|
|
111
|
+
* Precedence: an injected already-parsed Session Config wins per KEY (DI seam, avoids a
|
|
112
|
+
* second CLAUDE.md read); any key it does not carry falls back to parsing
|
|
113
|
+
* CLAUDE.md/AGENTS.md; an unreadable config falls back to `_parseReconcile('')`, i.e.
|
|
114
|
+
* the parser's OWN documented defaults — never a literal repeated here.
|
|
115
|
+
*
|
|
116
|
+
* Never throws.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} repoRoot
|
|
119
|
+
* @param {unknown} [config] already-parsed Session Config
|
|
120
|
+
* @returns {Promise<{enabled: boolean, minInsightChars: number}>}
|
|
121
|
+
*/
|
|
122
|
+
async function _resolveReconcileSettings(repoRoot, config) {
|
|
123
|
+
const injected =
|
|
124
|
+
config && typeof config === 'object' && /** @type {any} */ (config).reconcile &&
|
|
125
|
+
typeof (/** @type {any} */ (config).reconcile) === 'object'
|
|
126
|
+
? /** @type {Record<string, unknown>} */ (/** @type {any} */ (config).reconcile)
|
|
127
|
+
: null;
|
|
128
|
+
|
|
129
|
+
const hasEnabled = injected !== null && typeof injected.enabled === 'boolean';
|
|
130
|
+
const hasChars = injected !== null && Number.isFinite(injected['min-insight-chars']);
|
|
131
|
+
if (hasEnabled && hasChars) {
|
|
132
|
+
return {
|
|
133
|
+
enabled: /** @type {boolean} */ (injected.enabled),
|
|
134
|
+
minInsightChars: Number(injected['min-insight-chars']),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = _parseReconcile(await readConfigFile(repoRoot));
|
|
141
|
+
} catch {
|
|
142
|
+
// Config unreadable — take the parser's own defaults (single source of truth).
|
|
143
|
+
parsed = _parseReconcile('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
enabled: hasEnabled ? /** @type {boolean} */ (injected.enabled) : parsed.enabled,
|
|
148
|
+
minInsightChars: hasChars
|
|
149
|
+
? Number(injected['min-insight-chars'])
|
|
150
|
+
: parsed['min-insight-chars'],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
88
154
|
/**
|
|
89
155
|
* Resolve the max `created_at` across reconcile-candidate sidecar records —
|
|
90
156
|
* the most recent reconcile run's timestamp, or `null` when no run is on
|
|
@@ -110,11 +176,19 @@ function _lastRunAt(candidates) {
|
|
|
110
176
|
*
|
|
111
177
|
* @param {object} [opts]
|
|
112
178
|
* @param {string} [opts.repoRoot] — project root (defaults to process.cwd()).
|
|
113
|
-
* @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning
|
|
179
|
+
* @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning
|
|
180
|
+
* filter AND the eligibility expiry gate.
|
|
181
|
+
* @param {number} [opts.minInsightChars] — forwarded to the eligibility filter. When
|
|
182
|
+
* OMITTED the value is read from Session Config (`reconcile.min-insight-chars`,
|
|
183
|
+
* default 24) rather than left inert, so the nudge and `/reconcile` count the SAME
|
|
184
|
+
* population — see `_resolveReconcileSettings`.
|
|
185
|
+
* @param {object} [opts.config] — optional already-parsed Session Config (DI seam).
|
|
114
186
|
* @returns {Promise<{
|
|
115
187
|
* totalLearnings: number,
|
|
116
188
|
* activeLearnings: number,
|
|
117
189
|
* eligibleCount: number,
|
|
190
|
+
* alreadyMaterialized: number,
|
|
191
|
+
* backlogCount: number,
|
|
118
192
|
* lastRunAt: string|null,
|
|
119
193
|
* lastRunCandidateCount: number,
|
|
120
194
|
* delta: number,
|
|
@@ -131,6 +205,8 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
131
205
|
totalLearnings: 0,
|
|
132
206
|
activeLearnings: 0,
|
|
133
207
|
eligibleCount: 0,
|
|
208
|
+
alreadyMaterialized: 0,
|
|
209
|
+
backlogCount: 0,
|
|
134
210
|
lastRunAt: null,
|
|
135
211
|
lastRunCandidateCount: 0,
|
|
136
212
|
delta: 0,
|
|
@@ -166,20 +242,29 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
166
242
|
|
|
167
243
|
if (active.length === 0) return empty;
|
|
168
244
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
245
|
+
// Same partition a real /reconcile run applies (engine.mjs step 3/3a) — one
|
|
246
|
+
// truth, so the nudge can only fire on work /reconcile is able to clear.
|
|
247
|
+
// That includes the placeholder-insight gate: when the caller did not supply
|
|
248
|
+
// `minInsightChars`, read it from Session Config BEFORE counting, or the
|
|
249
|
+
// banner counts learnings `/reconcile` would reject.
|
|
250
|
+
const minInsightChars = Number.isFinite(opts.minInsightChars)
|
|
251
|
+
? Number(opts.minInsightChars)
|
|
252
|
+
: (await _resolveReconcileSettings(repoRoot, opts.config)).minInsightChars;
|
|
175
253
|
|
|
254
|
+
// The candidate store is read ONCE, BEFORE the backlog count — the count needs
|
|
255
|
+
// the same records for its sidecar-dedupe half, and reading them here lets it
|
|
256
|
+
// be handed down instead of read a second time (`candidatesRead` says whether
|
|
257
|
+
// the hand-down is trustworthy; on a failed read we hand nothing down and let
|
|
258
|
+
// `countReconcileBacklog` decide for itself rather than pass a fake empty set).
|
|
176
259
|
/** @type {Array<Record<string, unknown>>} */
|
|
177
260
|
let candidates;
|
|
178
261
|
/** @type {number|undefined} */
|
|
179
262
|
let skippedCandidates;
|
|
263
|
+
let candidatesRead = false;
|
|
180
264
|
try {
|
|
181
265
|
const diag = loadCandidates({ repoRoot });
|
|
182
266
|
candidates = Array.isArray(diag?.records) ? diag.records : [];
|
|
267
|
+
candidatesRead = Array.isArray(diag?.records);
|
|
183
268
|
// Absence-preserving, mirroring `engine.mjs` `summary.skipped`: only a
|
|
184
269
|
// finite count means "the store was inspected". Absent ⇒ never checked,
|
|
185
270
|
// 0 ⇒ checked and clean.
|
|
@@ -190,6 +275,25 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
190
275
|
// fabricating a clean 0.
|
|
191
276
|
}
|
|
192
277
|
|
|
278
|
+
// ONE corpus, ONE population. `entries` (read above via `readLearnings`) is
|
|
279
|
+
// handed down instead of letting the count re-read and re-normalize the same
|
|
280
|
+
// file: the banner line names `activeLearnings` and `backlogCount` side by
|
|
281
|
+
// side, and two independent reads of one file are two populations wearing one
|
|
282
|
+
// sentence (HR-106). `repoRoot` stays required — the sidecar and the
|
|
283
|
+
// `.claude/rules/` provenance scan are resolved from it.
|
|
284
|
+
// Never throws (all-zero on failure).
|
|
285
|
+
const {
|
|
286
|
+
eligible: eligibleCount,
|
|
287
|
+
alreadyMaterialized,
|
|
288
|
+
backlog: backlogCount,
|
|
289
|
+
} = countReconcileBacklog({
|
|
290
|
+
repoRoot,
|
|
291
|
+
learnings: entries,
|
|
292
|
+
...(candidatesRead ? { existingCandidates: candidates } : {}),
|
|
293
|
+
now: opts.now,
|
|
294
|
+
minInsightChars,
|
|
295
|
+
});
|
|
296
|
+
|
|
193
297
|
const lastRunAt = _lastRunAt(candidates);
|
|
194
298
|
const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
|
|
195
299
|
const delta = entries.length - lastRunCandidateCount;
|
|
@@ -213,15 +317,18 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
213
317
|
if (lastRunAt !== null && delta > NUDGE_MIN_DELTA) {
|
|
214
318
|
reasons.push(`${delta} new learnings since the last reconcile run`);
|
|
215
319
|
}
|
|
216
|
-
// (c) — enough rule-eligible learnings to be worth a
|
|
217
|
-
|
|
218
|
-
|
|
320
|
+
// (c) — enough NOT-YET-MATERIALIZED rule-eligible learnings to be worth a
|
|
321
|
+
// batch, independent of confidence. HR-106: the reason names the number judged.
|
|
322
|
+
if (backlogCount >= NUDGE_MIN_ELIGIBLE) {
|
|
323
|
+
reasons.push(`${backlogCount} rule-eligible learnings awaiting a rule`);
|
|
219
324
|
}
|
|
220
325
|
|
|
221
326
|
const computed = {
|
|
222
327
|
totalLearnings: entries.length,
|
|
223
328
|
activeLearnings: active.length,
|
|
224
329
|
eligibleCount,
|
|
330
|
+
alreadyMaterialized,
|
|
331
|
+
backlogCount,
|
|
225
332
|
lastRunAt,
|
|
226
333
|
lastRunCandidateCount,
|
|
227
334
|
delta,
|
|
@@ -236,25 +343,6 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
236
343
|
return computed;
|
|
237
344
|
}
|
|
238
345
|
|
|
239
|
-
/**
|
|
240
|
-
* Best-effort extraction of `reconcile.enabled` from an already-parsed Session
|
|
241
|
-
* Config object (DI/test seam — avoids re-reading CLAUDE.md when the caller
|
|
242
|
-
* already has it). Returns `null` when unresolvable from `config` alone, in
|
|
243
|
-
* which case `checkReconcileNudge` falls back to reading CLAUDE.md/AGENTS.md.
|
|
244
|
-
*
|
|
245
|
-
* @param {unknown} config
|
|
246
|
-
* @returns {boolean|null}
|
|
247
|
-
*/
|
|
248
|
-
function _reconcileEnabledFromConfig(config) {
|
|
249
|
-
if (config && typeof config === 'object') {
|
|
250
|
-
const rc = /** @type {Record<string, unknown>} */ (config).reconcile;
|
|
251
|
-
if (rc && typeof rc === 'object' && typeof (/** @type {any} */ (rc).enabled) === 'boolean') {
|
|
252
|
-
return /** @type {any} */ (rc).enabled;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
346
|
/**
|
|
259
347
|
* Check reconcile-nudge readiness and produce a session-start banner.
|
|
260
348
|
*
|
|
@@ -274,26 +362,30 @@ export async function checkReconcileNudge(opts = {}) {
|
|
|
274
362
|
const { repoRoot, config, now } = opts;
|
|
275
363
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
276
364
|
|
|
365
|
+
// ONE config read for both settings, BEFORE the backlog is counted — the
|
|
366
|
+
// `min-insight-chars` gate must reach `countReconcileBacklog`, and reading the
|
|
367
|
+
// config afterwards (as this did before) left it inert.
|
|
368
|
+
let reconcileEnabled = false;
|
|
369
|
+
let minInsightChars;
|
|
370
|
+
try {
|
|
371
|
+
({ enabled: reconcileEnabled, minInsightChars } = await _resolveReconcileSettings(
|
|
372
|
+
repoRoot,
|
|
373
|
+
config,
|
|
374
|
+
));
|
|
375
|
+
} catch {
|
|
376
|
+
// Unreachable in practice (_resolveReconcileSettings never throws) — keep the
|
|
377
|
+
// conservative default and let computeReconcileNudge resolve the gate itself.
|
|
378
|
+
reconcileEnabled = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
277
381
|
let computed;
|
|
278
382
|
try {
|
|
279
|
-
computed = await computeReconcileNudge({ repoRoot, now });
|
|
383
|
+
computed = await computeReconcileNudge({ repoRoot, now, minInsightChars });
|
|
280
384
|
} catch {
|
|
281
385
|
return null;
|
|
282
386
|
}
|
|
283
387
|
if (!computed || computed.nudge !== true) return null;
|
|
284
388
|
|
|
285
|
-
let reconcileEnabled = _reconcileEnabledFromConfig(config);
|
|
286
|
-
if (reconcileEnabled === null) {
|
|
287
|
-
try {
|
|
288
|
-
const content = await readConfigFile(repoRoot);
|
|
289
|
-
reconcileEnabled = _parseReconcile(content).enabled;
|
|
290
|
-
} catch {
|
|
291
|
-
// Config unreadable — fall back to the documented config default (false)
|
|
292
|
-
// so the informational parenthetical still renders conservatively.
|
|
293
|
-
reconcileEnabled = false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
389
|
// Three-state last-run label. `never` is a claim about history and must only
|
|
298
390
|
// be made when the store was inspected and held nothing: a store whose lines
|
|
299
391
|
// were quarantined by the shape guard is EVIDENCE OF A RUN that can no longer
|
|
@@ -323,7 +415,8 @@ export async function checkReconcileNudge(opts = {}) {
|
|
|
323
415
|
}
|
|
324
416
|
|
|
325
417
|
const lines = [
|
|
326
|
-
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.
|
|
418
|
+
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.backlogCount} rule-eligible awaiting a rule ` +
|
|
419
|
+
`(${computed.alreadyMaterialized} already materialized), ` +
|
|
327
420
|
`last reconcile run: ${lastRunLabel} — run /reconcile to convert learnings into rules.`,
|
|
328
421
|
];
|
|
329
422
|
if (reconcileEnabled === false) {
|
|
@@ -30,6 +30,37 @@ export function parseEtimeToMinutes(etime) {
|
|
|
30
30
|
return days * 24 * 60 + hours * 60 + mins;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Parse a `ps` etime field (`[[DD-]HH:]MM:SS`) into whole SECONDS.
|
|
35
|
+
*
|
|
36
|
+
* Twin of {@link parseEtimeToMinutes}, which drops the seconds field because
|
|
37
|
+
* minute resolution is all the zombie counter needs. The orphan reaper
|
|
38
|
+
* (`scripts/lib/orphan-reaper.mjs`) cannot use that: its minimum age is 300
|
|
39
|
+
* seconds and its identity check reconstructs a start time as
|
|
40
|
+
* `now - etimeSeconds * 1000`, where a discarded seconds field would be a
|
|
41
|
+
* systematic error of up to 59 s against a 2 s tolerance.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately a sibling rather than a refactor of the twin: `parseEtimeToMinutes`
|
|
44
|
+
* has live callers (`countZombieProcesses`) whose behaviour must not shift.
|
|
45
|
+
*
|
|
46
|
+
* Returns null when the format is unrecognised. Pure function.
|
|
47
|
+
* @param {string} etime
|
|
48
|
+
* @returns {number|null}
|
|
49
|
+
*/
|
|
50
|
+
export function parseEtimeToSeconds(etime) {
|
|
51
|
+
if (typeof etime !== 'string') return null;
|
|
52
|
+
const s = etime.trim();
|
|
53
|
+
// Same shape as parseEtimeToMinutes: optional "DD-", optional "HH:", then "MM:SS".
|
|
54
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(s);
|
|
55
|
+
if (!m) return null;
|
|
56
|
+
const days = parseInt(m[1] ?? '0', 10);
|
|
57
|
+
const hours = parseInt(m[2] ?? '0', 10);
|
|
58
|
+
const mins = parseInt(m[3], 10);
|
|
59
|
+
const secs = parseInt(m[4], 10);
|
|
60
|
+
if ([days, hours, mins, secs].some(Number.isNaN)) return null;
|
|
61
|
+
return days * 86400 + hours * 3600 + mins * 60 + secs;
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* Parse the detailed ps output and count Claude/Node zombie candidates.
|
|
35
66
|
* A zombie candidate is a process matching "claude" or "node" that:
|
|
@@ -49,7 +49,7 @@ import { ramSnapshot, cpuSnapshot, processCounts, swapUsedMb, memoryPressurePctF
|
|
|
49
49
|
|
|
50
50
|
export { evaluate, PROCESSES_PER_SESSION, PRESSURE_HARD_PCT, PRESSURE_SOFT_PCT, PRESSURE_HEALTHY_PCT, DEFAULT_RESOURCE_THRESHOLDS } from './resource-probe/evaluate.mjs';
|
|
51
51
|
export { peerSessionsCount } from './resource-probe/probe-platform.mjs';
|
|
52
|
-
export { parseEtimeToMinutes, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
|
|
52
|
+
export { parseEtimeToMinutes, parseEtimeToSeconds, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
|
|
53
53
|
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
55
|
// Public API — probe()
|
|
@@ -11,24 +11,26 @@
|
|
|
11
11
|
* when at least one `scopePath` matches at least one glob pattern.
|
|
12
12
|
*
|
|
13
13
|
* `paths:` is a same-shape alias for `globs:` (issue #795) — some repos use a
|
|
14
|
-
* `paths:` frontmatter convention instead of `globs:` (
|
|
15
|
-
*
|
|
14
|
+
* `paths:` frontmatter convention instead of `globs:` (projects-baseline did
|
|
15
|
+
* when #795 landed; it now carries both keys — measured count:
|
|
16
|
+
* docs/baseline.md). Before #795 these repos'
|
|
16
17
|
* path-scoped rules were silently misclassified as always-on, inflating the
|
|
17
18
|
* `instruction-budget-guard.mjs` (#687) always-on count with a false positive.
|
|
18
19
|
* `paths:` supports the identical inline-array and block-list forms as
|
|
19
20
|
* `globs:`. Precedence when BOTH keys are present on the same rule: `globs:`
|
|
20
21
|
* wins (silently — no merge, no warning) and `paths:` is ignored entirely.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* (
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* Frontmatter counts ONLY when its opening `---` is the very first line of the
|
|
24
|
+
* file (#1449) — the same rule Claude Code applies when it reads `paths:`.
|
|
25
|
+
* Until #1449 this parser skipped leading blank lines and single-line HTML
|
|
26
|
+
* comments (the vendoring pipeline's `<!-- source: … -->` provenance header),
|
|
27
|
+
* so a rule Claude Code loads ALWAYS-ON was classified path-scoped here and
|
|
28
|
+
* `instruction-budget-guard.mjs` under-counted the always-on surface. A
|
|
29
|
+
* provenance comment now belongs directly AFTER the closing `---` (body text,
|
|
30
|
+
* inert). A frontmatter-shaped block that is NOT on line 1 — any text,
|
|
31
|
+
* comment or blank line before it — yields no globs and no meta at all, and
|
|
32
|
+
* the entry carries `defect: 'frontmatter-not-at-top'` so callers can report
|
|
33
|
+
* the file instead of silently loading it always-on.
|
|
32
34
|
*
|
|
33
35
|
* Beyond the `globs:` key (issue #336), the frontmatter parser also captures
|
|
34
36
|
* these scalar activation keys (issue #694), surfaced on each RuleEntry:
|
|
@@ -149,34 +151,15 @@ function matchGlob(filePath, globPattern) {
|
|
|
149
151
|
// Minimal YAML frontmatter parser (matches state-md.mjs style)
|
|
150
152
|
// ---------------------------------------------------------------------------
|
|
151
153
|
|
|
154
|
+
// Anchored at offset 0: frontmatter exists only when `---` is line 1 (#1449).
|
|
152
155
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
153
156
|
|
|
154
|
-
// A single header line is either blank or a complete single-line HTML
|
|
155
|
-
// comment (`<!-- ... -->` on one line). Multi-line comment blocks are
|
|
156
|
-
// deliberately NOT supported — the vendoring convention is a single first
|
|
157
|
-
// line, and tolerating a few stacked comment lines (each matching this RE)
|
|
158
|
-
// is sufficient.
|
|
159
|
-
const HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
|
|
160
|
-
|
|
161
157
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* mandatory provenance header precedes it. Stops at the first line that is
|
|
166
|
-
* neither blank nor a complete single-line comment.
|
|
167
|
-
*
|
|
168
|
-
* @param {string} contents - raw file contents
|
|
169
|
-
* @returns {string} contents starting at the first non-header line (empty
|
|
170
|
-
* string when the entire content is header lines)
|
|
158
|
+
* Defect code for a frontmatter block that does not start on line 1 (#1449).
|
|
159
|
+
* Claude Code ignores such a block, so the rule loads always-on there; the
|
|
160
|
+
* loader mirrors that and tags the entry with this code.
|
|
171
161
|
*/
|
|
172
|
-
|
|
173
|
-
const lines = contents.split(/\r?\n/);
|
|
174
|
-
let idx = 0;
|
|
175
|
-
while (idx < lines.length && HEADER_LINE_RE.test(lines[idx])) {
|
|
176
|
-
idx++;
|
|
177
|
-
}
|
|
178
|
-
return lines.slice(idx).join('\n');
|
|
179
|
-
}
|
|
162
|
+
export const FRONTMATTER_NOT_AT_TOP = 'frontmatter-not-at-top';
|
|
180
163
|
|
|
181
164
|
// Scalar activation keys captured (issue #694 + #692). The `globs` key keeps
|
|
182
165
|
// its dedicated sequence/flow handling and is NOT routed through this table.
|
|
@@ -192,12 +175,36 @@ const SCALAR_META_KEYS = new Set([
|
|
|
192
175
|
'tier',
|
|
193
176
|
]);
|
|
194
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
|
|
180
|
+
*
|
|
181
|
+
* `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
|
|
182
|
+
* js-yaml, which picks SINGLE-quote style whenever the plain form would be
|
|
183
|
+
* ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
|
|
184
|
+
* leading/trailing-quote strip then returns `it''s` for `it's` — measured
|
|
185
|
+
* 2026-08-23 on 40 of 146 live learning insights carrying both characters.
|
|
186
|
+
* Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
|
|
187
|
+
* `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
|
|
188
|
+
*/
|
|
189
|
+
function unquoteYamlScalar(valuePart) {
|
|
190
|
+
const v = valuePart.trim();
|
|
191
|
+
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
|
|
192
|
+
return v.slice(1, -1).replace(/''/g, "'");
|
|
193
|
+
}
|
|
194
|
+
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
|
|
195
|
+
return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
|
|
196
|
+
}
|
|
197
|
+
return v.replace(/^["']|["']$/g, '');
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
/**
|
|
196
201
|
* Parses the YAML frontmatter block for the `globs:` field (issue #336),
|
|
197
202
|
* its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
|
|
198
203
|
*
|
|
199
204
|
* Returns:
|
|
200
205
|
* - `{ globs: string[] | null, meta: object }` on success
|
|
206
|
+
* - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
|
|
207
|
+
* frontmatter block exists but does not start on line 1 (#1449)
|
|
201
208
|
* - throws `Error` on malformed frontmatter so the caller can fall back
|
|
202
209
|
*
|
|
203
210
|
* `globs` is `null` when no frontmatter, and neither `globs:` nor `paths:` is
|
|
@@ -212,41 +219,63 @@ const SCALAR_META_KEYS = new Set([
|
|
|
212
219
|
* - all other recognised keys → quote-stripped strings
|
|
213
220
|
* Unknown keys are ignored without error.
|
|
214
221
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
222
|
+
* Only a block whose opening `---` is line 1 is frontmatter (#1449), exactly
|
|
223
|
+
* as Claude Code reads it. Anything before the opener — a provenance comment,
|
|
224
|
+
* a blank line, any text — makes the block body text: no globs, no meta, and
|
|
225
|
+
* the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
|
|
219
226
|
*
|
|
220
227
|
* @param {string} contents - raw file contents
|
|
221
|
-
* @returns {{ globs: string[] | null, meta: Record<string, unknown
|
|
228
|
+
* @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
|
|
222
229
|
*/
|
|
230
|
+
export function parseGlobsFrontmatter(contents) {
|
|
231
|
+
const match = FRONTMATTER_RE.exec(contents);
|
|
232
|
+
if (match) return parseFrontmatterBlock(match[1]);
|
|
233
|
+
if (hasDisplacedFrontmatter(contents)) {
|
|
234
|
+
return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
|
|
235
|
+
}
|
|
236
|
+
return { globs: null, meta: {} };
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
/**
|
|
224
|
-
*
|
|
240
|
+
* Does `contents` carry a frontmatter block that is NOT on line 1 (#1449)?
|
|
225
241
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
242
|
+
* Only the FIRST bare `---` line is considered, and its block must parse
|
|
243
|
+
* cleanly AND carry at least one key this loader honours (`globs`, `paths`,
|
|
244
|
+
* or a scalar activation key). That second condition keeps a horizontal rule
|
|
245
|
+
* pair around ordinary prose from being reported: prose between two `---`
|
|
246
|
+
* lines almost always holds a colon-less line (a heading, a sentence), which
|
|
247
|
+
* throws, and a recognised key at column 0 is what makes the misplacement
|
|
248
|
+
* change behaviour at all. The known blind spot is a displaced block that is
|
|
249
|
+
* itself malformed — it is treated as body text, which is what Claude Code
|
|
250
|
+
* does with it too.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} contents - raw file contents
|
|
253
|
+
* @returns {boolean}
|
|
233
254
|
*/
|
|
234
|
-
function
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
255
|
+
function hasDisplacedFrontmatter(contents) {
|
|
256
|
+
const lines = contents.split(/\r?\n/);
|
|
257
|
+
const opener = lines.indexOf('---');
|
|
258
|
+
// -1: no opener at all. 0: the opener IS line 1, so FRONTMATTER_RE already
|
|
259
|
+
// had its chance (an unclosed block) — not a displacement.
|
|
260
|
+
if (opener <= 0) return false;
|
|
261
|
+
const block = FRONTMATTER_RE.exec(lines.slice(opener).join('\n'));
|
|
262
|
+
if (!block) return false;
|
|
263
|
+
try {
|
|
264
|
+
const { globs, meta } = parseFrontmatterBlock(block[1]);
|
|
265
|
+
return globs !== null || Object.keys(meta).length > 0;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
241
268
|
}
|
|
242
|
-
return v.replace(/^["']|["']$/g, '');
|
|
243
269
|
}
|
|
244
270
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Parses the text BETWEEN the two `---` fences (the frontmatter body).
|
|
273
|
+
* Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} fmText - frontmatter body without its fences
|
|
276
|
+
* @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
|
|
277
|
+
*/
|
|
278
|
+
function parseFrontmatterBlock(fmText) {
|
|
250
279
|
const lines = fmText.split(/\r?\n/);
|
|
251
280
|
|
|
252
281
|
let globsValue = null;
|
|
@@ -365,6 +394,35 @@ function metaToEntryFields(meta) {
|
|
|
365
394
|
return out;
|
|
366
395
|
}
|
|
367
396
|
|
|
397
|
+
/**
|
|
398
|
+
* The expiry predicate, shared by the injection gate below and the `/reconcile`
|
|
399
|
+
* provenance reader (`scripts/lib/reconcile/backlog.mjs`) — one `Date.parse`
|
|
400
|
+
* for both, so "expired" can never mean two different things to the two
|
|
401
|
+
* consumers (#1387).
|
|
402
|
+
*
|
|
403
|
+
* FAIL-OPEN by design: an absent, empty or unparseable `expires-at` is NOT
|
|
404
|
+
* expired. Fail-closed here would un-inject (and, in the reconcile reader,
|
|
405
|
+
* un-materialize) the whole corpus on a single typo.
|
|
406
|
+
*
|
|
407
|
+
* Pure apart from the optional `onUnparseable` callback, which exists so the
|
|
408
|
+
* injection path can keep its stderr WARN while the reconcile reader stays
|
|
409
|
+
* silent.
|
|
410
|
+
*
|
|
411
|
+
* @param {unknown} rawExpiry - the raw `expires-at` frontmatter value (or undefined)
|
|
412
|
+
* @param {number} [now] - epoch ms; defaults to `Date.now()`
|
|
413
|
+
* @param {(raw: unknown) => void} [onUnparseable] - called when `expires-at` is present but unparseable
|
|
414
|
+
* @returns {boolean} true only when a PARSEABLE `expires-at` lies before `now`
|
|
415
|
+
*/
|
|
416
|
+
export function isRuleExpired(rawExpiry, now = Date.now(), onUnparseable) {
|
|
417
|
+
if (rawExpiry === undefined || rawExpiry === null) return false;
|
|
418
|
+
const ts = Date.parse(String(rawExpiry));
|
|
419
|
+
if (Number.isNaN(ts)) {
|
|
420
|
+
if (typeof onUnparseable === 'function') onUnparseable(rawExpiry);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return ts < now;
|
|
424
|
+
}
|
|
425
|
+
|
|
368
426
|
/**
|
|
369
427
|
* Deterministic gating check (issue #694 + #692). Returns `{ excluded: boolean,
|
|
370
428
|
* reason?: string }`. Applied to BOTH always-on and glob-matched rules.
|
|
@@ -378,18 +436,17 @@ function metaToEntryFields(meta) {
|
|
|
378
436
|
* @returns {{ excluded: boolean }}
|
|
379
437
|
*/
|
|
380
438
|
function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
381
|
-
// Expiry gate — fail-open on a malformed `expires-at
|
|
439
|
+
// Expiry gate — fail-open on a malformed `expires-at` ({@link isRuleExpired}).
|
|
382
440
|
if (Object.prototype.hasOwnProperty.call(meta, 'expires-at')) {
|
|
383
441
|
const rawExpiry = meta['expires-at'];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
442
|
+
if (
|
|
443
|
+
isRuleExpired(rawExpiry, now, (raw) =>
|
|
444
|
+
process.stderr.write(
|
|
445
|
+
`[rule-loader] Rule ${filePath} has unparseable expires-at ${JSON.stringify(raw)} — ignoring expiry\n`,
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
449
|
+
process.stderr.write(`[rule-loader] Rule ${filePath} expired at ${rawExpiry} — excluded\n`);
|
|
393
450
|
return { excluded: true };
|
|
394
451
|
}
|
|
395
452
|
}
|
|
@@ -437,6 +494,9 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
|
437
494
|
* from the `alwaysApply` frontmatter key)
|
|
438
495
|
* @property {string[]} matchedGlobs - globs that matched; empty when alwaysOn
|
|
439
496
|
* @property {true} [_parseError] - present only on frontmatter parse-error entries
|
|
497
|
+
* @property {'frontmatter-not-at-top'} [defect] - present only when a frontmatter
|
|
498
|
+
* block exists but does not start on line 1 (#1449); such an entry is always
|
|
499
|
+
* `alwaysOn: true` and carries no meta fields, mirroring Claude Code
|
|
440
500
|
* @property {string} [description] - frontmatter `description` (issue #694), when present
|
|
441
501
|
* @property {string} [mode] - frontmatter `mode` (issue #694), when present
|
|
442
502
|
* @property {string} [hostClass] - frontmatter `host-class` (issue #694), when present
|
|
@@ -455,6 +515,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
|
|
|
455
515
|
* Rules without `globs:` (or its `paths:` alias — issue #795) frontmatter are
|
|
456
516
|
* always included (alwaysOn: true). Rules with `globs:`/`paths:` are included
|
|
457
517
|
* only when at least one scopePath matches at least one glob pattern.
|
|
518
|
+
* A rule whose frontmatter does not start on line 1 is always-on with no meta
|
|
519
|
+
* (so no gate excludes it) and carries `defect: 'frontmatter-not-at-top'`
|
|
520
|
+
* (#1449) — no stderr line, because callers such as the instruction-budget
|
|
521
|
+
* guard load the directory several times per run and report the defect themselves.
|
|
458
522
|
*
|
|
459
523
|
* After a successful frontmatter parse, deterministic gates (issue #694) are
|
|
460
524
|
* applied to BOTH always-on and glob-matched rules — a rule must pass ALL
|
|
@@ -528,10 +592,11 @@ export function loadApplicableRules({
|
|
|
528
592
|
|
|
529
593
|
let globs;
|
|
530
594
|
let meta;
|
|
595
|
+
let defect;
|
|
531
596
|
let parseError = false;
|
|
532
597
|
|
|
533
598
|
try {
|
|
534
|
-
({ globs, meta } = parseGlobsFrontmatter(content));
|
|
599
|
+
({ globs, meta, defect } = parseGlobsFrontmatter(content));
|
|
535
600
|
} catch (err) {
|
|
536
601
|
process.stderr.write(
|
|
537
602
|
`[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,
|
|
@@ -552,7 +617,8 @@ export function loadApplicableRules({
|
|
|
552
617
|
const metaFields = metaToEntryFields(meta);
|
|
553
618
|
|
|
554
619
|
if (globs === null) {
|
|
555
|
-
// No globs frontmatter (or parse error
|
|
620
|
+
// No globs frontmatter (or parse error, or frontmatter not on line 1 —
|
|
621
|
+
// #1449) → always-on
|
|
556
622
|
results.push({
|
|
557
623
|
path: filePath,
|
|
558
624
|
content,
|
|
@@ -560,6 +626,7 @@ export function loadApplicableRules({
|
|
|
560
626
|
matchedGlobs: [],
|
|
561
627
|
...metaFields,
|
|
562
628
|
...(parseError ? { _parseError: true } : {}),
|
|
629
|
+
...(defect ? { defect } : {}),
|
|
563
630
|
});
|
|
564
631
|
continue;
|
|
565
632
|
}
|