session-orchestrator 3.16.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/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* post-tooluse-frontend-slop.mjs — PostToolUse hook that runs the deterministic
|
|
4
|
+
* frontend-slop detector on a UI file right after it is edited.
|
|
5
|
+
*
|
|
6
|
+
* Issue #684 (Track 2 / item B). OPT-IN, warn-only, NON-BLOCKING. PostToolUse
|
|
7
|
+
* fires AFTER the edit is already applied, so this hook can never block — it
|
|
8
|
+
* only surfaces an `additionalContext` roll-up of detector findings so the
|
|
9
|
+
* agent sees the slop signal on its next turn. Mirrors hooks/loop-guard.mjs.
|
|
10
|
+
*
|
|
11
|
+
* Decision flow:
|
|
12
|
+
* 1. shouldRunHook('frontend-slop-hook') gate — exit 0 when disabled via profile.
|
|
13
|
+
* 2. Read JSON payload from stdin (null-on-failure; never throws).
|
|
14
|
+
* 3. tool_name gate: proceed only for Edit | Write | MultiEdit, else exit 0.
|
|
15
|
+
* 4. file_path gate: exit 0 if absent / non-string.
|
|
16
|
+
* 5. Extension gate: exit 0 unless path.extname(file) ∈ SCANNABLE_EXTS (SSOT).
|
|
17
|
+
* 6. Session Config gate: exit 0 unless `frontend-slop-hook.enabled === true`
|
|
18
|
+
* (OPT-IN — default off).
|
|
19
|
+
* 7. detectFiles([file]) (fail-soft). No findings → exit 0 silently.
|
|
20
|
+
* 8. Output: additionalContext roll-up on stdout (NEVER blocks). Best-effort
|
|
21
|
+
* orchestrator.frontend_slop.warning event.
|
|
22
|
+
*
|
|
23
|
+
* Exit codes: 0 always (informational, never blocking).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
27
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
28
|
+
if (!shouldRunHook('frontend-slop-hook')) process.exit(0);
|
|
29
|
+
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
|
|
32
|
+
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
33
|
+
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
34
|
+
import { detectFiles, SCANNABLE_EXTS } from '../scripts/lib/frontend-detect/detect.mjs';
|
|
35
|
+
import { loadFrontendSlopHookConfig } from '../scripts/lib/config/frontend-slop-hook.mjs';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// stdin reading (inline null-on-failure — PostToolUse hooks never throw)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read stdin to EOF (best-effort). Returns parsed JSON or null on failure.
|
|
43
|
+
* Uses a 5 s timeout consistent with the Claude Code hook contract.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<object|null>}
|
|
46
|
+
*/
|
|
47
|
+
function readStdinJson() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
if (process.stdin.readableEnded || process.stdin.closed) {
|
|
50
|
+
resolve(null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const chunks = [];
|
|
54
|
+
const timer = setTimeout(() => { resolve(null); }, 5_000);
|
|
55
|
+
process.stdin.setEncoding('utf8');
|
|
56
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
57
|
+
process.stdin.on('end', () => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
const raw = chunks.join('').trim();
|
|
60
|
+
if (!raw) { resolve(null); return; }
|
|
61
|
+
try { resolve(JSON.parse(raw)); } catch { resolve(null); }
|
|
62
|
+
});
|
|
63
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(null); });
|
|
64
|
+
process.stdin.resume();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// helpers
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** Tool names whose payloads carry an editable file path. */
|
|
73
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the session id from the hook payload. Precedence:
|
|
77
|
+
* session_id → parent_session_id → null.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} input
|
|
80
|
+
* @returns {string|null}
|
|
81
|
+
*/
|
|
82
|
+
function resolveSessionId(input) {
|
|
83
|
+
if (typeof input.session_id === 'string' && input.session_id.length > 0) {
|
|
84
|
+
return input.session_id;
|
|
85
|
+
}
|
|
86
|
+
if (typeof input.parent_session_id === 'string' && input.parent_session_id.length > 0) {
|
|
87
|
+
return input.parent_session_id;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render the file path relative to the project root for display, falling back
|
|
94
|
+
* to the basename when it lives outside the root (e.g. a temp sandbox).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} filePath — absolute or cwd-relative file path
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function toRelPath(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
const rel = path.relative(SO_PROJECT_DIR, filePath);
|
|
102
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel;
|
|
103
|
+
} catch { /* fall through */ }
|
|
104
|
+
return path.basename(filePath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a compact one-line warning roll-up from the detector findings.
|
|
109
|
+
*
|
|
110
|
+
* @param {import('../scripts/lib/frontend-detect/detect.mjs').Finding[]} findings
|
|
111
|
+
* @param {string} relPath
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function buildWarning(findings, relPath) {
|
|
115
|
+
const total = findings.length;
|
|
116
|
+
// Show up to the first 5 findings as "<rule-id> L<line>"; aggregate fpRisk.
|
|
117
|
+
const shown = findings.slice(0, 5).map((f) => `${f.rule} L${f.line}`);
|
|
118
|
+
const more = total > shown.length ? `, +${total - shown.length} more` : '';
|
|
119
|
+
const fpRisks = [...new Set(findings.map((f) => f.fpRisk))];
|
|
120
|
+
return (
|
|
121
|
+
`⚠ frontend-slop: ${total} finding(s) in ${relPath} — ` +
|
|
122
|
+
`${shown.join(', ')}${more} (fpRisk: ${fpRisks.join('/')}). ` +
|
|
123
|
+
`See rules/opt-in-stack/frontend.md`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// main
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const input = await readStdinJson();
|
|
133
|
+
// No payload at all → nothing to scan. Exit 0 (handled in finally).
|
|
134
|
+
if (!input) return;
|
|
135
|
+
|
|
136
|
+
const toolName = typeof input.tool_name === 'string' ? input.tool_name : null;
|
|
137
|
+
// Only edit-shaped tools carry a file path worth scanning.
|
|
138
|
+
if (!toolName || !EDIT_TOOLS.has(toolName)) return;
|
|
139
|
+
|
|
140
|
+
const toolInput = input.tool_input;
|
|
141
|
+
const filePath =
|
|
142
|
+
toolInput && typeof toolInput.file_path === 'string' ? toolInput.file_path : null;
|
|
143
|
+
if (!filePath) return;
|
|
144
|
+
|
|
145
|
+
// Extension gate — reuse the detector's SSOT set, never re-list extensions.
|
|
146
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
147
|
+
if (!SCANNABLE_EXTS.has(ext)) return;
|
|
148
|
+
|
|
149
|
+
// Session Config gate — OPT-IN (default off).
|
|
150
|
+
const config = await loadFrontendSlopHookConfig({ repoRoot: SO_PROJECT_DIR });
|
|
151
|
+
if (config.enabled !== true) return;
|
|
152
|
+
|
|
153
|
+
// Run the deterministic detector (fail-soft: unreadable files → []).
|
|
154
|
+
const findings = detectFiles([filePath]);
|
|
155
|
+
if (findings.length === 0) return;
|
|
156
|
+
|
|
157
|
+
const relPath = toRelPath(filePath);
|
|
158
|
+
const sessionId = resolveSessionId(input);
|
|
159
|
+
|
|
160
|
+
// Best-effort telemetry — must never block the hook.
|
|
161
|
+
const high = findings.filter((f) => f.severity === 'high').length;
|
|
162
|
+
const byRule = {};
|
|
163
|
+
for (const f of findings) byRule[f.rule] = (byRule[f.rule] ?? 0) + 1;
|
|
164
|
+
try {
|
|
165
|
+
await emitEvent('orchestrator.frontend_slop.warning', {
|
|
166
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
167
|
+
file: relPath,
|
|
168
|
+
total: findings.length,
|
|
169
|
+
high,
|
|
170
|
+
by_rule: byRule,
|
|
171
|
+
});
|
|
172
|
+
} catch { /* best-effort — hook must remain non-blocking */ }
|
|
173
|
+
|
|
174
|
+
// Output: PostToolUse cannot block an applied edit — surface the roll-up only.
|
|
175
|
+
process.stdout.write(JSON.stringify({
|
|
176
|
+
hookSpecificOutput: {
|
|
177
|
+
hookEventName: 'PostToolUse',
|
|
178
|
+
additionalContext: buildWarning(findings, relPath),
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Exit 0 always — informational hook must never block Claude (#684).
|
|
184
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-bash-destructive-guard.mjs — PreToolUse hook: blocks destructive Bash commands
|
|
4
|
+
* at the MAIN SESSION level.
|
|
5
|
+
*
|
|
6
|
+
* Motivation: on 2026-04-19 the main session ran `git reset --hard HEAD~1` and
|
|
7
|
+
* destroyed concurrent WIP. The rule (.claude/rules/parallel-sessions.md, PSA-003)
|
|
8
|
+
* existed but had no executable guard. This hook is that guard.
|
|
9
|
+
*
|
|
10
|
+
* Part of issue #155 (deliverable 2). Node 20+, ESM, no bash-isms.
|
|
11
|
+
*
|
|
12
|
+
* Decision flow:
|
|
13
|
+
* G1 tool filter — only Bash is gated
|
|
14
|
+
* G2 command present + string
|
|
15
|
+
* G3 bypass: allow-destructive-ops: true in Session Config → exit 0
|
|
16
|
+
* G4 policy load: .orchestrator/policy/blocked-commands.json
|
|
17
|
+
* Missing → exit 0 (warn). Malformed → exit 0 (warn).
|
|
18
|
+
* G5 rule evaluation per rule in policy.rules
|
|
19
|
+
* severity:"block" → exit 2 with deny message
|
|
20
|
+
* severity:"warn" → emit warning, exit 0 (allow)
|
|
21
|
+
* Special cases:
|
|
22
|
+
* git-stash-any: only warn when stash is non-empty
|
|
23
|
+
* rm-rf-destructive: path exception for .orchestrator/tmp and node_modules
|
|
24
|
+
* G6 no match → exit 0
|
|
25
|
+
*
|
|
26
|
+
* Telemetry (Epic #803 process-safety dimension): best-effort
|
|
27
|
+
* `orchestrator.destructive_guard.blocked` / `orchestrator.destructive_guard.warned`
|
|
28
|
+
* events are appended to events.jsonl on the block/warn paths. Payload never
|
|
29
|
+
* includes the raw command — only a truncated sha256 `command_hash`. Emission
|
|
30
|
+
* is wrapped in try/catch and happens BEFORE the block/warn outcome is
|
|
31
|
+
* finalized; a telemetry failure never changes the guard's decision.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readStdin, emitAllow } from '../scripts/lib/io.mjs';
|
|
35
|
+
import { resolveProjectDir, resolvePluginRoot } from '../scripts/lib/platform.mjs';
|
|
36
|
+
import { commandMatchesBlocked, tokenizeCommand } from '../scripts/lib/hardening.mjs';
|
|
37
|
+
import { readConfigFile } from '../scripts/lib/config.mjs';
|
|
38
|
+
import { readJson } from '../scripts/lib/common.mjs';
|
|
39
|
+
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
40
|
+
import fs, { existsSync } from 'node:fs';
|
|
41
|
+
import os from 'node:os';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import crypto from 'node:crypto';
|
|
44
|
+
|
|
45
|
+
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
46
|
+
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
47
|
+
if (!shouldRunHook('pre-bash-destructive-guard')) process.exit(0);
|
|
48
|
+
|
|
49
|
+
// Module-level policy cache (issue #250). Safe because each hook invocation runs as
|
|
50
|
+
// an isolated Node subprocess — state is fresh per process, never shared across calls.
|
|
51
|
+
// Cache is invalidated on:
|
|
52
|
+
// (a) resolved policy path changes (different projectDir/CWD)
|
|
53
|
+
// (b) file mtime advances (user edited the policy)
|
|
54
|
+
// Any stat/read error → skip cache + fall back to uncached read (fail-safe).
|
|
55
|
+
let _cachedPolicy = null;
|
|
56
|
+
let _cachedPolicyPath = null;
|
|
57
|
+
let _cachedPolicyMtimeMs = null;
|
|
58
|
+
|
|
59
|
+
async function loadPolicyCached(policyPath) {
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.promises.stat(policyPath);
|
|
62
|
+
const mtimeMs = stat.mtimeMs;
|
|
63
|
+
if (
|
|
64
|
+
_cachedPolicy !== null &&
|
|
65
|
+
_cachedPolicyPath === policyPath &&
|
|
66
|
+
_cachedPolicyMtimeMs === mtimeMs
|
|
67
|
+
) {
|
|
68
|
+
return _cachedPolicy;
|
|
69
|
+
}
|
|
70
|
+
const fresh = await readJson(policyPath);
|
|
71
|
+
_cachedPolicy = fresh;
|
|
72
|
+
_cachedPolicyPath = policyPath;
|
|
73
|
+
_cachedPolicyMtimeMs = mtimeMs;
|
|
74
|
+
return fresh;
|
|
75
|
+
} catch {
|
|
76
|
+
// On any error (stat failure, read failure), re-throw to let caller's existing
|
|
77
|
+
// try/catch handle the "malformed policy" branch. Do NOT poison the cache.
|
|
78
|
+
return readJson(policyPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Internal helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* sha256(command) truncated to 16 hex chars — mirrors loop-guard.mjs
|
|
88
|
+
* hashArgs(). Never emit the raw command into telemetry (privacy).
|
|
89
|
+
*
|
|
90
|
+
* @param {string} command
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function hashCommand(command) {
|
|
94
|
+
return crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a session id for event payloads. Precedence mirrors
|
|
99
|
+
* loop-guard.mjs resolveSessionKey(): session_id → parent_session_id →
|
|
100
|
+
* null (field omitted from the payload when unavailable).
|
|
101
|
+
*
|
|
102
|
+
* @param {object|null} input
|
|
103
|
+
* @returns {string|null}
|
|
104
|
+
*/
|
|
105
|
+
function resolveSessionId(input) {
|
|
106
|
+
if (input) {
|
|
107
|
+
if (typeof input.session_id === 'string' && input.session_id.length > 0) {
|
|
108
|
+
return input.session_id;
|
|
109
|
+
}
|
|
110
|
+
if (typeof input.parent_session_id === 'string' && input.parent_session_id.length > 0) {
|
|
111
|
+
return input.parent_session_id;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Block a command: write structured deny JSON to stdout + exit 2.
|
|
119
|
+
* Uses raw process.exit(2) rather than emitDeny to emit the exact
|
|
120
|
+
* multi-line message format required by the spec.
|
|
121
|
+
*
|
|
122
|
+
* Emits a best-effort `orchestrator.destructive_guard.blocked` telemetry
|
|
123
|
+
* event BEFORE exiting. Emission failure (e.g. unwritable events.jsonl path)
|
|
124
|
+
* must NEVER change the block outcome — the guard's block-decision is
|
|
125
|
+
* strictly independent of telemetry success.
|
|
126
|
+
*/
|
|
127
|
+
async function blockCommand(pattern, ruleId, rationale, command, sessionId) {
|
|
128
|
+
const reason = [
|
|
129
|
+
`Destructive command blocked: '${pattern}' (rule: ${ruleId})`,
|
|
130
|
+
`Reason: ${rationale}`,
|
|
131
|
+
`Override: Set \`allow-destructive-ops: true\` in Session Config if intentional.`,
|
|
132
|
+
`See: issue #155, .claude/rules/parallel-sessions.md (PSA-003)`,
|
|
133
|
+
].join('\n');
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await emitEvent('orchestrator.destructive_guard.blocked', {
|
|
137
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
138
|
+
rule: ruleId,
|
|
139
|
+
command_hash: hashCommand(command),
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
// Best-effort — telemetry must never block or alter the guard decision.
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Structured deny for Claude Code hook protocol
|
|
146
|
+
process.stdout.write(JSON.stringify({ permissionDecision: 'deny', reason }) + '\n');
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve the policy file path, searching in priority order:
|
|
152
|
+
* 1. <CWD>/.orchestrator/policy/blocked-commands.json
|
|
153
|
+
* 2. <CLAUDE_PROJECT_DIR>/.orchestrator/policy/blocked-commands.json
|
|
154
|
+
* 3. <CLAUDE_PLUGIN_ROOT>/.orchestrator/policy/blocked-commands.json
|
|
155
|
+
* Returns the first existing path, or null if none found.
|
|
156
|
+
*/
|
|
157
|
+
function resolvePolicyPath(projectDir) {
|
|
158
|
+
const candidates = [
|
|
159
|
+
path.join(process.cwd(), '.orchestrator', 'policy', 'blocked-commands.json'),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
if (projectDir && projectDir !== process.cwd()) {
|
|
163
|
+
candidates.push(path.join(projectDir, '.orchestrator', 'policy', 'blocked-commands.json'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const pluginRoot = resolvePluginRoot();
|
|
167
|
+
if (pluginRoot) {
|
|
168
|
+
candidates.push(path.join(pluginRoot, '.orchestrator', 'policy', 'blocked-commands.json'));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
if (existsSync(candidate)) return candidate;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check whether git stash is non-empty by running `git stash list`.
|
|
179
|
+
* Returns true if non-empty (should warn), false if empty (silent allow).
|
|
180
|
+
* On any error returns true (conservative).
|
|
181
|
+
*/
|
|
182
|
+
async function isGitStashNonEmpty(projectDir) {
|
|
183
|
+
try {
|
|
184
|
+
const { $: zx$ } = await import('zx');
|
|
185
|
+
zx$.verbose = false;
|
|
186
|
+
zx$.quiet = true;
|
|
187
|
+
const cwd = projectDir || process.cwd();
|
|
188
|
+
const result = await zx$`git -C ${cwd} stash list`;
|
|
189
|
+
return result.stdout.trim().length > 0;
|
|
190
|
+
} catch {
|
|
191
|
+
// On git failure → conservative: warn
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse ALL non-flag path arguments from every `rm` invocation in a command.
|
|
198
|
+
*
|
|
199
|
+
* Hardened over the previous single-target parser (#641): handles `-r -f`,
|
|
200
|
+
* `-fr`, `-rf`, `--` end-of-options ordering, multiple targets per `rm`, and
|
|
201
|
+
* chained commands (`rm -rf /tmp/x; rm -rf src/`). Quote-aware via
|
|
202
|
+
* tokenizeCommand so a path with spaces inside quotes is one target.
|
|
203
|
+
*
|
|
204
|
+
* Returns an array of target path strings (possibly empty). The caller treats
|
|
205
|
+
* the rm-rf rule as "allowed" only when EVERY returned target is allowlisted.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} command
|
|
208
|
+
* @returns {string[]}
|
|
209
|
+
*/
|
|
210
|
+
function parseRmTargets(command) {
|
|
211
|
+
const tokens = tokenizeCommand(command);
|
|
212
|
+
const targets = [];
|
|
213
|
+
let i = 0;
|
|
214
|
+
|
|
215
|
+
while (i < tokens.length) {
|
|
216
|
+
const verb = tokens[i].text.replace(/^.*\//, ''); // basename
|
|
217
|
+
const isOperator = !tokens[i].quoted && /^(;|&&|\|\||\||&)$/.test(tokens[i].text);
|
|
218
|
+
if (isOperator) { i++; continue; }
|
|
219
|
+
if (verb !== 'rm') { i++; continue; }
|
|
220
|
+
|
|
221
|
+
// Consume this `rm` invocation's args until the next chain operator.
|
|
222
|
+
i++; // skip `rm`
|
|
223
|
+
let seenDashDash = false;
|
|
224
|
+
while (i < tokens.length) {
|
|
225
|
+
const tok = tokens[i];
|
|
226
|
+
// Stop at unquoted chain operators — they delimit the next command.
|
|
227
|
+
if (!tok.quoted && /^(;|&&|\|\||\||&)$/.test(tok.text)) break;
|
|
228
|
+
if (!seenDashDash && tok.text === '--') { seenDashDash = true; i++; continue; }
|
|
229
|
+
// A flag is unquoted and starts with '-' (and is not the bare '-' stdin marker).
|
|
230
|
+
if (!seenDashDash && !tok.quoted && tok.text.startsWith('-') && tok.text !== '-') {
|
|
231
|
+
i++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
targets.push(tok.text);
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return targets;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Detect whether the command contains an UNQUOTED `rm` invocation carrying BOTH
|
|
244
|
+
* recursive (`-r`/`-R`/`--recursive`) AND force (`-f`/`--force`) semantics,
|
|
245
|
+
* including combined/short forms (`-rf`, `-fr`, `-r -f`). This catches flag-form
|
|
246
|
+
* variants the literal "rm -rf" pattern misses (#641 gap closure) while staying
|
|
247
|
+
* consistent with the quoted-payload guard: an `rm` that appears only inside a
|
|
248
|
+
* quoted token is NOT treated as an invocation here.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} command
|
|
251
|
+
* @returns {boolean}
|
|
252
|
+
*/
|
|
253
|
+
function commandHasRecursiveForceRm(command) {
|
|
254
|
+
const tokens = tokenizeCommand(command);
|
|
255
|
+
let i = 0;
|
|
256
|
+
while (i < tokens.length) {
|
|
257
|
+
const tok = tokens[i];
|
|
258
|
+
const verb = tok.text.replace(/^.*\//, ''); // basename
|
|
259
|
+
if (tok.quoted || verb !== 'rm') { i++; continue; }
|
|
260
|
+
|
|
261
|
+
// Scan this rm invocation's flags until the next unquoted chain operator.
|
|
262
|
+
i++; // skip `rm`
|
|
263
|
+
let recursive = false;
|
|
264
|
+
let force = false;
|
|
265
|
+
let seenDashDash = false;
|
|
266
|
+
while (i < tokens.length) {
|
|
267
|
+
const t = tokens[i];
|
|
268
|
+
if (!t.quoted && /^(;|&&|\|\||\||&)$/.test(t.text)) break;
|
|
269
|
+
if (!seenDashDash && t.text === '--') { seenDashDash = true; i++; continue; }
|
|
270
|
+
if (!seenDashDash && !t.quoted && t.text.startsWith('-') && t.text !== '-') {
|
|
271
|
+
if (t.text === '--recursive') recursive = true;
|
|
272
|
+
else if (t.text === '--force') force = true;
|
|
273
|
+
else if (/^-[a-zA-Z]+$/.test(t.text)) {
|
|
274
|
+
// Bundled short flags: -rf, -fr, -Rf, etc.
|
|
275
|
+
if (/[rR]/.test(t.text)) recursive = true;
|
|
276
|
+
if (/f/.test(t.text)) force = true;
|
|
277
|
+
}
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
i++; // non-flag arg (a target) — skip
|
|
282
|
+
}
|
|
283
|
+
if (recursive && force) return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Return true when a single path is a safe `rm -rf` target.
|
|
290
|
+
*
|
|
291
|
+
* Safe targets:
|
|
292
|
+
* - <projectRoot>/.orchestrator/tmp (any depth)
|
|
293
|
+
* - <projectRoot>/node_modules (any depth)
|
|
294
|
+
* - /tmp/ (any depth) — agent-owned scratch
|
|
295
|
+
* - /private/tmp/ (any depth) — macOS canonical /tmp
|
|
296
|
+
* - resolved os.tmpdir() / $TMPDIR (any depth)
|
|
297
|
+
*
|
|
298
|
+
* The /tmp-class prefixes come from the rule's optional `path-allowlist` and are
|
|
299
|
+
* resolved at runtime here.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} targetPath
|
|
302
|
+
* @param {string} projectDir
|
|
303
|
+
* @param {string[]} ruleAllowlist — `path-allowlist` entries (e.g. "/tmp/", "$TMPDIR")
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
function isRmPathAllowed(targetPath, projectDir, ruleAllowlist = []) {
|
|
307
|
+
if (!targetPath) return false;
|
|
308
|
+
|
|
309
|
+
const base = projectDir || process.cwd();
|
|
310
|
+
const wasAbsolute = path.isAbsolute(targetPath);
|
|
311
|
+
|
|
312
|
+
// Project-relative safe dirs (always allowed, independent of the rule allowlist).
|
|
313
|
+
const abs = wasAbsolute
|
|
314
|
+
? path.normalize(targetPath)
|
|
315
|
+
: path.resolve(base, targetPath);
|
|
316
|
+
|
|
317
|
+
const safeProjectDirs = [
|
|
318
|
+
path.join(base, '.orchestrator', 'tmp'),
|
|
319
|
+
path.join(base, 'node_modules'),
|
|
320
|
+
];
|
|
321
|
+
for (const safe of safeProjectDirs) {
|
|
322
|
+
if (abs === safe || abs.startsWith(safe + path.sep)) return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Rule-driven absolute prefixes (/tmp/, /private/tmp/, $TMPDIR).
|
|
326
|
+
// Apply ONLY to targets that were ABSOLUTE in the command. A bare relative
|
|
327
|
+
// target (e.g. "src/") is project-relative and must NEVER be allowlisted by a
|
|
328
|
+
// /tmp prefix just because the project dir itself happens to live under /tmp
|
|
329
|
+
// (the case on CI runners where os.tmpdir() === /tmp). #641.
|
|
330
|
+
if (wasAbsolute) {
|
|
331
|
+
for (const prefix of resolveAllowlistPrefixes(ruleAllowlist)) {
|
|
332
|
+
// The target must be the prefix dir itself or a descendant of it.
|
|
333
|
+
if (abs === prefix || abs.startsWith(prefix + path.sep)) return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Resolve the rule's `path-allowlist` entries into concrete absolute directory
|
|
342
|
+
* prefixes. `$TMPDIR` expands to env.TMPDIR (if set) and os.tmpdir(); literal
|
|
343
|
+
* paths are normalised. Trailing slashes are stripped for prefix comparison.
|
|
344
|
+
*
|
|
345
|
+
* @param {string[]} ruleAllowlist
|
|
346
|
+
* @returns {string[]} normalised absolute prefixes (no trailing slash)
|
|
347
|
+
*/
|
|
348
|
+
function resolveAllowlistPrefixes(ruleAllowlist) {
|
|
349
|
+
const out = new Set();
|
|
350
|
+
const add = (p) => {
|
|
351
|
+
if (!p) return;
|
|
352
|
+
const norm = path.normalize(p).replace(/[/\\]+$/, '');
|
|
353
|
+
if (norm) out.add(norm);
|
|
354
|
+
};
|
|
355
|
+
const tempRoots = ['/tmp', '/private/tmp', '/var/folders'].map((p) => path.normalize(p));
|
|
356
|
+
const addTemp = (p) => {
|
|
357
|
+
if (!p || !path.isAbsolute(p)) return;
|
|
358
|
+
const norm = path.normalize(p).replace(/[/\\]+$/, '');
|
|
359
|
+
if (tempRoots.some((root) => norm === root || norm.startsWith(root + path.sep))) {
|
|
360
|
+
out.add(norm);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
for (const entry of Array.isArray(ruleAllowlist) ? ruleAllowlist : []) {
|
|
365
|
+
if (typeof entry !== 'string' || entry.length === 0) continue;
|
|
366
|
+
if (entry === '$TMPDIR' || entry === '${TMPDIR}') {
|
|
367
|
+
if (process.env.TMPDIR) addTemp(process.env.TMPDIR);
|
|
368
|
+
addTemp(os.tmpdir());
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (path.isAbsolute(entry)) add(entry);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return [...out];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Main
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
async function main() {
|
|
382
|
+
const input = await readStdin();
|
|
383
|
+
if (!input) return emitAllow();
|
|
384
|
+
|
|
385
|
+
// G1 — only Bash is gated
|
|
386
|
+
if (input.tool_name !== 'Bash') return emitAllow();
|
|
387
|
+
|
|
388
|
+
// G2 — command must be a non-empty string
|
|
389
|
+
const command = input?.tool_input?.command;
|
|
390
|
+
if (typeof command !== 'string' || command.length === 0) return emitAllow();
|
|
391
|
+
|
|
392
|
+
const projectDir = resolveProjectDir();
|
|
393
|
+
const sessionId = resolveSessionId(input);
|
|
394
|
+
|
|
395
|
+
// G3 — bypass: allow-destructive-ops: true in Session Config
|
|
396
|
+
// Note: parseSessionConfig only returns known fields; allow-destructive-ops is
|
|
397
|
+
// a new field, so we parse the raw markdown for it directly.
|
|
398
|
+
try {
|
|
399
|
+
const mdContent = await readConfigFile(projectDir);
|
|
400
|
+
// Look for "allow-destructive-ops: true" in the Session Config block.
|
|
401
|
+
// Simple line-based scan: find lines under ## Session Config, stop at next ##.
|
|
402
|
+
const lines = mdContent.split(/\r?\n/);
|
|
403
|
+
let inConfig = false;
|
|
404
|
+
for (const line of lines) {
|
|
405
|
+
if (line === '## Session Config') { inConfig = true; continue; }
|
|
406
|
+
if (inConfig && /^## /.test(line)) break;
|
|
407
|
+
if (inConfig) {
|
|
408
|
+
const m = line.match(/^\s*(?:-\s+\*\*)?allow-destructive-ops(?::\*\*)?\s*:\s*(\S+)/);
|
|
409
|
+
if (m && m[1].toLowerCase() === 'true') {
|
|
410
|
+
process.stderr.write('ℹ destructive-guard bypassed\n');
|
|
411
|
+
return emitAllow();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// No config file or parse error — proceed to policy check
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// G4 — policy load
|
|
420
|
+
const policyPath = resolvePolicyPath(projectDir);
|
|
421
|
+
if (!policyPath) {
|
|
422
|
+
process.stderr.write(
|
|
423
|
+
'⚠ pre-bash-destructive-guard: policy file not found ' +
|
|
424
|
+
'(.orchestrator/policy/blocked-commands.json) — skipping guard\n'
|
|
425
|
+
);
|
|
426
|
+
return emitAllow();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let policy;
|
|
430
|
+
try {
|
|
431
|
+
policy = await loadPolicyCached(policyPath);
|
|
432
|
+
} catch {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
'⚠ pre-bash-destructive-guard: policy file is malformed (invalid JSON) — skipping guard\n'
|
|
435
|
+
);
|
|
436
|
+
return emitAllow();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!policy || !Array.isArray(policy.rules)) {
|
|
440
|
+
process.stderr.write(
|
|
441
|
+
'⚠ pre-bash-destructive-guard: policy file missing .rules array — skipping guard\n'
|
|
442
|
+
);
|
|
443
|
+
return emitAllow();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// G5 — rule evaluation
|
|
447
|
+
for (const rule of policy.rules) {
|
|
448
|
+
const { id, pattern, severity, rationale = '' } = rule;
|
|
449
|
+
|
|
450
|
+
// The rm-rf-destructive rule also fires for recursive+force rm flag variants
|
|
451
|
+
// the literal "rm -rf" pattern misses (`rm -r -f`, `rm -fr`) — #641 gap closure.
|
|
452
|
+
const matched = id === 'rm-rf-destructive'
|
|
453
|
+
? (commandMatchesBlocked(command, pattern) || commandHasRecursiveForceRm(command))
|
|
454
|
+
: commandMatchesBlocked(command, pattern);
|
|
455
|
+
if (!matched) continue;
|
|
456
|
+
|
|
457
|
+
if (severity === 'warn') {
|
|
458
|
+
// Special: git-stash-any — only warn when stash is non-empty
|
|
459
|
+
if (id === 'git-stash-any') {
|
|
460
|
+
const nonEmpty = await isGitStashNonEmpty(projectDir);
|
|
461
|
+
if (!nonEmpty) {
|
|
462
|
+
// Empty stash — silent allow
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
process.stderr.write(
|
|
467
|
+
`⚠ pre-bash-destructive-guard: '${pattern}' (rule: ${id}) — ${rationale}\n`
|
|
468
|
+
);
|
|
469
|
+
// Best-effort telemetry — must never affect the warn/allow outcome.
|
|
470
|
+
try {
|
|
471
|
+
await emitEvent('orchestrator.destructive_guard.warned', {
|
|
472
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
473
|
+
rule: id,
|
|
474
|
+
command_hash: hashCommand(command),
|
|
475
|
+
});
|
|
476
|
+
} catch {
|
|
477
|
+
// Best-effort — telemetry must never block or alter the guard decision.
|
|
478
|
+
}
|
|
479
|
+
// warn → allow (exit 0), continue checking remaining rules
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (severity === 'block') {
|
|
484
|
+
// Special: rm-rf-destructive — path exception
|
|
485
|
+
if (id === 'rm-rf-destructive') {
|
|
486
|
+
const ruleAllowlist = Array.isArray(rule['path-allowlist']) ? rule['path-allowlist'] : [];
|
|
487
|
+
const targets = parseRmTargets(command);
|
|
488
|
+
// Allow ONLY when there is at least one target AND every target is
|
|
489
|
+
// allowlisted. An unparseable command (no targets) or any non-allowlisted
|
|
490
|
+
// target → block (conservative). This makes mixed chains like
|
|
491
|
+
// `rm -rf /tmp/x; rm -rf src/` block on the src/ target.
|
|
492
|
+
const allAllowed =
|
|
493
|
+
targets.length > 0 &&
|
|
494
|
+
targets.every((t) => isRmPathAllowed(t, projectDir, ruleAllowlist));
|
|
495
|
+
if (allAllowed) {
|
|
496
|
+
// Safe paths only (.orchestrator/tmp, node_modules, /tmp, $TMPDIR) — allow
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
await blockCommand(pattern, id, rationale, command, sessionId);
|
|
501
|
+
}
|
|
502
|
+
// Unknown severity → skip (conservative allow for unknown future severities)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// G6 — no blocking match
|
|
506
|
+
return emitAllow();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Top-level error handler — never let exit 1 leak
|
|
510
|
+
main().catch((e) => {
|
|
511
|
+
process.stderr.write(
|
|
512
|
+
`⚠ pre-bash-destructive-guard: internal error — ${e?.message || e}\n`
|
|
513
|
+
);
|
|
514
|
+
process.exit(0); // fail-open on internal errors to avoid blocking legitimate work
|
|
515
|
+
});
|