session-orchestrator 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +6 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +7 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +8 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +21 -0
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +7 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +7 -2
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +7 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +7 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +7 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +8 -3
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +22 -0
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +7 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +20 -0
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +6 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +6 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +8 -2
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +21 -0
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +6 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +22 -0
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +6 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +6 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +7 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +21 -0
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +7 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +13 -4
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +197 -0
- package/README.md +11 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +17 -3
- package/docs/baseline.md +12 -6
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +15 -3
- package/docs/components.md +13 -6
- package/docs/events-schema.md +59 -9
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +135 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +106 -11
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +125 -8
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +72 -25
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +211 -23
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +6 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +18 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +119 -28
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +385 -5
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +2 -2
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +171 -15
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +77 -32
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +220 -11
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +408 -33
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +76 -13
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +332 -50
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +122 -91
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +429 -7
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +91 -12
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
- package/scripts/lib/user-invocable-skills.mjs +205 -0
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +329 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +35 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
- package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
- package/scripts/lib/validate/check-unwired-features.mjs +220 -33
- package/scripts/lib/validate/check-validator-registration.mjs +36 -12
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +39 -12
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +109 -39
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +227 -0
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +11 -4
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +80 -11
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +52 -23
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +9 -2
- package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +2 -2
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +21 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +18 -6
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +20 -2
- package/skills/wave-executor/SKILL.md +14 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -743,6 +743,31 @@ export { splitSegments as splitChainSegments };
|
|
|
743
743
|
|
|
744
744
|
const ENV_ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
745
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Verbs that name a WRITE TARGET as a bare operand — no redirect operator, no
|
|
748
|
+
* wrapper flag (#1366). A sibling of WRAPPER_UNWRAP's `fileArgFlags`: that table
|
|
749
|
+
* answers "does this WRAPPER flag's operand name a file the wrapper writes?",
|
|
750
|
+
* this one "does this VERB's operand name a file the verb writes?".
|
|
751
|
+
*
|
|
752
|
+
* Spec fields:
|
|
753
|
+
* - prefix: the operand's literal prefix (`of=`), matched on a token AFTER
|
|
754
|
+
* the resolved verb.
|
|
755
|
+
* - mode: the default redirect mode the write is equivalent to.
|
|
756
|
+
* - appendRe: operands that turn the write into an APPEND. `dd` truncates by
|
|
757
|
+
* default (`conv=notrunc` / `oflag=append` do not), so reporting
|
|
758
|
+
* `truncate` unconditionally would over-report a rule class that
|
|
759
|
+
* deliberately permits `>>`.
|
|
760
|
+
*
|
|
761
|
+
* Deliberately narrow — one entry, measured. `tee`/`sponge` and in-process
|
|
762
|
+
* writers (`node -e fs.writeFileSync`) are NOT here: they are not redirect-shaped
|
|
763
|
+
* and belong to a different rule class (see the `redirect-harness-memory`
|
|
764
|
+
* rationale in .orchestrator/policy/blocked-commands.json). REVISIT TRIGGER: a
|
|
765
|
+
* second `<verb> <prefix>file` writer showing up in a transcript census.
|
|
766
|
+
*/
|
|
767
|
+
const VERB_FILE_OPERANDS = new Map([
|
|
768
|
+
['dd', { prefix: 'of=', mode: 'truncate', appendRe: /^(?:conv=.*\bnotrunc\b|oflag=.*\bappend\b)/ }],
|
|
769
|
+
]);
|
|
770
|
+
|
|
746
771
|
/**
|
|
747
772
|
* Transparent process-wrapper table for verb resolution (#982), keyed by
|
|
748
773
|
* basename. Each spec describes how to skip a wrapper's own options so the
|
|
@@ -946,15 +971,37 @@ function resolveCore(segment, unknownFlagsTakeValue) {
|
|
|
946
971
|
if (spec.splitString
|
|
947
972
|
&& (text === '-S' || text === '--split-string'
|
|
948
973
|
|| text.startsWith('--split-string=') || text.startsWith('-S'))) {
|
|
974
|
+
// env(1) APPENDS the remaining argv to the split string: `env -S bash
|
|
975
|
+
// -c '…'` runs `bash -c '…'`, not `bash` (#1366). Collecting only the
|
|
976
|
+
// operand made the trailing `-c '…'` execute unseen — the segment then
|
|
977
|
+
// resolved to `verb: 'echo x > CLAUDE.md'` (the quoted payload read as
|
|
978
|
+
// the verb) and neither the match surface nor the redirect surface ever
|
|
979
|
+
// saw it. So the payload is the operand PLUS every remaining token, and
|
|
980
|
+
// the wrapper CONSUMES the segment: nothing is left to resolve as a
|
|
981
|
+
// verb, which is why this now yields `verb: null, index: -1`.
|
|
982
|
+
let operand = null;
|
|
949
983
|
if (text.startsWith('--split-string=')) {
|
|
950
|
-
|
|
984
|
+
operand = text.slice('--split-string='.length);
|
|
985
|
+
i++;
|
|
951
986
|
} else if (text !== '-S' && text !== '--split-string') {
|
|
952
|
-
|
|
953
|
-
} else if (i + 1 < segment.length) {
|
|
954
|
-
payloads.push(segment[i + 1].text);
|
|
987
|
+
operand = text.slice(2); // attached form: -S'string'
|
|
955
988
|
i++;
|
|
989
|
+
} else if (i + 1 < segment.length) {
|
|
990
|
+
operand = segment[i + 1].text;
|
|
991
|
+
i += 2;
|
|
992
|
+
} else {
|
|
993
|
+
i++; // `env -S` with no operand at all — nothing to collect
|
|
994
|
+
}
|
|
995
|
+
if (operand !== null) {
|
|
996
|
+
// Raw token texts, joined by one space. Quoting is NOT re-applied:
|
|
997
|
+
// the tokenizer already stripped it, and re-inventing it would be a
|
|
998
|
+
// second quoting grammar. The direction of that loss is safe — an
|
|
999
|
+
// over-wide payload can only make MORE text visible to the matcher
|
|
1000
|
+
// (fail-closed), never hide a redirect operator.
|
|
1001
|
+
const rest = segment.slice(i).map((t) => t.text);
|
|
1002
|
+
payloads.push(rest.length > 0 ? [operand, ...rest].join(' ') : operand);
|
|
1003
|
+
i = segment.length;
|
|
956
1004
|
}
|
|
957
|
-
i++;
|
|
958
1005
|
continue;
|
|
959
1006
|
}
|
|
960
1007
|
if (spec.shellFlags && spec.shellFlags.has(text)) { sawShellFlag = true; i++; continue; }
|
|
@@ -1189,15 +1236,65 @@ function dedupedSegmentPayloads(segment, resolved) {
|
|
|
1189
1236
|
if (resolved.verb && DASH_C_SHELLS.has(resolved.verb)) {
|
|
1190
1237
|
for (const p of dashCPayloads(segment, resolved.index)) payloadSet.add(p);
|
|
1191
1238
|
}
|
|
1239
|
+
addArgJoinedPayload(segment, resolved.verb, resolved.index, payloadSet);
|
|
1192
1240
|
if (resolved.alt) {
|
|
1193
1241
|
for (const p of resolved.alt.payloads) payloadSet.add(p);
|
|
1194
1242
|
if (resolved.alt.verb && DASH_C_SHELLS.has(resolved.alt.verb)) {
|
|
1195
1243
|
for (const p of dashCPayloads(segment, resolved.alt.index)) payloadSet.add(p);
|
|
1196
1244
|
}
|
|
1245
|
+
addArgJoinedPayload(segment, resolved.alt.verb, resolved.alt.index, payloadSet);
|
|
1197
1246
|
}
|
|
1198
1247
|
return [...payloadSet];
|
|
1199
1248
|
}
|
|
1200
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* Interpreters that JOIN their remaining arguments with a space and execute the
|
|
1252
|
+
* result — no `-c` flag, no wrapper option to key on (#1366).
|
|
1253
|
+
*
|
|
1254
|
+
* `eval` is the whole set today. It was already in SHELL_EXEC_INTERPRETERS, but
|
|
1255
|
+
* that set only powers the QUOTED-TOKEN match in matchSegments — it never fed
|
|
1256
|
+
* the payload traversal, so `eval 'echo x > CLAUDE.md'` carried a redirect no
|
|
1257
|
+
* redirect rule ever saw (`resolveSegmentVerb` reports `payloads: []` for it).
|
|
1258
|
+
*
|
|
1259
|
+
* NAMED CEILING (BV-004): only a LITERAL payload is reachable. `eval "$CMD"` and
|
|
1260
|
+
* `eval $(…)` do not exist as command text at hook time and stay in the existing
|
|
1261
|
+
* `unresolved` class (#641). REVISIT TRIGGER: a second arg-joining interpreter
|
|
1262
|
+
* showing up in a transcript census — extend the set, never special-case `eval`.
|
|
1263
|
+
*/
|
|
1264
|
+
const ARG_JOINING_INTERPRETERS = new Set(['eval']);
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Add the joined post-verb argument text as a recursion payload when `verb` is
|
|
1268
|
+
* an arg-joining interpreter. No-op otherwise — every other caller shape keeps
|
|
1269
|
+
* the pre-#1366 payload set byte-identical.
|
|
1270
|
+
*
|
|
1271
|
+
* Quoting is not re-applied for the same reason as the `env -S` tail (see
|
|
1272
|
+
* resolveCore): the tokenizer stripped it, and a wider payload can only make
|
|
1273
|
+
* more text visible to the matcher, never hide an operator.
|
|
1274
|
+
*
|
|
1275
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
1276
|
+
* @param {string|null} verb
|
|
1277
|
+
* @param {number} index — the verb's token index (`-1` when unresolved)
|
|
1278
|
+
* @param {Set<string>} payloadSet — mutated in place
|
|
1279
|
+
*/
|
|
1280
|
+
function addArgJoinedPayload(segment, verb, index, payloadSet) {
|
|
1281
|
+
if (!verb || index < 0 || !ARG_JOINING_INTERPRETERS.has(verb)) return;
|
|
1282
|
+
const words = [];
|
|
1283
|
+
for (let i = index + 1; i < segment.length; i++) {
|
|
1284
|
+
if (segment[i].redirect) {
|
|
1285
|
+
// A redirect belongs to the SEGMENT (`eval 'x' > f` redirects eval's own
|
|
1286
|
+
// stdout), never to the joined argv — the segment scan already reported
|
|
1287
|
+
// it, and re-reporting it through the payload would duplicate the entry.
|
|
1288
|
+
const end = redirectSpanEnd(segment, i);
|
|
1289
|
+
if (end > i) i = end;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
words.push(segment[i].text);
|
|
1293
|
+
}
|
|
1294
|
+
const joined = words.join(' ').trim();
|
|
1295
|
+
if (joined.length > 0) payloadSet.add(joined);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1201
1298
|
/**
|
|
1202
1299
|
* Match a blocked-pattern regex against tokenized chain segments — the shared
|
|
1203
1300
|
* core of commandMatchesBlocked, recursion-capable for `-c` payloads (#982).
|
|
@@ -1347,6 +1444,37 @@ function collectRedirectTargets(segments, out, depth, budget) {
|
|
|
1347
1444
|
out.push({ target: wa.value, mode: 'truncate', fd: null });
|
|
1348
1445
|
}
|
|
1349
1446
|
|
|
1447
|
+
// A VERB can name its write target as a bare operand, with no redirect
|
|
1448
|
+
// operator and no wrapper flag: `dd of=CLAUDE.md` truncates CLAUDE.md while
|
|
1449
|
+
// `> CLAUDE.md` is denied (measured ALLOW pre-#1366 against the real
|
|
1450
|
+
// policy). Keyed on the RESOLVED verb, so `sudo dd of=X` is reached through
|
|
1451
|
+
// the existing wrapper unwrap; both readings contribute, as above.
|
|
1452
|
+
const seenVerbOperands = new Set();
|
|
1453
|
+
for (const reading of resolved.alt ? [resolved, resolved.alt] : [resolved]) {
|
|
1454
|
+
const spec = reading.verb ? VERB_FILE_OPERANDS.get(reading.verb) : null;
|
|
1455
|
+
if (!spec || reading.index < 0) continue;
|
|
1456
|
+
let operand = null;
|
|
1457
|
+
let append = false;
|
|
1458
|
+
for (let k = reading.index + 1; k < segment.length; k++) {
|
|
1459
|
+
const text = segment[k].text;
|
|
1460
|
+
if (segment[k].redirect) continue;
|
|
1461
|
+
if (text.startsWith(spec.prefix)) operand = text.slice(spec.prefix.length);
|
|
1462
|
+
else if (spec.appendRe?.test(text)) append = true;
|
|
1463
|
+
}
|
|
1464
|
+
if (operand === null || operand.length === 0) continue;
|
|
1465
|
+
const mode = append ? 'append' : spec.mode;
|
|
1466
|
+
const key = `${operand}:${mode}`;
|
|
1467
|
+
if (seenVerbOperands.has(key)) continue;
|
|
1468
|
+
seenVerbOperands.add(key);
|
|
1469
|
+
if (/[$`]/.test(operand)) {
|
|
1470
|
+
// Same fail-visible rule as a redirect operand (#983) — never guess at a
|
|
1471
|
+
// variable or a command substitution, never silently drop it either.
|
|
1472
|
+
out.push({ target: null, mode, fd: null, unresolved: true });
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
out.push({ target: operand, mode, fd: null });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1350
1478
|
// Both readings of an ambiguous unknown flag contribute payloads (#1000),
|
|
1351
1479
|
// UNIONED and de-duped (HIGH-2). Walking parse A only let
|
|
1352
1480
|
// `env -Q x bash -c 'echo pwned > CLAUDE.md'` — which resolves to the
|
|
@@ -1684,6 +1812,32 @@ export function redirectRuleMatches(rule, command, opts = {}) {
|
|
|
1684
1812
|
// an existing repo-relative rule, and a repo-relative entry still cannot
|
|
1685
1813
|
// escape the root. An unexpandable `~` (no usable `home`) matches NOTHING —
|
|
1686
1814
|
// fail-closed for the new class, since an unexpanded `~` names no file.
|
|
1815
|
+
const compiled = compileTargetDenylist(denylist, home);
|
|
1816
|
+
if (compiled.relRegexes.length === 0 && compiled.absRegexes.length === 0) return false;
|
|
1817
|
+
|
|
1818
|
+
for (const entry of extractRedirectTargets(command)) {
|
|
1819
|
+
if (entry.unresolved) continue;
|
|
1820
|
+
if (!modes.has(entry.mode)) continue;
|
|
1821
|
+
if (targetMatchesDenylist(entry.target, compiled, repoRoot, home)) return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Compile a rule's `target-denylist` into the two glob classes described in
|
|
1828
|
+
* `redirectRuleMatches` (#1362): repo-relative entries and home-anchored (`~/…`)
|
|
1829
|
+
* entries. Extracted so a SECOND rule class can reuse the exact same compilation
|
|
1830
|
+
* instead of growing a third copy of the glob logic (#1401) — the file already
|
|
1831
|
+
* carries that warning at `findMatchedRedirectEntry` in the consuming hook.
|
|
1832
|
+
*
|
|
1833
|
+
* An unexpandable `~` (no usable `home`) contributes NOTHING — fail-closed for
|
|
1834
|
+
* the home-anchored class, since an unexpanded `~` names no file.
|
|
1835
|
+
*
|
|
1836
|
+
* @param {string[]} denylist
|
|
1837
|
+
* @param {string|undefined} home
|
|
1838
|
+
* @returns {{ relRegexes: RegExp[], absRegexes: RegExp[] }}
|
|
1839
|
+
*/
|
|
1840
|
+
function compileTargetDenylist(denylist, home) {
|
|
1687
1841
|
const relRegexes = [];
|
|
1688
1842
|
const absRegexes = [];
|
|
1689
1843
|
for (const pattern of denylist) {
|
|
@@ -1696,38 +1850,48 @@ export function redirectRuleMatches(rule, command, opts = {}) {
|
|
|
1696
1850
|
relRegexes.push(redirectGlobToRegExp(text));
|
|
1697
1851
|
}
|
|
1698
1852
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
for (const entry of extractRedirectTargets(command)) {
|
|
1702
|
-
if (entry.unresolved) continue;
|
|
1703
|
-
if (!modes.has(entry.mode)) continue;
|
|
1853
|
+
return { relRegexes, absRegexes };
|
|
1854
|
+
}
|
|
1704
1855
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1856
|
+
/**
|
|
1857
|
+
* Test ONE resolved target text against a compiled denylist.
|
|
1858
|
+
*
|
|
1859
|
+
* A RELATIVE target is repo-root-relative in BOTH classes, exactly as the
|
|
1860
|
+
* repo-relative class already treats it (#994 R1) — otherwise
|
|
1861
|
+
* `../../.claude/projects/<enc>/memory/MEMORY.md` from the repo root names the
|
|
1862
|
+
* very file the `~` spelling blocks, and only the spelling would be denied
|
|
1863
|
+
* (measured 2026-09-13: relative `>`/`>>` ALLOW vs tilde/absolute BLOCK on one
|
|
1864
|
+
* and the same resolved path). This resolves TARGETS, never ENTRIES: a
|
|
1865
|
+
* repo-relative denylist entry still goes through `repoRelativeRedirectTarget`
|
|
1866
|
+
* and still cannot reach out of the root.
|
|
1867
|
+
*
|
|
1868
|
+
* @param {string} rawTarget — resolved target text (quotes already stripped)
|
|
1869
|
+
* @param {{ relRegexes: RegExp[], absRegexes: RegExp[] }} compiled
|
|
1870
|
+
* @param {string|null} repoRoot
|
|
1871
|
+
* @param {string|undefined} home
|
|
1872
|
+
* @returns {boolean}
|
|
1873
|
+
*/
|
|
1874
|
+
function targetMatchesDenylist(rawTarget, compiled, repoRoot, home) {
|
|
1875
|
+
const { relRegexes, absRegexes } = compiled;
|
|
1876
|
+
|
|
1877
|
+
if (absRegexes.length > 0) {
|
|
1878
|
+
const expandedTarget = expandLeadingHome(rawTarget, home);
|
|
1879
|
+
const absTarget = path.isAbsolute(expandedTarget)
|
|
1880
|
+
? expandedTarget
|
|
1881
|
+
: repoRoot && path.isAbsolute(repoRoot)
|
|
1882
|
+
? path.resolve(repoRoot, expandedTarget)
|
|
1883
|
+
: null;
|
|
1884
|
+
if (absTarget !== null) {
|
|
1885
|
+
const folded = foldAbsoluteTarget(absTarget);
|
|
1886
|
+
if (absRegexes.some((re) => re.test(folded))) return true;
|
|
1724
1887
|
}
|
|
1888
|
+
}
|
|
1725
1889
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1890
|
+
if (relRegexes.length > 0) {
|
|
1891
|
+
const target = repoRelativeRedirectTarget(rawTarget, repoRoot, home);
|
|
1892
|
+
if (target !== null && relRegexes.some((re) => re.test(target))) return true;
|
|
1730
1893
|
}
|
|
1894
|
+
|
|
1731
1895
|
return false;
|
|
1732
1896
|
}
|
|
1733
1897
|
|
|
@@ -1747,6 +1911,217 @@ function foldAbsoluteTarget(abs) {
|
|
|
1747
1911
|
);
|
|
1748
1912
|
}
|
|
1749
1913
|
|
|
1914
|
+
/**
|
|
1915
|
+
* Verbs that REMOVE or RELOCATE a file named as a bare operand (#1401).
|
|
1916
|
+
*
|
|
1917
|
+
* `mv` is here for BOTH of its operand roles: the SOURCE disappears from its
|
|
1918
|
+
* path, and the DESTINATION is overwritten when it already exists — either one
|
|
1919
|
+
* loses the file that was there. So every operand of these verbs is a candidate,
|
|
1920
|
+
* with no source/destination distinction to get wrong.
|
|
1921
|
+
*
|
|
1922
|
+
* NAMED CEILING (BV-004) — measured 2026-09-19 against `splitChainSegments` +
|
|
1923
|
+
* `resolveSegmentVerb`. COVERED, because the verb resolves through the existing
|
|
1924
|
+
* machinery: bare `rm`/`mv`/`unlink`; `env X=1 rm …`, `sudo rm …`, `timeout 5 rm
|
|
1925
|
+
* …`, `nohup`/`nice`/`stdbuf`/`command`/`exec` (WRAPPER_UNWRAP); every chain
|
|
1926
|
+
* position (`;`, `&&`, `||`, `|`, `&`, newline) and compound-statement head
|
|
1927
|
+
* (`do rm …`, `{ rm …; }` — COMMAND_POSITION_KEYWORDS); and the literal payload
|
|
1928
|
+
* of `bash -c '…'` / `env -S '…'` / `eval '…'` via the shared payload recursion.
|
|
1929
|
+
*
|
|
1930
|
+
* NOT covered, deliberately, because the deleted path is not lexically an
|
|
1931
|
+
* operand of a resolvable verb:
|
|
1932
|
+
* - `find … -delete` / `find … -exec rm {} \;` — the verb is `find`; its path
|
|
1933
|
+
* operand is a SEARCH ROOT, and treating a search root as a delete target
|
|
1934
|
+
* would also flag a pure `find … -print` over the same directory.
|
|
1935
|
+
* - `… | xargs rm` — the targets arrive on stdin and do not exist as command
|
|
1936
|
+
* text at hook time. `xargs` is deliberately NOT in WRAPPER_UNWRAP (see
|
|
1937
|
+
* there: unwrapping it could only LOOSEN the guard).
|
|
1938
|
+
* - `git rm <path>` — the verb is `git`; a git-subverb grammar is a separate
|
|
1939
|
+
* table. PSA-007 already bans `git rm` for dispatched subagents.
|
|
1940
|
+
* - in-process deletion (`node -e 'fs.unlinkSync(…)'`) — not redirect- or
|
|
1941
|
+
* operand-shaped; same class boundary the `redirect-harness-memory`
|
|
1942
|
+
* rationale draws for `tee -a` and `fs.appendFileSync`.
|
|
1943
|
+
* REVISIT TRIGGER: any of those four shapes showing up in a real deletion
|
|
1944
|
+
* incident or a transcript census — extend this set or add the sibling grammar,
|
|
1945
|
+
* never special-case one command.
|
|
1946
|
+
*/
|
|
1947
|
+
const PATH_DESTRUCTIVE_VERBS = new Set(['rm', 'mv', 'unlink']);
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Recursive collector behind {@link extractDeleteTargets}. Mirrors
|
|
1951
|
+
* collectRedirectTargets' structure — same segment walk, same dual-reading
|
|
1952
|
+
* union, same depth/budget caps and the same fail-VISIBLE cut-off markers.
|
|
1953
|
+
*
|
|
1954
|
+
* @param {Array<Array<{ text: string, quoted: boolean, redirect?: object }>>} segments
|
|
1955
|
+
* @param {Array<object>} out — accumulator
|
|
1956
|
+
* @param {number} depth
|
|
1957
|
+
* @param {{ remaining: number }} budget
|
|
1958
|
+
*/
|
|
1959
|
+
function collectDeleteTargets(segments, out, depth, budget) {
|
|
1960
|
+
for (const segment of segments) {
|
|
1961
|
+
const resolved = resolveSegmentVerb(segment);
|
|
1962
|
+
|
|
1963
|
+
// BOTH readings of an ambiguous unknown WRAPPER flag contribute (#1000),
|
|
1964
|
+
// unioned and never replaced: `env -Q x rm .orchestrator/metrics/events.jsonl`
|
|
1965
|
+
// resolves to the non-destructive verb `x` in parse A and to `rm` in parse B,
|
|
1966
|
+
// while `sudo -n rm <file>` is the mirror case (parse B swallows `rm` as
|
|
1967
|
+
// `-n`'s operand). Picking either reading alone drops one of the two.
|
|
1968
|
+
const readings = resolved.alt ? [resolved, resolved.alt] : [resolved];
|
|
1969
|
+
const seen = new Set();
|
|
1970
|
+
|
|
1971
|
+
for (const reading of readings) {
|
|
1972
|
+
if (!reading.verb || reading.index < 0) continue;
|
|
1973
|
+
if (!PATH_DESTRUCTIVE_VERBS.has(reading.verb)) continue;
|
|
1974
|
+
// A verb that only exists inside a quoted token is inert literal text for
|
|
1975
|
+
// this segment — same rule commandHasRecursiveForceRm applies. An
|
|
1976
|
+
// interpreter's quoted payload is reached through the recursion below
|
|
1977
|
+
// instead, where it is re-tokenized as the command it really is.
|
|
1978
|
+
if (segment[reading.index].quoted) continue;
|
|
1979
|
+
|
|
1980
|
+
let seenDashDash = false;
|
|
1981
|
+
for (let i = reading.index + 1; i < segment.length; i++) {
|
|
1982
|
+
const tok = segment[i];
|
|
1983
|
+
// A redirect operand is a filename the SHELL consumes, not an operand of
|
|
1984
|
+
// the verb — `rm -f x > out.log` deletes `x`, it does not delete
|
|
1985
|
+
// `out.log` (the redirect surface judges that one).
|
|
1986
|
+
if (tok.redirect) { i = redirectSpanEnd(segment, i); continue; }
|
|
1987
|
+
if (!seenDashDash && tok.text === '--') { seenDashDash = true; continue; }
|
|
1988
|
+
// FLAG SKIPPING, and why this is the UNION of both flag readings rather
|
|
1989
|
+
// than a pick (#1000 applied to the VERB's own options): an option table
|
|
1990
|
+
// cannot enumerate every flag `rm`/`mv` may grow, and the two possible
|
|
1991
|
+
// readings of an unknown one are "boolean" (the next word is an operand)
|
|
1992
|
+
// and "value-taking" (the next word is the flag's value). The
|
|
1993
|
+
// value-taking reading is a strict SUBSET of the boolean one here — it
|
|
1994
|
+
// can only HIDE a word, never reveal one — so collecting under the
|
|
1995
|
+
// boolean reading yields exactly the union of both. That is also the
|
|
1996
|
+
// fail-closed direction: `mv -t .orchestrator/metrics/ x` reports the
|
|
1997
|
+
// `-t` operand as a candidate (it IS a destination that can overwrite),
|
|
1998
|
+
// where a value-taking reading would have swallowed it silently.
|
|
1999
|
+
if (!seenDashDash && !tok.quoted && tok.text.startsWith('-') && tok.text !== '-') continue;
|
|
2000
|
+
|
|
2001
|
+
const key = `${reading.verb}\u0000${tok.text}`;
|
|
2002
|
+
if (seen.has(key)) continue;
|
|
2003
|
+
seen.add(key);
|
|
2004
|
+
|
|
2005
|
+
if (/[$`]/.test(tok.text)) {
|
|
2006
|
+
// Variable indirection (`rm -f "$LEDGER"`) or command substitution:
|
|
2007
|
+
// fail-visible, never guess (#983 rule, same direction).
|
|
2008
|
+
out.push({
|
|
2009
|
+
target: null, verb: reading.verb, unresolved: true, reason: 'variable/substitution',
|
|
2010
|
+
});
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
out.push({ target: tok.text, verb: reading.verb });
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Literal `-c` / `env -S` / `eval` payload recursion — the same deduped
|
|
2018
|
+
// union helper and the same shared budget the match and redirect surfaces
|
|
2019
|
+
// use, so this traversal cannot be starved any differently than those.
|
|
2020
|
+
const payloads = dedupedSegmentPayloads(segment, resolved);
|
|
2021
|
+
if (payloads.length === 0) continue;
|
|
2022
|
+
|
|
2023
|
+
// A cap that drops payloads SILENTLY is a bypass, not a cap (#988 T2) —
|
|
2024
|
+
// emit a marker so the consuming hook can surface it.
|
|
2025
|
+
if (depth >= MAX_PAYLOAD_DEPTH) {
|
|
2026
|
+
out.push({ target: null, verb: null, unresolved: true, reason: 'depth-exceeded' });
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
for (const payload of payloads) {
|
|
2030
|
+
if (budget.remaining <= 0) {
|
|
2031
|
+
out.push({ target: null, verb: null, unresolved: true, reason: 'budget-exhausted' });
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
budget.remaining -= 1;
|
|
2035
|
+
const subTokens = tokenizeCommand(
|
|
2036
|
+
normalizeShellWhitespaceExpansions(payload, { expandSingleQuoted: true }),
|
|
2037
|
+
);
|
|
2038
|
+
collectDeleteTargets(splitSegments(subTokens), out, depth + 1, budget);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Extract every path a command names as an operand of a file-destroying verb
|
|
2045
|
+
* (`rm`, `mv`, `unlink` — see {@link PATH_DESTRUCTIVE_VERBS} for the covered and
|
|
2046
|
+
* uncovered shapes), across all chain segments and literal shell payloads (#1401).
|
|
2047
|
+
*
|
|
2048
|
+
* This is the DELETE/RENAME sibling of `extractRedirectTargets`: that one answers
|
|
2049
|
+
* "which file does this command WRITE", this one "which file does it REMOVE or
|
|
2050
|
+
* MOVE". They are separate surfaces because a redirect target and a verb operand
|
|
2051
|
+
* are different grammar positions — `rm -f x > out.log` names one of each, and
|
|
2052
|
+
* the guard's 53,896-line telemetry loss (#1401) happened because only the write
|
|
2053
|
+
* surface was ever consulted.
|
|
2054
|
+
*
|
|
2055
|
+
* Entry shapes:
|
|
2056
|
+
* - `{ target: string, verb: 'rm'|'mv'|'unlink' }` — resolved operand, quotes
|
|
2057
|
+
* stripped by the tokenizer.
|
|
2058
|
+
* - `{ target: null, verb, unresolved: true, reason: 'variable/substitution' }`
|
|
2059
|
+
* — the operand carries a variable or a command substitution. Reported
|
|
2060
|
+
* fail-visible; never a match candidate (#641 FP class).
|
|
2061
|
+
* - `{ target: null, verb: null, unresolved: true,
|
|
2062
|
+
* reason: 'budget-exhausted'|'depth-exceeded' }` — a payload subtree was NOT
|
|
2063
|
+
* traversed because a recursion cap cut it off (#988 T2).
|
|
2064
|
+
*
|
|
2065
|
+
* @param {string} command
|
|
2066
|
+
* @returns {Array<{ target: string|null, verb: string|null, unresolved?: boolean, reason?: string }>}
|
|
2067
|
+
*/
|
|
2068
|
+
export function extractDeleteTargets(command) {
|
|
2069
|
+
if (typeof command !== 'string' || command.length === 0) return [];
|
|
2070
|
+
const out = [];
|
|
2071
|
+
const segments = splitSegments(tokenizeCommand(normalizeShellWhitespaceExpansions(command)));
|
|
2072
|
+
collectDeleteTargets(segments, out, 0, { remaining: MAX_PAYLOAD_EVALUATIONS });
|
|
2073
|
+
return out;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Return the FIRST collected delete/rename target a `path-delete` policy rule's
|
|
2078
|
+
* `target-denylist` matches, or `null` when none does (#1401).
|
|
2079
|
+
*
|
|
2080
|
+
* Returns the ENTRY, not a boolean, so the consuming hook can name the verb and
|
|
2081
|
+
* the target in its deny reason without re-probing through a boolean matcher —
|
|
2082
|
+
* the workaround `findMatchedRedirectEntry` needs, and which its own docblock
|
|
2083
|
+
* flags as one copy too many.
|
|
2084
|
+
*
|
|
2085
|
+
* Takes pre-collected `entries` rather than the raw command so the hook
|
|
2086
|
+
* tokenizes ONCE (it needs the same array for the unresolved-marker notices).
|
|
2087
|
+
*
|
|
2088
|
+
* PER-TARGET, never per-command: every entry is judged on its own, so a sibling
|
|
2089
|
+
* operand that is NOT on the denylist can never lift the verdict for one that
|
|
2090
|
+
* is. `rm -f .orchestrator/tmp/ok .orchestrator/metrics/events.jsonl` matches on
|
|
2091
|
+
* the second operand — the exemption is the ABSENCE of a denylist entry for
|
|
2092
|
+
* `.orchestrator/tmp/**`, never a whole-command escape clause. That shape is the
|
|
2093
|
+
* #1106 hole (`guard-design.md` § "Widening a matcher without narrowing its
|
|
2094
|
+
* bypass"), where an appended exempt statement lifted the verdict for the whole
|
|
2095
|
+
* command; it is structurally unreachable here.
|
|
2096
|
+
*
|
|
2097
|
+
* Denylist semantics (globs, the two classes, `~` handling, case-folding,
|
|
2098
|
+
* repo-root relativization) are IDENTICAL to `redirectRuleMatches` — both call
|
|
2099
|
+
* `compileTargetDenylist` / `targetMatchesDenylist`, so the two rule classes can
|
|
2100
|
+
* never drift apart on what a pattern means.
|
|
2101
|
+
*
|
|
2102
|
+
* @param {{ 'target-denylist'?: string[] }} rule
|
|
2103
|
+
* @param {Array<{ target: string|null, verb: string|null, unresolved?: boolean }>} entries
|
|
2104
|
+
* @param {{ repoRoot?: string|null, home?: string|undefined }} [opts]
|
|
2105
|
+
* @returns {{ target: string, verb: string }|null}
|
|
2106
|
+
*/
|
|
2107
|
+
export function firstDeniedDeleteTarget(rule, entries, opts = {}) {
|
|
2108
|
+
const { repoRoot = null, home = process.env.HOME } = opts;
|
|
2109
|
+
if (!rule || !Array.isArray(entries) || entries.length === 0) return null;
|
|
2110
|
+
const denylist = Array.isArray(rule['target-denylist']) ? rule['target-denylist'] : [];
|
|
2111
|
+
if (denylist.length === 0) return null;
|
|
2112
|
+
|
|
2113
|
+
const compiled = compileTargetDenylist(denylist, home);
|
|
2114
|
+
if (compiled.relRegexes.length === 0 && compiled.absRegexes.length === 0) return null;
|
|
2115
|
+
|
|
2116
|
+
for (const entry of entries) {
|
|
2117
|
+
if (entry.unresolved || typeof entry.target !== 'string') continue;
|
|
2118
|
+
if (targetMatchesDenylist(entry.target, compiled, repoRoot, home)) {
|
|
2119
|
+
return { target: entry.target, verb: entry.verb };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1750
2125
|
/**
|
|
1751
2126
|
* Test whether a command string contains a blocked pattern with shell-aware
|
|
1752
2127
|
* boundaries AND a quoted-payload guard (#641).
|
|
@@ -14,6 +14,15 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
14
14
|
|
|
15
15
|
const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Default `dialectic.budget-tokens` — the INPUT ceiling of one derivation call
|
|
19
|
+
* (SSOT; `scripts/dialectic-deriver.mjs` `DEFAULT_BUDGET.input` imports it).
|
|
20
|
+
* It is an upper bound the pre-dispatch estimate aborts on, not a spend: card
|
|
21
|
+
* bodies + steering alone estimated ~12k tokens here and ~30k in a consumer
|
|
22
|
+
* repo, so the former default of 8000 aborted real runs (#1380).
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_BUDGET_TOKENS = 32000;
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* Parse the top-level `dialectic:` YAML block from markdown content.
|
|
19
28
|
* Independent of the `## Session Config` section boundary.
|
|
@@ -21,7 +30,7 @@ const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
|
21
30
|
* Defaults:
|
|
22
31
|
* cadence: 5 (integer ≥ 0 — session count between auto-runs; 0 = disabled)
|
|
23
32
|
* model: 'haiku'
|
|
24
|
-
* budget-tokens:
|
|
33
|
+
* budget-tokens: DEFAULT_BUDGET_TOKENS (32000, integer ≥ 0)
|
|
25
34
|
*
|
|
26
35
|
* Throws if `model` is present but not one of haiku|sonnet|opus.
|
|
27
36
|
*
|
|
@@ -29,7 +38,7 @@ const ALLOWED_MODELS = new Set(['haiku', 'sonnet', 'opus']);
|
|
|
29
38
|
* @returns {{ cadence: number, model: string, "budget-tokens": number }}
|
|
30
39
|
*/
|
|
31
40
|
export function _parseDialectic(content) {
|
|
32
|
-
const defaults = { cadence: 5, model: 'haiku', 'budget-tokens':
|
|
41
|
+
const defaults = { cadence: 5, model: 'haiku', 'budget-tokens': DEFAULT_BUDGET_TOKENS };
|
|
33
42
|
|
|
34
43
|
const lines = preprocessBlockLines(content);
|
|
35
44
|
let inBlock = false;
|
|
@@ -49,7 +58,7 @@ export function _parseDialectic(content) {
|
|
|
49
58
|
|
|
50
59
|
let cadence = 5;
|
|
51
60
|
let model = 'haiku';
|
|
52
|
-
let budgetTokens =
|
|
61
|
+
let budgetTokens = DEFAULT_BUDGET_TOKENS;
|
|
53
62
|
|
|
54
63
|
for (const rawLine of blockLines) {
|
|
55
64
|
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
@@ -5,6 +5,25 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
5
5
|
* drift-check.mjs — Parser for the top-level `drift-check:` YAML block.
|
|
6
6
|
*
|
|
7
7
|
* Ported from config-yaml-parser.sh (v2).
|
|
8
|
+
*
|
|
9
|
+
* SCOPE NOTE — this file parses FLAGS ONLY; it implements no check. The
|
|
10
|
+
* `check-command-count` flag below toggles the `command-count` surface in
|
|
11
|
+
* `skills/claude-md-drift-check/checker.mjs`, which is where the counting lives.
|
|
12
|
+
*
|
|
13
|
+
* `command-count` STAYS on `ls commands/*.md | wc -l` on purpose. The checker is
|
|
14
|
+
* GENERIC over any consumer repo, where a slash command still IS a
|
|
15
|
+
* `commands/*.md` file. The 2026-09-16 command→skill fold (#1370) is a fact
|
|
16
|
+
* about THIS plugin — 24 command files folded into their same-named
|
|
17
|
+
* `skills/<name>/SKILL.md` behind an explicit `user-invocable: true` — so
|
|
18
|
+
* teaching the shared checker that union would mis-measure every repo that never
|
|
19
|
+
* folded. `scripts/site-numbers.mjs` `countCommands()` carries the union for this
|
|
20
|
+
* plugin's own tile, and names the divergence in its docblock.
|
|
21
|
+
*
|
|
22
|
+
* CONSEQUENCE for THIS repo, left deliberately visible rather than papered over:
|
|
23
|
+
* a prose claim of the shape "N commands" / "N slash commands" in a
|
|
24
|
+
* drift-check-scanned doc is compared against the `commands/` listing (2 after
|
|
25
|
+
* the fold), so such a claim must either be dropped or reworded off the
|
|
26
|
+
* `\b(\d+)\s+(?:\/)?commands?\b` pattern the checker matches.
|
|
8
27
|
*/
|
|
9
28
|
|
|
10
29
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { matchBlockHeader } from './block-header.mjs';
|
|
2
|
+
import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gate.mjs — Parser for the top-level `gate:` YAML block
|
|
6
|
+
* (PRD `docs/prd/2026-09-20-prozessgruppen-kill-und-waisen-waechter.md` §4 A3,
|
|
7
|
+
* Epic #1425 / issue #1432).
|
|
8
|
+
*
|
|
9
|
+
* Holds the wall-clock ceiling for GATE PATH B — the
|
|
10
|
+
* `scripts/run-quality-gate.mjs` → `scripts/lib/gates/gate-*.mjs` route, which
|
|
11
|
+
* had NO timeout at all before #1425 A3. Path A (`scripts/lib/quality-gate.mjs`)
|
|
12
|
+
* keeps its own `SO_GATE_TIMEOUT_MS` operator override; this block sets the
|
|
13
|
+
* COMMITTED default for path B so both paths may run equally long.
|
|
14
|
+
*
|
|
15
|
+
* Precedence in `run-quality-gate.mjs`: `SO_GATE_TIMEOUT_MS` (operator, wins) >
|
|
16
|
+
* this committed value > `DEFAULT_GATE_TIMEOUT_MS` (900 000).
|
|
17
|
+
*
|
|
18
|
+
* Tolerant parser: malformed values silently fall back to the default.
|
|
19
|
+
*
|
|
20
|
+
* Returns `{ 'timeout-path-b-ms': number }`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** 900 000 ms (15 min) — same ceiling as `GATE_TIMEOUT_MS` on path A (PRD §4). */
|
|
24
|
+
const DEFAULT_TIMEOUT_PATH_B_MS = 900_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse the top-level `gate:` YAML block from markdown content.
|
|
28
|
+
* Independent of the `## Session Config` section boundary.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} content — full file contents
|
|
31
|
+
* @returns {{'timeout-path-b-ms': number}}
|
|
32
|
+
*/
|
|
33
|
+
export function _parseGate(content) {
|
|
34
|
+
let timeoutPathBMs = DEFAULT_TIMEOUT_PATH_B_MS;
|
|
35
|
+
|
|
36
|
+
const lines = preprocessBlockLines(content);
|
|
37
|
+
let inBlock = false;
|
|
38
|
+
const blockLines = [];
|
|
39
|
+
|
|
40
|
+
for (const rawLine of lines) {
|
|
41
|
+
const line = rawLine.replace(/\r$/, '');
|
|
42
|
+
if (!inBlock) {
|
|
43
|
+
if (matchBlockHeader(line, 'gate')) inBlock = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
47
|
+
blockLines.push(line);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const rawLine of blockLines) {
|
|
51
|
+
const clean = rawLine.replace(/\s*#.*$/, '').replace(/\s+$/, '');
|
|
52
|
+
if (!clean.trim()) continue;
|
|
53
|
+
|
|
54
|
+
const kvMatch = clean.match(/^\s+([a-zA-Z_-]+):\s*(.*)/);
|
|
55
|
+
if (!kvMatch) continue;
|
|
56
|
+
|
|
57
|
+
const k = kvMatch[1];
|
|
58
|
+
let v = kvMatch[2].trim();
|
|
59
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) v = v.slice(1, -1);
|
|
60
|
+
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
61
|
+
|
|
62
|
+
if (k === 'timeout-path-b-ms') {
|
|
63
|
+
// A zero or negative ceiling would kill every command instantly — the
|
|
64
|
+
// failure mode is worse than the uncapped state this key exists to fix,
|
|
65
|
+
// so it falls back to the default rather than being honoured.
|
|
66
|
+
if (/^-?\d+$/.test(v)) {
|
|
67
|
+
const n = Number.parseInt(v, 10);
|
|
68
|
+
if (Number.isFinite(n) && n > 0) timeoutPathBMs = n;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { 'timeout-path-b-ms': timeoutPathBMs };
|
|
74
|
+
}
|