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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locks/lock-body.mjs — shared leaf helpers for the short-lived lock protocols
|
|
3
|
+
* (STATE.md write-lock + staging-fence commit-mutex) split out of
|
|
4
|
+
* session-lock.mjs in #630 (A1 barrel-preserving split).
|
|
5
|
+
*
|
|
6
|
+
* This is a PURE leaf: it imports NOTHING (Node stdlib only used inline). Both
|
|
7
|
+
* scripts/lib/locks/state-md-lock.mjs and staging-fence-lock.mjs import these
|
|
8
|
+
* three helpers so the previously-shared `nowIso` / `delay` / `parseLockBody`
|
|
9
|
+
* live in exactly one place instead of being duplicated across the two modules.
|
|
10
|
+
*
|
|
11
|
+
* It does NOT import session-lock.mjs (which re-exports the two protocol
|
|
12
|
+
* modules) — the dependency edge points locks/* → lock-body, never the reverse,
|
|
13
|
+
* so there is no import cycle.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Return the current time as an ISO-8601 string.
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sleep helper for the acquire poll-loop. Promise-returning, so the loop is
|
|
26
|
+
* async without blocking the event loop.
|
|
27
|
+
* @param {number} ms
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
export function delay(ms) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a lock-file body shared by the state-lock and staging-fence-lock.
|
|
36
|
+
* Both locks use identical { pid, host, acquiredAt, holder } shape so a single
|
|
37
|
+
* parser serves both. Returns null on any malformed input.
|
|
38
|
+
*
|
|
39
|
+
* Moved verbatim from session-lock.mjs in #630 — behaviour preserved exactly
|
|
40
|
+
* (renamed from parseStateLock in #558 M4 because it serves both locks).
|
|
41
|
+
*
|
|
42
|
+
* @param {string} raw
|
|
43
|
+
* @returns {{ pid: number, host: string, acquiredAt: string, holder: string }|null}
|
|
44
|
+
*/
|
|
45
|
+
export function parseLockBody(raw) {
|
|
46
|
+
try {
|
|
47
|
+
const obj = JSON.parse(raw);
|
|
48
|
+
if (
|
|
49
|
+
typeof obj === 'object' &&
|
|
50
|
+
obj !== null &&
|
|
51
|
+
typeof obj.pid === 'number' &&
|
|
52
|
+
typeof obj.host === 'string' &&
|
|
53
|
+
typeof obj.acquiredAt === 'string' &&
|
|
54
|
+
typeof obj.holder === 'string'
|
|
55
|
+
) {
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locks/staging-fence-lock.mjs — staging-fence commit-mutex protocol
|
|
3
|
+
* (PSA-004 sub-mode C, issue #552).
|
|
4
|
+
*
|
|
5
|
+
* Split out of session-lock.mjs in #630 (A1 barrel-preserving split). The
|
|
6
|
+
* symbols below are re-exported UNCHANGED from session-lock.mjs so the original
|
|
7
|
+
* import surface is preserved for all 17 importers (incl.
|
|
8
|
+
* hooks/wave-scope-commit-guard.mjs).
|
|
9
|
+
*
|
|
10
|
+
* Held only around the wave-scope-commit-guard cross-fence check. Two sibling
|
|
11
|
+
* wave-agents that both pass through the per-agent guard race to acquire this
|
|
12
|
+
* lock; the winner inspects ALL fence files, the loser polls until the winner
|
|
13
|
+
* releases. Without the mutex the check is TOCTOU-vulnerable: agent A reads
|
|
14
|
+
* agent B's fence file BEFORE B writes agent B's last `git add` intent, and
|
|
15
|
+
* both proceed to `git commit` with overlapping staged paths.
|
|
16
|
+
*
|
|
17
|
+
* Implementation reuses the same tmp+linkSync cross-process pattern as the
|
|
18
|
+
* STATE.md lock (delegated to the shared file-lock primitive). The two lockfiles
|
|
19
|
+
* are distinct so STATE.md writes never contend with commit-guard checks.
|
|
20
|
+
*
|
|
21
|
+
* Dependency edges point locks/staging-fence-lock → { file-lock, locks/lock-body },
|
|
22
|
+
* NEVER → session-lock.mjs, so there is no import cycle.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
|
|
30
|
+
import { tryAcquireFileLock } from '../file-lock.mjs';
|
|
31
|
+
import { nowIso, delay, parseLockBody } from './lock-body.mjs';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Constants
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
// Staging-fence commit-mutex (PSA-004 sub-mode C, issue #552). Held only for
|
|
38
|
+
// the duration of the wave-scope-commit-guard's cross-agent fence check.
|
|
39
|
+
// - state.lock = "STATE.md is being written right now"
|
|
40
|
+
// - staging-fence.lock = "the cross-fence commit check is running right now"
|
|
41
|
+
// Distinct lockfile so the two locks never contend with each other.
|
|
42
|
+
export const STAGING_FENCE_LOCK_PATH = '.orchestrator/staging-fence/.commit.lock';
|
|
43
|
+
export const DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS = 10000;
|
|
44
|
+
export const STAGING_FENCE_LOCK_POLL_MS = 100;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Internal helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the absolute path to the staging-fence commit lock file.
|
|
52
|
+
* @param {string|undefined} repoRoot
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function stagingFenceLockPathFor(repoRoot) {
|
|
56
|
+
return path.join(repoRoot ?? process.cwd(), STAGING_FENCE_LOCK_PATH);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a fresh staging-fence lock body. Same shape as buildStateLockBody so
|
|
61
|
+
* the parseLockBody parser works identically — there is no need for a
|
|
62
|
+
* second parser.
|
|
63
|
+
*
|
|
64
|
+
* @param {{ holder?: string }} args
|
|
65
|
+
* @returns {{ pid: number, host: string, acquiredAt: string, holder: string }}
|
|
66
|
+
*/
|
|
67
|
+
function buildStagingFenceLockBody({ holder }) {
|
|
68
|
+
return {
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
host: os.hostname(),
|
|
71
|
+
acquiredAt: nowIso(),
|
|
72
|
+
holder: typeof holder === 'string' && holder.length > 0 ? holder : `pid-${process.pid}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Single-pass acquire attempt for the staging-fence lock. Mirrors
|
|
78
|
+
* tryAcquireStateLock — only the lockfile path + tmp prefix differ.
|
|
79
|
+
*
|
|
80
|
+
* Returns:
|
|
81
|
+
* { ok: true, lock } — acquired
|
|
82
|
+
* { ok: false, reason: 'held', existingLock } — live holder; caller polls
|
|
83
|
+
* { ok: false, reason: 'fs-error', error } — filesystem failure
|
|
84
|
+
*/
|
|
85
|
+
function tryAcquireStagingFenceLock(lockFile, body) {
|
|
86
|
+
// Delegates to the shared file-lock primitive (issue #630). Structurally
|
|
87
|
+
// identical to tryAcquireStateLock — only the tmp prefix + WARN messages
|
|
88
|
+
// differ. Behavior preserved EXACTLY: pretty body (indent 2), PID staleCheck,
|
|
89
|
+
// console.warn channel, override prefix `.staging-fence.lock.tmp`,
|
|
90
|
+
// ENOENT-on-read collapsed into `held`/existingLock:null.
|
|
91
|
+
const attempt = tryAcquireFileLock(lockFile, {
|
|
92
|
+
staleCheck: 'pid',
|
|
93
|
+
holder: body.holder,
|
|
94
|
+
indent: 2,
|
|
95
|
+
tmpPrefix: '.staging-fence.lock.tmp',
|
|
96
|
+
warnMessage: (reason, _lp, existing) =>
|
|
97
|
+
existing === null
|
|
98
|
+
? 'stale staging-fence.lock (unparseable contents) overridden'
|
|
99
|
+
: `stale staging-fence.lock from PID ${existing.pid} overridden`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (attempt.acquired) return { ok: true, lock: attempt.body };
|
|
103
|
+
if (attempt.reason === 'fs-error') return { ok: false, reason: 'fs-error', error: attempt.error };
|
|
104
|
+
return { ok: false, reason: 'held', existingLock: attempt.existing ?? null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Exported API
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Acquire the staging-fence commit-lock. Polls every STAGING_FENCE_LOCK_POLL_MS
|
|
113
|
+
* until the lock is acquired or the timeout expires. Same semantics as
|
|
114
|
+
* acquireStateLock.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {number} [opts.timeoutMs=10000]
|
|
118
|
+
* @param {string} [opts.repoRoot]
|
|
119
|
+
* @param {string} [opts.holder]
|
|
120
|
+
* @param {number} [opts.pollMs]
|
|
121
|
+
*/
|
|
122
|
+
export async function acquireStagingFenceLock({
|
|
123
|
+
timeoutMs = DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS,
|
|
124
|
+
repoRoot,
|
|
125
|
+
holder,
|
|
126
|
+
pollMs = STAGING_FENCE_LOCK_POLL_MS,
|
|
127
|
+
} = {}) {
|
|
128
|
+
const lockFile = stagingFenceLockPathFor(repoRoot);
|
|
129
|
+
const body = buildStagingFenceLockBody({ holder });
|
|
130
|
+
const deadline = Date.now() + (typeof timeoutMs === 'number' && timeoutMs >= 0
|
|
131
|
+
? timeoutMs
|
|
132
|
+
: DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS);
|
|
133
|
+
const effectivePollMs = typeof pollMs === 'number' && pollMs > 0
|
|
134
|
+
? pollMs
|
|
135
|
+
: STAGING_FENCE_LOCK_POLL_MS;
|
|
136
|
+
|
|
137
|
+
for (;;) {
|
|
138
|
+
const attempt = tryAcquireStagingFenceLock(lockFile, body);
|
|
139
|
+
if (attempt.ok) return attempt;
|
|
140
|
+
if (attempt.reason === 'fs-error') return attempt;
|
|
141
|
+
|
|
142
|
+
if (Date.now() >= deadline) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: 'timeout',
|
|
146
|
+
existingLock: attempt.existingLock ?? null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
await delay(effectivePollMs);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Release the staging-fence commit-lock IFF the holder matches.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [opts]
|
|
157
|
+
* @param {string} [opts.repoRoot]
|
|
158
|
+
* @param {string} [opts.holder]
|
|
159
|
+
* @returns {{ ok: true } | { ok: false, reason: 'not-found'|'not-owner'|'fs-error', error?: string }}
|
|
160
|
+
*/
|
|
161
|
+
export function releaseStagingFenceLock({ repoRoot, holder } = {}) {
|
|
162
|
+
const lockFile = stagingFenceLockPathFor(repoRoot);
|
|
163
|
+
|
|
164
|
+
let raw;
|
|
165
|
+
try {
|
|
166
|
+
raw = fs.readFileSync(lockFile, 'utf8');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.code === 'ENOENT') {
|
|
169
|
+
return { ok: false, reason: 'not-found' };
|
|
170
|
+
}
|
|
171
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lock = parseLockBody(raw);
|
|
175
|
+
if (lock === null) {
|
|
176
|
+
return { ok: false, reason: 'not-owner' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const ownerMatch = typeof holder === 'string' && holder.length > 0
|
|
180
|
+
? lock.holder === holder
|
|
181
|
+
: lock.pid === process.pid && lock.host === os.hostname();
|
|
182
|
+
|
|
183
|
+
if (!ownerMatch) {
|
|
184
|
+
return { ok: false, reason: 'not-owner' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
fs.unlinkSync(lockFile);
|
|
189
|
+
return { ok: true };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err.code === 'ENOENT') {
|
|
192
|
+
return { ok: false, reason: 'not-found' };
|
|
193
|
+
}
|
|
194
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* High-level wrapper: acquire the staging-fence commit-lock, run `fn`,
|
|
200
|
+
* release on completion or throw. Always releases — even if `fn` throws —
|
|
201
|
+
* before re-raising.
|
|
202
|
+
*
|
|
203
|
+
* No Session Config short-circuit is provided. Unlike the STATE.md lock
|
|
204
|
+
* (which is bypassed when `state-md-lock.enabled: false`), this lock guards
|
|
205
|
+
* a single small read-modify-write on a hidden runtime directory and has
|
|
206
|
+
* no performance cost worth opting out of. If the lock genuinely needs to
|
|
207
|
+
* be disabled, callers can skip the wrapper entirely.
|
|
208
|
+
*
|
|
209
|
+
* Throws when:
|
|
210
|
+
* - acquireStagingFenceLock fails (timeout or fs-error) → labelled Error.
|
|
211
|
+
* - `fn` throws → the original error is re-thrown after release.
|
|
212
|
+
*
|
|
213
|
+
* @param {string|undefined} repoRoot
|
|
214
|
+
* @param {() => (T | Promise<T>)} fn
|
|
215
|
+
* @param {object} [opts]
|
|
216
|
+
* @param {number} [opts.timeoutMs]
|
|
217
|
+
* @param {string} [opts.holder]
|
|
218
|
+
* @param {number} [opts.pollMs]
|
|
219
|
+
* @returns {Promise<T>}
|
|
220
|
+
* @template T
|
|
221
|
+
*/
|
|
222
|
+
export async function withStagingFenceLock(repoRoot, fn, opts = {}) {
|
|
223
|
+
if (typeof fn !== 'function') {
|
|
224
|
+
throw new TypeError('withStagingFenceLock: fn must be a function');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const holder = typeof opts.holder === 'string' && opts.holder.length > 0
|
|
228
|
+
? opts.holder
|
|
229
|
+
: `pid-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
230
|
+
|
|
231
|
+
const acquireResult = await acquireStagingFenceLock({
|
|
232
|
+
repoRoot,
|
|
233
|
+
timeoutMs: opts.timeoutMs,
|
|
234
|
+
holder,
|
|
235
|
+
pollMs: opts.pollMs,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!acquireResult.ok) {
|
|
239
|
+
const reason = acquireResult.reason;
|
|
240
|
+
const extra = reason === 'timeout' && acquireResult.existingLock
|
|
241
|
+
? ` (held by ${acquireResult.existingLock.holder}, pid=${acquireResult.existingLock.pid})`
|
|
242
|
+
: reason === 'fs-error' && acquireResult.error
|
|
243
|
+
? `: ${acquireResult.error}`
|
|
244
|
+
: '';
|
|
245
|
+
const err = new Error(`withStagingFenceLock: acquire failed (${reason})${extra}`);
|
|
246
|
+
err.code = `STAGING_FENCE_LOCK_${reason.toUpperCase().replace(/-/g, '_')}`;
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let result;
|
|
251
|
+
let caughtError = null;
|
|
252
|
+
try {
|
|
253
|
+
result = await fn();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
caughtError = err;
|
|
256
|
+
} finally {
|
|
257
|
+
const releaseResult = releaseStagingFenceLock({ repoRoot, holder });
|
|
258
|
+
if (!releaseResult.ok && releaseResult.reason === 'fs-error') {
|
|
259
|
+
console.warn(
|
|
260
|
+
`withStagingFenceLock: release failed (fs-error: ${releaseResult.error ?? 'unknown'})`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (caughtError !== null) throw caughtError;
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* locks/state-md-lock.mjs — STATE.md write-lock protocol
|
|
3
|
+
* (PRD 2026-05-22 § 4 — Pattern 1, issue #518).
|
|
4
|
+
*
|
|
5
|
+
* Split out of session-lock.mjs in #630 (A1 barrel-preserving split). The
|
|
6
|
+
* symbols below are re-exported UNCHANGED from session-lock.mjs so the original
|
|
7
|
+
* import surface is preserved for all 17 importers.
|
|
8
|
+
*
|
|
9
|
+
* Mechanical enforcement of PSA-004 for STATE.md writes. Whereas the
|
|
10
|
+
* session-lock (session-lock.mjs) guards "this working-copy is held by one
|
|
11
|
+
* session", the state-lock guards "STATE.md is being written right now" — a
|
|
12
|
+
* short-lived lock acquired around every read-modify-write cycle.
|
|
13
|
+
*
|
|
14
|
+
* Design:
|
|
15
|
+
* - Atomic create via tmp + linkSync, delegated to the shared file-lock
|
|
16
|
+
* primitive (tryAcquireFileLock).
|
|
17
|
+
* - Body: { pid, host, acquiredAt, holder } — host is included so cross-host
|
|
18
|
+
* callers (rare but possible via shared filesystems) avoid spurious PID
|
|
19
|
+
* liveness checks against unrelated PIDs.
|
|
20
|
+
* - Stale detection: process.kill(pid, 0). When the holder is on the same
|
|
21
|
+
* host and the PID is dead (ESRCH), the lock is overridden atomically and
|
|
22
|
+
* a WARN is written to stderr. Cross-host stale locks are NOT auto-cleared
|
|
23
|
+
* — they fall through to the timeout path.
|
|
24
|
+
* - Poll cadence: 100 ms by default. Configurable via STATE_LOCK_POLL_MS but
|
|
25
|
+
* no public override — tests inject via the optional `pollMs` parameter.
|
|
26
|
+
*
|
|
27
|
+
* Returns structured results, never throws (acquireStateLock / releaseStateLock).
|
|
28
|
+
* withStateMdLock re-throws caller errors after releasing.
|
|
29
|
+
*
|
|
30
|
+
* Dependency edges point locks/state-md-lock → { file-lock, config/state-md-lock,
|
|
31
|
+
* locks/lock-body }, NEVER → session-lock.mjs, so there is no import cycle.
|
|
32
|
+
*
|
|
33
|
+
* NAMING NOTE: scripts/lib/config/state-md-lock.mjs is a DIFFERENT file — it
|
|
34
|
+
* parses the `state-md-lock:` Session Config key. This module is the lock IMPL.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import os from 'node:os';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import crypto from 'node:crypto';
|
|
41
|
+
|
|
42
|
+
import { _parseStateMdLock } from '../config/state-md-lock.mjs';
|
|
43
|
+
import { tryAcquireFileLock } from '../file-lock.mjs';
|
|
44
|
+
import { nowIso, delay, parseLockBody } from './lock-body.mjs';
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Constants
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
// STATE.md write-lock (PRD 2026-05-22 § 4 — Pattern 1, issue #518).
|
|
51
|
+
// Orthogonal to the session-lock:
|
|
52
|
+
// - session.lock = "this repo working-copy is held by an active session"
|
|
53
|
+
// - state.lock = "STATE.md is being written right now"
|
|
54
|
+
// Two distinct lock files so a session can hold its session-lock for hours
|
|
55
|
+
// while still allowing fast acquire/release cycles around individual writes.
|
|
56
|
+
export const STATE_LOCK_PATH = '.orchestrator/state.lock';
|
|
57
|
+
export const DEFAULT_STATE_LOCK_TIMEOUT_MS = 10000;
|
|
58
|
+
export const STATE_LOCK_POLL_MS = 100;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Internal helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the absolute path to the state-lock file.
|
|
66
|
+
* @param {string|undefined} repoRoot
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function stateLockPathFor(repoRoot) {
|
|
70
|
+
return path.join(repoRoot ?? process.cwd(), STATE_LOCK_PATH);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a fresh state-lock body.
|
|
75
|
+
* @param {{ holder?: string }} args
|
|
76
|
+
* @returns {{ pid: number, host: string, acquiredAt: string, holder: string }}
|
|
77
|
+
*/
|
|
78
|
+
function buildStateLockBody({ holder }) {
|
|
79
|
+
return {
|
|
80
|
+
pid: process.pid,
|
|
81
|
+
host: os.hostname(),
|
|
82
|
+
acquiredAt: nowIso(),
|
|
83
|
+
holder: typeof holder === 'string' && holder.length > 0 ? holder : `pid-${process.pid}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Attempt one acquisition pass. Returns:
|
|
89
|
+
* { ok: true, lock } — lock written
|
|
90
|
+
* { ok: false, reason: 'held', existingLock } — held by a live holder
|
|
91
|
+
* { ok: false, reason: 'fs-error', error } — filesystem failure
|
|
92
|
+
*
|
|
93
|
+
* Strategy:
|
|
94
|
+
* 1. Try O_EXCL create (cross-process mutex). Success → return immediately.
|
|
95
|
+
* 2. On EEXIST → read the existing lock, check PID liveness on same host.
|
|
96
|
+
* - Live PID → `held` (caller polls).
|
|
97
|
+
* - Dead PID (or unparseable contents) → stale, atomic override + WARN.
|
|
98
|
+
*
|
|
99
|
+
* Side-effect: stale-lock override writes a WARN to stderr. Cross-host locks
|
|
100
|
+
* are never auto-overridden (can't signal a process on another machine).
|
|
101
|
+
*/
|
|
102
|
+
function tryAcquireStateLock(lockFile, body) {
|
|
103
|
+
// Delegates to the shared file-lock primitive (issue #630). Behavior is
|
|
104
|
+
// preserved EXACTLY: pretty-printed body `{pid, host, acquiredAt, holder}`
|
|
105
|
+
// (indent 2), PID staleCheck, console.warn override channel with the original
|
|
106
|
+
// messages, override tmp prefix `.state.lock.tmp`, and the ENOENT-on-read race
|
|
107
|
+
// collapsed into `held`/existingLock:null (signalVanished:false).
|
|
108
|
+
//
|
|
109
|
+
// The primitive builds its own body (fresh acquiredAt per attempt) from the
|
|
110
|
+
// pid/host of THIS process plus the holder carried on `body`. acquiredAt is
|
|
111
|
+
// only meaningful on the written (winning) attempt, so reusing vs regenerating
|
|
112
|
+
// it across poll passes is observationally identical.
|
|
113
|
+
const attempt = tryAcquireFileLock(lockFile, {
|
|
114
|
+
staleCheck: 'pid',
|
|
115
|
+
holder: body.holder,
|
|
116
|
+
indent: 2,
|
|
117
|
+
tmpPrefix: '.state.lock.tmp',
|
|
118
|
+
warnMessage: (reason, _lp, existing) =>
|
|
119
|
+
existing === null
|
|
120
|
+
? 'stale state.lock (unparseable contents) overridden'
|
|
121
|
+
: `stale state.lock from PID ${existing.pid} overridden`,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (attempt.acquired) return { ok: true, lock: attempt.body };
|
|
125
|
+
if (attempt.reason === 'fs-error') return { ok: false, reason: 'fs-error', error: attempt.error };
|
|
126
|
+
// reason === 'held' (live holder, cross-host, or vanished-collapsed-to-held).
|
|
127
|
+
return { ok: false, reason: 'held', existingLock: attempt.existing ?? null };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Exported API
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Acquire the STATE.md write-lock. Polls every STATE_LOCK_POLL_MS until the
|
|
136
|
+
* lock is acquired or the timeout expires.
|
|
137
|
+
*
|
|
138
|
+
* Returns:
|
|
139
|
+
* { ok: true, lock } — lock acquired (possibly after waiting)
|
|
140
|
+
* { ok: false, reason: 'timeout', existingLock? } — timed out waiting for live holder
|
|
141
|
+
* { ok: false, reason: 'fs-error', error: string } — filesystem failure
|
|
142
|
+
*
|
|
143
|
+
* Stale-lock side-effects: when the existing lock points to a dead PID on
|
|
144
|
+
* the same host, the helper overrides it atomically and writes a WARN to
|
|
145
|
+
* stderr. The next poll iteration will then succeed.
|
|
146
|
+
*
|
|
147
|
+
* Never throws.
|
|
148
|
+
*
|
|
149
|
+
* @param {object} [opts]
|
|
150
|
+
* @param {number} [opts.timeoutMs=10000] — max wait in milliseconds.
|
|
151
|
+
* @param {string} [opts.repoRoot] — defaults to process.cwd().
|
|
152
|
+
* @param {string} [opts.holder] — human-readable holder string (default `pid-<pid>`).
|
|
153
|
+
* @param {number} [opts.pollMs] — test-only override of poll cadence.
|
|
154
|
+
*/
|
|
155
|
+
export async function acquireStateLock({
|
|
156
|
+
timeoutMs = DEFAULT_STATE_LOCK_TIMEOUT_MS,
|
|
157
|
+
repoRoot,
|
|
158
|
+
holder,
|
|
159
|
+
pollMs = STATE_LOCK_POLL_MS,
|
|
160
|
+
} = {}) {
|
|
161
|
+
const lockFile = stateLockPathFor(repoRoot);
|
|
162
|
+
const body = buildStateLockBody({ holder });
|
|
163
|
+
const deadline = Date.now() + (typeof timeoutMs === 'number' && timeoutMs >= 0 ? timeoutMs : DEFAULT_STATE_LOCK_TIMEOUT_MS);
|
|
164
|
+
const effectivePollMs = typeof pollMs === 'number' && pollMs > 0 ? pollMs : STATE_LOCK_POLL_MS;
|
|
165
|
+
|
|
166
|
+
// Loop until acquired or deadline reached. The first iteration runs
|
|
167
|
+
// unconditionally so a timeoutMs of 0 still attempts one acquisition.
|
|
168
|
+
for (;;) {
|
|
169
|
+
const attempt = tryAcquireStateLock(lockFile, body);
|
|
170
|
+
if (attempt.ok) return attempt;
|
|
171
|
+
if (attempt.reason === 'fs-error') return attempt;
|
|
172
|
+
|
|
173
|
+
if (Date.now() >= deadline) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: 'timeout',
|
|
177
|
+
existingLock: attempt.existingLock ?? null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
await delay(effectivePollMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Release the STATE.md write-lock IFF the holder matches.
|
|
186
|
+
*
|
|
187
|
+
* Caller must pass the same identifier they used in acquireStateLock — either
|
|
188
|
+
* `holder` (free-form string) OR `sessionId` (matched against the holder
|
|
189
|
+
* field when holder follows the `<sessionId>` convention). If neither is
|
|
190
|
+
* provided, the helper falls back to PID equality.
|
|
191
|
+
*
|
|
192
|
+
* Returns (per PRD § 4):
|
|
193
|
+
* { ok: true } — lock unlinked
|
|
194
|
+
* { ok: false, reason: 'not-found' } — no lock file exists
|
|
195
|
+
* { ok: false, reason: 'not-owner' } — lock held by different holder/PID
|
|
196
|
+
* { ok: false, reason: 'fs-error', error } — filesystem failure
|
|
197
|
+
*
|
|
198
|
+
* Never throws.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} [opts]
|
|
201
|
+
* @param {string} [opts.repoRoot]
|
|
202
|
+
* @param {string} [opts.sessionId] — matched against the `holder` field.
|
|
203
|
+
* @param {string} [opts.holder] — matched against the `holder` field (overrides sessionId).
|
|
204
|
+
*/
|
|
205
|
+
export function releaseStateLock({ repoRoot, sessionId, holder } = {}) {
|
|
206
|
+
const lockFile = stateLockPathFor(repoRoot);
|
|
207
|
+
|
|
208
|
+
let raw;
|
|
209
|
+
try {
|
|
210
|
+
raw = fs.readFileSync(lockFile, 'utf8');
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err.code === 'ENOENT') {
|
|
213
|
+
return { ok: false, reason: 'not-found' };
|
|
214
|
+
}
|
|
215
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const lock = parseLockBody(raw);
|
|
219
|
+
if (lock === null) {
|
|
220
|
+
// Unparseable — refuse to delete; some other process may be writing now.
|
|
221
|
+
return { ok: false, reason: 'not-owner' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const expectedHolder = holder ?? sessionId ?? null;
|
|
225
|
+
const ownerMatch = expectedHolder !== null
|
|
226
|
+
? lock.holder === expectedHolder
|
|
227
|
+
: lock.pid === process.pid && lock.host === os.hostname();
|
|
228
|
+
|
|
229
|
+
if (!ownerMatch) {
|
|
230
|
+
return { ok: false, reason: 'not-owner' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
fs.unlinkSync(lockFile);
|
|
235
|
+
return { ok: true };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err.code === 'ENOENT') {
|
|
238
|
+
return { ok: false, reason: 'not-found' };
|
|
239
|
+
}
|
|
240
|
+
return { ok: false, reason: 'fs-error', error: err.message };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* High-level wrapper: acquire the STATE.md write-lock, run `fn`, release on
|
|
246
|
+
* completion or throw. Always releases the lock — even if `fn` throws —
|
|
247
|
+
* before re-raising the original error.
|
|
248
|
+
*
|
|
249
|
+
* Short-circuit: when `state-md-lock.enabled: false` is set in CLAUDE.md
|
|
250
|
+
* (or AGENTS.md on Codex CLI — the two are aliases per
|
|
251
|
+
* `skills/_shared/instruction-file-resolution.md`) Session Config, the lock
|
|
252
|
+
* is bypassed entirely and `fn` is called directly. A stderr WARN line is
|
|
253
|
+
* emitted so operators can detect the bypass. This honours the config knob
|
|
254
|
+
* documented in `.claude/rules/parallel-sessions.md` PSA-005 without
|
|
255
|
+
* removing the lock infrastructure.
|
|
256
|
+
*
|
|
257
|
+
* Per-call override via `opts._stateMdLockEnabled` (boolean): when provided,
|
|
258
|
+
* takes precedence over the config value. Useful for tests that need to
|
|
259
|
+
* exercise the short-circuit without touching CLAUDE.md on disk.
|
|
260
|
+
* The leading underscore marks this as a test-only seam — production callers
|
|
261
|
+
* MUST omit this option.
|
|
262
|
+
*
|
|
263
|
+
* Fail-safe: if CLAUDE.md cannot be read or the config block is malformed,
|
|
264
|
+
* `enabled` defaults to `true` — lock is always acquired on errors.
|
|
265
|
+
*
|
|
266
|
+
* Throws when:
|
|
267
|
+
* - acquireStateLock fails (timeout or fs-error) → throws a labelled Error
|
|
268
|
+
* so callers see the failure as an exception rather than a silent
|
|
269
|
+
* {ok:false} return. This is the contract that lets call sites use plain
|
|
270
|
+
* `await withStateMdLock(repoRoot, async () => …)` without branching.
|
|
271
|
+
* - `fn` throws → the original error is re-thrown after release.
|
|
272
|
+
*
|
|
273
|
+
* @param {string|undefined} repoRoot
|
|
274
|
+
* @param {() => (T | Promise<T>)} fn
|
|
275
|
+
* @param {object} [opts]
|
|
276
|
+
* @param {number} [opts.timeoutMs]
|
|
277
|
+
* @param {string} [opts.holder]
|
|
278
|
+
* @param {number} [opts.pollMs]
|
|
279
|
+
* @param {boolean} [opts._stateMdLockEnabled] — test-only per-call override;
|
|
280
|
+
* takes precedence over the Session Config value when set. Production
|
|
281
|
+
* callers MUST omit this option.
|
|
282
|
+
* @returns {Promise<T>}
|
|
283
|
+
* @template T
|
|
284
|
+
*/
|
|
285
|
+
export async function withStateMdLock(repoRoot, fn, opts = {}) {
|
|
286
|
+
if (typeof fn !== 'function') {
|
|
287
|
+
throw new TypeError('withStateMdLock: fn must be a function');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Short-circuit: respect state-md-lock.enabled: false from Session Config.
|
|
291
|
+
// opts._stateMdLockEnabled (test-only per-call override) takes precedence when set.
|
|
292
|
+
let enabled = opts._stateMdLockEnabled;
|
|
293
|
+
if (enabled === undefined) {
|
|
294
|
+
try {
|
|
295
|
+
const claudeMdPath = path.join(repoRoot ?? process.cwd(), 'CLAUDE.md');
|
|
296
|
+
const claudeMdContents = fs.readFileSync(claudeMdPath, 'utf8');
|
|
297
|
+
const cfg = _parseStateMdLock(claudeMdContents);
|
|
298
|
+
enabled = cfg.enabled;
|
|
299
|
+
} catch {
|
|
300
|
+
// Fail-safe: if CLAUDE.md is absent or unreadable, default to locked.
|
|
301
|
+
enabled = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (enabled === false) {
|
|
306
|
+
process.stderr.write('⚠ withStateMdLock: short-circuit (state-md-lock.enabled: false) — running fn without lock\n');
|
|
307
|
+
return await fn();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const holder = typeof opts.holder === 'string' && opts.holder.length > 0
|
|
311
|
+
? opts.holder
|
|
312
|
+
: `pid-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
313
|
+
|
|
314
|
+
const acquireResult = await acquireStateLock({
|
|
315
|
+
repoRoot,
|
|
316
|
+
timeoutMs: opts.timeoutMs,
|
|
317
|
+
holder,
|
|
318
|
+
pollMs: opts.pollMs,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (!acquireResult.ok) {
|
|
322
|
+
const reason = acquireResult.reason;
|
|
323
|
+
const extra = reason === 'timeout' && acquireResult.existingLock
|
|
324
|
+
? ` (held by ${acquireResult.existingLock.holder}, pid=${acquireResult.existingLock.pid})`
|
|
325
|
+
: reason === 'fs-error' && acquireResult.error
|
|
326
|
+
? `: ${acquireResult.error}`
|
|
327
|
+
: '';
|
|
328
|
+
const err = new Error(`withStateMdLock: acquire failed (${reason})${extra}`);
|
|
329
|
+
err.code = `STATE_LOCK_${reason.toUpperCase().replace(/-/g, '_')}`;
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let result;
|
|
334
|
+
let caughtError = null;
|
|
335
|
+
try {
|
|
336
|
+
result = await fn();
|
|
337
|
+
} catch (err) {
|
|
338
|
+
caughtError = err;
|
|
339
|
+
} finally {
|
|
340
|
+
// Always release — even on fn() throw — so the lock does not leak.
|
|
341
|
+
// Only WARN on fs-error: 'not-found' and 'not-owner' are recoverable race
|
|
342
|
+
// conditions (someone else cleaned up our lock — already safe to proceed).
|
|
343
|
+
const releaseResult = releaseStateLock({ repoRoot, holder });
|
|
344
|
+
if (!releaseResult.ok && releaseResult.reason === 'fs-error') {
|
|
345
|
+
console.warn(`withStateMdLock: release failed (fs-error: ${releaseResult.error ?? 'unknown'})`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (caughtError !== null) throw caughtError;
|
|
350
|
+
return result;
|
|
351
|
+
}
|