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
|
@@ -52,6 +52,8 @@ import {
|
|
|
52
52
|
} from '../scripts/lib/session-registry.mjs';
|
|
53
53
|
import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detector.mjs';
|
|
54
54
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
55
|
+
import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
|
|
56
|
+
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
55
57
|
|
|
56
58
|
const execFileAsync = promisify(execFile);
|
|
57
59
|
|
|
@@ -79,6 +81,19 @@ const execFileAsync = promisify(execFile);
|
|
|
79
81
|
// re-introduce the bug.
|
|
80
82
|
const bannerLines = [];
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Coordinator-directed context for the single end-of-hook flush (#1138).
|
|
86
|
+
*
|
|
87
|
+
* The banner above is for the OPERATOR (`systemMessage`); this is for the
|
|
88
|
+
* MODEL (`hookSpecificOutput.additionalContext`). Both leave in the SAME JSON
|
|
89
|
+
* object for the same reason the banner was consolidated: Claude Code surfaces
|
|
90
|
+
* only the FIRST JSON object a SessionStart hook writes to stdout, so a second
|
|
91
|
+
* `writeSync` would be silently discarded.
|
|
92
|
+
*
|
|
93
|
+
* @type {string|null}
|
|
94
|
+
*/
|
|
95
|
+
let pendingAdditionalContext = null;
|
|
96
|
+
|
|
82
97
|
/**
|
|
83
98
|
* Queue one or more banner lines for the single end-of-hook flush.
|
|
84
99
|
* @param {string|null|undefined} line — multi-line strings are pushed verbatim
|
|
@@ -89,7 +104,70 @@ function pushBanner(line) {
|
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
/**
|
|
92
|
-
*
|
|
107
|
+
* Render a session id compactly for a banner line.
|
|
108
|
+
*
|
|
109
|
+
* Semantic ids (`main-2026-08-23-deep-1`) are already short and are printed
|
|
110
|
+
* verbatim — they carry the branch and the mode, which is exactly what an
|
|
111
|
+
* operator needs to recognise a peer. A UUID (Claude Code's `session_id` on
|
|
112
|
+
* hosts where no semantic id was resolved) is truncated to its first 8 chars:
|
|
113
|
+
* long enough to disambiguate the handful of peers a repo can have, short
|
|
114
|
+
* enough that three of them fit on one banner line.
|
|
115
|
+
*
|
|
116
|
+
* Ceiling: 8 chars is ~4 billion values against at most a few dozen live
|
|
117
|
+
* sessions per host — revisit only if two peers ever collide in a banner.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} id
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function shortSessionId(id) {
|
|
123
|
+
if (typeof id !== 'string' || id.length === 0) return '?';
|
|
124
|
+
return id.length > 24 ? id.slice(0, 8) : id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* GH#67 — render the provenance marker for ONE mechanically-detected peer.
|
|
129
|
+
*
|
|
130
|
+
* `discoverActiveSessions()` annotates registry-sourced peers additively
|
|
131
|
+
* (`registryOnly` / `lockSuperseded` / `lockOwnerId`); lock-sourced peers carry
|
|
132
|
+
* none of the three, so they render byte-identically to the pre-GH#67 banner.
|
|
133
|
+
*
|
|
134
|
+
* `lockSuperseded: true` means a LIVE lock at THIS root is held by a different
|
|
135
|
+
* raw session_id than the registry entry — the common cause is a finished task
|
|
136
|
+
* whose SessionEnd never ran, leaving a fresh-but-orphaned registry record.
|
|
137
|
+
* It is a HINT, never a verdict: the session lock is advisory, so the entry may
|
|
138
|
+
* equally be a live peer that lost the acquire race (#1085). The marker is
|
|
139
|
+
* therefore purely additive — it never removes the peer, and never changes the
|
|
140
|
+
* count or the WARN decision (HR-106).
|
|
141
|
+
*
|
|
142
|
+
* @param {{registryOnly?:boolean, lockSuperseded?:boolean, lockOwnerId?:string|null}} peer
|
|
143
|
+
* @returns {string} '' for a lock-sourced peer, else a bracketed marker.
|
|
144
|
+
*/
|
|
145
|
+
function supersessionMarker(peer) {
|
|
146
|
+
if (!peer || peer.registryOnly !== true) return '';
|
|
147
|
+
if (peer.lockSuperseded === true) return ' [registry-only, superseded]';
|
|
148
|
+
return ' [registry-only]';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Queue the ONE `additionalContext` string for the single end-of-hook flush.
|
|
153
|
+
* Last writer wins; there is deliberately no accumulation, because every
|
|
154
|
+
* character here costs context on every session start.
|
|
155
|
+
* @param {string|null|undefined} text
|
|
156
|
+
*/
|
|
157
|
+
function setAdditionalContext(text) {
|
|
158
|
+
if (typeof text !== 'string' || text.length === 0) return;
|
|
159
|
+
pendingAdditionalContext = text;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Emit the queued banner lines AND any queued additionalContext as ONE JSON
|
|
164
|
+
* envelope.
|
|
165
|
+
*
|
|
166
|
+
* Both keys are valid siblings of one hook-output object: the shipped Claude
|
|
167
|
+
* Code binary (v2.1.241) documents `systemMessage` ("Display a message to the
|
|
168
|
+
* user (all hooks)") and `hookSpecificOutput.additionalContext` ("Text injected
|
|
169
|
+
* into model context") as fields of the same "Hook JSON Output" object, and its
|
|
170
|
+
* output validator recognises both keys on that one object.
|
|
93
171
|
*
|
|
94
172
|
* Uses `fs.writeSync(1, …)` rather than `console.log`: the top-level guard
|
|
95
173
|
* calls `process.exit(0)`, which discards anything still sitting in libuv's
|
|
@@ -101,11 +179,22 @@ function pushBanner(line) {
|
|
|
101
179
|
* Idempotent: a second call after a flush is a no-op.
|
|
102
180
|
*/
|
|
103
181
|
function flushBanner() {
|
|
104
|
-
|
|
105
|
-
const
|
|
182
|
+
const hasBanner = bannerLines.length > 0;
|
|
183
|
+
const hasContext = typeof pendingAdditionalContext === 'string' && pendingAdditionalContext.length > 0;
|
|
184
|
+
if (!hasBanner && !hasContext) return;
|
|
185
|
+
|
|
186
|
+
const envelope = {};
|
|
187
|
+
if (hasBanner) envelope.systemMessage = bannerLines.join('\n');
|
|
188
|
+
if (hasContext) {
|
|
189
|
+
envelope.hookSpecificOutput = {
|
|
190
|
+
hookEventName: 'SessionStart',
|
|
191
|
+
additionalContext: pendingAdditionalContext,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
106
194
|
bannerLines.length = 0;
|
|
195
|
+
pendingAdditionalContext = null;
|
|
107
196
|
try {
|
|
108
|
-
writeSync(1, `${
|
|
197
|
+
writeSync(1, `${JSON.stringify(envelope)}\n`);
|
|
109
198
|
} catch { /* stdout closed — the hook is informational and never blocks */ }
|
|
110
199
|
}
|
|
111
200
|
|
|
@@ -315,6 +404,12 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
315
404
|
if (Object.prototype.hasOwnProperty.call(prev, 'last_batch')) {
|
|
316
405
|
preserved.last_batch = prev.last_batch;
|
|
317
406
|
}
|
|
407
|
+
// #1193 — the final-wave completion marker must survive a
|
|
408
|
+
// clear/compact too: dropping it re-arms a duplicate SessionEnd
|
|
409
|
+
// `orchestrator.wave.completed` for a wave already closed.
|
|
410
|
+
if (Object.prototype.hasOwnProperty.call(prev, 'last_wave_completed')) {
|
|
411
|
+
preserved.last_wave_completed = prev.last_wave_completed;
|
|
412
|
+
}
|
|
318
413
|
}
|
|
319
414
|
} catch { /* absent / unparseable → no preservation (reset) */ }
|
|
320
415
|
}
|
|
@@ -349,6 +444,10 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
349
444
|
*
|
|
350
445
|
* Returns the candidate string on success, null on any failure. Never throws.
|
|
351
446
|
*
|
|
447
|
+
* Contract (#1066, "Minimal"): the semantic id is a best-effort, host-wide
|
|
448
|
+
* monotonic LABEL, never an ownership key — ownership stays `(raw session_id,
|
|
449
|
+
* owner proof)`, and the two identity forms are never interchangeable.
|
|
450
|
+
*
|
|
352
451
|
* Used by both branches of resolveSessionId() so the semantic_session_id
|
|
353
452
|
* field on the session.lock is consistently populated regardless of whether
|
|
354
453
|
* the SessionStart stdin payload provided a UUID (Claude Code) or nothing
|
|
@@ -359,7 +458,7 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
359
458
|
*/
|
|
360
459
|
async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
361
460
|
try {
|
|
362
|
-
const { resolveSemanticSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
461
|
+
const { resolveSemanticSessionId, parseSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
363
462
|
const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
|
|
364
463
|
const { execSync } = await import('node:child_process');
|
|
365
464
|
|
|
@@ -387,10 +486,28 @@ async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
|
387
486
|
discoverActiveSessions(projectRoot).catch(() => []),
|
|
388
487
|
readRegistry().catch(() => []),
|
|
389
488
|
]);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
489
|
+
|
|
490
|
+
// Registry census (#1066 AC1): the registry keeps the two identity forms in
|
|
491
|
+
// SEPARATE fields — `session_id` is the RAW id (a UUID on Claude Code) and
|
|
492
|
+
// `semantic_session_id` is the label. resolveSemanticSessionId counts only
|
|
493
|
+
// semantic candidates, so projecting `session_id` alone made the host-wide
|
|
494
|
+
// registry contribute NOTHING to the n-increment, and two sessions on one
|
|
495
|
+
// host could mint the same label. Preference order per entry:
|
|
496
|
+
// 1. `semantic_session_id`, when it parses as semantic;
|
|
497
|
+
// 2. `session_id`, ONLY when it itself parses as semantic (Codex/Cursor
|
|
498
|
+
// may write a semantic raw id).
|
|
499
|
+
// A legacy v1 entry (no `semantic_session_id`, UUID `session_id`) matches
|
|
500
|
+
// neither and is dropped — a UUID is never reintroduced as a candidate.
|
|
501
|
+
const semanticLabelOf = (entry) => {
|
|
502
|
+
for (const candidate of [entry?.semantic_session_id, entry?.session_id]) {
|
|
503
|
+
if (parseSessionId(candidate)?.format === 'semantic') return candidate;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
};
|
|
507
|
+
const registrySessions = registryEntries.flatMap((r) => {
|
|
508
|
+
const label = semanticLabelOf(r);
|
|
509
|
+
return label === null ? [] : [{ sessionId: label, mode: r.mode ?? 'session' }];
|
|
510
|
+
});
|
|
394
511
|
const activeSessions = [...localSessions, ...registrySessions];
|
|
395
512
|
|
|
396
513
|
return await resolveSemanticSessionId({
|
|
@@ -467,6 +584,60 @@ async function emitHostBanner(projectRoot) {
|
|
|
467
584
|
}
|
|
468
585
|
}
|
|
469
586
|
|
|
587
|
+
/**
|
|
588
|
+
* The one-time telemetry-consent instruction handed to the coordinator (#1138).
|
|
589
|
+
*
|
|
590
|
+
* Deliberately terse: it is injected into the model context on EVERY session
|
|
591
|
+
* start until the operator decides, so every character is a recurring cost.
|
|
592
|
+
* It names the phase rather than restating it, so the AUQ wording stays
|
|
593
|
+
* single-sourced in `skills/session-start/SKILL.md` § Phase 6.8.
|
|
594
|
+
*/
|
|
595
|
+
const CONSENT_NUDGE =
|
|
596
|
+
'Telemetrie-Consent ist auf diesem Host unentschieden. Stelle JETZT, vor jeder anderen Arbeit, '
|
|
597
|
+
+ 'genau einmal die consent-neutrale AskUserQuestion aus skills/session-start/SKILL.md § Phase 6.8 '
|
|
598
|
+
+ '(zwei Optionen, keine "(Recommended)"-Markierung; Codex/Cursor: nummerierte Liste) und rufe danach '
|
|
599
|
+
+ 'grantConsent() bzw. denyConsent() aus scripts/lib/telemetry/consent.mjs. Danach nie wieder fragen.';
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* #1138 — queue the consent nudge when, and only when, this host has no
|
|
603
|
+
* telemetry decision on record.
|
|
604
|
+
*
|
|
605
|
+
* WHY A HOOK AT ALL. Phase 6.8 was prose only: it fired when the coordinator
|
|
606
|
+
* happened to reach line ~1060 of a 1230-line skill, after 24 other phases.
|
|
607
|
+
* Measured 2026-08-23: 0 records from any host other than the author's.
|
|
608
|
+
*
|
|
609
|
+
* WHY `isCiEnv` AND NOT `isHeadless`. `isHeadless()` answers "can THIS process
|
|
610
|
+
* prompt on a TTY?" — and this process is a hook whose stdout is a pipe, so it
|
|
611
|
+
* would answer `true` unconditionally and the nudge could never fire. Measured
|
|
612
|
+
* in a plain non-TTY subprocess on this host, 2026-08-23:
|
|
613
|
+
* `isHeadless()=true isCiEnv()=false stdout.isTTY=undefined`
|
|
614
|
+
* The question that actually matters here is a different one: "is there an
|
|
615
|
+
* operator who can answer an AskUserQuestion?" The prompting happens in the
|
|
616
|
+
* live session, not in this process, so TTY-ness of the hook is irrelevant and
|
|
617
|
+
* `isCiEnv()` — the fail-closed "this is unattended automation" probe — is the
|
|
618
|
+
* correct gate. (The same trap sits in the Phase 6.8 prose, which passes
|
|
619
|
+
* `interactive: !isHeadless()` from a `node -e` subprocess whose stdout is also
|
|
620
|
+
* a pipe: it would have resolved `prompt:false` every time even if executed.)
|
|
621
|
+
*
|
|
622
|
+
* `resolveConsent` remains the single decision point: `prompt` is true only for
|
|
623
|
+
* a host with no stored decision, no env override, and no fleet flag.
|
|
624
|
+
*
|
|
625
|
+
* Never throws — a failure here must not affect session start.
|
|
626
|
+
*/
|
|
627
|
+
function maybeQueueConsentNudge() {
|
|
628
|
+
try {
|
|
629
|
+
if (isCiEnv(process.env)) return;
|
|
630
|
+
const consent = resolveConsent({
|
|
631
|
+
env: process.env,
|
|
632
|
+
ownerConfig: loadOwnerConfig().config,
|
|
633
|
+
state: readTelemetryState().record,
|
|
634
|
+
interactive: true,
|
|
635
|
+
});
|
|
636
|
+
if (consent.prompt !== true) return;
|
|
637
|
+
setAdditionalContext(CONSENT_NUDGE);
|
|
638
|
+
} catch { /* best effort — the nudge is informational, never blocks */ }
|
|
639
|
+
}
|
|
640
|
+
|
|
470
641
|
// ---------------------------------------------------------------------------
|
|
471
642
|
// Main
|
|
472
643
|
// ---------------------------------------------------------------------------
|
|
@@ -597,6 +768,11 @@ async function main() {
|
|
|
597
768
|
} catch { /* hook must remain non-blocking */ }
|
|
598
769
|
|
|
599
770
|
let peers = [];
|
|
771
|
+
// GH#67 consumer half — how many mechanically-detected peers were registry-
|
|
772
|
+
// sourced entries that a LIVE lock at this root supersedes. Counted for the
|
|
773
|
+
// session.started payload below; NEVER subtracted from any peer count
|
|
774
|
+
// (HR-106: the banner reports the number the verdict judged).
|
|
775
|
+
let mechanicalPeersSuperseded = 0;
|
|
600
776
|
try {
|
|
601
777
|
await sweepZombies().catch(() => ({ removed: [], logged: 0 }));
|
|
602
778
|
try {
|
|
@@ -698,23 +874,89 @@ async function main() {
|
|
|
698
874
|
const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
|
|
699
875
|
const allActive = await discoverActiveSessions(projectRoot);
|
|
700
876
|
const mechanicalPeers = allActive.filter((s) => s.sessionId !== sessionId);
|
|
877
|
+
mechanicalPeersSuperseded = mechanicalPeers.filter((p) => p.lockSuperseded === true).length;
|
|
701
878
|
if (mechanicalPeers.length > 0) {
|
|
879
|
+
// #1137 part 1 — say WHERE, not just how many. discoverActiveSessions()
|
|
880
|
+
// walks EVERY path `git worktree list` reports, which includes worktrees
|
|
881
|
+
// another coordinator parked outside this working copy (e.g. under
|
|
882
|
+
// /private/tmp/…). Measured 2026-08-23: all 4 reported "peers" were the
|
|
883
|
+
// 4 registered worktrees, none of them in this checkout — and the old
|
|
884
|
+
// wording ("active in same repo") read as 4 sessions competing for THIS
|
|
885
|
+
// directory. The surface is deliberately unchanged (a foreign-worktree
|
|
886
|
+
// session IS a peer of this repo); only the label and the per-peer
|
|
887
|
+
// detail are, so the operator can tell the two apart at a glance.
|
|
702
888
|
const summary = mechanicalPeers
|
|
703
889
|
.slice(0, 3)
|
|
704
|
-
.map((p) =>
|
|
890
|
+
.map((p) => {
|
|
891
|
+
const where = p.worktreePath ? path.basename(p.worktreePath) : '?';
|
|
892
|
+
// #1151 — carry the peer's MODE when the source recorded one.
|
|
893
|
+
// `worktree:id` says where a peer sits but not what it is doing,
|
|
894
|
+
// and the operator's next decision (wait / promote / proceed)
|
|
895
|
+
// turns on exactly that: a `deep` peer holding this worktree set
|
|
896
|
+
// is a different situation from a `discovery` one. Guarded rather
|
|
897
|
+
// than assumed — sessionFromLock() passes `lock.mode` verbatim, so
|
|
898
|
+
// a lock written without the field yields undefined here, while
|
|
899
|
+
// sessionFromRegistryEntry() always defaults it to 'session'.
|
|
900
|
+
const mode = typeof p.mode === 'string' && p.mode.length > 0 ? `:${p.mode}` : '';
|
|
901
|
+
return `${where}:${shortSessionId(p.sessionId)}${mode}${supersessionMarker(p)}`;
|
|
902
|
+
})
|
|
705
903
|
.join(', ');
|
|
706
904
|
const overflow = mechanicalPeers.length > 3 ? ` +${mechanicalPeers.length - 3} more` : '';
|
|
707
|
-
pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in
|
|
905
|
+
pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in this repo's worktree set (${summary}${overflow})`);
|
|
906
|
+
if (mechanicalPeersSuperseded > 0) {
|
|
907
|
+
pushBanner(` ${mechanicalPeersSuperseded} of them registry-only and superseded by this root's live lock — likely a finished task without SessionEnd (GH#67). Still counted above: the session lock is advisory.`);
|
|
908
|
+
}
|
|
708
909
|
}
|
|
709
910
|
} catch { /* best effort — banner is informational, never blocks */ }
|
|
710
911
|
}
|
|
711
912
|
|
|
913
|
+
// Phase 4 measurement probes — the mechanical caller (#1128).
|
|
914
|
+
//
|
|
915
|
+
// `skills/session-start/SKILL.md` § Phase 4 names 18 probes with module paths
|
|
916
|
+
// and entry functions. Measured 2026-08-23 at `4f6404e`, not one of them had
|
|
917
|
+
// a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
|
|
918
|
+
// only caller was the prose itself, and across 336 recorded session starts
|
|
919
|
+
// there was no event proving any of them had ever run. Built, documented,
|
|
920
|
+
// never wired.
|
|
921
|
+
//
|
|
922
|
+
// Placement is deliberate: AFTER the backfill (so `sessions-staleness` and
|
|
923
|
+
// `sessions-integrity` measure the reconstructed ledger, not the stale one)
|
|
924
|
+
// and BEFORE flushBanner() (so probe findings ride the single systemMessage
|
|
925
|
+
// envelope instead of adding a second stdout object Claude Code would
|
|
926
|
+
// discard — see the bannerLines docstring and HR-106).
|
|
927
|
+
//
|
|
928
|
+
// Best-effort exactly like `backfillOnSessionStart` above: the runner has no
|
|
929
|
+
// rejecting path of its own, and this try/catch is defence-in-depth so a
|
|
930
|
+
// future regression inside it still cannot block a session start.
|
|
931
|
+
// Escape hatch: SO_DISABLE_STARTUP_PROBES=1.
|
|
932
|
+
//
|
|
933
|
+
// `enable-host-banner: false` silences the DISPLAY, never the MEASUREMENT.
|
|
934
|
+
// The documented opt-out ("a user who silenced session-start banners has
|
|
935
|
+
// opted out of ALL session-start banners") governs `pushBanner` and nothing
|
|
936
|
+
// else: the probes still run and `orchestrator.probes.completed` is still
|
|
937
|
+
// written, because the defect this wiring repairs is precisely that nobody
|
|
938
|
+
// could tell whether the probes had ever run (HR-105). Gating the run on a
|
|
939
|
+
// display preference would rebuild that blind spot behind a config key.
|
|
940
|
+
if (process.env.SO_DISABLE_STARTUP_PROBES !== '1') {
|
|
941
|
+
try {
|
|
942
|
+
const { runSessionStartProbes } = await import('../scripts/lib/session-start-probes.mjs');
|
|
943
|
+
const probeRun = await runSessionStartProbes({ repoRoot: projectRoot });
|
|
944
|
+
if (bannerData) {
|
|
945
|
+
for (const line of probeRun.bannerLines) pushBanner(line);
|
|
946
|
+
}
|
|
947
|
+
} catch { /* hook must remain non-blocking */ }
|
|
948
|
+
}
|
|
949
|
+
|
|
712
950
|
const payload = {
|
|
713
951
|
platform,
|
|
714
952
|
project: projectName,
|
|
715
953
|
branch,
|
|
716
954
|
session_id: sessionId,
|
|
717
955
|
peer_count: peers.length,
|
|
956
|
+
// GH#67 — additive: how many mechanically-detected peers a live lock at
|
|
957
|
+
// this root supersedes. `peer_count` is deliberately unchanged; this field
|
|
958
|
+
// makes the supersession rate measurable instead of inferred (HR-105).
|
|
959
|
+
peers_superseded: mechanicalPeersSuperseded,
|
|
718
960
|
};
|
|
719
961
|
if (bannerData) {
|
|
720
962
|
payload.host_class = bannerData.host.host_class;
|
|
@@ -736,7 +978,16 @@ async function main() {
|
|
|
736
978
|
payload.memory_pressure_pct_free = bannerData.resources.memory_pressure_pct_free ?? null;
|
|
737
979
|
payload.peer_sessions_count = bannerData.resources.peer_sessions_count ?? null;
|
|
738
980
|
}
|
|
739
|
-
|
|
981
|
+
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
982
|
+
// effect (scripts/lib/events.mjs); this hook must never abort on that, so
|
|
983
|
+
// the emit is wrapped rather than left to propagate.
|
|
984
|
+
try {
|
|
985
|
+
await emitEvent('orchestrator.session.started', payload);
|
|
986
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
987
|
+
|
|
988
|
+
// #1138 — one-time telemetry-consent nudge. Queued (never written) here so it
|
|
989
|
+
// rides the single stdout envelope below.
|
|
990
|
+
maybeQueueConsentNudge();
|
|
740
991
|
|
|
741
992
|
// Single flush — see the bannerLines docstring for why this must stay the
|
|
742
993
|
// only stdout write in the hook.
|