session-orchestrator 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +9 -30
- package/CHANGELOG.md +206 -0
- package/README.md +13 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +22 -7
- package/docs/ci-setup.md +57 -4
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +12 -10
- package/docs/events-schema.md +65 -15
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +84 -17
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +154 -12
- package/docs/session-config-template.md +41 -3
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +87 -6
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +101 -11
- package/hooks/on-stop.mjs +170 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +202 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
- package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +792 -74
- package/hooks/subagent-telemetry.mjs +111 -38
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +3 -3
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +7 -2
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +6 -2
- package/rules/opt-in-stack/backend-data.md +10 -2
- package/rules/opt-in-stack/backend.md +10 -4
- package/rules/opt-in-stack/frontend.md +7 -2
- package/rules/opt-in-stack/security-web.md +10 -4
- package/rules/opt-in-stack/swift.md +6 -2
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +70 -53
- package/scripts/backfill-abandoned-sessions.mjs +118 -16
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +371 -31
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +18 -2
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +19 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +163 -0
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +85 -10
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +545 -7
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +40 -5
- package/scripts/lib/gates/gate-full.mjs +35 -5
- package/scripts/lib/gates/gate-helpers.mjs +336 -23
- package/scripts/lib/gates/gate-incremental.mjs +49 -23
- package/scripts/lib/gates/gate-per-file.mjs +42 -8
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +208 -8
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +316 -0
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +950 -0
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +152 -33
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +141 -74
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +56 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +118 -4
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +210 -84
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +306 -0
- package/scripts/release.mjs +40 -8
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +357 -197
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +16 -4
- package/scripts/validate-plugin.mjs +19 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +81 -47
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +47 -18
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +3 -3
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +32 -60
- package/skills/session-plan/SKILL.md +17 -8
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +9 -5
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
package/docs/rule-authoring.md
CHANGED
|
@@ -23,6 +23,7 @@ All keys are optional. Unknown keys are **ignored without error** — adding a n
|
|
|
23
23
|
| Field | Type | Required | Meaning | Example |
|
|
24
24
|
|-------|------|----------|---------|---------|
|
|
25
25
|
| `globs` | `string[]` (block or flow style) | no | Glob patterns relative to repo root. Rule loads only when a `scopePath` matches at least one. Absent = always-on; `[]` = matches nothing (disabled). | `globs:` then ` - src/**/*.tsx` |
|
|
26
|
+
| `paths` | `string[]` (block or flow style) | no — **required alongside `globs` with the identical list in a path-scoped VENDORED rule** (`rules/` library, #1449) | Alias for `globs` in `rule-loader.mjs` (#795; `globs` wins when both are present). It is the ONLY key Claude Code's native loader reads, and only from a frontmatter that opens on line 1 — a rule without it loads always-on there. A vendored rule therefore carries both keys: `paths` for Claude Code, `globs` for `rule-loader.mjs`, which reads it first. A repo's own consolidated `.claude/rules/` files are `paths`-only (see [Consolidated rules](#consolidated-rules-n-provenance-pairs-in-one-file-the-merge-contract) point 3). | `paths:` then ` - src/**/*.tsx` |
|
|
26
27
|
| `description` | `string` | no | Human-readable summary of what the rule covers. Surfaced on the rule entry; used by FA2/FA4 tooling and authors. | `description: Tailwind + a11y conventions` |
|
|
27
28
|
| `mode` | `string` (`housekeeping` \| `feature` \| `deep`) | no | Session-mode gate. Rule loads only in the named session mode. Absent = passes every mode. | `mode: deep` |
|
|
28
29
|
| `host-class` | `string` | no | Host-class gate. Matched against `host_class` in `.orchestrator/host.json`. Rule loads only on matching hosts. Absent = passes every host. | `host-class: macos-arm64-m4pro` |
|
|
@@ -93,27 +94,42 @@ Since issue #743, `rules/opt-in-stack/{backend,backend-data,frontend,swift,secur
|
|
|
93
94
|
|
|
94
95
|
### Provenance header + frontmatter coexistence (issue #722)
|
|
95
96
|
|
|
96
|
-
Vendored rule sources carry a mandatory single-line provenance header
|
|
97
|
+
Vendored rule sources carry a mandatory single-line provenance header — `rules-sync.mjs` uses that header (`PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin ...'`) to tell "plugin-owned, safe to overwrite on re-sync" apart from "local override, preserve". Where it goes depends on whether the rule has frontmatter (#1449):
|
|
98
|
+
|
|
99
|
+
- **Rule with frontmatter** — the frontmatter opens on **line 1** (the file's first bytes are `---`), and the header is the first line directly after the closing `---`. Claude Code reads a rule's frontmatter only when it opens on line 1, so a header above the opener makes the whole block body text and the rule loads always-on in every consumer.
|
|
100
|
+
- **Rule without frontmatter** — the header stays on line 1.
|
|
101
|
+
|
|
102
|
+
The shape for a path-scoped vendored rule:
|
|
97
103
|
|
|
98
104
|
```markdown
|
|
99
|
-
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
|
|
100
105
|
---
|
|
101
106
|
globs:
|
|
102
107
|
- src/**/*.tsx
|
|
108
|
+
paths:
|
|
109
|
+
- src/**/*.tsx
|
|
110
|
+
tier: wave-only
|
|
103
111
|
---
|
|
112
|
+
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/foo.md) -->
|
|
104
113
|
# Foo Rules (Path-scoped)
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
A path-scoped vendored rule declares its patterns under **both** `globs:` and `paths:`, with identical lists. `paths:` is the only key Claude Code reads from a rule; `globs:` is what `rule-loader.mjs` reads first (#742/#795 — when both are present `globs:` wins silently), so one key alone, or two different lists, would scope the rule differently per loader.
|
|
117
|
+
|
|
118
|
+
`rule-loader.mjs`'s frontmatter parser (`parseGlobsFrontmatter`) counts a frontmatter block only when its opening `---` is line 1 — the same rule Claude Code applies. Until #1449 it skipped a leading run of blank lines and single-line HTML comments, which classified a header-first rule as path-scoped while Claude Code loaded it always-on. Now a block with anything before its opener (a comment, a blank line, any text) yields no globs and no meta: the rule is always-on, and its entry carries `defect: 'frontmatter-not-at-top'` so callers such as the instruction-budget guard can report the file instead of loading it always-on without a trace. The header after the closing `---` is ordinary body text and has no effect on loading.
|
|
119
|
+
|
|
120
|
+
Consumer copies written before #1449 (header on line 1, frontmatter after it) still count as plugin-owned: `isPluginOwnedContent()` (`validate-vendored-rules.mjs`, re-exported by `rules-sync.mjs`) accepts the header at either position, so the next `/bootstrap --sync-rules` rewrites such a copy into the current format rather than preserving it as a local override.
|
|
121
|
+
|
|
122
|
+
This convention binds only files that are actual sync SOURCES — the entries `syncRules()` resolves from `rules/_index.md` (`join(pluginRoot, 'rules', '_index.md')`, the manifest it reads before writing anything into a consumer's `.claude/rules/`). `rules/README.md` and `rules/_index.md` itself are never entries in that manifest, so they are never sync targets and carry no provenance header by construction — not an oversight to fix.
|
|
108
123
|
|
|
109
124
|
### Vendoring validation (issue #722)
|
|
110
125
|
|
|
111
|
-
Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`).
|
|
126
|
+
Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/`, it runs a pre-write gate via `validateRuleContent()` (`scripts/lib/validate-vendored-rules.mjs`). Six probes:
|
|
112
127
|
|
|
113
128
|
| Probe | Severity | Rejects / flags |
|
|
114
129
|
|-------|----------|------------------|
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
130
|
+
| `frontmatter-not-at-top` | error | A `---` frontmatter block preceded by anything — the provenance comment, a blank line, any text (#1449). Claude Code reads frontmatter only when it opens on line 1, so such a rule loads ALWAYS-ON in every consumer repo, whatever its scope keys say. Fix: move the block to line 1 and put the provenance header on the first line after the closing `---`. |
|
|
131
|
+
| `globs-paths-mismatch` | error | A library source whose line-1 frontmatter carries exactly one of `globs:` / `paths:`, or both with different lists (compared order-insensitively) (#1449). A path-scoped vendored rule carries both with the identical list: `paths:` is the only key Claude Code reads, `globs:` is what `rule-loader.mjs` reads first (#742/#795). Replaces the pre-#1449 `paths-frontmatter` probe, which rejected `paths:` in a library source outright. Its population is what `syncRules()` reads, i.e. `<pluginRoot>/rules/**` as listed by `rules/_index.md`; the consolidated files under `.claude/rules/` are `paths:`-only by design (see § Consolidated rules point 3) and are never its input. |
|
|
132
|
+
| `provenance-header` | error (opt-in via `requireProvenance`, default `true` in `syncRules()`) | Missing provenance header on a library source — without it, `rules-sync.mjs` mis-detects the file as a local override on the next re-sync and can never update it again. Two positions are accepted: line 1 (a rule without frontmatter) or the first non-blank line after the closing `---` of a frontmatter that opens on line 1. A header on line 1 ABOVE a frontmatter (the pre-#1449 format) still satisfies this probe — `frontmatter-not-at-top` reports that shape, once — and keeps an old consumer copy plugin-owned, so the next sync upgrades it to the current format instead of freezing it as a local override. |
|
|
117
133
|
| `placeholder` | error | Unfilled placeholder tokens: `{{PROJECT_NAME}}`-style handlebars, a `## TODO: Customize` heading, or a `<!-- TODO:` comment — skeleton content, not a finished rule. |
|
|
118
134
|
| `zero-match-globs` | warn | A `globs:` pattern matching 0 files in the target repo's tracked file list (`git ls-files`, falling back to a directory walk). Legitimately possible in a freshly-scaffolded repo. |
|
|
119
135
|
| `foreign-glob` | warn | A glob segment carrying a PascalCase, product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope. |
|
|
@@ -285,6 +301,44 @@ them silently loses a learning or regenerates it:
|
|
|
285
301
|
TTL past what its type registry granted it.) State the rule in the file
|
|
286
302
|
itself, so the next editor does not "fix" it upward.
|
|
287
303
|
|
|
304
|
+
Since #1387 an expired file also stops DEDUPING: the `/reconcile` provenance
|
|
305
|
+
reader (`scripts/lib/reconcile/backlog.mjs`
|
|
306
|
+
`defaultReadMaterializedProvenance`) skips an expired file whole —
|
|
307
|
+
frontmatter key and body bullets alike — through the same `isRuleExpired`
|
|
308
|
+
predicate the loader's injection gate uses, fail-open on an absent or
|
|
309
|
+
unparseable date. Two KNOWN LIMITS, both measured 2026-09-18 and both left
|
|
310
|
+
unsolved on purpose:
|
|
311
|
+
|
|
312
|
+
- **Only the on-disk half.** `partitionMaterialized` marks a learning
|
|
313
|
+
materialized on `sidecarTerminal || onDisk`; this covers `onDisk` only. On
|
|
314
|
+
this repo 79 of 92 provenance keys are ALSO terminal in
|
|
315
|
+
`.orchestrator/runtime/reconcile-candidates.jsonl`, so only 13 keys
|
|
316
|
+
actually become re-proposable. The sidecar half stays untouched because it
|
|
317
|
+
carries operator DECLINES — expiring a rule must not re-ask a question the
|
|
318
|
+
operator already answered.
|
|
319
|
+
- **A file the sweep cannot split re-proposes forever.** A file the sweep
|
|
320
|
+
skips (`no-1to1-mapping`, `no-provenance-block`, `unreadable`,
|
|
321
|
+
`no-counter-sentence`) that then
|
|
322
|
+
expires legitimately becomes re-proposable on EVERY run, with no
|
|
323
|
+
mechanical exit: the sweep will not rewrite it, so nothing retires its
|
|
324
|
+
markers. The live tree is currently clear of this —
|
|
325
|
+
`node scripts/sweep-expired-rules.mjs --dry-run --json` reported
|
|
326
|
+
`skipped: []` over 7 generated files (2026-09-18) — but the class has no
|
|
327
|
+
guard, so a future unsplittable file lands in it silently.
|
|
328
|
+
|
|
329
|
+
The expiry sweep (`node scripts/sweep-expired-rules.mjs`) maintains the
|
|
330
|
+
earliest-date rule in
|
|
331
|
+
ONE direction: a header sitting EARLIER than the earliest absorbed date is
|
|
332
|
+
RAISED to it — the raise is its own rewrite trigger (`action: "rewrite"`,
|
|
333
|
+
`reason: "header-raise"`, visible in `--json` before `--apply`), and it
|
|
334
|
+
touches nothing but the frontmatter line and the sentence above. Without it
|
|
335
|
+
the file passes its header date with nothing expired, `rule-loader.mjs`
|
|
336
|
+
stops injecting it, and the surviving provenance markers keep `/reconcile`
|
|
337
|
+
treating its learnings as materialized — the substance goes dark and is
|
|
338
|
+
never re-proposed. The sweep never LOWERS a header: a header that outlives
|
|
339
|
+
its content is reported as an `advisory` only, because lowering on every run
|
|
340
|
+
would cut a healthy entry's TTL short.
|
|
341
|
+
|
|
288
342
|
3. **`paths:` is the canonical scope key, and it carries the UNION of the
|
|
289
343
|
parts.** The merged file loads for any path any of its parts covered, so its
|
|
290
344
|
list is the union of theirs. `paths:` is the key Claude Code's OWN native
|
|
@@ -295,13 +349,18 @@ them silently loses a learning or regenerates it:
|
|
|
295
349
|
(`parseGlobsFrontmatter`, issue #795) and `instruction-budget-guard.mjs`
|
|
296
350
|
(`:960`) goes through that same parser, so a `paths:`-only file is
|
|
297
351
|
glob-scoped for every reader in this repo and still counts under
|
|
298
|
-
`bySurface.pathScoped`. Measured 2026-09-
|
|
352
|
+
`bySurface.pathScoped`. Measured 2026-09-18 over this repo's own
|
|
353
|
+
`.claude/rules/` (25 files; NOT the vendored `rules/` library, and not the
|
|
354
|
+
private baseline's `rules/` population `docs/baseline.md` counts separately)
|
|
299
355
|
(`for f in .claude/rules/*.md; do awk '/^---$/{n++;next} n==1 && /^(paths|globs):/{print FILENAME": "$1}' "$f"; done`):
|
|
300
|
-
10 path-scoped rule files
|
|
301
|
-
both
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
356
|
+
10 path-scoped rule files, ALL 10 `paths:`-only — 0 carry `globs:`, 0 carry
|
|
357
|
+
both (`grep -rn '^globs:' .claude/rules/` → no match, exit 1). Until this
|
|
358
|
+
session `cli-design.md` carried both; the duplicate `globs:` was removed
|
|
359
|
+
here. Only rules VENDORED OUT through the `rules/` fleet library carry
|
|
360
|
+
both keys, with identical lists — `paths:` for Claude Code, `globs:` for
|
|
361
|
+
`rule-loader.mjs` (issues #742, #1449) — and `validate-vendored-rules.mjs`'s
|
|
362
|
+
`globs-paths-mismatch` probe enforces it; that probe judges `rules/`
|
|
363
|
+
sources only and never sees a consolidated file under `.claude/rules/`. Carrying both keys is
|
|
305
364
|
allowed, but NEVER with different values: `globs:` wins SILENTLY when both
|
|
306
365
|
are present (#795), and `check-rules.mjs` fails a divergent pair outright.
|
|
307
366
|
|
|
@@ -354,7 +413,7 @@ under four contracts, all of them consequences of the four merge rules above:
|
|
|
354
413
|
learning look unmaterialized and `/reconcile` would regenerate it. **A pair
|
|
355
414
|
is never deleted while its file survives.**
|
|
356
415
|
|
|
357
|
-
2. **
|
|
416
|
+
2. **Three fail-open cases, all reported rather than guessed.** An entry carries
|
|
358
417
|
no date of its own; its date is recoverable only via `learning-id` →
|
|
359
418
|
`.orchestrator/metrics/learnings.jsonl` `expires_at` (measured 2026-09-17 @
|
|
360
419
|
`9e8146b4`: 87 of 92 unique ids resolve, 5 do not). An **unresolvable id**
|
|
@@ -364,8 +423,16 @@ under four contracts, all of them consequences of the four merge rules above:
|
|
|
364
423
|
plus a `skipped` record with reason `no-1to1-mapping`. Measured the same day,
|
|
365
424
|
the 1:1 mapping held in 3 of the 7 live files
|
|
366
425
|
(`measurement-discipline` 12/12, `process-contracts` 6/6,
|
|
367
|
-
`toolchain-and-build` 10/10) and failed in the other 4.
|
|
368
|
-
|
|
426
|
+
`toolchain-and-build` 10/10) and failed in the other 4. The third case is an
|
|
427
|
+
**unrecognised counter sentence** (GH#70, 2026-09-20): when the body sentence
|
|
428
|
+
restating `expires-at` is in no spelling `COUNTER_FORMS` knows, the file gets
|
|
429
|
+
`action: 'keep'` plus a `skipped` record with reason `no-counter-sentence`
|
|
430
|
+
and NO write of any kind — not a rewrite, not a raise, and not a delete
|
|
431
|
+
either, because a shape the sweep cannot read is the last shape whose most
|
|
432
|
+
destructive action should run. Moving only the header would ship a
|
|
433
|
+
frontmatter date contradicting a body sentence that forbids exactly that
|
|
434
|
+
correction. Malformed `learnings.jsonl` lines are COUNTED
|
|
435
|
+
(`malformedLines`), never skipped.
|
|
369
436
|
|
|
370
437
|
3. **Deleting a whole file obeys the stamp-before-delete rule above.** A file is
|
|
371
438
|
deleted only when it has zero kept AND zero unresolved pairs, and EVERY pair
|
|
@@ -416,7 +483,7 @@ Core security principles that apply to ALL code.
|
|
|
416
483
|
|
|
417
484
|
### (b) Glob-scoped rule (block-style `globs:`)
|
|
418
485
|
|
|
419
|
-
Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md
|
|
486
|
+
Loads only on waves whose `allowedPaths` intersect the patterns. Mirrors the real `frontend.md` (the vendored `rules/opt-in-stack/frontend.md` additionally carries `paths:` with the same list, `tier: wave-only`, and its provenance header after the closing `---` — see [Vendored Rules](#vendored-rules-issue-722-epic-a)):
|
|
420
487
|
|
|
421
488
|
```markdown
|
|
422
489
|
---
|
|
@@ -506,4 +573,4 @@ timeout masks real perf regressions.
|
|
|
506
573
|
- [`scripts/lib/reconcile/eligibility.mjs`](../scripts/lib/reconcile/eligibility.mjs) — `CONVERT_TYPES` (derived from `LEARNING_TYPE_REGISTRY`), rule-conversion eligibility gates
|
|
507
574
|
- [`scripts/lib/memory-proposals/schema.mjs`](../scripts/lib/memory-proposals/schema.mjs) — `PROPOSAL_TYPES` (derived from `LEARNING_TYPE_REGISTRY`)
|
|
508
575
|
- [`docs/session-config-reference.md`](session-config-reference.md#reconcile-693--696--697) § Reconcile — `reconcile.rule-expiry-days` / `min-rule-days` / `min-insight-chars` config keys that tune the emitted `expires-at` and eligibility gates
|
|
509
|
-
- Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode)
|
|
576
|
+
- Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode), #1449 (frontmatter on line 1, `globs:` + `paths:` pair and provenance-header position for vendored rules)
|
|
@@ -127,11 +127,11 @@ The blast radius is asymmetric. A false positive on the dispatch path blocks eve
|
|
|
127
127
|
| 7 | ledger unreadable / corrupt | WARN + ALLOW + **self-heal** (the verdict carries a fresh ledger) |
|
|
128
128
|
| 8 | `git ls-files` failed | ALLOW, degraded (stage 3a loses witnesses; concrete collisions still found) |
|
|
129
129
|
| 9 | `findScopeCollisions` not evaluable | WARN + ALLOW |
|
|
130
|
-
| 10 | same agent id re-dispatched | ALLOW
|
|
130
|
+
| 10 | same agent id re-dispatched | ALLOW; the earlier record is replaced only once the probe shows it FINISHED (a retry after a failure must not self-lock). A still-running predecessor with a distinct `tool_use_id` stays a collision partner (overlap → row 11); without ids on both sides it stays in the ledger as a claim, with no self-collision (#1480 B4). Replaced without a probe only where no claim is lost: the same `tool_use_id`, or (no ids on either side) a predecessor wholly covered by the retry's scope |
|
|
131
131
|
| 10a | collision, but every colliding prior agent has FINISHED | ALLOW + prune those records |
|
|
132
132
|
| 11 | collision with a prior agent still IN FLIGHT | **DENY** |
|
|
133
133
|
| 12 | unexpected throw in `main()` | ALLOW + stderr |
|
|
134
|
-
| 13 | liveness probe throws / no evidence at all | treated as IN FLIGHT (bounded by the TTL, § 5) |
|
|
134
|
+
| 13 | liveness probe throws / no evidence at all / only a result form that proves nothing | treated as IN FLIGHT (bounded by the TTL, § 5) |
|
|
135
135
|
| 14 | ledger lock not acquirable within its budget | run the cycle UNLOCKED (degraded), never deny |
|
|
136
136
|
|
|
137
137
|
Two structural rules keep this matrix honest, both recorded in the hook header:
|
|
@@ -174,7 +174,11 @@ The discriminator is therefore neither time nor the agent's name, but whether th
|
|
|
174
174
|
- **Synchronous dispatch** — the `tool_result` for the dispatch's `tool_use` id arrives when the agent is done (measured: five `Agent` rows within 0.44 s, their results 5–11 minutes later). At the fifth agent's `PreToolUse` none of the first four has a result → all in flight → a real same-batch overlap still denies.
|
|
175
175
|
- **Asynchronous dispatch** — the `tool_result` arrives in ~0.2 s and reads `Async agent launched successfully`. **That text is a launch receipt, not a completion.** Treating it as one would let every real background-batch collision through. The completion arrives later as a `<task-notification>` record carrying `<tool-use-id>` and `<status>completed</status>` (measured: launch 14:14:26.768 → notification 14:24:39.360).
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
**A completion needs positive evidence (#1480 B2).** Before 2026-09-30 every `tool_result` that did not contain the launch receipt counted as a completion, so any drift in the receipt's wording turned a launch into a finish. That was measured, not hypothetical: a forked dispatch answers `Fork started — processing in background`, which was read as done. A `tool_result` now finishes its dispatch only in a form the census found on real completions (`RESULT_COMPLETION_FORMS`): `is_error`, the sync report's `<usage>`/`total_tokens` trailer (690 of 690), or a text beginning `This agent's report was delivered` (35 of 35, none followed by any notification). Any other form is *no evidence* and falls to the TTL (row 13), never to "finished".
|
|
178
|
+
|
|
179
|
+
**Resume re-opens a dispatch (#1459 item 1).** A failed or finished agent can be resumed with `SendMessage` addressed to its task id (the `agentId` in its own result, or the `<task-id>` of its notification). The resumed run finishes under the SendMessage's `tool_use` id, not the original one (census 2026-09-30: 455 of 455 resumes with a later completion). The probe therefore treats every `SendMessage` to the task id as an activation, unless its result says `"success":false` or is an error, and counts the dispatch as finished only when a completion follows the LAST activation. The completion can be a terminal notification under the original id, a SendMessage id or the task id. Closing still comes only from harness-written notification records or the dispatch's own `tool_result`. A message cannot close anything; it can only re-open.
|
|
180
|
+
|
|
181
|
+
**Exact id first (#1480 B3).** Each ledger record carries the dispatch's own `useId` (the payload's `tool_use_id`) and is resolved by that id. The description is a fallback for records without one, used only when it is non-empty, and it excludes the current dispatch's own id. A record is never looked up by its ledger id, which for an empty description is just the agent type and matched any namesake. `buildTranscriptIndex()` stays the description view: a description counts as finished only when **every** one of its dispatch ids is. Cost containment: the transcript is read **only once a collision has already been found** — i.e. only on the path that is about to deny; the no-collision path pays nothing. Transcripts above 256 MiB are treated as *no evidence*, never as a completion.
|
|
178
182
|
|
|
179
183
|
**Blind fallback and its named ceiling (BV-004).** With no transcript, or none carrying a record of that agent, liveness falls back to the ledger entry's own age with `IN_FLIGHT_TTL_MS = 30 min`. The ceiling is derived: the largest **measured** same-batch dispatch spread is 95.7 s, so 30 min is ~19× headroom against the false-ALLOW direction, while both measured sequential repair gaps (36 min, 49 min) sit above it. **Revisit trigger:** a same-batch spread above ~5 min appearing in `.orchestrator/metrics/`, or a harness change that stops writing `transcript_path` — either invalidates the headroom the number rests on.
|
|
180
184
|
|
|
@@ -196,7 +200,7 @@ Complete list of what this guard does **not** see, or sees only approximately:
|
|
|
196
200
|
|
|
197
201
|
## 7. Debugging
|
|
198
202
|
|
|
199
|
-
**A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, files, at}` record per already-dispatched agent. The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
|
|
203
|
+
**A dispatch was denied and you do not believe it.** Read `.orchestrator/wave-dispatch-scopes.json`: it carries `waveKey`, `updated`, and one `{id, desc, useId, files, at}` record per already-dispatched agent (`useId` absent when the harness sent no `tool_use_id`). The deny reason names the other agent — find its record and compare its `files` to the ones in your prompt's `FILE-SCOPE` block. Three outcomes:
|
|
200
204
|
|
|
201
205
|
- The other agent is genuinely running and the overlap is real → fix the ownership in the session plan (one file, one agent), rewrite the affected `filescopes/wave-<N>/*.json`, re-assert, re-dispatch.
|
|
202
206
|
- The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete `.orchestrator/wave-dispatch-scopes.json`; the next dispatch rebuilds it.
|
|
@@ -223,6 +227,24 @@ Every figure above is quoted from a measurement recorded next to the code that c
|
|
|
223
227
|
|
|
224
228
|
Re-measure before citing any of these downstream. A count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006).
|
|
225
229
|
|
|
230
|
+
## The two-shape trap (#1020)
|
|
231
|
+
|
|
232
|
+
> Moved from CLAUDE.md Critical Gotchas, 2026-09-25.
|
|
233
|
+
|
|
234
|
+
**`allowedPaths` is COMPUTED, and the scope declaration has TWO shapes — writing only one is the silent failure (#1020).**
|
|
235
|
+
|
|
236
|
+
(a) **Per agent**, `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, a JSON array of path STRINGS. This is `$AGENT_FILESCOPE_JSON` — what the `FILE-SCOPE` prompt injection, the Learnings-Index and `--assert-subset` read.
|
|
237
|
+
|
|
238
|
+
(b) **Per wave**, ONE aggregate sidecar holding an array of `{id, files}` RECORDS, built from every file in (a) plus the coordinator's own. This is the only thing `--assert-disjoint` and `--union` accept: pass them the wave directory and you get `Cannot read --union file`; pass them a single (a)-file and you get `must be a JSON array of {id, files} records`.
|
|
239
|
+
|
|
240
|
+
`node scripts/materialize-wave-scope.mjs --state-dir <state-dir> --wave <N>` is the canonical writer: it receives that one record array on stdin, writes every (a) file first, then (b) last. Neither is a `$TMPDIR` copy, and neither substitutes for the other.
|
|
241
|
+
|
|
242
|
+
Skipping (a) degrades the chain to signal-free ALLOW: no `FILE-SCOPE` block reaches the prompt, so `hooks/pre-task-scope-disjoint.mjs` extracts nothing, permits the dispatch, and writes no ledger entry — the absence looks identical to a clean run.
|
|
243
|
+
|
|
244
|
+
**Measured 2026-08-19:** a coordinator that wrote only (b) ran SIX waves and ~27 dispatches with zero injection; `.orchestrator/wave-dispatch-scopes.json` still carried a `|w2|` entry from two days earlier. Only `wave-scope.json` `allowedPaths` (global Gate 7) was live, which is why blocks still fired and the gap stayed invisible.
|
|
245
|
+
|
|
246
|
+
The coordinator's OWN direct edits go in a `coordinator.json` under (a) **and must be folded into (b)** — a `coordinator.json` that is never folded in is read by nothing. 2 of the 5 recorded divergences were coordinator-direct edits, which no agent scope file covers by construction.
|
|
247
|
+
|
|
226
248
|
## See Also
|
|
227
249
|
|
|
228
250
|
- `skills/wave-executor/wave-loop.md` § Scope Manifest — the coordinator runbook (steps 3.1–3.3) and § Pre-Dispatch: File-Scope Injection (the prompt block shape).
|
|
@@ -200,6 +200,15 @@ down session-start.
|
|
|
200
200
|
| `stale-branch-days` | integer | `7` | Days of inactivity before a branch is flagged as stale. |
|
|
201
201
|
| `stale-issue-days` | integer | `30` | Days without progress before an issue is flagged for triage. |
|
|
202
202
|
|
|
203
|
+
**`cross-repo.projects` block form (#469, #478).** Not to be confused with the flat `cross-repos` list above. The dotted name is only how `parseSessionConfig()` returns it (`scripts/lib/config.mjs:446`); in the config file it is a `projects:` sub-key under a top-level `cross-repo:` block:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
cross-repo:
|
|
207
|
+
projects: [~/Projects/my-app, ~/Projects/another-app] # INLINE list only
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Parsed by `_parseCrossRepo()` (`scripts/lib/config/cross-repo.mjs:50-84`). Only the inline form on the `projects:` line is read — a block list of `- ` items under `projects:` yields `[]` without a warning (measured 2026-09-18). An entry containing anything outside `[A-Za-z0-9._~/-]` (a shell metacharacter, a space) is dropped with one stderr line `cross-repo: rejected project entry with shell metacharacter: …`. Absent, empty, `none` or `null` → `[]`. Readers additionally drop entries resolving outside `~/Projects/` (confinement root, `getConfinementRoot()`). Read by `scripts/run-migrate-v2-cross-repo.mjs`, `scripts/vault-integration-watcher.mjs`, `scripts/promote-vault-strict.mjs`, and — as a secondary repo source after the filesystem scan — `scripts/lib/dispatcher/enumerate.mjs:303`.
|
|
211
|
+
|
|
203
212
|
## Auto-Skill Dispatch (#337)
|
|
204
213
|
|
|
205
214
|
Opt-in phrase-match meta-skill (`skills/using-orchestrator/SKILL.md`) that inspects the user's first message for implicit slash-command intent (e.g. "plane neues Projekt", "run discovery on the backlog") and dispatches to the highest-confidence matching entry-point skill via the `Skill` tool — before that skill's own Phase 1 — once the bootstrap gate is open. Off by default; when `false` (or absent) the meta-skill returns immediately with zero reads, zero logging, zero side effects, and every calling skill behaves exactly as if it was never invoked.
|
|
@@ -343,9 +352,15 @@ slopcheck:
|
|
|
343
352
|
| `grounding-check` | boolean | `true` | Enable file-level grounding verification in session-end Phase 1.1a (planned vs touched files). When true, session-end compares each agent's declared file scope against `git diff --name-only $SESSION_START_REF..HEAD` and reports scope creep + incomplete coverage. Informational — does not block session close. |
|
|
344
353
|
| `grounding-injection-max-files` | integer | `3` | Max files with recent `edit-format-friction` stagnation history to inject as line-numbered GROUNDING blocks into each agent's prompt before dispatch (wave-executor pre-dispatch step). Per-agent scope; selects top N by recency. `0` disables the feature. Gated on `persistence: true`. (#85) |
|
|
345
354
|
| `isolation` | string | `auto` | Agent isolation mode: `worktree`, `none`, or `auto`. `auto` resolves per-wave via the graduated default (#194): ≤2 agents → `none`, 3–4 agents on feature/deep → `worktree`, ≥5 agents → `worktree`, housekeeping 3–4 → `none`. Explicit `worktree` or `none` overrides the graduation. The resolved value surfaces per wave as `waves[].isolation` in the session shape's JSON output (`scripts/session-shape.mjs`) — a coordinator-direct or read-only wave resolves `none` without consulting the graduation at all. See [isolation graduation](#isolation-graduation) below. |
|
|
355
|
+
| `worktree-exclude` | list | `[node_modules, dist, build, .next, .nuxt, coverage, .cache, .turbo, .vercel, out]` | Top-level directory names deleted (recursive `fs.rm`) from a freshly created worktree, to cut disk and RAM on constrained hosts (#192). Parsed as a flat list by `scripts/lib/config.mjs:237-241`; read by `createWorktree()` (`scripts/lib/worktree/lifecycle.mjs:155-181`) and applied by `applyWorktreeExcludes()` (`scripts/lib/worktree/listing.mjs`). Names only, no globs, no nested paths; a name absent from the worktree is skipped silently. `null` / `none` / `[]` disable all excludes. **Scope limit (measured 2026-09-18):** `createWorktree()` has no production caller — `grep -rn "createWorktree(" scripts hooks skills` finds only a comment in `scripts/lib/wave-executor/foreign-dispatch.mjs`, which creates its own detached worktree with `git worktree add`. Wave dispatch under `isolation: worktree` uses the harness's native Agent worktree and never reads this key, so setting it changes nothing in a wave today. |
|
|
346
356
|
| `max-turns` | integer or string | `auto` | Maximum agent turns before PARTIAL. Auto expands PER SESSION TYPE inside the resolved shape (`scripts/lib/session-shape.mjs` `MAX_TURNS_DEFAULT`, § Session Shapes above): housekeeping=8, feature=15, deep=25 (`maxTurnsDefault` in the shape JSON, applied to every wave). The `ultradeep` profile does **not** use one flat number — it sets `max-turns` PER WAVE: 40 for the Research+Code-Discovery wave, 25 for Impl-Core/Impl-Polish/Quality, 15 for Release/Finalization (the Synthesis-Gate wave is coordinator-direct and carries no `max-turns` at all). |
|
|
347
357
|
| `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. <!-- path-check: historical --> |
|
|
348
358
|
|
|
359
|
+
**Guard coverage (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** **Destructive-Command Guard is active in main + subagent waves on harnesses with a compatible command hook.** On Claude Code, and through the supported Cursor and Pi bridges, `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json`:
|
|
360
|
+
**12 of its 16 rules are `severity: block`, the other 4 only warn** (measured 2026-09-19: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`).
|
|
361
|
+
The 16th is `ledger-delete-protected` (#1401): `rm`/`mv`/`unlink` on `.orchestrator/metrics/**` — added after a wave agent deleted a rotated 53.896-line ledger archive that no rule covered, because `rm` existed only as the pattern `rm -rf` and the redirect denylist's `*.jsonl` did not match `events.jsonl.1`.
|
|
362
|
+
On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired. Rule source of truth: [`.claude/rules/parallel-sessions.md`](../.claude/rules/parallel-sessions.md) (PSA-003). Per-session bypass via Session Config: `allow-destructive-ops: true` (intentional maintenance only).
|
|
363
|
+
|
|
349
364
|
### enforcement-gates: the five gate keys (#800/#915)
|
|
350
365
|
|
|
351
366
|
`enforcement-gates` is surfaced to the hook layer as `gates` inside the wave's
|
|
@@ -765,7 +780,25 @@ vault-integration:
|
|
|
765
780
|
vault-name: # optional (#660) — per-project vault namespace override
|
|
766
781
|
```
|
|
767
782
|
|
|
768
|
-
> **Host-local override (#653; extended #819).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves
|
|
783
|
+
> **Host-local override (#653; extended #819, agents/vault#319, #1448).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `vault-dirs:` directory-prefix match against cwd > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves the same way: `SO_BASELINE_PATH` env > `owner.yaml` `baselines:` directory-prefix match against cwd > `owner.yaml` `paths.baseline-path` (legacy scalar) > the committed default. The match tier exists because one host-wide `paths.vault-dir` otherwise redirects EVERY repo on the host into one vault, including a repo whose own vault lives elsewhere on that host. First match wins, so list the more specific prefix first:
|
|
784
|
+
>
|
|
785
|
+
> ```yaml
|
|
786
|
+
> # ~/.config/session-orchestrator/owner.yaml
|
|
787
|
+
> paths:
|
|
788
|
+
> vault-dir: ~/Projects/work/vault # host-wide default
|
|
789
|
+
> vault-dirs:
|
|
790
|
+
> - path: ~/Projects/private-world/vault # used for any cwd under the prefix
|
|
791
|
+
> match: { path-prefix: ~/Projects/private-world/ }
|
|
792
|
+
> ```
|
|
793
|
+
>
|
|
794
|
+
> A leading `~` is expanded to the home directory: `path-prefix` when matching, and the `path` / `paths.vault-dir` value the two owner.yaml tiers return (`match`, `owner`). The `env` and `committed` tiers pass through unexpanded; `scripts/vault-mirror.mjs` expands its `--vault-dir` at entry, so a `~/…` from any tier reaches the vault rather than `<cwd>/~/…`.
|
|
795
|
+
>
|
|
796
|
+
> The parsed config reports which tier won as `vault-integration.vault-dir-source` (`env`, `match`, `owner`, `committed`). This keeps maintainer-specific absolute paths out of version control. Resolvers: `scripts/lib/config.mjs` (`resolveVaultDir`, module-private), `scripts/lib/config/host-paths.mjs` (`resolveHostPath`, used for `plan-baseline-path`) and `scripts/lib/named-baseline-resolver.mjs` (the `baselines:` / `vault-dirs:` match tier).
|
|
797
|
+
>
|
|
798
|
+
> **Fake-repoRoot trap (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** A throwaway invocation of a vault writer (`mirrorNarrative()`, `vault-mirror.mjs`, the board writer) with a synthetic `repoRoot` does **not** land in a sandbox — the fake repo path only fakes the SOURCE, never the DESTINATION. A reviewer in this repo walked into exactly that.
|
|
799
|
+
> Export `SO_VAULT_DIR=<tmpdir>` before ANY manual invocation of vault-writing code. <!-- consistency:exempt:runtime-only -->
|
|
800
|
+
>
|
|
801
|
+
> **Host-local switch (#1448).** A host can switch the integration off or soften it without touching the committed file, e.g. a host with no vault clone, where `vault-mirror.mjs` would otherwise exit 2 (`missing-vault-dir`) and a `strict` repo would block `/close`. Precedence: env `SO_VAULT_INTEGRATION=off|warn|strict` > `owner.yaml` `vault-integration: { enabled: false }` or `vault-integration: { mode: off|warn|strict }` > the committed block. The host may only LOWER the committed level (`strict` → `warn` → `off`); `enabled: true` or a stronger mode is ignored, so a host can never mirror a repo that did not opt in. An invalid `owner.yaml` `vault-integration:` section is dropped on its own (the rest of `owner.yaml` stays in force). When the host lowered the level, `vault-integration.host-override` names the tier (`env:SO_VAULT_INTEGRATION` or `owner.yaml`), otherwise it is `null`; session-end then prints `vault-mirror: bewusst aus auf diesem Host (<tier>)` instead of failing.
|
|
769
802
|
|
|
770
803
|
> **`SO_CONFIG_HOME` — the host-private config directory itself.** A sibling override, one layer below `owner.yaml`'s own contents rather than a key inside it: `scripts/lib/config/private-config-dir.mjs` (`resolvePrivateConfigDir()`) is THE resolver (#1223) for the directory holding `owner.yaml`, `host-private.json`, and the host-alias ledger (`SO_HOST_ALIASES_FILE`) — `scripts/lib/host-identity.mjs`'s `_privateDir()` delegates to it — with precedence env-var (`SO_CONFIG_HOME`, names the private dir ITSELF) > `XDG_CONFIG_HOME` (names its PARENT) > the homedir default `~/.config/session-orchestrator`. Both env vars are read with `.trim() || fallback`, not a bare `||` (`.claude/rules/development.md` § Error Handling env-var-fallback-whitespace trap).
|
|
771
804
|
|
|
@@ -774,7 +807,7 @@ vault-integration:
|
|
|
774
807
|
| Field | Type | Default | Description |
|
|
775
808
|
|-------|------|---------|-------------|
|
|
776
809
|
| `vault-integration.enabled` | boolean | `false` | If true, session-end and evolve skills invoke `vault-mirror.mjs` to sync learnings and sessions into the vault. When false (or missing), mirroring is skipped silently. |
|
|
777
|
-
| `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository.
|
|
810
|
+
| `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository. Host-local tiers override it (see the note above). If it is still empty after them, only the bash snippet in session-end and evolve falls back to the `$VAULT_DIR` env variable. Required when `enabled` is true. |
|
|
778
811
|
| `vault-integration.mode` | string | `warn` | Mirror error handling. `strict` blocks session close if the mirror exits non-zero. `warn` reports errors but does not block. `off` bypasses mirror invocation entirely (useful when transitioning). |
|
|
779
812
|
| `vault-integration.vault-name` | string or null | `null` | Optional override for the per-project vault namespace segment (#660). When set (or via CLI `--vault-name`), vault writes go to `40-learnings/<vault-name>/` and `50-sessions/<vault-name>/`, sanitised to a kebab slug. When null/absent, the namespace is derived from the git origin via `deriveRepo()`. Owner-privacy leaks (personal home path / private slug / personal name) are redacted to `redacted-repo`. NOT a filesystem path → NOT host-path-resolved. **Coverage gap:** portfolio board rows are NOT yet namespace-aware — the override only reaches `40-learnings/`, `50-sessions/`, and the per-repo narrative mirror (`narrative-mirror.mjs`, #832 item 2); tracked separately in #832. Resolver: `scripts/lib/vault-mirror/namespace.mjs` (`resolveRepoNamespace`). |
|
|
780
813
|
| `vault-integration.gitlab-groups` | string[] or null | `null` | List of GitLab group paths to scan for repos missing `.vault.yaml`. Consumed by `scripts/vault-backfill.mjs` (via `readVaultIntegrationConfig()`) and the `/plan retro` vault-backfill sub-mode (`skills/plan/mode-retro.md` Phase 1.6 Step 1). When null/unset, the backfill CLI exits with a "no groups configured" notice. |
|
|
@@ -1003,14 +1036,14 @@ All fields live under a top-level `dialectic` object in your Session Config (CLA
|
|
|
1003
1036
|
dialectic:
|
|
1004
1037
|
cadence: 5 # integer ≥ 0; 0 = kill-switch
|
|
1005
1038
|
model: haiku # haiku | sonnet | opus
|
|
1006
|
-
budget-tokens:
|
|
1039
|
+
budget-tokens: 32000 # input token ceiling per call
|
|
1007
1040
|
```
|
|
1008
1041
|
|
|
1009
1042
|
| Field | Type | Default | Description |
|
|
1010
1043
|
|-------|------|---------|-------------|
|
|
1011
1044
|
| `dialectic.cadence` | integer | `5` | Number of sessions between auto-dialectic dispatches. Set to `0` to disable all dispatches (kill-switch). Non-integer and negative values silently fall back to default. **The dispatch moment moved (2026-09-09):** `shouldDispatchAutoDialectic()` is still the decision function this key feeds, but session-end Phase 3.6.7's own auto-trigger nudge is gone — retired as a standalone close-time prompt, and its recording wrapper (with the `orchestrator.dialectic.nudge_decided` event) removed in #1288 — see § Persistence & Safety above "Nudge retirement". The session-start `maintenance-due` probe now calls it as one of seven signals, so a due dialectic surfaces where the operator can act on it (session start), not where they are closing down. |
|
|
1012
1045
|
| `dialectic.model` | string | `haiku` | Model tier for the critique call. Must be one of `haiku`, `sonnet`, `opus`. **Fail-fast**: unknown values cause parse-config.mjs to exit 1 at startup — NOT silently ignored. |
|
|
1013
|
-
| `dialectic.budget-tokens` | integer | `
|
|
1046
|
+
| `dialectic.budget-tokens` | integer | `32000` | Input token ceiling per call (`DEFAULT_BUDGET_TOKENS`, `scripts/lib/config/dialectic.mjs:24`). `32000` — card bodies + steering alone estimated ~12k tokens in this repo, a consumer repo measured 30,262 (2026-09-18); the value is a ceiling, the pre-dispatch estimate aborts above it (`checkBudget()`, `scripts/dialectic-deriver.mjs:125`, status `budget-exceeded` — never a truncation). Output budget is fixed at 4000 (per #506). Non-integer and negative values fall back to default. |
|
|
1014
1047
|
|
|
1015
1048
|
**Used by:** `skills/evolve/SKILL.md` Phase 6, `scripts/dialectic-deriver.mjs`, `scripts/lib/auto-dialectic.mjs`, and the session-start `maintenance-due` probe (`scripts/lib/maintenance-due-banner.mjs`). Session-end Phase 3.6.7 no longer reads it (retired 2026-09-09).
|
|
1016
1049
|
|
|
@@ -1018,9 +1051,9 @@ dialectic:
|
|
|
1018
1051
|
|
|
1019
1052
|
**Trigger behavior:** When `cadence > 0` AND sessions-since-last-dialectic ≥ cadence AND (≥1 new session OR ≥1 new learning since last run), `shouldDispatchAutoDialectic()` returns `trigger: true` and the session-start `maintenance-due` probe reports it; the housekeeping session then runs the deriver in dry-run mode (session-end Phase 3.6.7 is retired — it dispatches nothing). The diff sidecar lands at `.orchestrator/dialectic-pending.md` (gitignored, vault-mirror-excluded). When `cadence: 0`, the signal never fires; manual `/evolve --dialectic` always works. <!-- path-check: example -->
|
|
1020
1053
|
|
|
1021
|
-
**Token cost:**
|
|
1054
|
+
**Token cost:** `budget-tokens` is a ceiling, not a spend — a run costs its actual input plus at most 4000 output tokens, once every 5 sessions with the default cadence (model haiku). Surfaced in Final Report.
|
|
1022
1055
|
|
|
1023
|
-
**Empirical note (
|
|
1056
|
+
**Empirical note — why the default is `32000` (#1380).** The former parser default of `8000` proved structurally unreachable: in this repo (2026-07-04 session-3) the fixed overhead (Peer-Cards + Steering + scaffold) alone ran ≈13k tokens and a full input set (top-50 learnings + last-10 sessions) ≈28.4k tokens; a consumer repo measured 30,262 (2026-09-18). Because the budget gate aborts BEFORE dispatch instead of truncating (`scripts/dialectic-deriver.mjs:604-612`), a corpus of that size can never run under `8000` — it ends `budget-exceeded` every time. If your peer-card/steering corpus outgrows `32000`, raise `budget-tokens` — a too-low ceiling aborts the run, it never shortens the input.
|
|
1024
1057
|
|
|
1025
1058
|
## Eval (#803)
|
|
1026
1059
|
|
|
@@ -1049,6 +1082,10 @@ eval:
|
|
|
1049
1082
|
|
|
1050
1083
|
**Parser gotcha (learning confidence 0.9 — mirrors `custom-phases:` and every other block-shaped key):** the `eval:` key-line itself MUST NOT carry an inline comment. The block-open scan uses the shared `matchBlockHeader(line, 'eval')` (`scripts/lib/config/block-header.mjs`) — it tolerates the bold-bullet `- **eval:**` rendering (#830) but a trailing `# comment` on the header line still fails the match, so the parser never enters the block and ALL fields silently fall back to their defaults — no error, no warning surfaces anywhere. Sub-key lines (`enabled:`, `mode:`, …) tolerate inline comments without issue. See § Parser Gotcha: No-Inline-Comment Block Headers (top of this file) for the general contract this key shares with 36 other block-shaped keys.
|
|
1051
1084
|
|
|
1085
|
+
**Rubric version is NOT a Session Config key (#1037).** No key here selects the check set. The engine pins it as a constant — `RUBRIC_VERSION` in `scripts/lib/eval/engine.mjs` — because the rubric is *pre-registered*: a changed check mints a new version and a new file, it is never a per-repo dial. Current value: **`rubric-v2`** (`skills/eval/rubric-v2.md`, 2026-09-19), which supersedes `rubric-v1` (2026-07-16) for every newly written record. The two changes: `process-safety` no longer fails on `destructive_guard.blocked` (measured 2026-09-19 @ `d92c2ca4`: 32 of 40 records `fail`, all 32 solely from `blocked`, spiral `0` in all 40 — `.claude/rules/host-resources.md` HR-101), and the guard counts moved to a new reported-only `guard-friction` dimension. Stored records keep the `rubric_version` they were scored under and stay readable: neither `scripts/lib/eval/schema.mjs` nor `scripts/lib/eval/report.mjs` enumerates a fixed dimension set. `--verify` in `scripts/eval-session.mjs` used to re-score every stored record with the current engine and therefore reported `DRIFT` on all 40 `rubric-v1` records (measured 2026-09-19); since #1400 it compares only at matching `rubric_version` and otherwise returns its own verdict `version-mismatch` with exit code **3** — a planned rubric change is no longer indistinguishable from a forged record.
|
|
1086
|
+
|
|
1087
|
+
Neither `eval.enabled` nor `eval.judge` changed in v2 — the harness runs exactly where it ran before.
|
|
1088
|
+
|
|
1052
1089
|
**Used by:** `scripts/lib/config/eval.mjs` (`_parseEval`), `scripts/lib/config.mjs`. Skill consumer (`skills/eval/SKILL.md`) is a follow-up wave of Epic #803 — not yet implemented as of this parser.
|
|
1053
1090
|
|
|
1054
1091
|
## Vault Staleness
|
|
@@ -1205,9 +1242,38 @@ docs-orchestrator:
|
|
|
1205
1242
|
- `skills/session-end/SKILL.md` — Phase 3.2 gap-reporting step (activated when `enabled: true`)
|
|
1206
1243
|
- `agents/docs-writer.md` — agent dispatched for documentation generation
|
|
1207
1244
|
|
|
1245
|
+
## GitLab Portfolio (GH #41)
|
|
1246
|
+
|
|
1247
|
+
Opt-in cross-repo health dashboard for the `gitlab-portfolio` skill (`/portfolio`): discovers repos from `<vault>/01-projects/*/_overview.md`, aggregates open issues, MRs, critical labels and stale signals, and writes `_PORTFOLIO.md` into the vault. It needs `vault-integration.vault-dir` — the CLI exits 2 without it (`scripts/lib/gitlab-portfolio/cli.mjs`).
|
|
1248
|
+
|
|
1249
|
+
All fields live under a top-level `gitlab-portfolio` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`):
|
|
1250
|
+
|
|
1251
|
+
```yaml
|
|
1252
|
+
gitlab-portfolio:
|
|
1253
|
+
enabled: false # opt-in
|
|
1254
|
+
mode: warn # warn | strict | off
|
|
1255
|
+
stale-days: 30 # integer ≥ 1
|
|
1256
|
+
critical-labels: # BLOCK list only — see the row below
|
|
1257
|
+
- priority::critical
|
|
1258
|
+
- priority::high
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
| Field | Type | Default | Allowed values | Description |
|
|
1262
|
+
|-------|------|---------|----------------|-------------|
|
|
1263
|
+
| `gitlab-portfolio.enabled` | boolean | `false` | `true` / `false` | Only the literal `true` enables it. When false, `/portfolio` prints `disabled` and exits 0 (`cli.mjs:192-199`), and session-start Phase 2.7 skips its dry-run banner (`skills/session-start/SKILL.md` § Phase 2.7). |
|
|
1264
|
+
| `gitlab-portfolio.mode` | string | `warn` | `warn` / `strict` / `off` | `strict` makes the CLI exit 2 when any repo failed to fetch (`cli.mjs:375`). `warn` and `off` both exit 0 in the CLI; `off` additionally suppresses the session-start banner. An unknown value falls back to `warn`. |
|
|
1265
|
+
| `gitlab-portfolio.stale-days` | integer | `30` | integer ≥ 1 | An open issue whose `updated_at` is older than this counts as stale (`scripts/lib/gitlab-portfolio/aggregator.mjs`). An invalid value falls back to `30`. |
|
|
1266
|
+
| `gitlab-portfolio.critical-labels` | array of string | `[priority::critical, priority::high]` | non-empty strings | Labels that count an issue as critical; matching is scope-tolerant (`priority:critical` also matches). **Block list only:** items need their own `- ` lines indented at least 3 spaces. The inline form `critical-labels: [a, b]` is NOT read — the parser silently keeps the default (measured 2026-09-18 via `_parseGitlabPortfolio`). |
|
|
1267
|
+
|
|
1268
|
+
Parsing is tolerant: `_parseGitlabPortfolio()` (`scripts/lib/config/gitlab-portfolio.mjs`, called at `scripts/lib/config.mjs:443`) drops unknown sub-keys and replaces invalid values with the defaults above, never throwing. The strict check lives separately in `validateGitlabPortfolio()` (`scripts/lib/config-schema.mjs:369-405`, wired at `:104-106`), which reports wrong types and unknown sub-keys as schema errors under `enforcement`.
|
|
1269
|
+
|
|
1270
|
+
**Used by:** `scripts/lib/gitlab-portfolio/cli.mjs` (`/portfolio`, and session-start Phase 2.7 via `--dry-run`), `skills/gitlab-portfolio/SKILL.md`.
|
|
1271
|
+
|
|
1208
1272
|
## Events Rotation
|
|
1209
1273
|
|
|
1210
|
-
Size-based rotation for `.orchestrator/metrics/events.jsonl` (#251). Rotation fires at **session-start only** — per-append overhead is rejected design given typical growth of ~6 KiB/day. When the active log exceeds `max-size-mb`, it is renamed
|
|
1274
|
+
Size-based rotation for `.orchestrator/metrics/events.jsonl` (#251). Rotation fires at **session-start only** — per-append overhead is rejected design given typical growth of ~6 KiB/day. When the active log exceeds `max-size-mb`, it is renamed into `.orchestrator/metrics/_archive/events-<firstTs>_<lastTs>.jsonl` (each stamp `YYYYMMDDTHHMMSSZ`, derived from the archived content; `unknown` for the first when no timestamp parsed, plus a `-<n>` suffix on a name collision), archives beyond `max-backups` are pruned oldest-first, and an `orchestrator.events.rotated` record naming the archive, its byte size, line count, timestamp range and any pruned files is appended as the **first line of the new active file**. That record is what lets a reader tell a rotation from a deletion. Rotation failure never blocks session-start; errors are logged to stderr and swallowed.
|
|
1275
|
+
|
|
1276
|
+
**Reading across a rotation:** use `readEventsWithRotations(repoRoot, opts)` from `scripts/lib/events.mjs` — active file plus every archive in time order, `malformed_lines` counted, and a rotation whose `archived_as` target is missing reported in `gaps` with `complete: false`. It also still reads a legacy `events.jsonl.1`..`.N` ring left on disk by pre-#1401 versions; the writer never creates, shifts or prunes one any more. The ring was replaced because its shift step renamed every surviving backup on each rotation, which would have made the `archived_as` pointer stale one rotation after it was written.
|
|
1211
1277
|
|
|
1212
1278
|
All fields live under a top-level `events-rotation` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
|
|
1213
1279
|
|
|
@@ -1222,9 +1288,9 @@ events-rotation:
|
|
|
1222
1288
|
|-------|------|---------|-------------|
|
|
1223
1289
|
| `events-rotation.enabled` | boolean | `true` | If false, rotation is skipped entirely and `events.jsonl` grows unbounded. |
|
|
1224
1290
|
| `events-rotation.max-size-mb` | integer | `10` | Size threshold in MiB. When `events.jsonl` exceeds this, it is rotated at the next session-start. Bounds: `1..1024`; out-of-range values silently fall back to the default. |
|
|
1225
|
-
| `events-rotation.max-backups` | integer | `5` | Number of retained
|
|
1291
|
+
| `events-rotation.max-backups` | integer | `5` | Number of retained archives in `.orchestrator/metrics/_archive/`. After each rotation the excess is deleted oldest-first (by the leading timestamp in the filename) and the deletions are listed in the rotation record's `pruned` field. Only files matching the archive naming contract are eligible, so a hand-placed file in `_archive/` is never pruned. Bounds: `1..20`; out-of-range values silently fall back to the default. |
|
|
1226
1292
|
|
|
1227
|
-
**Rename safety (POSIX):** Atomic rename is safe with in-flight writers. Open file descriptors continue writing to the original inode (now
|
|
1293
|
+
**Rename safety (POSIX):** Atomic rename is safe with in-flight writers. Open file descriptors continue writing to the original inode (now the archive); new writers open the new file on next append. Maximum observed line size is 220 bytes, well under the 4096-byte PIPE_BUF atomicity guarantee.
|
|
1228
1294
|
|
|
1229
1295
|
## Test
|
|
1230
1296
|
|
|
@@ -1307,7 +1373,7 @@ remote-hosts:
|
|
|
1307
1373
|
|
|
1308
1374
|
**Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
|
|
1309
1375
|
|
|
1310
|
-
**Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct
|
|
1376
|
+
**Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct` — or, with `offload-first: true`, on `proceed` as well (see § `offload-first` below). It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
|
|
1311
1377
|
|
|
1312
1378
|
**agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
|
|
1313
1379
|
|
|
@@ -1315,6 +1381,32 @@ Read by: `scripts/lib/config/remote-hosts.mjs` (parser), `scripts/lib/config.mjs
|
|
|
1315
1381
|
|
|
1316
1382
|
See `skills/remote-offload/SKILL.md` for the wave-executor-side decision rule, the three offload channels, and how a declared alias here is what an `agent-mapping` `ssh:<alias>` value validates against.
|
|
1317
1383
|
|
|
1384
|
+
### `offload-first` (#1465)
|
|
1385
|
+
|
|
1386
|
+
Opt-in switch that makes placement the FIRST choice for an offloadable wave role, not only the answer to local pressure. Scalar key, so a trailing `# comment` on its line is fine (the block-header gotcha above does not apply).
|
|
1387
|
+
|
|
1388
|
+
```yaml
|
|
1389
|
+
offload-first: false # true = offloadable roles go to a witnessed-ready remote host even on `proceed`
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
| Field | Type | Default | Description |
|
|
1393
|
+
|-------|------|---------|-------------|
|
|
1394
|
+
| `offload-first` | boolean | `false` | When `true`, the gate's offload arm fires on a `proceed` verdict too, not only on `reduce` / `coordinator-direct`. `false` (or absent) changes nothing. |
|
|
1395
|
+
|
|
1396
|
+
**Behaviour with `offload-first: true`.**
|
|
1397
|
+
|
|
1398
|
+
- Only roles in `OFFLOADABLE_WAVE_ROLES` (`scripts/lib/wave-resource-gate.mjs`) move: wave roles `quality` / `test` → `test`, `ui` → `ui`, `perf` → `perf`. The table is NOT extended by this switch — build, lint and audit work is covered by the Quality wave's `test` role. `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident` and `refactor-crosscut` never leave the local host.
|
|
1399
|
+
- The target is the FIRST declared `remote-hosts` entry whose `roles-allowed` carries the mapped role AND that is witnessed ready (`remoteReady: { m5: true }` or an async `probeFn`). The gate still never probes the network itself, so the witness is mandatory for the switch to have any effect: without one, nothing offloads even with the switch on.
|
|
1400
|
+
- The HR-004 heavy-repo cap still binds the offloaded agent count.
|
|
1401
|
+
- No ready host for the role → the verdict is unchanged (`proceed`, or the `reduce`/`coordinator-direct` the resource rules computed) and `reasons` carries `offload-first: no ready host for '<role>' — staying local`.
|
|
1402
|
+
- Switch on but no `remote-hosts` declared → the verdict is unchanged and `reasons` carries `offload-first: true but no remote-hosts declared — staying local`. `validateSessionConfig()` (`scripts/lib/config-schema.mjs`) additionally emits a warn-level cross-field finding `offload-first-cross-field` for that combination; it never flips `ok`.
|
|
1403
|
+
- `resource-awareness: false` remains a FULL opt-out (no probe, no HR-004 cap, no placement), as documented in § Environment Awareness — the switch does not change that.
|
|
1404
|
+
- On the `probe failed (ignored)` path placement DOES apply when the switch is on, because placement needs no measurement; the `probe failed (ignored)` reason is kept.
|
|
1405
|
+
|
|
1406
|
+
**Committed-only.** There is no host-local precedence (env > `owner.yaml` > committed) for this key: the existing `resolveDispatcherAutonomy` resolver is enum-specific and `owner.yaml` freezes its allowed sections, so reusing it would need a schema change for a need nobody has raised yet (BV-001) — the value in the committed Session Config is the value that applies.
|
|
1407
|
+
|
|
1408
|
+
Read by: `scripts/lib/config.mjs` (parser), `scripts/lib/config-schema.mjs` (cross-field warning), `scripts/lib/wave-resource-gate.mjs` (placement).
|
|
1409
|
+
|
|
1318
1410
|
## Evolve Extra Sources (#638)
|
|
1319
1411
|
|
|
1320
1412
|
Opt-in EXTRA learning sources for `/evolve`. A domain measurement (e.g. an eval-learn regression harness) runs OUT-OF-BAND and writes a sidecar JSON of regression flags; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per flag that has persisted across ≥2 consecutive sessions. This is a strict **read-only consumption contract**: `/evolve` never runs the domain measurement — it only consumes the sidecar output. Absent/empty ⇒ `[]` ⇒ no extra sources are read; existing `/evolve` runs are unaffected.
|
|
@@ -1503,6 +1595,56 @@ loop-guard:
|
|
|
1503
1595
|
|
|
1504
1596
|
**Used by:** `hooks/loop-guard.mjs`, `scripts/lib/config/loop-guard.mjs` (`_parseLoopGuard`). Issue #619.
|
|
1505
1597
|
|
|
1598
|
+
## Orphan Reaper (#1425 / #1432)
|
|
1599
|
+
|
|
1600
|
+
Watchdog against orphaned descendants of quality-gate commands. On **2026-09-20** four orphaned `tsgo --noEmit` processes from another repo on the same host — two with PPID 1, 86–588 % CPU, up to 8.0 GB RSS each — drove the machine to 13 % free memory and a 67.4 load average. Part A of the PRD stops them at the source (process-group kill in the gate); this block configures part B, the net that catches what still escapes.
|
|
1601
|
+
|
|
1602
|
+
The scan itself lives in `scripts/lib/orphan-reaper.mjs`. Two hooks TRIGGER it — `hooks/post-tool-batch-wave-signal.mjs` (PostToolBatch, the only hook with measurably high frequency *during* a wave) and `hooks/on-stop.mjs` (Stop + SubagentStop, which lands exactly when gate children have just finished). Neither runs the scan inline: one `ps` round-trip out of Node was measured at ~47 ms, which alone would exceed `max-hook-latency-ms`. The hook does one config read plus one `stat` of the throttle marker (`.orchestrator/tmp/reaper-last-scan`, shared by both hooks and gitignored), then hands the work to a DETACHED, `unref()`-ed child that owns the kill, the audit and the event.
|
|
1603
|
+
|
|
1604
|
+
**Default OFF, and deliberately so.** Unlike `loop-guard`, this signal can send signals to processes, so it ships inert. `.claude/rules/host-resources.md` HR-101 binds it: a warning class above ~10 % is a broken instrument to be re-aimed, not obeyed — measure the firing rate over `false-alarm-window` decisions in the JSONL audit BEFORE arming `mode: kill`. HR-105 binds the other half: a rate nothing records is unfalsifiable, which is why every decision (including a withdrawn one) is appended to `.orchestrator/metrics/reaper-audit.jsonl`.
|
|
1605
|
+
|
|
1606
|
+
```yaml
|
|
1607
|
+
reaper:
|
|
1608
|
+
enabled: false # default OFF; only an explicit `true` arms the watchdog
|
|
1609
|
+
mode: report # report (decide + audit only) | kill (send signals)
|
|
1610
|
+
min-age-seconds: 300
|
|
1611
|
+
min-scan-interval-seconds: 30
|
|
1612
|
+
kill-grace-ms: 10000
|
|
1613
|
+
verify-wait-ms: 500
|
|
1614
|
+
max-hook-latency-ms: 50
|
|
1615
|
+
false-alarm-window: 50
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
| Field | Type | Default | Provenance / Description |
|
|
1619
|
+
|-------|------|---------|--------------------------|
|
|
1620
|
+
| `reaper.enabled` | boolean | `false` | Master toggle. Only an explicit `enabled: true` arms it; `false` means the hooks do no `stat` and no spawn at all. |
|
|
1621
|
+
| `reaper.mode` | `report` \| `kill` | `report` | `report` runs the scan in dry-run (decide + audit, no signal); `kill` arms the signal path (PRD Stufe 2, after calibration). An unrecognised value falls back to `report` — never to `kill`. |
|
|
1622
|
+
| `reaper.min-age-seconds` | integer (≥ 0) | `300` | DevWatchdog's hard limit for `tsgo`; the 2026-09-20 orphans were 7–17 min old, well above it. Self-healing clamp: a value below `min-scan-interval-seconds` is widened to it, because a process younger than one scan period can be born and reaped between two scans. |
|
|
1623
|
+
| `reaper.min-scan-interval-seconds` | integer (≥ 1) | `30` | DevWatchdog's normal scan cadence. The throttle that keeps a `PostToolBatch` storm from taxing every tool call. |
|
|
1624
|
+
| `reaper.kill-grace-ms` | integer (≥ 0) | `10000` | `DEFAULT_KILL_GRACE_MS` from `process-group.mjs:52` — repo convention, not newly invented. SIGTERM → grace → SIGKILL. |
|
|
1625
|
+
| `reaper.verify-wait-ms` | integer (≥ 0) | `500` | Wait before reading the effect back. Without it the 2026-09-20 check falsely reported "still alive" measuring immediately after `kill -9` (PRD B6: a sent signal proves nothing). |
|
|
1626
|
+
| `reaper.max-hook-latency-ms` | integer (≥ 1) | `50` | Ceiling the TRIGGER may delay a hook by — the config read, the marker `stat` and the detached spawn, measured with `performance.now()` in `maybeTriggerOrphanScan()`. Overrunning it writes ONE stderr WARN line and nothing else: no event (a `PostToolBatch`-class hook emitting per fire is the always-on signal HR-101 calls a broken instrument) and no abort (the child is already detached by then). The scan itself is not covered — that is the point of spawning detached. |
|
|
1627
|
+
| `reaper.false-alarm-window` | integer (≥ 1) | `50` | Rolling window of audit decisions the HR-101 10 % false-alarm rate is judged over — a window in DECISIONS, not calendar time, so the rate has a population on quiet hosts too. Reaches the scan as `--false-alarm-window` on the detached child's argv and becomes `runOrphanScan`'s `falseAlarmWindow`; the CLI refuses `0`, which `falseAlarmRate()` would read as “every firing ever recorded” rather than as a window. |
|
|
1628
|
+
|
|
1629
|
+
**Used by:** `hooks/post-tool-batch-wave-signal.mjs`, `hooks/on-stop.mjs` (both via `maybeTriggerOrphanScan()`), `scripts/lib/orphan-reaper.mjs`, `scripts/lib/config/reaper.mjs` (`_parseReaper`). PRD `docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md`, Epic #1425, issue #1432.
|
|
1630
|
+
|
|
1631
|
+
## Gate Timeout — Path B (#1425 A3)
|
|
1632
|
+
|
|
1633
|
+
The quality gate has two execution paths. Path A (`scripts/lib/quality-gate.mjs`) has always had a 900 000 ms cap. Path B — `scripts/run-quality-gate.mjs` → `scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs` — had **no timeout at all**, which is how a wedged `tsgo` two levels down could run unbounded and be reparented to PPID 1. This block sets the committed ceiling for path B so both paths may run equally long.
|
|
1634
|
+
|
|
1635
|
+
```yaml
|
|
1636
|
+
gate:
|
|
1637
|
+
timeout-path-b-ms: 900000 # 15 min; same cap as GATE_TIMEOUT_MS on path A
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
| Field | Type | Default | Description |
|
|
1641
|
+
|-------|------|---------|-------------|
|
|
1642
|
+
| `gate.timeout-path-b-ms` | integer (> 0) | `900000` | Per-command wall-clock ceiling for gate path B. A zero or negative value falls back to the default rather than being honoured — an instant-kill ceiling is worse than the uncapped state this key exists to fix. |
|
|
1643
|
+
|
|
1644
|
+
**Precedence, highest first:** `SO_GATE_TIMEOUT_MS` (operator override, per run) > `gate.timeout-path-b-ms` (committed) > `900000`. `run-quality-gate.mjs` resolves the three and publishes the ANSWER to the gate sub-script as the distinct env var `GATE_TIMEOUT_MS` — writing it back into `SO_GATE_TIMEOUT_MS` would make a committed default indistinguishable from an operator decision for every process further down the tree. The gate's own outer wrapper ceiling is derived from the same resolved number plus a 60 s reserve, so the inner per-command kill always fires first and the gate can still print its JSON envelope.
|
|
1645
|
+
|
|
1646
|
+
**Used by:** `scripts/run-quality-gate.mjs`, `scripts/lib/gates/gate-{baseline,incremental,full,per-file}.mjs`, `scripts/lib/config/gate.mjs` (`_parseGate`). PRD §4 A3, Epic #1425, issue #1432.
|
|
1647
|
+
|
|
1506
1648
|
## Config Protection (#622)
|
|
1507
1649
|
|
|
1508
1650
|
`PreToolUse` `Edit`/`Write` guard that intercepts edits to a small allow-list of quality-gate config files (eslint, vitest, tsconfig, prettier, commitlint, gitleaks) and warns — or, in `strict` mode, blocks — when an edit LOOSENS a gate (a threshold lowered, a disable/ignore directive added, a rule removed, a gitleaks allowlist widened, tsconfig strictness relaxed). The edit-tool analogue of the test-the-mock gate-cheating anti-pattern (see `.claude/rules/testing.md`). First-time file creation, tightening edits, and neutral edits are always allowed regardless of mode.
|
|
@@ -1741,7 +1883,7 @@ persona-gate-wave:
|
|
|
1741
1883
|
after: quality # quality | impl-polish — wave name after which to fire
|
|
1742
1884
|
threshold: "all" # "M-of-N" | "all" | "N-of-N" — passed to parseThreshold
|
|
1743
1885
|
personas: [] # list of persona names from .claude/personas/; empty = all catalog
|
|
1744
|
-
dispatch-model:
|
|
1886
|
+
dispatch-model: opus # alias or full model ID — default 'opus'
|
|
1745
1887
|
mode: off # off | warn | strict
|
|
1746
1888
|
```
|
|
1747
1889
|
|
|
@@ -1751,7 +1893,7 @@ persona-gate-wave:
|
|
|
1751
1893
|
| `persona-gate-wave.after` | string (`quality` \| `impl-polish`) | `quality` | The wave role after which the hook fires. The hook runs once per session, immediately after the named wave's STATE.md update. |
|
|
1752
1894
|
| `persona-gate-wave.threshold` | string | `"all"` | Voting threshold passed to `parseThreshold()` from `scripts/lib/persona-panel/threshold.mjs`. Accepts `"all"`, `"any"`, or `"M-of-N"` where `1 ≤ M ≤ N ≤ 20`. Example: `"6-of-6"` requires every persona to vote PASS; `"4-of-6"` allows two dissenters. |
|
|
1753
1895
|
| `persona-gate-wave.personas` | string[] | `[]` | Roster of persona names to dispatch. Each entry must match `^[a-z0-9-]{1,64}$` and refer to a persona file under `.claude/personas/<name>.md`. When the list is empty (default), every persona in the catalog is dispatched. |
|
|
1754
|
-
| `persona-gate-wave.dispatch-model` | string | `
|
|
1896
|
+
| `persona-gate-wave.dispatch-model` | string | `opus` | Model used for each persona agent dispatch. Accepts the same shape as agent frontmatter `model:` — one of `inherit` \| `sonnet` \| `opus` \| `haiku` \| `fable`, or a full model ID like `claude-opus-5-5`. Default is the alias `opus`, which tracks the current Opus; pin a full ID only for a named reproducibility need. |
|
|
1755
1897
|
| `persona-gate-wave.mode` | string (`off` \| `warn` \| `strict`) | `off` | Behaviour on consolidator result. `off` skips dispatch entirely even when `enabled: true` (silent no-op). `warn` consolidates and logs findings under a `Persona-gate:` bullet in the wave progress update without blocking. `strict` consolidates and on any non-PROCEED verdict prompts the operator via `AskUserQuestion` to proceed-as-is, revise remaining waves, or abort. |
|
|
1756
1898
|
|
|
1757
1899
|
**Validation:**
|