session-orchestrator 5.11.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 +12 -1
- 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 +180 -3
- package/README.md +126 -117
- 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 +4 -2
- 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 +102 -25
- 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 +55 -21
- package/docs/session-config-template.md +32 -24
- 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 +158 -59
- 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 -3
- 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 +73 -3
- 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/remote-hosts.mjs +2 -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 +3 -3
- 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 +21 -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-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- 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 +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- 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 +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- 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 +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- 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 +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- 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 +57 -8
- 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 +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- 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/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- 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 +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- 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 +97 -13
- 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-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -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 +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- 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 +13 -5
- 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/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -35,8 +35,10 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import fs from 'node:fs';
|
|
38
|
+
import os from 'node:os';
|
|
38
39
|
import path from 'node:path';
|
|
39
40
|
import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
|
|
41
|
+
import { readNavigatorLease } from '../../scripts/lib/fleet-protocol.mjs';
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Native SessionStart `source` values that are a RE-ENTRY into the same logical
|
|
@@ -55,6 +57,184 @@ import { writeJsonAtomicSync } from '../../scripts/lib/io.mjs';
|
|
|
55
57
|
*/
|
|
56
58
|
export const SAME_LOGICAL_SESSION_SOURCES = new Set(['resume', 'clear', 'compact']);
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Native `source` values on which a fleet lead's lease keeps this session out
|
|
62
|
+
* of the MAIN checkout (#1635). A subset of SAME_LOGICAL_SESSION_SOURCES:
|
|
63
|
+
* `compact` keeps the lock it already holds, `startup` stays untouched. A
|
|
64
|
+
* session that yielded holds none: the yield leaves a #1651 release marker, so
|
|
65
|
+
* its later `compact` stays lockless too (#1747).
|
|
66
|
+
*
|
|
67
|
+
* @type {ReadonlySet<string>}
|
|
68
|
+
*/
|
|
69
|
+
const LEAD_YIELD_SOURCES = new Set(['clear', 'resume']);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Native `source` values on which a lock this same raw session released on
|
|
73
|
+
* purpose stays released (#1651). A subset of SAME_LOGICAL_SESSION_SOURCES:
|
|
74
|
+
* `clear` starts new work and takes a lock as before, `startup` too.
|
|
75
|
+
*
|
|
76
|
+
* @type {ReadonlySet<string>}
|
|
77
|
+
*/
|
|
78
|
+
const RELEASED_LOCK_STAYS_RELEASED_SOURCES = new Set(['resume', 'compact']);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* #1651 — true when `sessionId` left a release marker in this repo. Never
|
|
82
|
+
* throws: a missing module or any error reads as "no marker" (the pre-#1651
|
|
83
|
+
* acquire path).
|
|
84
|
+
*
|
|
85
|
+
* @param {string} repoRoot
|
|
86
|
+
* @param {string} sessionId
|
|
87
|
+
* @returns {Promise<boolean>}
|
|
88
|
+
*/
|
|
89
|
+
async function hasOwnReleaseMarker(repoRoot, sessionId) {
|
|
90
|
+
try {
|
|
91
|
+
const { hasReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
|
|
92
|
+
return hasReleaseMarker({ repoRoot, sessionId }) === true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** #1747 — `reason` of a release marker the lead yield writes. */
|
|
99
|
+
const LEAD_YIELD_REASON = 'lead-yield';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* #1747 — the #1651 marker body for `sessionId`, or null. Never throws.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} repoRoot
|
|
105
|
+
* @param {string} sessionId
|
|
106
|
+
* @returns {Promise<object|null>}
|
|
107
|
+
*/
|
|
108
|
+
async function readOwnReleaseMarker(repoRoot, sessionId) {
|
|
109
|
+
try {
|
|
110
|
+
const { readReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
|
|
111
|
+
return readReleaseMarker({ repoRoot, sessionId });
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* #1747 — a lead yield is a release too: leave the #1651 marker for `sessionId`,
|
|
119
|
+
* tagged `reason: 'lead-yield'`, so a later `compact` or `resume` of this same
|
|
120
|
+
* session stays lockless while the lead's lease is active (compact is no yield
|
|
121
|
+
* source, and without the marker it took the free lock). Once the lease is
|
|
122
|
+
* gone, re-entry acquires and acquire() removes the marker as before. Never
|
|
123
|
+
* throws; a failed write is one stderr WARN and leaves the pre-#1747 behaviour.
|
|
124
|
+
*
|
|
125
|
+
* A marker that is already there and was not written by a yield stays as it is:
|
|
126
|
+
* without `reason` it is an explicit release (release-session-lock.mjs), which
|
|
127
|
+
* keeps its #1651 behaviour after the lease ends. Rewriting it as `lead-yield`
|
|
128
|
+
* made the next compaction after the lease take the lock back (review of !244,
|
|
129
|
+
* M1). An unreadable marker is kept for the same reason: the branch below
|
|
130
|
+
* already reads it as explicit.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} repoRoot
|
|
133
|
+
* @param {string} sessionId
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async function writeOwnReleaseMarker(repoRoot, sessionId) {
|
|
137
|
+
if (await hasOwnReleaseMarker(repoRoot, sessionId)) {
|
|
138
|
+
const existing = await readOwnReleaseMarker(repoRoot, sessionId);
|
|
139
|
+
if (existing?.reason !== LEAD_YIELD_REASON) return;
|
|
140
|
+
}
|
|
141
|
+
let reason;
|
|
142
|
+
try {
|
|
143
|
+
const { writeReleaseMarker } = await import('../../scripts/lib/session-lock.mjs');
|
|
144
|
+
const result = writeReleaseMarker({ repoRoot, sessionId, reason: LEAD_YIELD_REASON });
|
|
145
|
+
if (result?.ok !== true) reason = result?.reason ?? 'unknown';
|
|
146
|
+
} catch (err) {
|
|
147
|
+
reason = err?.message ?? 'unknown';
|
|
148
|
+
}
|
|
149
|
+
if (reason !== undefined) {
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`⚠ lock-bootstrap: no release marker after the lead yield (${reason}) — a compaction may take the lock again\n`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* #1641 — the Claude Code process id this hook runs under, or null. The value
|
|
158
|
+
* comes from `CLAUDE_PID`, but it counts ONLY when that process is this hook's
|
|
159
|
+
* direct parent: the variable is inherited by everything a Claude session
|
|
160
|
+
* starts, so a Codex child launched from a session's Bash would otherwise carry
|
|
161
|
+
* the parent's pid into its own SessionStart (`hooks-codex.json` matches
|
|
162
|
+
* resume|clear|compact) and take over the parent's LIVE lock (REFUTE review,
|
|
163
|
+
* reproduced against the real acquire/forceAcquire).
|
|
164
|
+
*
|
|
165
|
+
* Measured 2026-10-15 (claude 2.1.300, macOS): a SessionStart hook launched as
|
|
166
|
+
* `sh run-node.sh <hook>` runs node with `process.ppid === CLAUDE_PID` and the
|
|
167
|
+
* parent's comm `claude` — both shells exec. A Codex hook's parent is Codex or
|
|
168
|
+
* its shell, never Claude, so it reads null here. Depth 1, deliberately: a
|
|
169
|
+
* depth-2 walk would admit a hook whose parent is a Codex process that bash
|
|
170
|
+
* exec'd straight under Claude. Spawn-free.
|
|
171
|
+
*
|
|
172
|
+
* #1717: Claude runs each hook as `/bin/sh -c "<command>"`. On Linux/WSL
|
|
173
|
+
* /bin/sh is dash, and the WSL dash 0.5.12 FORKS a single `-c` command
|
|
174
|
+
* instead of exec'ing it, so node's parent was that intermediate sh and this
|
|
175
|
+
* read null on every hook (measured 2026-10-21 on win-wsl, 3/3 runs; with an
|
|
176
|
+
* `exec ` prefix the ppid matched). Every run-node.sh command in
|
|
177
|
+
* `hooks/hooks.json` therefore starts with `exec sh …`; run-node.sh itself
|
|
178
|
+
* execs node on every branch, so both hops are exec and depth 1 holds.
|
|
179
|
+
* The other manifests (hooks-codex/-cursor/-pi.json) deliberately keep the
|
|
180
|
+
* plain form: only a Claude hook can match its own CLAUDE_PID, so the prefix
|
|
181
|
+
* would buy nothing there (their parent is that harness either way).
|
|
182
|
+
* Measured 2026-10-21 on macOS: /bin/sh (bash 3.2) and /bin/dash (dash-16)
|
|
183
|
+
* both match ppid with AND without `exec`, and exit status 2, stderr and stdin
|
|
184
|
+
* pass through unchanged in both forms — the prefix costs nothing there.
|
|
185
|
+
* Ceiling (BV-004): a harness whose launcher is not `/bin/sh -c` (or that
|
|
186
|
+
* wraps the command in a further shell) still reads null and keeps the
|
|
187
|
+
* pre-#1641 behaviour (fail-closed). Revisit if `reclaimed: own-process`
|
|
188
|
+
* never appears on a host where /clear without SessionEnd does.
|
|
189
|
+
*
|
|
190
|
+
* The process survives `/clear` while the raw session id is minted anew, so
|
|
191
|
+
* this is the witness of "the same terminal session" that the label and the
|
|
192
|
+
* raw id cannot give.
|
|
193
|
+
*
|
|
194
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
195
|
+
* @param {number} [ppid] — this process' parent pid (DI for tests).
|
|
196
|
+
* @returns {number|null}
|
|
197
|
+
*/
|
|
198
|
+
export function readClaudePid(env = process.env, ppid = process.ppid) {
|
|
199
|
+
const raw = typeof env?.CLAUDE_PID === 'string' ? env.CLAUDE_PID.trim() : '';
|
|
200
|
+
if (!/^[1-9][0-9]{0,9}$/.test(raw)) return null;
|
|
201
|
+
const pid = Number(raw);
|
|
202
|
+
return Number.isSafeInteger(pid) && pid === ppid ? pid : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* #1641 — true when `lock` was written under the Claude Code process this hook
|
|
207
|
+
* runs under, on this host. Only then may a lock with a DIFFERENT raw id count
|
|
208
|
+
* as our own predecessor (startup → /clear mints a new raw id in the same
|
|
209
|
+
* process). #1746 judges the `claude_pid`/`host` pair current-session.json
|
|
210
|
+
* carries with this same predicate, so both witnesses mean the same thing.
|
|
211
|
+
*
|
|
212
|
+
* Why the pid alone is enough, without a start time: both sides come from
|
|
213
|
+
* readClaudePid(), which admits a pid only as the writer hook's DIRECT parent.
|
|
214
|
+
* So the lock's pid names the Claude process that wrote it, and equal to ours
|
|
215
|
+
* it names either this process or a dead one whose pid the OS recycled to us —
|
|
216
|
+
* not another live Claude process in the same pid namespace.
|
|
217
|
+
* The host check keeps a lock on a shared filesystem from another machine out.
|
|
218
|
+
* Ceilings (BV-004), named rather than claimed closed: (1) two pid namespaces
|
|
219
|
+
* that report the same hostname (WSL2 distros, a container and its host on one
|
|
220
|
+
* checkout) can hold equal pids; (2) an in-process agent team whose teammates
|
|
221
|
+
* each ran SessionStart under one Claude process (measured: subagents fire no
|
|
222
|
+
* SessionStart). Revisit when either shape works in a shared checkout.
|
|
223
|
+
*
|
|
224
|
+
* @param {object|null} lock
|
|
225
|
+
* @param {number|null} claudePid — from readClaudePid().
|
|
226
|
+
* @returns {Promise<boolean>}
|
|
227
|
+
*/
|
|
228
|
+
async function isSameClaudeProcess(lock, claudePid) {
|
|
229
|
+
if (typeof claudePid !== 'number' || !lock || lock.claude_pid !== claudePid) return false;
|
|
230
|
+
try {
|
|
231
|
+
const { hostnamesMatch, lockHostCandidate } = await import('../../scripts/lib/host-identity.mjs');
|
|
232
|
+
return hostnamesMatch(lockHostCandidate(lock), os.hostname());
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
58
238
|
/** Re-reads of a just-acquired lock that answers ENOENT, and the pause between them (#1494). */
|
|
59
239
|
const ENRICH_READ_RETRIES = 5;
|
|
60
240
|
const ENRICH_READ_RETRY_MS = 10;
|
|
@@ -81,14 +261,34 @@ const ENRICH_READ_RETRY_MS = 10;
|
|
|
81
261
|
* Only consulted for the same-logical-session force-refresh below; every
|
|
82
262
|
* existing caller that omits it keeps its pre-#1091 behaviour exactly.
|
|
83
263
|
* @param {string|null} [opts.predecessorSessionId=null] — the raw `session_id`
|
|
84
|
-
*
|
|
85
|
-
*
|
|
264
|
+
* the last session start in this checkout recorded in
|
|
265
|
+
* `.orchestrator/current-session.json` — our own previous run of this hook
|
|
266
|
+
* unless a peer started here since (every start rewrites the file, #1746).
|
|
267
|
+
* It is the third, load-bearing conjunct of the re-entry
|
|
86
268
|
* force-refresh below: without it a same-host semantic-label collision (#1066)
|
|
87
269
|
* lets one session take over a live peer's lock. Omitting it (the default)
|
|
88
270
|
* disables the re-entry force-refresh entirely — fail-closed.
|
|
271
|
+
* @param {{ claude_pid?: unknown, host?: unknown }|null} [opts.predecessorProcess=null]
|
|
272
|
+
* — the `claude_pid`/`host` pair the writer of that `current-session.json`
|
|
273
|
+
* recorded (#1746), unvalidated. The #1635 lead yield takes
|
|
274
|
+
* `predecessorSessionId` for our own predecessor only when this pair names
|
|
275
|
+
* our own Claude Code process (isSameClaudeProcess); null or any mismatch
|
|
276
|
+
* yields.
|
|
277
|
+
* @param {number|null} [opts.claudePid=null] — the Claude Code process id from
|
|
278
|
+
* readClaudePid() (#1641). Stamped on the lock as `claude_pid`; a lock that
|
|
279
|
+
* carries this same pid on this host is our own predecessor after /clear, for
|
|
280
|
+
* the re-entry force-refresh AND the #1635 lead-yield release. Null (Codex,
|
|
281
|
+
* Cursor, tests that omit it) leaves both exactly as before.
|
|
89
282
|
* @param {Function} [opts._acquireImpl] — DI for tests (defaults to importing acquire from session-lock.mjs).
|
|
90
283
|
* @param {Function} [opts._forceAcquireImpl] — DI for tests (defaults to importing forceAcquire from session-lock.mjs).
|
|
91
284
|
* @param {Function} [opts._emitEventImpl] — DI for tests (defaults to importing emitEvent from events.mjs).
|
|
285
|
+
* @param {(context: string, banner: string) => void} [opts.onNotice] — receives
|
|
286
|
+
* the model notice and the operator banner line when the lock is deliberately
|
|
287
|
+
* NOT taken because a fleet lead holds the main checkout (#1635), or because
|
|
288
|
+
* this raw id released its lock on purpose and the source is `resume` /
|
|
289
|
+
* `compact` (#1651).
|
|
290
|
+
* @param {Function} [opts._readLeaseImpl] — DI for tests (defaults to readNavigatorLease from fleet-protocol.mjs).
|
|
291
|
+
* @param {Function} [opts._releaseImpl] — DI for tests (defaults to release from session-lock.mjs).
|
|
92
292
|
* @returns {Promise<object|null>} the enriched v2 lock body on success, null on any failure.
|
|
93
293
|
*/
|
|
94
294
|
export async function bootstrapLock({
|
|
@@ -99,15 +299,92 @@ export async function bootstrapLock({
|
|
|
99
299
|
ttlHours = 4,
|
|
100
300
|
nativeSource = null,
|
|
101
301
|
predecessorSessionId = null,
|
|
302
|
+
predecessorProcess = null,
|
|
303
|
+
claudePid = null,
|
|
102
304
|
_acquireImpl,
|
|
103
305
|
_forceAcquireImpl,
|
|
104
306
|
_emitEventImpl,
|
|
307
|
+
onNotice,
|
|
308
|
+
_readLeaseImpl = readNavigatorLease,
|
|
309
|
+
_releaseImpl,
|
|
105
310
|
} = {}) {
|
|
106
311
|
// Sanity-check required inputs. Anything missing → bail silently.
|
|
107
312
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
|
|
108
313
|
if (typeof sessionId !== 'string' || sessionId.length === 0) return null;
|
|
109
314
|
if (typeof mode !== 'string' || mode.length === 0) return null;
|
|
110
315
|
|
|
316
|
+
// #1635: after /clear or resume in the MAIN checkout, a lead's active lease
|
|
317
|
+
// means the lead runs headless jobs here — our lock would make its slot check
|
|
318
|
+
// report the repo as occupied. Yield instead of acquiring, and give back a
|
|
319
|
+
// lock this same raw id already holds here.
|
|
320
|
+
const leadHolder = await leadHoldingMainCheckout({
|
|
321
|
+
repoRoot,
|
|
322
|
+
sessionId,
|
|
323
|
+
predecessorSessionId,
|
|
324
|
+
predecessorProcess,
|
|
325
|
+
claudePid,
|
|
326
|
+
nativeSource,
|
|
327
|
+
readLease: _readLeaseImpl,
|
|
328
|
+
});
|
|
329
|
+
if (leadHolder !== null) {
|
|
330
|
+
// Released: a lock with our own raw id (a resume where the harness kept the
|
|
331
|
+
// id), or one written under our own Claude Code process (#1641 — startup →
|
|
332
|
+
// /clear mints a new raw id in the same process). Never a lock whose only
|
|
333
|
+
// "it was us" witness is current-session.json: every session start in this
|
|
334
|
+
// checkout rewrites that file, so a peer's /clear or resume would release a
|
|
335
|
+
// LIVE foreign lock. A predecessor from before #1641 carries no
|
|
336
|
+
// `claude_pid` and still stays until its TTL.
|
|
337
|
+
await releaseOwnLockForLead({ repoRoot, sessionId, claudePid, releaseImpl: _releaseImpl });
|
|
338
|
+
await writeOwnReleaseMarker(repoRoot, sessionId);
|
|
339
|
+
if (typeof onNotice === 'function') {
|
|
340
|
+
// The lease reader admits only [A-Za-z0-9._-] ids, so the id is banner-safe.
|
|
341
|
+
const sid8 = leadHolder.slice(0, 8);
|
|
342
|
+
try {
|
|
343
|
+
onNotice(
|
|
344
|
+
`Haupt-Checkout belegt durch Lead ${sid8}, arbeite im Worktree`,
|
|
345
|
+
`🧭 Haupt-Checkout belegt durch Lead ${sid8} — kein Session-Lock, arbeite im Worktree`,
|
|
346
|
+
);
|
|
347
|
+
} catch { /* the notice is informational */ }
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// #1651: this raw session gave its lock back on purpose
|
|
353
|
+
// (scripts/release-session-lock.mjs), typically so a headless run can use
|
|
354
|
+
// the slot. A compaction or resume of that same session is not new work and
|
|
355
|
+
// must not take the lock again; startup and clear acquire as before. The
|
|
356
|
+
// marker is removed by the session's next deliberate acquire().
|
|
357
|
+
// #1747: a marker the lead yield above wrote counts only while a lead lease
|
|
358
|
+
// is still active; once the lead is gone the session acquires again.
|
|
359
|
+
if (
|
|
360
|
+
typeof nativeSource === 'string' &&
|
|
361
|
+
RELEASED_LOCK_STAYS_RELEASED_SOURCES.has(nativeSource) &&
|
|
362
|
+
(await hasOwnReleaseMarker(repoRoot, sessionId))
|
|
363
|
+
) {
|
|
364
|
+
const marker = await readOwnReleaseMarker(repoRoot, sessionId);
|
|
365
|
+
const yielded = marker?.reason === LEAD_YIELD_REASON;
|
|
366
|
+
const leadId = yielded ? await activeLeadSessionId(_readLeaseImpl) : null;
|
|
367
|
+
if (!yielded || leadId !== null) {
|
|
368
|
+
if (typeof onNotice === 'function') {
|
|
369
|
+
try {
|
|
370
|
+
if (yielded) {
|
|
371
|
+
const sid8 = leadId.slice(0, 8);
|
|
372
|
+
onNotice(
|
|
373
|
+
`Haupt-Checkout belegt durch Lead ${sid8}, arbeite im Worktree`,
|
|
374
|
+
`🧭 Haupt-Checkout belegt durch Lead ${sid8} — kein Session-Lock, arbeite im Worktree`,
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
onNotice(
|
|
378
|
+
'Session-Lock wurde in dieser Session bewusst freigegeben und nach dem Neueinstieg nicht neu angelegt',
|
|
379
|
+
'ℹ Session-Lock bewusst freigegeben — nach dem Neueinstieg nicht neu angelegt',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
} catch { /* the notice is informational */ }
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
111
388
|
// Resolve DI shims at call time so test mocks can replace the imports.
|
|
112
389
|
let acquireFn = _acquireImpl;
|
|
113
390
|
let forceAcquireFn = _forceAcquireImpl;
|
|
@@ -181,10 +458,23 @@ export async function bootstrapLock({
|
|
|
181
458
|
// live lock, conjunct (c) holds for the wrong session. `current-session.json`
|
|
182
459
|
// carries no session field of its own (see
|
|
183
460
|
// `.claude/rules/identity-and-locks.md` § shared repo artefacts), which is
|
|
184
|
-
// exactly why this is a ceiling and not a proof.
|
|
185
|
-
//
|
|
461
|
+
// exactly why this is a ceiling and not a proof.
|
|
462
|
+
//
|
|
463
|
+
// #1641 — (b) never holds after /clear: resolveSessionId() mints the label
|
|
464
|
+
// anew as maxN+1, because the predecessor's own lock is counted (measured
|
|
465
|
+
// 2026-10-15: lock `…-session-1`, new label `…-session-2`). The process-bound
|
|
466
|
+
// alternative replaces (b)+(c) with ONE witness the lock itself carries:
|
|
467
|
+
// `claude_pid` equal to the Claude Code process this hook runs under, on this
|
|
468
|
+
// host (isSameClaudeProcess). It needs no repo-global artefact, so the
|
|
469
|
+
// ceiling above does not apply to it. (a) still gates it.
|
|
186
470
|
const isSameLogicalReentry =
|
|
187
471
|
typeof nativeSource === 'string' && SAME_LOGICAL_SESSION_SOURCES.has(nativeSource);
|
|
472
|
+
const ownProcessPredecessor =
|
|
473
|
+
acquireResult.ok !== true &&
|
|
474
|
+
acquireResult.reason === 'active' &&
|
|
475
|
+
isSameLogicalReentry &&
|
|
476
|
+
acquireResult.existingLock?.session_id !== sessionId &&
|
|
477
|
+
(await isSameClaudeProcess(acquireResult.existingLock, claudePid));
|
|
188
478
|
|
|
189
479
|
const shouldForce =
|
|
190
480
|
acquireResult.ok !== true && (
|
|
@@ -200,6 +490,7 @@ export async function bootstrapLock({
|
|
|
200
490
|
// which this predicate could never force: one damaged file wedged every
|
|
201
491
|
// new session in the repo.
|
|
202
492
|
acquireResult.reason === 'corrupt' ||
|
|
493
|
+
ownProcessPredecessor ||
|
|
203
494
|
(acquireResult.reason === 'active' &&
|
|
204
495
|
acquireResult.existingLock &&
|
|
205
496
|
(acquireResult.existingLock.session_id === sessionId ||
|
|
@@ -238,6 +529,9 @@ export async function bootstrapLock({
|
|
|
238
529
|
(forcedFrom === 'corrupt' || (forcedFrom === 'stale-heartbeat' && priorOwner !== sessionId))
|
|
239
530
|
) {
|
|
240
531
|
reclaimed = forcedFrom;
|
|
532
|
+
} else if (acquireResult?.ok === true && ownProcessPredecessor) {
|
|
533
|
+
// #1641: countable, so the rate of /clear takeovers can be measured.
|
|
534
|
+
reclaimed = 'own-process';
|
|
241
535
|
}
|
|
242
536
|
if (reclaimingCorrupt) {
|
|
243
537
|
// The file's content is not echoed: it is unbounded and may be anything.
|
|
@@ -348,6 +642,10 @@ export async function bootstrapLock({
|
|
|
348
642
|
typeof semanticSessionId === 'string' && semanticSessionId.length > 0
|
|
349
643
|
? semanticSessionId
|
|
350
644
|
: (typeof baseLock.session_id === 'string' ? baseLock.session_id : sessionId),
|
|
645
|
+
// #1641: the process-bound witness a later /clear in this same process
|
|
646
|
+
// recognises its predecessor by (isSameClaudeProcess). Absent, not null,
|
|
647
|
+
// when the harness sets no CLAUDE_PID.
|
|
648
|
+
...(typeof claudePid === 'number' ? { claude_pid: claudePid } : {}),
|
|
351
649
|
};
|
|
352
650
|
|
|
353
651
|
{
|
|
@@ -383,6 +681,15 @@ export async function bootstrapLock({
|
|
|
383
681
|
} catch { /* best-effort — a missing proof never breaks session-start */ }
|
|
384
682
|
|
|
385
683
|
// Step 3: best-effort observability breadcrumb.
|
|
684
|
+
// #1636 Pkt 2: a live peer that works here while its lock sits in another
|
|
685
|
+
// checkout is invisible to acquire(); record it (true/false/null) so its rate
|
|
686
|
+
// is measured before any conflict banner exists. Absent only if the reader
|
|
687
|
+
// itself cannot be loaded.
|
|
688
|
+
let foreignWorkRoot;
|
|
689
|
+
try {
|
|
690
|
+
const { foreignWorkRootIn } = await import('./hook-root.mjs');
|
|
691
|
+
foreignWorkRoot = foreignWorkRootIn(repoRoot, sessionId);
|
|
692
|
+
} catch { /* the measurement is best-effort */ }
|
|
386
693
|
await emitBestEffort(_emitEventImpl, 'orchestrator.session.lock.acquired', {
|
|
387
694
|
session_id: enriched.session_id,
|
|
388
695
|
semantic_session_id: enriched.semantic_session_id,
|
|
@@ -391,11 +698,134 @@ export async function bootstrapLock({
|
|
|
391
698
|
host: enriched.host,
|
|
392
699
|
ttl_hours: enriched.ttl_hours,
|
|
393
700
|
...(reclaimed ? { reclaimed } : {}),
|
|
701
|
+
...(foreignWorkRoot !== undefined ? { foreign_work_root: foreignWorkRoot } : {}),
|
|
394
702
|
});
|
|
395
703
|
|
|
396
704
|
return enriched;
|
|
397
705
|
}
|
|
398
706
|
|
|
707
|
+
/**
|
|
708
|
+
* #1635 — on a lead yield, release the main checkout's lock IFF it carries our
|
|
709
|
+
* own raw `session_id`, or was written under our own Claude Code process on this
|
|
710
|
+
* host (#1641, isSameClaudeProcess); any other lock is never touched. The delete
|
|
711
|
+
* goes through session-lock.mjs `release()` under the LOCK's raw id with the
|
|
712
|
+
* repo's persisted owner proof, so release() re-checks both the id and the proof
|
|
713
|
+
* itself. Never throws; a failed release is one stderr WARN and changes nothing
|
|
714
|
+
* else — the caller still skips the acquire.
|
|
715
|
+
*
|
|
716
|
+
* @param {{ repoRoot: string, sessionId: string, claudePid?: number|null, releaseImpl?: Function }} args
|
|
717
|
+
* @returns {Promise<void>}
|
|
718
|
+
*/
|
|
719
|
+
async function releaseOwnLockForLead({ repoRoot, sessionId, claudePid = null, releaseImpl }) {
|
|
720
|
+
let reason = null;
|
|
721
|
+
try {
|
|
722
|
+
const lockMod = await import('../../scripts/lib/session-lock.mjs');
|
|
723
|
+
const existing = lockMod.readLock({ repoRoot });
|
|
724
|
+
if (!existing || typeof existing.session_id !== 'string') return;
|
|
725
|
+
if (existing.session_id !== sessionId && !(await isSameClaudeProcess(existing, claudePid))) return;
|
|
726
|
+
const result = (releaseImpl ?? lockMod.release)({
|
|
727
|
+
sessionId: existing.session_id,
|
|
728
|
+
repoRoot,
|
|
729
|
+
proof: lockMod.loadOwnerProof({ repoRoot }),
|
|
730
|
+
});
|
|
731
|
+
if (!result || result.ok !== true) reason = result?.reason ?? 'unknown';
|
|
732
|
+
else if (result.deleted !== true) reason = result.reason ?? 'not-deleted';
|
|
733
|
+
else if (result.verified === false) reason = 'lock-still-present';
|
|
734
|
+
} catch (err) {
|
|
735
|
+
reason = err?.message ?? 'unknown';
|
|
736
|
+
}
|
|
737
|
+
if (reason !== null) {
|
|
738
|
+
process.stderr.write(
|
|
739
|
+
`⚠ lock-bootstrap: own session.lock could not be released for the fleet lead (${reason}) — it blocks the lead until its TTL\n`,
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* #1635 — the session id of a fleet lead whose ACTIVE lease keeps this session
|
|
746
|
+
* out of the main checkout, or null when the lock is taken as before.
|
|
747
|
+
*
|
|
748
|
+
* Non-null only when ALL hold: `nativeSource` is clear/resume; `repoRoot/.git`
|
|
749
|
+
* is a directory (the main worktree — a linked worktree or submodule has a
|
|
750
|
+
* `.git` FILE, no subprocess needed); `repoRoot/.orchestrator/runtime/fleet`
|
|
751
|
+
* is a directory (the lease is HOST-global, this marks the repos the fleet
|
|
752
|
+
* works in); the lease reads `active`; and its `session_id` is neither our
|
|
753
|
+
* raw id nor our predecessor's (the lead's own tab after /clear has a new raw
|
|
754
|
+
* id while the lease still carries the old one — it keeps the old behaviour).
|
|
755
|
+
*
|
|
756
|
+
* #1746 — "our predecessor's" needs a process witness. `predecessorSessionId`
|
|
757
|
+
* comes from current-session.json, which every session start in this checkout
|
|
758
|
+
* rewrites: once the lead started here LAST, its raw id was every other tab's
|
|
759
|
+
* "predecessor", and their /clear took the lock (LSO #778, twice on
|
|
760
|
+
* 2026-10-22). The id counts only when `predecessorProcess` names our own
|
|
761
|
+
* Claude Code process on this host (isSameClaudeProcess, the #1641 witness).
|
|
762
|
+
* Cost, accepted: the lead's own tab now also yields when the entry carries no
|
|
763
|
+
* witness — no readable CLAUDE_PID (Codex, Cursor) or a file written before
|
|
764
|
+
* #1746. Harmless: the lead works through its lease, not this lock. (When
|
|
765
|
+
* another session started in between, the tab yielded before #1746 already:
|
|
766
|
+
* the file then names that session.)
|
|
767
|
+
* Ceiling (BV-004): a dead lead process whose pid the OS recycled to our
|
|
768
|
+
* Claude process still reads as "ours" (the same residual isSameClaudeProcess
|
|
769
|
+
* names) and takes the lock as before #1746.
|
|
770
|
+
*
|
|
771
|
+
* Fail direction: every other outcome — no lease (no fleet: silent), an
|
|
772
|
+
* expired lease, an unreadable one (one stderr WARN), any exception — returns
|
|
773
|
+
* null, i.e. the pre-#1635 acquire path. It never blocks the session.
|
|
774
|
+
*
|
|
775
|
+
* @param {{ repoRoot: string, sessionId: string, predecessorSessionId: string|null, predecessorProcess: object|null, claudePid: number|null, nativeSource: string|null, readLease: Function }} args
|
|
776
|
+
* @returns {Promise<string|null>}
|
|
777
|
+
*/
|
|
778
|
+
async function leadHoldingMainCheckout({ repoRoot, sessionId, predecessorSessionId, predecessorProcess, claudePid, nativeSource, readLease }) {
|
|
779
|
+
try {
|
|
780
|
+
if (typeof nativeSource !== 'string' || !LEAD_YIELD_SOURCES.has(nativeSource)) return null;
|
|
781
|
+
if (!isDirectory(path.join(repoRoot, '.git'))) return null;
|
|
782
|
+
if (!isDirectory(path.join(repoRoot, '.orchestrator', 'runtime', 'fleet'))) return null;
|
|
783
|
+
const verdict = await readLease();
|
|
784
|
+
if (verdict?.state === 'unreadable') {
|
|
785
|
+
process.stderr.write(
|
|
786
|
+
`⚠ lock-bootstrap: navigator lease unreadable (${verdict.reason ?? 'unknown'}) — lead occupancy not measurable, taking the lock as before\n`,
|
|
787
|
+
);
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
if (verdict?.state !== 'active') return null;
|
|
791
|
+
const leadId = verdict.lease?.session_id;
|
|
792
|
+
if (typeof leadId !== 'string' || leadId.length === 0) return null;
|
|
793
|
+
if (leadId === sessionId) return null;
|
|
794
|
+
if (leadId === predecessorSessionId && (await isSameClaudeProcess(predecessorProcess, claudePid))) return null;
|
|
795
|
+
return leadId;
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* #1747 — the session id of an ACTIVE fleet-lead lease, or null for every
|
|
803
|
+
* other outcome (none, expired, unreadable, a throwing reader). The marker
|
|
804
|
+
* was written only in the main checkout under a foreign lead, so the lease
|
|
805
|
+
* alone decides here. Never throws.
|
|
806
|
+
*
|
|
807
|
+
* @param {Function} readLease
|
|
808
|
+
* @returns {Promise<string|null>}
|
|
809
|
+
*/
|
|
810
|
+
async function activeLeadSessionId(readLease) {
|
|
811
|
+
try {
|
|
812
|
+
const verdict = await readLease();
|
|
813
|
+
const leadId = verdict?.state === 'active' ? verdict.lease?.session_id : null;
|
|
814
|
+
return typeof leadId === 'string' && leadId.length > 0 ? leadId : null;
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** True when `p` is a directory itself (lstat: a symlink does not count). Never throws. */
|
|
821
|
+
function isDirectory(p) {
|
|
822
|
+
try {
|
|
823
|
+
return fs.lstatSync(p).isDirectory();
|
|
824
|
+
} catch {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
399
829
|
/**
|
|
400
830
|
* Emit one event; every failure is swallowed so a missing events module never
|
|
401
831
|
* breaks the hook (observability is best-effort).
|
|
@@ -52,6 +52,9 @@ export const GENERATED_IGNORE = [
|
|
|
52
52
|
'!*/',
|
|
53
53
|
'!/bootstrap.lock',
|
|
54
54
|
'!/metrics/learnings.jsonl',
|
|
55
|
+
// #1516: the inbox-role store and its receipts travel with learnings.jsonl.
|
|
56
|
+
'!/metrics/learnings-inbox.jsonl',
|
|
57
|
+
'!/metrics/learnings-inbox-ingested.jsonl',
|
|
55
58
|
'!/metrics/sessions.jsonl',
|
|
56
59
|
'!/metrics/README.md',
|
|
57
60
|
'!/policy/quality-gates.json',
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* claim, with an `occurrences` count (#1198: the fleet's worst repo held
|
|
28
28
|
* 3,360 records over 205 distinct `claim_text` values = duplication
|
|
29
29
|
* factor 16.4, measured 2026-09-06 over
|
|
30
|
-
*
|
|
30
|
+
* a consumer repo's `.orchestrator/metrics/events.jsonl`).
|
|
31
31
|
*
|
|
32
32
|
* @module hooks/_lib/subagent-transcript
|
|
33
33
|
*/
|
|
@@ -495,7 +495,7 @@ const MAX_OBSERVATIONS = 50;
|
|
|
495
495
|
* of them was measured as a live false-positive class.
|
|
496
496
|
*
|
|
497
497
|
* Measured 2026-09-06 over the 205 DISTINCT `claim_text` values in
|
|
498
|
-
*
|
|
498
|
+
* a consumer repo's `.orchestrator/metrics/events.jsonl`
|
|
499
499
|
* (the fleet's worst-affected repo; command in the module test header):
|
|
500
500
|
* 201/205 re-fire on the pre-#1218 matcher, of which 35 are markdown table
|
|
501
501
|
* rows and 9 are ATX headings — 44 firings, 21.9% of the corpus, with a true
|
|
@@ -88,11 +88,11 @@
|
|
|
88
88
|
* ## Named ceilings (BV-004)
|
|
89
89
|
*
|
|
90
90
|
* - A create call reached only through a command substitution
|
|
91
|
-
* (`x=$(glab issue create …)`)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* — `resolveSegmentVerb` reports the payload string, but this
|
|
95
|
-
* not recurse into it. Revisit
|
|
91
|
+
* (`x=$(glab issue create …)`) IS matched since #1667: the shared lexer
|
|
92
|
+
* hoists the substitution body into a segment of its own. A create carried
|
|
93
|
+
* as an interpreter PAYLOAD (`bash -c 'glab issue create'`) is still NOT
|
|
94
|
+
* matched — `resolveSegmentVerb` reports the payload string, but this
|
|
95
|
+
* matcher does not recurse into it. Revisit if the shape shows up in the overflow
|
|
96
96
|
* triage of a per-session counter file
|
|
97
97
|
* (`.orchestrator/runtime/issue-budget/<hash>.json`, #1141) or in a
|
|
98
98
|
* transcript census of real create calls.
|
|
@@ -115,11 +115,10 @@
|
|
|
115
115
|
* `hooks/pre-bash-issue-budget.mjs` treats it exactly like a loop body —
|
|
116
116
|
* deny in `strict`, report the undercount in `warn`. The remaining ceiling
|
|
117
117
|
* is the option grammar, named at {@link xargsDrivenRemainder}.
|
|
118
|
-
* - A paren
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* tokens five consumers read positionally.
|
|
118
|
+
* - A subshell paren before the verb (`(glab issue create …)`) IS reached
|
|
119
|
+
* since #1667: the lexer turns an unquoted word-initial `(` and its `)`
|
|
120
|
+
* into statement breaks — the fix landed in the lexer, as this ceiling
|
|
121
|
+
* asked, not as a re-patch here.
|
|
123
122
|
* - {@link matchesBypass} still compares from token index 0, NOT from the
|
|
124
123
|
* resolved verb index. So a wrapped create (`nohup gh pr create --dry-run`)
|
|
125
124
|
* is gated even when `gh pr create --dry-run` is a bypass entry. That is
|
|
@@ -514,9 +513,10 @@ const CAT_SUBST_RE = /^\$\(cat\s+(\S+)\)$/;
|
|
|
514
513
|
/**
|
|
515
514
|
* Read the value of a flag in `names` (`--x v`, `--x=v`, `-x v`). When the flag
|
|
516
515
|
* repeats, the LAST value wins — the pflag semantics `gh`/`glab` apply.
|
|
517
|
-
* An UNQUOTED `$(cat p)`
|
|
518
|
-
*
|
|
519
|
-
* `--x=$(cat p)`
|
|
516
|
+
* An UNQUOTED `$(cat p)` reaches this function as ONE token: since #1667 the
|
|
517
|
+
* lexer keeps a substitution's raw text in the outer word (and hoists its body
|
|
518
|
+
* as a statement of its own), so `--x $(cat p)` and `--x=$(cat p)` need no
|
|
519
|
+
* rejoining.
|
|
520
520
|
*
|
|
521
521
|
* @param {Array<{ text: string, quoted: boolean }>} tokens
|
|
522
522
|
* @param {string[]} names
|
|
@@ -537,10 +537,6 @@ function flagValue(tokens, names) {
|
|
|
537
537
|
} else {
|
|
538
538
|
continue;
|
|
539
539
|
}
|
|
540
|
-
if (value === '$(cat' && typeof tokens[i + 1]?.text === 'string') {
|
|
541
|
-
value = `$(cat ${tokens[i + 1].text}`;
|
|
542
|
-
i += 1;
|
|
543
|
-
}
|
|
544
540
|
found = value;
|
|
545
541
|
break;
|
|
546
542
|
}
|
|
@@ -31,6 +31,7 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
|
31
31
|
|
|
32
32
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
33
33
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
34
|
+
import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
|
|
34
35
|
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Constants
|
|
@@ -88,7 +89,10 @@ async function main() {
|
|
|
88
89
|
new_cwd: newCwd,
|
|
89
90
|
};
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
// #1605 B5 — annotate THIS session's current-session.json where
|
|
93
|
+
// on-session-start.mjs put it (the entered worktree after a /clear there),
|
|
94
|
+
// found by the lock carrying the raw payload id; launch dir otherwise.
|
|
95
|
+
const sessionFile = path.join(resolveOwnLockRoot(input, getProjectDir()), '.orchestrator', 'current-session.json');
|
|
92
96
|
|
|
93
97
|
const result = await atomicMutateJson(sessionFile, {}, (current) => {
|
|
94
98
|
const existing = Array.isArray(current.cwd_changes)
|
|
@@ -317,9 +317,10 @@ async function main() {
|
|
|
317
317
|
// (#1194).
|
|
318
318
|
//
|
|
319
319
|
// CEILING (BV-004): on a harness that exports no session env var and puts no
|
|
320
|
-
// `session_id` in the hook payload (
|
|
321
|
-
//
|
|
322
|
-
//
|
|
320
|
+
// `session_id` in the hook payload (Cursor today) both tiers are empty, so
|
|
321
|
+
// this gate is permanently `'unknown'` = enforce = pre-#1153 behaviour there.
|
|
322
|
+
// Codex CLI does send `session_id` (measured 2026-10-20, codex-cli 0.160.1,
|
|
323
|
+
// #1691), so a manifest bound to another id classifies `'foreign'` there.
|
|
323
324
|
{
|
|
324
325
|
// `ownIds` is the process-local set built above G3.
|
|
325
326
|
const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
|