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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-transition.mjs — the PROCESS-BOUNDARY teardown of a source repo root.
|
|
3
|
+
*
|
|
4
|
+
* Issue #1069 ("EnterWorktree/CwdChanged als eindeutigen Ein-Root-Übergang
|
|
5
|
+
* modellieren"), operator decision 2026-08-28: **Prozessgrenze**, not live
|
|
6
|
+
* migration. The source session ENDS regularly in the old root; a NEW session
|
|
7
|
+
* with its own identity starts in the destination worktree. Nothing is
|
|
8
|
+
* transferred — no lock hand-off, no registry rewrite, no proof migration.
|
|
9
|
+
*
|
|
10
|
+
* The gap this closes: session-start Phase 0.5 Worktree-Promotion calls
|
|
11
|
+
* `enterWorktree()` and then "exits Phase 0 immediately". It released nothing
|
|
12
|
+
* on the OLD root (measured 2026-08-28: zero `deregisterSelf` / `release(`
|
|
13
|
+
* call sites across `skills/session-start/SKILL.md` and
|
|
14
|
+
* `skills/_shared/parallel-aware-auq.md`). The abandoned registry entry keeps
|
|
15
|
+
* reading FRESH to `detectPeers()` for `freshnessMin=15` minutes and is only
|
|
16
|
+
* removed by `sweepZombies()` after `thresholdMin=60` — so the promoted-away
|
|
17
|
+
* root advertises a PHANTOM peer for up to an hour, and the abandoned
|
|
18
|
+
* `session.lock` blocks/skews every exclusivity decision in that root until
|
|
19
|
+
* its TTL lapses.
|
|
20
|
+
*
|
|
21
|
+
* Ownership rules are NOT re-derived here. The lock half is the same shape
|
|
22
|
+
* `hooks/on-session-end.mjs` uses: `readLockDetailed()` to distinguish an
|
|
23
|
+
* absent lock from an unparseable one, an exact raw `lock.session_id ===
|
|
24
|
+
* sessionId` compare BEFORE any proof is loaded, then `loadOwnerProof()`
|
|
25
|
+
* handed to `release()` as the second identity factor (#987/#989).
|
|
26
|
+
*
|
|
27
|
+
* ## Deliberate deltas vs `hooks/on-session-end.mjs` (its release block)
|
|
28
|
+
*
|
|
29
|
+
* The two teardowns share the ownership gate and now the
|
|
30
|
+
* `orchestrator.session.lock.released` breadcrumb, and diverge in four places
|
|
31
|
+
* ON PURPOSE. Each divergence is a narrower authority, never an oversight:
|
|
32
|
+
*
|
|
33
|
+
* 1. **No reconciliation on a raw-ID mismatch.** SessionEnd calls
|
|
34
|
+
* `attemptLockReconciliation()`, which may reap a provably-dead foreign
|
|
35
|
+
* lease. A worktree promotion is not a lifecycle end for the foreign
|
|
36
|
+
* session and carries no mandate over its lease: this function returns
|
|
37
|
+
* `lock-session-mismatch:<owner>` and touches nothing.
|
|
38
|
+
* 2. **No owner-proof unlink after a successful release.** SessionEnd removes
|
|
39
|
+
* the genesis proof as hygiene because the session is ending. Here the
|
|
40
|
+
* session continues in the DESTINATION root; the source proof is
|
|
41
|
+
* self-invalidating (its millisecond `started_at` can never match a future
|
|
42
|
+
* lock) and deleting it would be a write outside the teardown's remit.
|
|
43
|
+
* 3. **No `orchestrator.session.lock.read_anomaly` breadcrumb.** An
|
|
44
|
+
* unparseable lock aborts this teardown outright and is reported in the
|
|
45
|
+
* RETURN value (`ok: false`, `reason: 'lock-unreadable'|'lock-corrupt'`),
|
|
46
|
+
* which the caller must surface as a WARN. SessionEnd has no return channel
|
|
47
|
+
* to its caller, so there the event is the only place the anomaly can land.
|
|
48
|
+
* 4. **No `orchestrator.session.lock.release_failed` breadcrumb.** Same
|
|
49
|
+
* reason as (3): a failed release surfaces as `ok: false` plus
|
|
50
|
+
* `reason: 'lock-<release-reason>'` to a caller that is required to WARN on
|
|
51
|
+
* it, so the stream is not the only witness.
|
|
52
|
+
*
|
|
53
|
+
* @module scripts/lib/session-transition
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import path from 'node:path';
|
|
57
|
+
|
|
58
|
+
import { deregisterSelf, readRegistry, repoPathHash } from './session-registry.mjs';
|
|
59
|
+
import { readLockDetailed, loadOwnerProof, release } from './session-lock.mjs';
|
|
60
|
+
import { emitEvent } from './events.mjs';
|
|
61
|
+
|
|
62
|
+
/** Event name emitted once per completed source-root departure. */
|
|
63
|
+
export const ROOT_LEFT_EVENT = 'orchestrator.session.root_left';
|
|
64
|
+
|
|
65
|
+
/** Shared lock-release breadcrumb — same event the other two `release()` call sites emit. */
|
|
66
|
+
export const LOCK_RELEASED_EVENT = 'orchestrator.session.lock.released';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Release this session's claim on `repoRoot` because the session is leaving it
|
|
70
|
+
* for good. Ordering is load-bearing and fixed:
|
|
71
|
+
*
|
|
72
|
+
* 1. `readLockDetailed()` — OWNERSHIP IS ESTABLISHED BEFORE ANYTHING IS
|
|
73
|
+
* REMOVED. A lock this session does not own, or one that cannot be parsed,
|
|
74
|
+
* aborts the teardown with `ok: false` and zero side effects. (This used to
|
|
75
|
+
* run second, after the registry entry had already been deleted: a caller
|
|
76
|
+
* passing the wrong `sessionId` destroyed a live registry entry and was
|
|
77
|
+
* told `ok: true` — the registry half of the very phantom-peer state the
|
|
78
|
+
* function exists to prevent.)
|
|
79
|
+
* 2. `deregisterSelf(sessionId)` — the host-wide registry entry. The registry
|
|
80
|
+
* is one file per harness session id, so this removes the session's ONLY
|
|
81
|
+
* entry; the destination worktree's own SessionStart registers itself
|
|
82
|
+
* afresh under its own identity (that is what "process boundary" means).
|
|
83
|
+
* Gated a second time on the entry's `repo_path_hash`: an entry that
|
|
84
|
+
* describes a DIFFERENT root is not this root's claim, and deleting it
|
|
85
|
+
* would silently unregister a session living somewhere else.
|
|
86
|
+
* 3. `release()` of the old root's `session.lock`, gated exactly as
|
|
87
|
+
* `hooks/on-session-end.mjs` gates it, followed by the shared
|
|
88
|
+
* `orchestrator.session.lock.released` breadcrumb (`caller:
|
|
89
|
+
* 'session-transition'`) so this third `release()` call site is visible on
|
|
90
|
+
* the same stream as the other two.
|
|
91
|
+
* 4. `emitEvent(ROOT_LEFT_EVENT)` into the OLD root's events.jsonl — the
|
|
92
|
+
* breadcrumb that says which root was left and why. Emitted even when a
|
|
93
|
+
* step above found nothing: "nothing to release" is itself a fact worth
|
|
94
|
+
* recording, and a departure that left no trace is exactly the
|
|
95
|
+
* indistinguishable-from-a-crash state #1069 was filed about. NOT emitted
|
|
96
|
+
* on an aborted teardown (foreign lock, unparseable lock, invalid args):
|
|
97
|
+
* the event asserts that a root was LEFT, and on those branches nothing
|
|
98
|
+
* was — writing it would put a fiction in a stream that, unlike the
|
|
99
|
+
* registry, may belong to the session that actually owns this root.
|
|
100
|
+
*
|
|
101
|
+
* The caller must invoke this BEFORE the destination worktree's own Phase 1.2
|
|
102
|
+
* `acquire()` runs — never both roots owning a live lock/registry entry at
|
|
103
|
+
* once, which is the double-live-UUID state the issue's acceptance criteria
|
|
104
|
+
* forbid.
|
|
105
|
+
*
|
|
106
|
+
* NEVER THROWS. Every failure is reported in the returned structure, because
|
|
107
|
+
* the sole caller is a promotion path that has already created the destination
|
|
108
|
+
* worktree: a thrown error there would abort the transition halfway, leaving
|
|
109
|
+
* precisely the two-live-roots state this function exists to prevent.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} args
|
|
112
|
+
* @param {string} args.repoRoot — absolute path of the root being LEFT.
|
|
113
|
+
* @param {string} args.sessionId — the raw (physical) session id that owns the
|
|
114
|
+
* registry entry and the lock in `repoRoot`. NOT the semantic id.
|
|
115
|
+
* @param {string|null} [args.semanticSessionId] — semantic id, when known.
|
|
116
|
+
* Omitted from the event payload when null/absent rather than emitted as
|
|
117
|
+
* `null` (same honest-encoding rule as `sessionAttribution()` in events.mjs).
|
|
118
|
+
* @param {string} [args.reason='unspecified'] — why the root is being left,
|
|
119
|
+
* e.g. `'worktree-promotion'`. Recorded verbatim in the event payload.
|
|
120
|
+
* Defaults to `'unspecified'` rather than guessing `'worktree-promotion'`:
|
|
121
|
+
* a mislabelled reason is worse than an admittedly unknown one.
|
|
122
|
+
* @param {object} [deps] — test seams (same shape as `foreign-dispatch.mjs`).
|
|
123
|
+
* @param {Function} [deps.emitFn] — event-emitter seam; defaults to `emitEvent`.
|
|
124
|
+
* @returns {Promise<{
|
|
125
|
+
* ok: boolean,
|
|
126
|
+
* steps: { deregistered: boolean, released: boolean, emitted: boolean },
|
|
127
|
+
* reason?: string
|
|
128
|
+
* }>}
|
|
129
|
+
* `steps` are literal outcomes: `deregistered` = a registry file was removed,
|
|
130
|
+
* `released` = the lock file was deleted, `emitted` = the `root_left` event
|
|
131
|
+
* was appended.
|
|
132
|
+
*
|
|
133
|
+
* `ok: false` means the departure is NOT provably complete — invalid input, a
|
|
134
|
+
* lock owned by someone else, an unparseable lock, a registry entry pinned to
|
|
135
|
+
* a different root, a registry unlink that failed, a contradictory owner
|
|
136
|
+
* proof, or a filesystem error. Callers WARN on it and continue; see the
|
|
137
|
+
* `enterWorktree: leaveSourceRoot: <reason>` contract in
|
|
138
|
+
* `skills/_shared/parallel-aware-auq.md`.
|
|
139
|
+
*
|
|
140
|
+
* `reason` is present whenever something other than a full clean teardown
|
|
141
|
+
* happened; `'already-gone'` is the idempotent-rerun case (nothing left to
|
|
142
|
+
* remove), which is `ok: true`.
|
|
143
|
+
*/
|
|
144
|
+
export async function leaveSourceRoot({
|
|
145
|
+
repoRoot,
|
|
146
|
+
sessionId,
|
|
147
|
+
semanticSessionId = null,
|
|
148
|
+
reason = 'unspecified',
|
|
149
|
+
} = {}, deps = {}) {
|
|
150
|
+
const emit = deps.emitFn ?? emitEvent;
|
|
151
|
+
const steps = { deregistered: false, released: false, emitted: false };
|
|
152
|
+
|
|
153
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0
|
|
154
|
+
|| typeof sessionId !== 'string' || sessionId.length === 0) {
|
|
155
|
+
// Nothing is attempted on unusable input — in particular no event, since
|
|
156
|
+
// an event without a resolvable root has nowhere honest to be written.
|
|
157
|
+
return { ok: false, steps, reason: 'invalid-args' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -- Step 1: ownership, BEFORE any removal --------------------------------
|
|
161
|
+
// Mirrors hooks/on-session-end.mjs's release block: discriminate the read
|
|
162
|
+
// first, compare the RAW session_id before touching the proof, and only then
|
|
163
|
+
// hand the proof to release() as a second factor. An 'absent' lock is the
|
|
164
|
+
// idempotent-rerun / never-acquired case and does NOT block the teardown.
|
|
165
|
+
const lockDetail = readLockDetailed({ repoRoot });
|
|
166
|
+
|
|
167
|
+
if (lockDetail.status === 'unreadable' || lockDetail.status === 'corrupt') {
|
|
168
|
+
// Fail-closed and VISIBLE (issue #1069 AC: "fehlende oder widersprüchliche
|
|
169
|
+
// Proofs degradieren sichtbar und fail-closed"). A lock we cannot parse is
|
|
170
|
+
// neither released nor proven foreign — ownership stays indeterminate, so
|
|
171
|
+
// nothing is removed and nothing is claimed.
|
|
172
|
+
return { ok: false, steps, reason: `lock-${lockDetail.status}` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (lockDetail.status === 'ok' && lockDetail.lock.session_id !== sessionId) {
|
|
176
|
+
// A different session owns this root's lock, so this root is not ours to
|
|
177
|
+
// tear down — not the lock, and not the registry entry either. Aborting
|
|
178
|
+
// with `ok: false` (rather than the pre-#1069-review `ok: true`) is the
|
|
179
|
+
// point: a caller that passed the wrong id has NOT departed anything, and
|
|
180
|
+
// reporting success let that mistake through silently.
|
|
181
|
+
return { ok: false, steps, reason: `lock-session-mismatch:${lockDetail.lock.session_id}` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let ok = true;
|
|
185
|
+
let outcome = null;
|
|
186
|
+
|
|
187
|
+
// -- Step 2: registry (host-wide) -----------------------------------------
|
|
188
|
+
// Second ownership factor, on the registry's own terms: the entry records the
|
|
189
|
+
// root it belongs to as a hash. When it names a DIFFERENT root, the entry is
|
|
190
|
+
// some other checkout's live claim and unlinking it would unregister a
|
|
191
|
+
// session that is still running there. Legacy v1 entries carry no hash — the
|
|
192
|
+
// gate is skipped rather than fail-closed, because rejecting them would make
|
|
193
|
+
// this teardown a no-op against exactly the old entries most likely to be
|
|
194
|
+
// stale (same back-compat rule as `_validEntry`'s optional `mode`).
|
|
195
|
+
let registryGateOk = true;
|
|
196
|
+
try {
|
|
197
|
+
const entry = (await readRegistry()).find((e) => e.session_id === sessionId);
|
|
198
|
+
if (entry && typeof entry.repo_path_hash === 'string' && entry.repo_path_hash.length > 0
|
|
199
|
+
&& entry.repo_path_hash !== repoPathHash(repoRoot)) {
|
|
200
|
+
registryGateOk = false;
|
|
201
|
+
ok = false;
|
|
202
|
+
outcome ??= 'registry-root-mismatch';
|
|
203
|
+
}
|
|
204
|
+
} catch { /* an unreadable registry is handled by deregisterSelf below */ }
|
|
205
|
+
|
|
206
|
+
if (registryGateOk) {
|
|
207
|
+
try {
|
|
208
|
+
steps.deregistered = await deregisterSelf(sessionId);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
ok = false;
|
|
211
|
+
outcome ??= `deregister-failed:${err?.message ?? 'unknown'}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// -- Step 3: lock (this root only) ----------------------------------------
|
|
216
|
+
if (lockDetail.status === 'ok') {
|
|
217
|
+
const proof = loadOwnerProof({ repoRoot });
|
|
218
|
+
// `proof` is null whenever ownership cannot be proven; release() treats
|
|
219
|
+
// null as absent (#989) and degrades to the session_id-only path, so it
|
|
220
|
+
// is passed through unguarded — the spread-guard call sites needed
|
|
221
|
+
// before that fix are exactly what the API contract removed.
|
|
222
|
+
const result = release({ sessionId, repoRoot, proof });
|
|
223
|
+
steps.released = result.ok === true && result.deleted === true;
|
|
224
|
+
// 'no-lock' = the lock vanished between the read above and the release —
|
|
225
|
+
// benign (idempotent rerun / concurrent SessionEnd), and forensically the
|
|
226
|
+
// interesting half: someone else removed a lock we still held.
|
|
227
|
+
const alreadyGone = result.ok === true && result.reason === 'no-lock';
|
|
228
|
+
if (!steps.released) {
|
|
229
|
+
if (!alreadyGone) ok = false;
|
|
230
|
+
outcome ??= `lock-${result.reason ?? 'not-deleted'}`;
|
|
231
|
+
}
|
|
232
|
+
if (steps.released || alreadyGone) {
|
|
233
|
+
// ARCH-MED-1 — this is the THIRD `release()` call site in the repo and
|
|
234
|
+
// was the only one silent on `orchestrator.session.lock.*`. Emitted at
|
|
235
|
+
// the call site, never inside release(): session-lock.mjs carries no
|
|
236
|
+
// dependency on events.mjs, and release() is synchronous.
|
|
237
|
+
try {
|
|
238
|
+
await emit(LOCK_RELEASED_EVENT, {
|
|
239
|
+
session_id: sessionId,
|
|
240
|
+
...(semanticSessionId ? { semantic_session_id: semanticSessionId } : {}),
|
|
241
|
+
caller: 'session-transition',
|
|
242
|
+
outcome: steps.released ? 'deleted' : 'already-gone',
|
|
243
|
+
verified: result.verified === true,
|
|
244
|
+
}, { repoRoot });
|
|
245
|
+
} catch { /* observability is best-effort — the return value still tells */ }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (outcome === null && !steps.deregistered && !steps.released) {
|
|
250
|
+
outcome = 'already-gone';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// -- Step 4: breadcrumb ----------------------------------------------------
|
|
254
|
+
try {
|
|
255
|
+
await emit(ROOT_LEFT_EVENT, {
|
|
256
|
+
session_id: sessionId,
|
|
257
|
+
...(semanticSessionId ? { semantic_session_id: semanticSessionId } : {}),
|
|
258
|
+
// NEVER the absolute path. emitEvent forwards the whole payload to the
|
|
259
|
+
// optional Clank webhook with no redaction, and an absolute repo root on
|
|
260
|
+
// this host is `/Users/<operator>/…`. The hash is the SAME function the
|
|
261
|
+
// session registry keys its entries by (`repo_path_hash`), so a consumer
|
|
262
|
+
// can still join a departure to the registry entry it removed; the
|
|
263
|
+
// basename matches the registry's `repo_name`. Same rule as the
|
|
264
|
+
// `board_written` / `mirror_completed` payloads and
|
|
265
|
+
// `relativeWorktreePath` in worktree-pipeline.mjs.
|
|
266
|
+
from_root_hash: repoPathHash(repoRoot),
|
|
267
|
+
from_root_basename: path.basename(path.resolve(repoRoot)),
|
|
268
|
+
reason,
|
|
269
|
+
}, { repoRoot });
|
|
270
|
+
steps.emitted = true;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
ok = false;
|
|
273
|
+
outcome ??= `emit-failed:${err?.message ?? 'unknown'}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return outcome === null ? { ok, steps } : { ok, steps, reason: outcome };
|
|
277
|
+
}
|