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,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cold-start-detector.mjs — F1.3 Cold-Start Abandonment Fix
|
|
3
|
+
*
|
|
4
|
+
* Detects whether a repo is in a "bootstrapped but never used" state and
|
|
5
|
+
* produces a banner-emit decision for the SessionStart hook. The headline
|
|
6
|
+
* adoption finding (PRD 2026-05-21, §Background) is that 58% of audited
|
|
7
|
+
* repos never get past bootstrap — the `SessionStart` hook stays silent and
|
|
8
|
+
* the operator never returns. This detector adds one nudge.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour (PRD §F1.3):
|
|
11
|
+
* 1. Skip if `bootstrap.lock` is missing (repo not yet bootstrapped —
|
|
12
|
+
* the bootstrap-gate handles that path).
|
|
13
|
+
* 2. Skip if `sessions.jsonl` line count ≥ `silence-after-sessions`
|
|
14
|
+
* (operator has already engaged at least once).
|
|
15
|
+
* 3. Skip if bootstrap age < `nudge-after-hours` (give the operator a
|
|
16
|
+
* reasonable window after bootstrap before nudging).
|
|
17
|
+
* 4. Emit otherwise, with a `markerPath` when the migration marker
|
|
18
|
+
* `.orchestrator/welcome-banner-pending` is present (the hook then
|
|
19
|
+
* calls `consumeMarker()` after emission).
|
|
20
|
+
*
|
|
21
|
+
* Hard contract:
|
|
22
|
+
* - Never throws. Any error → return {shouldEmit: false, reason: '...'}.
|
|
23
|
+
* - Pure async fs.promises — must not block the SessionStart hook's 5s
|
|
24
|
+
* budget. Internally uses stat + readFile only on small files.
|
|
25
|
+
* - No external deps. ESM only. Node 20+ `node:fs/promises`.
|
|
26
|
+
*
|
|
27
|
+
* Wired from `hooks/on-session-start.mjs` immediately after the host-banner
|
|
28
|
+
* block (~line 240) and before the registry/peer block.
|
|
29
|
+
*
|
|
30
|
+
* @module cold-start-detector
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { stat, readFile, unlink } from 'node:fs/promises';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
import { parseBootstrapLock } from './bootstrap-lock-freshness.mjs';
|
|
37
|
+
|
|
38
|
+
/** Milliseconds in one hour — exported for testability. */
|
|
39
|
+
export const MS_PER_HOUR = 60 * 60 * 1000;
|
|
40
|
+
|
|
41
|
+
/** Relative path of the migration-seed marker file. */
|
|
42
|
+
export const WELCOME_MARKER_REL = path.join('.orchestrator', 'welcome-banner-pending');
|
|
43
|
+
|
|
44
|
+
/** Relative path of the bootstrap lock. */
|
|
45
|
+
const BOOTSTRAP_LOCK_REL = path.join('.orchestrator', 'bootstrap.lock');
|
|
46
|
+
|
|
47
|
+
/** Relative path of the sessions roll-up (NDJSON). */
|
|
48
|
+
const SESSIONS_JSONL_REL = path.join('.orchestrator', 'metrics', 'sessions.jsonl');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Cold-start banner copy. English first (Anthropic Claude Code surface).
|
|
52
|
+
* Returned as `bannerLines` so the caller can join('\n') for stdout emission
|
|
53
|
+
* or splice in additional context (e.g. bootstrap timestamp) without
|
|
54
|
+
* re-parsing the message.
|
|
55
|
+
*
|
|
56
|
+
* Returned array, not a constant — keeps the function pure (no shared
|
|
57
|
+
* mutable state).
|
|
58
|
+
*
|
|
59
|
+
* @param {{bootstrappedAt?: string|null}} [opts]
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
function buildBannerLines({ bootstrappedAt = null } = {}) {
|
|
63
|
+
const tail = bootstrappedAt
|
|
64
|
+
? [`Bootstrap completed: ${bootstrappedAt}`]
|
|
65
|
+
: [];
|
|
66
|
+
return [
|
|
67
|
+
'📚 First session not yet — your repo is set up but the orchestrator hasn\'t run anything yet.',
|
|
68
|
+
'',
|
|
69
|
+
'What this gives you:',
|
|
70
|
+
' • Auto-detects open issues, branch state, and CI status at session-start',
|
|
71
|
+
' • Distributes work across parallel subagents with file-lane isolation',
|
|
72
|
+
' • Captures learnings and decisions to vault automatically',
|
|
73
|
+
'',
|
|
74
|
+
'Try: `/session housekeeping` ← 3-5 minutes, low-risk warm-up',
|
|
75
|
+
...tail,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Best-effort line counter for NDJSON files. Returns 0 when the file is
|
|
81
|
+
* missing OR empty OR unreadable — all three are equivalent for the
|
|
82
|
+
* cold-start decision ("no sessions yet").
|
|
83
|
+
*
|
|
84
|
+
* Reads the full file because sessions.jsonl is small (1 line/session, ~2 KB
|
|
85
|
+
* each — even 100 sessions = ~200 KB).
|
|
86
|
+
*
|
|
87
|
+
* @param {string} filePath
|
|
88
|
+
* @returns {Promise<number>}
|
|
89
|
+
*/
|
|
90
|
+
async function countLines(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
const raw = await readFile(filePath, 'utf8');
|
|
93
|
+
if (!raw) return 0;
|
|
94
|
+
// Trailing newline must not inflate the count. split-then-filter handles
|
|
95
|
+
// both Unix (\n) and Windows (\r\n) line endings.
|
|
96
|
+
const lines = raw.split('\n').filter((l) => l.length > 0);
|
|
97
|
+
return lines.length;
|
|
98
|
+
} catch {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Existence check via fs.promises.stat. Returns true only when the path
|
|
105
|
+
* resolves to a regular file or directory. Any error → false.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} p
|
|
108
|
+
* @returns {Promise<boolean>}
|
|
109
|
+
*/
|
|
110
|
+
async function exists(p) {
|
|
111
|
+
try {
|
|
112
|
+
await stat(p);
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Detect cold-start state and produce a banner-emit decision.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} opts
|
|
123
|
+
* @param {string} opts.repoRoot - resolved repo root (caller passes
|
|
124
|
+
* `resolveProjectDir()` from platform.mjs)
|
|
125
|
+
* @param {number} [opts.nudgeAfterHours=1] - PRD default 1 — emit only if
|
|
126
|
+
* bootstrap age >= this. Override via Session Config
|
|
127
|
+
* `cold-start.nudge-after-hours`.
|
|
128
|
+
* @param {number} [opts.silenceAfterSessions=1] - PRD default 1 — silence
|
|
129
|
+
* after >= this many sessions exist. Override via Session Config
|
|
130
|
+
* `cold-start.silence-after-sessions`.
|
|
131
|
+
* @param {boolean} [opts.enabled=true] - master switch (Session Config
|
|
132
|
+
* `cold-start.enabled`). When false, returns shouldEmit=false with
|
|
133
|
+
* reason='disabled'.
|
|
134
|
+
* @param {number} [opts.now] - injected clock for tests (defaults to
|
|
135
|
+
* Date.now()).
|
|
136
|
+
* @returns {Promise<{shouldEmit: boolean, reason: string, markerPath?: string, bannerLines?: string[]}>}
|
|
137
|
+
*/
|
|
138
|
+
export async function detectColdStart(opts = {}) {
|
|
139
|
+
const {
|
|
140
|
+
repoRoot,
|
|
141
|
+
nudgeAfterHours = 1,
|
|
142
|
+
silenceAfterSessions = 1,
|
|
143
|
+
enabled = true,
|
|
144
|
+
now = Date.now(),
|
|
145
|
+
} = opts;
|
|
146
|
+
|
|
147
|
+
// Defensive: missing repoRoot is a programming error, but the hook
|
|
148
|
+
// contract is "never break" — fall through cleanly.
|
|
149
|
+
if (!repoRoot || typeof repoRoot !== 'string') {
|
|
150
|
+
return { shouldEmit: false, reason: 'no-repo-root' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (enabled === false) {
|
|
154
|
+
return { shouldEmit: false, reason: 'disabled' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const lockPath = path.join(repoRoot, BOOTSTRAP_LOCK_REL);
|
|
158
|
+
const sessionsPath = path.join(repoRoot, SESSIONS_JSONL_REL);
|
|
159
|
+
const markerPath = path.join(repoRoot, WELCOME_MARKER_REL);
|
|
160
|
+
|
|
161
|
+
// PRD §F1.3 Unwanted behaviour: if bootstrap.lock missing, do NOT emit.
|
|
162
|
+
// Bootstrap-gate handles that path — emitting here would race the gate.
|
|
163
|
+
if (!(await exists(lockPath))) {
|
|
164
|
+
return { shouldEmit: false, reason: 'no-bootstrap-lock' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// PRD §F1.3 Ubiquitous: silence once the repo has ≥ N sessions. This is
|
|
168
|
+
// the auto-silence path — banner is one-shot per repo (default N=1).
|
|
169
|
+
const sessionCount = await countLines(sessionsPath);
|
|
170
|
+
if (sessionCount >= silenceAfterSessions) {
|
|
171
|
+
return {
|
|
172
|
+
shouldEmit: false,
|
|
173
|
+
reason: `sessions-floor-met (${sessionCount} >= ${silenceAfterSessions})`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// PRD §F1.3 Event-driven: bootstrap age >= nudge-after-hours.
|
|
178
|
+
let bootstrappedAt;
|
|
179
|
+
try {
|
|
180
|
+
const lockRaw = await readFile(lockPath, 'utf8');
|
|
181
|
+
const parsed = parseBootstrapLock(lockRaw);
|
|
182
|
+
// Prefer modern `bootstrapped-at` (post-#186); fall back to legacy
|
|
183
|
+
// `timestamp` for older locks.
|
|
184
|
+
bootstrappedAt = parsed['bootstrapped-at'] || parsed['timestamp'] || null;
|
|
185
|
+
} catch {
|
|
186
|
+
return { shouldEmit: false, reason: 'lock-read-failed' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!bootstrappedAt) {
|
|
190
|
+
return { shouldEmit: false, reason: 'lock-no-timestamp' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ts = Date.parse(bootstrappedAt);
|
|
194
|
+
if (Number.isNaN(ts)) {
|
|
195
|
+
return { shouldEmit: false, reason: 'lock-unparseable-timestamp' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ageMs = now - ts;
|
|
199
|
+
const thresholdMs = nudgeAfterHours * MS_PER_HOUR;
|
|
200
|
+
if (ageMs < thresholdMs) {
|
|
201
|
+
const ageHours = Math.floor(ageMs / MS_PER_HOUR);
|
|
202
|
+
return {
|
|
203
|
+
shouldEmit: false,
|
|
204
|
+
reason: `bootstrap-too-fresh (${ageHours}h < ${nudgeAfterHours}h)`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// All conditions met — emit. Marker presence is informational: the hook
|
|
209
|
+
// calls `consumeMarker(markerPath)` only when this field is non-null.
|
|
210
|
+
const markerPresent = await exists(markerPath);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
shouldEmit: true,
|
|
214
|
+
reason: markerPresent
|
|
215
|
+
? 'migration-marker-present'
|
|
216
|
+
: `bootstrap-age-met (${Math.floor(ageMs / MS_PER_HOUR)}h >= ${nudgeAfterHours}h, sessions=${sessionCount})`,
|
|
217
|
+
markerPath: markerPresent ? markerPath : undefined,
|
|
218
|
+
bannerLines: buildBannerLines({ bootstrappedAt }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Delete the migration-seed marker after the banner has been emitted.
|
|
224
|
+
*
|
|
225
|
+
* Best-effort: any failure (already deleted by another session, permission
|
|
226
|
+
* issue, missing parent dir) is swallowed. The next session-start will
|
|
227
|
+
* re-evaluate via the standard sessions-jsonl path.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} markerPath - absolute path returned by `detectColdStart()`
|
|
230
|
+
* @returns {Promise<boolean>} true on successful delete, false otherwise
|
|
231
|
+
*/
|
|
232
|
+
export async function consumeMarker(markerPath) {
|
|
233
|
+
if (!markerPath || typeof markerPath !== 'string') return false;
|
|
234
|
+
try {
|
|
235
|
+
await unlink(markerPath);
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-blocker.mjs — command-blocking tokenizer + matcher.
|
|
3
|
+
*
|
|
4
|
+
* Split out of scripts/lib/hardening.mjs (concern C). This is the
|
|
5
|
+
* destructive-command guard powering pre-bash-destructive-guard /
|
|
6
|
+
* enforce-commands — security-sensitive; behaviour is preserved EXACTLY.
|
|
7
|
+
* Re-exported by hardening.mjs as a barrel so existing importers keep working
|
|
8
|
+
* unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Layering: hook-safe — pure functions only; no I/O at import time;
|
|
11
|
+
* ESM-pure for fast hook hot-paths. Hooks (under `hooks/`) import from
|
|
12
|
+
* this lib; this lib MUST NOT reverse-import from `hooks/`. Cross-cutting
|
|
13
|
+
* invariant for all exports below — see #554 A2.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shell interpreters whose QUOTED argument text is still executed as a command.
|
|
18
|
+
* When a command segment's argv[0] (or `command <verb>` / `env … <verb>`) is one
|
|
19
|
+
* of these, a blocked pattern found inside a quoted token is NOT inert — it is the
|
|
20
|
+
* payload the interpreter will run. Includes SQL executors (`psql -c "DROP TABLE …"`)
|
|
21
|
+
* and `find` (`find . -exec rm -rf {} \;`).
|
|
22
|
+
*
|
|
23
|
+
* Used by the quoted-payload guard in commandMatchesBlocked (#641).
|
|
24
|
+
*/
|
|
25
|
+
const SHELL_EXEC_INTERPRETERS = new Set([
|
|
26
|
+
'bash', 'sh', 'zsh', 'dash', 'ksh',
|
|
27
|
+
'eval', 'xargs', 'env', 'command',
|
|
28
|
+
'psql', 'mysql', 'sqlite3',
|
|
29
|
+
'find',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function matchIfsWhitespaceExpansion(command, index) {
|
|
33
|
+
if (command.startsWith('${IFS', index)) {
|
|
34
|
+
const end = command.indexOf('}', index + 2);
|
|
35
|
+
if (end !== -1) {
|
|
36
|
+
const body = command.slice(index + 2, end);
|
|
37
|
+
if (body === 'IFS' || /^IFS:-\s*$/.test(body)) return end + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (command.startsWith('$IFS', index)) {
|
|
42
|
+
const next = command[index + 4] ?? '';
|
|
43
|
+
if (!/[A-Za-z0-9_]/.test(next)) return index + 4;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWhitespaceCode(code) {
|
|
50
|
+
return Number.isFinite(code) && /\s/.test(String.fromCharCode(code));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function matchAnsiCWhitespaceQuote(command, index) {
|
|
54
|
+
if (!command.startsWith("$'", index)) return -1;
|
|
55
|
+
|
|
56
|
+
let i = index + 2;
|
|
57
|
+
let sawWhitespace = false;
|
|
58
|
+
while (i < command.length) {
|
|
59
|
+
const ch = command[i];
|
|
60
|
+
if (ch === "'") return sawWhitespace ? i + 1 : -1;
|
|
61
|
+
|
|
62
|
+
if (ch === '\\') {
|
|
63
|
+
if (i + 1 >= command.length) return -1;
|
|
64
|
+
const next = command[i + 1];
|
|
65
|
+
|
|
66
|
+
if (next === 't' || next === 'n' || next === 'r' || next === 'v' || next === 'f') {
|
|
67
|
+
sawWhitespace = true;
|
|
68
|
+
i += 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (next === 'x') {
|
|
73
|
+
const hex = command.slice(i + 2).match(/^[0-9A-Fa-f]{1,2}/)?.[0];
|
|
74
|
+
if (!hex) return -1;
|
|
75
|
+
if (!isWhitespaceCode(Number.parseInt(hex, 16))) return -1;
|
|
76
|
+
sawWhitespace = true;
|
|
77
|
+
i += 2 + hex.length;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (/^[0-7]$/.test(next)) {
|
|
82
|
+
const octal = command.slice(i + 1).match(/^[0-7]{1,3}/)?.[0];
|
|
83
|
+
if (!octal) return -1;
|
|
84
|
+
if (!isWhitespaceCode(Number.parseInt(octal, 8))) return -1;
|
|
85
|
+
sawWhitespace = true;
|
|
86
|
+
i += 1 + octal.length;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (/\s/.test(next)) {
|
|
91
|
+
sawWhitespace = true;
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (/\s/.test(ch)) {
|
|
100
|
+
sawWhitespace = true;
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchShellWhitespaceExpansion(command, index) {
|
|
112
|
+
const ifsEnd = matchIfsWhitespaceExpansion(command, index);
|
|
113
|
+
if (ifsEnd !== -1) return ifsEnd;
|
|
114
|
+
return matchAnsiCWhitespaceQuote(command, index);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Normalize common shell whitespace obfuscations into literal spaces before
|
|
119
|
+
* guard parsing. This is intentionally narrow: it recognizes IFS expansions and
|
|
120
|
+
* ANSI-C quotes that decode entirely to whitespace, not arbitrary shell syntax.
|
|
121
|
+
*
|
|
122
|
+
* By default single-quoted text is preserved because the outer shell treats it
|
|
123
|
+
* literally. Callers that inspect shell-interpreter payload strings can opt in
|
|
124
|
+
* to `expandSingleQuoted` because those strings are parsed by a later shell.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} command
|
|
127
|
+
* @param {{ expandSingleQuoted?: boolean, expandDoubleQuoted?: boolean }} [options]
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function normalizeShellWhitespaceExpansions(command, options = {}) {
|
|
131
|
+
const expandSingleQuoted = options.expandSingleQuoted === true;
|
|
132
|
+
const expandDoubleQuoted = options.expandDoubleQuoted !== false;
|
|
133
|
+
|
|
134
|
+
let out = '';
|
|
135
|
+
let state = 'normal';
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < command.length;) {
|
|
138
|
+
const ch = command[i];
|
|
139
|
+
|
|
140
|
+
if (state === 'single') {
|
|
141
|
+
if (ch === "'") { state = 'normal'; out += ch; i++; continue; }
|
|
142
|
+
if (expandSingleQuoted) {
|
|
143
|
+
const end = matchShellWhitespaceExpansion(command, i);
|
|
144
|
+
if (end !== -1) { out += ' '; i = end; continue; }
|
|
145
|
+
}
|
|
146
|
+
out += ch;
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (state === 'double') {
|
|
152
|
+
if (ch === '"') { state = 'normal'; out += ch; i++; continue; }
|
|
153
|
+
if (expandDoubleQuoted) {
|
|
154
|
+
const end = matchShellWhitespaceExpansion(command, i);
|
|
155
|
+
if (end !== -1) { out += ' '; i = end; continue; }
|
|
156
|
+
}
|
|
157
|
+
if (ch === '\\' && i + 1 < command.length) {
|
|
158
|
+
out += ch + command[i + 1];
|
|
159
|
+
i += 2;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
out += ch;
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const end = matchShellWhitespaceExpansion(command, i);
|
|
168
|
+
if (end !== -1) { out += ' '; i = end; continue; }
|
|
169
|
+
if (ch === '\\' && i + 1 < command.length) {
|
|
170
|
+
out += ch + command[i + 1];
|
|
171
|
+
i += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ch === "'") { state = 'single'; out += ch; i++; continue; }
|
|
175
|
+
if (ch === '"') { state = 'double'; out += ch; i++; continue; }
|
|
176
|
+
|
|
177
|
+
out += ch;
|
|
178
|
+
i++;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Hand-rolled quote-aware command lexer.
|
|
186
|
+
*
|
|
187
|
+
* Splits a command string into tokens on UNQUOTED whitespace, tracking single- and
|
|
188
|
+
* double-quote state and backslash escapes. Each token records whether ANY of its
|
|
189
|
+
* characters originated inside quotes (`quoted: true`). Quote characters and the
|
|
190
|
+
* escaping backslash are consumed (not part of the token text), so the returned
|
|
191
|
+
* token text is the logical argument value a shell would pass.
|
|
192
|
+
*
|
|
193
|
+
* This is deliberately NOT node:util.parseArgs — parseArgs operates on an already-
|
|
194
|
+
* tokenized argv array and does not lex raw shell strings with quote semantics.
|
|
195
|
+
* No new npm dependency is introduced (hook hot-path constraint).
|
|
196
|
+
*
|
|
197
|
+
* Notes / scope (sufficient for the guard, not a full POSIX shell parser):
|
|
198
|
+
* - Single quotes: literal, no escapes inside (POSIX).
|
|
199
|
+
* - Double quotes: backslash escapes the next char.
|
|
200
|
+
* - Outside quotes: backslash escapes the next char (incl. whitespace → same token).
|
|
201
|
+
* - A token that mixes quoted + unquoted runs (e.g. foo"bar") is `quoted: true`
|
|
202
|
+
* because part of it came from a quoted run — conservative for the guard.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} command
|
|
205
|
+
* @returns {Array<{ text: string, quoted: boolean }>}
|
|
206
|
+
*/
|
|
207
|
+
export function tokenizeCommand(command) {
|
|
208
|
+
const tokens = [];
|
|
209
|
+
if (typeof command !== 'string' || command.length === 0) return tokens;
|
|
210
|
+
command = normalizeShellWhitespaceExpansions(command);
|
|
211
|
+
|
|
212
|
+
let text = '';
|
|
213
|
+
let started = false; // a token is in progress
|
|
214
|
+
let sawQuote = false; // any char of the current token came from inside quotes
|
|
215
|
+
let state = 'normal'; // 'normal' | 'single' | 'double'
|
|
216
|
+
|
|
217
|
+
const flush = () => {
|
|
218
|
+
if (started) {
|
|
219
|
+
tokens.push({ text, quoted: sawQuote });
|
|
220
|
+
text = '';
|
|
221
|
+
started = false;
|
|
222
|
+
sawQuote = false;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < command.length; i++) {
|
|
227
|
+
const ch = command[i];
|
|
228
|
+
|
|
229
|
+
if (state === 'single') {
|
|
230
|
+
if (ch === "'") { state = 'normal'; continue; }
|
|
231
|
+
text += ch; started = true; sawQuote = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (state === 'double') {
|
|
236
|
+
if (ch === '"') { state = 'normal'; continue; }
|
|
237
|
+
if (ch === '\\' && i + 1 < command.length) {
|
|
238
|
+
const next = command[i + 1];
|
|
239
|
+
// In double quotes, backslash only escapes a small set; keep it simple:
|
|
240
|
+
// consume the backslash and take the next char literally.
|
|
241
|
+
text += next; started = true; sawQuote = true; i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
text += ch; started = true; sawQuote = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// state === 'normal'
|
|
249
|
+
if (ch === "'") { state = 'single'; started = true; continue; }
|
|
250
|
+
if (ch === '"') { state = 'double'; started = true; continue; }
|
|
251
|
+
if (ch === '\\' && i + 1 < command.length) {
|
|
252
|
+
text += command[i + 1]; started = true; i++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (/\s/.test(ch)) { flush(); continue; }
|
|
256
|
+
|
|
257
|
+
// Unquoted shell control operators become standalone tokens so chain-splitting
|
|
258
|
+
// and per-segment verb detection work even without surrounding whitespace
|
|
259
|
+
// (e.g. `/tmp/x;rm -rf src/`). Recognised: ; && || | & — longest match first.
|
|
260
|
+
if (ch === ';' || ch === '|' || ch === '&') {
|
|
261
|
+
flush();
|
|
262
|
+
let op = ch;
|
|
263
|
+
if ((ch === '&' || ch === '|') && command[i + 1] === ch) { op = ch + ch; i++; }
|
|
264
|
+
tokens.push({ text: op, quoted: false });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
text += ch; started = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Unterminated quote → flush whatever accumulated (mark quoted so the guard treats
|
|
272
|
+
// the dangling text conservatively).
|
|
273
|
+
if (state === 'single' || state === 'double') sawQuote = true;
|
|
274
|
+
flush();
|
|
275
|
+
|
|
276
|
+
return tokens;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Split a tokenized command into chained segments on shell control operators
|
|
281
|
+
* (`;`, `&&`, `||`, `|`, `&`). Only UNQUOTED single-token operators split; an
|
|
282
|
+
* operator that arrived inside quotes stays part of its segment.
|
|
283
|
+
*
|
|
284
|
+
* @param {Array<{ text: string, quoted: boolean }>} tokens
|
|
285
|
+
* @returns {Array<Array<{ text: string, quoted: boolean }>>}
|
|
286
|
+
*/
|
|
287
|
+
function splitSegments(tokens) {
|
|
288
|
+
const segments = [];
|
|
289
|
+
let current = [];
|
|
290
|
+
const operators = new Set([';', '&&', '||', '|', '&']);
|
|
291
|
+
for (const tok of tokens) {
|
|
292
|
+
if (!tok.quoted && operators.has(tok.text)) {
|
|
293
|
+
segments.push(current);
|
|
294
|
+
current = [];
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
current.push(tok);
|
|
298
|
+
}
|
|
299
|
+
segments.push(current);
|
|
300
|
+
return segments.filter((s) => s.length > 0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolve the effective argv[0] (the command verb) for a chain segment, skipping
|
|
305
|
+
* leading `VAR=value` env assignments and unwrapping `env …`/`command …` prefixes.
|
|
306
|
+
* Returns the bare program name (basename, no path) or null.
|
|
307
|
+
*
|
|
308
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
309
|
+
* @returns {string|null}
|
|
310
|
+
*/
|
|
311
|
+
function segmentVerb(segment) {
|
|
312
|
+
let i = 0;
|
|
313
|
+
// Skip leading FOO=bar env assignments (unquoted).
|
|
314
|
+
while (i < segment.length && !segment[i].quoted && /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[i].text)) {
|
|
315
|
+
i++;
|
|
316
|
+
}
|
|
317
|
+
// Unwrap `env [VAR=val …]` and `command` prefixes that delegate to a real verb.
|
|
318
|
+
while (i < segment.length) {
|
|
319
|
+
const raw = segment[i].text;
|
|
320
|
+
const verb = raw.replace(/^.*\//, ''); // basename
|
|
321
|
+
if (verb === 'env') {
|
|
322
|
+
i++;
|
|
323
|
+
// env may carry its own VAR=val assignments before the real command
|
|
324
|
+
while (i < segment.length && !segment[i].quoted && /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[i].text)) {
|
|
325
|
+
i++;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (verb === 'command') { i++; continue; }
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (i >= segment.length) return null;
|
|
333
|
+
return segment[i].text.replace(/^.*\//, '');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Build the case-sensitive boundary regex for a blocked pattern. Boundary chars:
|
|
338
|
+
* whitespace + shell operators + quotes. Matches at start/end too.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} pattern
|
|
341
|
+
* @returns {RegExp}
|
|
342
|
+
*/
|
|
343
|
+
function boundaryRegex(pattern) {
|
|
344
|
+
const escaped = pattern.replace(/[.*+?|[\](){}\\^$]/g, '\\$&');
|
|
345
|
+
const boundary = '[\\s;|&(){}`\'"]';
|
|
346
|
+
return new RegExp(`(^|${boundary})${escaped}(${boundary}|$)`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Test whether a blocked pattern occurs in the raw concatenation of a segment's
|
|
351
|
+
* QUOTED token payloads (with the boundary rule applied per token). Used to decide
|
|
352
|
+
* whether an interpreter segment carries the pattern inside its quoted argument.
|
|
353
|
+
*
|
|
354
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
355
|
+
* @param {RegExp} re
|
|
356
|
+
* @returns {boolean}
|
|
357
|
+
*/
|
|
358
|
+
function quotedTokensMatch(segment, re) {
|
|
359
|
+
for (const tok of segment) {
|
|
360
|
+
if (tok.quoted && re.test(normalizeShellWhitespaceExpansions(tok.text, { expandSingleQuoted: true }))) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Test whether a blocked pattern occurs OUTSIDE quoted tokens within a segment.
|
|
369
|
+
* Reconstructs the unquoted skeleton (quoted tokens replaced by a single space
|
|
370
|
+
* placeholder so they cannot bridge an adjacent-token match) and applies the
|
|
371
|
+
* boundary regex.
|
|
372
|
+
*
|
|
373
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
374
|
+
* @param {RegExp} re
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function unquotedSegmentMatch(segment, re) {
|
|
378
|
+
const skeleton = segment.map((t) => (t.quoted ? ' ' : t.text)).join(' ');
|
|
379
|
+
return re.test(skeleton);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Test whether a command string contains a blocked pattern with shell-aware
|
|
384
|
+
* boundaries AND a quoted-payload guard (#641).
|
|
385
|
+
*
|
|
386
|
+
* Verb detection stays boundary-tolerant: a pattern that appears UNQUOTED — including
|
|
387
|
+
* across shell operators (`ls;rm -rf /`, `ls&&rm -rf /`, `(rm -rf /)`, piped into
|
|
388
|
+
* `xargs rm -rf`, prefixed by `FOO=1 …`) — still matches.
|
|
389
|
+
*
|
|
390
|
+
* Quoted-payload guard: a pattern whose ONLY occurrences are wholly inside quoted
|
|
391
|
+
* tokens is treated as inert literal text (no match) UNLESS the enclosing chain
|
|
392
|
+
* segment's verb (argv[0], after skipping env-assignments and unwrapping
|
|
393
|
+
* `env`/`command`) is a shell-exec interpreter (`bash -c "rm -rf /"`,
|
|
394
|
+
* `eval "…"`, `psql -c "DROP TABLE …"`, `find … -exec …`). The guard is applied
|
|
395
|
+
* PER chain segment: a quoted pattern in segment N is judged against segment N's verb.
|
|
396
|
+
*
|
|
397
|
+
* Boundary characters: whitespace, shell operators (`;`, `|`, `&`, `(`, `)`,
|
|
398
|
+
* `{`, `}`, backtick), or string quotes (`'`, `"`). Case-sensitive.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} command — full command string
|
|
401
|
+
* @param {string} pattern — blocked pattern to search for
|
|
402
|
+
* @returns {boolean}
|
|
403
|
+
*/
|
|
404
|
+
export function commandMatchesBlocked(command, pattern) {
|
|
405
|
+
if (!pattern) return false;
|
|
406
|
+
if (typeof command !== 'string' || command.length === 0) return false;
|
|
407
|
+
|
|
408
|
+
const re = boundaryRegex(pattern);
|
|
409
|
+
const normalizedCommand = normalizeShellWhitespaceExpansions(command);
|
|
410
|
+
const payloadNormalizedCommand = normalizeShellWhitespaceExpansions(command, {
|
|
411
|
+
expandSingleQuoted: true,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Fast path: if the boundary regex does not match the raw string at all, no
|
|
415
|
+
// tokenization can produce a match. Shell whitespace expansions can create
|
|
416
|
+
// matches, so test the normalized forms rather than the original command.
|
|
417
|
+
if (!re.test(normalizedCommand) && !re.test(payloadNormalizedCommand)) return false;
|
|
418
|
+
|
|
419
|
+
const segments = splitSegments(tokenizeCommand(normalizedCommand));
|
|
420
|
+
|
|
421
|
+
for (const segment of segments) {
|
|
422
|
+
// 1) Unquoted occurrence anywhere in the segment → always a match.
|
|
423
|
+
if (unquotedSegmentMatch(segment, re)) return true;
|
|
424
|
+
|
|
425
|
+
// 2) Quoted occurrence → only a match when the segment verb is an interpreter
|
|
426
|
+
// that executes its quoted payload.
|
|
427
|
+
if (quotedTokensMatch(segment, re)) {
|
|
428
|
+
const verb = segmentVerb(segment);
|
|
429
|
+
if (verb && SHELL_EXEC_INTERPRETERS.has(verb)) return true;
|
|
430
|
+
// else: inert literal inside quotes for a non-interpreter verb → no match
|
|
431
|
+
// for THIS segment; keep scanning other segments.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Build an actionable suggestion string for a blocked command pattern.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} pattern — the blocked command pattern
|
|
442
|
+
* @returns {string}
|
|
443
|
+
*/
|
|
444
|
+
export function suggestForCommandBlock(pattern) {
|
|
445
|
+
switch (pattern) {
|
|
446
|
+
case 'rm -rf':
|
|
447
|
+
return 'Destructive deletion is blocked. Move specific files instead or use trash-cli.';
|
|
448
|
+
case 'git push --force':
|
|
449
|
+
case 'git push -f':
|
|
450
|
+
return "Force-push is blocked. Use 'git push --force-with-lease' after coordinator approval.";
|
|
451
|
+
case 'git reset --hard':
|
|
452
|
+
return "Hard reset is blocked. Use 'git reset --soft' or 'git stash' to preserve work.";
|
|
453
|
+
case 'git checkout -- .':
|
|
454
|
+
return 'Whole-tree discard is blocked. Target specific files instead.';
|
|
455
|
+
default:
|
|
456
|
+
return `Blocked command pattern '${pattern}' is not permitted during wave execution.`;
|
|
457
|
+
}
|
|
458
|
+
}
|