session-orchestrator 3.22.0 → 3.24.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-lock.mjs — cross-repo mutex for the vault live-status board (issue #1180).
|
|
3
|
+
*
|
|
4
|
+
* The board at `<vault-dir>/01-projects/_active-sessions.md` is ONE file shared
|
|
5
|
+
* by every repo on the host: `sweepBoard()` runs at every session-start and
|
|
6
|
+
* session-end of EVERY repo, and each run performs a read-modify-write
|
|
7
|
+
* (`mirrorBoardInner` reads the existing board to seed `preservedRows` /
|
|
8
|
+
* `priorStatusByRepo`, merges the freshly-derived rows over it, then writes).
|
|
9
|
+
* `atomicWriteWithBackup`'s tmp+rename protects READERS from a half-written
|
|
10
|
+
* file; it does NOT protect the merge, so two sessions that read the same base
|
|
11
|
+
* concurrently both write a board missing the other's row — last writer wins.
|
|
12
|
+
*
|
|
13
|
+
* Why not `withStateMdLock`: that lock lives at `<repoRoot>/.orchestrator/state.lock`
|
|
14
|
+
* and is PER-REPO. Two different repos racing on the one shared board never
|
|
15
|
+
* contend on it — wrong domain.
|
|
16
|
+
*
|
|
17
|
+
* Why `staleCheck: 'mtime'` and not `'pid'`: the vault directory can be shared
|
|
18
|
+
* across hosts (Obsidian sync / a network volume), so the recorded pid is not
|
|
19
|
+
* probeable here. `mtime` ages the lock out after `staleMs` regardless of who
|
|
20
|
+
* wrote it. (`tryAcquireFileLock` never auto-overrides a CROSS-HOST body — see
|
|
21
|
+
* `file-lock.mjs` `isExistingStale` — so a foreign-host lock is waited out and
|
|
22
|
+
* then handled by the fail-open path below, never stolen.)
|
|
23
|
+
*
|
|
24
|
+
* FAIL-OPEN by contract: a board update is best-effort telemetry and must never
|
|
25
|
+
* abort a session phase (same posture as `writeBoard`'s `skipped-write-failed`
|
|
26
|
+
* return). On acquire timeout or fs-error we emit exactly ONE stderr WARN and
|
|
27
|
+
* run `fn` unlocked.
|
|
28
|
+
*
|
|
29
|
+
* Return shape follows {@link import('../locks/state-md-lock.mjs').withStateMdLock}:
|
|
30
|
+
* the value of `fn` is returned verbatim, so call sites need no branching. The
|
|
31
|
+
* lock outcome — which is diagnostic, not part of the board result — is exposed
|
|
32
|
+
* through the optional `onLockOutcome` callback instead of widening the return
|
|
33
|
+
* type for every caller. That is the simpler of the two shapes the issue offered:
|
|
34
|
+
* `mirrorBoardInner` already returns a `{ result, rows }` envelope of its own and
|
|
35
|
+
* would have had to unwrap a second one on every path.
|
|
36
|
+
*
|
|
37
|
+
* No external deps — Node stdlib + `file-lock.mjs`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import crypto from 'node:crypto';
|
|
42
|
+
|
|
43
|
+
import { withFileLock } from '../file-lock.mjs';
|
|
44
|
+
import { expandTilde } from '../common.mjs';
|
|
45
|
+
|
|
46
|
+
/** Default acquire budget: short — the critical section is a read + a rename. */
|
|
47
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
48
|
+
/** Default poll cadence while another writer holds the board. */
|
|
49
|
+
const DEFAULT_POLL_MS = 50;
|
|
50
|
+
/**
|
|
51
|
+
* Default mtime staleness TTL — a board write that took a minute is dead.
|
|
52
|
+
*
|
|
53
|
+
* CEILING (BV-004): 60 s bounds the WHOLE critical section, and that section is
|
|
54
|
+
* not just the rename — `mirrorBoardInner` runs `collectRows` over every repo
|
|
55
|
+
* registered on this host. A sweep that ever exceeds 60 s makes a LIVE writer
|
|
56
|
+
* look stale, and the second writer overrides its lock and re-opens the exact
|
|
57
|
+
* lost-update race this mutex exists to close. It is a constant rather than a
|
|
58
|
+
* measurement because the sweep is fast today and a self-tuning TTL would be a
|
|
59
|
+
* second thing to be wrong.
|
|
60
|
+
* REVISIT when a board sweep is measured above 30 s (half the TTL — the point
|
|
61
|
+
* at which a slow host crosses it), or when an `onLockOutcome` carrying
|
|
62
|
+
* `staleOverride` is observed in the events ledger on a host that had no crash.
|
|
63
|
+
*/
|
|
64
|
+
const DEFAULT_STALE_MS = 60_000;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the board lock path for a vault directory.
|
|
68
|
+
*
|
|
69
|
+
* `<vaultDir>/.orchestrator/board.lock` — deliberately NOT beside the board in
|
|
70
|
+
* `01-projects/`: the vault's own `.gitignore` already ignores
|
|
71
|
+
* `.orchestrator/*.lock`, and `vault-sync` walks `.md` files only, so the lock
|
|
72
|
+
* is invisible to both the vault's VCS and its validator.
|
|
73
|
+
*
|
|
74
|
+
* Home-expansion is `expandTilde` from `../common.mjs` — the consolidation the
|
|
75
|
+
* comment above once deferred (issue #1182): 8 inline `expandHome` copies in
|
|
76
|
+
* 3 non-equivalent shapes, one of them (`gitlab-portfolio/cli.mjs`) actively
|
|
77
|
+
* wrong on `~user/x`. All 8 call sites now import the shared helper.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} vaultDir — absolute or `~`-prefixed vault root.
|
|
80
|
+
* @returns {string} absolute lock path.
|
|
81
|
+
*/
|
|
82
|
+
export function boardLockPathFor(vaultDir) {
|
|
83
|
+
return path.join(expandTilde(vaultDir), '.orchestrator', 'board.lock');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run `fn` while holding the board mutex for `vaultDir`.
|
|
88
|
+
*
|
|
89
|
+
* Acquire polls until `timeoutMs`; the containing `.orchestrator/` directory is
|
|
90
|
+
* created on demand (`tryAcquireFileLock` → `createExclusive` does a
|
|
91
|
+
* `mkdirSync(dir, { recursive: true })` before linking). The lock is always
|
|
92
|
+
* released in a `finally`, including when `fn` throws — a throw propagates
|
|
93
|
+
* unchanged and is NEVER converted into the fail-open path (an unlocked retry
|
|
94
|
+
* of a throwing merge would run it twice).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} vaultDir
|
|
97
|
+
* @param {() => (T | Promise<T>)} fn
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {number} [opts.timeoutMs=5000]
|
|
100
|
+
* @param {number} [opts.pollMs=50]
|
|
101
|
+
* @param {number} [opts.staleMs=60000] — mtime age after which a lock is overridden.
|
|
102
|
+
* @param {string} [opts.holder] — holder label recorded in the lock body.
|
|
103
|
+
* @param {(outcome: { locked: boolean, lockPath: string, reason?: string, staleOverride?: string }) => void} [opts.onLockOutcome]
|
|
104
|
+
* — diagnostic sink, called exactly once before `fn` runs. `staleOverride`
|
|
105
|
+
* is present only when this acquire OVERRODE an aged lock, and carries
|
|
106
|
+
* `file-lock.mjs`'s own reason token — the observable behind the
|
|
107
|
+
* DEFAULT_STALE_MS revisit trigger.
|
|
108
|
+
* @param {(lockPath: string, fn: Function, opts: object) => Promise<object>} [opts.lockImpl]
|
|
109
|
+
* — test seam; defaults to {@link withFileLock}. Must honour the same
|
|
110
|
+
* `{ ok: true, value } | { ok: false, reason }` contract.
|
|
111
|
+
* @param {(msg: string) => void} [opts.warn] — WARN sink (default: stderr).
|
|
112
|
+
* @returns {Promise<T>} whatever `fn` returned.
|
|
113
|
+
* @template T
|
|
114
|
+
*/
|
|
115
|
+
export async function withBoardLock(vaultDir, fn, opts = {}) {
|
|
116
|
+
if (typeof fn !== 'function') {
|
|
117
|
+
throw new TypeError('withBoardLock: fn must be a function');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const {
|
|
121
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
122
|
+
pollMs = DEFAULT_POLL_MS,
|
|
123
|
+
staleMs = DEFAULT_STALE_MS,
|
|
124
|
+
holder: holderOpt,
|
|
125
|
+
onLockOutcome,
|
|
126
|
+
lockImpl = withFileLock,
|
|
127
|
+
warn = (msg) => process.stderr.write(msg),
|
|
128
|
+
} = opts;
|
|
129
|
+
|
|
130
|
+
const lockPath = boardLockPathFor(vaultDir);
|
|
131
|
+
const holder = typeof holderOpt === 'string' && holderOpt.length > 0
|
|
132
|
+
? holderOpt
|
|
133
|
+
: `board-writer-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
134
|
+
|
|
135
|
+
let outcomeReported = false;
|
|
136
|
+
// A stale-override is the one event that can silently break the mutex's
|
|
137
|
+
// guarantee (see DEFAULT_STALE_MS § CEILING), and `withFileLock` announces it
|
|
138
|
+
// ONLY through its `warn` sink — where it is prose nobody can aggregate. Lift
|
|
139
|
+
// it onto the diagnostic outcome so the revisit trigger is observable rather
|
|
140
|
+
// than anecdotal. It rides the SAME single `onLockOutcome` call (the override
|
|
141
|
+
// happens during acquire, i.e. strictly before `fn`), which keeps the
|
|
142
|
+
// documented "called exactly once" contract intact.
|
|
143
|
+
let staleOverride = null;
|
|
144
|
+
const warnAndWatch = (msg) => {
|
|
145
|
+
const hit = /overriding stale lock \(([^)]*)\)/.exec(msg);
|
|
146
|
+
if (hit) staleOverride = hit[1];
|
|
147
|
+
warn(`${msg}\n`);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await lockImpl(
|
|
151
|
+
lockPath,
|
|
152
|
+
async () => {
|
|
153
|
+
outcomeReported = true;
|
|
154
|
+
onLockOutcome?.({
|
|
155
|
+
locked: true,
|
|
156
|
+
lockPath,
|
|
157
|
+
...(staleOverride ? { staleOverride } : {}),
|
|
158
|
+
});
|
|
159
|
+
return await fn();
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
timeoutMs,
|
|
163
|
+
pollMs,
|
|
164
|
+
staleCheck: 'mtime',
|
|
165
|
+
staleMs,
|
|
166
|
+
holder,
|
|
167
|
+
indent: 2,
|
|
168
|
+
tmpPrefix: '.board.lock',
|
|
169
|
+
warn: warnAndWatch,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (result?.ok) return result.value;
|
|
174
|
+
|
|
175
|
+
// Fail-open: never let a contended or broken lock abort a session phase.
|
|
176
|
+
// `outcomeReported` guards the (impossible-by-contract, but cheap to pin)
|
|
177
|
+
// case of an impl that both ran `fn` and reported failure.
|
|
178
|
+
if (!outcomeReported) {
|
|
179
|
+
const reason = result?.reason ?? 'unknown';
|
|
180
|
+
warn(`⚠ withBoardLock: ${reason} acquiring ${lockPath} — writing board WITHOUT the lock (best-effort)\n`);
|
|
181
|
+
onLockOutcome?.({ locked: false, lockPath, reason });
|
|
182
|
+
return await fn();
|
|
183
|
+
}
|
|
184
|
+
return result?.value;
|
|
185
|
+
}
|