session-orchestrator 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +6 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +7 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +8 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +21 -0
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +7 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +7 -2
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +7 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +7 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +7 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +8 -3
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +22 -0
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +7 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +20 -0
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +6 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +6 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +8 -2
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +21 -0
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +6 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +22 -0
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +6 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +6 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +7 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +21 -0
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +7 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +13 -4
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +197 -0
- package/README.md +11 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +17 -3
- package/docs/baseline.md +12 -6
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +15 -3
- package/docs/components.md +13 -6
- package/docs/events-schema.md +59 -9
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +135 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +106 -11
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +125 -8
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +72 -25
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +211 -23
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +6 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +18 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +119 -28
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +385 -5
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +2 -2
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +171 -15
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +77 -32
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +220 -11
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +408 -33
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +76 -13
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +332 -50
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +122 -91
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +429 -7
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +91 -12
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
- package/scripts/lib/user-invocable-skills.mjs +205 -0
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +329 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +35 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
- package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
- package/scripts/lib/validate/check-unwired-features.mjs +220 -33
- package/scripts/lib/validate/check-validator-registration.mjs +36 -12
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +39 -12
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +109 -39
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +227 -0
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +11 -4
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +80 -11
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +52 -23
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +9 -2
- package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +2 -2
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +21 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +18 -6
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +20 -2
- package/skills/wave-executor/SKILL.md +14 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
package/scripts/lib/events.mjs
CHANGED
|
@@ -39,7 +39,18 @@
|
|
|
39
39
|
* 0.0961 ms/call.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
promises as fs,
|
|
44
|
+
closeSync,
|
|
45
|
+
existsSync,
|
|
46
|
+
fstatSync,
|
|
47
|
+
openSync,
|
|
48
|
+
readFileSync,
|
|
49
|
+
readSync,
|
|
50
|
+
readdirSync,
|
|
51
|
+
realpathSync,
|
|
52
|
+
} from 'node:fs';
|
|
53
|
+
import { tmpdir } from 'node:os';
|
|
43
54
|
import path from 'node:path';
|
|
44
55
|
import { getProjectDir, SO_SHARED_DIR } from './platform.mjs';
|
|
45
56
|
import { readLock } from './session-lock.mjs';
|
|
@@ -49,8 +60,14 @@ import {
|
|
|
49
60
|
readProcessLocalSessionIds,
|
|
50
61
|
} from './session-identity/own-session.mjs';
|
|
51
62
|
import {
|
|
63
|
+
ARCHIVE_DIR_NAME,
|
|
64
|
+
ARCHIVE_NAME_RE,
|
|
52
65
|
EventValidationError,
|
|
66
|
+
LEGACY_RING_MAX,
|
|
67
|
+
ROTATION_EVENT,
|
|
68
|
+
parseEventLines,
|
|
53
69
|
stampEventSchemaVersion,
|
|
70
|
+
summarizeEventRecords,
|
|
54
71
|
validateEventRecord,
|
|
55
72
|
} from './events-schema.mjs';
|
|
56
73
|
|
|
@@ -58,6 +75,67 @@ import {
|
|
|
58
75
|
// Public API
|
|
59
76
|
// ---------------------------------------------------------------------------
|
|
60
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Test seam (#1397 item 11): the absolute path of a sandbox ledger that the
|
|
80
|
+
* DEFAULT destination is redirected to whenever that default would land outside
|
|
81
|
+
* the OS temp root. Set by `tests/setup/events-ledger-guard.mjs` in every
|
|
82
|
+
* vitest worker, and inherited by every child process the suite spawns — which
|
|
83
|
+
* is the point: the default resolves via `SO_PROJECT_DIR` (env, else a walk up
|
|
84
|
+
* from the cwd), so a spawned script with `cwd = <repo>` otherwise appended
|
|
85
|
+
* synthetic records, stamped with the LIVE session id, to the real ledger.
|
|
86
|
+
* Nothing in production sets it.
|
|
87
|
+
*/
|
|
88
|
+
export const EVENTS_LEDGER_SANDBOX_ENV = 'SO_EVENTS_LEDGER_SANDBOX';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* OS temp root in both spellings (macOS `os.tmpdir()` is `/var/folders/…`, a
|
|
92
|
+
* symlink to `/private/var/folders/…`; `process.cwd()` inside it reports the
|
|
93
|
+
* canonical form). Only called while the sandbox variable is set.
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
function tmpRoots() {
|
|
97
|
+
const raw = path.resolve(tmpdir());
|
|
98
|
+
let real = raw;
|
|
99
|
+
try { real = realpathSync(raw); } catch { /* keep the raw spelling */ }
|
|
100
|
+
return real === raw ? [raw] : [raw, real];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @param {string} p @param {string[]} roots @returns {boolean} */
|
|
104
|
+
function isUnderAny(p, roots) {
|
|
105
|
+
const abs = path.resolve(p);
|
|
106
|
+
return roots.some((r) => abs === r || abs.startsWith(r + path.sep));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Apply the sandbox redirect to a DEFAULT-resolved ledger path.
|
|
111
|
+
*
|
|
112
|
+
* Rule: redirect only a default that would leave the temp root. A fixture
|
|
113
|
+
* project dir under tmp (`CLAUDE_PROJECT_DIR=<mkdtemp>`, or `cwd: <mkdtemp>`) is
|
|
114
|
+
* where a test EXPECTS its records — measured 2026-09-19 @ d92c2ca4, 38 test
|
|
115
|
+
* files set a project-dir env var and read `events.jsonl` back — so it is kept
|
|
116
|
+
* byte-identical. Keying on "outside tmp" rather than on "is this repo's ledger"
|
|
117
|
+
* is deliberate: an allow-list of protected roots fails open on the root it did
|
|
118
|
+
* not name (a sibling repo, a copied plugin tree), this invariant does not.
|
|
119
|
+
*
|
|
120
|
+
* Why the sandbox value must itself sit under the temp root: a stray or
|
|
121
|
+
* malformed export in an operator's shell must never be able to aim real
|
|
122
|
+
* telemetry at another tracked file. A value outside tmp, a relative value, or
|
|
123
|
+
* a whitespace-only value is IGNORED → production resolution, unchanged. What
|
|
124
|
+
* this cannot prevent (BV-004 ceiling): a stray export that DOES point under
|
|
125
|
+
* tmp diverts a real session's records there. The name marks it a test seam;
|
|
126
|
+
* revisit if it is ever found set outside a vitest process.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} defaultPath
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
function sandboxedDefault(defaultPath) {
|
|
132
|
+
const sandbox = (process.env[EVENTS_LEDGER_SANDBOX_ENV] || '').trim();
|
|
133
|
+
if (!sandbox || !path.isAbsolute(sandbox)) return defaultPath;
|
|
134
|
+
const roots = tmpRoots();
|
|
135
|
+
if (!isUnderAny(sandbox, roots) || isUnderAny(defaultPath, roots)) return defaultPath;
|
|
136
|
+
return path.resolve(sandbox);
|
|
137
|
+
}
|
|
138
|
+
|
|
61
139
|
/**
|
|
62
140
|
* Returns the absolute path to `.orchestrator/metrics/events.jsonl` under `repoRoot`.
|
|
63
141
|
*
|
|
@@ -67,11 +145,17 @@ import {
|
|
|
67
145
|
* CWD/env-resolved project — e.g. a unit test running the gate against a tmp
|
|
68
146
|
* repo, which must NOT append synthetic records to the real fleet telemetry.
|
|
69
147
|
*
|
|
148
|
+
* Only the zero-arg (default) form honours {@link EVENTS_LEDGER_SANDBOX_ENV};
|
|
149
|
+
* an explicit `repoRoot` is never redirected. Readers that resolve the default
|
|
150
|
+
* (session-start rotation) and raw writers that use it (the discovery-validator
|
|
151
|
+
* hook) therefore see the same sandbox `emitEvent()` writes to.
|
|
152
|
+
*
|
|
70
153
|
* @param {string} [repoRoot=SO_PROJECT_DIR] — project root the events log lives under.
|
|
71
154
|
* @returns {string}
|
|
72
155
|
*/
|
|
73
|
-
export function eventsFilePath(repoRoot
|
|
74
|
-
return path.join(repoRoot, SO_SHARED_DIR, 'metrics', 'events.jsonl');
|
|
156
|
+
export function eventsFilePath(repoRoot) {
|
|
157
|
+
if (repoRoot !== undefined) return path.join(repoRoot, SO_SHARED_DIR, 'metrics', 'events.jsonl');
|
|
158
|
+
return sandboxedDefault(path.join(getProjectDir(), SO_SHARED_DIR, 'metrics', 'events.jsonl'));
|
|
75
159
|
}
|
|
76
160
|
|
|
77
161
|
/**
|
|
@@ -352,8 +436,9 @@ export async function emitEvent(type, payload = {}, opts = {}) {
|
|
|
352
436
|
// 1. explicit opts.filePath (a pre-resolved path — #611)
|
|
353
437
|
// 2. opts.repoRoot → <repoRoot>/.orchestrator/metrics/events.jsonl (#941)
|
|
354
438
|
// 3. the SO_PROJECT_DIR default (unchanged for 2-arg callers)
|
|
355
|
-
// eventsFilePath(undefined) falls through to its SO_PROJECT_DIR default
|
|
356
|
-
//
|
|
439
|
+
// eventsFilePath(undefined) falls through to its SO_PROJECT_DIR default, so a
|
|
440
|
+
// caller passing neither behaves EXACTLY as before (additive) — except under
|
|
441
|
+
// the test-only EVENTS_LEDGER_SANDBOX_ENV seam, which only 3. honours.
|
|
357
442
|
const filePath = opts.filePath ?? eventsFilePath(opts.repoRoot);
|
|
358
443
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
359
444
|
await fs.appendFile(filePath, line, 'utf8');
|
|
@@ -379,3 +464,437 @@ export async function emitEvent(type, payload = {}, opts = {}) {
|
|
|
379
464
|
}).catch(() => {});
|
|
380
465
|
}
|
|
381
466
|
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Rotation-aware reading (#1401)
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Read one JSONL source into a descriptor — never throws.
|
|
474
|
+
*
|
|
475
|
+
* @param {string} filePath
|
|
476
|
+
* @param {'active'|'archive'|'legacy-ring'} kind
|
|
477
|
+
* @returns {{path: string, kind: string, readable: boolean, records: object[],
|
|
478
|
+
* malformed_lines: number, first_ts: string|null, last_ts: string|null,
|
|
479
|
+
* error?: string}}
|
|
480
|
+
*/
|
|
481
|
+
function readEventSource(filePath, kind) {
|
|
482
|
+
let text;
|
|
483
|
+
try {
|
|
484
|
+
text = readFileSync(filePath, 'utf8');
|
|
485
|
+
} catch (err) {
|
|
486
|
+
return {
|
|
487
|
+
path: filePath,
|
|
488
|
+
kind,
|
|
489
|
+
readable: false,
|
|
490
|
+
records: [],
|
|
491
|
+
malformed_lines: 0,
|
|
492
|
+
first_ts: null,
|
|
493
|
+
last_ts: null,
|
|
494
|
+
error: err?.message ?? String(err),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const { records, malformedLines } = parseEventLines(text);
|
|
498
|
+
const { firstTs, lastTs } = summarizeEventRecords(records);
|
|
499
|
+
return {
|
|
500
|
+
path: filePath,
|
|
501
|
+
kind,
|
|
502
|
+
readable: true,
|
|
503
|
+
records,
|
|
504
|
+
malformed_lines: malformedLines,
|
|
505
|
+
first_ts: firstTs,
|
|
506
|
+
last_ts: lastTs,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Every archive belonging to `logPath`, in BOTH schemes.
|
|
512
|
+
*
|
|
513
|
+
* Reading the legacy `.1`..`.N` ring is not politeness towards old code — it is
|
|
514
|
+
* a live requirement: measured 2026-09-19, two fleet repos hold a ~10 MB
|
|
515
|
+
* `events.jsonl.1` written before #1401 switched the writer to `_archive/`. A
|
|
516
|
+
* reader that saw only the new scheme would drop that history and call the
|
|
517
|
+
* result complete.
|
|
518
|
+
*
|
|
519
|
+
* `unindexed` (#1423) names every OTHER `.jsonl` in that directory — a file the
|
|
520
|
+
* reader will NOT fold into the timeline because rotation did not write it.
|
|
521
|
+
* Reporting it is not the same as counting it as missing: this repo's own
|
|
522
|
+
* `_archive/` holds a hand-placed `events-worktree-vault-session-analysis-…`
|
|
523
|
+
* (80 records), and calling that a GAP would pin the ledger at
|
|
524
|
+
* `complete: false` forever, which is the instrument HR-101 calls broken. It is
|
|
525
|
+
* a NOTICE — visible, never a verdict.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} logPath — absolute path of the ACTIVE log.
|
|
528
|
+
* @returns {{archives: string[], legacy: string[], ringHoles: number[], unindexed: string[]}}
|
|
529
|
+
*/
|
|
530
|
+
function discoverArchives(logPath) {
|
|
531
|
+
const dir = path.dirname(logPath);
|
|
532
|
+
|
|
533
|
+
const archives = [];
|
|
534
|
+
const unindexed = [];
|
|
535
|
+
const archiveDir = path.join(dir, ARCHIVE_DIR_NAME);
|
|
536
|
+
try {
|
|
537
|
+
for (const name of readdirSync(archiveDir).sort()) {
|
|
538
|
+
const abs = path.join(archiveDir, name);
|
|
539
|
+
if (ARCHIVE_NAME_RE.test(name)) archives.push(abs);
|
|
540
|
+
else if (name.endsWith('.jsonl')) unindexed.push(abs);
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
/* no archive directory yet — not a gap, just nothing rotated here */
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// The ring was contiguous BY CONSTRUCTION (each rotation shifted every slot
|
|
547
|
+
// up by one), so a hole between two present slots can only mean a backup was
|
|
548
|
+
// removed out of band. Slots above the highest present one are simply
|
|
549
|
+
// "not rotated that many times" and are not holes.
|
|
550
|
+
const present = [];
|
|
551
|
+
for (let i = 1; i <= LEGACY_RING_MAX; i += 1) {
|
|
552
|
+
if (existsSync(`${logPath}.${i}`)) present.push(i);
|
|
553
|
+
}
|
|
554
|
+
const highest = present.length > 0 ? present[present.length - 1] : 0;
|
|
555
|
+
const ringHoles = [];
|
|
556
|
+
for (let i = 1; i < highest; i += 1) {
|
|
557
|
+
if (!present.includes(i)) ringHoles.push(i);
|
|
558
|
+
}
|
|
559
|
+
return { archives, legacy: present.map((i) => `${logPath}.${i}`), ringHoles, unindexed };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Read the events ledger ACROSS rotation boundaries — active file plus every
|
|
564
|
+
* archive still on disk — in time order, reporting what is missing instead of
|
|
565
|
+
* silently returning less.
|
|
566
|
+
*
|
|
567
|
+
* ## Why this exists (#1401)
|
|
568
|
+
*
|
|
569
|
+
* Before it, nothing in `scripts/` or `hooks/` read a rotated backup at all
|
|
570
|
+
* (census 2026-09-19 @ `8f15f77b`: `rg -n 'jsonl\.1|jsonl\.[0-9]' scripts/ hooks/`
|
|
571
|
+
* excluding tests → zero code hits). Every window analysis therefore lost its
|
|
572
|
+
* whole history at each rotation, silently — which is what reduced the #1037
|
|
573
|
+
* guard-attribution join to 2 of 38 sessions.
|
|
574
|
+
*
|
|
575
|
+
* ## The three honesty rules
|
|
576
|
+
*
|
|
577
|
+
* 1. **A missing archive is a FINDING, not an empty result.** When a record in
|
|
578
|
+
* a later file names `archived_as: X` and X is not on disk, that appears in
|
|
579
|
+
* `gaps` with the range X covered, and `complete` is `false`. This is the
|
|
580
|
+
* exact shape of the 2026-09-19 loss, and it is detectable only because the
|
|
581
|
+
* rotation writes that pointer (see `events-rotation.mjs`) — an archive
|
|
582
|
+
* deleted before #1401 left no trace and is undetectable by construction.
|
|
583
|
+
* EXISTENCE IS NOT READING (#1423): the sibling counts only when it was an
|
|
584
|
+
* actual SOURCE of this read. A file that exists beside the active log but
|
|
585
|
+
* whose name misses `ARCHIVE_NAME_RE` is never read, so treating
|
|
586
|
+
* `existsSync` as proof silenced the tombstone while its records stayed
|
|
587
|
+
* out — that case is its own gap kind, `unindexed-archive`.
|
|
588
|
+
* 2. **Unreadable lines are COUNTED** (`malformed_lines`, per source and total).
|
|
589
|
+
* A silently skipping JSONL parser turns a partial result into a clean
|
|
590
|
+
* verdict.
|
|
591
|
+
* 3. **Order is by measured time, not by filename.** Sources are sorted on
|
|
592
|
+
* their earliest parseable timestamp; records within a source keep file
|
|
593
|
+
* (append) order. A source with no parseable timestamp sorts last rather
|
|
594
|
+
* than being dropped.
|
|
595
|
+
* 4. **`complete` has THREE states, because the answer does (#1423).** `true` =
|
|
596
|
+
* measured and whole, `false` = measured with a named gap, `null` = NOT
|
|
597
|
+
* MEASURED: no source existed at all (no active file, no archive, no legacy
|
|
598
|
+
* ring), so `events: []` is the absence of a ledger and not the absence of
|
|
599
|
+
* events. `complete: true` on a repo with no ledger is the house failure
|
|
600
|
+
* class "a missing measurement looks like zero".
|
|
601
|
+
*
|
|
602
|
+
* Separate from `gaps`, `notices` carries what was SEEN but deliberately not
|
|
603
|
+
* read — today only `unindexed-archive-file` (see {@link discoverArchives}).
|
|
604
|
+
* A notice never moves `complete`.
|
|
605
|
+
*
|
|
606
|
+
* CEILING (BV-004): every source is read fully into memory — at the default
|
|
607
|
+
* `max-size-mb: 10` / `max-backups: 5` that is up to ~60 MB transient. There is
|
|
608
|
+
* no windowing parameter because no caller has asked for one; revisit when a
|
|
609
|
+
* consumer needs a `since` filter or `max-size-mb` is raised past ~100.
|
|
610
|
+
*
|
|
611
|
+
* @param {string} [repoRoot] — project root; defaults exactly as
|
|
612
|
+
* {@link eventsFilePath} does (and only the default form honours the test
|
|
613
|
+
* sandbox seam).
|
|
614
|
+
* @param {object} [opts={}]
|
|
615
|
+
* @param {string} [opts.filePath] — override the active-log path outright.
|
|
616
|
+
* @returns {{events: object[], sources: object[], malformed_lines: number,
|
|
617
|
+
* gaps: object[], notices: object[], complete: boolean|null,
|
|
618
|
+
* active_path: string}}
|
|
619
|
+
*/
|
|
620
|
+
export function readEventsWithRotations(repoRoot, opts = {}) {
|
|
621
|
+
const activePath = opts.filePath ?? eventsFilePath(repoRoot);
|
|
622
|
+
const { archives, legacy, ringHoles, unindexed } = discoverArchives(activePath);
|
|
623
|
+
|
|
624
|
+
const sources = [
|
|
625
|
+
...archives.map((p) => readEventSource(p, 'archive')),
|
|
626
|
+
...legacy.map((p) => readEventSource(p, 'legacy-ring')),
|
|
627
|
+
];
|
|
628
|
+
if (existsSync(activePath)) sources.push(readEventSource(activePath, 'active'));
|
|
629
|
+
|
|
630
|
+
// Time order across sources; undatable sources last, stable by path.
|
|
631
|
+
sources.sort((a, b) => {
|
|
632
|
+
const am = a.first_ts ? Date.parse(a.first_ts) : Infinity;
|
|
633
|
+
const bm = b.first_ts ? Date.parse(b.first_ts) : Infinity;
|
|
634
|
+
if (am !== bm) return am - bm;
|
|
635
|
+
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const gaps = [];
|
|
639
|
+
const onDisk = new Set(sources.map((s) => s.path));
|
|
640
|
+
// Same construction `discoverArchives` uses, so its unresolved entries in
|
|
641
|
+
// `onDisk` still match by string.
|
|
642
|
+
const ownArchiveDir = path.join(path.dirname(activePath), ARCHIVE_DIR_NAME);
|
|
643
|
+
|
|
644
|
+
// Rule 1 — every rotation tombstone must still point at a file.
|
|
645
|
+
for (const source of sources) {
|
|
646
|
+
for (const record of source.records) {
|
|
647
|
+
if (record?.event !== ROTATION_EVENT) continue;
|
|
648
|
+
const target = record.archived_as;
|
|
649
|
+
if (typeof target !== 'string' || target.length === 0) continue;
|
|
650
|
+
// #1411 — resolve the tombstone against THIS ledger's own `_archive/`,
|
|
651
|
+
// by BASENAME, and never against the absolute value it stores.
|
|
652
|
+
//
|
|
653
|
+
// The writer keeps `archived_as` absolute (`events-rotation.mjs` joins
|
|
654
|
+
// the repo root) and that value stays in the record as PROVENANCE. It is
|
|
655
|
+
// not a lookup key: a moved checkout, a clone, or a sibling git worktree
|
|
656
|
+
// — routine here — makes every tombstone name a path that does not exist
|
|
657
|
+
// in THIS tree, so the old exact comparison reported a phantom
|
|
658
|
+
// `missing-archive` for an archive sitting right beside the active file.
|
|
659
|
+
// Basename, not `realpath`: realpath cannot resolve a path that no longer
|
|
660
|
+
// exists, which IS the failure mode.
|
|
661
|
+
//
|
|
662
|
+
// ORDER (why the absolute value has no second chance): the sibling answer
|
|
663
|
+
// is consulted first, and an absolute hit would only be trustworthy while
|
|
664
|
+
// it pointed INSIDE this ledger's own archive dir — but any such path
|
|
665
|
+
// resolves to exactly the sibling path already tested, so once the
|
|
666
|
+
// sibling misses, an absolute hit can ONLY be a still-present OLD
|
|
667
|
+
// checkout. Honouring it would validate THIS ledger against a FOREIGN
|
|
668
|
+
// repo's archive: a silent false negative, worse than the phantom gap.
|
|
669
|
+
const sibling = path.join(ownArchiveDir, path.basename(target));
|
|
670
|
+
if (onDisk.has(sibling)) continue;
|
|
671
|
+
gaps.push({
|
|
672
|
+
// #1423 — THREE outcomes, not two: read (above), present-but-unread
|
|
673
|
+
// (here), gone (below). The middle one used to be silently folded into
|
|
674
|
+
// the first by `existsSync`, so a tombstone whose archive had been
|
|
675
|
+
// RENAMED out of `ARCHIVE_NAME_RE` reported `complete: true` while its
|
|
676
|
+
// records were absent from `events`.
|
|
677
|
+
kind: existsSync(sibling) ? 'unindexed-archive' : 'missing-archive',
|
|
678
|
+
path: sibling,
|
|
679
|
+
archived_as: target,
|
|
680
|
+
first_ts: record.first_ts ?? null,
|
|
681
|
+
last_ts: record.last_ts ?? null,
|
|
682
|
+
lines: record.lines ?? null,
|
|
683
|
+
size_before: record.size_before ?? null,
|
|
684
|
+
reported_by: source.path,
|
|
685
|
+
rotated_at: record.timestamp ?? null,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const slot of ringHoles) {
|
|
691
|
+
gaps.push({
|
|
692
|
+
kind: 'ring-hole',
|
|
693
|
+
archived_as: `${activePath}.${slot}`,
|
|
694
|
+
first_ts: null,
|
|
695
|
+
last_ts: null,
|
|
696
|
+
reported_by: activePath,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
for (const source of sources) {
|
|
701
|
+
if (source.readable) continue;
|
|
702
|
+
gaps.push({
|
|
703
|
+
kind: 'unreadable-source',
|
|
704
|
+
archived_as: source.path,
|
|
705
|
+
first_ts: null,
|
|
706
|
+
last_ts: null,
|
|
707
|
+
reported_by: source.path,
|
|
708
|
+
error: source.error ?? null,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const events = [];
|
|
713
|
+
let malformed = 0;
|
|
714
|
+
for (const source of sources) {
|
|
715
|
+
events.push(...source.records);
|
|
716
|
+
malformed += source.malformed_lines;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
events,
|
|
721
|
+
sources: sources.map(({ records, ...rest }) => ({ ...rest, records: records.length })),
|
|
722
|
+
malformed_lines: malformed,
|
|
723
|
+
gaps,
|
|
724
|
+
notices: unindexed.map((p) => ({ kind: 'unindexed-archive-file', path: p })),
|
|
725
|
+
// Honesty rule 4: no source at all ⇒ NOT MEASURED (`null`), never a clean
|
|
726
|
+
// `true` over an empty read.
|
|
727
|
+
complete: sources.length === 0 ? null : gaps.length === 0,
|
|
728
|
+
active_path: activePath,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
// Streaming reads across rotations (#1414)
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Default chunk for the streaming readers — 256 KiB, ~800 records in this
|
|
738
|
+
* repo's ledger.
|
|
739
|
+
*
|
|
740
|
+
* NAMED CEILING (BV-004): a line LONGER than one chunk is carried, never
|
|
741
|
+
* dropped, so the constant bounds memory and not correctness. REVISIT when one
|
|
742
|
+
* repo's `events.jsonl` passes 50 MB — at that size the backwards "never
|
|
743
|
+
* happened" walk needs an index rather than a scan.
|
|
744
|
+
*/
|
|
745
|
+
export const SCAN_CHUNK_BYTES = 256 * 1024;
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Every source of this ledger that EXISTS, NEWEST FIRST — without reading one
|
|
749
|
+
* byte of their contents (#1414).
|
|
750
|
+
*
|
|
751
|
+
* Order: the active file, then `_archive/` archives by name DESCENDING (the
|
|
752
|
+
* name encodes `first_ts`/`last_ts`, so lexical order is chronological), then
|
|
753
|
+
* the legacy ring ASCENDING (`.1` is the most recent backup, `.N` the oldest).
|
|
754
|
+
* Reverse the result for an oldest-first pass.
|
|
755
|
+
*
|
|
756
|
+
* Hand-placed files in `_archive/` that rotation did not write are NOT sources
|
|
757
|
+
* here, exactly as in {@link readEventsWithRotations} — one definition of "what
|
|
758
|
+
* belongs to this ledger", not two.
|
|
759
|
+
*
|
|
760
|
+
* @param {object} [opts={}]
|
|
761
|
+
* @param {string} [opts.filePath] — the ACTIVE log path; defaults as {@link eventsFilePath}.
|
|
762
|
+
* @param {string} [opts.repoRoot] — used only when `filePath` is absent.
|
|
763
|
+
* @returns {Array<{path: string, kind: 'active'|'archive'|'legacy-ring'}>}
|
|
764
|
+
*/
|
|
765
|
+
export function listEventSourcesNewestFirst(opts = {}) {
|
|
766
|
+
const activePath = opts.filePath ?? eventsFilePath(opts.repoRoot);
|
|
767
|
+
const { archives, legacy } = discoverArchives(activePath);
|
|
768
|
+
const out = [];
|
|
769
|
+
if (existsSync(activePath)) out.push({ path: activePath, kind: 'active' });
|
|
770
|
+
for (const p of [...archives].reverse()) out.push({ path: p, kind: 'archive' });
|
|
771
|
+
for (const p of legacy) out.push({ path: p, kind: 'legacy-ring' });
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Walk the ledger BACKWARDS across rotation boundaries, newest record first,
|
|
777
|
+
* stopping at the first record the caller accepts (#1414).
|
|
778
|
+
*
|
|
779
|
+
* ## Why this exists
|
|
780
|
+
*
|
|
781
|
+
* `readEventsWithRotations` loads every source fully — up to ~60 MB transient
|
|
782
|
+
* at the default `max-size-mb: 10` × `max-backups: 5`. That is the right shape
|
|
783
|
+
* for a cold analysis and the wrong one for a session-start probe, so the two
|
|
784
|
+
* HOT-PATH readers (`maintenance-due-banner.mjs`, `backfill-abandoned-sessions.mjs`)
|
|
785
|
+
* stayed single-file and answered "never happened" from the active file alone.
|
|
786
|
+
* This reader gives them the whole history at a bounded memory cost.
|
|
787
|
+
*
|
|
788
|
+
* A record SPLIT across a chunk boundary is CARRIED, never parsed twice or
|
|
789
|
+
* dropped: the bytes before a chunk's first newline are the tail of a line
|
|
790
|
+
* whose head sits in the chunk not read yet. Splitting on the 0x0A byte is safe
|
|
791
|
+
* on UTF-8 (no continuation byte equals a newline), and the carry is joined as
|
|
792
|
+
* a BUFFER, so a multibyte character never splits either.
|
|
793
|
+
*
|
|
794
|
+
* @param {object} opts
|
|
795
|
+
* @param {string} [opts.filePath] — the ACTIVE log path; defaults as {@link eventsFilePath}.
|
|
796
|
+
* @param {string} [opts.repoRoot] — used only when `filePath` is absent.
|
|
797
|
+
* @param {number} [opts.chunkBytes={@link SCAN_CHUNK_BYTES}]
|
|
798
|
+
* @param {(record: object, source: {path: string, kind: string}) => boolean} opts.onRecord —
|
|
799
|
+
* called with each PARSED record, newest first; returning `true` stops the walk.
|
|
800
|
+
* @param {string} [opts.filter] — cheap substring pre-filter applied to the RAW
|
|
801
|
+
* line before `JSON.parse`. The measured reason it exists: the caller that
|
|
802
|
+
* this replaced pre-filtered on the event name, and parsing every line of a
|
|
803
|
+
* 7.9 MB ledger to answer one question is the cost that made it do so.
|
|
804
|
+
* NOTE: with a filter set, `malformed_lines` counts only unreadable lines
|
|
805
|
+
* AMONG THE MATCHING ones — the rest are never parsed.
|
|
806
|
+
* @param {number} [opts.budgetMs] — wall-clock budget. When it runs out before
|
|
807
|
+
* a stop, the walk ends with `truncated: true` — which is NOT "not found":
|
|
808
|
+
* the caller must report undeterminable, never a clean negative.
|
|
809
|
+
* @returns {{stopped: boolean, truncated: boolean, malformed_lines: number,
|
|
810
|
+
* sources: string[], unreadable: string[]}}
|
|
811
|
+
*/
|
|
812
|
+
export function scanEventsBackwards(opts = {}) {
|
|
813
|
+
const { onRecord, filter, budgetMs } = opts;
|
|
814
|
+
if (typeof onRecord !== 'function') {
|
|
815
|
+
throw new TypeError('scanEventsBackwards: opts.onRecord must be a function');
|
|
816
|
+
}
|
|
817
|
+
const chunkBytes =
|
|
818
|
+
Number.isInteger(opts.chunkBytes) && opts.chunkBytes > 0 ? opts.chunkBytes : SCAN_CHUNK_BYTES;
|
|
819
|
+
const deadline = Number.isFinite(budgetMs) ? Date.now() + budgetMs : null;
|
|
820
|
+
const outOfTime = () => deadline !== null && Date.now() > deadline;
|
|
821
|
+
|
|
822
|
+
const scanned = [];
|
|
823
|
+
const unreadable = [];
|
|
824
|
+
let malformed = 0;
|
|
825
|
+
let stopped = false;
|
|
826
|
+
let truncated = false;
|
|
827
|
+
|
|
828
|
+
/** @returns {boolean} true ⇒ the caller accepted a record; stop everything. */
|
|
829
|
+
const consume = (block, source) => {
|
|
830
|
+
const lines = block.toString('utf8').split('\n');
|
|
831
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
832
|
+
const line = lines[i];
|
|
833
|
+
if (!line) continue;
|
|
834
|
+
if (typeof filter === 'string' && !line.includes(filter)) continue;
|
|
835
|
+
let record;
|
|
836
|
+
try {
|
|
837
|
+
record = JSON.parse(line);
|
|
838
|
+
} catch {
|
|
839
|
+
malformed += 1; // counted, never silently skipped (honesty rule 2)
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (onRecord(record, source) === true) return true;
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
for (const source of listEventSourcesNewestFirst(opts)) {
|
|
848
|
+
if (stopped || truncated) break;
|
|
849
|
+
if (outOfTime()) {
|
|
850
|
+
truncated = true;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
scanned.push(source.path);
|
|
854
|
+
let fd;
|
|
855
|
+
try {
|
|
856
|
+
fd = openSync(source.path, 'r');
|
|
857
|
+
let pos = fstatSync(fd).size;
|
|
858
|
+
/** Partial line at the FRONT of everything read from THIS source so far. */
|
|
859
|
+
let carry = Buffer.alloc(0);
|
|
860
|
+
while (pos > 0) {
|
|
861
|
+
if (outOfTime()) {
|
|
862
|
+
truncated = true;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
const length = Math.min(chunkBytes, pos);
|
|
866
|
+
pos -= length;
|
|
867
|
+
const buf = Buffer.alloc(length);
|
|
868
|
+
readSync(fd, buf, 0, length, pos);
|
|
869
|
+
const block = carry.length > 0 ? Buffer.concat([buf, carry]) : buf;
|
|
870
|
+
const firstNewline = block.indexOf(0x0a);
|
|
871
|
+
if (firstNewline === -1) {
|
|
872
|
+
carry = block; // a line longer than one chunk — no complete line yet
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (consume(block.subarray(firstNewline + 1), source)) {
|
|
876
|
+
stopped = true;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
carry = block.subarray(0, firstNewline);
|
|
880
|
+
}
|
|
881
|
+
// pos === 0: the carry is this source's FIRST line, complete by construction.
|
|
882
|
+
if (!stopped && !truncated && consume(carry, source)) stopped = true;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
// An unreadable source is a FINDING, not an empty one: it is exactly the
|
|
885
|
+
// case where "no record found" must not be reported as "never happened".
|
|
886
|
+
unreadable.push(source.path);
|
|
887
|
+
void err;
|
|
888
|
+
} finally {
|
|
889
|
+
if (fd !== undefined) {
|
|
890
|
+
try {
|
|
891
|
+
closeSync(fd);
|
|
892
|
+
} catch {
|
|
893
|
+
/* best-effort */
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return { stopped, truncated, malformed_lines: malformed, sources: scanned, unreadable };
|
|
900
|
+
}
|
|
@@ -30,6 +30,8 @@ import { promises as fs } from 'node:fs';
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { execSync } from 'node:child_process';
|
|
32
32
|
|
|
33
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
34
|
+
|
|
33
35
|
// ---------------------------------------------------------------------------
|
|
34
36
|
// Configuration defaults (mirrors the .sh defaults)
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -296,14 +298,7 @@ async function _cliMain() {
|
|
|
296
298
|
process.exit(3);
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
const isMain = process.argv[1] && (
|
|
301
|
-
process.argv[1] === new URL(import.meta.url).pathname ||
|
|
302
|
-
// Handle invocation without file:// prefix
|
|
303
|
-
process.argv[1].endsWith('fetch-baseline.mjs')
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (isMain) {
|
|
301
|
+
if (isMainModule(import.meta.url)) {
|
|
307
302
|
_cliMain().catch((err) => {
|
|
308
303
|
process.stderr.write(`FATAL: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
309
304
|
process.exit(3);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { digestSha256Short } from './crypto-digest-utils.mjs';
|
|
14
14
|
import { resolveHostPath } from './config/host-paths.mjs';
|
|
15
|
+
import { maskSource } from './js-mask.mjs';
|
|
15
16
|
import { readFileSync, statSync } from 'node:fs';
|
|
16
17
|
import { homedir } from 'node:os';
|
|
17
18
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -117,25 +118,34 @@ let _warnedFallback = false;
|
|
|
117
118
|
*/
|
|
118
119
|
const _cache = { mtime: null, result: null };
|
|
119
120
|
|
|
121
|
+
/** One quoted string literal — its interior in group 2. */
|
|
122
|
+
const STRING_LITERAL_RE = /(['"`])((?:\\.|(?!\1)[^\\\n])*)\1/g;
|
|
123
|
+
|
|
120
124
|
/**
|
|
121
|
-
* Parse the
|
|
125
|
+
* Parse the enum values out of a Zod `z.enum([...])` call in source text.
|
|
122
126
|
*
|
|
123
|
-
*
|
|
127
|
+
* `codeText` must be the source with its COMMENTS blanked
|
|
128
|
+
* (`maskSource(text, { keepLiterals: true })`): the values are the quoted
|
|
129
|
+
* literals of the enum body and nothing else. Splitting the raw body on commas
|
|
130
|
+
* rendered comment prose between the literals as enum values — 6 of 16
|
|
131
|
+
* `statusEnum` entries against the real baseline schema, one fused onto
|
|
132
|
+
* `'maintenance'` (#1298) — and every one reached agent prompts as an allowed
|
|
133
|
+
* `status`. Masking via the shared lexer also keeps a `//` inside a literal
|
|
134
|
+
* intact and a `]` inside a comment from ending the body early.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} codeText - comment-masked source
|
|
124
137
|
* @param {string} exportName - e.g. 'vaultNoteTypeSchema'
|
|
125
138
|
* @returns {string[]}
|
|
126
139
|
*/
|
|
127
|
-
function _extractEnum(
|
|
140
|
+
function _extractEnum(codeText, exportName) {
|
|
128
141
|
// Match: export const <name> = z.enum([ ...values... ]);
|
|
129
142
|
const re = new RegExp(
|
|
130
143
|
`export\\s+const\\s+${exportName}\\s*=\\s*z\\.enum\\(\\s*\\[([^\\]]+)\\]`,
|
|
131
144
|
's',
|
|
132
145
|
);
|
|
133
|
-
const match =
|
|
146
|
+
const match = codeText.match(re);
|
|
134
147
|
if (!match) return [];
|
|
135
|
-
return match[1]
|
|
136
|
-
.split(',')
|
|
137
|
-
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
138
|
-
.filter(Boolean);
|
|
148
|
+
return [...match[1].matchAll(STRING_LITERAL_RE)].map((m) => m[2]).filter(Boolean);
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
/**
|
|
@@ -145,8 +155,9 @@ function _extractEnum(text, exportName) {
|
|
|
145
155
|
* @returns {{ typeEnum: string[], statusEnum: string[], requiredFields: string[], idRegex: string, tagsRegex: string, schemaText: string }}
|
|
146
156
|
*/
|
|
147
157
|
function _parseSchema(text) {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
158
|
+
const codeText = maskSource(text, { keepLiterals: true });
|
|
159
|
+
const typeEnum = _extractEnum(codeText, 'vaultNoteTypeSchema');
|
|
160
|
+
const statusEnum = _extractEnum(codeText, 'vaultNoteStatusSchema');
|
|
150
161
|
|
|
151
162
|
// Required fields are id, type, created, updated — stable; extracted
|
|
152
163
|
// from the schema object declaration (non-optional fields).
|