session-orchestrator 3.21.0 → 3.23.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 +11 -2
- 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/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- 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/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 +269 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/memory-proposal-collector.md +6 -4
- 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/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- 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 +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -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 +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- 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/monitors/monitors.json +6 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- 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/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- 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/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- 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/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- 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 +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- 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 +159 -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-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -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 +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +59 -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-rules.mjs +153 -9
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- 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-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- 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/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- 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 +96 -48
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- 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/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
* to AskUserQuestion in the coordinator. Each object has the shape:
|
|
14
14
|
* { question, header, options: [{ label, description }], multiSelect }
|
|
15
15
|
*
|
|
16
|
+
* optionValue(label)
|
|
17
|
+
* Maps a displayed option label to the value stored in owner.yaml by
|
|
18
|
+
* stripping the trailing `(Recommended)` marker. The label and the stored
|
|
19
|
+
* value are deliberately NOT the same string — see the function comment.
|
|
20
|
+
*
|
|
16
21
|
* applyInterviewAnswers(answers, { path? } = {})
|
|
17
22
|
* Accepts an array of selected option labels (one per question, same order
|
|
18
|
-
* as getInterviewQuestions()),
|
|
19
|
-
* and writes owner.yaml via
|
|
23
|
+
* as getInterviewQuestions()), resolves each through optionValue(), validates
|
|
24
|
+
* the result against validateOwnerConfig, and writes owner.yaml via
|
|
25
|
+
* writeOwnerConfig.
|
|
20
26
|
* Returns { ok, path, errors }.
|
|
21
27
|
*
|
|
22
28
|
* runOwnerInterview({ skipIfExists?, force?, path? } = {})
|
|
@@ -37,6 +43,40 @@ import {
|
|
|
37
43
|
getDefaults,
|
|
38
44
|
} from './owner-yaml.mjs';
|
|
39
45
|
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Label ←→ stored value
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A trailing recommendation marker on an option LABEL — the display half of a
|
|
52
|
+
* label, never part of the stored value.
|
|
53
|
+
*
|
|
54
|
+
* Anchored at the end and non-greedy about whitespace so it can only ever strip
|
|
55
|
+
* a suffix; a value that merely CONTAINS the word (there is none today) survives.
|
|
56
|
+
*/
|
|
57
|
+
const RECOMMENDED_SUFFIX = /\s*\((?:Recommended|Empfohlen|Default)\)\s*$/u;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Map an AUQ option label back to the value that is written to owner.yaml.
|
|
61
|
+
*
|
|
62
|
+
* The label and the stored value used to be the SAME string, which made the
|
|
63
|
+
* label unchangeable: `applyInterviewAnswers()` matches the answer against
|
|
64
|
+
* closed enums (`['direct','neutral','friendly']` and friends) and falls back to
|
|
65
|
+
* a default on any miss. Adding the `(Recommended)` marker AUQ-003 requires
|
|
66
|
+
* would therefore have turned a picked `direct` into a silently-stored
|
|
67
|
+
* `neutral` — a wrong answer written to disk with no error anywhere.
|
|
68
|
+
*
|
|
69
|
+
* Splitting the two keeps the label free for the operator and the value pinned
|
|
70
|
+
* to the enum. Everything before the marker is the value, verbatim.
|
|
71
|
+
*
|
|
72
|
+
* @param {unknown} label — the option label as selected by the operator
|
|
73
|
+
* @returns {string} the enum value to store, or '' for a non-string input
|
|
74
|
+
*/
|
|
75
|
+
export function optionValue(label) {
|
|
76
|
+
if (typeof label !== 'string') return '';
|
|
77
|
+
return label.replace(RECOMMENDED_SUFFIX, '').trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
// ---------------------------------------------------------------------------
|
|
41
81
|
// Question definitions
|
|
42
82
|
// ---------------------------------------------------------------------------
|
|
@@ -50,51 +90,51 @@ import {
|
|
|
50
90
|
export function getInterviewQuestions() {
|
|
51
91
|
return [
|
|
52
92
|
{
|
|
53
|
-
question: 'Which language should the assistant
|
|
54
|
-
header: '
|
|
93
|
+
question: 'Which language should the assistant answer in?',
|
|
94
|
+
header: 'Language 1/5',
|
|
55
95
|
options: [
|
|
56
|
-
{ label: 'de', description: 'German —
|
|
57
|
-
{ label: 'en', description: 'English —
|
|
58
|
-
{ label: 'other', description: '
|
|
96
|
+
{ label: 'de', description: 'German — answers, narration and questions all in Deutsch.' },
|
|
97
|
+
{ label: 'en', description: 'English — answers, narration and questions all in English.' },
|
|
98
|
+
{ label: 'other', description: 'Stored as English either way — only de and en are accepted. Change it later in owner.yaml (your settings file).' },
|
|
59
99
|
],
|
|
60
100
|
multiSelect: false,
|
|
61
101
|
},
|
|
62
102
|
{
|
|
63
|
-
question: '
|
|
64
|
-
header: '
|
|
103
|
+
question: 'How should the assistant talk to you?',
|
|
104
|
+
header: 'Tone 2/5',
|
|
65
105
|
options: [
|
|
66
|
-
{ label: 'direct', description: '
|
|
67
|
-
{ label: 'neutral', description: '
|
|
68
|
-
{ label: 'friendly', description: 'Warm and conversational
|
|
106
|
+
{ label: 'direct (Recommended)', description: 'No filler, no praise, straight to the point — fastest to read once you know the project.' },
|
|
107
|
+
{ label: 'neutral', description: 'Professional without being terse. Pick this if direct reads too blunt.' },
|
|
108
|
+
{ label: 'friendly', description: 'Warm and conversational. Costs a few lines per answer, and suits open-ended exploration.' },
|
|
69
109
|
],
|
|
70
110
|
multiSelect: false,
|
|
71
111
|
},
|
|
72
112
|
{
|
|
73
|
-
question: 'How much
|
|
74
|
-
header: '
|
|
113
|
+
question: 'How much should the assistant write by default?',
|
|
114
|
+
header: 'Output 3/5',
|
|
75
115
|
options: [
|
|
76
|
-
{ label: '
|
|
77
|
-
{ label: '
|
|
78
|
-
{ label: 'ultra', description: '
|
|
116
|
+
{ label: 'full (Recommended)', description: 'Terse but complete: narration trimmed, every fact kept. Safe default — you lose words, never data.' },
|
|
117
|
+
{ label: 'lite', description: 'Keeps the explanations and background too. Slower to read, better while the codebase is still new to you.' },
|
|
118
|
+
{ label: 'ultra', description: 'Code and decisions only, no narration. You will have to ask why more often.' },
|
|
79
119
|
],
|
|
80
120
|
multiSelect: false,
|
|
81
121
|
},
|
|
82
122
|
{
|
|
83
|
-
question: 'How should the assistant
|
|
84
|
-
header: '
|
|
123
|
+
question: 'How much should the assistant explain before it starts working?',
|
|
124
|
+
header: 'Preamble 4/5',
|
|
85
125
|
options: [
|
|
86
|
-
{ label: 'minimal', description: 'One
|
|
87
|
-
{ label: 'verbose', description: '
|
|
126
|
+
{ label: 'minimal (Recommended)', description: 'One line of status, then it works. Safe default — you can still ask for the reasoning afterwards.' },
|
|
127
|
+
{ label: 'verbose', description: 'Plan and reasoning before each major action. Costs a few lines every step.' },
|
|
88
128
|
],
|
|
89
129
|
multiSelect: false,
|
|
90
130
|
},
|
|
91
131
|
{
|
|
92
|
-
question: '
|
|
93
|
-
header: '
|
|
132
|
+
question: 'May the plugin share anonymized hardware data to improve its resource defaults?',
|
|
133
|
+
header: 'Sharing 5/5',
|
|
94
134
|
options: [
|
|
95
|
-
{ label: 'No', description: '
|
|
96
|
-
{ label: 'Yes', description: '
|
|
97
|
-
{ label: 'Preview', description: '
|
|
135
|
+
{ label: 'No (Recommended)', description: 'Nothing leaves this machine. Safe default — you can switch it on later without redoing this interview.' },
|
|
136
|
+
{ label: 'Yes', description: 'Shares hashed hardware patterns — never file names, paths or content. Helps tune the wave and session defaults.' },
|
|
137
|
+
{ label: 'Preview', description: 'Shows exactly what would be sent, then asks again. Costs one extra step.' },
|
|
98
138
|
],
|
|
99
139
|
multiSelect: false,
|
|
100
140
|
},
|
|
@@ -108,12 +148,15 @@ export function getInterviewQuestions() {
|
|
|
108
148
|
/**
|
|
109
149
|
* Map interview answer labels to an owner.yaml config object and write it.
|
|
110
150
|
*
|
|
151
|
+
* Each answer is the LABEL the operator picked, which may carry a trailing
|
|
152
|
+
* `(Recommended)` marker; optionValue() strips it before the enum match below.
|
|
153
|
+
*
|
|
111
154
|
* @param {string[]} answers - Array of selected option labels, one per question (5 total).
|
|
112
|
-
* answers[0] = language label ('de' | 'en' |
|
|
113
|
-
* answers[1] = tone style label
|
|
114
|
-
* answers[2] = output level label
|
|
115
|
-
* answers[3] = preamble label
|
|
116
|
-
* answers[4] = hardware-sharing label ('Yes' | 'No' | 'Preview')
|
|
155
|
+
* answers[0] = language label ('de' | 'en' | 'other')
|
|
156
|
+
* answers[1] = tone style label ('direct' | 'neutral' | 'friendly', ± marker)
|
|
157
|
+
* answers[2] = output level label ('full' | 'lite' | 'ultra', ± marker)
|
|
158
|
+
* answers[3] = preamble label ('minimal' | 'verbose', ± marker)
|
|
159
|
+
* answers[4] = hardware-sharing label ('Yes' | 'No' | 'Preview', ± marker)
|
|
117
160
|
* @param {{ path?: string }} [opts]
|
|
118
161
|
* @returns {{ ok: boolean, path: string, errors: string[] }}
|
|
119
162
|
*/
|
|
@@ -124,7 +167,10 @@ export function applyInterviewAnswers(answers, opts = {}) {
|
|
|
124
167
|
return { ok: false, path: filePath, errors: ['applyInterviewAnswers requires exactly 5 answers'] };
|
|
125
168
|
}
|
|
126
169
|
|
|
127
|
-
|
|
170
|
+
// Strip the display-only `(Recommended)` marker before matching against the
|
|
171
|
+
// enums below — see optionValue(). A non-string answer becomes '' and falls
|
|
172
|
+
// through to the same default it always did.
|
|
173
|
+
const [langRaw, toneRaw, outputLevelRaw, preambleRaw, hwConsentRaw] = answers.map(optionValue);
|
|
128
174
|
|
|
129
175
|
// --- Language ---
|
|
130
176
|
// Accept 'de', 'en', or treat anything else as a free-text language code.
|
|
@@ -67,6 +67,7 @@ import { discoverActiveSessions } from './session-discovery.mjs';
|
|
|
67
67
|
import { readLock, isLockLive, LOCK_PATH } from './session-lock.mjs';
|
|
68
68
|
import { checkPeerStateMd } from './state-md-peer-guard.mjs';
|
|
69
69
|
import { listWorktreesChecked } from './worktree/listing.mjs';
|
|
70
|
+
import { parseSessionId } from './session-id.mjs';
|
|
70
71
|
|
|
71
72
|
/** Closed enum of provenance sources. */
|
|
72
73
|
const SOURCE_DISCOVERED = 'discovered'; // lock + registry unified (irreversibly merged upstream)
|
|
@@ -120,24 +121,64 @@ function _peerFromDiscovered(s, nowMs) {
|
|
|
120
121
|
return peer;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a semantic hint to the concrete local raw ID for discovered surfaces.
|
|
126
|
+
*
|
|
127
|
+
* Semantic labels are attribution, not ownership. A mapping is usable only when
|
|
128
|
+
* current-session.json names the hint and its raw id exactly matches the local
|
|
129
|
+
* readable session.lock. Otherwise return null so a potentially foreign record
|
|
130
|
+
* stays visible. Raw caller ids retain the legacy direct comparison unchanged.
|
|
131
|
+
*
|
|
132
|
+
* This is a consistency check on two repo-global files, NOT a proof of ownership;
|
|
133
|
+
* a colliding semantic label from a foreign live session satisfies it. See the
|
|
134
|
+
* call site in findPeers for the measured case and why it is not closed here.
|
|
135
|
+
*
|
|
136
|
+
* @param {string|null} mySessionId
|
|
137
|
+
* @param {string} repoRoot
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
function _discoveredSelfSessionId(mySessionId, repoRoot) {
|
|
141
|
+
if (typeof mySessionId !== 'string' || mySessionId.length === 0) return null;
|
|
142
|
+
if (parseSessionId(mySessionId)?.format !== 'semantic') return mySessionId;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(
|
|
146
|
+
path.join(repoRoot, '.orchestrator', 'current-session.json'),
|
|
147
|
+
'utf8',
|
|
148
|
+
);
|
|
149
|
+
const current = JSON.parse(raw);
|
|
150
|
+
const lock = readLock({ repoRoot });
|
|
151
|
+
if (
|
|
152
|
+
current &&
|
|
153
|
+
typeof current === 'object' &&
|
|
154
|
+
!Array.isArray(current) &&
|
|
155
|
+
typeof current.session_id === 'string' &&
|
|
156
|
+
current.session_id.length > 0 &&
|
|
157
|
+
current.semantic_session_id === mySessionId &&
|
|
158
|
+
lock !== null &&
|
|
159
|
+
typeof lock.session_id === 'string' &&
|
|
160
|
+
lock.session_id === current.session_id
|
|
161
|
+
) {
|
|
162
|
+
return lock.session_id;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Missing or malformed local state cannot prove a semantic alias.
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
123
170
|
/**
|
|
124
171
|
* findPeers — union of all 3 peer-discovery surfaces, fail-open per surface.
|
|
125
172
|
*
|
|
126
173
|
* @param {string} repoRoot Absolute path to the repository root.
|
|
127
174
|
* @param {object} [opts] passthrough seams shared with the underlying surfaces.
|
|
128
|
-
* @param {string|null} [opts.mySessionId] Current session id
|
|
129
|
-
* self-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* the SAME id-space `mySessionId` was derived from (see the PRECONDITION
|
|
136
|
-
* note in state-md-peer-guard.mjs). Passing a semantic id here self-excludes
|
|
137
|
-
* correctly on Surface C but NOT on Surface A+B (the UUID lock/registry
|
|
138
|
-
* entry for the same session will still surface as a 'discovered' peer of
|
|
139
|
-
* itself) — callers that need both surfaces to self-exclude MUST pass the
|
|
140
|
-
* UUID.
|
|
175
|
+
* @param {string|null} [opts.mySessionId] Current session id hint. Raw
|
|
176
|
+
* ids self-exclude directly on Surface A+B. A semantic hint maps to a raw
|
|
177
|
+
* discovered-surface id only after the local current-session.json semantic
|
|
178
|
+
* label matches it AND that file's raw id exactly equals local
|
|
179
|
+
* session.lock.session_id. Missing, malformed, or mismatched local state
|
|
180
|
+
* maps nothing, leaving discovered entries visible. Surface C receives this
|
|
181
|
+
* original hint unchanged and compares it to STATE.md's `session:` field.
|
|
141
182
|
* @param {number} [opts.now] ms-since-epoch (test seam for freshness/age).
|
|
142
183
|
* @param {number} [opts.freshnessMin] Registry-entry freshness threshold (minutes).
|
|
143
184
|
* @param {number} [opts.maxAgeHours] STATE.md abandonment threshold (hours).
|
|
@@ -155,6 +196,7 @@ function _peerFromDiscovered(s, nowMs) {
|
|
|
155
196
|
export async function findPeers(repoRoot, opts = {}) {
|
|
156
197
|
const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
|
|
157
198
|
const mySessionId = opts.mySessionId ?? null;
|
|
199
|
+
const discoveredSelfSessionId = _discoveredSelfSessionId(mySessionId, repoRoot);
|
|
158
200
|
|
|
159
201
|
const peers = [];
|
|
160
202
|
|
|
@@ -172,15 +214,24 @@ export async function findPeers(repoRoot, opts = {}) {
|
|
|
172
214
|
});
|
|
173
215
|
if (Array.isArray(discovered)) {
|
|
174
216
|
for (const s of discovered) {
|
|
175
|
-
// Self-exclusion (#798):
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
217
|
+
// Self-exclusion (#798, #1085): discovered entries carry raw ids. A
|
|
218
|
+
// semantic caller hint filters one such id only after the local raw
|
|
219
|
+
// binding was verified above.
|
|
220
|
+
//
|
|
221
|
+
// The binding proves CONSISTENCY of the two local files, not OWNERSHIP.
|
|
222
|
+
// Both are repo-global, so if a foreign live session wrote them last and
|
|
223
|
+
// its semantic label collides with this hint (labels like
|
|
224
|
+
// `main-2026-08-21-session-2` are routinely shared — see
|
|
225
|
+
// hooks/on-session-start.mjs), that foreign raw id is filtered and the
|
|
226
|
+
// peer PSA-002 exists to surface goes missing. Measured 2026-08-21:
|
|
227
|
+
// null hint -> peer visible, colliding semantic hint -> peers: [].
|
|
228
|
+
// Closing it needs a per-process ownership proof, not a stronger read
|
|
229
|
+
// of the same two files.
|
|
230
|
+
if (
|
|
231
|
+
s &&
|
|
232
|
+
typeof s.sessionId === 'string' &&
|
|
233
|
+
s.sessionId !== discoveredSelfSessionId
|
|
234
|
+
) {
|
|
184
235
|
peers.push(_peerFromDiscovered(s, nowMs));
|
|
185
236
|
}
|
|
186
237
|
}
|
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
|
}
|