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
|
@@ -28,6 +28,7 @@ import path from 'node:path';
|
|
|
28
28
|
import crypto from 'node:crypto';
|
|
29
29
|
|
|
30
30
|
import { tryAcquireFileLock } from '../file-lock.mjs';
|
|
31
|
+
import { hostnamesMatch, lockHostCandidate, stableHostname } from '../host-identity.mjs';
|
|
31
32
|
import { nowIso, delay, parseLockBody } from './lock-body.mjs';
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
@@ -67,7 +68,9 @@ function stagingFenceLockPathFor(repoRoot) {
|
|
|
67
68
|
function buildStagingFenceLockBody({ holder }) {
|
|
68
69
|
return {
|
|
69
70
|
pid: process.pid,
|
|
71
|
+
// `host` raw + `host_id` normalised (#1072) — see buildStateLockBody.
|
|
70
72
|
host: os.hostname(),
|
|
73
|
+
host_id: stableHostname(),
|
|
71
74
|
acquiredAt: nowIso(),
|
|
72
75
|
holder: typeof holder === 'string' && holder.length > 0 ? holder : `pid-${process.pid}`,
|
|
73
76
|
};
|
|
@@ -178,7 +181,8 @@ export function releaseStagingFenceLock({ repoRoot, holder } = {}) {
|
|
|
178
181
|
|
|
179
182
|
const ownerMatch = typeof holder === 'string' && holder.length > 0
|
|
180
183
|
? lock.holder === holder
|
|
181
|
-
:
|
|
184
|
+
// #1072: alias-aware host identity, not a raw os.hostname() comparison.
|
|
185
|
+
: lock.pid === process.pid && hostnamesMatch(lockHostCandidate(lock), os.hostname());
|
|
182
186
|
|
|
183
187
|
if (!ownerMatch) {
|
|
184
188
|
return { ok: false, reason: 'not-owner' };
|
|
@@ -41,6 +41,7 @@ import crypto from 'node:crypto';
|
|
|
41
41
|
|
|
42
42
|
import { _parseStateMdLock } from '../config/state-md-lock.mjs';
|
|
43
43
|
import { tryAcquireFileLock } from '../file-lock.mjs';
|
|
44
|
+
import { hostnamesMatch, lockHostCandidate, stableHostname } from '../host-identity.mjs';
|
|
44
45
|
import { nowIso, delay, parseLockBody } from './lock-body.mjs';
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
@@ -78,7 +79,11 @@ function stateLockPathFor(repoRoot) {
|
|
|
78
79
|
function buildStateLockBody({ holder }) {
|
|
79
80
|
return {
|
|
80
81
|
pid: process.pid,
|
|
82
|
+
// `host` raw + `host_id` normalised (#1072). The written body comes from
|
|
83
|
+
// tryAcquireFileLock, which carries the same pair — this keeps the shape
|
|
84
|
+
// declared here truthful about what lands on disk.
|
|
81
85
|
host: os.hostname(),
|
|
86
|
+
host_id: stableHostname(),
|
|
82
87
|
acquiredAt: nowIso(),
|
|
83
88
|
holder: typeof holder === 'string' && holder.length > 0 ? holder : `pid-${process.pid}`,
|
|
84
89
|
};
|
|
@@ -224,7 +229,9 @@ export function releaseStateLock({ repoRoot, sessionId, holder } = {}) {
|
|
|
224
229
|
const expectedHolder = holder ?? sessionId ?? null;
|
|
225
230
|
const ownerMatch = expectedHolder !== null
|
|
226
231
|
? lock.holder === expectedHolder
|
|
227
|
-
:
|
|
232
|
+
// #1072: alias-aware host identity — a raw comparison strands the lock as
|
|
233
|
+
// 'not-owner' once os.hostname() flips spelling mid-session.
|
|
234
|
+
: lock.pid === process.pid && hostnamesMatch(lockHostCandidate(lock), os.hostname());
|
|
228
235
|
|
|
229
236
|
if (!ownerMatch) {
|
|
230
237
|
return { ok: false, reason: 'not-owner' };
|
|
@@ -27,6 +27,7 @@ import { resolveMemoryDir } from './memory-paths.mjs';
|
|
|
27
27
|
import { readDreamSignals } from './auto-dream.mjs';
|
|
28
28
|
import { readPeerCards } from './peer-cards/reader.mjs';
|
|
29
29
|
import { surfaceTopN, decayOptsFromConfig } from './learnings/surface.mjs';
|
|
30
|
+
import { countSessionsInJsonl } from './sessions-canonical.mjs';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Constants
|
|
@@ -259,19 +260,30 @@ export function _formatBanner(inputs) {
|
|
|
259
260
|
// ---------------------------------------------------------------------------
|
|
260
261
|
|
|
261
262
|
/**
|
|
262
|
-
* Count
|
|
263
|
-
* resolve to 0; read errors resolve to 0.
|
|
264
|
-
*
|
|
263
|
+
* Count DISTINCT physical sessions in `sessions.jsonl` — the "sessions ever"
|
|
264
|
+
* stat. Missing files resolve to 0; read errors resolve to 0.
|
|
265
|
+
*
|
|
266
|
+
* Identities, not lines (#1167). `sessions.jsonl` is append-only, so one
|
|
267
|
+
* physical session can occupy two lines (an abandoned stub plus the
|
|
268
|
+
* authoritative record that supersedes it, or the systemic double-stub pair
|
|
269
|
+
* from the two backfill writers). Counting lines inflated this stat by exactly
|
|
270
|
+
* the number of duplicates — 286 lines for 275 sessions in this repo, measured
|
|
271
|
+
* 2026-09-02.
|
|
272
|
+
*
|
|
273
|
+
* The abandoned-session semantic is DELIBERATELY unchanged: an abandoned
|
|
274
|
+
* session is a real session and still counts. Only DUPLICATE records of ONE
|
|
275
|
+
* physical session collapse — see `sessions-canonical.mjs`, which is not a
|
|
276
|
+
* phantom filter.
|
|
265
277
|
*
|
|
266
278
|
* @param {string} filePath
|
|
267
279
|
* @returns {Promise<number>}
|
|
268
280
|
*/
|
|
269
|
-
async function
|
|
281
|
+
async function countSessionIdentities(filePath) {
|
|
270
282
|
if (!existsSync(filePath)) return 0;
|
|
271
283
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return
|
|
284
|
+
// Counting rule (blank/malformed lines, id-less records) lives in
|
|
285
|
+
// `countSessionsInJsonl`; only the async read is local.
|
|
286
|
+
return countSessionsInJsonl(await readFile(filePath, 'utf8'));
|
|
275
287
|
} catch {
|
|
276
288
|
return 0;
|
|
277
289
|
}
|
|
@@ -308,7 +320,7 @@ async function countMemoryFiles(dir) {
|
|
|
308
320
|
*
|
|
309
321
|
* @param {object} args
|
|
310
322
|
* @param {string} args.repoRoot — absolute path
|
|
311
|
-
* @param {string} [args.memoryDir] — defaults to `resolveMemoryDir()`
|
|
323
|
+
* @param {string} [args.memoryDir] — defaults to `resolveMemoryDir(repoRoot)`
|
|
312
324
|
* @param {string} [args.learningsPath] — defaults to `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
|
|
313
325
|
* @param {Date} [args.now] — injectable clock for tests
|
|
314
326
|
* @param {{enabled?: boolean, 'half-life-days'?: number, 'floor-factor'?: number}} [args.evolveDecay]
|
|
@@ -326,9 +338,12 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
|
|
|
326
338
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
327
339
|
throw new TypeError('readBannerInputs: repoRoot is required (absolute path)');
|
|
328
340
|
}
|
|
341
|
+
// #1071: bind the memory dir to the SAME root as learnings/sessions/peer-cards
|
|
342
|
+
// below. Called with no argument it derived from the ambient `process.cwd()`,
|
|
343
|
+
// which reported `0 memory files` beside a 245-session count from `repoRoot`.
|
|
329
344
|
const memDir = typeof memoryDir === 'string' && memoryDir.length > 0
|
|
330
345
|
? memoryDir
|
|
331
|
-
: resolveMemoryDir();
|
|
346
|
+
: resolveMemoryDir(repoRoot);
|
|
332
347
|
const learningsFile = typeof learningsPath === 'string' && learningsPath.length > 0
|
|
333
348
|
? learningsPath
|
|
334
349
|
: path.join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
|
|
@@ -375,7 +390,7 @@ export async function readBannerInputs({ repoRoot, memoryDir, learningsPath, now
|
|
|
375
390
|
// computed the same way, so we re-derive locally to keep this dependency
|
|
376
391
|
// surface lean and explicit.
|
|
377
392
|
const sessionsPath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
378
|
-
const sessionsEver = await
|
|
393
|
+
const sessionsEver = await countSessionIdentities(sessionsPath);
|
|
379
394
|
|
|
380
395
|
let daysSinceCleanup = null;
|
|
381
396
|
if (typeof signals?.lastCleanupAt === 'string' && signals.lastCleanupAt.length > 0) {
|
|
@@ -15,17 +15,26 @@ import os from 'node:os';
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolve the project-specific memory directory used by the Claude Code harness.
|
|
17
17
|
*
|
|
18
|
-
* Mirrors the harness convention: `~/.claude/projects/<encoded-
|
|
19
|
-
* where `<encoded-
|
|
20
|
-
* dot replacement matters for users with a trailing-`.` in their home
|
|
21
|
-
* (e.g. `/Users/<owner>.`) — without it the resolved path diverges from
|
|
18
|
+
* Mirrors the harness convention: `~/.claude/projects/<encoded-root>/memory/`
|
|
19
|
+
* where `<encoded-root>` is the project root with BOTH `/` AND `.` replaced by
|
|
20
|
+
* `-`. The dot replacement matters for users with a trailing-`.` in their home
|
|
21
|
+
* dir (e.g. `/Users/<owner>.`) — without it the resolved path diverges from
|
|
22
22
|
* what the harness actually wrote.
|
|
23
23
|
*
|
|
24
24
|
* Verified empirically against `~/.claude/projects/` directory naming.
|
|
25
25
|
*
|
|
26
|
+
* `repoRoot` is explicit (#1071) because every OTHER banner input is already
|
|
27
|
+
* bound to it: with the memory dir alone derived from the ambient `process.cwd()`,
|
|
28
|
+
* a caller reading sessions/learnings/peer-cards out of the primary checkout
|
|
29
|
+
* reported `0 memory files` whenever the process ran from a worktree or a
|
|
30
|
+
* subdirectory. Defaulted, so existing zero-arg callers keep their behaviour.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} [repoRoot] — absolute project root; defaults to `process.cwd()`.
|
|
26
33
|
* @returns {string} Absolute path to the memory directory (not guaranteed to exist).
|
|
27
34
|
*/
|
|
28
|
-
export function resolveMemoryDir() {
|
|
29
|
-
const
|
|
35
|
+
export function resolveMemoryDir(repoRoot = process.cwd()) {
|
|
36
|
+
const root =
|
|
37
|
+
typeof repoRoot === 'string' && repoRoot.length > 0 ? repoRoot : process.cwd();
|
|
38
|
+
const encoded = root.replaceAll('/', '-').replaceAll('.', '-');
|
|
30
39
|
return path.join(os.homedir(), '.claude', 'projects', encoded, 'memory');
|
|
31
40
|
}
|
|
@@ -82,6 +82,57 @@ export function safeBootstrapLock(signals) {
|
|
|
82
82
|
: null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Read a session record's completion rate, tolerating both the shape
|
|
87
|
+
* `sessions.jsonl` actually writes and the legacy flat one.
|
|
88
|
+
*
|
|
89
|
+
* Production records nest it: `{effectiveness: {completion_rate: 0.77, …}}`.
|
|
90
|
+
* The top-level `completion_rate` key is ABSENT — not present-but-null, which
|
|
91
|
+
* an earlier revision of this comment claimed. Measured 2026-08-28 @ 7daa3d2
|
|
92
|
+
* over `.orchestrator/metrics/sessions.jsonl` (281 records):
|
|
93
|
+
* `jq -s '{hasTopLevelKey: ([.[]|select(has("completion_rate"))]|length), …}'`
|
|
94
|
+
* → `hasTopLevelKey: 0`, `nested: 182`, and 99 records carrying NO completion
|
|
95
|
+
* rate in either shape. Reading only the flat field therefore made every
|
|
96
|
+
* average 0 in production — see #1071.
|
|
97
|
+
*
|
|
98
|
+
* Precedence mirrors `completionOf()` in
|
|
99
|
+
* `scripts/lib/evolve/autopilot-effectiveness.mjs`: nested canonical fields
|
|
100
|
+
* first, flat legacy fields as a backward-compatible fallback for
|
|
101
|
+
* hand-written/pre-nesting records.
|
|
102
|
+
*
|
|
103
|
+
* @param {unknown} session
|
|
104
|
+
* @returns {number} completion rate, or 0 when unknown
|
|
105
|
+
*/
|
|
106
|
+
function completionRateOf(session) {
|
|
107
|
+
if (session === null || typeof session !== 'object') return 0;
|
|
108
|
+
const eff = (session.effectiveness !== null && typeof session.effectiveness === 'object')
|
|
109
|
+
? session.effectiveness
|
|
110
|
+
: {};
|
|
111
|
+
const raw =
|
|
112
|
+
eff.completion_rate ?? eff.completion_ratio ?? session.completion_rate ?? session.completion_ratio;
|
|
113
|
+
return typeof raw === 'number' && !Number.isNaN(raw) ? raw : 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mean completion rate across a session slice. Unknown values contribute 0 and
|
|
118
|
+
* the divisor stays the slice length — an incomplete slice can never inflate
|
|
119
|
+
* the average past the >= 0.9 bonus threshold.
|
|
120
|
+
*
|
|
121
|
+
* The divisor is the load-bearing half, not the numerator: 99 of the 281
|
|
122
|
+
* records in production carry no completion rate in EITHER shape (measured
|
|
123
|
+
* 2026-08-28 @ 7daa3d2), so a divisor of "known values only" would let a single
|
|
124
|
+
* 1.0 session beside two unknowns score a perfect 1.0 and hand out the trend
|
|
125
|
+
* bonus on one data point. Pinned by the divisor test in
|
|
126
|
+
* `tests/lib/mode-selector/scoring.test.mjs`.
|
|
127
|
+
*
|
|
128
|
+
* @param {any[]} sessions
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
function avgCompletionRate(sessions) {
|
|
132
|
+
if (sessions.length === 0) return 0;
|
|
133
|
+
return sessions.reduce((sum, s) => sum + completionRateOf(s), 0) / sessions.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
85
136
|
// ---------------------------------------------------------------------------
|
|
86
137
|
// Public API
|
|
87
138
|
// ---------------------------------------------------------------------------
|
|
@@ -107,9 +158,7 @@ export function computeDelta(candidateMode, signals) {
|
|
|
107
158
|
(s) => typeof s === 'object' && s !== null && s.session_type === candidateMode,
|
|
108
159
|
);
|
|
109
160
|
if (allMatch) {
|
|
110
|
-
const avgCompletion =
|
|
111
|
-
last3.reduce((sum, s) => sum + (typeof s.completion_rate === 'number' ? s.completion_rate : 0), 0) /
|
|
112
|
-
3;
|
|
161
|
+
const avgCompletion = avgCompletionRate(last3);
|
|
113
162
|
trendBonus = avgCompletion >= 0.9 ? 0.15 : 0.075;
|
|
114
163
|
}
|
|
115
164
|
}
|
|
@@ -151,9 +200,7 @@ export function computeDelta(candidateMode, signals) {
|
|
|
151
200
|
(s) => typeof s === 'object' && s !== null && s.session_type === otherMode,
|
|
152
201
|
);
|
|
153
202
|
if (allOther) {
|
|
154
|
-
const avgCompletion =
|
|
155
|
-
last3.reduce((sum, s) => sum + (typeof s.completion_rate === 'number' ? s.completion_rate : 0), 0) /
|
|
156
|
-
3;
|
|
203
|
+
const avgCompletion = avgCompletionRate(last3);
|
|
157
204
|
if (avgCompletion >= 0.9) {
|
|
158
205
|
penalty += 0.10;
|
|
159
206
|
break; // only one other dominant mode possible
|
|
@@ -100,7 +100,16 @@ function _ageHoursFrom(startedAt, nowMs) {
|
|
|
100
100
|
/**
|
|
101
101
|
* Map one discoverActiveSessions entry into a provenance-tagged peer.
|
|
102
102
|
*
|
|
103
|
-
*
|
|
103
|
+
* GH#67 pass-through: `registryOnly` / `lockSuperseded` / `lockOwnerId` are
|
|
104
|
+
* threaded through UNCHANGED when the upstream entry carries them (registry-
|
|
105
|
+
* sourced sessions only). Absent stays absent, so a lock-sourced peer object is
|
|
106
|
+
* byte-identical to the pre-GH#67 shape. `lockSuperseded: true` is a HINT that
|
|
107
|
+
* a LIVE lock at that repoRoot is owned by a different raw session_id — not a
|
|
108
|
+
* verdict that the peer is dead (the lock is advisory, #1085). Consumers
|
|
109
|
+
* deciding a worktree PROMOTION_OFFER downgrade such a peer to an advisory
|
|
110
|
+
* line; consumers counting or displaying peers keep it.
|
|
111
|
+
*
|
|
112
|
+
* @param {{worktreePath:string,sessionId:string,mode:string,startedAt:string,pid:number,host:string,branch:string,registryOnly?:boolean,lockSuperseded?:boolean,lockOwnerId?:string|null}} s
|
|
104
113
|
* @param {number} nowMs
|
|
105
114
|
* @returns {object}
|
|
106
115
|
*/
|
|
@@ -118,6 +127,10 @@ function _peerFromDiscovered(s, nowMs) {
|
|
|
118
127
|
worktreePath: s.worktreePath,
|
|
119
128
|
};
|
|
120
129
|
if (ageHours !== undefined) peer.ageHours = ageHours;
|
|
130
|
+
// GH#67 — additive, absent-stays-absent.
|
|
131
|
+
if (s.registryOnly !== undefined) peer.registryOnly = s.registryOnly;
|
|
132
|
+
if (s.lockSuperseded !== undefined) peer.lockSuperseded = s.lockSuperseded;
|
|
133
|
+
if (s.lockOwnerId !== undefined) peer.lockOwnerId = s.lockOwnerId;
|
|
121
134
|
return peer;
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -189,7 +202,12 @@ function _discoveredSelfSessionId(mySessionId, repoRoot) {
|
|
|
189
202
|
* per-source — only the fields the originating surface can supply are emitted
|
|
190
203
|
* (no field is advertised that the implementation does not set):
|
|
191
204
|
* - source 'discovered' (from discoverActiveSessions — lock + registry unified):
|
|
192
|
-
* { source, sessionId, mode|null, host, pid, worktreePath, ageHours
|
|
205
|
+
* { source, sessionId, mode|null, host, pid, worktreePath, ageHours?,
|
|
206
|
+
* registryOnly?, lockSuperseded?, lockOwnerId? }
|
|
207
|
+
* The last three appear ONLY on registry-sourced entries (GH#67
|
|
208
|
+
* annotation, passed through verbatim — see `_peerFromDiscovered`).
|
|
209
|
+
* `lockSuperseded` is a HINT, never a liveness verdict: no peer is
|
|
210
|
+
* dropped on account of it, because the session lock is advisory.
|
|
193
211
|
* - source 'state-md' (from checkPeerStateMd):
|
|
194
212
|
* { source, sessionId, mode|null, currentWave, reason, ageHours? }
|
|
195
213
|
*/
|
package/scripts/lib/platform.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, statSync } from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { resolvePluginRoot as _resolvePluginRootRobust } from './plugin-root.mjs';
|
|
14
15
|
|
|
@@ -42,9 +43,66 @@ function _hasEnvValue(envName) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Absolute home directory, or null when the host cannot report one.
|
|
47
|
+
* os.homedir() reads $HOME on POSIX and %USERPROFILE% on Windows.
|
|
48
|
+
*
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
function _homeDir() {
|
|
52
|
+
try {
|
|
53
|
+
const home = os.homedir();
|
|
54
|
+
return home ? path.resolve(home) : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* True when `dir` is the home directory itself or one of its ancestors
|
|
62
|
+
* ("/", "/Users", "C:\\", …). Unrelated branches of the tree (e.g. "/opt")
|
|
63
|
+
* are neither — they are walked normally.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} dir
|
|
66
|
+
* @param {string|null} home
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function _isHomeOrAbove(dir, home) {
|
|
70
|
+
if (home === null) return false;
|
|
71
|
+
if (dir === home) return true;
|
|
72
|
+
// The filesystem root already ends in the separator ("/", "C:\\") — appending
|
|
73
|
+
// a second one ("//") would make it match nothing and silently exempt the one
|
|
74
|
+
// directory that is an ancestor of every home.
|
|
75
|
+
const prefix = dir.endsWith(path.sep) ? dir : dir + path.sep;
|
|
76
|
+
return home.startsWith(prefix);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Walk up the directory tree from startDir looking for marker, bounded by the
|
|
81
|
+
* project the walk started in.
|
|
82
|
+
*
|
|
83
|
+
* Two boundaries, both load-bearing (#1139) — a marker outside the project can
|
|
84
|
+
* never describe the project:
|
|
85
|
+
*
|
|
86
|
+
* 1. **Repo root.** The first ancestor holding `.git` is the LAST directory
|
|
87
|
+
* inspected (inclusive — a marker at the repo root is still found). `.git`
|
|
88
|
+
* is a directory in a normal clone and a FILE in a worktree or submodule,
|
|
89
|
+
* so existence is checked, not the kind.
|
|
90
|
+
* 2. **Home directory.** os.homedir() and its ancestors are never inspected —
|
|
91
|
+
* even when no repo root was found (cwd outside every checkout). This is
|
|
92
|
+
* what stops a stray `~/.pi` / `~/.cursor/rules` / `~/CLAUDE.md` from being
|
|
93
|
+
* adopted by every markerless directory on the host: before this boundary
|
|
94
|
+
* existed, 63 of 84 telemetry records from Claude Code sessions reported
|
|
95
|
+
* `platform=pi` because the walk reached `$HOME` and found `~/.pi` there.
|
|
96
|
+
*
|
|
97
|
+
* Named ceiling (BV-004): when a checkout's root IS the home directory (a
|
|
98
|
+
* dotfiles repo at `$HOME`), boundary 2 wins over boundary 1 and no marker is
|
|
99
|
+
* detected there — deliberate, because that false positive is silent and
|
|
100
|
+
* host-wide while the false negative degrades to the documented default
|
|
101
|
+
* ('claude' for detectPlatform, cwd for resolveProjectDir). Revisit if a
|
|
102
|
+
* repo-at-$HOME layout ever has to carry orchestrator state.
|
|
103
|
+
*
|
|
104
|
+
* Terminates correctly on Windows ("C:\\") and POSIX ("/") via the
|
|
105
|
+
* parent === dir fixpoint.
|
|
48
106
|
*
|
|
49
107
|
* @param {string} startDir Absolute directory to begin walking from
|
|
50
108
|
* @param {string} marker Relative sub-path to look for inside each candidate dir
|
|
@@ -53,7 +111,7 @@ function _hasEnvValue(envName) {
|
|
|
53
111
|
*/
|
|
54
112
|
function walkUpFor(startDir, marker, kind) {
|
|
55
113
|
let dir = path.resolve(startDir);
|
|
56
|
-
const
|
|
114
|
+
const home = _homeDir();
|
|
57
115
|
|
|
58
116
|
const check = (candidate) => {
|
|
59
117
|
if (!existsSync(candidate)) return false;
|
|
@@ -62,16 +120,15 @@ function walkUpFor(startDir, marker, kind) {
|
|
|
62
120
|
return true; // 'any'
|
|
63
121
|
};
|
|
64
122
|
|
|
65
|
-
|
|
123
|
+
for (;;) {
|
|
124
|
+
if (_isHomeOrAbove(dir, home)) break; // boundary 2 — never inspected
|
|
66
125
|
if (check(path.join(dir, marker))) return dir;
|
|
126
|
+
if (existsSync(path.join(dir, '.git'))) break; // boundary 1 — repo root was the last candidate
|
|
67
127
|
const parent = path.dirname(dir);
|
|
68
|
-
if (parent === dir) break;
|
|
128
|
+
if (parent === dir) break; // filesystem root reached
|
|
69
129
|
dir = parent;
|
|
70
130
|
}
|
|
71
131
|
|
|
72
|
-
// Check root itself
|
|
73
|
-
if (check(path.join(root, marker))) return root;
|
|
74
|
-
|
|
75
132
|
return null;
|
|
76
133
|
}
|
|
77
134
|
|
|
@@ -109,6 +166,12 @@ export function detectPlatform() {
|
|
|
109
166
|
if (walkUpFor(cwd, path.join('.cursor', 'rules'), 'dir')) return 'cursor';
|
|
110
167
|
if (walkUpFor(cwd, '.pi', 'dir')) return 'pi';
|
|
111
168
|
|
|
169
|
+
// Signal-free fallback, deliberate rather than inherited: Claude Code is the
|
|
170
|
+
// only harness that drives this code through `hooks.json` without exporting a
|
|
171
|
+
// platform env var, so it is the harness that actually reaches this line.
|
|
172
|
+
// Codex, Cursor and pi each set their own env var (step 2) or ship a marker
|
|
173
|
+
// directory (step 3), so a wrong answer here costs them nothing they had.
|
|
174
|
+
// No config key for this (BV-001) — SO_PLATFORM already overrides it.
|
|
112
175
|
return 'claude';
|
|
113
176
|
}
|
|
114
177
|
|
|
@@ -5,20 +5,45 @@
|
|
|
5
5
|
* provides a deterministic, testable resolution strategy so hook handlers and
|
|
6
6
|
* scripts never silently fall back to an empty path or wrong directory.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1.
|
|
10
|
-
* 2. Compatibility
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* TIER ORDER — SINGLE SOURCE OF TRUTH (stops at first success):
|
|
9
|
+
* 1. PLUGIN_ROOT native env var
|
|
10
|
+
* 2-5. Compatibility roots: the one matching an explicit SO_PLATFORM first,
|
|
11
|
+
* then the rest in legacy Claude → Codex → Cursor → Pi order
|
|
12
|
+
* 6. Walk up from import.meta.url looking for package.json whose
|
|
13
|
+
* name === "session-orchestrator"
|
|
14
|
+
* 7. Walk up from process.cwd() looking for the same marker
|
|
15
|
+
* 8. Scan the client plugin caches (marketplace install, no env, cwd outside
|
|
16
|
+
* any checkout — GH Kanevry/session-orchestrator#64)
|
|
14
17
|
*
|
|
15
|
-
*
|
|
18
|
+
* `.mcp.json`'s bash bootstrap MIRRORS this order and cites this block. It
|
|
19
|
+
* implements: tiers 1-5 in this exact env order → `git rev-parse
|
|
20
|
+
* --show-toplevel` (its analogue of tier 7) → `node -e resolvePluginRoot()`
|
|
21
|
+
* (which runs THIS function, tiers 1-8) → its own cache scan, reachable only
|
|
22
|
+
* when the module is not node-resolvable at all.
|
|
23
|
+
*
|
|
24
|
+
* THE ONE REMAINING DIVERGENCE, stated exactly: **tier 6** — a shell has no
|
|
25
|
+
* `import.meta.url`, so the walk up from this file's own location has no
|
|
26
|
+
* analogue there. Everything else is mirrored byte-for-byte in behaviour: the
|
|
27
|
+
* env list AND its order, the `CODEX_HOME` trim (see `_pluginCacheBases`), the
|
|
28
|
+
* `package.json{name === "session-orchestrator"}` test (the shell runs the same
|
|
29
|
+
* `JSON.parse` via `node -e`, not a substring `grep` — a `grep` matched
|
|
30
|
+
* `"name": "session-orchestrator-fork"` that this function rejects), and the
|
|
31
|
+
* newest-wins-by-mtime tiebreak (`[ "$c" -nt "$r" ]` is strict, exactly like
|
|
32
|
+
* the `>` below).
|
|
16
33
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
34
|
+
* Two divergences that existed until 2026-08-28 and no longer do: the shell
|
|
35
|
+
* scanned the caches BEFORE the node tier (a host with both a global npm
|
|
36
|
+
* install and a marketplace cache copy had the shell pick the cache while this
|
|
37
|
+
* function picked the npm copy), and its env list read `CLAUDE_PLUGIN_ROOT,
|
|
38
|
+
* CODEX_PLUGIN_ROOT, PLUGIN_ROOT` — a different tier-1 winner, and no
|
|
39
|
+
* `CURSOR_RULES_DIR`/`PI_PLUGIN_ROOT` at all. When either side's order changes,
|
|
40
|
+
* change both — the shell is the mirror, this list is the original.
|
|
41
|
+
*
|
|
42
|
+
* Throws PluginRootResolutionError when all resolution levels fail.
|
|
19
43
|
*/
|
|
20
44
|
|
|
21
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
45
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
46
|
+
import os from 'node:os';
|
|
22
47
|
import path from 'node:path';
|
|
23
48
|
import { fileURLToPath } from 'node:url';
|
|
24
49
|
|
|
@@ -157,6 +182,103 @@ function _walkUp(startDir) {
|
|
|
157
182
|
return null;
|
|
158
183
|
}
|
|
159
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Base directories under which a client keeps its plugin cache.
|
|
187
|
+
*
|
|
188
|
+
* Measured 2026-08-28 on codex-cli 0.141.0 and Claude Code: an installed
|
|
189
|
+
* plugin is COPIED (not symlinked) to
|
|
190
|
+
* `<base>/plugins/cache/<marketplace>/<plugin-name>/<version>/`, e.g.
|
|
191
|
+
* `~/.codex/plugins/cache/local/session-orchestrator/<version>+codex.<stamp>/`
|
|
192
|
+
* and `~/.claude/plugins/cache/session-orchestrator/session-orchestrator/3.13.0/`.
|
|
193
|
+
*
|
|
194
|
+
* Cursor is deliberately absent: no cache layout was measured for it on this
|
|
195
|
+
* host (`~/.cursor/plugins/cache` did not exist), and a guessed path would
|
|
196
|
+
* resolve nothing while implying coverage. Add it once a real install is seen.
|
|
197
|
+
*
|
|
198
|
+
* `CODEX_HOME` is TRIMMED before the emptiness test, not merely defaulted.
|
|
199
|
+
* `||` and shell `${X:-…}` both fire on unset/empty only, so a whitespace-only
|
|
200
|
+
* value passes straight through and the scan globs a nonsense base
|
|
201
|
+
* (`.claude/rules/development.md` § Env-var fallback whitespace trap).
|
|
202
|
+
* `.mcp.json`'s bash bootstrap carries the same trim for the same reason —
|
|
203
|
+
* before 2026-08-28 it used `${CODEX_HOME:-$HOME/.codex}` and diverged from here on
|
|
204
|
+
* exactly that input.
|
|
205
|
+
*
|
|
206
|
+
* @returns {string[]} Absolute base directories, most-specific client first
|
|
207
|
+
*/
|
|
208
|
+
function _pluginCacheBases() {
|
|
209
|
+
const home = os.homedir();
|
|
210
|
+
const codexHome = (process.env.CODEX_HOME || '').trim() || path.join(home, '.codex');
|
|
211
|
+
return [codexHome, path.join(home, '.claude')];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Scan the client plugin caches for an installed copy of this plugin.
|
|
216
|
+
*
|
|
217
|
+
* This is the only level that can succeed for a marketplace install launched
|
|
218
|
+
* outside any checkout. Measured 2026-08-28 by probing a registered MCP server
|
|
219
|
+
* from `/tmp` (codex-cli 0.141.0): the child process received NO plugin-root
|
|
220
|
+
* environment variable (`CLAUDE_PLUGIN_ROOT`, `CODEX_PLUGIN_ROOT`,
|
|
221
|
+
* `PLUGIN_ROOT` and `CODEX_HOME` were all unset), its `PWD` was the launch
|
|
222
|
+
* directory, and `HOME` was set — so every earlier level is blind while the
|
|
223
|
+
* installed copy sits at a well-known path under `HOME`.
|
|
224
|
+
* See GH Kanevry/session-orchestrator#64.
|
|
225
|
+
*
|
|
226
|
+
* Ceiling (BV-004): among valid candidates the newest wins by directory
|
|
227
|
+
* **mtime**, not by semver. That reads as "the copy the client installed most
|
|
228
|
+
* recently", which is the intent, and it keeps this resolver free of a semver
|
|
229
|
+
* parser — a lexical sort would already be wrong today (`3.9.0` sorts above
|
|
230
|
+
* `3.10.0`). Revisit if a client starts pre-seeding caches it never launches.
|
|
231
|
+
*
|
|
232
|
+
* Equal-mtime tiebreak: the comparison is STRICTLY greater, so on a tie the
|
|
233
|
+
* FIRST candidate in scan order wins — bases in `_pluginCacheBases()` order
|
|
234
|
+
* (`CODEX_HOME` before `~/.claude`), then `readdirSync` order within each. Two
|
|
235
|
+
* cache copies sharing an mtime to the millisecond means the same content
|
|
236
|
+
* installed twice, so the choice is arbitrary rather than wrong; what matters
|
|
237
|
+
* is that it is DETERMINISTIC and identical to `.mcp.json`'s `[ "$c" -nt "$r" ]`,
|
|
238
|
+
* which is also strict. Untested by design — a test would pin an arbitrary
|
|
239
|
+
* choice as a contract.
|
|
240
|
+
*
|
|
241
|
+
* @param {string[]} tried Diagnostic accumulator, appended to on failure
|
|
242
|
+
* @returns {string|null} Absolute path to the newest cached plugin copy
|
|
243
|
+
*/
|
|
244
|
+
function _scanPluginCaches(tried) {
|
|
245
|
+
const bases = _pluginCacheBases();
|
|
246
|
+
let best = null;
|
|
247
|
+
let bestMtimeMs = -1;
|
|
248
|
+
|
|
249
|
+
for (const base of bases) {
|
|
250
|
+
const cacheDir = path.join(base, 'plugins', 'cache');
|
|
251
|
+
if (!_isDir(cacheDir)) continue;
|
|
252
|
+
|
|
253
|
+
let marketplaces;
|
|
254
|
+
try { marketplaces = readdirSync(cacheDir); } catch { continue; }
|
|
255
|
+
|
|
256
|
+
for (const marketplace of marketplaces) {
|
|
257
|
+
const pluginDir = path.join(cacheDir, marketplace, 'session-orchestrator');
|
|
258
|
+
if (!_isDir(pluginDir)) continue;
|
|
259
|
+
|
|
260
|
+
let versions;
|
|
261
|
+
try { versions = readdirSync(pluginDir); } catch { continue; }
|
|
262
|
+
|
|
263
|
+
for (const version of versions) {
|
|
264
|
+
const candidate = path.join(pluginDir, version);
|
|
265
|
+
// The directory NAME is not proof — a foreign package may sit under a
|
|
266
|
+
// `session-orchestrator/` marketplace folder. The package.json marker is.
|
|
267
|
+
if (!_isPluginRoot(candidate)) continue;
|
|
268
|
+
let mtimeMs;
|
|
269
|
+
try { mtimeMs = statSync(candidate).mtimeMs; } catch { continue; }
|
|
270
|
+
if (mtimeMs > bestMtimeMs) { bestMtimeMs = mtimeMs; best = candidate; }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!best) {
|
|
276
|
+
const globs = bases.map((base) => path.join(base, 'plugins', 'cache', '*', 'session-orchestrator', '*'));
|
|
277
|
+
tried.push(`plugin caches (${globs.join(', ')}) — no package.json{name:session-orchestrator} found`);
|
|
278
|
+
}
|
|
279
|
+
return best;
|
|
280
|
+
}
|
|
281
|
+
|
|
160
282
|
// ---------------------------------------------------------------------------
|
|
161
283
|
// resolvePluginRoot
|
|
162
284
|
// ---------------------------------------------------------------------------
|
|
@@ -164,13 +286,9 @@ function _walkUp(startDir) {
|
|
|
164
286
|
/**
|
|
165
287
|
* Resolve the absolute path to the session-orchestrator plugin directory.
|
|
166
288
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* 3. Remaining compatibility roots in legacy order
|
|
171
|
-
* 4. Walk up from import.meta.url (the location of this file) looking for a
|
|
172
|
-
* package.json with name "session-orchestrator"
|
|
173
|
-
* 5. Walk up from process.cwd() looking for the same marker
|
|
289
|
+
* Tier order is defined ONCE in this file's top docblock (§ TIER ORDER) and
|
|
290
|
+
* mirrored by `.mcp.json`'s bash bootstrap. Do not restate it here — a second
|
|
291
|
+
* copy is what let the two drift.
|
|
174
292
|
*
|
|
175
293
|
* @param {string} [platformHint] Optional compatibility hint for wrapper callers
|
|
176
294
|
* @returns {string} Absolute path to the plugin root
|
|
@@ -200,11 +318,17 @@ export function resolvePluginRoot(platformHint) {
|
|
|
200
318
|
if (byCwd) return byCwd;
|
|
201
319
|
tried.push(`walk from cwd (${process.cwd()}) — no package.json{name:session-orchestrator} found`);
|
|
202
320
|
|
|
321
|
+
// Level 8: client plugin caches — the marketplace-install case, where no env
|
|
322
|
+
// var is provided and the cwd is outside every checkout (#64).
|
|
323
|
+
const byPluginCache = _scanPluginCaches(tried);
|
|
324
|
+
if (byPluginCache) return byPluginCache;
|
|
325
|
+
|
|
203
326
|
throw new PluginRootResolutionError(
|
|
204
327
|
'Could not resolve session-orchestrator plugin root. ' +
|
|
205
328
|
'Set PLUGIN_ROOT, CLAUDE_PLUGIN_ROOT, CODEX_PLUGIN_ROOT, CURSOR_RULES_DIR, or PI_PLUGIN_ROOT ' +
|
|
206
|
-
'to the plugin directory,
|
|
207
|
-
'ancestor of the cwd or this script
|
|
329
|
+
'to the plugin directory, ensure a package.json with name "session-orchestrator" exists in an ' +
|
|
330
|
+
'ancestor of the cwd or this script, or (re)install the plugin through your client\'s marketplace ' +
|
|
331
|
+
'so a cached copy exists. Attempted: ' + tried.join('; '),
|
|
208
332
|
tried,
|
|
209
333
|
);
|
|
210
334
|
}
|