session-orchestrator 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-lock.mjs — distributed session-lock with TTL (issue #330).
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic acquire/release/inspect for a per-repo session.lock file
|
|
5
|
+
* stored at `.orchestrator/session.lock`. Prevents concurrent deep sessions
|
|
6
|
+
* from stomping each other's metrics writes and wave executor state.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - No-throw: every exported function catches filesystem errors and returns a
|
|
10
|
+
* structured failure object instead of propagating exceptions.
|
|
11
|
+
* - Atomic writes: acquire() writes to a temp file then renames, giving a
|
|
12
|
+
* single syscall that is either visible or not on POSIX systems.
|
|
13
|
+
* - Cross-host aware: PID liveness checks are skipped when the lock came from
|
|
14
|
+
* a different hostname (can't signal a remote process).
|
|
15
|
+
* - Decision deferred: acquire() reports stale locks but does NOT auto-clear
|
|
16
|
+
* them. session-start handles the recovery AUQ flow (W3-C3).
|
|
17
|
+
*
|
|
18
|
+
* BARREL CONTRACT (#630 A1 barrel-preserving split):
|
|
19
|
+
* This module bundled THREE orthogonal lock protocols. Two of them — the
|
|
20
|
+
* STATE.md write-lock and the staging-fence commit-mutex — were moved into
|
|
21
|
+
* dedicated modules under `scripts/lib/locks/`. This file STAYS the canonical
|
|
22
|
+
* barrel: it keeps the session-lock CORE protocol AND re-exports every moved
|
|
23
|
+
* symbol, so the original 22-symbol import surface is preserved EXACTLY and
|
|
24
|
+
* all 17 importers keep working UNCHANGED. The moved modules import shared
|
|
25
|
+
* primitives from leaf modules (file-lock.mjs, locks/lock-body.mjs) and NEVER
|
|
26
|
+
* from this file, so the barrel ↔ protocol-module edge is one-directional
|
|
27
|
+
* (no import cycle).
|
|
28
|
+
* - STATE.md write-lock → ./locks/state-md-lock.mjs
|
|
29
|
+
* - staging-fence mutex → ./locks/staging-fence-lock.mjs
|
|
30
|
+
*
|
|
31
|
+
* No external dependencies — Node 20+ stdlib only.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import os from 'node:os';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
import crypto from 'node:crypto';
|
|
38
|
+
import { classifyMode } from './exclusivity-matrix.mjs';
|
|
39
|
+
import { isPidAliveOnHost } from './file-lock.mjs';
|
|
40
|
+
|
|
41
|
+
// isPidAliveOnHost moved into file-lock.mjs in #630 (the file-lock primitive
|
|
42
|
+
// owns it so the dependency edge points file-lock → io, never the reverse).
|
|
43
|
+
// Re-exported here so existing importers (agent-status historically,
|
|
44
|
+
// session-discovery's forensic note, memory-proposals historically, and any
|
|
45
|
+
// external caller) keep resolving `isPidAliveOnHost` from session-lock.mjs.
|
|
46
|
+
export { isPidAliveOnHost };
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Re-exports — STATE.md write-lock + staging-fence commit-mutex (#630 split).
|
|
50
|
+
// These two protocols live in dedicated modules now; the barrel re-exports
|
|
51
|
+
// their full public surface so importers of session-lock.mjs are unchanged.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
STATE_LOCK_PATH,
|
|
56
|
+
DEFAULT_STATE_LOCK_TIMEOUT_MS,
|
|
57
|
+
STATE_LOCK_POLL_MS,
|
|
58
|
+
acquireStateLock,
|
|
59
|
+
releaseStateLock,
|
|
60
|
+
withStateMdLock,
|
|
61
|
+
} from './locks/state-md-lock.mjs';
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
STAGING_FENCE_LOCK_PATH,
|
|
65
|
+
DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS,
|
|
66
|
+
STAGING_FENCE_LOCK_POLL_MS,
|
|
67
|
+
acquireStagingFenceLock,
|
|
68
|
+
releaseStagingFenceLock,
|
|
69
|
+
withStagingFenceLock,
|
|
70
|
+
} from './locks/staging-fence-lock.mjs';
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Constants
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export const DEFAULT_TTL_HOURS = 4;
|
|
77
|
+
export const LOCK_PATH = '.orchestrator/session.lock';
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Internal helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
// isPidAliveOnHost lives in file-lock.mjs (#630) and is re-exported at the top
|
|
84
|
+
// of this module. It is a SAME-HOST PID liveness probe (POSIX signal-0). It is
|
|
85
|
+
// NOT the discovery-path liveness check — since Epic #583 the discovery
|
|
86
|
+
// decision tree uses heartbeat-age via {@link isLockLive} instead, because the
|
|
87
|
+
// `pid` recorded on a session.lock is the *ephemeral hook subprocess* PID.
|
|
88
|
+
// Same-host callers (`acquire`, `checkStale`, and the state-lock /
|
|
89
|
+
// staging-fence stale-override paths, now via the file-lock primitive) use it
|
|
90
|
+
// only for the short-lived stale-override path where the recorded PID IS the
|
|
91
|
+
// live writer's PID. See file-lock.mjs for the full @forensic + PID-recycle
|
|
92
|
+
// trade-off note.
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the absolute path to the lock file.
|
|
96
|
+
* @param {string|undefined} repoRoot
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function lockPathFor(repoRoot) {
|
|
100
|
+
return path.join(repoRoot ?? process.cwd(), LOCK_PATH);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return the current time as an ISO-8601 string.
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function nowIso() {
|
|
108
|
+
return new Date().toISOString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determine whether a lock's TTL has expired.
|
|
113
|
+
* @param {{ started_at: string, ttl_hours: number }} lock
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function isTtlExpired(lock) {
|
|
117
|
+
const age = Date.now() - Date.parse(lock.started_at);
|
|
118
|
+
return age > lock.ttl_hours * 3600 * 1000;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compute the age of a lock in fractional hours.
|
|
123
|
+
* Returns null if started_at is unparseable.
|
|
124
|
+
* @param {{ started_at: string }} lock
|
|
125
|
+
* @returns {number|null}
|
|
126
|
+
*/
|
|
127
|
+
function lockAgeHours(lock) {
|
|
128
|
+
const ts = Date.parse(lock.started_at);
|
|
129
|
+
if (Number.isNaN(ts)) return null;
|
|
130
|
+
return (Date.now() - ts) / (3600 * 1000);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse lock file contents into an object. Returns null on any parse error.
|
|
135
|
+
*
|
|
136
|
+
* Schema v2 (Epic #583, W2-I3): adds `last_heartbeat` (optional, populated by
|
|
137
|
+
* `updateHeartbeat()`) + `semantic_session_id` (optional, populated when the
|
|
138
|
+
* `session_id` field carries a UUID and the caller wants to preserve the
|
|
139
|
+
* always-semantic id alongside it).
|
|
140
|
+
*
|
|
141
|
+
* Back-compat: v1 locks (no `last_heartbeat`) are normalised on read with
|
|
142
|
+
* `last_heartbeat = started_at`. The optional `semantic_session_id` field is
|
|
143
|
+
* left undefined when absent. This lets pre-#583 lockfiles flow through the
|
|
144
|
+
* new liveness rule transparently — the v1 lock's `started_at` becomes its
|
|
145
|
+
* effective heartbeat, so TTL freshness still rescues recent locks even when
|
|
146
|
+
* the writer process is dead (the D2/D5 production case).
|
|
147
|
+
*
|
|
148
|
+
* @param {string} raw
|
|
149
|
+
* @returns {object|null}
|
|
150
|
+
*/
|
|
151
|
+
function parseLock(raw) {
|
|
152
|
+
try {
|
|
153
|
+
const obj = JSON.parse(raw);
|
|
154
|
+
if (
|
|
155
|
+
typeof obj === 'object' &&
|
|
156
|
+
obj !== null &&
|
|
157
|
+
typeof obj.session_id === 'string' &&
|
|
158
|
+
typeof obj.started_at === 'string' &&
|
|
159
|
+
typeof obj.mode === 'string' &&
|
|
160
|
+
typeof obj.pid === 'number' &&
|
|
161
|
+
typeof obj.host === 'string' &&
|
|
162
|
+
typeof obj.ttl_hours === 'number'
|
|
163
|
+
) {
|
|
164
|
+
// Schema v1 → v2 normalisation: when `last_heartbeat` is absent or
|
|
165
|
+
// non-string, treat the lock as if it heartbeat-ed once at started_at.
|
|
166
|
+
const normalised = { ...obj };
|
|
167
|
+
if (typeof normalised.last_heartbeat !== 'string' || normalised.last_heartbeat.length === 0) {
|
|
168
|
+
normalised.last_heartbeat = normalised.started_at;
|
|
169
|
+
}
|
|
170
|
+
// semantic_session_id stays undefined when absent — callers that need it
|
|
171
|
+
// should fall back to session_id.
|
|
172
|
+
return normalised;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build a fresh lock object from caller-supplied fields.
|
|
182
|
+
*
|
|
183
|
+
* Schema v2 (Epic #583, W2-I3): `last_heartbeat` is seeded equal to
|
|
184
|
+
* `started_at`. Callers MUST call `updateHeartbeat()` on a known cadence
|
|
185
|
+
* (session-start, inter-wave, session-end) to keep the lock alive past TTL.
|
|
186
|
+
*
|
|
187
|
+
* @param {{ sessionId: string, mode: string, ttlHours: number, semanticSessionId?: string }} args
|
|
188
|
+
* @returns {object}
|
|
189
|
+
*/
|
|
190
|
+
function buildLock({ sessionId, mode, ttlHours, semanticSessionId }) {
|
|
191
|
+
const startedAt = nowIso();
|
|
192
|
+
const lock = {
|
|
193
|
+
session_id: sessionId,
|
|
194
|
+
started_at: startedAt,
|
|
195
|
+
last_heartbeat: startedAt,
|
|
196
|
+
mode,
|
|
197
|
+
pid: process.pid,
|
|
198
|
+
host: os.hostname(),
|
|
199
|
+
ttl_hours: ttlHours,
|
|
200
|
+
};
|
|
201
|
+
if (typeof semanticSessionId === 'string' && semanticSessionId.length > 0) {
|
|
202
|
+
lock.semantic_session_id = semanticSessionId;
|
|
203
|
+
}
|
|
204
|
+
return lock;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Determine whether a lock is "live" based on its last_heartbeat freshness
|
|
209
|
+
* relative to TTL. Replaces PID-liveness as the primary discovery-time
|
|
210
|
+
* liveness check (Epic #583, W1-D1 + W1-D4 consensus): the writer-process
|
|
211
|
+
* PID is the *hook* PID, not the session PID, so PID-liveness incorrectly
|
|
212
|
+
* filtered out locks whose semantic owner (the Claude harness process) was
|
|
213
|
+
* still alive.
|
|
214
|
+
*
|
|
215
|
+
* Liveness rule: a lock is live when (now - last_heartbeat) < ttl_hours.
|
|
216
|
+
*
|
|
217
|
+
* @param {{ last_heartbeat: string, started_at: string, ttl_hours?: number }} lock
|
|
218
|
+
* @param {number} [nowMs]
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
export function isLockLive(lock, nowMs = Date.now()) {
|
|
222
|
+
if (!lock || typeof lock !== 'object') return false;
|
|
223
|
+
// Back-compat: prefer last_heartbeat; fall back to started_at when absent.
|
|
224
|
+
const hbStr = (typeof lock.last_heartbeat === 'string' && lock.last_heartbeat.length > 0)
|
|
225
|
+
? lock.last_heartbeat
|
|
226
|
+
: lock.started_at;
|
|
227
|
+
const heartbeatMs = Date.parse(hbStr);
|
|
228
|
+
if (Number.isNaN(heartbeatMs)) return false;
|
|
229
|
+
const ttlHours = typeof lock.ttl_hours === 'number' ? lock.ttl_hours : DEFAULT_TTL_HOURS;
|
|
230
|
+
const ttlMs = ttlHours * 3600 * 1000;
|
|
231
|
+
return (nowMs - heartbeatMs) < ttlMs;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Atomically write a lock object to disk.
|
|
236
|
+
* Uses a tmp file in a mkdtemp directory + rename to avoid a partial-write race.
|
|
237
|
+
* @param {string} lockFile Absolute path to the lock file.
|
|
238
|
+
* @param {object} lock Lock object to serialize.
|
|
239
|
+
* @returns {{ ok: true } | { ok: false, reason: 'fs-error', error: string }}
|
|
240
|
+
*/
|
|
241
|
+
function writeLockAtomic(lockFile, lock) {
|
|
242
|
+
try {
|
|
243
|
+
const dir = path.dirname(lockFile);
|
|
244
|
+
// Ensure .orchestrator/ directory exists.
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
|
|
247
|
+
// Write to a uniquely-named temp file in the same directory so rename()
|
|
248
|
+
// is guaranteed to be an atomic same-filesystem operation on POSIX.
|
|
249
|
+
const tmpSuffix = crypto.randomBytes(6).toString('hex');
|
|
250
|
+
const tmpFile = path.join(dir, `.session.lock.tmp.${tmpSuffix}`);
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(tmpFile, JSON.stringify(lock, null, 2) + '\n', { encoding: 'utf8' });
|
|
253
|
+
fs.renameSync(tmpFile, lockFile);
|
|
254
|
+
|
|
255
|
+
return { ok: true };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Atomically create the session-lock file via tmp + hardlink (create-or-fail).
|
|
263
|
+
*
|
|
264
|
+
* TOCTOU fix (#590 Item 2): the previous fresh-acquire path used
|
|
265
|
+
* `writeLockAtomic` (tmp + renameSync), which is last-writer-wins — two
|
|
266
|
+
* concurrent SessionStart hooks that BOTH observed `readLock() === null` would
|
|
267
|
+
* BOTH rename their tmp file over the lock and BOTH believe they acquired it.
|
|
268
|
+
* `linkSync` is POSIX-atomic create-or-fail: exactly one concurrent caller wins
|
|
269
|
+
* the create, every other caller gets EEXIST. This is the same idiom used by
|
|
270
|
+
* the file-lock primitive (tryAcquireFileLock) backing the state-lock and
|
|
271
|
+
* staging-fence-lock modules.
|
|
272
|
+
*
|
|
273
|
+
* Used ONLY by the no-existing-lock branch of {@link acquire}. The
|
|
274
|
+
* intentional-overwrite paths (`forceAcquire`, `updateHeartbeat`) keep using
|
|
275
|
+
* `writeLockAtomic` because they MUST replace an existing lock, not fail on it.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} lockFile Absolute path to .orchestrator/session.lock.
|
|
278
|
+
* @param {object} lock Lock object to serialize.
|
|
279
|
+
* @returns {{ ok: true } | { ok: false, reason: 'exists' } | { ok: false, reason: 'fs-error', error: string }}
|
|
280
|
+
*/
|
|
281
|
+
function createSessionLockExclusive(lockFile, lock) {
|
|
282
|
+
const dir = path.dirname(lockFile);
|
|
283
|
+
let tmpFile;
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
286
|
+
const tmpSuffix = crypto.randomBytes(8).toString('hex');
|
|
287
|
+
tmpFile = path.join(dir, `.session.lock.create.tmp.${tmpSuffix}`);
|
|
288
|
+
fs.writeFileSync(tmpFile, JSON.stringify(lock, null, 2) + '\n', { encoding: 'utf8' });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (tmpFile) {
|
|
291
|
+
try { fs.unlinkSync(tmpFile); } catch { /* best-effort */ }
|
|
292
|
+
}
|
|
293
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
fs.linkSync(tmpFile, lockFile);
|
|
298
|
+
return { ok: true };
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (err.code === 'EEXIST') {
|
|
301
|
+
return { ok: false, reason: 'exists' };
|
|
302
|
+
}
|
|
303
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
304
|
+
} finally {
|
|
305
|
+
try { fs.unlinkSync(tmpFile); } catch { /* best-effort */ }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Exported API
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Read the lock file without modifying it.
|
|
315
|
+
* Returns the parsed lock object, or null if absent or unparseable.
|
|
316
|
+
* Never throws.
|
|
317
|
+
*
|
|
318
|
+
* @param {{ repoRoot?: string }} [opts]
|
|
319
|
+
* @returns {{ session_id: string, started_at: string, mode: string, pid: number, host: string, ttl_hours: number } | null}
|
|
320
|
+
*/
|
|
321
|
+
export function readLock(opts = {}) {
|
|
322
|
+
const lockFile = lockPathFor(opts.repoRoot);
|
|
323
|
+
try {
|
|
324
|
+
const raw = fs.readFileSync(lockFile, 'utf8');
|
|
325
|
+
return parseLock(raw);
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Atomically acquire the session lock.
|
|
333
|
+
*
|
|
334
|
+
* Consults the P1.1 exclusivity-matrix when `activeSessions` is provided.
|
|
335
|
+
* When omitted, falls back to the legacy local-lock-only logic (backward compat).
|
|
336
|
+
*
|
|
337
|
+
* @param {object} args
|
|
338
|
+
* @param {string} args.sessionId
|
|
339
|
+
* @param {string} args.mode
|
|
340
|
+
* @param {number} [args.ttlHours]
|
|
341
|
+
* @param {string} [args.repoRoot]
|
|
342
|
+
* @param {string} [args.semanticSessionId]
|
|
343
|
+
* Optional always-semantic session id (e.g., `<branch>-<date>-<mode>-<n>`).
|
|
344
|
+
* When `sessionId` is a UUID (Claude Code path) and a semantic id is also
|
|
345
|
+
* known, pass it here to be persisted alongside the UUID. Schema v2.
|
|
346
|
+
* @param {boolean} [args.quiet=false]
|
|
347
|
+
* When true, the unknown-mode classify path SKIPS the `console.warn`-to-stderr
|
|
348
|
+
* while still defaulting the caller-class to 'parallel-ok'. Added for #592
|
|
349
|
+
* MED-2: the SessionStart hook (lock-bootstrap.mjs) must keep stderr empty,
|
|
350
|
+
* and previously pre-sanitised the mode locally to dodge this warn. This flag
|
|
351
|
+
* lets callers opt into silent unknown-mode handling without duplicating the
|
|
352
|
+
* mode-mapping logic. PURELY ADDITIVE — default (warn) behaviour is unchanged,
|
|
353
|
+
* and the raw `mode` is still persisted on the lock body either way.
|
|
354
|
+
* @param {Array<{mode:string,pid:number,host:string,sessionId:string}>} [args.activeSessions]
|
|
355
|
+
* Optional pre-computed array from discoverActiveSessions(repoRoot). When omitted,
|
|
356
|
+
* matrix consultation is skipped (legacy behavior). Callers (worktree-pipeline.mjs,
|
|
357
|
+
* hooks/on-session-start.mjs) call discoverActiveSessions() themselves and pass the
|
|
358
|
+
* result — this keeps acquire() synchronous.
|
|
359
|
+
*
|
|
360
|
+
* Returns one of:
|
|
361
|
+
* { ok: true, lock, exclusivityClass? }
|
|
362
|
+
* — lock created
|
|
363
|
+
* { ok: false, reason: 'active', existingLock, exclusivityClass? }
|
|
364
|
+
* — local lock held (live TTL, live PID)
|
|
365
|
+
* { ok: false, reason: 'stale-pid-dead', existingLock, exclusivityClass? }
|
|
366
|
+
* — local lock stale (dead PID)
|
|
367
|
+
* { ok: false, reason: 'stale-pid-alive', existingLock, exclusivityClass? }
|
|
368
|
+
* — local lock stale (live PID, TTL expired)
|
|
369
|
+
* { ok: false, reason: 'fs-error', error, exclusivityClass? }
|
|
370
|
+
* — filesystem failure
|
|
371
|
+
* { ok: false, reason: 'active-incompatible-exclusive', allActiveSessions, blockingSession, exclusivityClass }
|
|
372
|
+
* — caller blocked by an active exclusive-class session (P1.2 #570)
|
|
373
|
+
* { ok: false, reason: 'active-compatible-parallel', allActiveSessions, exclusivityClass }
|
|
374
|
+
* — caller could create a parallel session; preamble offers Worktree-Auto-Promotion (P1.2 #570)
|
|
375
|
+
* { ok: false, reason: 'active-readonly-bypass', allActiveSessions, exclusivityClass: 'always-ok' }
|
|
376
|
+
* — caller is read-only-class; preamble passes through without AUQ (P1.2 #570).
|
|
377
|
+
* Callers for 'always-ok' modes SHOULD interpret this as "proceed without AUQ, no lock needed".
|
|
378
|
+
*
|
|
379
|
+
* The `exclusivityClass` field is optional (undefined when activeSessions is not passed)
|
|
380
|
+
* and is added to ALL return shapes so callers can always observe the caller's class.
|
|
381
|
+
*
|
|
382
|
+
* The caller (session-start) decides whether to invoke forceAcquire() after
|
|
383
|
+
* obtaining user consent.
|
|
384
|
+
*/
|
|
385
|
+
export function acquire({ sessionId, mode, ttlHours = DEFAULT_TTL_HOURS, repoRoot, activeSessions, semanticSessionId, quiet = false } = {}) {
|
|
386
|
+
const lockFile = lockPathFor(repoRoot);
|
|
387
|
+
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
// Safe classifyMode wrapper — unknown modes default to 'parallel-ok' (most
|
|
390
|
+
// permissive) rather than propagating an exception into the try/catch below
|
|
391
|
+
// where it would be silently turned into an 'fs-error'. A console.warn is
|
|
392
|
+
// emitted for visibility UNLESS the caller passes `quiet: true` (#592 MED-2 —
|
|
393
|
+
// lets lock-bootstrap.mjs keep stderr empty without pre-mapping the mode).
|
|
394
|
+
// This call is intentionally OUTSIDE the main try/catch so that only
|
|
395
|
+
// fs-errors reach the catch block.
|
|
396
|
+
// -------------------------------------------------------------------------
|
|
397
|
+
let callerClass;
|
|
398
|
+
try {
|
|
399
|
+
callerClass = classifyMode(mode);
|
|
400
|
+
} catch {
|
|
401
|
+
if (quiet !== true) {
|
|
402
|
+
console.warn(
|
|
403
|
+
`acquire: unknown mode "${mode}" — defaulting exclusivityClass to "parallel-ok". ` +
|
|
404
|
+
'Add the mode to exclusivity-matrix.mjs if intentional.',
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
callerClass = 'parallel-ok';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// -------------------------------------------------------------------------
|
|
411
|
+
// P1.2 exclusivity-matrix consultation — only when activeSessions is provided.
|
|
412
|
+
// Run BEFORE local-lock check so parallel-session conflicts surface first.
|
|
413
|
+
// -------------------------------------------------------------------------
|
|
414
|
+
if (Array.isArray(activeSessions) && activeSessions.length > 0) {
|
|
415
|
+
let hasCompatibleParallel = false;
|
|
416
|
+
|
|
417
|
+
for (const entry of activeSessions) {
|
|
418
|
+
// Safe classify for each active session's mode.
|
|
419
|
+
let entryClass;
|
|
420
|
+
try {
|
|
421
|
+
entryClass = classifyMode(entry.mode);
|
|
422
|
+
} catch {
|
|
423
|
+
// Unknown active session mode — treat as parallel-ok (most permissive default).
|
|
424
|
+
entryClass = 'parallel-ok';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (entryClass === 'exclusive' && callerClass !== 'always-ok') {
|
|
428
|
+
// An exclusive active session blocks all non-always-ok callers.
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
reason: 'active-incompatible-exclusive',
|
|
432
|
+
exclusivityClass: callerClass,
|
|
433
|
+
allActiveSessions: activeSessions,
|
|
434
|
+
blockingSession: entry,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (entryClass === 'parallel-ok' && callerClass === 'parallel-ok') {
|
|
439
|
+
hasCompatibleParallel = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// After loop: handle always-ok bypass (read-only caller).
|
|
444
|
+
if (callerClass === 'always-ok') {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
reason: 'active-readonly-bypass',
|
|
448
|
+
exclusivityClass: 'always-ok',
|
|
449
|
+
allActiveSessions: activeSessions,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Parallel-compatible situation: preamble should offer Worktree-Auto-Promotion.
|
|
454
|
+
if (hasCompatibleParallel) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
reason: 'active-compatible-parallel',
|
|
458
|
+
exclusivityClass: callerClass,
|
|
459
|
+
allActiveSessions: activeSessions,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// All active sessions are 'always-ok' and caller is non-always-ok, or
|
|
464
|
+
// no blocking condition was found — fall through to local-lock check.
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// -------------------------------------------------------------------------
|
|
468
|
+
// Local lock check — unchanged logic from original acquire().
|
|
469
|
+
// -------------------------------------------------------------------------
|
|
470
|
+
try {
|
|
471
|
+
// Classify an existing lock into the correct failure result. Shared by the
|
|
472
|
+
// up-front readLock() check AND the create-race EEXIST-loser path below so
|
|
473
|
+
// both report identical active / stale-pid-dead / stale-pid-alive reasons.
|
|
474
|
+
const classifyExisting = (existing) => {
|
|
475
|
+
const sameHost = existing.host === os.hostname();
|
|
476
|
+
// PID liveness is only meaningful on the same host.
|
|
477
|
+
const pidAlive = sameHost ? isPidAliveOnHost(existing.pid) : null;
|
|
478
|
+
|
|
479
|
+
// Heartbeat-first liveness (#744): isLockLive is the SOLE active gate.
|
|
480
|
+
// A dead recorded PID must NOT veto a fresh last_heartbeat — the pid on
|
|
481
|
+
// a session.lock is the ephemeral hook subprocess PID, not the semantic
|
|
482
|
+
// session's own PID (Epic #583, W1-D1 + W1-D4 consensus). Likewise,
|
|
483
|
+
// isTtlExpired measures age from started_at, which wrongly flags a
|
|
484
|
+
// long-running-but-heartbeating session as expired. Both bugs together
|
|
485
|
+
// produced the #744 incident: a live heartbeating session was
|
|
486
|
+
// misclassified 'stale-pid-dead' mid-wave.
|
|
487
|
+
if (isLockLive(existing)) {
|
|
488
|
+
return { ok: false, reason: 'active', existingLock: existing, exclusivityClass: callerClass };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Heartbeat expired — classify the stale variant. Cross-host locks never
|
|
492
|
+
// have a confirmable dead PID (pidAlive stays null), so they always land
|
|
493
|
+
// on 'stale-pid-alive' rather than 'stale-pid-dead'.
|
|
494
|
+
const reason = (pidAlive === false) ? 'stale-pid-dead' : 'stale-pid-alive';
|
|
495
|
+
return { ok: false, reason, existingLock: existing, exclusivityClass: callerClass };
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const existing = readLock({ repoRoot });
|
|
499
|
+
|
|
500
|
+
if (existing !== null) {
|
|
501
|
+
// A lock is present — classify it.
|
|
502
|
+
return classifyExisting(existing);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// No existing lock — create one with a TOCTOU-safe create-or-fail (#590).
|
|
506
|
+
// Two concurrent SessionStart hooks can both reach this branch having each
|
|
507
|
+
// observed readLock() === null; linkSync guarantees exactly one wins.
|
|
508
|
+
const lock = buildLock({ sessionId, mode, ttlHours, semanticSessionId });
|
|
509
|
+
const createResult = createSessionLockExclusive(lockFile, lock);
|
|
510
|
+
|
|
511
|
+
if (createResult.ok) {
|
|
512
|
+
return { ok: true, lock, exclusivityClass: callerClass };
|
|
513
|
+
}
|
|
514
|
+
if (createResult.reason === 'fs-error') {
|
|
515
|
+
return { ok: false, reason: 'fs-error', error: createResult.error, exclusivityClass: callerClass };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// reason === 'exists' — we lost the create race. Re-read the now-present
|
|
519
|
+
// lock and classify it exactly as if we had seen it on the up-front check.
|
|
520
|
+
const raced = readLock({ repoRoot });
|
|
521
|
+
if (raced === null) {
|
|
522
|
+
// The EEXIST winner's lock vanished before we could re-read it (ENOENT /
|
|
523
|
+
// unparseable). Defensive fallback: report 'active' so the caller defers
|
|
524
|
+
// rather than racing again — mirrors tryAcquireStateLock's vanish-race
|
|
525
|
+
// handling (a lost-then-vanished race resolves conservatively).
|
|
526
|
+
return { ok: false, reason: 'active', existingLock: null, exclusivityClass: callerClass };
|
|
527
|
+
}
|
|
528
|
+
return classifyExisting(raced);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
return { ok: false, reason: 'fs-error', error: err.message, exclusivityClass: callerClass };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Force-acquire the lock, unconditionally replacing any existing lock.
|
|
536
|
+
* Call only after the user has explicitly authorised stale-lock takeover.
|
|
537
|
+
*
|
|
538
|
+
* Returns:
|
|
539
|
+
* { ok: true, lock, replacedLock? } — lock written (replacedLock present if one was overwritten)
|
|
540
|
+
* { ok: false, reason: 'fs-error', ... } — filesystem failure
|
|
541
|
+
*
|
|
542
|
+
* @param {{ sessionId: string, mode: string, ttlHours?: number, repoRoot?: string, semanticSessionId?: string }} args
|
|
543
|
+
*/
|
|
544
|
+
export function forceAcquire({ sessionId, mode, ttlHours = DEFAULT_TTL_HOURS, repoRoot, semanticSessionId } = {}) {
|
|
545
|
+
try {
|
|
546
|
+
const replacedLock = readLock({ repoRoot });
|
|
547
|
+
const lock = buildLock({ sessionId, mode, ttlHours, semanticSessionId });
|
|
548
|
+
const lockFile = lockPathFor(repoRoot);
|
|
549
|
+
|
|
550
|
+
const writeResult = writeLockAtomic(lockFile, lock);
|
|
551
|
+
if (!writeResult.ok) return writeResult;
|
|
552
|
+
|
|
553
|
+
const result = { ok: true, lock };
|
|
554
|
+
if (replacedLock !== null) result.replacedLock = replacedLock;
|
|
555
|
+
return result;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Release the lock IFF it belongs to the given session_id.
|
|
563
|
+
* Silent no-op when the lock belongs to a different session or does not exist.
|
|
564
|
+
* Never throws.
|
|
565
|
+
*
|
|
566
|
+
* Post-delete verify (#744 Fix 3, refined): after unlinking, re-reads the
|
|
567
|
+
* lock path to confirm the delete was durable. The retry is **ownership-
|
|
568
|
+
* scoped** — only OUR OWN lock reappearing (same `session_id`) warrants a
|
|
569
|
+
* bounded retry unlink, which covers a transient unlink/stat race where our
|
|
570
|
+
* just-deleted file is briefly still observable. A FOREIGN lock (a
|
|
571
|
+
* different `session_id`) present at the re-read means a sibling session
|
|
572
|
+
* legitimately `acquire()`d the path in the race window between our unlink
|
|
573
|
+
* and this re-read — OUR lock is already gone (the delete succeeded), so
|
|
574
|
+
* this is treated as verified and the foreign lock is left untouched. This
|
|
575
|
+
* is load-bearing for PSA-005: a releaser must never unlink a lock it does
|
|
576
|
+
* not own, even indirectly via a "still present, so retry-unlink" heuristic
|
|
577
|
+
* that does not check who the present lock belongs to.
|
|
578
|
+
*
|
|
579
|
+
* `verified` is computed as "our lock is no longer present" — a foreign
|
|
580
|
+
* lock present is fine (ours is gone); only our own lock still being
|
|
581
|
+
* observable after the bounded retry sets `verified: false`.
|
|
582
|
+
*
|
|
583
|
+
* @param {{ sessionId: string, repoRoot?: string }} args
|
|
584
|
+
* @returns {{ ok: true, deleted: boolean, reason?: string, verified?: boolean }}
|
|
585
|
+
*/
|
|
586
|
+
export function release({ sessionId, repoRoot } = {}) {
|
|
587
|
+
const lockFile = lockPathFor(repoRoot);
|
|
588
|
+
try {
|
|
589
|
+
const existing = readLock({ repoRoot });
|
|
590
|
+
|
|
591
|
+
if (existing === null) {
|
|
592
|
+
return { ok: true, deleted: false, reason: 'no-lock' };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (existing.session_id !== sessionId) {
|
|
596
|
+
return { ok: true, deleted: false, reason: 'session-mismatch' };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
fs.unlinkSync(lockFile);
|
|
600
|
+
|
|
601
|
+
// Post-delete verify: only retry when OUR OWN lock is still observable.
|
|
602
|
+
// A foreign lock here belongs to a sibling that re-acquired in the race
|
|
603
|
+
// window — never touch it (PSA-005).
|
|
604
|
+
let after = readLock({ repoRoot });
|
|
605
|
+
if (after !== null && after.session_id === sessionId) {
|
|
606
|
+
try {
|
|
607
|
+
fs.unlinkSync(lockFile);
|
|
608
|
+
} catch {
|
|
609
|
+
// best-effort retry — fall through to the final re-check regardless.
|
|
610
|
+
}
|
|
611
|
+
after = readLock({ repoRoot });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const verified = after === null || after.session_id !== sessionId;
|
|
615
|
+
return { ok: true, deleted: true, verified };
|
|
616
|
+
} catch (err) {
|
|
617
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Refresh the `last_heartbeat` field on an existing lock, atomically.
|
|
623
|
+
*
|
|
624
|
+
* Schema v2 (Epic #583, W2-I3): the lock's liveness is determined by
|
|
625
|
+
* `(now - last_heartbeat) < ttl_hours`. Callers MUST invoke this on a known
|
|
626
|
+
* cadence (session-start, inter-wave, session-end) to keep the lock alive
|
|
627
|
+
* across long sessions.
|
|
628
|
+
*
|
|
629
|
+
* Same-session guard: refuses to update someone else's lock. Returns `false`
|
|
630
|
+
* when the lock is absent, malformed, or held by a different session_id.
|
|
631
|
+
* Returns `true` on a successful atomic update.
|
|
632
|
+
*
|
|
633
|
+
* Atomicity: same tmp + rename pattern as writeLockAtomic — single syscall
|
|
634
|
+
* visibility on POSIX. Never throws.
|
|
635
|
+
*
|
|
636
|
+
* @param {{ repoRoot?: string, sessionId: string }} opts
|
|
637
|
+
* @returns {boolean}
|
|
638
|
+
*/
|
|
639
|
+
export function updateHeartbeat({ repoRoot, sessionId } = {}) {
|
|
640
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return false;
|
|
641
|
+
const existing = readLock({ repoRoot });
|
|
642
|
+
if (existing === null) return false;
|
|
643
|
+
if (existing.session_id !== sessionId) return false;
|
|
644
|
+
const updated = { ...existing, last_heartbeat: nowIso() };
|
|
645
|
+
const lockFile = lockPathFor(repoRoot);
|
|
646
|
+
const writeResult = writeLockAtomic(lockFile, updated);
|
|
647
|
+
return writeResult.ok === true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Inspect the lock file and compute staleness metadata.
|
|
652
|
+
*
|
|
653
|
+
* @param {{ repoRoot?: string }} [opts]
|
|
654
|
+
* @returns {{
|
|
655
|
+
* exists: boolean,
|
|
656
|
+
* lock: object|null,
|
|
657
|
+
* ageHours: number|null,
|
|
658
|
+
* ttlExpired: boolean,
|
|
659
|
+
* pidAlive: boolean|null,
|
|
660
|
+
* host: string|null,
|
|
661
|
+
* sameHost: boolean,
|
|
662
|
+
* isLive: boolean
|
|
663
|
+
* }}
|
|
664
|
+
*/
|
|
665
|
+
export function checkStale({ repoRoot } = {}) {
|
|
666
|
+
const lock = readLock({ repoRoot });
|
|
667
|
+
|
|
668
|
+
if (lock === null) {
|
|
669
|
+
return {
|
|
670
|
+
exists: false,
|
|
671
|
+
lock: null,
|
|
672
|
+
ageHours: null,
|
|
673
|
+
ttlExpired: false,
|
|
674
|
+
pidAlive: null,
|
|
675
|
+
host: null,
|
|
676
|
+
sameHost: false,
|
|
677
|
+
isLive: false,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const ageHours = lockAgeHours(lock);
|
|
682
|
+
const ttlExpired = isTtlExpired(lock);
|
|
683
|
+
const sameHost = lock.host === os.hostname();
|
|
684
|
+
// Only attempt PID check when the lock was written on this machine.
|
|
685
|
+
const pidAlive = sameHost ? isPidAliveOnHost(lock.pid) : null;
|
|
686
|
+
// Heartbeat-based liveness (#744) — additive field alongside the pre-existing
|
|
687
|
+
// ttlExpired/pidAlive/sameHost fields (back-compat). This is the SAME check
|
|
688
|
+
// acquire()'s classifyExisting now uses as its sole active gate, surfaced
|
|
689
|
+
// here so callers of checkStale() (recovery-flow diagnostics) can observe
|
|
690
|
+
// when isLive diverges from the legacy pidAlive/ttlExpired signals.
|
|
691
|
+
const isLive = isLockLive(lock);
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
exists: true,
|
|
695
|
+
lock,
|
|
696
|
+
ageHours,
|
|
697
|
+
ttlExpired,
|
|
698
|
+
pidAlive,
|
|
699
|
+
host: lock.host,
|
|
700
|
+
sameHost,
|
|
701
|
+
isLive,
|
|
702
|
+
};
|
|
703
|
+
}
|