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
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* records that between them said nothing happened (#1151). Its count is
|
|
12
12
|
* carried by the run event's `skipped` total and `action_breakdown`, so the
|
|
13
13
|
* class stays measured — only its per-line locators are gone.
|
|
14
|
+
* `skipped-duplicate-session` (#1291) is one of these per-entry records:
|
|
15
|
+
* a `--kind session` line whose `session_id` a later line in the same
|
|
16
|
+
* source superseded, so it was never dispatched. It carries `record_id`
|
|
17
|
+
* and `line` and never `path` — no target file exists for it.
|
|
14
18
|
* - `orchestrator.vault.mirror_run_completed` — ONE record per CLI run,
|
|
15
19
|
* carrying the DENOMINATOR (`total` plus the per-class counts).
|
|
16
20
|
*
|
|
@@ -162,7 +166,10 @@ export async function emitMirrorEvent({
|
|
|
162
166
|
* @param {number} opts.updated — entries whose action was `updated`.
|
|
163
167
|
* @param {number} opts.skipped — entries skipped for a NON-failure reason
|
|
164
168
|
* (`skipped-noop`, `skipped-handwritten`, `skipped-quality-low`,
|
|
165
|
-
* `skipped-collision-resolved`, `skipped-abandoned
|
|
169
|
+
* `skipped-collision-resolved`, `skipped-abandoned`,
|
|
170
|
+
* `skipped-duplicate-session`). The last one (#1291) is what keeps
|
|
171
|
+
* `created + updated + skipped + failed === total` true for a `--kind
|
|
172
|
+
* session` run whose dedup pass collapsed a duplicate `session_id` line.
|
|
166
173
|
* @param {number} opts.failed — entries that produced `skipped-invalid`
|
|
167
174
|
* (validation error or mapper crash). Split out from `skipped` because these
|
|
168
175
|
* are the runs where a session silently ends up WITHOUT its vault note.
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Telemetry: every {@link mirrorBoard} call — and therefore every
|
|
14
14
|
* {@link sweepBoard} call — emits exactly ONE {@link BOARD_EVENT} record,
|
|
15
|
-
* including the no-op paths (`skipped-vault-disabled`,
|
|
16
|
-
* `skipped-
|
|
15
|
+
* including the no-op paths (`skipped-vault-disabled`,
|
|
16
|
+
* `skipped-vault-not-canonical`, `skipped-handwritten`, `skipped-noop`,
|
|
17
|
+
* `skipped-write-failed`). Those are the states that previously
|
|
17
18
|
* looked identical to a healthy write from outside the process. Emission is
|
|
18
19
|
* best-effort and can never fail a board write.
|
|
19
20
|
*
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
* 2. Defense-in-depth: writeBoard hard-refuses to touch `_overview.md`
|
|
44
45
|
* (sven-owned, must NEVER be written by this generator).
|
|
45
46
|
*
|
|
46
|
-
* No console noise — library code
|
|
47
|
+
* No console noise — library code — except the one stderr line on a
|
|
48
|
+
* canonical-vault refusal (#1450). Plain Node ESM. No external deps.
|
|
47
49
|
*/
|
|
48
50
|
|
|
49
51
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
@@ -60,6 +62,8 @@ import { enumerateCandidates } from '../dispatcher/enumerate.mjs';
|
|
|
60
62
|
import { atomicWriteWithBackup } from '../io.mjs';
|
|
61
63
|
import { withBoardLock } from './board-lock.mjs';
|
|
62
64
|
import { expandTilde } from '../common.mjs';
|
|
65
|
+
import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
|
|
66
|
+
import { loadHostPaths } from '../config/host-paths.mjs';
|
|
63
67
|
|
|
64
68
|
/** Frontmatter sentinel that identifies generator-owned board files. */
|
|
65
69
|
export const GENERATOR_MARKER = 'session-orchestrator-active-sessions@1';
|
|
@@ -825,6 +829,8 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
|
|
|
825
829
|
* Vault path assertion (Epic #673 safety): the resolved vault dir MUST live
|
|
826
830
|
* under $HOME — a vault outside the home tree is refused as `skipped-vault-disabled`
|
|
827
831
|
* so a misconfigured path can never drive a write into an arbitrary location.
|
|
832
|
+
* It must also be a canonical Meta-Vault ({@link checkCanonicalVault}, #1450):
|
|
833
|
+
* otherwise `skipped-vault-not-canonical`, nothing written, no lock, dry-run included.
|
|
828
834
|
*
|
|
829
835
|
* @param {object} opts
|
|
830
836
|
* @param {string} opts.repoRoot — the repo whose row is being updated.
|
|
@@ -840,11 +846,14 @@ async function emitBoardEvent({ repoRoot, caller, action, path: outputPath, rows
|
|
|
840
846
|
* @param {Date} [opts.now]
|
|
841
847
|
* @param {boolean} [opts.dryRun]
|
|
842
848
|
* @param {object} [opts.fs] — injectable fs for tests.
|
|
843
|
-
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object
|
|
849
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
850
|
+
* readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
|
|
844
851
|
* — forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653).
|
|
852
|
+
* The same ctx feeds {@link checkCanonicalVault}; `readOriginUrl` replaces its
|
|
853
|
+
* `git remote get-url origin` read (tests only — tmp vaults have no origin).
|
|
845
854
|
* Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig: undefined }`) when
|
|
846
855
|
* asserting a fixture's committed `vault-dir` — omitting it reads the REAL host
|
|
847
|
-
* `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the fixture value
|
|
856
|
+
* `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir` override (if set) wins over the fixture value
|
|
848
857
|
* and bleeds into the assertion (issue #783). Production callers omit this — the
|
|
849
858
|
* default (real owner.yaml resolution) is the correct host-local behavior there.
|
|
850
859
|
* @returns {Promise<{ result: { action: string, path?: string }, rows?: number,
|
|
@@ -861,10 +870,19 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
861
870
|
}
|
|
862
871
|
|
|
863
872
|
// Read + parse Session Config. Any failure → silent no-op.
|
|
873
|
+
// The host context is resolved ONCE here and reused by the canonical-vault
|
|
874
|
+
// guard below: loadOwnerConfig() has no cache and its section-drop WARNs are
|
|
875
|
+
// not rate-limited, so a second load would print every WARN of a broken
|
|
876
|
+
// owner.yaml twice per board write (#1450 follow-up). The config file is read
|
|
877
|
+
// BEFORE owner.yaml — the order narrative-mirror.mjs shares (#1479); vault-dir
|
|
878
|
+
// precedence is applied in parseSessionConfig, so the order changes only
|
|
879
|
+
// whether a repo without a config file reads owner.yaml at all.
|
|
864
880
|
let config;
|
|
881
|
+
let hostCtx;
|
|
865
882
|
try {
|
|
866
883
|
const text = await readConfigFile(repoRoot);
|
|
867
|
-
|
|
884
|
+
hostCtx = hostPaths ?? loadHostPaths();
|
|
885
|
+
config = parseSessionConfig(text, { hostPaths: hostCtx });
|
|
868
886
|
} catch {
|
|
869
887
|
return { result: { action: 'skipped-vault-disabled' } };
|
|
870
888
|
}
|
|
@@ -886,6 +904,24 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
886
904
|
return { result: { action: 'skipped-vault-disabled' } };
|
|
887
905
|
}
|
|
888
906
|
|
|
907
|
+
// Safety: the vault must be a canonical Meta-Vault (#1450) — the same guard
|
|
908
|
+
// vault-mirror applies, so a vault-dir that resolved to a FOREIGN vault can
|
|
909
|
+
// never receive the board. Reuses the `hostCtx` the config parse used, so a
|
|
910
|
+
// hermetic ctx never reads the real owner.yaml and owner.yaml is loaded at
|
|
911
|
+
// most once. Runs before the dry-run branch and the lock: fail-closed.
|
|
912
|
+
const canonical = checkCanonicalVault({
|
|
913
|
+
vaultDir: expandedVault,
|
|
914
|
+
ownerConfig: hostCtx.ownerConfig,
|
|
915
|
+
env: hostCtx.env ?? process.env,
|
|
916
|
+
readOriginUrl: hostCtx.readOriginUrl,
|
|
917
|
+
});
|
|
918
|
+
if (!canonical.ok) {
|
|
919
|
+
process.stderr.write(
|
|
920
|
+
`vault-status board: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`,
|
|
921
|
+
);
|
|
922
|
+
return { result: { action: 'skipped-vault-not-canonical' } };
|
|
923
|
+
}
|
|
924
|
+
|
|
889
925
|
// `vault-name` (#660) overrides the git-derived repo slug for per-project
|
|
890
926
|
// vault namespacing. Without it `collectRows` falls back to
|
|
891
927
|
// `path.basename(repoRoot)`, so a repo whose directory name differs from its
|
|
@@ -1112,10 +1148,10 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1112
1148
|
* Public {@link mirrorBoardInner} wrapper that emits exactly ONE
|
|
1113
1149
|
* {@link BOARD_EVENT} per call — on EVERY path, no-ops included.
|
|
1114
1150
|
*
|
|
1115
|
-
* The wrapper exists so the emit cannot be forgotten: the
|
|
1116
|
-
* inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards
|
|
1117
|
-
* whatever {@link writeBoard}
|
|
1118
|
-
*
|
|
1151
|
+
* The wrapper exists so the emit cannot be forgotten: the seven return points
|
|
1152
|
+
* inside {@link mirrorBoardInner} (five `skipped-vault-disabled` guards, the
|
|
1153
|
+
* `skipped-vault-not-canonical` guard, plus whatever {@link writeBoard}
|
|
1154
|
+
* decides) all funnel through here, and so does the eighth someone adds next. Emitting per-return instead would leave each new
|
|
1119
1155
|
* early return silent by default — which is the exact defect being fixed.
|
|
1120
1156
|
*
|
|
1121
1157
|
* A THROW from the inner function is deliberately NOT converted into an event:
|
|
@@ -1276,7 +1312,8 @@ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
|
|
|
1276
1312
|
* @param {object} [opts.deps] — injectable deps for {@link enumerateCandidates}
|
|
1277
1313
|
* (test seam: `readdirSync`, `existsSync`, `readLock`, `isLockLive`,
|
|
1278
1314
|
* `getCrossRepoProjects`, `validatePathInsideProject`, `now`).
|
|
1279
|
-
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object
|
|
1315
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
1316
|
+
* readOriginUrl?: (vaultDir: string) => string }} [opts.hostPaths]
|
|
1280
1317
|
* — forwarded verbatim to every {@link mirrorBoard} call below (both the happy-path
|
|
1281
1318
|
* and the enumeration-failure-fallback path), which in turn forwards it to
|
|
1282
1319
|
* {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653/#783). See
|
|
@@ -43,6 +43,9 @@ import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
|
43
43
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
44
44
|
import { createSecretValueMasker } from '../secret-masker.mjs';
|
|
45
45
|
import { expandTilde } from '../common.mjs';
|
|
46
|
+
import { readVaultSlug } from '../vault-yaml.mjs';
|
|
47
|
+
import { checkCanonicalVault, describeOriginForLog } from '../named-vault-resolver.mjs';
|
|
48
|
+
import { loadHostPaths } from '../config/host-paths.mjs';
|
|
46
49
|
|
|
47
50
|
/** Frontmatter sentinel that identifies generator-owned narrative files. */
|
|
48
51
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
|
|
@@ -762,9 +765,10 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
|
|
|
762
765
|
* before any write.
|
|
763
766
|
*
|
|
764
767
|
* THIS FUNCTION WRITES TO THE OPERATOR'S REAL VAULT UNLESS YOU STOP IT. `vault-dir`
|
|
765
|
-
* resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `
|
|
766
|
-
* committed Session Config (`
|
|
767
|
-
* value in a CLAUDE.md you just wrote
|
|
768
|
+
* resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `vault-dirs:` cwd match >
|
|
769
|
+
* `owner.yaml` `paths.vault-dir` > committed Session Config (`resolveVaultDir` in
|
|
770
|
+
* `scripts/lib/config.mjs`) — so the literal value in a CLAUDE.md you just wrote
|
|
771
|
+
* into a tmp dir LOSES to every override. A
|
|
768
772
|
* probe that passes a synthetic `repoRoot` and nothing else has already written
|
|
769
773
|
* into the live vault once (#1025 review). Any caller that must not touch the real
|
|
770
774
|
* vault passes `hostPaths: { env: {}, ownerConfig: undefined }` (see below),
|
|
@@ -776,13 +780,16 @@ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
|
|
|
776
780
|
* now?: Date,
|
|
777
781
|
* dryRun?: boolean,
|
|
778
782
|
* fs?: object,
|
|
779
|
-
* hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object
|
|
783
|
+
* hostPaths?: { env?: Record<string, string|undefined>, ownerConfig?: object,
|
|
784
|
+
* readOriginUrl?: (vaultDir: string) => string },
|
|
780
785
|
* }} opts
|
|
781
786
|
* `hostPaths` is forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI
|
|
782
|
-
* seam (issue #653).
|
|
787
|
+
* seam (issue #653). The same ctx feeds {@link checkCanonicalVault} (#1450);
|
|
788
|
+
* `readOriginUrl` replaces its `git remote get-url origin` read (tests only —
|
|
789
|
+
* tmp vaults have no origin). Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig:
|
|
783
790
|
* undefined }`) when asserting a fixture's committed `vault-dir` — omitting it reads
|
|
784
|
-
* the REAL host `owner.yaml`, whose `paths.vault-dir`
|
|
785
|
-
* fixture value and bleeds into the assertion (issue #783). Production callers omit
|
|
791
|
+
* the REAL host `owner.yaml`, whose `vault-dirs:` cwd match or `paths.vault-dir`
|
|
792
|
+
* override (if set) wins over the fixture value and bleeds into the assertion (issue #783). Production callers omit
|
|
786
793
|
* this — the default (real owner.yaml resolution) is the correct host-local behavior.
|
|
787
794
|
* @returns {Promise<{ action: string, path?: string }>}
|
|
788
795
|
*/
|
|
@@ -828,7 +835,7 @@ export async function mirrorNarrative(opts) {
|
|
|
828
835
|
* Split out so that the two emit sites in `mirrorNarrative` (the narrative
|
|
829
836
|
* event AND, since #1028, the masker event) each cover EVERY outcome from
|
|
830
837
|
* ONE call: a future early return added inside this function is telemetered
|
|
831
|
-
* by construction, whereas hand-placing an emit beside each of the
|
|
838
|
+
* by construction, whereas hand-placing an emit beside each of the eight
|
|
832
839
|
* `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
|
|
833
840
|
* rather than inside it because the returned object is a PUBLIC shape that
|
|
834
841
|
* callers (and tests) compare with `toEqual` — adding a key there would be an
|
|
@@ -854,10 +861,19 @@ async function runNarrativeMirror(opts) {
|
|
|
854
861
|
const masker = createSecretValueMasker(process.env);
|
|
855
862
|
|
|
856
863
|
// Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
|
|
864
|
+
// The host context is loaded ONCE and shared by the Session Config parse and
|
|
865
|
+
// the canonical-vault guard below, so owner.yaml is read a single time per run.
|
|
866
|
+
// Order (#1479, same as board-writer.mjs): config file first, owner.yaml
|
|
867
|
+
// second. The `vault-dir` precedence (SO_VAULT_DIR > owner.yaml > committed)
|
|
868
|
+
// is applied inside parseSessionConfig, after both are in hand, so the order
|
|
869
|
+
// cannot change the resolved dir; it only spares a repo without a config file
|
|
870
|
+
// the owner.yaml read and its section-drop WARNs on a run that skips anyway.
|
|
857
871
|
let config;
|
|
872
|
+
let hostCtx;
|
|
858
873
|
try {
|
|
859
874
|
const configText = await readConfigFile(repoRoot);
|
|
860
|
-
|
|
875
|
+
hostCtx = hostPaths ?? loadHostPaths();
|
|
876
|
+
config = parseSessionConfig(configText, { hostPaths: hostCtx });
|
|
861
877
|
} catch {
|
|
862
878
|
return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
863
879
|
}
|
|
@@ -869,15 +885,29 @@ async function runNarrativeMirror(opts) {
|
|
|
869
885
|
|
|
870
886
|
// Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
|
|
871
887
|
// it from the operator-configured `vault-name` override (#660/#832) when set,
|
|
872
|
-
// else the
|
|
873
|
-
//
|
|
888
|
+
// else the `.vault.yaml` slug (#1131), else the repoRoot basename — never
|
|
889
|
+
// silently mis-file under 'unknown' (#675 review).
|
|
874
890
|
const vaultNameOverride =
|
|
875
891
|
typeof vaultIntegration['vault-name'] === 'string' && vaultIntegration['vault-name'].trim()
|
|
876
892
|
? vaultIntegration['vault-name'].trim()
|
|
877
893
|
: null;
|
|
878
|
-
const
|
|
894
|
+
const explicitName = (typeof repo === 'string' && repo.trim().length > 0)
|
|
879
895
|
? repo
|
|
880
|
-
: vaultNameOverride
|
|
896
|
+
: vaultNameOverride;
|
|
897
|
+
|
|
898
|
+
// #1131: with NO explicit override, the repo's own `.vault.yaml`
|
|
899
|
+
// `metadata.slug` — the CANONICAL vault registration — outranks the
|
|
900
|
+
// directory basename. Precedence: explicit `repo` opt > `vault-name` >
|
|
901
|
+
// `.vault.yaml` slug > basename.
|
|
902
|
+
//
|
|
903
|
+
// WHY the two overrides still win: both are things a CALLER/operator states
|
|
904
|
+
// for THIS call (`repo` is only ever passed to override the derived name —
|
|
905
|
+
// the sole production caller, `skills/session-end/session-metrics-write.md`,
|
|
906
|
+
// passes `repoRoot` alone), so honouring the file over them would make an
|
|
907
|
+
// explicit override unhonourable. The file wins over the basename, which is
|
|
908
|
+
// a guess nobody stated.
|
|
909
|
+
const vaultYamlSlug = explicitName ? null : readVaultSlug(repoRoot);
|
|
910
|
+
const repoName = explicitName ?? vaultYamlSlug ?? path.basename(path.resolve(repoRoot));
|
|
881
911
|
|
|
882
912
|
const rawVaultDir = vaultIntegration['vault-dir'];
|
|
883
913
|
if (!rawVaultDir || typeof rawVaultDir !== 'string') {
|
|
@@ -885,11 +915,36 @@ async function runNarrativeMirror(opts) {
|
|
|
885
915
|
}
|
|
886
916
|
|
|
887
917
|
const vaultDir = path.resolve(expandTilde(rawVaultDir));
|
|
918
|
+
|
|
919
|
+
// #1450: same shared guard as board-writer.mjs and scripts/vault-mirror.mjs —
|
|
920
|
+
// refuse a vault-dir whose git origin is not a canonical Meta-Vault. The
|
|
921
|
+
// stderr line names the expected suffixes and the origin, never the vault
|
|
922
|
+
// path itself.
|
|
923
|
+
const canonical = checkCanonicalVault({
|
|
924
|
+
vaultDir,
|
|
925
|
+
ownerConfig: hostCtx.ownerConfig,
|
|
926
|
+
env: hostCtx.env ?? process.env,
|
|
927
|
+
readOriginUrl: hostCtx.readOriginUrl,
|
|
928
|
+
});
|
|
929
|
+
if (!canonical.ok) {
|
|
930
|
+
process.stderr.write(`narrative-mirror: refusing to write — vault is not the canonical Meta-Vault (expected git origin ending in one of: ${canonical.expected.join(', ')}; got ${describeOriginForLog(canonical.got) || 'no git origin'})\n`);
|
|
931
|
+
return { result: { action: 'skipped-vault-not-canonical' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
932
|
+
}
|
|
933
|
+
|
|
888
934
|
const candidateSlug = subjectToSlug(repoName) || 'unknown';
|
|
889
|
-
//
|
|
890
|
-
//
|
|
935
|
+
// A declared `.vault.yaml` slug needs no healing — it IS the canonical folder
|
|
936
|
+
// name, so resolveLooseSlug is SKIPPED for it. Running the healer over it
|
|
937
|
+
// would re-open the bug this fixes: a vault that also carries an unhealed
|
|
938
|
+
// legacy folder (e.g. `foobarapp` beside the declared `foo-bar-app`) makes
|
|
939
|
+
// the loose match ambiguous, and ambiguity falls back to the candidate —
|
|
940
|
+
// which is how a duplicate pair, once created, keeps being written to.
|
|
941
|
+
//
|
|
942
|
+
// Otherwise: loose-match against existing 01-projects/ folders before minting
|
|
943
|
+
// a new slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
|
|
891
944
|
// rules. Falls through to `candidateSlug` unchanged on any read failure.
|
|
892
|
-
const repoSlug =
|
|
945
|
+
const repoSlug = vaultYamlSlug
|
|
946
|
+
? candidateSlug
|
|
947
|
+
: resolveLooseSlug(vaultDir, candidateSlug, { readdirSync: injectedFs?.readdirSync });
|
|
893
948
|
const outputPath = resolveNarrativePath(vaultDir, repoSlug);
|
|
894
949
|
|
|
895
950
|
// Defense-in-depth: ensure the resolved file stays inside the vault root.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vault-yaml.mjs — Reader for a repo's `.vault.yaml` registration slug (#1131).
|
|
3
|
+
*
|
|
4
|
+
* A repo registered in the Meta-Vault carries `.vault.yaml` at its root, whose
|
|
5
|
+
* `metadata.slug` is the CANONICAL vault slug for that project — the name of
|
|
6
|
+
* its `01-projects/<slug>/` folder. Until #1131 nothing READ that key: every
|
|
7
|
+
* vault writer derived the slug from the checkout DIRECTORY name instead
|
|
8
|
+
* (`scripts/lib/vault-status/narrative-mirror.mjs` via `subjectToSlug`,
|
|
9
|
+
* `scripts/lib/vault-mirror/namespace.mjs` via `basename(process.cwd())`), so a
|
|
10
|
+
* repo whose directory name differs lexically from its registered slug had its
|
|
11
|
+
* narrative and its learnings/sessions filed under a second, wrong folder — and
|
|
12
|
+
* the loose-slug healer in narrative-mirror (#829) only repairs case and
|
|
13
|
+
* punctuation drift, never a genuinely different name.
|
|
14
|
+
*
|
|
15
|
+
* Exports:
|
|
16
|
+
* VAULT_YAML_FILE — the basename this module reads
|
|
17
|
+
* readVaultSlug — repoRoot → validated `metadata.slug` string, or null
|
|
18
|
+
*
|
|
19
|
+
* Contract: NEVER throws. Absent file, unreadable file, malformed YAML, missing
|
|
20
|
+
* key, non-string value, or a value that fails the repo's slug predicate all
|
|
21
|
+
* return `null`, leaving every caller on its pre-#1131 derivation chain.
|
|
22
|
+
*
|
|
23
|
+
* Import closure is deliberately tiny (`node:fs`, `node:path`, `node:module`,
|
|
24
|
+
* `./vault-mirror/utils.mjs`) — `namespace.mjs` is documented as a leaf-ward
|
|
25
|
+
* module and must stay one.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
|
|
32
|
+
import { isValidSlug } from './vault-mirror/utils.mjs';
|
|
33
|
+
|
|
34
|
+
/** Basename of the per-repo vault registration file. */
|
|
35
|
+
export const VAULT_YAML_FILE = '.vault.yaml';
|
|
36
|
+
|
|
37
|
+
// ── Lazy js-yaml resolution ──────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// Same shape (and same reason) as `getYaml()` in scripts/lib/owner-yaml.mjs:
|
|
40
|
+
// a static `import yaml from 'js-yaml'` crashes any consumer running without
|
|
41
|
+
// `node_modules`, at MODULE LOAD, where the failure is not catchable. Both
|
|
42
|
+
// consumers of this reader are synchronous, so `createRequire` — not
|
|
43
|
+
// `await import()` — is the resolution mechanism.
|
|
44
|
+
|
|
45
|
+
/** @type {null | false | { load: Function }} */
|
|
46
|
+
let _yaml = null;
|
|
47
|
+
let _yamlWarned = false;
|
|
48
|
+
|
|
49
|
+
function getYaml() {
|
|
50
|
+
if (_yaml !== null) return _yaml === false ? null : _yaml;
|
|
51
|
+
try {
|
|
52
|
+
_yaml = createRequire(import.meta.url)('js-yaml');
|
|
53
|
+
} catch {
|
|
54
|
+
_yaml = false;
|
|
55
|
+
}
|
|
56
|
+
return _yaml === false ? null : _yaml;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One-per-process actionable WARN, emitted ONLY when a `.vault.yaml` actually
|
|
61
|
+
* exists and could therefore have changed the resolved slug. A repo without the
|
|
62
|
+
* file loses nothing by the parser being absent, so it stays silent.
|
|
63
|
+
*/
|
|
64
|
+
function warnYamlMissing() {
|
|
65
|
+
if (_yamlWarned) return;
|
|
66
|
+
_yamlWarned = true;
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
"WARN vault-yaml: 'js-yaml' is not installed — .vault.yaml cannot be parsed; " +
|
|
69
|
+
'the vault slug falls back to the directory-derived name (run \'npm install\' in the plugin directory)\n',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read the canonical vault slug declared at `metadata.slug` in
|
|
75
|
+
* `<repoRoot>/.vault.yaml`.
|
|
76
|
+
*
|
|
77
|
+
* The value becomes a FILESYSTEM PATH SEGMENT under the operator's vault, so it
|
|
78
|
+
* is validated with the repo's existing slug predicate ({@link isValidSlug},
|
|
79
|
+
* `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`) rather than trusted. That rejects `/`, `..`,
|
|
80
|
+
* leading dots, whitespace and uppercase — this validation is a trust-boundary
|
|
81
|
+
* control, not hygiene: `.vault.yaml` is repo-supplied data, and a slug such as
|
|
82
|
+
* `../../etc` would otherwise escape the vault root.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} repoRoot - absolute or relative path to the repository root.
|
|
85
|
+
* @returns {string|null} the validated slug, or `null` when it cannot be read.
|
|
86
|
+
*/
|
|
87
|
+
export function readVaultSlug(repoRoot) {
|
|
88
|
+
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') return null;
|
|
89
|
+
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync(path.join(repoRoot, VAULT_YAML_FILE), 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
// Absent, unreadable, a directory, … — all "no declared slug".
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const yaml = getYaml();
|
|
99
|
+
if (!yaml) {
|
|
100
|
+
warnYamlMissing();
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let doc;
|
|
105
|
+
try {
|
|
106
|
+
doc = yaml.load(text);
|
|
107
|
+
} catch {
|
|
108
|
+
// Malformed YAML must never break a vault write.
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!doc || typeof doc !== 'object') return null;
|
|
113
|
+
const metadata = doc.metadata;
|
|
114
|
+
if (!metadata || typeof metadata !== 'object') return null;
|
|
115
|
+
|
|
116
|
+
const slug = metadata.slug;
|
|
117
|
+
return isValidSlug(slug) ? slug : null;
|
|
118
|
+
}
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
* exists to be.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Re-exported, not re-declared: the value lives in `process-group.mjs` since
|
|
26
|
+
* 773a994b, but this module exported it in 5.3.x and the package ships
|
|
27
|
+
* `scripts/` with no `exports` map — so a deep importer still links against
|
|
28
|
+
* this name. One definition, two import paths.
|
|
29
|
+
*/
|
|
30
|
+
export { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
31
|
+
|
|
24
32
|
/**
|
|
25
33
|
* Roles that may NEVER run on a foreign model. Copied verbatim from
|
|
26
34
|
* account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
|
|
@@ -44,22 +52,6 @@ export const NEVER_FOREIGN_ROLES = Object.freeze([
|
|
|
44
52
|
* manufactures timeouts and reads as model failure. */
|
|
45
53
|
export const DEFAULT_TIMEOUT_SEC = 900;
|
|
46
54
|
|
|
47
|
-
/**
|
|
48
|
-
* Grace period between SIGTERM and SIGKILL, in ms.
|
|
49
|
-
*
|
|
50
|
-
* `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
|
|
51
|
-
* ignores it never emits `close`, and this module's only resolve paths are
|
|
52
|
-
* `close` and `error` — so without escalation a wedged `cursor-agent` hangs
|
|
53
|
-
* `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
|
|
54
|
-
* catchable, so the escalation always terminates.
|
|
55
|
-
*
|
|
56
|
-
* Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
|
|
57
|
-
* stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
|
|
58
|
-
* measured needing longer than that to shut down — raise the constant, never
|
|
59
|
-
* drop the escalation.
|
|
60
|
-
*/
|
|
61
|
-
export const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
62
|
-
|
|
63
55
|
/** A runId names a directory and a log file. Anything outside this alphabet —
|
|
64
56
|
* or the two relative-path literals the alphabet happens to admit — can escape
|
|
65
57
|
* the parent it is joined to. */
|
|
@@ -90,8 +82,8 @@ export function isNeverForeignRole(role) {
|
|
|
90
82
|
*
|
|
91
83
|
* The escalation is the difference between a bounded dispatch and a hung wave:
|
|
92
84
|
* this function's only resolve paths are the child's `close` and `error`
|
|
93
|
-
* events, so a child that ignores SIGTERM never lets the promise settle.
|
|
94
|
-
*
|
|
85
|
+
* events, so a child that ignores SIGTERM never lets the promise settle. The
|
|
86
|
+
* callers pass `DEFAULT_KILL_GRACE_MS` from `process-group.mjs`.
|
|
95
87
|
*
|
|
96
88
|
* @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
|
|
97
89
|
* timerFired: boolean, killSignals: string[], errorCode: string|null}>}
|
|
@@ -54,8 +54,8 @@ import path from 'node:path';
|
|
|
54
54
|
|
|
55
55
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
56
56
|
import { isPathInside } from '../path-utils.mjs';
|
|
57
|
+
import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
57
58
|
import {
|
|
58
|
-
DEFAULT_KILL_GRACE_MS,
|
|
59
59
|
DEFAULT_TIMEOUT_SEC,
|
|
60
60
|
isNeverForeignRole,
|
|
61
61
|
isSafeRunId,
|
|
@@ -34,8 +34,8 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
36
36
|
import { isPathInside } from '../path-utils.mjs';
|
|
37
|
+
import { DEFAULT_KILL_GRACE_MS } from '../process-group.mjs';
|
|
37
38
|
import {
|
|
38
|
-
DEFAULT_KILL_GRACE_MS,
|
|
39
39
|
DEFAULT_TIMEOUT_SEC,
|
|
40
40
|
isNeverForeignRole,
|
|
41
41
|
isSafeRunId,
|
|
@@ -123,6 +123,14 @@ async function applyDecisionRules(measurements, opts) {
|
|
|
123
123
|
* declares a remote host that accepts this wave role, route the wave to that
|
|
124
124
|
* host at its full planned agent count instead of shrinking it.
|
|
125
125
|
*
|
|
126
|
+
* `offload-first: true` in Session Config (#1465) widens that trigger to a
|
|
127
|
+
* `proceed` verdict too: an offloadable wave role goes to a ready host even when
|
|
128
|
+
* the local host has headroom. Host selection, the witness contract and the
|
|
129
|
+
* HR-004 cap below are identical on both paths. With the switch on, a wave that
|
|
130
|
+
* stays local anyway (no host declared, none ready) carries a named
|
|
131
|
+
* `offload-first:` reason, so the switch never fails silently; a role absent
|
|
132
|
+
* from OFFLOADABLE_WAVE_ROLES stays local without one.
|
|
133
|
+
*
|
|
126
134
|
* The gate does NOT probe the network — a placement decision must stay a pure
|
|
127
135
|
* function of its inputs. The caller supplies a readiness WITNESS:
|
|
128
136
|
* - `opts.remoteReady` — `{ [alias]: boolean }`, e.g. built from the
|
|
@@ -140,13 +148,24 @@ async function applyDecisionRules(measurements, opts) {
|
|
|
140
148
|
* @returns {Promise<{decision: string, agents: number, reasons: string[], measurements: object, host?: string}>}
|
|
141
149
|
*/
|
|
142
150
|
async function applyOffloadDecision(result, opts) {
|
|
143
|
-
|
|
151
|
+
const offloadFirst = opts.config?.['offload-first'] === true;
|
|
152
|
+
if (!offloadFirst && result.decision !== 'reduce' && result.decision !== 'coordinator-direct') {
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
144
155
|
|
|
145
156
|
const { config, plannedAgents, waveRole, remoteReady, probeFn = null } = opts;
|
|
157
|
+
const role = String(waveRole ?? '').trim().toLowerCase();
|
|
146
158
|
const hosts = config?.['remote-hosts'];
|
|
147
|
-
if (!Array.isArray(hosts) || hosts.length === 0)
|
|
159
|
+
if (!Array.isArray(hosts) || hosts.length === 0) {
|
|
160
|
+
// Only an offloadable role could have been placed, so only it gets the note.
|
|
161
|
+
return offloadFirst && OFFLOADABLE_WAVE_ROLES[role] !== undefined
|
|
162
|
+
? {
|
|
163
|
+
...result,
|
|
164
|
+
reasons: [...result.reasons, 'offload-first: true but no remote-hosts declared — staying local'],
|
|
165
|
+
}
|
|
166
|
+
: result;
|
|
167
|
+
}
|
|
148
168
|
|
|
149
|
-
const role = String(waveRole ?? '').trim().toLowerCase();
|
|
150
169
|
// No isNeverForeignRole() check here (D8 #1204 LOW-1): the invariant is
|
|
151
170
|
// enforced STRUCTURALLY by this mapping table, not by an extra guard.
|
|
152
171
|
// NEVER_FOREIGN_ROLES {impl-core, security-review, migration, release,
|
|
@@ -186,9 +205,10 @@ async function applyOffloadDecision(result, opts) {
|
|
|
186
205
|
}
|
|
187
206
|
}
|
|
188
207
|
if (host === undefined) {
|
|
189
|
-
|
|
190
|
-
?
|
|
191
|
-
:
|
|
208
|
+
const notes = offloadFirst
|
|
209
|
+
? [...probeFailures, `offload-first: no ready host for '${role}' — staying local`]
|
|
210
|
+
: probeFailures;
|
|
211
|
+
return notes.length === 0 ? result : { ...result, reasons: [...result.reasons, ...notes] };
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
// The wave runs at its planned size again — but never above the HR-004 static
|
|
@@ -207,7 +227,9 @@ async function applyOffloadDecision(result, opts) {
|
|
|
207
227
|
host: host.alias,
|
|
208
228
|
reasons: [
|
|
209
229
|
...result.reasons,
|
|
210
|
-
|
|
230
|
+
result.decision === 'proceed'
|
|
231
|
+
? `offload-first: ${role} routed to host '${host.alias}' (verdict was proceed)`
|
|
232
|
+
: `offload: ${role} routed to host '${host.alias}' instead of reducing to ${result.agents}`,
|
|
211
233
|
],
|
|
212
234
|
measurements: result.measurements,
|
|
213
235
|
};
|
|
@@ -383,14 +405,18 @@ export async function evaluateWaveResourceGate(opts) {
|
|
|
383
405
|
}
|
|
384
406
|
|
|
385
407
|
// Rule 2: probe the system (or use override for tests).
|
|
408
|
+
// A failed probe yields `proceed`. Under `offload-first: true` (#1465) that
|
|
409
|
+
// result still goes through placement: routing to a witnessed-ready host needs
|
|
410
|
+
// no local measurement, so a broken probe must not silently disable the switch.
|
|
386
411
|
const measured = await extractMeasurements(opts);
|
|
387
412
|
if ('probeFailed' in measured) {
|
|
388
|
-
|
|
413
|
+
const failed = {
|
|
389
414
|
decision: 'proceed',
|
|
390
415
|
agents: plannedAgents,
|
|
391
416
|
reasons: ['probe failed (ignored)'],
|
|
392
417
|
measurements: {},
|
|
393
418
|
};
|
|
419
|
+
return config['offload-first'] === true ? applyOffloadDecision(failed, opts) : failed;
|
|
394
420
|
}
|
|
395
421
|
|
|
396
422
|
// Rules 3-8: apply decision rule sequence.
|