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
|
@@ -173,6 +173,53 @@
|
|
|
173
173
|
* confused" as a bypass strategy. This is the one place the guard is closed
|
|
174
174
|
* rather than open, and it is closed because the alternative is a silent skip.
|
|
175
175
|
*
|
|
176
|
+
* ## Interpreter eval payloads (GitLab #1408)
|
|
177
|
+
*
|
|
178
|
+
* The blind spot this file used to NAME became the habitual shape. On
|
|
179
|
+
* 2026-09-19 a coordinator appended its own `sessions.jsonl` record with
|
|
180
|
+
* `node --input-type=module -e "… appendFileSync('.orchestrator/metrics/
|
|
181
|
+
* sessions.jsonl', JSON.stringify(rec) + '\n')"`; `emit-session.mjs` never ran
|
|
182
|
+
* that session, and the record landed with `ended_at` instead of `completed_at`
|
|
183
|
+
* and four required fields missing. Measured over 58 transcripts of this repo:
|
|
184
|
+
* 438 Bash commands mention the ledger, the SHELL write shapes above occurred
|
|
185
|
+
* **0 times**, and 46 are interpreter one-liners. Fleet-wide: 40 interpreter
|
|
186
|
+
* write calls, 38 with the ledger path as the FIRST argument. The guard was
|
|
187
|
+
* aimed at a shape nobody uses.
|
|
188
|
+
*
|
|
189
|
+
* What is covered now: ONE predicate (`findInterpreterLedgerWrite`), hung off
|
|
190
|
+
* the SAME per-segment loop as every other matcher here, so scoping is per
|
|
191
|
+
* STATEMENT — `node -e '<read>' ; echo ok` allows, `cmd && node -e '<append>'`
|
|
192
|
+
* denies on the second statement alone. It takes the eval-flag operand
|
|
193
|
+
* (`-e`/`--eval`/`-p`/`--print`, `python -c`, `perl -e`, `ruby -e`,
|
|
194
|
+
* `deno eval`, in separated / attached / `=`-joined spelling) and denies iff a
|
|
195
|
+
* write call from a CLOSED list (`appendFile(Sync)`, `writeFile(Sync)`,
|
|
196
|
+
* `createWriteStream`; Python `open(…, 'a'|'w'|'x'|…)`; Ruby `File.write` /
|
|
197
|
+
* `IO.write` / `File.open`; Perl's two `open` forms) has the literal basename
|
|
198
|
+
* `sessions.jsonl` in its FIRST ARGUMENT TEXT.
|
|
199
|
+
*
|
|
200
|
+
* The first-argument scoping is load-bearing, not an optimisation: keying on
|
|
201
|
+
* mere co-occurrence denies all three of the ledger READS measured in this
|
|
202
|
+
* repo's transcripts (each reads the ledger and writes
|
|
203
|
+
* `.orchestrator/tmp/session-entry.json` before piping into
|
|
204
|
+
* `emit-session.mjs`), and denies `readFileSync('…/sessions.jsonl')` +
|
|
205
|
+
* `writeFileSync('/tmp/x')` in one one-liner.
|
|
206
|
+
*
|
|
207
|
+
* KNOWN LIMITS — these stay limits, deliberately (BV-004):
|
|
208
|
+
* - The path held in a VARIABLE: `const p = '…/sessions.jsonl';
|
|
209
|
+
* appendFileSync(p, …)`. No constant propagation here; the first-argument
|
|
210
|
+
* text carries no basename.
|
|
211
|
+
* - A here-doc-fed interpreter (`node <<'EOF' … EOF`) — the body is skipped
|
|
212
|
+
* upstream by `scanCommand`, so no payload reaches this predicate.
|
|
213
|
+
* - An invoked SCRIPT FILE (`node tools/append.mjs`) — the ledger name is not
|
|
214
|
+
* in the command string at all.
|
|
215
|
+
* - A write API outside the closed list (`fs.open` + `write`, a stream from a
|
|
216
|
+
* helper, `Deno.writeTextFile`), and Python kwargs-only `open(file=…,
|
|
217
|
+
* mode='a')`.
|
|
218
|
+
* REVISIT TRIGGER: re-run the transcript census (`rg` for interpreter
|
|
219
|
+
* one-liners naming the ledger). If a shape from the four bullets above
|
|
220
|
+
* appears in ANY real transcript, widen the predicate then — not before. A
|
|
221
|
+
* guard widened against imagined shapes buys false positives at full price.
|
|
222
|
+
*
|
|
176
223
|
* ## What this does NOT catch (stated plainly — the bounds are the contract)
|
|
177
224
|
*
|
|
178
225
|
* - Indirection through a variable: `>> "$LEDGER"`, `tee "$LEDGER"`,
|
|
@@ -193,19 +240,37 @@
|
|
|
193
240
|
* buried in a quoted substitution is not the accident shape this guard is
|
|
194
241
|
* for — it is the "determined circumvention" line below.
|
|
195
242
|
* - In-place editors: `sed -i`, `perl -i`, `ed`, an interactive editor.
|
|
196
|
-
* - A write performed
|
|
197
|
-
* `
|
|
198
|
-
*
|
|
199
|
-
* `
|
|
200
|
-
*
|
|
201
|
-
*
|
|
243
|
+
* - A write performed by a SCRIPT FILE the command invokes
|
|
244
|
+
* (`node tools/append.mjs`) — the ledger name never appears in the command
|
|
245
|
+
* string at all, so nothing here can see it. Interpreter EVAL payloads
|
|
246
|
+
* (`node -e`, `python3 -c`) used to sit in this bullet too; since #1408 they
|
|
247
|
+
* are covered — see "Interpreter eval payloads" below for the exact bound.
|
|
248
|
+
* - A here-doc-fed interpreter (`node <<'EOF' … EOF`): here-doc BODIES are
|
|
249
|
+
* skipped as the data they are, so the payload never reaches the matcher.
|
|
250
|
+
* - A shell `-c` payload the segment does not NAME a shell for. Since
|
|
251
|
+
* 2026-09-20 a `-c` operand following a `bash`/`sh`/`zsh`/`dash`/`ksh`/`su`
|
|
252
|
+
* TOKEN is re-entered by the same matcher (`bash -c '… >> …/sessions.jsonl'`
|
|
253
|
+
* and `xargs -I{} sh -c '…'` both deny), but three shapes stay out:
|
|
254
|
+
* `ssh host '… >> …/sessions.jsonl'` (a REMOTE ledger — not this repo's
|
|
255
|
+
* file, so out of scope by nature, not by omission); a payload whose shell
|
|
256
|
+
* is only reachable through expansion (`"$SHELL" -c …`, `eval "$CMD"`);
|
|
257
|
+
* and a payload nested past MAX_PAYLOAD_DEPTH (2) — that cut is marked, not
|
|
258
|
+
* silent, when the dropped payload mentions the ledger (#998 item 2).
|
|
259
|
+
* This bullet REPLACES the one #1408 deleted (it named
|
|
260
|
+
* `bash -c '… >> …/sessions.jsonl'` as uncovered, which was true, and the
|
|
261
|
+
* replacement bullet narrowed the claim to script files and here-docs while
|
|
262
|
+
* the hole stayed open — a guard may have limits, it may not misstate them).
|
|
263
|
+
* REVISIT TRIGGER: re-run the transcript census for interpreter one-liners
|
|
264
|
+
* naming the ledger; widen only if one of these three shapes appears in a
|
|
265
|
+
* REAL transcript.
|
|
202
266
|
* - Obfuscation: `eval`, `base64 -d | sh`, a here-doc-fed shell. Here-doc
|
|
203
267
|
* BODIES are skipped as the data they are — `bash <<EOF … EOF` therefore
|
|
204
268
|
* hides its payload from this matcher by construction.
|
|
205
269
|
* - `sponge`, `install`, `rsync`, `awk > file` and other less common write
|
|
206
270
|
* verbs (a redirect inside an `awk` program string is quoted data here).
|
|
207
|
-
* - The Write/Edit tools — a different PreToolUse matcher entirely
|
|
208
|
-
*
|
|
271
|
+
* - The Write/Edit/MultiEdit tools — a different PreToolUse matcher entirely,
|
|
272
|
+
* guarded by `hooks/pre-edit-sessions-ledger-guard.mjs` (#1443), not this
|
|
273
|
+
* hook's surface.
|
|
209
274
|
* - The #385 repair-apply subshell/backtick deny (`unwrapSubshellLayers`)
|
|
210
275
|
* only unwraps a command that IS, in its entirety, one wrapped run —
|
|
211
276
|
* `x=$(node scripts/repair-invalid-sessions.mjs --apply)` (the wrapper is
|
|
@@ -249,8 +314,7 @@ import path from 'node:path';
|
|
|
249
314
|
import { pathToFileURL } from 'node:url';
|
|
250
315
|
|
|
251
316
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
252
|
-
|
|
253
|
-
if (!shouldRunHook('pre-bash-sessions-ledger-guard')) process.exit(0);
|
|
317
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
254
318
|
|
|
255
319
|
// ---------------------------------------------------------------------------
|
|
256
320
|
// #993 — late-bound repo dependencies
|
|
@@ -324,8 +388,9 @@ const GUARD_CONSEQUENCE = {
|
|
|
324
388
|
' COMMITTED (HEAD) command lexer — any uncommitted change to that file is NOT in effect.',
|
|
325
389
|
],
|
|
326
390
|
inactive: [
|
|
327
|
-
' Consequence: a direct
|
|
328
|
-
' (>, >>, tee, dd of=, cp/mv destination
|
|
391
|
+
' Consequence: a direct write into .orchestrator/metrics/sessions.jsonl',
|
|
392
|
+
' (>, >>, tee, dd of=, cp/mv destination, or an interpreter payload such as',
|
|
393
|
+
' node -e / python -c — #1408) is NOT being blocked. This hook is a',
|
|
329
394
|
' fail-open nudge, not a security boundary — but repair it so the #958 corruption',
|
|
330
395
|
' class stays caught.',
|
|
331
396
|
],
|
|
@@ -859,6 +924,293 @@ function scanCommand(command) {
|
|
|
859
924
|
return { targets, sanitized: out, balanced: true };
|
|
860
925
|
}
|
|
861
926
|
|
|
927
|
+
// ---------------------------------------------------------------------------
|
|
928
|
+
// Interpreter eval payloads (#1408)
|
|
929
|
+
//
|
|
930
|
+
// ONE predicate, hung off the EXISTING per-segment loop in findWriteVerbTarget.
|
|
931
|
+
// Deliberately NOT a Bash parser and deliberately NOT in command-blocker.mjs:
|
|
932
|
+
// that module has five other consumers, and a JS payload re-entered by its SHELL
|
|
933
|
+
// matcher would be misread as shell.
|
|
934
|
+
// ---------------------------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Interpreter verbs whose eval payload is read, mapped to the DIALECT its code
|
|
938
|
+
* is judged in. `node`/`bun`/`deno` share the `fs` API surface, so one dialect
|
|
939
|
+
* covers all three.
|
|
940
|
+
*/
|
|
941
|
+
const INTERPRETER_FAMILY = new Map([
|
|
942
|
+
['node', 'js'],
|
|
943
|
+
['nodejs', 'js'],
|
|
944
|
+
['bun', 'js'],
|
|
945
|
+
['deno', 'js'],
|
|
946
|
+
['python', 'python'],
|
|
947
|
+
['python2', 'python'],
|
|
948
|
+
['python3', 'python'],
|
|
949
|
+
['perl', 'perl'],
|
|
950
|
+
['ruby', 'ruby'],
|
|
951
|
+
]);
|
|
952
|
+
|
|
953
|
+
/** Flags whose operand is CODE, not a script path — per dialect. */
|
|
954
|
+
const INTERPRETER_EVAL_FLAGS = {
|
|
955
|
+
js: new Set(['-e', '--eval', '-p', '--print']),
|
|
956
|
+
python: new Set(['-c']),
|
|
957
|
+
perl: new Set(['-e', '-E']),
|
|
958
|
+
ruby: new Set(['-e']),
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* CLOSED list of write calls per dialect. Nothing outside it is matched — a
|
|
963
|
+
* matcher keyed on "the payload mentions the ledger AND writes something" was
|
|
964
|
+
* measured to false-positive on all three real read one-liners in this repo's
|
|
965
|
+
* transcripts (they read the ledger and write `.orchestrator/tmp/…`).
|
|
966
|
+
*/
|
|
967
|
+
const INTERPRETER_WRITE_CALLS = {
|
|
968
|
+
js: ['appendFileSync', 'appendFile', 'writeFileSync', 'writeFile', 'createWriteStream'],
|
|
969
|
+
python: ['open'],
|
|
970
|
+
perl: ['open'],
|
|
971
|
+
ruby: ['File.write', 'IO.write', 'File.open'],
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Collect the eval-payload operands of one interpreter invocation.
|
|
976
|
+
*
|
|
977
|
+
* `deno` is the odd one: its eval mode is a SUBCOMMAND (`deno eval '<code>'`),
|
|
978
|
+
* not a flag. Everything else takes `-e`/`-c`-style flags in three spellings —
|
|
979
|
+
* separated (`-e '<code>'`), attached (`-e'<code>'`) and `=`-joined
|
|
980
|
+
* (`--eval='<code>'`).
|
|
981
|
+
*
|
|
982
|
+
* A QUOTED `-e` token is still treated as the flag: bash passes `'-e'` and `-e`
|
|
983
|
+
* identically, so skipping the quoted spelling would be a one-character bypass.
|
|
984
|
+
*
|
|
985
|
+
* @param {string} verb
|
|
986
|
+
* @param {string} family
|
|
987
|
+
* @param {Array<{ text: string, quoted: boolean }>} args - tokens after the verb
|
|
988
|
+
* @returns {string[]}
|
|
989
|
+
*/
|
|
990
|
+
function collectCodeOperands(verb, family, args) {
|
|
991
|
+
const operands = [];
|
|
992
|
+
if (verb === 'deno') {
|
|
993
|
+
const at = args.findIndex((a) => a.text === 'eval');
|
|
994
|
+
if (at === -1) return operands;
|
|
995
|
+
for (const a of args.slice(at + 1)) {
|
|
996
|
+
if (!a.quoted && a.text.startsWith('-')) continue;
|
|
997
|
+
operands.push(a.text);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
return operands;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const flags = INTERPRETER_EVAL_FLAGS[family];
|
|
1004
|
+
if (!flags) return operands;
|
|
1005
|
+
for (let i = 0; i < args.length; i++) {
|
|
1006
|
+
const text = args[i].text;
|
|
1007
|
+
const eq = text.indexOf('=');
|
|
1008
|
+
if (eq > 0 && flags.has(text.slice(0, eq))) { operands.push(text.slice(eq + 1)); continue; }
|
|
1009
|
+
if (flags.has(text)) {
|
|
1010
|
+
if (i + 1 < args.length) { operands.push(args[i + 1].text); i++; }
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
for (const flag of flags) {
|
|
1014
|
+
if (flag.length === 2 && !text.startsWith('--') && text.length > 2 && text.startsWith(flag)) {
|
|
1015
|
+
operands.push(text.slice(2));
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return operands;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Indices of the `(` that OPENS a call to `name` in `code`.
|
|
1025
|
+
*
|
|
1026
|
+
* Identifier-boundary aware on the left, so `appendFile` does not match inside
|
|
1027
|
+
* `appendFileSync` (which is a separate list entry) and `myWriteFile` is not a
|
|
1028
|
+
* `writeFile`. A receiver prefix is fine — `.` is not an identifier char, so
|
|
1029
|
+
* `fs.appendFileSync` and `require('fs').appendFileSync` both resolve.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {string} code
|
|
1032
|
+
* @param {string} name
|
|
1033
|
+
* @returns {number[]}
|
|
1034
|
+
*/
|
|
1035
|
+
function findCallOpenings(code, name) {
|
|
1036
|
+
const out = [];
|
|
1037
|
+
for (let from = 0; ;) {
|
|
1038
|
+
const idx = code.indexOf(name, from);
|
|
1039
|
+
if (idx === -1) return out;
|
|
1040
|
+
from = idx + name.length;
|
|
1041
|
+
const before = idx > 0 ? code[idx - 1] : '';
|
|
1042
|
+
if (before && /[A-Za-z0-9_$]/.test(before)) continue;
|
|
1043
|
+
let j = from;
|
|
1044
|
+
while (j < code.length && (code[j] === ' ' || code[j] === '\t' || code[j] === '\n')) j++;
|
|
1045
|
+
if (code[j] === '(') out.push(j);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Split the argument list opening at `open` on TOP-LEVEL commas.
|
|
1051
|
+
*
|
|
1052
|
+
* Quote- and nesting-aware, single pass, no backtracking regex — this runs on
|
|
1053
|
+
* every Bash call of every session, and a path that can throw or hang reads as
|
|
1054
|
+
* NO decision under the exit-0 protocol, i.e. fail-OPEN in the wrong direction.
|
|
1055
|
+
* Returns `null` when the list never closes (judged as "no hit", never a throw).
|
|
1056
|
+
*
|
|
1057
|
+
* @param {string} code
|
|
1058
|
+
* @param {number} open - index of the opening `(`
|
|
1059
|
+
* @returns {string[]|null}
|
|
1060
|
+
*/
|
|
1061
|
+
function splitCallArgs(code, open) {
|
|
1062
|
+
const args = [];
|
|
1063
|
+
let cur = '';
|
|
1064
|
+
let depth = 0;
|
|
1065
|
+
let quote = null;
|
|
1066
|
+
for (let i = open; i < code.length; i++) {
|
|
1067
|
+
const ch = code[i];
|
|
1068
|
+
if (quote !== null) {
|
|
1069
|
+
if (ch === '\\' && i + 1 < code.length) { cur += ch + code[i + 1]; i++; continue; }
|
|
1070
|
+
if (ch === quote) quote = null;
|
|
1071
|
+
cur += ch;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (ch === '"' || ch === "'" || ch === '`') { quote = ch; cur += ch; continue; }
|
|
1075
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
1076
|
+
depth++;
|
|
1077
|
+
if (depth > 1) cur += ch;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
1081
|
+
depth--;
|
|
1082
|
+
if (depth === 0) { args.push(cur); return args; }
|
|
1083
|
+
cur += ch;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (ch === ',' && depth === 1) { args.push(cur); cur = ''; continue; }
|
|
1087
|
+
cur += ch;
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Does any operand after the first name a WRITE mode?
|
|
1094
|
+
*
|
|
1095
|
+
* `open(p, 'a')` / `open(p, mode='w')` / `File.open(p, 'a')` write; `open(p)`
|
|
1096
|
+
* and `open(p, 'r')` read. `+` counts (`'r+'` writes).
|
|
1097
|
+
*
|
|
1098
|
+
* @param {string[]} args
|
|
1099
|
+
* @returns {boolean}
|
|
1100
|
+
*/
|
|
1101
|
+
function hasWriteModeOperand(args) {
|
|
1102
|
+
for (let i = 1; i < args.length; i++) {
|
|
1103
|
+
const m = /^\s*(?:mode\s*=\s*)?(['"])([^'"]*)\1\s*$/.exec(args[i]);
|
|
1104
|
+
if (m && /[awx+]/.test(m[2])) return true;
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Judge ONE resolved write call. The FIRST-ARGUMENT scoping is load-bearing:
|
|
1111
|
+
* keying on mere co-occurrence of a write verb and the ledger basename anywhere
|
|
1112
|
+
* in the payload denies `readFileSync('…/sessions.jsonl')` +
|
|
1113
|
+
* `writeFileSync('/tmp/x')` in one one-liner — the shape all three measured
|
|
1114
|
+
* ledger READS in this repo's transcripts actually have.
|
|
1115
|
+
*
|
|
1116
|
+
* Perl is the exception, by its own grammar: `open(FH, '>>', $p)` puts the path
|
|
1117
|
+
* THIRD and `open(FH, ">>$p")` glues mode to path. A read open never carries a
|
|
1118
|
+
* `>`, so "the ledger is an operand AND some operand carries `>`" is exact
|
|
1119
|
+
* rather than a guess.
|
|
1120
|
+
*
|
|
1121
|
+
* @param {string} family
|
|
1122
|
+
* @param {string} name
|
|
1123
|
+
* @param {string[]} args
|
|
1124
|
+
* @returns {string|null} the offending argument text, or null
|
|
1125
|
+
*/
|
|
1126
|
+
function judgeInterpreterCall(family, name, args) {
|
|
1127
|
+
if (args.length === 0) return null;
|
|
1128
|
+
if (family === 'perl') {
|
|
1129
|
+
const hit = args.find((a) => a.includes(LEDGER_BASENAME));
|
|
1130
|
+
if (!hit) return null;
|
|
1131
|
+
return args.some((a) => a.includes('>')) ? hit : null;
|
|
1132
|
+
}
|
|
1133
|
+
if (!args[0].includes(LEDGER_BASENAME)) return null;
|
|
1134
|
+
if (family === 'python' || name === 'File.open') {
|
|
1135
|
+
return hasWriteModeOperand(args) ? args[0] : null;
|
|
1136
|
+
}
|
|
1137
|
+
return args[0];
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* The #1408 predicate: does this interpreter invocation write the ledger from
|
|
1142
|
+
* INSIDE its eval payload?
|
|
1143
|
+
*
|
|
1144
|
+
* Called from the per-segment loop of {@link findWriteVerbTarget}, so scoping is
|
|
1145
|
+
* per STATEMENT: `node -e '<read>' ; echo ok` is judged on the statement that
|
|
1146
|
+
* hit, and `cmd && node -e '<append>'` denies on the second one alone.
|
|
1147
|
+
*
|
|
1148
|
+
* @param {string} verb - resolved segment verb (basename, wrappers already peeled)
|
|
1149
|
+
* @param {Array<{ text: string, quoted: boolean }>} args - tokens after the verb
|
|
1150
|
+
* @returns {string|null} the offending target, or null
|
|
1151
|
+
*/
|
|
1152
|
+
function findInterpreterLedgerWrite(verb, args) {
|
|
1153
|
+
const family = INTERPRETER_FAMILY.get(verb);
|
|
1154
|
+
if (!family) return null;
|
|
1155
|
+
for (const code of collectCodeOperands(verb, family, args)) {
|
|
1156
|
+
if (typeof code !== 'string' || !code.includes(LEDGER_BASENAME)) continue;
|
|
1157
|
+
for (const name of INTERPRETER_WRITE_CALLS[family]) {
|
|
1158
|
+
for (const open of findCallOpenings(code, name)) {
|
|
1159
|
+
const callArgs = splitCallArgs(code, open);
|
|
1160
|
+
if (!callArgs) continue;
|
|
1161
|
+
const hit = judgeInterpreterCall(family, name, callArgs);
|
|
1162
|
+
if (hit) return `${hit.trim()} (${name}(…) inside a ${verb} eval payload)`;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Shells whose `-c <payload>` operand is a command line the shell EXECUTES.
|
|
1171
|
+
*
|
|
1172
|
+
* DELIBERATE DUPLICATE of `DASH_C_SHELLS` in scripts/lib/command-blocker.mjs,
|
|
1173
|
+
* which does not export it (nor its `dashCPayloads`). Change one, change both.
|
|
1174
|
+
*/
|
|
1175
|
+
const DASH_C_SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'su']);
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Collect the command lines a shell nested in this segment will EXECUTE, so
|
|
1179
|
+
* {@link findWriteVerbTarget} can re-enter the SAME matcher on them.
|
|
1180
|
+
*
|
|
1181
|
+
* `resolveSegmentVerb` reports WRAPPER payloads (`env -S '…'`) and this hook
|
|
1182
|
+
* has recursed on those since #982 — but a `bash -c` payload is not a wrapper
|
|
1183
|
+
* payload, and command-blocker's own `dedupedSegmentPayloads` (the piece that
|
|
1184
|
+
* adds them) is internal to that module. So the guard saw `bash -c` as the
|
|
1185
|
+
* verb `bash` with an inert quoted operand and ALLOWED, while the identical
|
|
1186
|
+
* unwrapped command DENIED — one word of bypass, measured 2026-09-20.
|
|
1187
|
+
*
|
|
1188
|
+
* Keyed on a shell TOKEN anywhere in the segment rather than on the resolved
|
|
1189
|
+
* verb: that is what reaches `xargs -I{} sh -c "…"` (verb `xargs`) and
|
|
1190
|
+
* `find … -exec sh -c "…" \;`, and it keeps `grep -c <pattern>` out, because
|
|
1191
|
+
* `grep` is not a shell name. Recursion is bounded by MAX_PAYLOAD_DEPTH, and a
|
|
1192
|
+
* payload that carries no write intent still ALLOWs — the false-positive floor
|
|
1193
|
+
* (`bash -c "jq . <ledger>"`, `bash -c "tail -1 <ledger> | jq ."`) is judged by
|
|
1194
|
+
* the same matcher as its unwrapped form, not by a second, weaker one.
|
|
1195
|
+
*
|
|
1196
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
1197
|
+
* @returns {string[]} distinct payload strings
|
|
1198
|
+
*/
|
|
1199
|
+
function dashCShellPayloads(segment) {
|
|
1200
|
+
const out = new Set();
|
|
1201
|
+
for (let i = 0; i < segment.length - 1; i++) {
|
|
1202
|
+
const tok = segment[i];
|
|
1203
|
+
if (tok.quoted || !DASH_C_SHELLS.has(path.basename(tok.text))) continue;
|
|
1204
|
+
for (let j = i + 1; j < segment.length - 1; j++) {
|
|
1205
|
+
if (/^-[A-Za-z]*c$/.test(segment[j].text)) {
|
|
1206
|
+
out.add(segment[j + 1].text);
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return [...out];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
862
1214
|
/**
|
|
863
1215
|
* Find a non-redirect write verb (`tee`, `dd of=`, `cp`/`mv` destination)
|
|
864
1216
|
* whose target is the ledger.
|
|
@@ -923,6 +1275,12 @@ function findWriteVerbTarget(command, depth = 0, marks = []) {
|
|
|
923
1275
|
}
|
|
924
1276
|
}
|
|
925
1277
|
|
|
1278
|
+
// A shell `-c` payload is a command line too, and until 2026-09-20 it was
|
|
1279
|
+
// the guard's one-word bypass: `bash -c "echo x >> <ledger>"` ALLOWED while
|
|
1280
|
+
// `echo x >> <ledger>` DENIED. See dashCShellPayloads for why this is keyed
|
|
1281
|
+
// on a shell token rather than the resolved verb.
|
|
1282
|
+
for (const p of dashCShellPayloads(segment)) payloadSet.add(p);
|
|
1283
|
+
|
|
926
1284
|
// `env -S 'tee -a <ledger>'` hides a whole command line in one operand.
|
|
927
1285
|
// Recurse on the payload with the SAME matcher rather than a second,
|
|
928
1286
|
// weaker one — bounded by MAX_PAYLOAD_DEPTH. Kept BEFORE the verb dispatch
|
|
@@ -951,6 +1309,12 @@ function findWriteVerbTarget(command, depth = 0, marks = []) {
|
|
|
951
1309
|
// other's (the lib docblock's positional-walk caveat).
|
|
952
1310
|
const args = segment.slice(index + 1);
|
|
953
1311
|
|
|
1312
|
+
// #1408 — the write happens INSIDE an interpreter payload (`node -e`,
|
|
1313
|
+
// `python3 -c`, …), where there is no shell write intent to resolve. One
|
|
1314
|
+
// predicate, judged on THIS statement; see findInterpreterLedgerWrite.
|
|
1315
|
+
const interpreterTarget = findInterpreterLedgerWrite(verb, args);
|
|
1316
|
+
if (interpreterTarget) return interpreterTarget;
|
|
1317
|
+
|
|
954
1318
|
// The repair CLI mutates the ledger from inside Node, so there is no
|
|
955
1319
|
// redirect target for the structural matcher to see. Match only its
|
|
956
1320
|
// effective apply contract at the resolved token seam: `node`, the
|
|
@@ -1158,13 +1522,39 @@ async function main() {
|
|
|
1158
1522
|
? `${target.slice(0, TARGET_ECHO_MAX)}… (${target.length} chars)`
|
|
1159
1523
|
: target;
|
|
1160
1524
|
|
|
1525
|
+
// #1442 — a repair-apply VARIANT (extra flag, wrapper, env prefix) reached
|
|
1526
|
+
// here because it is not the exact sanctioned string. Text only, no matching
|
|
1527
|
+
// change: name the one command that IS allowed, and say why the most common
|
|
1528
|
+
// re-spelling cannot work — this hook reads SO_DISABLED_HOOKS from its OWN
|
|
1529
|
+
// process env (hooks/_lib/profile-gate.mjs), never from the command text, and
|
|
1530
|
+
// a leading `VAR=val` token also breaks the exact match above.
|
|
1531
|
+
if (target.startsWith(REPAIR_APPLY_MARKER)) {
|
|
1532
|
+
emitDeny(
|
|
1533
|
+
[
|
|
1534
|
+
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1535
|
+
`The sanctioned repair invocation is allowed ONLY as the exact bare command:`,
|
|
1536
|
+
` ${REPAIR_APPLY_CANONICAL_COMMAND}`,
|
|
1537
|
+
`Any variant — an extra flag, a wrapper, an env prefix — is denied.`,
|
|
1538
|
+
`A SO_DISABLED_HOOKS=… prefix INSIDE the command has no effect: this hook reads`,
|
|
1539
|
+
`it from the environment of the whole session, never from the command text.`,
|
|
1540
|
+
`Override (intentional maintenance only, operator-side — e.g. started with \`!\`):`,
|
|
1541
|
+
`SO_DISABLED_HOOKS=pre-bash-sessions-ledger-guard as the environment of the session`,
|
|
1542
|
+
`See: GitLab #958, #1442, skills/session-end/session-metrics-write.md`,
|
|
1543
|
+
].join('\n'),
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1161
1548
|
emitDeny(
|
|
1162
1549
|
[
|
|
1163
1550
|
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1164
1551
|
`The ledger is append-only through its validating writer:`,
|
|
1165
1552
|
` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
|
|
1166
|
-
`
|
|
1167
|
-
`
|
|
1553
|
+
`After the writer succeeds, verify the record (read-only; does not repair):`,
|
|
1554
|
+
` node scripts/check-sessions-integrity.mjs --session-id '<semantic-id>'`,
|
|
1555
|
+
`Hand-composing a record and appending it — with a shell redirect, or from`,
|
|
1556
|
+
`inside an interpreter (node -e / python -c) — skips schema validation. That is`,
|
|
1557
|
+
`exactly how the malformed records in GitLab #958 and #1408 landed.`,
|
|
1168
1558
|
`Override (intentional maintenance only): run the session with`,
|
|
1169
1559
|
`SO_DISABLED_HOOKS=pre-bash-sessions-ledger-guard`,
|
|
1170
1560
|
`See: GitLab #958, skills/session-end/session-metrics-write.md`,
|
|
@@ -1203,11 +1593,18 @@ try {
|
|
|
1203
1593
|
process.exit(0); // fail-open, but no longer fail-silent
|
|
1204
1594
|
}
|
|
1205
1595
|
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1596
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
1597
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
1598
|
+
if (isMainModule(import.meta.url)) {
|
|
1599
|
+
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
1600
|
+
if (!shouldRunHook('pre-bash-sessions-ledger-guard')) process.exit(0);
|
|
1601
|
+
|
|
1602
|
+
// Top-level error handler — fail-OPEN (see the module docblock). Never let a
|
|
1603
|
+
// non-zero exit leak: on this protocol exit 0 + empty stdout is "no decision".
|
|
1604
|
+
main().catch((e) => {
|
|
1605
|
+
process.stderr.write(
|
|
1606
|
+
`⚠ pre-bash-sessions-ledger-guard: internal error — ${e?.message || e}\n`,
|
|
1607
|
+
);
|
|
1608
|
+
process.exit(0);
|
|
1609
|
+
});
|
|
1610
|
+
}
|