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
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* auto-dialectic.mjs — Cadence helper for
|
|
2
|
+
* auto-dialectic.mjs — Cadence helper for the dialectic derivation (#506, F2.5).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the auto-dream.mjs API shape exactly. Decides whether
|
|
5
|
-
*
|
|
4
|
+
* Mirrors the auto-dream.mjs API shape exactly. Decides whether a dialectic
|
|
5
|
+
* derivation is DUE, writes the proposed peer-card diff to
|
|
6
6
|
* `.orchestrator/dialectic-pending.md` atomically, and tracks the last-run
|
|
7
|
-
* timestamp at `.orchestrator/dialectic-last-run
|
|
8
|
-
*
|
|
7
|
+
* timestamp at `.orchestrator/dialectic-last-run`.
|
|
8
|
+
*
|
|
9
|
+
* Who calls what (the session-end Phase 3.6.7 auto-trigger is GONE — #1288):
|
|
10
|
+
* - `shouldDispatchAutoDialectic` — read by the session-start `maintenance-due`
|
|
11
|
+
* probe (`scripts/lib/maintenance-due-banner.mjs`) to report the `dialectic`
|
|
12
|
+
* signal. Side-effect-free by contract: a variant advancing the last-run stamp
|
|
13
|
+
* would consume the signal it reports.
|
|
14
|
+
* - `writeDialecticPending` / `consumeDialecticPending` / `writeDialecticLastRun` —
|
|
15
|
+
* called by `/evolve dialectic` (Step 6.4 in
|
|
16
|
+
* `skills/evolve/references/evolve-dialectic-mode.md`), the only trigger today.
|
|
17
|
+
* - `renderPendingBody` / `comparePendingBody` (#1386) — the same Step 6.4 builds
|
|
18
|
+
* the sidecar body with the former on the dry-run path and, on `--apply`,
|
|
19
|
+
* compares the freshly derived body against the reviewed sidecar with the
|
|
20
|
+
* latter before writing any peer card.
|
|
21
|
+
* - Both sidecars are READ by the same `maintenance-due` probe.
|
|
9
22
|
*
|
|
10
23
|
* Decision inputs (PRD F2.5 acceptance criteria):
|
|
11
24
|
* - dialectic.cadence (default 5) — sessions since last dialectic run
|
|
@@ -17,7 +30,7 @@
|
|
|
17
30
|
* separate helpers. No external deps — Node 20+ stdlib only.
|
|
18
31
|
*/
|
|
19
32
|
|
|
20
|
-
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
33
|
+
import { readFile, writeFile, rename, unlink, mkdir, readdir, stat, lstat } from 'node:fs/promises';
|
|
21
34
|
import { existsSync } from 'node:fs';
|
|
22
35
|
import { randomUUID } from 'node:crypto';
|
|
23
36
|
import path from 'node:path';
|
|
@@ -37,6 +50,20 @@ export const DIALECTIC_LAST_RUN_PATH = '.orchestrator/dialectic-last-run';
|
|
|
37
50
|
/** Repo-relative path to the pending dialectic proposal sidecar. */
|
|
38
51
|
export const DIALECTIC_PENDING_PATH = '.orchestrator/dialectic-pending.md';
|
|
39
52
|
|
|
53
|
+
/** Repo-relative directory the consumed sidecars are archived into (#1388 P9). */
|
|
54
|
+
export const DIALECTIC_CONSUMED_DIR = '.orchestrator/consumed';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Retention ceiling for `.orchestrator/consumed/` — newest N archived sidecars
|
|
58
|
+
* survive a consume, older ones are pruned.
|
|
59
|
+
*
|
|
60
|
+
* Ceiling + revisit trigger (BV-004): newest 10; revisit if `checkStaleArtifacts`
|
|
61
|
+
* (`scripts/lib/project-hygiene.mjs`) ever reports a `consumed/` file. That probe
|
|
62
|
+
* counts every untracked `.orchestrator/**` file older than 30 days, so an
|
|
63
|
+
* unbounded archive would become a standing hygiene finding.
|
|
64
|
+
*/
|
|
65
|
+
export const DIALECTIC_CONSUMED_RETENTION = 10;
|
|
66
|
+
|
|
40
67
|
// ---------------------------------------------------------------------------
|
|
41
68
|
// Path helpers
|
|
42
69
|
// ---------------------------------------------------------------------------
|
|
@@ -49,6 +76,10 @@ function pendingPath(repoRoot) {
|
|
|
49
76
|
return path.join(repoRoot, DIALECTIC_PENDING_PATH);
|
|
50
77
|
}
|
|
51
78
|
|
|
79
|
+
function consumedDirPath(repoRoot) {
|
|
80
|
+
return path.join(repoRoot, DIALECTIC_CONSUMED_DIR);
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
function sessionsJsonlPath(repoRoot) {
|
|
53
84
|
return path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
54
85
|
}
|
|
@@ -180,15 +211,16 @@ export async function readDialecticSignals({ repoRoot } = {}) {
|
|
|
180
211
|
// ---------------------------------------------------------------------------
|
|
181
212
|
|
|
182
213
|
/**
|
|
183
|
-
* Decide whether
|
|
184
|
-
* `/evolve
|
|
214
|
+
* Decide whether a dialectic derivation is DUE — i.e. whether the operator should
|
|
215
|
+
* run `/evolve dialectic` (dry-run first). Read by the session-start
|
|
216
|
+
* `maintenance-due` probe; no auto-trigger consumes this any more (#1288).
|
|
185
217
|
*
|
|
186
218
|
* Rules (PRD F2.5):
|
|
187
219
|
* - cadence === 0 → never trigger (kill-switch).
|
|
188
220
|
* - AC4 precondition: sessionsSinceLast === 0 AND learningsSinceLast === 0 →
|
|
189
221
|
* no new input since last run → skip with reason
|
|
190
|
-
* `no-new-input-since-last-run` (
|
|
191
|
-
*
|
|
222
|
+
* `no-new-input-since-last-run` (the reason string is part of the contract —
|
|
223
|
+
* the `maintenance-due` probe reports it verbatim).
|
|
192
224
|
* - sessionsSinceLast >= cadence → trigger (cadence threshold met).
|
|
193
225
|
* - Otherwise → skip with reason `under-threshold (sessions=N/M)`.
|
|
194
226
|
*
|
|
@@ -257,8 +289,12 @@ export async function shouldDispatchAutoDialectic({
|
|
|
257
289
|
* never a half-written intermediate (mirrors auto-dream.mjs:248-251).
|
|
258
290
|
*
|
|
259
291
|
* Defensive: returns `{ok: false, error}` on filesystem failure rather than
|
|
260
|
-
* throwing —
|
|
261
|
-
*
|
|
292
|
+
* throwing — the caller logs the error and continues.
|
|
293
|
+
*
|
|
294
|
+
* Caller: `/evolve dialectic` Step 6.4 (`skills/evolve/references/evolve-dialectic-mode.md`),
|
|
295
|
+
* after a successful `--apply` AND after the operator explicitly discards a
|
|
296
|
+
* proposal. Without this write the maintenance-due `dialectic` signal never
|
|
297
|
+
* resets (#1380). The session-end auto-trigger that used to call it is gone.
|
|
262
298
|
*
|
|
263
299
|
* @param {object} args
|
|
264
300
|
* @param {string} args.repoRoot
|
|
@@ -293,11 +329,17 @@ export async function writeDialecticLastRun({ repoRoot, isoTimestamp } = {}) {
|
|
|
293
329
|
* Write the proposed dialectic diff to `.orchestrator/dialectic-pending.md`
|
|
294
330
|
* atomically.
|
|
295
331
|
*
|
|
332
|
+
* Caller: `/evolve dialectic` Step 6.4's dry-run branch
|
|
333
|
+
* (`skills/evolve/references/evolve-dialectic-mode.md`) — `runDialecticDeriver()`
|
|
334
|
+
* returns the diff and the caller persists it here. There is no session-end
|
|
335
|
+
* auto-trigger any more (#1288).
|
|
336
|
+
*
|
|
296
337
|
* Caller supplies the body (a Markdown document containing the peer-card
|
|
297
338
|
* diff and any narrative). This helper prepends a minimal hand-rolled YAML
|
|
298
|
-
* frontmatter block carrying the metadata
|
|
299
|
-
*
|
|
300
|
-
*
|
|
339
|
+
* frontmatter block carrying the metadata the operator's review and the later
|
|
340
|
+
* `--apply` step rely on; the session-start `maintenance-due` probe reads the
|
|
341
|
+
* resulting file's presence + age for its `pending-sidecar` signal. The
|
|
342
|
+
* frontmatter is hand rolled (no js-yaml dep) — auto-dream pattern.
|
|
301
343
|
*
|
|
302
344
|
* Atomicity: tmp+rename (mirrors auto-dream.mjs:248-251).
|
|
303
345
|
*
|
|
@@ -366,6 +408,146 @@ export async function writeDialecticPending({
|
|
|
366
408
|
return { path: target, bytes: Buffer.byteLength(content, 'utf8') };
|
|
367
409
|
}
|
|
368
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Archive file name for a consumed sidecar: `<ISO-stamp>-<8 hex>-dialectic-pending.md`.
|
|
413
|
+
* The stamp is `toISOString()` with `:` and `.` replaced by `-` — both are
|
|
414
|
+
* illegal or awkward on several filesystems, and the form still sorts
|
|
415
|
+
* lexicographically. The random suffix separates two consumes landing in the
|
|
416
|
+
* SAME millisecond — without it the second rename would overwrite the first
|
|
417
|
+
* archive silently.
|
|
418
|
+
*
|
|
419
|
+
* @returns {string}
|
|
420
|
+
*/
|
|
421
|
+
function archiveFileName() {
|
|
422
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
423
|
+
return `${stamp}-${randomUUID().slice(0, 8)}-dialectic-pending.md`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Exactly the names {@link archiveFileName} produces. The prune touches nothing
|
|
428
|
+
* else in the directory (#1390 P6): a loose suffix match would still delete an
|
|
429
|
+
* unrelated `operator-dialectic-pending.md`.
|
|
430
|
+
*/
|
|
431
|
+
const ARCHIVE_NAME_RE =
|
|
432
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}-dialectic-pending\.md$/;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Prune `.orchestrator/consumed/` to the newest `DIALECTIC_CONSUMED_RETENTION`
|
|
436
|
+
* archives. Only regular files whose name matches {@link ARCHIVE_NAME_RE} are
|
|
437
|
+
* counted or removed — anything else in the directory is not this module's to
|
|
438
|
+
* delete. Best-effort: every failure degrades to "pruned nothing" — an archive
|
|
439
|
+
* that grew one file too long is never worth failing a consume over.
|
|
440
|
+
*
|
|
441
|
+
* "Newest" is mtime DESC with the filename as tiebreaker: the archive names
|
|
442
|
+
* carry an ISO timestamp, which sorts lexicographically, so the two orderings
|
|
443
|
+
* agree except inside one millisecond.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} dir Absolute path to the consumed archive directory.
|
|
446
|
+
* @returns {Promise<number>} Number of files removed.
|
|
447
|
+
*/
|
|
448
|
+
async function pruneConsumedArchive(dir) {
|
|
449
|
+
let removed = 0;
|
|
450
|
+
try {
|
|
451
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
452
|
+
const files = entries
|
|
453
|
+
.filter((e) => e.isFile() && ARCHIVE_NAME_RE.test(e.name))
|
|
454
|
+
.map((e) => e.name);
|
|
455
|
+
if (files.length <= DIALECTIC_CONSUMED_RETENTION) return 0;
|
|
456
|
+
|
|
457
|
+
const dated = [];
|
|
458
|
+
for (const name of files) {
|
|
459
|
+
let mtimeMs = 0;
|
|
460
|
+
try {
|
|
461
|
+
mtimeMs = (await stat(path.join(dir, name))).mtimeMs;
|
|
462
|
+
} catch {
|
|
463
|
+
mtimeMs = 0; // unreadable → oldest, pruned first
|
|
464
|
+
}
|
|
465
|
+
dated.push({ name, mtimeMs });
|
|
466
|
+
}
|
|
467
|
+
dated.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? 1 : -1));
|
|
468
|
+
|
|
469
|
+
for (const { name } of dated.slice(DIALECTIC_CONSUMED_RETENTION)) {
|
|
470
|
+
try {
|
|
471
|
+
await unlink(path.join(dir, name));
|
|
472
|
+
removed += 1;
|
|
473
|
+
} catch {
|
|
474
|
+
/* best-effort */
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch {
|
|
478
|
+
/* best-effort — a prune failure never fails the consume */
|
|
479
|
+
}
|
|
480
|
+
return removed;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Consume `.orchestrator/dialectic-pending.md` once its proposal has been
|
|
485
|
+
* applied or explicitly discarded: the file is MOVED into
|
|
486
|
+
* `.orchestrator/consumed/<ISO-timestamp>-dialectic-pending.md` rather than
|
|
487
|
+
* unlinked (#1388 P9 — the sidecar was the only copy of what the operator
|
|
488
|
+
* reviewed, and an unlink destroyed it). The maintenance-due probe reads an
|
|
489
|
+
* explicit two-path list (`PENDING_SIDECARS`,
|
|
490
|
+
* `scripts/lib/maintenance-due-banner.mjs`), so an archived file does not
|
|
491
|
+
* re-raise `pending-sidecar` (#1380 stays fixed).
|
|
492
|
+
*
|
|
493
|
+
* The archive is pruned to `DIALECTIC_CONSUMED_RETENTION` entries on every
|
|
494
|
+
* consume — see that constant for the ceiling and its revisit trigger.
|
|
495
|
+
*
|
|
496
|
+
* ENOENT is tolerated (`consumed: false`) — "already gone" is the desired end
|
|
497
|
+
* state. Any other filesystem error returns `{ok: false, error}` rather than
|
|
498
|
+
* throwing, matching `writeDialecticLastRun`.
|
|
499
|
+
*
|
|
500
|
+
* Symlink (#1390 P6, measured 2026-09-19): a pre-existing `.orchestrator/consumed`
|
|
501
|
+
* symlink used to be followed — `mkdir` was a no-op on it, `rename` moved the
|
|
502
|
+
* sidecar into its target, and the retention prune unlinked that target's
|
|
503
|
+
* regular files beyond the newest 10 (a tmp repro deleted 3 of 12). The consume
|
|
504
|
+
* now `lstat`s the directory first and REFUSES a symlink: it returns
|
|
505
|
+
* `{ok: false, error}`, moves nothing and prunes nothing, so the pending
|
|
506
|
+
* sidecar stays where it is for the caller to report. The prune additionally
|
|
507
|
+
* only ever removes names this module writes (see `ARCHIVE_NAME_RE`). Ceiling:
|
|
508
|
+
* the `lstat` → `rename` window is not atomic — a process swapping in a symlink
|
|
509
|
+
* inside it wins; revisit if the consumed directory ever becomes writable by a
|
|
510
|
+
* less-trusted uid than the operator's.
|
|
511
|
+
*
|
|
512
|
+
* @param {object} args
|
|
513
|
+
* @param {string} args.repoRoot
|
|
514
|
+
* @returns {Promise<{ok:boolean, consumed?:boolean, error?:string, path?:string, archivedTo?:string, pruned?:number}>}
|
|
515
|
+
*/
|
|
516
|
+
export async function consumeDialecticPending({ repoRoot } = {}) {
|
|
517
|
+
if (!repoRoot) {
|
|
518
|
+
return { ok: false, error: 'consumeDialecticPending: repoRoot is required' };
|
|
519
|
+
}
|
|
520
|
+
const target = pendingPath(repoRoot);
|
|
521
|
+
if (!existsSync(target)) return { ok: true, consumed: false, path: target };
|
|
522
|
+
|
|
523
|
+
const dir = consumedDirPath(repoRoot);
|
|
524
|
+
const archivedTo = path.join(dir, archiveFileName());
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
let dirStat = null;
|
|
528
|
+
try {
|
|
529
|
+
dirStat = await lstat(dir);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
532
|
+
}
|
|
533
|
+
if (dirStat?.isSymbolicLink()) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
path: target,
|
|
537
|
+
error: `consumeDialecticPending: ${DIALECTIC_CONSUMED_DIR} is a symlink — refusing to archive or prune through it`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
await mkdir(dir, { recursive: true });
|
|
541
|
+
await rename(target, archivedTo);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (err?.code === 'ENOENT') return { ok: true, consumed: false, path: target };
|
|
544
|
+
return { ok: false, error: err.message };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const pruned = await pruneConsumedArchive(dir);
|
|
548
|
+
return { ok: true, consumed: true, path: target, archivedTo, pruned };
|
|
549
|
+
}
|
|
550
|
+
|
|
369
551
|
/**
|
|
370
552
|
* Read `.orchestrator/dialectic-pending.md` if present. Returns the raw file
|
|
371
553
|
* body (including frontmatter) so callers can decide how to parse it.
|
|
@@ -389,3 +571,110 @@ export async function readDialecticPending({ repoRoot } = {}) {
|
|
|
389
571
|
return null;
|
|
390
572
|
}
|
|
391
573
|
}
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// pending body — serializer + drift comparison (#1386, variant b)
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Serialize a deriver diff OBJECT (`result.diff = {user?, agent?}`) into the
|
|
581
|
+
* Markdown body `writeDialecticPending` persists.
|
|
582
|
+
*
|
|
583
|
+
* This lived as a JS snippet in `skills/evolve/references/evolve-dialectic-mode.md`
|
|
584
|
+
* Step 6.4 and is moved here VERBATIM so the dry-run write and the apply-time
|
|
585
|
+
* comparison build the body from the same code — two hand-copied serializers
|
|
586
|
+
* would report drift that is only formatting (#1386).
|
|
587
|
+
*
|
|
588
|
+
* Pure: no I/O, no throw. A non-object argument, or one with neither target as
|
|
589
|
+
* a string, yields `''` — the caller's "nothing to review, write no sidecar"
|
|
590
|
+
* branch.
|
|
591
|
+
*
|
|
592
|
+
* @param {{user?: string, agent?: string}} diff
|
|
593
|
+
* @returns {string} Fenced Markdown body, or `''` when nothing was proposed.
|
|
594
|
+
*/
|
|
595
|
+
export function renderPendingBody(diff) {
|
|
596
|
+
if (!diff || typeof diff !== 'object') return '';
|
|
597
|
+
const FENCE = '`'.repeat(3);
|
|
598
|
+
return ['user', 'agent']
|
|
599
|
+
.filter((t) => typeof diff[t] === 'string')
|
|
600
|
+
.map((t) => [`${FENCE}diff`, `# target: ${t}`, diff[t].trimEnd(), FENCE].join('\n'))
|
|
601
|
+
.join('\n\n');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Strip EXACTLY the leading frontmatter block `writeDialecticPending` wrote.
|
|
606
|
+
*
|
|
607
|
+
* The delimiters sit at fixed positions — line 0 and the next `---` LINE — so
|
|
608
|
+
* this splits on the first two delimiter LINES only. A naive `split('---')`
|
|
609
|
+
* breaks on a body containing a `---` line, and diff bodies do.
|
|
610
|
+
*
|
|
611
|
+
* @param {string} content
|
|
612
|
+
* @returns {string} The body, or the whole input when no frontmatter is present.
|
|
613
|
+
*/
|
|
614
|
+
function stripPendingFrontmatter(content) {
|
|
615
|
+
const lines = content.split('\n');
|
|
616
|
+
if (lines[0] !== '---') return content;
|
|
617
|
+
const end = lines.indexOf('---', 1);
|
|
618
|
+
if (end === -1) return content; // unterminated → nothing to strip
|
|
619
|
+
return lines.slice(end + 1).join('\n');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Whitespace normalisation for the drift comparison: exactly ONE trailing
|
|
624
|
+
* newline is removed from each side.
|
|
625
|
+
*
|
|
626
|
+
* `writeDialecticPending` appends a newline when the body lacks one, so the
|
|
627
|
+
* persisted body is the rendered body plus `\n` — without this normalisation a
|
|
628
|
+
* clean round-trip would always report drift. Nothing else is normalised (no
|
|
629
|
+
* trim, no line-ending or indentation folding): anything more would hide real
|
|
630
|
+
* drift in a diff body, where trailing whitespace is content.
|
|
631
|
+
*/
|
|
632
|
+
function normalizeTrailingNewline(text) {
|
|
633
|
+
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Compare a freshly derived pending body against the sidecar the operator
|
|
638
|
+
* reviewed (#1386, variant b).
|
|
639
|
+
*
|
|
640
|
+
* `/evolve dialectic --apply` re-derives from the model, so what gets applied
|
|
641
|
+
* is not necessarily what was approved in `.orchestrator/dialectic-pending.md`.
|
|
642
|
+
* This makes that divergence VISIBLE at apply time; it does not make apply
|
|
643
|
+
* deterministic.
|
|
644
|
+
*
|
|
645
|
+
* Never throws — every failure degrades to a result object, matching this
|
|
646
|
+
* module's other readers.
|
|
647
|
+
*
|
|
648
|
+
* @param {object} args
|
|
649
|
+
* @param {string} args.repoRoot
|
|
650
|
+
* @param {string} args.body Fresh body, typically from `renderPendingBody()`.
|
|
651
|
+
* @returns {Promise<{ok:boolean, drifted:boolean, sidecarAbsent:boolean, sidecarBody?:string, freshBody?:string, error?:string}>}
|
|
652
|
+
*/
|
|
653
|
+
export async function comparePendingBody({ repoRoot, body } = {}) {
|
|
654
|
+
if (!repoRoot) {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
drifted: false,
|
|
658
|
+
sidecarAbsent: false,
|
|
659
|
+
error: 'comparePendingBody: repoRoot is required',
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (typeof body !== 'string') {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
drifted: false,
|
|
666
|
+
sidecarAbsent: false,
|
|
667
|
+
error: 'comparePendingBody: body must be a string',
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const raw = await readDialecticPending({ repoRoot });
|
|
672
|
+
// Applying without a sidecar is legitimate (the operator may never have run
|
|
673
|
+
// the dry-run) — that is NOT drift.
|
|
674
|
+
if (raw === null) return { ok: true, drifted: false, sidecarAbsent: true };
|
|
675
|
+
|
|
676
|
+
const sidecarBody = stripPendingFrontmatter(raw);
|
|
677
|
+
const drifted = normalizeTrailingNewline(sidecarBody) !== normalizeTrailingNewline(body);
|
|
678
|
+
|
|
679
|
+
return { ok: true, drifted, sidecarAbsent: false, sidecarBody, freshBody: body };
|
|
680
|
+
}
|
|
@@ -58,7 +58,7 @@ function parseNumeric(raw) {
|
|
|
58
58
|
* silently to bounds. Unknown flags are ignored. `--dry-run` is a boolean flag.
|
|
59
59
|
*
|
|
60
60
|
* @param {string[]} argv — argument tokens (e.g. ['--max-sessions=3', '--dry-run'])
|
|
61
|
-
* @returns {{maxSessions: number, maxHours: number, confidenceThreshold: number, dryRun: boolean}}
|
|
61
|
+
* @returns {{maxSessions: number, maxHours: number, confidenceThreshold: number, maxTokens: number, dryRun: boolean}}
|
|
62
62
|
*/
|
|
63
63
|
export function parseFlags(argv) {
|
|
64
64
|
const tokens = Array.isArray(argv) ? argv : [];
|
|
@@ -66,6 +66,7 @@ export function parseFlags(argv) {
|
|
|
66
66
|
let rawSessions = null;
|
|
67
67
|
let rawHours = null;
|
|
68
68
|
let rawConfidence = null;
|
|
69
|
+
let rawTokens = null;
|
|
69
70
|
let dryRun = false;
|
|
70
71
|
|
|
71
72
|
for (const tok of tokens) {
|
|
@@ -81,6 +82,7 @@ export function parseFlags(argv) {
|
|
|
81
82
|
if (key === '--max-sessions') rawSessions = parseNumeric(val);
|
|
82
83
|
else if (key === '--max-hours') rawHours = parseNumeric(val);
|
|
83
84
|
else if (key === '--confidence-threshold') rawConfidence = parseNumeric(val);
|
|
85
|
+
else if (key === '--max-tokens') rawTokens = parseNumeric(val);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
return {
|
|
@@ -99,6 +101,15 @@ export function parseFlags(argv) {
|
|
|
99
101
|
max: FLAG_BOUNDS.confidenceThreshold.max,
|
|
100
102
|
fallback: FLAG_BOUNDS.confidenceThreshold.default,
|
|
101
103
|
}),
|
|
104
|
+
// Integer token count — the TOKEN_BUDGET_EXCEEDED switch compares it against
|
|
105
|
+
// a summed `usage.output_tokens`, which is never fractional. `0` disables the
|
|
106
|
+
// switch (`kill-switches.mjs` guards on `maxTokens > 0`), so the lower bound
|
|
107
|
+
// stays 0 rather than 1.
|
|
108
|
+
maxTokens: Math.floor(clampNumber(rawTokens, {
|
|
109
|
+
min: FLAG_BOUNDS.maxTokens.min,
|
|
110
|
+
max: FLAG_BOUNDS.maxTokens.max,
|
|
111
|
+
fallback: FLAG_BOUNDS.maxTokens.default,
|
|
112
|
+
})),
|
|
102
113
|
dryRun,
|
|
103
114
|
};
|
|
104
115
|
}
|
|
@@ -110,7 +110,8 @@ export function preIterationKillSwitch(args) {
|
|
|
110
110
|
* @param {object | null | undefined} sessionResult
|
|
111
111
|
* @param {object} opts
|
|
112
112
|
* @param {number} opts.carryoverThreshold
|
|
113
|
-
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler
|
|
113
|
+
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler fallback marker
|
|
114
|
+
* @param {string} [opts.sessionLockPath] — STALL_TIMEOUT sampler primary marker
|
|
114
115
|
* @param {number} [opts.stallTimeoutSeconds] — STALL_TIMEOUT threshold (default 600)
|
|
115
116
|
* @param {() => number} [opts.nowMs] — wall-clock supplier (DI seam for tests)
|
|
116
117
|
* @returns {{kill: string, detail: string} | null}
|
|
@@ -120,12 +121,14 @@ export function postSessionKillSwitch(sessionResult, opts) {
|
|
|
120
121
|
const { carryoverThreshold } = opts;
|
|
121
122
|
|
|
122
123
|
// STALL_TIMEOUT (ADR-364 §3, issue #371) — one-strike v1.
|
|
123
|
-
// Sampler
|
|
124
|
-
//
|
|
124
|
+
// Sampler prefers the session.lock heartbeat and falls back to autopilot.jsonl
|
|
125
|
+
// mtime; missing marker → stallSeconds=0 → no kill (documented contract:
|
|
126
|
+
// a missing file is NOT a kill condition).
|
|
125
127
|
// Events route to autopilot.jsonl, NOT failures.jsonl (ADR-364 cross-connections rule 4).
|
|
126
128
|
const stallTimeoutSeconds = opts.stallTimeoutSeconds ?? 600;
|
|
127
129
|
const stallSample = sampleProgress({
|
|
128
130
|
autopilotJsonlPath: opts.autopilotJsonlPath,
|
|
131
|
+
sessionLockPath: opts.sessionLockPath,
|
|
129
132
|
stallTimeoutSeconds,
|
|
130
133
|
nowMs: opts.nowMs,
|
|
131
134
|
});
|
|
@@ -59,7 +59,11 @@ function clampNumber(value, { min, max, fallback }) {
|
|
|
59
59
|
* @param {string} [opts.jsonlPath] @param {string} [opts.runId] @param {string} [opts.branch]
|
|
60
60
|
* @param {string} [opts.hostJsonPath] @param {number} [opts.peerAbortThreshold]
|
|
61
61
|
* @param {number} [opts.carryoverThreshold] @param {number} [opts.maxTokens]
|
|
62
|
-
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler
|
|
62
|
+
* @param {string} [opts.autopilotJsonlPath] — STALL_TIMEOUT sampler fallback marker (defaults to jsonlPath)
|
|
63
|
+
* @param {string} [opts.sessionLockPath] — STALL_TIMEOUT sampler PRIMARY marker (`session.lock`
|
|
64
|
+
* `last_heartbeat`). Deliberately undefaulted: the default would be cwd-relative, which in a
|
|
65
|
+
* test run resolves to the LIVE repo lock and would silently decide the sampler's branch. The
|
|
66
|
+
* CLI driver (`scripts/autopilot.mjs`) supplies it; omitting it keeps the legacy mtime marker.
|
|
63
67
|
* @param {number} [opts.stallTimeoutSeconds] — STALL_TIMEOUT threshold seconds (default 600)
|
|
64
68
|
* @param {string} [opts.worktreePath] @param {string} [opts.parentRunId]
|
|
65
69
|
* @param {number} [opts.blockedByIssue] — Phase D (#341) forward-compat: issue number this loop is waiting on (OPEN-4 commit-deps); callers may omit, defaults to null
|
|
@@ -125,6 +129,14 @@ export async function runLoop(opts = {}) {
|
|
|
125
129
|
max_sessions: maxSessions,
|
|
126
130
|
max_hours: maxHours,
|
|
127
131
|
confidence_threshold: confidenceThreshold,
|
|
132
|
+
// Persisted for the same reason as the three above (HR-105): a switch whose
|
|
133
|
+
// input is never recorded cannot be falsified afterwards. `0` = disabled.
|
|
134
|
+
// CEILING: reaching a NON-zero value here does not make TOKEN_BUDGET_EXCEEDED
|
|
135
|
+
// live under the headless driver — `readTailSession()` in scripts/autopilot.mjs
|
|
136
|
+
// returns no `usage`, and sessions.jsonl carries no token field to build one
|
|
137
|
+
// from, so `total_tokens_used` stays 0. Revisit when a session record gains
|
|
138
|
+
// token counts.
|
|
139
|
+
max_tokens: opts.maxTokens ?? 0,
|
|
128
140
|
iterations_completed: 0,
|
|
129
141
|
kill_switch: null,
|
|
130
142
|
kill_switch_detail: null,
|
|
@@ -275,6 +287,7 @@ export async function runLoop(opts = {}) {
|
|
|
275
287
|
const postCheck = postSessionKillSwitch(sessionResult, {
|
|
276
288
|
carryoverThreshold,
|
|
277
289
|
autopilotJsonlPath: opts.autopilotJsonlPath ?? jsonlPath,
|
|
290
|
+
sessionLockPath: opts.sessionLockPath,
|
|
278
291
|
stallTimeoutSeconds: opts.stallTimeoutSeconds ?? 600,
|
|
279
292
|
nowMs: opts.nowMs,
|
|
280
293
|
});
|
|
@@ -2,21 +2,39 @@
|
|
|
2
2
|
* autopilot/stall-sampler.mjs — Stateless progress sampler for STALL_TIMEOUT
|
|
3
3
|
* kill-switch (ADR-364, issue #371).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* autopilot loop writes one record per session atomically (tmp+rename), so the
|
|
7
|
-
* file mtime advances exactly when the loop makes progress. No false-positive
|
|
8
|
-
* churn from in-place writes, no need to parse JSON, microsecond precision.
|
|
5
|
+
* PROGRESS MARKER PRECEDENCE (HR-102: a better signal REPLACES a worse one).
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* 1. `session.lock` `last_heartbeat` (schema v2) — advances DURING a run:
|
|
8
|
+
* `hooks/on-stop.mjs` refreshes it at every turn-end and
|
|
9
|
+
* `hooks/post-tool-batch-wave-signal.mjs` at every tool batch.
|
|
10
|
+
* 2. `autopilot.jsonl` mtime — the legacy marker, used only when no lock path
|
|
11
|
+
* is supplied or the lock is absent/corrupt/timestamp-less.
|
|
12
|
+
*
|
|
13
|
+
* The header of this module used to claim `autopilot.jsonl` carries "one record
|
|
14
|
+
* per session". It does not: `telemetry.mjs` writes ONE record per /autopilot
|
|
15
|
+
* INVOCATION, and `loop.mjs` calls it exactly twice — once for the dry-run
|
|
16
|
+
* preview and once AFTER the `for(;;)` loop. So during a real run the mtime the
|
|
17
|
+
* sampler read belonged to the PREVIOUS autopilot run, typically hours or days
|
|
18
|
+
* old, and the post-session check fired STALL_TIMEOUT after iteration 1 every
|
|
19
|
+
* time. The append-once contract is correct and stays; the marker was wrong.
|
|
20
|
+
*
|
|
21
|
+
* CEILING (BV-004). The heartbeat only advances while some live session owns the
|
|
22
|
+
* lock: `updateHeartbeat()` refuses on a session_id mismatch, and a child
|
|
23
|
+
* `claude -p` spawned while a parent session already holds the lock never
|
|
24
|
+
* acquires one (`hooks/_lib/lock-bootstrap.mjs` bails on `reason: 'active'`).
|
|
25
|
+
* In that one invocation shape — the headless driver started FROM a chat
|
|
26
|
+
* session rather than from a bare shell — the heartbeat freezes and the check
|
|
27
|
+
* degrades to today's behaviour. Revisit if STALL_TIMEOUT is ever observed
|
|
28
|
+
* firing on a run whose child session did real work.
|
|
29
|
+
*
|
|
30
|
+
* Stateless — at most two sync reads per invocation, no side effects, no async.
|
|
13
31
|
*
|
|
14
32
|
* Exports:
|
|
15
33
|
* SAMPLE_CADENCE_MS — shared constant (30_000) defining the "fresh" window
|
|
16
34
|
* sampleProgress(opts) — returns progress descriptor; never throws
|
|
17
35
|
*/
|
|
18
36
|
|
|
19
|
-
import { statSync } from 'node:fs';
|
|
37
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
20
38
|
|
|
21
39
|
// ---------------------------------------------------------------------------
|
|
22
40
|
// Constants
|
|
@@ -32,9 +50,40 @@ export const SAMPLE_CADENCE_MS = 30_000;
|
|
|
32
50
|
const DEFAULT_AUTOPILOT_JSONL = '.orchestrator/metrics/autopilot.jsonl';
|
|
33
51
|
const DEFAULT_STALL_TIMEOUT_SECONDS = 600;
|
|
34
52
|
const MARKER_SUCCESS = 'autopilot.jsonl:mtime';
|
|
53
|
+
const MARKER_HEARTBEAT = 'session.lock:last_heartbeat';
|
|
35
54
|
const MARKER_MISSING = 'missing';
|
|
36
55
|
const MARKER_INVALID = 'invalid';
|
|
37
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Read the live heartbeat timestamp (ms) out of `session.lock`.
|
|
59
|
+
*
|
|
60
|
+
* Returns `null` — meaning "fall back to the mtime marker" — for every failure
|
|
61
|
+
* mode: no path supplied, file absent, unreadable, not JSON, or carrying no
|
|
62
|
+
* parsable timestamp. A missing measurement is NOT a zero here; conflating the
|
|
63
|
+
* two would report a stall of `now` seconds the moment the lock is unreadable.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} [sessionLockPath]
|
|
66
|
+
* @returns {number | null}
|
|
67
|
+
*/
|
|
68
|
+
function readHeartbeatMs(sessionLockPath) {
|
|
69
|
+
if (typeof sessionLockPath !== 'string' || sessionLockPath.length === 0) return null;
|
|
70
|
+
let lock;
|
|
71
|
+
try {
|
|
72
|
+
lock = JSON.parse(readFileSync(sessionLockPath, 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (typeof lock !== 'object' || lock === null) return null;
|
|
77
|
+
// Same precedence `session-lock.mjs` uses on read: a v1 lock has no
|
|
78
|
+
// `last_heartbeat`, and its `started_at` is the best available stand-in.
|
|
79
|
+
const raw = typeof lock.last_heartbeat === 'string' && lock.last_heartbeat.length > 0
|
|
80
|
+
? lock.last_heartbeat
|
|
81
|
+
: lock.started_at;
|
|
82
|
+
if (typeof raw !== 'string' || raw.length === 0) return null;
|
|
83
|
+
const ms = Date.parse(raw);
|
|
84
|
+
return Number.isFinite(ms) ? ms : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
38
87
|
// ---------------------------------------------------------------------------
|
|
39
88
|
// Progress sampler
|
|
40
89
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +94,10 @@ const MARKER_INVALID = 'invalid';
|
|
|
45
94
|
* @param {object} [opts]
|
|
46
95
|
* @param {string} [opts.autopilotJsonlPath] — path to autopilot.jsonl (default
|
|
47
96
|
* '.orchestrator/metrics/autopilot.jsonl').
|
|
97
|
+
* @param {string} [opts.sessionLockPath] — path to `session.lock`. When supplied
|
|
98
|
+
* and readable, its `last_heartbeat` (falling back to `started_at`, the schema
|
|
99
|
+
* v1 normalisation `session-lock.mjs` applies on read) REPLACES the mtime
|
|
100
|
+
* marker. Omitted → mtime only, which is what every pre-existing caller gets.
|
|
48
101
|
* @param {number} [opts.stallTimeoutSeconds] — kill-switch threshold (default
|
|
49
102
|
* 600). NOT range-clamped here; caller is responsible for bounds.
|
|
50
103
|
* @param {() => number} [opts.nowMs] — wall-clock supplier (default Date.now).
|
|
@@ -66,22 +119,26 @@ export function sampleProgress(opts = {}) {
|
|
|
66
119
|
void _stallTimeoutSeconds;
|
|
67
120
|
const nowMs = typeof opts.nowMs === 'function' ? opts.nowMs : Date.now;
|
|
68
121
|
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
let markerMs = readHeartbeatMs(opts.sessionLockPath);
|
|
123
|
+
let markerName = MARKER_HEARTBEAT;
|
|
124
|
+
|
|
125
|
+
if (markerMs === null) {
|
|
126
|
+
markerName = MARKER_SUCCESS;
|
|
127
|
+
try {
|
|
128
|
+
markerMs = statSync(autopilotJsonlPath).mtimeMs;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const marker = err && err.code === 'ENOENT' ? MARKER_MISSING : MARKER_INVALID;
|
|
131
|
+
return {
|
|
132
|
+
progressed: false,
|
|
133
|
+
lastMarker: null,
|
|
134
|
+
stallSeconds: 0,
|
|
135
|
+
marker,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
81
138
|
}
|
|
82
139
|
|
|
83
140
|
const now = nowMs();
|
|
84
|
-
const deltaMs = now -
|
|
141
|
+
const deltaMs = now - markerMs;
|
|
85
142
|
// Clamp negative deltas (clock skew, file mtime in the future) to 0.
|
|
86
143
|
const stallSeconds = Math.max(0, Math.round(deltaMs / 1000));
|
|
87
144
|
// Fresh if within the sample cadence window (also covers the clock-skew
|
|
@@ -90,8 +147,8 @@ export function sampleProgress(opts = {}) {
|
|
|
90
147
|
|
|
91
148
|
return {
|
|
92
149
|
progressed,
|
|
93
|
-
lastMarker:
|
|
150
|
+
lastMarker: markerMs,
|
|
94
151
|
stallSeconds,
|
|
95
|
-
marker:
|
|
152
|
+
marker: markerName,
|
|
96
153
|
};
|
|
97
154
|
}
|