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
package/scripts/site-numbers.mjs
CHANGED
|
@@ -91,6 +91,13 @@
|
|
|
91
91
|
* silent skip — a generator that quietly stops filling a surface is the exact
|
|
92
92
|
* failure this file exists to end.
|
|
93
93
|
*
|
|
94
|
+
* ## The sitemap `<lastmod>` (`site/sitemap.xml`, #1484)
|
|
95
|
+
*
|
|
96
|
+
* `--write` changes what `/` and `/de` SHOW, so it also dates them in the
|
|
97
|
+
* sitemap: `lastmod` becomes max(lastmod, the page's own `counted-at` cell).
|
|
98
|
+
* `--check` reports a `lastmod` older than that cell as drift. Rule, mapping and
|
|
99
|
+
* reasons: `SITEMAP_PAGES`.
|
|
100
|
+
*
|
|
94
101
|
* Exit codes (`.claude/rules/cli-design.md`):
|
|
95
102
|
* 0 — no drift (--check) / files updated or already current (--write)
|
|
96
103
|
* 1 — drift found (--check), or a contract violation in either mode
|
|
@@ -99,7 +106,7 @@
|
|
|
99
106
|
*/
|
|
100
107
|
|
|
101
108
|
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
102
|
-
import { join, resolve, relative } from 'node:path';
|
|
109
|
+
import { join, resolve, relative, sep } from 'node:path';
|
|
103
110
|
import { execFileSync } from 'node:child_process';
|
|
104
111
|
|
|
105
112
|
import { writeStdoutLineSync, writeJsonAtomicSync } from './lib/io.mjs';
|
|
@@ -963,6 +970,118 @@ export function syncCensusBlocks(siteDir, values, { write = false } = {}) {
|
|
|
963
970
|
return CENSUS_BLOCK_FILES.map((file) => syncCensusBlock(siteDir, values, { write, file }));
|
|
964
971
|
}
|
|
965
972
|
|
|
973
|
+
// ---------------------------------------------------------------------------
|
|
974
|
+
// The sitemap <lastmod> in site/sitemap.xml (#1484)
|
|
975
|
+
// ---------------------------------------------------------------------------
|
|
976
|
+
|
|
977
|
+
/** The sitemap, relative to the SITE directory. */
|
|
978
|
+
export const SITEMAP_FILE = 'sitemap.xml';
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Every page ↔ sitemap `<loc>`, explicitly.
|
|
982
|
+
*
|
|
983
|
+
* Why the sitemap is this generator's business: `--write` changes what `/` and
|
|
984
|
+
* `/de` show (version card, number cards, "Counted on … at commit …"), and the
|
|
985
|
+
* release that ran it left their `lastmod` behind. Measured 2026-10-01: `/`
|
|
986
|
+
* claimed 2026-09-08 while its content was counted 2026-09-22.
|
|
987
|
+
*
|
|
988
|
+
* The rule takes no second clock. A page's `counted-at` cell is the date this
|
|
989
|
+
* script last changed that page — `rewrite()` stamps every cell in one pass —
|
|
990
|
+
* so `lastmod` becomes max(lastmod, that cell), and never moves backwards. A
|
|
991
|
+
* page without a `counted-at` cell (the guide and the legal pages, whose only
|
|
992
|
+
* cell is the header version) is never dated: a version bump in a page header
|
|
993
|
+
* is not a content change.
|
|
994
|
+
*
|
|
995
|
+
* Unknown either way is an ERROR, never a skip (the `METRIC_IDS` rule): a
|
|
996
|
+
* sitemap `<loc>` missing here would never be dated, and a page carrying cells
|
|
997
|
+
* that is missing here would drift unseen.
|
|
998
|
+
*/
|
|
999
|
+
export const SITEMAP_PAGES = Object.freeze([
|
|
1000
|
+
{ page: 'index.html', loc: 'https://session-orchestrator.com/' },
|
|
1001
|
+
{ page: 'de/index.html', loc: 'https://session-orchestrator.com/de' },
|
|
1002
|
+
{ page: 'guide/index.html', loc: 'https://session-orchestrator.com/guide' },
|
|
1003
|
+
{ page: 'impressum/index.html', loc: 'https://session-orchestrator.com/impressum' },
|
|
1004
|
+
{ page: 'datenschutz/index.html', loc: 'https://session-orchestrator.com/datenschutz' },
|
|
1005
|
+
]);
|
|
1006
|
+
|
|
1007
|
+
const URL_BLOCK_RE = /<url>([\s\S]*?)<\/url>/g;
|
|
1008
|
+
const ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Judge (and optionally rewrite) the `<lastmod>` of every sitemap entry. Only the
|
|
1012
|
+
* date text of an entry that moves is rewritten; the rest of the file is handed
|
|
1013
|
+
* back byte-for-byte.
|
|
1014
|
+
*
|
|
1015
|
+
* @param {string} siteDir
|
|
1016
|
+
* @param {Array<{page:string, metrics:string[], countedAt:string|null}>} pages
|
|
1017
|
+
* every HTML file under `siteDir` carrying metric cells: its site-relative path
|
|
1018
|
+
* (`/`-separated), its metric ids, and its `counted-at` cell as it stands AFTER
|
|
1019
|
+
* this run (`null` when it has none)
|
|
1020
|
+
* @param {{write?: boolean}} [opts]
|
|
1021
|
+
* @returns {{present:boolean, file:string, errors?:string[],
|
|
1022
|
+
* drift?:Array<{loc:string, line:number, actual:string, expected:string}>,
|
|
1023
|
+
* written?:boolean}}
|
|
1024
|
+
*/
|
|
1025
|
+
export function syncSitemapLastmod(siteDir, pages, { write = false } = {}) {
|
|
1026
|
+
const abs = join(siteDir, SITEMAP_FILE);
|
|
1027
|
+
// Absent file: like the census blocks, not this generator's to create.
|
|
1028
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) return { present: false, file: abs };
|
|
1029
|
+
|
|
1030
|
+
const errors = [];
|
|
1031
|
+
const dated = new Map(); // loc → the counting date its page carries
|
|
1032
|
+
for (const p of pages) {
|
|
1033
|
+
const entry = SITEMAP_PAGES.find((e) => e.page === p.page);
|
|
1034
|
+
if (!entry) {
|
|
1035
|
+
errors.push(
|
|
1036
|
+
`${p.page} carries metric cells but has no sitemap mapping — known pages: ` +
|
|
1037
|
+
SITEMAP_PAGES.map((e) => e.page).join(', '),
|
|
1038
|
+
);
|
|
1039
|
+
} else if (p.countedAt !== null) {
|
|
1040
|
+
if (ISO_DAY_RE.test(p.countedAt)) dated.set(entry.loc, p.countedAt);
|
|
1041
|
+
else errors.push(`${p.page}: counted-at cell "${p.countedAt}" is not a YYYY-MM-DD date`);
|
|
1042
|
+
} else if (p.metrics.some((m) => m !== 'version')) {
|
|
1043
|
+
// Counted cells with no stamp to date them by: their changes would never
|
|
1044
|
+
// reach the sitemap.
|
|
1045
|
+
errors.push(`${p.page} carries counted cells but no counted-at cell — its lastmod cannot be dated`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const text = readFileSync(abs, 'utf8');
|
|
1050
|
+
const drift = [];
|
|
1051
|
+
const seen = new Set();
|
|
1052
|
+
const next = text.replace(URL_BLOCK_RE, (block, inner, offset) => {
|
|
1053
|
+
const loc = /<loc>([^<]*)<\/loc>/.exec(inner)?.[1].trim();
|
|
1054
|
+
if (loc === undefined || !SITEMAP_PAGES.some((e) => e.loc === loc)) {
|
|
1055
|
+
errors.push(
|
|
1056
|
+
`unknown sitemap <loc> "${loc ?? '(none)'}" — known: ${SITEMAP_PAGES.map((e) => e.loc).join(', ')}`,
|
|
1057
|
+
);
|
|
1058
|
+
return block;
|
|
1059
|
+
}
|
|
1060
|
+
seen.add(loc);
|
|
1061
|
+
const lm = /<lastmod>([^<]*)<\/lastmod>/.exec(block);
|
|
1062
|
+
if (!lm || !ISO_DAY_RE.test(lm[1])) {
|
|
1063
|
+
errors.push(`<lastmod> of ${loc} is missing or not a YYYY-MM-DD date`);
|
|
1064
|
+
return block;
|
|
1065
|
+
}
|
|
1066
|
+
const date = dated.get(loc);
|
|
1067
|
+
// ISO days compare as strings. Never backwards: a lastmod a hand edit dated
|
|
1068
|
+
// after the count stays.
|
|
1069
|
+
if (date === undefined || date <= lm[1]) return block;
|
|
1070
|
+
drift.push({ loc, line: lineOf(text, offset + lm.index), actual: lm[1], expected: date });
|
|
1071
|
+
return `${block.slice(0, lm.index)}<lastmod>${date}</lastmod>${block.slice(lm.index + lm[0].length)}`;
|
|
1072
|
+
});
|
|
1073
|
+
for (const loc of dated.keys()) {
|
|
1074
|
+
if (!seen.has(loc)) errors.push(`${loc} carries a counted-at cell but has no sitemap <url> entry`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const out = { present: true, file: abs, errors, drift, written: false };
|
|
1078
|
+
if (write && errors.length === 0 && next !== text) {
|
|
1079
|
+
writeFileSync(abs, next, 'utf8');
|
|
1080
|
+
out.written = true;
|
|
1081
|
+
}
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
966
1085
|
// ---------------------------------------------------------------------------
|
|
967
1086
|
// Markup
|
|
968
1087
|
// ---------------------------------------------------------------------------
|
|
@@ -1134,6 +1253,7 @@ function printHelp() {
|
|
|
1134
1253
|
writeStdoutLineSync(' --check (default) report drift, change nothing; exit 1 on drift');
|
|
1135
1254
|
writeStdoutLineSync(` --write rewrite the cells in place; also refreshes ${CENSUS_FILE.join('/')}`);
|
|
1136
1255
|
writeStdoutLineSync(' (skipped when --site points outside the repo\'s own site/)');
|
|
1256
|
+
writeStdoutLineSync(` and dates each counted page's <lastmod> in <site>/${SITEMAP_FILE}`);
|
|
1137
1257
|
writeStdoutLineSync(' --json machine-readable envelope on stdout');
|
|
1138
1258
|
writeStdoutLineSync(' --site <dir> site directory (default <repo-root>/site)');
|
|
1139
1259
|
writeStdoutLineSync(' --version print the package version');
|
|
@@ -1208,12 +1328,18 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1208
1328
|
// the source producing it is corrupt, and writing the rest would be a
|
|
1209
1329
|
// partial write reported as success.
|
|
1210
1330
|
let rejectedTotal = 0;
|
|
1331
|
+
// Every page carrying cells, for the sitemap's <lastmod> (syncSitemapLastmod).
|
|
1332
|
+
const pages = [];
|
|
1211
1333
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1334
|
+
// Repo-relative when the file is inside the repo; absolute otherwise (a
|
|
1335
|
+
// `--site` fixture in $TMPDIR would otherwise render as ../../../../var/...).
|
|
1336
|
+
const display = (abs) => {
|
|
1215
1337
|
const r = relative(root, abs);
|
|
1216
|
-
|
|
1338
|
+
return r && !r.startsWith('..') ? r : abs;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
for (const abs of files) {
|
|
1342
|
+
const rel = display(abs);
|
|
1217
1343
|
const html = readFileSync(abs, 'utf8');
|
|
1218
1344
|
const spans = inspectHtml(html, values);
|
|
1219
1345
|
spanTotal += spans.length;
|
|
@@ -1250,6 +1376,16 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1250
1376
|
}
|
|
1251
1377
|
}
|
|
1252
1378
|
|
|
1379
|
+
if (spans.length > 0) {
|
|
1380
|
+
const stamp = spans.find((s) => s.metric === 'counted-at');
|
|
1381
|
+
pages.push({
|
|
1382
|
+
page: relative(siteDir, abs).split(sep).join('/'),
|
|
1383
|
+
metrics: spans.map((s) => s.metric),
|
|
1384
|
+
// As the page stands AFTER this run: a written file carries every value.
|
|
1385
|
+
countedAt: stamp ? (written > 0 ? values['counted-at'] : stamp.actual.trim()) : null,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1253
1389
|
report.push({
|
|
1254
1390
|
file: rel,
|
|
1255
1391
|
spans: spans.length,
|
|
@@ -1294,6 +1430,17 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1294
1430
|
// read `llmsFull`; `censusBlocks` below carries all of them.
|
|
1295
1431
|
const llms = censusBlocks[CENSUS_BLOCK_FILES.indexOf(LLMS_FULL_FILE)];
|
|
1296
1432
|
|
|
1433
|
+
// The sitemap <lastmod>: a date older than the counting stamp on its page is
|
|
1434
|
+
// drift, like a stale number. Written from the pages as they stand after the
|
|
1435
|
+
// loop above, so page and sitemap come from the same run.
|
|
1436
|
+
const sitemap = syncSitemapLastmod(siteDir, pages, { write: args.write });
|
|
1437
|
+
const sitemapRel = display(sitemap.file);
|
|
1438
|
+
for (const e of sitemap.errors ?? []) {
|
|
1439
|
+
stderr(`Error: ${sitemapRel}: ${e}`);
|
|
1440
|
+
contractTotal += 1;
|
|
1441
|
+
}
|
|
1442
|
+
driftTotal += sitemap.drift?.length ?? 0;
|
|
1443
|
+
|
|
1297
1444
|
// The named silent-failure class: a generator that matches nothing, changes
|
|
1298
1445
|
// nothing, and reports success. Zero spans means the markup contract is not in
|
|
1299
1446
|
// the page — that is a defect, in BOTH modes, never a no-op.
|
|
@@ -1390,6 +1537,13 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1390
1537
|
stale: b.stale === true,
|
|
1391
1538
|
written: b.written === true,
|
|
1392
1539
|
})),
|
|
1540
|
+
sitemap: {
|
|
1541
|
+
file: sitemap.present ? sitemapRel : null,
|
|
1542
|
+
present: sitemap.present,
|
|
1543
|
+
errors: sitemap.errors ?? [],
|
|
1544
|
+
drift: sitemap.drift ?? [],
|
|
1545
|
+
written: sitemap.written === true,
|
|
1546
|
+
},
|
|
1393
1547
|
censusWritten,
|
|
1394
1548
|
ok,
|
|
1395
1549
|
},
|
|
@@ -1404,6 +1558,7 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1404
1558
|
censusBlocks
|
|
1405
1559
|
.map((b, i) => (b.written ? ` + ${CENSUS_BLOCK_FILES[i]}` : ''))
|
|
1406
1560
|
.join('') +
|
|
1561
|
+
(sitemap.written ? ` + ${SITEMAP_FILE}` : '') +
|
|
1407
1562
|
(censusWritten ? ` + ${CENSUS_FILE.join('/')}` : ''),
|
|
1408
1563
|
);
|
|
1409
1564
|
} else {
|
|
@@ -1416,6 +1571,9 @@ export function main(argv = process.argv.slice(2), env = {}) {
|
|
|
1416
1571
|
}
|
|
1417
1572
|
}
|
|
1418
1573
|
for (const l of censusDriftLines) stdout(l);
|
|
1574
|
+
for (const d of sitemap.drift ?? []) {
|
|
1575
|
+
stdout(`DRIFT ${sitemapRel}:${d.line} lastmod ${d.loc}: sitemap says "${d.actual}", page counted "${d.expected}"`);
|
|
1576
|
+
}
|
|
1419
1577
|
stdout(
|
|
1420
1578
|
driftTotal === 0 && !noSpans && contractTotal === 0
|
|
1421
1579
|
? `site-numbers: ${spanTotal} metric cell(s) current across ${files.length} file(s)`
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
* --entries PATH JSONL sidecar holding the caller's next store generation.
|
|
38
38
|
* PRUNE ONLY. Must exist, parse cleanly, and hold at least
|
|
39
39
|
* one record — absent/malformed/empty all exit 1 untouched.
|
|
40
|
+
* A NEW record (its `id` not in the store) must pass strict
|
|
41
|
+
* `validateLearning()`, else exit 1 untouched (GH#69);
|
|
42
|
+
* records already in the store keep the tolerant path.
|
|
40
43
|
* Omitted ⇒ a pure prune+consolidate pass over the on-disk
|
|
41
44
|
* store.
|
|
42
45
|
* --file PATH Learnings store (default: .orchestrator/metrics/learnings.jsonl)
|
|
@@ -61,14 +64,18 @@
|
|
|
61
64
|
*
|
|
62
65
|
* Exit codes:
|
|
63
66
|
* 0 Success (including no-op when nothing is archive-eligible)
|
|
64
|
-
* 1 Usage/input error (bad flag/value, flag used in the wrong mode,
|
|
65
|
-
* absent/malformed/empty `--entries` sidecar
|
|
67
|
+
* 1 Usage/input error (bad flag/value, flag used in the wrong mode, an
|
|
68
|
+
* absent/malformed/empty `--entries` sidecar, or a NEW `--entries` record
|
|
69
|
+
* that fails strict schema validation — dry run and apply alike)
|
|
66
70
|
* 2 Sweep/prune error (I/O or validation failure inside the lib)
|
|
67
71
|
*/
|
|
68
72
|
|
|
69
73
|
import { existsSync } from 'node:fs';
|
|
74
|
+
import path from 'node:path';
|
|
75
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
70
76
|
import { sweepExpiredLearnings, pruneLearnings } from './lib/learnings/expiry-sweep.mjs';
|
|
71
77
|
import { readLearnings } from './lib/learnings/io.mjs';
|
|
78
|
+
import { validateLearning } from './lib/learnings/schema.mjs';
|
|
72
79
|
import { emitEvolveCompleted } from './lib/learnings/evolve-telemetry.mjs';
|
|
73
80
|
|
|
74
81
|
const DEFAULT_FILE = '.orchestrator/metrics/learnings.jsonl';
|
|
@@ -91,11 +98,14 @@ Options:
|
|
|
91
98
|
--json Emit a single machine-parseable JSON summary line
|
|
92
99
|
--grace-days N Days past expiry before archiving (default: ${DEFAULT_GRACE_DAYS}); sweep only
|
|
93
100
|
--entries PATH JSONL sidecar with the next store generation; prune only.
|
|
94
|
-
Must exist, parse cleanly, and hold >= 1 record
|
|
101
|
+
Must exist, parse cleanly, and hold >= 1 record; a NEW
|
|
102
|
+
record (id not in the store) must pass strict schema
|
|
103
|
+
validation, else exit 1 with nothing written
|
|
95
104
|
--file PATH Learnings store (default: ${DEFAULT_FILE})
|
|
96
105
|
--archive PATH Archive sidecar (default: ${DEFAULT_ARCHIVE})
|
|
97
106
|
|
|
98
|
-
Exit codes: 0 success 1 usage/input error
|
|
107
|
+
Exit codes: 0 success 1 usage/input error (incl. an invalid new --entries record)
|
|
108
|
+
2 sweep/prune error
|
|
99
109
|
`
|
|
100
110
|
);
|
|
101
111
|
}
|
|
@@ -184,7 +194,8 @@ function parseArgs(argv) {
|
|
|
184
194
|
args.skipped = typeof raw === 'string' ? raw.split(',').filter((s) => s.length > 0) : [];
|
|
185
195
|
args.telemetryExplicit = true;
|
|
186
196
|
} else if (a === '--repo-root') {
|
|
187
|
-
|
|
197
|
+
// Absolute on purpose: emitEvent refuses a relative root (#1468).
|
|
198
|
+
args.repoRoot = path.resolve(argv[++i]);
|
|
188
199
|
args.telemetryExplicit = true;
|
|
189
200
|
} else if (a === '--help' || a === '-h') {
|
|
190
201
|
printHelp();
|
|
@@ -308,6 +319,45 @@ async function loadEntriesSidecar(entriesPath) {
|
|
|
308
319
|
return read.entries;
|
|
309
320
|
}
|
|
310
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Strict-validate every `--entries` record the store does not already hold.
|
|
324
|
+
*
|
|
325
|
+
* `pruneLearnings()` rewrites through `rewriteLearnings()`, which runs
|
|
326
|
+
* `legacyTolerant` so records ALREADY in the store (legacy shapes, EventDrop
|
|
327
|
+
* #386) survive a round-trip. Without this gate the same tolerance admitted
|
|
328
|
+
* NEW records too — a sidecar record with `scope: "src/app/x.tsx"` and no
|
|
329
|
+
* `created_at`/`expires_at` was written (GH#69). New = its `id` is absent from
|
|
330
|
+
* the store; an id-less record counts as known only when an id-less store
|
|
331
|
+
* record is deep-equal to it (the carry-through case — linear scan over the
|
|
332
|
+
* id-less subset, fine while such records stay rare; revisit if a store holds
|
|
333
|
+
* hundreds of them). Runs before any write, in dry run and apply alike.
|
|
334
|
+
*
|
|
335
|
+
* @param {object[]} entries - the parsed `--entries` generation
|
|
336
|
+
* @param {string} filePath - the learnings store
|
|
337
|
+
*/
|
|
338
|
+
async function rejectInvalidNewRecords(entries, filePath) {
|
|
339
|
+
let store;
|
|
340
|
+
try {
|
|
341
|
+
({ entries: store } = await readLearnings(filePath));
|
|
342
|
+
} catch (err) {
|
|
343
|
+
process.stderr.write(`sweep-expired-learnings: prune failed: cannot read ${filePath}: ${err.message}\n`);
|
|
344
|
+
process.exit(2);
|
|
345
|
+
}
|
|
346
|
+
const idOf = (e) => (typeof e?.id === 'string' && e.id.length > 0 ? e.id : null);
|
|
347
|
+
const storeIds = new Set(store.map(idOf).filter((id) => id !== null));
|
|
348
|
+
const storeIdless = store.filter((e) => idOf(e) === null);
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
const id = idOf(entry);
|
|
351
|
+
const known = id !== null ? storeIds.has(id) : storeIdless.some((s) => isDeepStrictEqual(s, entry));
|
|
352
|
+
if (known) continue;
|
|
353
|
+
try {
|
|
354
|
+
validateLearning(entry);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
usageError(`--entries: new record ${id ?? '(no id)'} is invalid: ${err.message} — nothing written`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
311
361
|
/**
|
|
312
362
|
* Decision-driven prune + consolidate + rewrite (issue #1017).
|
|
313
363
|
*
|
|
@@ -315,6 +365,7 @@ async function loadEntriesSidecar(entriesPath) {
|
|
|
315
365
|
*/
|
|
316
366
|
async function runPrune(args) {
|
|
317
367
|
const entries = args.entries === null ? undefined : await loadEntriesSidecar(args.entries);
|
|
368
|
+
if (entries !== undefined) await rejectInvalidNewRecords(entries, args.file);
|
|
318
369
|
|
|
319
370
|
let result;
|
|
320
371
|
try {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* per-file error on the `--apply` path)
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import path from 'node:path';
|
|
32
33
|
import { emitEvent } from './lib/events.mjs';
|
|
33
34
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
34
35
|
import {
|
|
@@ -100,7 +101,7 @@ export function parseArgs(argv) {
|
|
|
100
101
|
} else if (a === '--repo-root') {
|
|
101
102
|
const raw = argv[++i];
|
|
102
103
|
if (typeof raw !== 'string' || raw.length === 0) usageError('--repo-root requires a path');
|
|
103
|
-
args.repoRoot = raw;
|
|
104
|
+
args.repoRoot = path.resolve(raw);
|
|
104
105
|
} else if (a === '--learnings') {
|
|
105
106
|
const raw = argv[++i];
|
|
106
107
|
if (typeof raw !== 'string' || raw.length === 0) usageError('--learnings requires a path');
|
|
@@ -115,15 +116,25 @@ export function parseArgs(argv) {
|
|
|
115
116
|
return args;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Roll the plan up into the summary both output modes render.
|
|
121
|
+
*
|
|
122
|
+
* `header_raises` is additive (#1388 P7): a header-raise is an
|
|
123
|
+
* `action: 'rewrite'` plan carrying `reason: 'header-raise'`, so it was counted
|
|
124
|
+
* inside `rewrites` and invisible on the human stdout line although `--json`
|
|
125
|
+
* always carried `plans[].reason`. It is a SUBSET of `rewrites`, never a
|
|
126
|
+
* sibling category — the two are deliberately not disjoint.
|
|
127
|
+
*/
|
|
128
|
+
export function summarize(plan, args, applied) {
|
|
120
129
|
const counts = { rewrite: 0, delete: 0, keep: 0 };
|
|
121
130
|
for (const p of plan.plans) counts[p.action] = (counts[p.action] ?? 0) + 1;
|
|
131
|
+
const headerRaises = plan.plans.filter((p) => p.reason === 'header-raise').length;
|
|
122
132
|
return {
|
|
123
133
|
dry_run: args.dryRun,
|
|
124
134
|
grace_days: args.graceDays,
|
|
125
135
|
files_scanned: plan.plans.length,
|
|
126
136
|
rewrites: counts.rewrite,
|
|
137
|
+
header_raises: headerRaises,
|
|
127
138
|
deletes: counts.delete,
|
|
128
139
|
keeps: counts.keep,
|
|
129
140
|
expired_entries: plan.plans.reduce((n, p) => n + p.expiredPairIds.length, 0),
|
|
@@ -163,7 +174,8 @@ async function main(argv) {
|
|
|
163
174
|
else {
|
|
164
175
|
process.stdout.write(
|
|
165
176
|
`sweep-expired-rules: ${args.dryRun ? 'dry-run' : 'applied'} — ` +
|
|
166
|
-
`${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s)
|
|
177
|
+
`${summary.files_scanned} generated rule file(s), ${summary.rewrites} rewrite(s) ` +
|
|
178
|
+
`(${summary.header_raises} header-raise(s)), ` +
|
|
167
179
|
`${summary.deletes} delete(s), ${summary.expired_entries} expired entr(ies), ` +
|
|
168
180
|
`${summary.unresolved_pairs} unresolved pair(s), ${summary.skipped.length} skipped, ` +
|
|
169
181
|
`${summary.malformed_lines} malformed learnings line(s)\n`,
|
|
@@ -433,6 +433,18 @@ if (runCheck('check-dead-bridge.mjs') !== 0) checkFailed = 1;
|
|
|
433
433
|
process.stdout.write('\n');
|
|
434
434
|
if (runCheck('check-entry-guard.mjs') !== 0) checkFailed = 1;
|
|
435
435
|
|
|
436
|
+
// BLOCKING (#1422), the complement of the check above: `check-entry-guard`
|
|
437
|
+
// judges the FORM of a guard that already exists, so a registered hook with NO
|
|
438
|
+
// guard at all passes it trivially — the #1393 regression class exactly. This
|
|
439
|
+
// one asks the other half of the question over the union of the four platform
|
|
440
|
+
// manifests: does every registered hook run its entry only under
|
|
441
|
+
// `isMainModule(import.meta.url)` / `invokedAsScript()`, and does no profile
|
|
442
|
+
// gate sit at module top level where its `process.exit(0)` kills a mere
|
|
443
|
+
// importer. Blocking honestly: 27/27 registered hooks were guarded at the
|
|
444
|
+
// moment it landed, because #1393 drained the backlog first.
|
|
445
|
+
process.stdout.write('\n');
|
|
446
|
+
if (runCheck('check-hook-entry-guards.mjs') !== 0) checkFailed = 1;
|
|
447
|
+
|
|
436
448
|
// FAIL-capable, unlike the WARN-only censuses below: R2+R4 were measured at
|
|
437
449
|
// 1 hit / 1 TP / 0 FP each against the full test corpus, so this check cannot be
|
|
438
450
|
// red on arrival for anything but a real instance of the class. The alternatives
|
|
@@ -501,6 +513,13 @@ runStrictShAdvisory();
|
|
|
501
513
|
// session-end tail phases from their dispatcher.
|
|
502
514
|
if (runCheck('check-skill-links.mjs') !== 0) checkFailed = 1;
|
|
503
515
|
|
|
516
|
+
// BLOCKING (#1446 / GH#69): instruction prose that tells an agent to write
|
|
517
|
+
// `learnings.jsonl` with a shell `>` / `>>`. The store is gitignored, so a bad
|
|
518
|
+
// write has no VCS restore; the sanctioned writers (`apply-session-learnings.mjs`,
|
|
519
|
+
// `sweep-expired-learnings.mjs --prune`) validate, snapshot `.bak` and archive.
|
|
520
|
+
process.stdout.write('\n');
|
|
521
|
+
if (runCheck('check-learnings-shell-writes.mjs') !== 0) checkFailed = 1;
|
|
522
|
+
|
|
504
523
|
// WARN-only: a state-mutating `git` call in tests/ that names no target resolves
|
|
505
524
|
// its destination from the ambient cwd (or an inherited GIT_DIR) — the 2026-08-19
|
|
506
525
|
// incident, where fixture commits, a fixture remote and a fixture identity landed
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
* 2 — I/O error (file not found, unreadable stdin, unreadable sidecar file)
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
|
-
import path from 'node:path';
|
|
73
72
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
74
73
|
import { execFileSync } from 'node:child_process';
|
|
75
74
|
import { warn } from './lib/common.mjs';
|
|
@@ -86,6 +85,17 @@ import {
|
|
|
86
85
|
// which roles are allowed to grant zero paths.
|
|
87
86
|
isReadOnlyWaveRole,
|
|
88
87
|
READ_ONLY_WAVE_ROLES,
|
|
88
|
+
// #1398 cond. 4 / #1405 / #1406 — THE grading predicate for an absolute Gate 5b
|
|
89
|
+
// grant. Lives in scope-gate.mjs so this CLI and `hooks/enforce-scope.mjs`
|
|
90
|
+
// grade one and the same way (BV-003): before the move, only this CLI could
|
|
91
|
+
// grade at all, so the hook honoured grants the CLI would have refused and a
|
|
92
|
+
// manifest that skipped the CLI was never graded.
|
|
93
|
+
gradeScopeEntry,
|
|
94
|
+
// #1398 cond. 4 — the `resolve` injection the predicate above takes. Moved
|
|
95
|
+
// into scope-gate.mjs (from a private copy here) once the hot-path cost was
|
|
96
|
+
// measured rather than assumed, so this CLI and `hooks/enforce-scope.mjs` pass
|
|
97
|
+
// the SAME resolver and can no longer reach different verdicts for one grant.
|
|
98
|
+
canonicalizeGrantPrefix,
|
|
89
99
|
// Aliased: `expandTestSiblings` is ALSO the name of the pre-existing
|
|
90
100
|
// boolean parameter threaded through validate()/assertSubsetOrDie for the
|
|
91
101
|
// #970 flag. Aliasing the import avoids shadowing that parameter rather than
|
|
@@ -338,79 +348,6 @@ function validateSessionBinding(obj, errors, warnings) {
|
|
|
338
348
|
}
|
|
339
349
|
}
|
|
340
350
|
|
|
341
|
-
/**
|
|
342
|
-
* Literal filesystem-root forms — POSIX "/" and the Windows equivalents "\"
|
|
343
|
-
* and a bare drive root ("C:\", "C:\\", ...). Checked independently of
|
|
344
|
-
* `path.isAbsolute()` because that primitive is platform-native: on a POSIX
|
|
345
|
-
* host (this repo's dev/CI hosts) it never reports `C:\` as absolute, so a
|
|
346
|
-
* Windows-literal-root entry would otherwise slip past every check below.
|
|
347
|
-
* @type {ReadonlySet<string>}
|
|
348
|
-
*/
|
|
349
|
-
const FILESYSTEM_ROOT_LITERALS = new Set(['/', '\\']);
|
|
350
|
-
const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:\\+$/;
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* @param {string} entry
|
|
354
|
-
* @returns {boolean}
|
|
355
|
-
*/
|
|
356
|
-
function isFilesystemRootLiteral(entry) {
|
|
357
|
-
return FILESYSTEM_ROOT_LITERALS.has(entry) || WINDOWS_DRIVE_ROOT_RE.test(entry);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Well-known top-level system/home directories. A FIXED DENYLIST (not a
|
|
362
|
-
* segment-count threshold): the #792 legitimate grant
|
|
363
|
-
* (`/private/tmp/<session>/scratchpad/**`) is itself a single-segment-deep
|
|
364
|
-
* grant under an unusual root ("private"), so any segment-count heuristic
|
|
365
|
-
* tight enough to catch `/etc/**` risks catching that too, or must be tuned
|
|
366
|
-
* loosely enough to leave a gap. A fixed list of the directories a
|
|
367
|
-
* hallucinated/mis-copied wave-scope entry could plausibly land on is
|
|
368
|
-
* predictable, auditable in a one-line diff, and does not touch legitimate
|
|
369
|
-
* deep grants under any other root.
|
|
370
|
-
* @type {ReadonlySet<string>}
|
|
371
|
-
*/
|
|
372
|
-
const DENIED_ABSOLUTE_TOP_SEGMENTS = new Set([
|
|
373
|
-
'etc',
|
|
374
|
-
'Users',
|
|
375
|
-
'home',
|
|
376
|
-
'root',
|
|
377
|
-
'bin',
|
|
378
|
-
'sbin',
|
|
379
|
-
'usr',
|
|
380
|
-
'System',
|
|
381
|
-
'var',
|
|
382
|
-
'boot',
|
|
383
|
-
'dev',
|
|
384
|
-
'proc',
|
|
385
|
-
'sys',
|
|
386
|
-
'Library',
|
|
387
|
-
'Applications',
|
|
388
|
-
'Windows',
|
|
389
|
-
]);
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* The top-level path segment of an absolute POSIX-style entry (the segment
|
|
393
|
-
* immediately after the leading "/"), if it is on the denylist above.
|
|
394
|
-
* @param {string} entry
|
|
395
|
-
* @returns {string|null}
|
|
396
|
-
*/
|
|
397
|
-
function deniedTopSegment(entry) {
|
|
398
|
-
const first = entry.split('/').filter(Boolean)[0];
|
|
399
|
-
return first && DENIED_ABSOLUTE_TOP_SEGMENTS.has(first) ? first : null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Does this entry contain a glob wildcard? Mirrors this codebase's own glob
|
|
404
|
-
* convention (`isGlobScopeEntry` in scripts/lib/scope-gate.mjs / #796): `*`
|
|
405
|
-
* is the sole wildcard metachar used in allowedPaths/fileScope entries
|
|
406
|
-
* throughout this repo (no `?`/`[]`/`{}` glob syntax is supported or tested
|
|
407
|
-
* anywhere else in scope-gate.mjs or enforce-scope.mjs).
|
|
408
|
-
* @param {string} entry
|
|
409
|
-
* @returns {boolean}
|
|
410
|
-
*/
|
|
411
|
-
function hasWildcard(entry) {
|
|
412
|
-
return entry.includes('*');
|
|
413
|
-
}
|
|
414
351
|
|
|
415
352
|
/**
|
|
416
353
|
* Validate allowedPaths array: must exist, be an array of non-empty strings,
|
|
@@ -463,37 +400,27 @@ function validateAllowedPaths(obj, errors, warnings) {
|
|
|
463
400
|
errors.push('allowedPaths contains empty string');
|
|
464
401
|
continue;
|
|
465
402
|
}
|
|
466
|
-
// #870
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
} else if (!hasWildcard(entry)) {
|
|
488
|
-
errors.push(
|
|
489
|
-
`allowedPaths contains a bare absolute file grant with no wildcard: ${entry} — a single concrete out-of-repo file has no established legitimate use in this codebase; scope a glob instead`,
|
|
490
|
-
);
|
|
491
|
-
} else {
|
|
492
|
-
warnings.push(
|
|
493
|
-
`allowedPaths contains an absolute (out-of-repo) path: ${entry} — honoured by hooks/enforce-scope.mjs Gate 5b; verify this grant is intentional`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
403
|
+
// #870 / #1398 / #1405 / #1406 — grade the entry as a Gate 5b grant through
|
|
404
|
+
// the SHARED predicate (`scripts/lib/scope-gate.mjs` gradeScopeEntry), the
|
|
405
|
+
// same one `hooks/enforce-scope.mjs` Gate 5b calls. Most absolute entries are
|
|
406
|
+
// a SANCTIONED out-of-repo grant and only WARN — the validator must not
|
|
407
|
+
// contradict the hook it validates for (#792/#870) — while the catastrophic
|
|
408
|
+
// subclasses (filesystem root, denylisted system directory in either its
|
|
409
|
+
// literal or its canonical spelling, a home grant at or above the user level
|
|
410
|
+
// or into a credential directory, a tilde that nothing expands, and a
|
|
411
|
+
// non-canonical prefix Gate 5b can never match) hard-reject here, BEFORE the
|
|
412
|
+
// hook is ever consulted. `null` = an ordinary relative entry, nothing to say.
|
|
413
|
+
//
|
|
414
|
+
// SAME PREDICATE, SAME RESOLVER (#1398 cond. 4). The resolver used to live
|
|
415
|
+
// here, which made the two callers' ARGUMENT shapes differ and let one grant
|
|
416
|
+
// reach two verdicts (`/tmp/x/**`: hook `warn`, this CLI `error`). Since the
|
|
417
|
+
// hot-path cost was measured (+0.067 ms per Gate 5b hit — see
|
|
418
|
+
// {@link canonicalizeGrantPrefix}) both callers pass this exact function, so
|
|
419
|
+
// the only remaining asymmetry is the CONSEQUENCE: exit-1 refusal here,
|
|
420
|
+
// allow-with-notice there.
|
|
421
|
+
const grade = gradeScopeEntry(entry, { resolve: canonicalizeGrantPrefix });
|
|
422
|
+
if (grade !== null) {
|
|
423
|
+
(grade.verdict === 'error' ? errors : warnings).push(`allowedPaths ${grade.message}`);
|
|
497
424
|
}
|
|
498
425
|
// Reject path traversal: any `../` segment. INDEPENDENT of the absolute checks
|
|
499
426
|
// above — an absolute entry that ALSO contains `../` must still be rejected
|
|
@@ -35,6 +35,7 @@ import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-rep
|
|
|
35
35
|
import { validatePathInsideProject } from './lib/path-utils.mjs';
|
|
36
36
|
import { resolveRepoSpec, redactUrlCredentials } from './lib/vcs-repo-spec.mjs';
|
|
37
37
|
import { expandTilde } from './lib/common.mjs';
|
|
38
|
+
import { shellQuote } from './lib/sh-quote.mjs';
|
|
38
39
|
|
|
39
40
|
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -285,7 +286,7 @@ function buildTickBody(issueMap, depIds, verdict, streak, date) {
|
|
|
285
286
|
function buildFlipBody(repos) {
|
|
286
287
|
// Defense-in-depth: regex+confinement upstream already filter, but shell-quote here
|
|
287
288
|
// makes the embedded bash safe under any future bypass.
|
|
288
|
-
const repoList = repos.map(
|
|
289
|
+
const repoList = repos.map(shellQuote).join(' ');
|
|
289
290
|
// The sed/add/commit one-liner targets the project-instruction file. Most
|
|
290
291
|
// ecosystem repos use CLAUDE.md, but Codex-CLI repos use AGENTS.md as a
|
|
291
292
|
// transparent alias — see skills/_shared/instruction-file-resolution.md.
|