session-orchestrator 5.12.0 → 5.13.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 +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -33,19 +33,22 @@ import { existsSync, writeSync } from 'node:fs';
|
|
|
33
33
|
import { execFile } from 'node:child_process';
|
|
34
34
|
import { promisify } from 'node:util';
|
|
35
35
|
import { randomUUID } from 'node:crypto';
|
|
36
|
+
import { hostname } from 'node:os';
|
|
36
37
|
import path from 'node:path';
|
|
37
38
|
|
|
38
39
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
39
40
|
import { ensureOrchestratorIgnore } from './_lib/orchestrator-ignore.mjs';
|
|
41
|
+
import { resolveSessionStartRoot } from './_lib/hook-root.mjs';
|
|
40
42
|
|
|
41
43
|
import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
|
|
42
44
|
import { readNavigatorLease, fleetIntegrationEnabled } from '../scripts/lib/fleet-protocol.mjs';
|
|
43
45
|
import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
|
|
44
46
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
45
|
-
import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
47
|
+
import { getPlatform, resolveProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
46
48
|
import {
|
|
47
49
|
registerSelf,
|
|
48
50
|
detectPeers,
|
|
51
|
+
countRecentStarts,
|
|
49
52
|
sweepZombies,
|
|
50
53
|
logSweepEvent,
|
|
51
54
|
repoPathHash,
|
|
@@ -58,8 +61,9 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
|
58
61
|
// SSOT for the "is this a re-entry into the same logical session?" question
|
|
59
62
|
// (#1091). Defined in the lock-bootstrap leaf module, which this hook already
|
|
60
63
|
// loads, so the preservation branch below and the lock force-refresh gate can
|
|
61
|
-
// never drift apart.
|
|
62
|
-
|
|
64
|
+
// never drift apart. readClaudePid() is the process witness current-session.json
|
|
65
|
+
// carries next to the raw id (#1746).
|
|
66
|
+
import { SAME_LOGICAL_SESSION_SOURCES, readClaudePid } from './_lib/lock-bootstrap.mjs';
|
|
63
67
|
|
|
64
68
|
const execFileAsync = promisify(execFile);
|
|
65
69
|
|
|
@@ -187,14 +191,20 @@ function supersessionMarker(peer) {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
/**
|
|
190
|
-
* Queue the ONE `additionalContext` string
|
|
191
|
-
*
|
|
192
|
-
*
|
|
194
|
+
* Queue text for the ONE `additionalContext` string of the single end-of-hook
|
|
195
|
+
* flush. Two writers exist (the consent nudge, the #1635 lead notice) and both
|
|
196
|
+
* are needed by the model, so a second text is appended on its own line; an
|
|
197
|
+
* identical text is not repeated. Every character costs context on every
|
|
198
|
+
* session start, so writers stay terse and conditional.
|
|
193
199
|
* @param {string|null|undefined} text
|
|
194
200
|
*/
|
|
195
201
|
function setAdditionalContext(text) {
|
|
196
202
|
if (typeof text !== 'string' || text.length === 0) return;
|
|
197
|
-
pendingAdditionalContext
|
|
203
|
+
if (typeof pendingAdditionalContext !== 'string' || pendingAdditionalContext.length === 0) {
|
|
204
|
+
pendingAdditionalContext = text;
|
|
205
|
+
} else if (!pendingAdditionalContext.split('\n').includes(text)) {
|
|
206
|
+
pendingAdditionalContext = `${pendingAdditionalContext}\n${text}`;
|
|
207
|
+
}
|
|
198
208
|
}
|
|
199
209
|
|
|
200
210
|
/**
|
|
@@ -241,14 +251,16 @@ function flushBanner() {
|
|
|
241
251
|
// ---------------------------------------------------------------------------
|
|
242
252
|
|
|
243
253
|
/**
|
|
244
|
-
* Run a git command in cwd; return trimmed stdout. Returns null on failure
|
|
254
|
+
* Run a git command in cwd; return trimmed stdout. Returns null on failure
|
|
255
|
+
* (including a timeout, when `timeoutMs` is given).
|
|
245
256
|
* @param {string[]} args
|
|
246
257
|
* @param {string} cwd
|
|
258
|
+
* @param {number} [timeoutMs] — kill the git child after this many ms (0 = no limit)
|
|
247
259
|
* @returns {Promise<string|null>}
|
|
248
260
|
*/
|
|
249
|
-
async function gitOutput(args, cwd) {
|
|
261
|
+
async function gitOutput(args, cwd, timeoutMs = 0) {
|
|
250
262
|
try {
|
|
251
|
-
const { stdout } = await execFileAsync('git', args, { cwd });
|
|
263
|
+
const { stdout } = await execFileAsync('git', args, { cwd, timeout: timeoutMs });
|
|
252
264
|
return stdout.trim();
|
|
253
265
|
} catch {
|
|
254
266
|
return null;
|
|
@@ -316,25 +328,27 @@ export async function readColdStartConfig(projectRoot) {
|
|
|
316
328
|
}
|
|
317
329
|
|
|
318
330
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
331
|
+
* Start-wave rule (#1650): WARN when at least START_WAVE_MIN foreign registry
|
|
332
|
+
* entries started within the last START_WAVE_WINDOW_MIN minutes.
|
|
333
|
+
*
|
|
334
|
+
* Why not `peer_count >= concurrent-sessions-warn` (the previous WARN trigger):
|
|
335
|
+
* that is a permanent state, not an event. Measured 2026-10-17 08:08Z/08:09Z
|
|
336
|
+
* over `orchestrator.session.started` since 2026-08-24, dedup session_id +
|
|
337
|
+
* timestamp, per `host_class`: peer_count >= 5 fired on 880/1305 = 67.4% (M4)
|
|
338
|
+
* and 255/991 = 25.7% (M5) of starts — a broken instrument per HR-101.
|
|
339
|
+
* This rule, reconstructed from the start timestamps themselves, fires on
|
|
340
|
+
* 35/1305 = 2.7% (M4) and 52/991 = 5.2% (M5). Neighbouring candidates:
|
|
341
|
+
* >=3 in 2 min 4.0% / 7.1%, >=5 in 2 min 1.7% / 3.3%, >=3 in 5 min 6.3% / 10.7%.
|
|
342
|
+
* 5 min was chosen over 2 min so a wave spread over a few minutes still shows.
|
|
343
|
+
*
|
|
344
|
+
* Ceiling (BV-004): fixed constants, no config key; the reconstruction is an
|
|
345
|
+
* upper bound (sessions that ended inside the window leave the registry).
|
|
346
|
+
* Revisit when either host's firing rate on `recent_starts_count >= 5` in the
|
|
347
|
+
* event ledger exceeds 10%, or a start wave that hurt was not flagged.
|
|
348
|
+
* Report only — this single soft signal caps nothing (HR-104).
|
|
322
349
|
*/
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
326
|
-
try {
|
|
327
|
-
const raw = await readFile(path.join(projectRoot, name), 'utf8');
|
|
328
|
-
// Match `concurrent-sessions-warn: <int>` inside resource-thresholds or top-level.
|
|
329
|
-
const m = raw.match(/^\s*concurrent-sessions-warn:\s*(\d+)\b/im);
|
|
330
|
-
if (m) {
|
|
331
|
-
const n = parseInt(m[1], 10);
|
|
332
|
-
if (!Number.isNaN(n) && n > 0) return n;
|
|
333
|
-
}
|
|
334
|
-
} catch { /* file missing is fine */ }
|
|
335
|
-
}
|
|
336
|
-
return DEFAULT;
|
|
337
|
-
}
|
|
350
|
+
const START_WAVE_WINDOW_MIN = 5;
|
|
351
|
+
const START_WAVE_MIN = 5;
|
|
338
352
|
|
|
339
353
|
/**
|
|
340
354
|
* Non-blocking stdin read for Claude Code hook payload. Returns parsed JSON or
|
|
@@ -372,8 +386,13 @@ async function readStdinJson(timeoutMs = 500) {
|
|
|
372
386
|
* Codex/Cursor may not).
|
|
373
387
|
*
|
|
374
388
|
* Best-effort: any persistence failure is swallowed (hook must remain non-blocking).
|
|
389
|
+
*
|
|
390
|
+
* @param {object|null} input — the parsed SessionStart payload.
|
|
391
|
+
* @param {string} projectRoot
|
|
392
|
+
* @param {number|null} [claudePid=null] — readClaudePid(); recorded with the
|
|
393
|
+
* host as the writer's process witness (#1746).
|
|
375
394
|
*/
|
|
376
|
-
async function resolveSessionId(input, projectRoot) {
|
|
395
|
+
async function resolveSessionId(input, projectRoot, claudePid = null) {
|
|
377
396
|
const nativeSource = typeof input?.source === 'string' && input.source.length > 0
|
|
378
397
|
? input.source
|
|
379
398
|
: null;
|
|
@@ -477,9 +496,12 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
477
496
|
// (`resume` was ABSENT from hooks.json until #1091, so on Claude Code this
|
|
478
497
|
// hook never ran on a native resume), `startup|reload|new|resume|fork` on
|
|
479
498
|
// pi, and every source on Cursor (empty matcher). Several of those sources
|
|
480
|
-
// are re-entries into the SAME logical session.
|
|
481
|
-
//
|
|
482
|
-
//
|
|
499
|
+
// are re-entries into the SAME logical session. The `semantic_session_id`
|
|
500
|
+
// (branch+date+mode+n) is meant to stay stable across them. The raw UUID
|
|
501
|
+
// `session_id` does not behave alike per source: on Claude Code `compact`
|
|
502
|
+
// KEEPS it (12/12 compact events in events.jsonl carry
|
|
503
|
+
// `resume_linkage: raw-id`, measured 2026-10-14), while `clear` showed
|
|
504
|
+
// 6/6 `resume_linkage: none`. Whether Claude Code PRESERVES the raw
|
|
483
505
|
// `session_id` across a native resume is what #1091 measures via
|
|
484
506
|
// `native_source` + `resume_linkage` on `orchestrator.session.started` —
|
|
485
507
|
// nothing here ASSUMES either answer, it only reacts to the raw id it can
|
|
@@ -548,6 +570,11 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
548
570
|
pid: process.pid,
|
|
549
571
|
source,
|
|
550
572
|
timestamp: new Date().toISOString(),
|
|
573
|
+
// #1746 — `pid` above is this short-lived hook process, no witness of
|
|
574
|
+
// anything after it exits. The Claude Code process survives /clear, so
|
|
575
|
+
// the next run can tell its own predecessor from a peer that started
|
|
576
|
+
// later in this checkout. Absent without a readable CLAUDE_PID.
|
|
577
|
+
...(typeof claudePid === 'number' ? { claude_pid: claudePid, host: hostname() } : {}),
|
|
551
578
|
// Spread AFTER the base fields so the preserved high-water marks survive
|
|
552
579
|
// (and never clobber the identity fields above).
|
|
553
580
|
...preserved,
|
|
@@ -565,12 +592,18 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
565
592
|
mode: normalizedMode,
|
|
566
593
|
nativeSource,
|
|
567
594
|
resumeLinkage,
|
|
568
|
-
// The raw session_id
|
|
569
|
-
//
|
|
570
|
-
//
|
|
595
|
+
// The raw session_id the LAST session start in this checkout recorded, or
|
|
596
|
+
// null. Every session start rewrites the file, so it is our own previous
|
|
597
|
+
// run only when no peer started here since (#1746). Surfaced (rather than
|
|
598
|
+
// kept local) because bootstrapLock() needs it as the third conjunct of
|
|
599
|
+
// the #1091 F2 force-refresh — see the call site in main().
|
|
571
600
|
predecessorSessionId: typeof prev?.session_id === 'string' && prev.session_id.length > 0
|
|
572
601
|
? prev.session_id
|
|
573
602
|
: null,
|
|
603
|
+
// #1746 — the process witness that writer recorded, unvalidated: the
|
|
604
|
+
// consumer compares it with isSameClaudeProcess(), which admits only an
|
|
605
|
+
// exact pid match on this host.
|
|
606
|
+
predecessorProcess: prev ? { claude_pid: prev.claude_pid, host: prev.host } : null,
|
|
574
607
|
};
|
|
575
608
|
}
|
|
576
609
|
|
|
@@ -780,7 +813,14 @@ function maybeQueueConsentNudge() {
|
|
|
780
813
|
// ---------------------------------------------------------------------------
|
|
781
814
|
|
|
782
815
|
async function main() {
|
|
783
|
-
|
|
816
|
+
// #1605 — the root depends on the payload `source`/`cwd`/`session_id`, so the
|
|
817
|
+
// optional stdin payload is read BEFORE the first writer below. Best-effort:
|
|
818
|
+
// readStdinJson resolves null on a closed/empty/unparseable stdin and caps
|
|
819
|
+
// itself at 500 ms. The rule (entered worktree only for clear/compact/resume,
|
|
820
|
+
// only a verified linked worktree, never over another live session's state)
|
|
821
|
+
// lives in hooks/_lib/hook-root.mjs, beside the readers' own-lock lookup.
|
|
822
|
+
const input = await readStdinJson();
|
|
823
|
+
const projectRoot = await resolveSessionStartRoot(input, resolveProjectDir());
|
|
784
824
|
|
|
785
825
|
// #1515 Pkt 5 — before this hook's own writers (host.json, session.lock,
|
|
786
826
|
// events.jsonl) run: an un-bootstrapped repo gets `.orchestrator/.gitignore`
|
|
@@ -788,11 +828,6 @@ async function main() {
|
|
|
788
828
|
// pay two existsSync and nothing else. Never throws.
|
|
789
829
|
await ensureOrchestratorIgnore(projectRoot);
|
|
790
830
|
|
|
791
|
-
// Read optional stdin payload in parallel with git info so we don't stall
|
|
792
|
-
// the hook. Both are best-effort and the promise races against a 500 ms cap
|
|
793
|
-
// inside readStdinJson itself.
|
|
794
|
-
const stdinPromise = readStdinJson();
|
|
795
|
-
|
|
796
831
|
// Resolve project name: basename of git toplevel, falling back to cwd basename.
|
|
797
832
|
const topLevel = await gitOutput(['rev-parse', '--show-toplevel'], projectRoot);
|
|
798
833
|
const projectName = topLevel
|
|
@@ -809,6 +844,24 @@ async function main() {
|
|
|
809
844
|
const rawHeadSha = await gitOutput(['rev-parse', 'HEAD'], projectRoot);
|
|
810
845
|
const headSha = rawHeadSha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawHeadSha) ? rawHeadSha : null;
|
|
811
846
|
|
|
847
|
+
// #1668 — the upstream state at start, so emit-session can tell a stale
|
|
848
|
+
// head_sha (checkout behind its upstream: a later pull drags foreign commits
|
|
849
|
+
// into `<head_sha>..HEAD`) from a current one. Local refs only — no fetch, no
|
|
850
|
+
// network — and bounded at 1 s per call. No upstream configured, a timeout or
|
|
851
|
+
// a non-numeric count → BOTH keys omitted (not measured), never 0.
|
|
852
|
+
let upstreamSha = null;
|
|
853
|
+
let behind = null;
|
|
854
|
+
if (headSha) {
|
|
855
|
+
const [rawUpstream, rawBehind] = await Promise.all([
|
|
856
|
+
gitOutput(['rev-parse', '--verify', '--quiet', '@{u}^{commit}'], projectRoot, 1000),
|
|
857
|
+
gitOutput(['rev-list', '--count', 'HEAD..@{u}'], projectRoot, 1000),
|
|
858
|
+
]);
|
|
859
|
+
if (rawUpstream && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawUpstream) && rawBehind && /^\d+$/.test(rawBehind)) {
|
|
860
|
+
upstreamSha = rawUpstream;
|
|
861
|
+
behind = Number(rawBehind);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
812
865
|
// v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
|
|
813
866
|
// The ask-via-tool nudge rides the same opt-out flag — both are coordinator
|
|
814
867
|
// reminders shown at session start; users who silence one expect silence.
|
|
@@ -872,13 +925,18 @@ async function main() {
|
|
|
872
925
|
|
|
873
926
|
// v3.1.0 multi-session registry (#168). All steps best-effort — failures
|
|
874
927
|
// must never break the hook, which is informational-only.
|
|
875
|
-
const input = await stdinPromise;
|
|
876
928
|
// #1091 — the native SessionStart `source` (Claude Code: startup|resume|
|
|
877
929
|
// clear|compact), plus the linkage decision resolveSessionId() derived from
|
|
878
930
|
// it (see SAME_LOGICAL_SESSION_SOURCES). Both are surfaced on the event so
|
|
879
931
|
// #1091 gets its measurement instead of an inference.
|
|
880
|
-
|
|
881
|
-
|
|
932
|
+
// #1641: the Claude Code process survives /clear while the raw id and the
|
|
933
|
+
// semantic label are minted anew — its pid is the witness that a live lock,
|
|
934
|
+
// or the current-session.json entry (#1746), is our own predecessor's.
|
|
935
|
+
// Admitted only as this hook's direct parent: a Codex child of a Claude
|
|
936
|
+
// session inherits the variable.
|
|
937
|
+
const claudePid = readClaudePid();
|
|
938
|
+
const { sessionId, semanticSessionId, mode, nativeSource, resumeLinkage, predecessorSessionId, predecessorProcess } =
|
|
939
|
+
await resolveSessionId(input, projectRoot, claudePid);
|
|
882
940
|
// getPlatform() ALREADY implements the SO_PLATFORM override as step 1 of its
|
|
883
941
|
// precedence — and, unlike a bare `??`, it validates the value against the
|
|
884
942
|
// four-platform allowlist and trims it. The former `process.env.SO_PLATFORM ??
|
|
@@ -905,12 +963,23 @@ async function main() {
|
|
|
905
963
|
mode,
|
|
906
964
|
ttlHours: 4,
|
|
907
965
|
nativeSource,
|
|
908
|
-
// The raw session_id
|
|
966
|
+
// The raw session_id the last session start in this checkout recorded —
|
|
967
|
+
// our own previous run unless a peer started here since. It is the
|
|
909
968
|
// third conjunct of the same-logical-session force-refresh (#1091 F2): a
|
|
910
969
|
// semantic-label match alone is collidable on one host (#1066), so the
|
|
911
970
|
// live lock must ALSO carry our predecessor's raw id before we take it
|
|
912
971
|
// over. `prev` is read above, before the semantic id is minted.
|
|
913
972
|
predecessorSessionId,
|
|
973
|
+
// #1746: who wrote that raw id — the lead yield trusts it as "our own
|
|
974
|
+
// predecessor" only when this same Claude process did.
|
|
975
|
+
predecessorProcess,
|
|
976
|
+
claudePid,
|
|
977
|
+
// #1635: a fleet lead holds this main checkout — no lock was taken; the
|
|
978
|
+
// model and the operator are told to work in a worktree instead.
|
|
979
|
+
onNotice: (context, banner) => {
|
|
980
|
+
setAdditionalContext(context);
|
|
981
|
+
pushBanner(banner);
|
|
982
|
+
},
|
|
914
983
|
});
|
|
915
984
|
} catch { /* hook must remain non-blocking */ }
|
|
916
985
|
|
|
@@ -973,6 +1042,12 @@ async function main() {
|
|
|
973
1042
|
platform,
|
|
974
1043
|
mode,
|
|
975
1044
|
hostClass: bannerData?.host?.host_class ?? null,
|
|
1045
|
+
// #1605 — the pointer the readers (Stop, PostToolBatch, SessionEnd, the
|
|
1046
|
+
// current-session.json side writers) follow to THIS session's lock,
|
|
1047
|
+
// whatever their cwd; and the working copy, so a later relocation
|
|
1048
|
+
// into it by another session sees this one (B4). Stop refreshes it.
|
|
1049
|
+
sessionRoot: path.resolve(projectRoot),
|
|
1050
|
+
workRoot: path.resolve(resolveSessionRoot(input?.cwd)),
|
|
976
1051
|
});
|
|
977
1052
|
} catch (err) {
|
|
978
1053
|
// Registration failed — emit an observability breadcrumb to sweep.log.
|
|
@@ -981,6 +1056,9 @@ async function main() {
|
|
|
981
1056
|
}
|
|
982
1057
|
peers = await detectPeers({ sessionId }).catch(() => []);
|
|
983
1058
|
} catch { /* swallow — hook must remain non-blocking */ }
|
|
1059
|
+
// #1650 — foreign starts inside the window. detectPeers() already excludes
|
|
1060
|
+
// self, and its 15-min freshness covers the 5-min window (see helper JSDoc).
|
|
1061
|
+
const recentStarts = countRecentStarts(peers, { windowMin: START_WAVE_WINDOW_MIN });
|
|
984
1062
|
|
|
985
1063
|
// Append a peer line to the host banner when a banner was already emitted
|
|
986
1064
|
// and at least one peer is live on this host.
|
|
@@ -1004,10 +1082,11 @@ async function main() {
|
|
|
1004
1082
|
selfRepoHash && p.repo_path_hash ? p.repo_path_hash === selfRepoHash : false);
|
|
1005
1083
|
const elsewhere = peers.filter((p) => !sameCopy.includes(p));
|
|
1006
1084
|
|
|
1007
|
-
const threshold = await peerWarnThreshold(projectRoot);
|
|
1008
1085
|
// WARN on any peer in THIS working copy — one is already enough to collide
|
|
1009
|
-
// — or on host-wide count
|
|
1010
|
-
|
|
1086
|
+
// — or on a start wave (#1650). The host-wide peer count stays on the line
|
|
1087
|
+
// as information only: as a WARN trigger it fired on 67.4% of starts.
|
|
1088
|
+
const startWave = recentStarts >= START_WAVE_MIN;
|
|
1089
|
+
const icon = sameCopy.length > 0 || startWave ? '⚠️ ' : '';
|
|
1011
1090
|
|
|
1012
1091
|
// Semantic ids are NOT unique — several live sessions routinely share e.g.
|
|
1013
1092
|
// `main-2026-08-21-session-2`, which makes a bare semantic label unusable
|
|
@@ -1019,6 +1098,9 @@ async function main() {
|
|
|
1019
1098
|
return `${label}${disc}:${p.branch ?? 'unknown'}:wave-${p.current_wave ?? 0}`;
|
|
1020
1099
|
};
|
|
1021
1100
|
pushBanner(`${icon}👥 Peers: ${peers.length} live on this host`);
|
|
1101
|
+
if (startWave) {
|
|
1102
|
+
pushBanner(` ⚠️ Start wave: ${recentStarts} sessions started or resumed in the last ${START_WAVE_WINDOW_MIN} min — stagger further starts`);
|
|
1103
|
+
}
|
|
1022
1104
|
if (sameCopy.length > 0) {
|
|
1023
1105
|
const list = sameCopy.slice(0, 3).map(fmt).join(', ');
|
|
1024
1106
|
const more = sameCopy.length > 3 ? ` +${sameCopy.length - 3} more` : '';
|
|
@@ -1174,7 +1256,7 @@ async function main() {
|
|
|
1174
1256
|
pluginUpdate = await checkPluginUpdate({
|
|
1175
1257
|
cacheDir: path.join(projectRoot, '.orchestrator', 'runtime'),
|
|
1176
1258
|
// The probe's own fallback is env-only (SO_PLATFORM, then each harness's
|
|
1177
|
-
// compat var) and cannot run detectPlatform()'s
|
|
1259
|
+
// compat var) and cannot run detectPlatform()'s step 4, the marker-
|
|
1178
1260
|
// directory walk from cwd. This hook already resolved the platform at the
|
|
1179
1261
|
// top of run() through exactly that full precedence, so handing it over
|
|
1180
1262
|
// makes the remedy instruction MEASURED rather than guessed — a Codex or
|
|
@@ -1195,8 +1277,16 @@ async function main() {
|
|
|
1195
1277
|
// this root supersedes. `peer_count` is deliberately unchanged; this field
|
|
1196
1278
|
// makes the supersession rate measurable instead of inferred (HR-105).
|
|
1197
1279
|
peers_superseded: mechanicalPeersSuperseded,
|
|
1280
|
+
// #1650 — start-wave signal (HR-105): foreign registry entries whose
|
|
1281
|
+
// started_at lies within the last `recent_starts_window_min` minutes.
|
|
1282
|
+
recent_starts_count: recentStarts,
|
|
1283
|
+
recent_starts_window_min: START_WAVE_WINDOW_MIN,
|
|
1198
1284
|
};
|
|
1199
1285
|
if (headSha) payload.head_sha = headSha;
|
|
1286
|
+
if (upstreamSha !== null) {
|
|
1287
|
+
payload.upstream_sha = upstreamSha;
|
|
1288
|
+
payload.behind = behind;
|
|
1289
|
+
}
|
|
1200
1290
|
|
|
1201
1291
|
// #1091 — record the native SessionStart source so "does the same raw
|
|
1202
1292
|
// session_id repeat under source=resume?" becomes answerable from
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
|
47
47
|
|
|
48
48
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
49
49
|
import { detectPlatform, getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
50
|
+
import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
|
|
50
51
|
import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
51
52
|
import {
|
|
52
53
|
classifyManifestSession,
|
|
@@ -119,7 +120,7 @@ function discriminate(input) {
|
|
|
119
120
|
// dependency degradation (GH Kanevry/session-orchestrator#63)
|
|
120
121
|
// ---------------------------------------------------------------------------
|
|
121
122
|
//
|
|
122
|
-
// zx is loaded LAZILY. A static `import { $ } from 'zx'` fails at MODULE LOAD
|
|
123
|
+
// zx is loaded LAZILY. A static `import { $ } from '../scripts/lib/zx.mjs'` fails at MODULE LOAD
|
|
123
124
|
// time when node_modules is absent (interrupted install, EPERM sandbox, half-
|
|
124
125
|
// synced plugin cache), so the harness prints a 10-frame ERR_MODULE_NOT_FOUND
|
|
125
126
|
// stack on EVERY turn end with no hint that `npm install` is the fix. This
|
|
@@ -175,7 +176,7 @@ function warnDependenciesMissingOnce() {
|
|
|
175
176
|
*/
|
|
176
177
|
async function loadZx() {
|
|
177
178
|
try {
|
|
178
|
-
return (await import('zx')).$;
|
|
179
|
+
return (await import('../scripts/lib/zx.mjs')).$;
|
|
179
180
|
} catch (err) {
|
|
180
181
|
if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
181
182
|
warnDependenciesMissingOnce();
|
|
@@ -401,11 +402,13 @@ function reaperTriggerField(trigger) {
|
|
|
401
402
|
*/
|
|
402
403
|
async function handleStop(input, reaperTrigger = null) {
|
|
403
404
|
// Two roots, by writer (#1511 point b). `current-session.json` and
|
|
404
|
-
// `session.lock` (session id, lock heartbeat, duration)
|
|
405
|
-
//
|
|
405
|
+
// `session.lock` (session id, lock heartbeat, duration) are read where
|
|
406
|
+
// on-session-start.mjs wrote them: the launch dir, or — after EnterWorktree
|
|
407
|
+
// and a /clear, compact or resume there (#1605) — the entered worktree, found
|
|
408
|
+
// by the lock carrying this session's raw id (`resolveOwnLockRoot`). The wave
|
|
406
409
|
// and the commit/branch describe the tree the session WORKS in — after
|
|
407
410
|
// EnterWorktree that is the session root, not the launch checkout.
|
|
408
|
-
const projectRoot = getProjectDir();
|
|
411
|
+
const projectRoot = resolveOwnLockRoot(input, getProjectDir());
|
|
409
412
|
const sessionRoot = resolveSessionRoot(input?.cwd);
|
|
410
413
|
|
|
411
414
|
const wave = await readWaveNumber(sessionRoot, input);
|
|
@@ -453,7 +456,9 @@ async function handleStop(input, reaperTrigger = null) {
|
|
|
453
456
|
// is visible in sweep.log rather than invisible. One line per turn while
|
|
454
457
|
// the entry is absent; that volume IS the signal, and the next
|
|
455
458
|
// SessionStart's registerSelf() ends it.
|
|
456
|
-
|
|
459
|
+
// #1605 B4 — `work_root` follows the session into an entered worktree,
|
|
460
|
+
// so a relocating SessionStart of another session sees it working there.
|
|
461
|
+
const refreshed = await heartbeat(sessionId, { workRoot: path.resolve(sessionRoot) });
|
|
457
462
|
if (refreshed === null) {
|
|
458
463
|
logSweepEvent({
|
|
459
464
|
event: 'heartbeat-missing',
|
|
@@ -100,7 +100,12 @@ async function resolveSessionId(input, projectDir) {
|
|
|
100
100
|
|
|
101
101
|
let currentSession = null;
|
|
102
102
|
if (projectDir) {
|
|
103
|
-
|
|
103
|
+
// #1605 — read THIS session's record where on-session-start.mjs wrote it
|
|
104
|
+
// (the entered worktree after a /clear there), found by the lock carrying
|
|
105
|
+
// the raw id. Lazy: only a matched issue command pays for the lookup.
|
|
106
|
+
const { resolveOwnLockRoot } = await import('./_lib/hook-root.mjs');
|
|
107
|
+
const ownRoot = resolveOwnLockRoot({ session_id: nativeRawId, cwd: input?.cwd }, projectDir);
|
|
108
|
+
const persisted = path.join(ownRoot, '.orchestrator', 'current-session.json');
|
|
104
109
|
if (existsSync(persisted)) {
|
|
105
110
|
try {
|
|
106
111
|
currentSession = await readJson(persisted);
|
|
@@ -1093,9 +1093,10 @@ async function main() {
|
|
|
1093
1093
|
// manifest a Bash call just corrupted. `'own'` and `'unknown'` (legacy,
|
|
1094
1094
|
// unbound, or no resolvable own identity) fall through unchanged.
|
|
1095
1095
|
//
|
|
1096
|
-
// CEILING (BV-004): on a harness that supplies no session id at all (
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1096
|
+
// CEILING (BV-004): on a harness that supplies no session id at all (Cursor
|
|
1097
|
+
// today) the own-id set is empty, so this gate is permanently `'unknown'` =
|
|
1098
|
+
// full pre-#1153 behaviour there. Codex CLI sends `session_id` in the hook
|
|
1099
|
+
// payload (measured 2026-10-20, codex-cli 0.160.1, #1691).
|
|
1099
1100
|
{
|
|
1100
1101
|
const { verdict, manifestIds } = classifyManifestSession(scope ?? {}, ownIds);
|
|
1101
1102
|
if (verdict === 'foreign') {
|
|
@@ -1172,7 +1173,10 @@ async function main() {
|
|
|
1172
1173
|
// `toContain('out-of-scope.mjs')` test to say otherwise.
|
|
1173
1174
|
const missingSnapshotNotice = prevScopeState === null
|
|
1174
1175
|
? formatSnapshotMissingNotice({
|
|
1175
|
-
|
|
1176
|
+
// #1634: the clock lives beside this session's own lock, which after
|
|
1177
|
+
// ExitWorktree is its `session_root`, not the manifest root. Loaded lazily:
|
|
1178
|
+
// this branch is rare and the module graph costs ~7 ms per Bash call.
|
|
1179
|
+
sessionAge: sessionAgeMs((await import('./_lib/hook-root.mjs')).resolveOwnLockRoot(input, context.manifestRoot)),
|
|
1176
1180
|
scopeAge: typeof scopeMtimeMs === 'number' ? Date.now() - scopeMtimeMs : null,
|
|
1177
1181
|
reason: snapshot === null ? 'absent' : 'untrusted',
|
|
1178
1182
|
})
|
|
@@ -14,22 +14,23 @@
|
|
|
14
14
|
* G3 file_path present
|
|
15
15
|
* G4 file extension filter — only .ts|.tsx|.js|.jsx|.mjs|.cjs
|
|
16
16
|
* G5 gate check — wave-scope.json.gates['post-edit-validate'] === false → exit 0
|
|
17
|
-
* G6 resolve typecheck command: Session Config → tsgo → tsc →
|
|
18
|
-
* G7 run typecheck with 2s
|
|
17
|
+
* G6 resolve typecheck command: Session Config → tsgo → tsc → local tsc → skip
|
|
18
|
+
* G7 run typecheck with 2s process-group timeout
|
|
19
19
|
* G8 emit JSONL result to stderr
|
|
20
20
|
*
|
|
21
|
-
* Output (stderr): {"check":"typecheck","status":"pass|fail|skip","file":"<rel>","duration_ms":<int>}
|
|
21
|
+
* Output (stderr): {"check":"typecheck","status":"pass|fail|timeout|skip","file":"<rel>","duration_ms":<int>}
|
|
22
22
|
* Exit codes: 0 — always (PostToolUse hooks are informational)
|
|
23
23
|
*
|
|
24
24
|
* SECURITY notes:
|
|
25
25
|
* REQ-01 top-level try/catch → silent exit 0 on unexpected error (never blocks)
|
|
26
|
-
* REQ-02 typecheck
|
|
26
|
+
* REQ-02 typecheck runs the intact shell command; timeout kills its process tree
|
|
27
27
|
* REQ-03 relative path computed via path.relative(projectRoot, absFilePath)
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import path from 'node:path';
|
|
31
|
+
import { existsSync } from 'node:fs';
|
|
31
32
|
import { spawnSync } from 'node:child_process';
|
|
32
|
-
import {
|
|
33
|
+
import { spawnInGroup } from '../scripts/lib/process-group.mjs';
|
|
33
34
|
|
|
34
35
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
36
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
@@ -54,9 +55,9 @@ const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
|
54
55
|
/**
|
|
55
56
|
* Emit the JSONL result line to stderr.
|
|
56
57
|
* @param {string} file - relative file path
|
|
57
|
-
* @param {'pass'|'fail'|'skip'} status
|
|
58
|
+
* @param {'pass'|'fail'|'timeout'|'skip'} status
|
|
58
59
|
* @param {number} [duration_ms] - omit for skip
|
|
59
|
-
* @param {string} [reason] - human-readable reason for skip or fail
|
|
60
|
+
* @param {string} [reason] - human-readable reason for skip, timeout or fail
|
|
60
61
|
* @param {string} [remediation] - actionable fix hint for fail status only
|
|
61
62
|
*/
|
|
62
63
|
function emitResult(file, status, duration_ms, reason, remediation) {
|
|
@@ -69,10 +70,10 @@ function emitResult(file, status, duration_ms, reason, remediation) {
|
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Try to resolve the typecheck command.
|
|
72
|
-
* Order: Session Config typecheck-command → tsgo → tsc →
|
|
73
|
+
* Order: Session Config typecheck-command → tsgo → tsc → local tsc → null
|
|
73
74
|
*
|
|
74
75
|
* @param {string} projectRoot
|
|
75
|
-
* @returns {Promise<
|
|
76
|
+
* @returns {Promise<string|string[]|null>}
|
|
76
77
|
*/
|
|
77
78
|
async function resolveTypecheckCommand(projectRoot) {
|
|
78
79
|
// 1. Session Config typecheck-command
|
|
@@ -81,8 +82,8 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
81
82
|
const config = parseSessionConfig(md);
|
|
82
83
|
const cmd = config['typecheck-command'];
|
|
83
84
|
if (cmd && typeof cmd === 'string') {
|
|
84
|
-
const
|
|
85
|
-
return
|
|
85
|
+
const command = cmd.trim();
|
|
86
|
+
return command === 'skip' ? null : command || null;
|
|
86
87
|
}
|
|
87
88
|
} catch {
|
|
88
89
|
// config unavailable — fall through
|
|
@@ -90,15 +91,16 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
90
91
|
|
|
91
92
|
// 2. tsgo
|
|
92
93
|
const tsgoParts = _tryWhich('tsgo');
|
|
93
|
-
if (tsgoParts) return
|
|
94
|
+
if (tsgoParts) return 'tsgo --noEmit';
|
|
94
95
|
|
|
95
96
|
// 3. tsc
|
|
96
97
|
const tscParts = _tryWhich('tsc');
|
|
97
|
-
if (tscParts) return
|
|
98
|
+
if (tscParts) return 'tsc --noEmit';
|
|
98
99
|
|
|
99
|
-
// 4. npx
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// 4. Only an installed project compiler; npx may install the unrelated
|
|
101
|
+
// npm package `tsc` in projects without TypeScript (#1612).
|
|
102
|
+
const localTsc = path.join(projectRoot, 'node_modules', 'typescript', 'bin', 'tsc');
|
|
103
|
+
if (existsSync(localTsc)) return [process.execPath, localTsc, '--noEmit'];
|
|
102
104
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
@@ -111,9 +113,11 @@ async function resolveTypecheckCommand(projectRoot) {
|
|
|
111
113
|
* @returns {string|null}
|
|
112
114
|
*/
|
|
113
115
|
function _tryWhich(name) {
|
|
114
|
-
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
116
|
+
const whichCmd = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
117
|
+
// Two lookups get at most 1s total; revisit together with the outer
|
|
118
|
+
// 5s hook budget if a host needs slower executable discovery.
|
|
115
119
|
try {
|
|
116
|
-
const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout:
|
|
120
|
+
const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 500 });
|
|
117
121
|
if (result.status === 0 && result.stdout.trim()) return name;
|
|
118
122
|
} catch {
|
|
119
123
|
// ignore
|
|
@@ -122,43 +126,32 @@ function _tryWhich(name) {
|
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* @param {string} cmd
|
|
128
|
-
* @param {string
|
|
129
|
-
* @returns {{status: 'pass'|'fail', duration_ms: number, reason?: string}}
|
|
129
|
+
* Configured commands retain shell semantics; the local compiler uses Node argv.
|
|
130
|
+
* Keep cleanup short for hooks.json's 5s budget; Windows skips CIM verification.
|
|
131
|
+
* @param {string|string[]} cmd
|
|
132
|
+
* @param {string} projectRoot
|
|
133
|
+
* @returns {Promise<{status: 'pass'|'fail'|'timeout', duration_ms: number, reason?: string}>}
|
|
130
134
|
*/
|
|
131
|
-
function runTypecheck(cmd,
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const duration_ms = Math.max(0, Math.round(performance.now() - start));
|
|
149
|
-
|
|
150
|
-
// AbortError or SIGTERM from timeout
|
|
151
|
-
if (spawnResult.error?.name === 'AbortError' || spawnResult.signal === 'SIGTERM') {
|
|
152
|
-
return { status: 'fail', duration_ms, reason: 'typecheck timed out after 2s' };
|
|
135
|
+
async function runTypecheck(cmd, projectRoot) {
|
|
136
|
+
const result = await spawnInGroup(cmd, {
|
|
137
|
+
cwd: projectRoot,
|
|
138
|
+
timeoutMs: 2000,
|
|
139
|
+
// Hook-specific ceiling: escalate quickly so an uncooperative compiler
|
|
140
|
+
// cannot consume the outer 5s hook budget. Revisit if compiler cleanup
|
|
141
|
+
// needs more than 100ms; the hook's outer budget must change with it.
|
|
142
|
+
killGraceMs: 100,
|
|
143
|
+
verifyWaitMs: 100,
|
|
144
|
+
// Two PowerShell startups would exceed the remaining hook budget (R1-04).
|
|
145
|
+
verifyTree: false,
|
|
146
|
+
});
|
|
147
|
+
const duration_ms = Math.max(0, Math.round(result.durationMs));
|
|
148
|
+
if (result.timedOut) {
|
|
149
|
+
return { status: 'timeout', duration_ms, reason: 'typecheck timed out after 2s' };
|
|
153
150
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Capture the first non-empty line of stderr/stdout as a compact reason.
|
|
157
|
-
const combined = ((spawnResult.stderr ?? '') + (spawnResult.stdout ?? '')).trim();
|
|
158
|
-
const firstLine = combined.split('\n').find((l) => l.trim()) ?? 'typecheck exited non-zero';
|
|
151
|
+
if (result.exitCode !== 0) {
|
|
152
|
+
const firstLine = result.fullOutput.split('\n').find((line) => line.trim()) ?? 'typecheck exited non-zero';
|
|
159
153
|
return { status: 'fail', duration_ms, reason: firstLine.slice(0, 256) };
|
|
160
154
|
}
|
|
161
|
-
|
|
162
155
|
return { status: 'pass', duration_ms };
|
|
163
156
|
}
|
|
164
157
|
|
|
@@ -216,7 +209,7 @@ async function main() {
|
|
|
216
209
|
}
|
|
217
210
|
|
|
218
211
|
// G7: run typecheck with 2s timeout (REQ-02)
|
|
219
|
-
const { status, duration_ms, reason } = runTypecheck(tc
|
|
212
|
+
const { status, duration_ms, reason } = await runTypecheck(tc, projectRoot);
|
|
220
213
|
|
|
221
214
|
// G8: emit JSONL result to stderr
|
|
222
215
|
const remediation =
|