session-orchestrator 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
30
30
|
import { readFile } from 'node:fs/promises';
|
|
31
31
|
import path from 'node:path';
|
|
32
|
-
import os from 'node:os';
|
|
33
32
|
|
|
34
33
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
35
34
|
import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
|
|
@@ -43,6 +42,7 @@ import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
|
|
|
43
42
|
import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
44
43
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
45
44
|
import { createSecretValueMasker } from '../secret-masker.mjs';
|
|
45
|
+
import { expandTilde } from '../common.mjs';
|
|
46
46
|
|
|
47
47
|
/** Frontmatter sentinel that identifies generator-owned narrative files. */
|
|
48
48
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
|
|
@@ -63,23 +63,6 @@ const SECTION_TITLES = {
|
|
|
63
63
|
|
|
64
64
|
// ── Raw section extraction ──────────────────────────────────────────────────────
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Expand a leading `~` to the current user's home directory.
|
|
68
|
-
*
|
|
69
|
-
* NOTE: deferred shared-helper extraction. The same `expandHome` pattern lives in
|
|
70
|
-
* other vault-status modules; W2 forbids introducing a shared new file, so this is
|
|
71
|
-
* inlined here. Consolidate into a shared util in a follow-up wave.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} p
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
function expandHome(p) {
|
|
77
|
-
if (typeof p !== 'string' || p.length === 0) return p;
|
|
78
|
-
if (p === '~') return os.homedir();
|
|
79
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
80
|
-
return p;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
66
|
/**
|
|
84
67
|
* Match an ATX markdown heading line. Returns `{ level, text }` or null.
|
|
85
68
|
*
|
|
@@ -235,15 +218,27 @@ export function extractNarrative(stateMdContents) {
|
|
|
235
218
|
* REFERENCE (byte-identical downstream), and `mask` passes non-strings through —
|
|
236
219
|
* masking must never be the reason a mirror run dies.
|
|
237
220
|
*
|
|
221
|
+
* Takes the ALREADY-BUILT masker (rather than building its own) so
|
|
222
|
+
* `runNarrativeMirror` can build it exactly ONCE per run and reuse it for both
|
|
223
|
+
* the actual masking below AND the `orchestrator.secret_masker.applied`
|
|
224
|
+
* telemetry (#1028) — the masker's `needleCount` is measured once, not
|
|
225
|
+
* potentially twice with process.env read between the two reads.
|
|
226
|
+
*
|
|
238
227
|
* @param {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }} narrative
|
|
239
|
-
* @
|
|
228
|
+
* @param {{ mask: (text: string) => string, needleCount: number }} masker
|
|
229
|
+
* @returns {{ narrative: { waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }, hits: number }}
|
|
240
230
|
*/
|
|
241
|
-
function maskNarrative(narrative) {
|
|
242
|
-
const { mask, needleCount } =
|
|
243
|
-
if (needleCount === 0) return narrative;
|
|
231
|
+
function maskNarrative(narrative, masker) {
|
|
232
|
+
const { mask, needleCount } = masker;
|
|
233
|
+
if (needleCount === 0) return { narrative, hits: 0 };
|
|
244
234
|
|
|
235
|
+
let hits = 0;
|
|
245
236
|
const walk = (value) => {
|
|
246
|
-
if (typeof value === 'string')
|
|
237
|
+
if (typeof value === 'string') {
|
|
238
|
+
const masked = mask(value);
|
|
239
|
+
if (masked !== value) hits++;
|
|
240
|
+
return masked;
|
|
241
|
+
}
|
|
247
242
|
if (Array.isArray(value)) return value.map(walk);
|
|
248
243
|
if (value && typeof value === 'object') {
|
|
249
244
|
const out = {};
|
|
@@ -253,7 +248,7 @@ function maskNarrative(narrative) {
|
|
|
253
248
|
return value;
|
|
254
249
|
};
|
|
255
250
|
|
|
256
|
-
return walk(narrative);
|
|
251
|
+
return { narrative: walk(narrative), hits };
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
// ── Pure render ────────────────────────────────────────────────────────────────
|
|
@@ -421,6 +416,30 @@ function normalizeUpdated(content) {
|
|
|
421
416
|
* between runs: run 1 (env set) → `skipped-noop`, run 2 (env absent) → `written`
|
|
422
417
|
* with the raw value on disk.
|
|
423
418
|
*
|
|
419
|
+
* WHY THERE IS NO `maskerWouldChange` RE-PROBE HERE (#1214). The two sibling
|
|
420
|
+
* sinks in `vault-mirror/process.mjs` (processLearning, processSession) guard
|
|
421
|
+
* all five of their `skipped-noop` returns with `maskerWouldChange(existing)`,
|
|
422
|
+
* because their five-field / date comparisons can report a match while the
|
|
423
|
+
* on-disk note still carries a raw needle. This sink compares the WHOLE
|
|
424
|
+
* document, which makes that residue unreachable: `matchesModuloRedaction`
|
|
425
|
+
* only fires when the EXISTING side carries a `[REDACTED]` marker, and every
|
|
426
|
+
* literal segment around such a marker must appear verbatim in the candidate —
|
|
427
|
+
* but the candidate comes through `maskNarrative` with the CURRENT masker, so
|
|
428
|
+
* it can never contain a value that masker would redact. A raw needle on disk
|
|
429
|
+
* therefore always lands in a literal segment that fails to match, and the run
|
|
430
|
+
* writes. Measured 2026-09-04 at HEAD cd785003 in both directions (no marker on
|
|
431
|
+
* disk; marker already on disk plus a second needle entering the env): both
|
|
432
|
+
* returned `written` with the raw value gone. The two tests named `(#1214)` in
|
|
433
|
+
* tests/lib/vault-status/narrative-mirror.test.mjs pin that invariant — an
|
|
434
|
+
* argument-order slip on the call below makes both return `skipped-noop` and
|
|
435
|
+
* republish the raw value, which is how they were verified to bite.
|
|
436
|
+
*
|
|
437
|
+
* NAMED CEILING: the invariant rests on `maskNarrative` walking EVERY string
|
|
438
|
+
* that reaches the render. `repo` is fed to `renderNarrative` outside that walk
|
|
439
|
+
* (it is a directory basename, not STATE.md content), so a future rendered
|
|
440
|
+
* field added outside the walk would reopen this. Revisit trigger: any new
|
|
441
|
+
* `renderNarrative` input that does not pass through `maskNarrative`.
|
|
442
|
+
*
|
|
424
443
|
* `matchesModuloRedaction` is IMPORTED from `../vault-mirror/process.mjs`, not
|
|
425
444
|
* re-derived here: `secret-masker.mjs`'s header states this compensation as a
|
|
426
445
|
* contract binding on every consumer that diffs a written artifact against a fresh
|
|
@@ -681,6 +700,56 @@ async function emitNarrativeEvent({ repoRoot, action, path: outputPath, chars, e
|
|
|
681
700
|
}
|
|
682
701
|
}
|
|
683
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Emit this channel's `orchestrator.secret_masker.applied` record (#1028
|
|
705
|
+
* residue 2). Until now the narrative mirror was the ONLY masker channel of
|
|
706
|
+
* the three (`vault-mirror`, `narrative-mirror`, `export-hw-learnings`) that
|
|
707
|
+
* never emitted this event — `maskNarrative` has masked unconditionally since
|
|
708
|
+
* #1025, but nothing told the ledger. A census grepping event NAMES for
|
|
709
|
+
* `narrative` therefore read as "this channel masks nothing", which was never
|
|
710
|
+
* true; it just never SAID so.
|
|
711
|
+
*
|
|
712
|
+
* Emitted UNCONDITIONALLY once per `mirrorNarrative` run whenever a `repoRoot`
|
|
713
|
+
* was resolvable — including every skip outcome, not only `written` — so
|
|
714
|
+
* `needle_count: 0` is a REAL measured "the env carries no secret-shaped
|
|
715
|
+
* value" rather than a stand-in for "the masker never ran" (same
|
|
716
|
+
* force-build-don't-default posture as `getMaskerStats()` in
|
|
717
|
+
* `scripts/lib/vault-mirror/process.mjs`). `records` is the literal `1`: one
|
|
718
|
+
* `mirrorNarrative` run always masks (or attempts to mask) exactly one
|
|
719
|
+
* STATE.md, unlike the CLI channels that fan out over many JSONL lines.
|
|
720
|
+
*
|
|
721
|
+
* Same field set as the `vault-mirror` channel's own masker emit
|
|
722
|
+
* (`channel`, `needle_count`, `records`, `hits`, `dry_run`), PLUS
|
|
723
|
+
* `session_id`/`semantic_session_id` via `sessionAttribution` — matching this
|
|
724
|
+
* module's own `emitNarrativeEvent` above rather than the CLI's 2-arg
|
|
725
|
+
* `emitEvent` call, because `mirrorNarrative` (unlike the CLI) already always
|
|
726
|
+
* carries a `repoRoot` to attribute against.
|
|
727
|
+
*
|
|
728
|
+
* Best-effort: never throws, never alters the mirror result.
|
|
729
|
+
*
|
|
730
|
+
* @param {{ repoRoot?: string, needleCount: number, hits: number, dryRun: boolean }} opts
|
|
731
|
+
* @returns {Promise<void>}
|
|
732
|
+
*/
|
|
733
|
+
async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
|
|
734
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
|
|
735
|
+
try {
|
|
736
|
+
await emitEvent(
|
|
737
|
+
'orchestrator.secret_masker.applied',
|
|
738
|
+
{
|
|
739
|
+
channel: 'narrative-mirror',
|
|
740
|
+
needle_count: needleCount,
|
|
741
|
+
records: 1,
|
|
742
|
+
hits,
|
|
743
|
+
dry_run: dryRun,
|
|
744
|
+
...sessionAttribution(repoRoot),
|
|
745
|
+
},
|
|
746
|
+
{ repoRoot },
|
|
747
|
+
);
|
|
748
|
+
} catch {
|
|
749
|
+
/* Best-effort telemetry — see emitNarrativeEvent above for why. */
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
684
753
|
// ── Convenience orchestration ────────────────────────────────────────────────────
|
|
685
754
|
|
|
686
755
|
/**
|
|
@@ -740,42 +809,62 @@ export async function mirrorNarrative(opts) {
|
|
|
740
809
|
chars: outcome.chars,
|
|
741
810
|
});
|
|
742
811
|
|
|
812
|
+
// #1028 residue 2: the masker-telemetry sibling of the narrative event
|
|
813
|
+
// above, on every non-throwing outcome (including skips) — see
|
|
814
|
+
// emitMaskerEvent for why this fires unconditionally.
|
|
815
|
+
await emitMaskerEvent({
|
|
816
|
+
repoRoot: opts?.repoRoot,
|
|
817
|
+
needleCount: outcome.needleCount,
|
|
818
|
+
hits: outcome.hits,
|
|
819
|
+
dryRun: outcome.dryRun,
|
|
820
|
+
});
|
|
821
|
+
|
|
743
822
|
return outcome.result;
|
|
744
823
|
}
|
|
745
824
|
|
|
746
825
|
/**
|
|
747
826
|
* The mirror itself — every early return of {@link mirrorNarrative} lives here.
|
|
748
827
|
*
|
|
749
|
-
* Split out so that
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
828
|
+
* Split out so that the two emit sites in `mirrorNarrative` (the narrative
|
|
829
|
+
* event AND, since #1028, the masker event) each cover EVERY outcome from
|
|
830
|
+
* ONE call: a future early return added inside this function is telemetered
|
|
831
|
+
* by construction, whereas hand-placing an emit beside each of the seven
|
|
832
|
+
* `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
|
|
753
833
|
* rather than inside it because the returned object is a PUBLIC shape that
|
|
754
834
|
* callers (and tests) compare with `toEqual` — adding a key there would be an
|
|
755
835
|
* observable contract change for a purely internal measurement.
|
|
756
836
|
*
|
|
757
837
|
* @param {Parameters<typeof mirrorNarrative>[0]} opts
|
|
758
|
-
* @returns {Promise<{ result: { action: string, path?: string }, chars?: number }>}
|
|
838
|
+
* @returns {Promise<{ result: { action: string, path?: string }, chars?: number, needleCount: number, hits: number, dryRun: boolean }>}
|
|
759
839
|
*/
|
|
760
840
|
async function runNarrativeMirror(opts) {
|
|
761
841
|
const { repoRoot, repo, now = new Date(), dryRun = false, fs: injectedFs, hostPaths } = opts;
|
|
762
842
|
|
|
763
843
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
764
|
-
return { result: { action: 'skipped-vault-disabled' } };
|
|
844
|
+
return { result: { action: 'skipped-vault-disabled' }, needleCount: 0, hits: 0, dryRun };
|
|
765
845
|
}
|
|
766
846
|
|
|
847
|
+
// #1028: the masker is built ONCE per run, right here — before every
|
|
848
|
+
// remaining early-return below — so `mirrorNarrative`'s
|
|
849
|
+
// `orchestrator.secret_masker.applied` emit always carries a REAL measured
|
|
850
|
+
// `needle_count` on every outcome (including a skip), rather than a
|
|
851
|
+
// defaulted 0 that would be indistinguishable from "the masker never ran".
|
|
852
|
+
// Same "force-build, never default" posture as `getMaskerStats()` in
|
|
853
|
+
// `scripts/lib/vault-mirror/process.mjs`.
|
|
854
|
+
const masker = createSecretValueMasker(process.env);
|
|
855
|
+
|
|
767
856
|
// Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
|
|
768
857
|
let config;
|
|
769
858
|
try {
|
|
770
859
|
const configText = await readConfigFile(repoRoot);
|
|
771
860
|
config = parseSessionConfig(configText, { hostPaths });
|
|
772
861
|
} catch {
|
|
773
|
-
return { result: { action: 'skipped-vault-disabled' } };
|
|
862
|
+
return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
774
863
|
}
|
|
775
864
|
|
|
776
865
|
const vaultIntegration = config?.['vault-integration'];
|
|
777
866
|
if (!vaultIntegration || vaultIntegration.enabled !== true) {
|
|
778
|
-
return { result: { action: 'skipped-vault-disabled' } };
|
|
867
|
+
return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
779
868
|
}
|
|
780
869
|
|
|
781
870
|
// Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
|
|
@@ -792,10 +881,10 @@ async function runNarrativeMirror(opts) {
|
|
|
792
881
|
|
|
793
882
|
const rawVaultDir = vaultIntegration['vault-dir'];
|
|
794
883
|
if (!rawVaultDir || typeof rawVaultDir !== 'string') {
|
|
795
|
-
return { result: { action: 'skipped-vault-disabled' } };
|
|
884
|
+
return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
796
885
|
}
|
|
797
886
|
|
|
798
|
-
const vaultDir = path.resolve(
|
|
887
|
+
const vaultDir = path.resolve(expandTilde(rawVaultDir));
|
|
799
888
|
const candidateSlug = subjectToSlug(repoName) || 'unknown';
|
|
800
889
|
// Loose-match against existing 01-projects/ folders before minting a new
|
|
801
890
|
// slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
|
|
@@ -816,7 +905,7 @@ async function runNarrativeMirror(opts) {
|
|
|
816
905
|
canonicalizeRoot: true,
|
|
817
906
|
});
|
|
818
907
|
if (!inside.ok) {
|
|
819
|
-
return { result: { action: 'skipped-invalid-path', path: outputPath } };
|
|
908
|
+
return { result: { action: 'skipped-invalid-path', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
820
909
|
}
|
|
821
910
|
|
|
822
911
|
// Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
|
|
@@ -825,13 +914,13 @@ async function runNarrativeMirror(opts) {
|
|
|
825
914
|
try {
|
|
826
915
|
stateContents = await readFile(stateMdPath, 'utf8');
|
|
827
916
|
} catch {
|
|
828
|
-
return { result: { action: 'skipped-no-statemd', path: outputPath } };
|
|
917
|
+
return { result: { action: 'skipped-no-statemd', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
829
918
|
}
|
|
830
919
|
|
|
831
920
|
// #1025: the ONE masking site for the narrative mirror — after the frontmatter
|
|
832
921
|
// parse (so `[REDACTED]` can never break it) and before the render, the
|
|
833
922
|
// idempotency comparison and the write. See maskNarrative above.
|
|
834
|
-
const narrative = maskNarrative(extractNarrative(stateContents));
|
|
923
|
+
const { narrative, hits } = maskNarrative(extractNarrative(stateContents), masker);
|
|
835
924
|
const content = renderNarrative({ repo: repoName, narrative, now });
|
|
836
925
|
|
|
837
926
|
// `chars` measures the document THIS call rendered — so it is present on
|
|
@@ -840,5 +929,8 @@ async function runNarrativeMirror(opts) {
|
|
|
840
929
|
return {
|
|
841
930
|
result: writeNarrative({ outputPath, content, dryRun, fs: injectedFs }),
|
|
842
931
|
chars: content.length,
|
|
932
|
+
needleCount: masker.needleCount,
|
|
933
|
+
hits,
|
|
934
|
+
dryRun,
|
|
843
935
|
};
|
|
844
936
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch-common.mjs — shared base for the two foreign-dispatch adapters:
|
|
3
|
+
* `foreign-dispatch.mjs` (Cursor channel, `cursor-agent`) and
|
|
4
|
+
* `remote-dispatch.mjs` (remote-host channel, `offload`). Two adapters, one
|
|
5
|
+
* base (#1204).
|
|
6
|
+
*
|
|
7
|
+
* These six symbols moved here because each one is an enforcement point or a
|
|
8
|
+
* budget constant that must NOT diverge between the two channels: the
|
|
9
|
+
* `never_foreign` role lock, the run-id path-segment validator, the
|
|
10
|
+
* SIGTERM→SIGKILL child runner, and the two dispatch-budget constants. Before
|
|
11
|
+
* this module existed, `remote-dispatch.mjs` imported all five from
|
|
12
|
+
* `foreign-dispatch.mjs` directly — a Cursor-specific file acting as a shared
|
|
13
|
+
* base for a channel that has nothing to do with Cursor. Both adapters now
|
|
14
|
+
* import from here instead, so neither exports something the other must reach
|
|
15
|
+
* through.
|
|
16
|
+
*
|
|
17
|
+
* Revisit-Trigger: if a THIRD dispatch channel ever needs a different budget
|
|
18
|
+
* default or a different never-foreign list, split the constant per-channel
|
|
19
|
+
* at that point rather than overriding it at the call site — a call-site
|
|
20
|
+
* override would silently diverge from the one enforcement point this module
|
|
21
|
+
* exists to be.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Roles that may NEVER run on a foreign model. Copied verbatim from
|
|
26
|
+
* account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
|
|
27
|
+
* Keep in sync by hand — the YAML lives in a different repo and is parsed there
|
|
28
|
+
* by awk, not by a shared library.
|
|
29
|
+
* @type {readonly string[]}
|
|
30
|
+
*/
|
|
31
|
+
export const NEVER_FOREIGN_ROLES = Object.freeze([
|
|
32
|
+
'impl-core',
|
|
33
|
+
'security-review',
|
|
34
|
+
'migration',
|
|
35
|
+
'release',
|
|
36
|
+
'secrets',
|
|
37
|
+
'incident',
|
|
38
|
+
'refactor-crosscut',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** Default wall-clock budget for one foreign dispatch, in seconds.
|
|
42
|
+
* This is a FLOOR, not a suggestion: measured 2026-08-23, `cursor-grok-4.6-high`
|
|
43
|
+
* ran 2 of 3 hard-test tasks past a 540 s cap (recorded as DNF). Lowering this
|
|
44
|
+
* manufactures timeouts and reads as model failure. */
|
|
45
|
+
export const DEFAULT_TIMEOUT_SEC = 900;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Grace period between SIGTERM and SIGKILL, in ms.
|
|
49
|
+
*
|
|
50
|
+
* `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
|
|
51
|
+
* ignores it never emits `close`, and this module's only resolve paths are
|
|
52
|
+
* `close` and `error` — so without escalation a wedged `cursor-agent` hangs
|
|
53
|
+
* `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
|
|
54
|
+
* catchable, so the escalation always terminates.
|
|
55
|
+
*
|
|
56
|
+
* Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
|
|
57
|
+
* stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
|
|
58
|
+
* measured needing longer than that to shut down — raise the constant, never
|
|
59
|
+
* drop the escalation.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
62
|
+
|
|
63
|
+
/** A runId names a directory and a log file. Anything outside this alphabet —
|
|
64
|
+
* or the two relative-path literals the alphabet happens to admit — can escape
|
|
65
|
+
* the parent it is joined to. */
|
|
66
|
+
const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {unknown} runId
|
|
70
|
+
* @returns {boolean} true when the id is safe to use as a path segment.
|
|
71
|
+
*/
|
|
72
|
+
export function isSafeRunId(runId) {
|
|
73
|
+
const s = String(runId ?? '');
|
|
74
|
+
if (s === '.' || s === '..') return false; // admitted by SAFE_RUN_ID, still an escape
|
|
75
|
+
return SAFE_RUN_ID.test(s);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} role
|
|
80
|
+
* @returns {boolean} true when the role is locked to Claude.
|
|
81
|
+
*/
|
|
82
|
+
export function isNeverForeignRole(role) {
|
|
83
|
+
return NEVER_FOREIGN_ROLES.includes(String(role || '').trim());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run the child process, capturing stdout+stderr merged, with a hand-rolled
|
|
88
|
+
* SIGTERM timeout that ESCALATES to SIGKILL. `spawnFn` is the DI seam so tests
|
|
89
|
+
* never touch the real `cursor-agent` binary.
|
|
90
|
+
*
|
|
91
|
+
* The escalation is the difference between a bounded dispatch and a hung wave:
|
|
92
|
+
* this function's only resolve paths are the child's `close` and `error`
|
|
93
|
+
* events, so a child that ignores SIGTERM never lets the promise settle. See
|
|
94
|
+
* {@link DEFAULT_KILL_GRACE_MS}.
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
|
|
97
|
+
* timerFired: boolean, killSignals: string[], errorCode: string|null}>}
|
|
98
|
+
*/
|
|
99
|
+
export function runChild(cmd, args, options, { spawnFn, timeoutMs, killGraceMs }) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
/** @type {string[]} */
|
|
102
|
+
const killSignals = [];
|
|
103
|
+
let child;
|
|
104
|
+
try {
|
|
105
|
+
child = spawnFn(cmd, args, options);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
resolve({
|
|
108
|
+
capture: `spawn failed: ${err && err.message}`,
|
|
109
|
+
exitCode: null,
|
|
110
|
+
signal: null,
|
|
111
|
+
timerFired: false,
|
|
112
|
+
killSignals,
|
|
113
|
+
errorCode: (err && err.code) || null,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let capture = '';
|
|
119
|
+
let timerFired = false;
|
|
120
|
+
let settled = false;
|
|
121
|
+
let errorCode = null;
|
|
122
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
123
|
+
let graceTimer = null;
|
|
124
|
+
|
|
125
|
+
const onChunk = (chunk) => {
|
|
126
|
+
capture += chunk.toString();
|
|
127
|
+
};
|
|
128
|
+
child.stdout?.on('data', onChunk);
|
|
129
|
+
child.stderr?.on('data', onChunk);
|
|
130
|
+
|
|
131
|
+
const send = (signal) => {
|
|
132
|
+
killSignals.push(signal);
|
|
133
|
+
try {
|
|
134
|
+
child.kill(signal);
|
|
135
|
+
} catch {
|
|
136
|
+
/* already gone */
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
timerFired = true;
|
|
142
|
+
send('SIGTERM');
|
|
143
|
+
// SIGTERM is a request; SIGKILL is not. A child that installs a handler
|
|
144
|
+
// and declines to exit would otherwise hang this promise forever.
|
|
145
|
+
graceTimer = setTimeout(() => send('SIGKILL'), killGraceMs);
|
|
146
|
+
graceTimer.unref?.();
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
|
|
149
|
+
const finish = (exitCode, signal) => {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
154
|
+
resolve({ capture, exitCode, signal, timerFired, killSignals, errorCode });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
child.on('error', (err) => {
|
|
158
|
+
errorCode = (err && err.code) || null;
|
|
159
|
+
capture += `\nchild error: ${err && err.message}`;
|
|
160
|
+
finish(null, null);
|
|
161
|
+
});
|
|
162
|
+
child.on('close', (code, signal) => finish(code, signal ?? null));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -54,45 +54,13 @@ import path from 'node:path';
|
|
|
54
54
|
|
|
55
55
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
56
56
|
import { isPathInside } from '../path-utils.mjs';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
export const NEVER_FOREIGN_ROLES = Object.freeze([
|
|
66
|
-
'impl-core',
|
|
67
|
-
'security-review',
|
|
68
|
-
'migration',
|
|
69
|
-
'release',
|
|
70
|
-
'secrets',
|
|
71
|
-
'incident',
|
|
72
|
-
'refactor-crosscut',
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
/** Default wall-clock budget for one foreign dispatch, in seconds.
|
|
76
|
-
* This is a FLOOR, not a suggestion: measured 2026-08-23, `cursor-grok-4.6-high`
|
|
77
|
-
* ran 2 of 3 hard-test tasks past a 540 s cap (recorded as DNF). Lowering this
|
|
78
|
-
* manufactures timeouts and reads as model failure. */
|
|
79
|
-
export const DEFAULT_TIMEOUT_SEC = 900;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Grace period between SIGTERM and SIGKILL, in ms.
|
|
83
|
-
*
|
|
84
|
-
* `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
|
|
85
|
-
* ignores it never emits `close`, and this module's only resolve paths are
|
|
86
|
-
* `close` and `error` — so without escalation a wedged `cursor-agent` hangs
|
|
87
|
-
* `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
|
|
88
|
-
* catchable, so the escalation always terminates.
|
|
89
|
-
*
|
|
90
|
-
* Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
|
|
91
|
-
* stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
|
|
92
|
-
* measured needing longer than that to shut down — raise the constant, never
|
|
93
|
-
* drop the escalation.
|
|
94
|
-
*/
|
|
95
|
-
export const DEFAULT_KILL_GRACE_MS = 10_000;
|
|
57
|
+
import {
|
|
58
|
+
DEFAULT_KILL_GRACE_MS,
|
|
59
|
+
DEFAULT_TIMEOUT_SEC,
|
|
60
|
+
isNeverForeignRole,
|
|
61
|
+
isSafeRunId,
|
|
62
|
+
runChild,
|
|
63
|
+
} from './dispatch-common.mjs';
|
|
96
64
|
|
|
97
65
|
/**
|
|
98
66
|
* argv prefix for every foreign dispatch, ahead of `--model`/`--output-format`.
|
|
@@ -156,29 +124,6 @@ export const FOREIGN_ENV_ALLOWLIST = Object.freeze([
|
|
|
156
124
|
* directory named this way. */
|
|
157
125
|
export const FOREIGN_TMP_PREFIX = 'so-foreign';
|
|
158
126
|
|
|
159
|
-
/** A runId names a directory and a log file. Anything outside this alphabet —
|
|
160
|
-
* or the two relative-path literals the alphabet happens to admit — can escape
|
|
161
|
-
* the parent it is joined to. */
|
|
162
|
-
const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* @param {unknown} runId
|
|
166
|
-
* @returns {boolean} true when the id is safe to use as a path segment.
|
|
167
|
-
*/
|
|
168
|
-
function isSafeRunId(runId) {
|
|
169
|
-
const s = String(runId ?? '');
|
|
170
|
-
if (s === '.' || s === '..') return false; // admitted by SAFE_RUN_ID, still an escape
|
|
171
|
-
return SAFE_RUN_ID.test(s);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* @param {string} role
|
|
176
|
-
* @returns {boolean} true when the role is locked to Claude.
|
|
177
|
-
*/
|
|
178
|
-
export function isNeverForeignRole(role) {
|
|
179
|
-
return NEVER_FOREIGN_ROLES.includes(String(role || '').trim());
|
|
180
|
-
}
|
|
181
|
-
|
|
182
127
|
/**
|
|
183
128
|
* Build the allowlisted child environment.
|
|
184
129
|
* @param {Record<string, string|undefined>} [source]
|
|
@@ -317,86 +262,6 @@ export function parseCursorStream(raw) {
|
|
|
317
262
|
*/
|
|
318
263
|
export const MEASUREMENT_EXCLUDES = Object.freeze(['node_modules']);
|
|
319
264
|
|
|
320
|
-
/**
|
|
321
|
-
* Run the child process, capturing stdout+stderr merged, with a hand-rolled
|
|
322
|
-
* SIGTERM timeout that ESCALATES to SIGKILL. `spawnFn` is the DI seam so tests
|
|
323
|
-
* never touch the real `cursor-agent` binary.
|
|
324
|
-
*
|
|
325
|
-
* The escalation is the difference between a bounded dispatch and a hung wave:
|
|
326
|
-
* this function's only resolve paths are the child's `close` and `error`
|
|
327
|
-
* events, so a child that ignores SIGTERM never lets the promise settle. See
|
|
328
|
-
* {@link DEFAULT_KILL_GRACE_MS}.
|
|
329
|
-
*
|
|
330
|
-
* @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
|
|
331
|
-
* timerFired: boolean, killSignals: string[], errorCode: string|null}>}
|
|
332
|
-
*/
|
|
333
|
-
function runChild(cmd, args, options, { spawnFn, timeoutMs, killGraceMs }) {
|
|
334
|
-
return new Promise((resolve) => {
|
|
335
|
-
/** @type {string[]} */
|
|
336
|
-
const killSignals = [];
|
|
337
|
-
let child;
|
|
338
|
-
try {
|
|
339
|
-
child = spawnFn(cmd, args, options);
|
|
340
|
-
} catch (err) {
|
|
341
|
-
resolve({
|
|
342
|
-
capture: `spawn failed: ${err && err.message}`,
|
|
343
|
-
exitCode: null,
|
|
344
|
-
signal: null,
|
|
345
|
-
timerFired: false,
|
|
346
|
-
killSignals,
|
|
347
|
-
errorCode: (err && err.code) || null,
|
|
348
|
-
});
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
let capture = '';
|
|
353
|
-
let timerFired = false;
|
|
354
|
-
let settled = false;
|
|
355
|
-
let errorCode = null;
|
|
356
|
-
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
357
|
-
let graceTimer = null;
|
|
358
|
-
|
|
359
|
-
const onChunk = (chunk) => {
|
|
360
|
-
capture += chunk.toString();
|
|
361
|
-
};
|
|
362
|
-
child.stdout?.on('data', onChunk);
|
|
363
|
-
child.stderr?.on('data', onChunk);
|
|
364
|
-
|
|
365
|
-
const send = (signal) => {
|
|
366
|
-
killSignals.push(signal);
|
|
367
|
-
try {
|
|
368
|
-
child.kill(signal);
|
|
369
|
-
} catch {
|
|
370
|
-
/* already gone */
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const timer = setTimeout(() => {
|
|
375
|
-
timerFired = true;
|
|
376
|
-
send('SIGTERM');
|
|
377
|
-
// SIGTERM is a request; SIGKILL is not. A child that installs a handler
|
|
378
|
-
// and declines to exit would otherwise hang this promise forever.
|
|
379
|
-
graceTimer = setTimeout(() => send('SIGKILL'), killGraceMs);
|
|
380
|
-
graceTimer.unref?.();
|
|
381
|
-
}, timeoutMs);
|
|
382
|
-
|
|
383
|
-
const finish = (exitCode, signal) => {
|
|
384
|
-
if (settled) return;
|
|
385
|
-
settled = true;
|
|
386
|
-
clearTimeout(timer);
|
|
387
|
-
if (graceTimer) clearTimeout(graceTimer);
|
|
388
|
-
resolve({ capture, exitCode, signal, timerFired, killSignals, errorCode });
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
child.on('error', (err) => {
|
|
392
|
-
errorCode = (err && err.code) || null;
|
|
393
|
-
capture += `\nchild error: ${err && err.message}`;
|
|
394
|
-
finish(null, null);
|
|
395
|
-
});
|
|
396
|
-
child.on('close', (code, signal) => finish(code, signal ?? null));
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
265
|
/** Read `git` output as text, returning '' on failure (a detached worktree with
|
|
401
266
|
* no commits still answers, but a broken invocation must not throw here). */
|
|
402
267
|
function gitText(args, execFn) {
|