session-orchestrator 3.22.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 +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/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 +168 -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/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 +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -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 +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- 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 +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- 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-bash-issue-budget.mjs +117 -4
- 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 +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 +5 -0
- 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/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- 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/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/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 +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- 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 +281 -13
- 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 +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 +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/sync.mjs +46 -8
- 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 +16 -32
- 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-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-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/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- 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/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- 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 +11 -3
- 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 +30 -16
- 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 +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- 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,46 @@ 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
|
+
* Queue the ONE `additionalContext` string for the single end-of-hook flush.
|
|
129
|
+
* Last writer wins; there is deliberately no accumulation, because every
|
|
130
|
+
* character here costs context on every session start.
|
|
131
|
+
* @param {string|null|undefined} text
|
|
132
|
+
*/
|
|
133
|
+
function setAdditionalContext(text) {
|
|
134
|
+
if (typeof text !== 'string' || text.length === 0) return;
|
|
135
|
+
pendingAdditionalContext = text;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Emit the queued banner lines AND any queued additionalContext as ONE JSON
|
|
140
|
+
* envelope.
|
|
141
|
+
*
|
|
142
|
+
* Both keys are valid siblings of one hook-output object: the shipped Claude
|
|
143
|
+
* Code binary (v2.1.241) documents `systemMessage` ("Display a message to the
|
|
144
|
+
* user (all hooks)") and `hookSpecificOutput.additionalContext` ("Text injected
|
|
145
|
+
* into model context") as fields of the same "Hook JSON Output" object, and its
|
|
146
|
+
* output validator recognises both keys on that one object.
|
|
93
147
|
*
|
|
94
148
|
* Uses `fs.writeSync(1, …)` rather than `console.log`: the top-level guard
|
|
95
149
|
* calls `process.exit(0)`, which discards anything still sitting in libuv's
|
|
@@ -101,11 +155,22 @@ function pushBanner(line) {
|
|
|
101
155
|
* Idempotent: a second call after a flush is a no-op.
|
|
102
156
|
*/
|
|
103
157
|
function flushBanner() {
|
|
104
|
-
|
|
105
|
-
const
|
|
158
|
+
const hasBanner = bannerLines.length > 0;
|
|
159
|
+
const hasContext = typeof pendingAdditionalContext === 'string' && pendingAdditionalContext.length > 0;
|
|
160
|
+
if (!hasBanner && !hasContext) return;
|
|
161
|
+
|
|
162
|
+
const envelope = {};
|
|
163
|
+
if (hasBanner) envelope.systemMessage = bannerLines.join('\n');
|
|
164
|
+
if (hasContext) {
|
|
165
|
+
envelope.hookSpecificOutput = {
|
|
166
|
+
hookEventName: 'SessionStart',
|
|
167
|
+
additionalContext: pendingAdditionalContext,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
106
170
|
bannerLines.length = 0;
|
|
171
|
+
pendingAdditionalContext = null;
|
|
107
172
|
try {
|
|
108
|
-
writeSync(1, `${
|
|
173
|
+
writeSync(1, `${JSON.stringify(envelope)}\n`);
|
|
109
174
|
} catch { /* stdout closed — the hook is informational and never blocks */ }
|
|
110
175
|
}
|
|
111
176
|
|
|
@@ -349,6 +414,10 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
349
414
|
*
|
|
350
415
|
* Returns the candidate string on success, null on any failure. Never throws.
|
|
351
416
|
*
|
|
417
|
+
* Contract (#1066, "Minimal"): the semantic id is a best-effort, host-wide
|
|
418
|
+
* monotonic LABEL, never an ownership key — ownership stays `(raw session_id,
|
|
419
|
+
* owner proof)`, and the two identity forms are never interchangeable.
|
|
420
|
+
*
|
|
352
421
|
* Used by both branches of resolveSessionId() so the semantic_session_id
|
|
353
422
|
* field on the session.lock is consistently populated regardless of whether
|
|
354
423
|
* the SessionStart stdin payload provided a UUID (Claude Code) or nothing
|
|
@@ -359,7 +428,7 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
359
428
|
*/
|
|
360
429
|
async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
361
430
|
try {
|
|
362
|
-
const { resolveSemanticSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
431
|
+
const { resolveSemanticSessionId, parseSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
363
432
|
const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
|
|
364
433
|
const { execSync } = await import('node:child_process');
|
|
365
434
|
|
|
@@ -387,10 +456,28 @@ async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
|
387
456
|
discoverActiveSessions(projectRoot).catch(() => []),
|
|
388
457
|
readRegistry().catch(() => []),
|
|
389
458
|
]);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
459
|
+
|
|
460
|
+
// Registry census (#1066 AC1): the registry keeps the two identity forms in
|
|
461
|
+
// SEPARATE fields — `session_id` is the RAW id (a UUID on Claude Code) and
|
|
462
|
+
// `semantic_session_id` is the label. resolveSemanticSessionId counts only
|
|
463
|
+
// semantic candidates, so projecting `session_id` alone made the host-wide
|
|
464
|
+
// registry contribute NOTHING to the n-increment, and two sessions on one
|
|
465
|
+
// host could mint the same label. Preference order per entry:
|
|
466
|
+
// 1. `semantic_session_id`, when it parses as semantic;
|
|
467
|
+
// 2. `session_id`, ONLY when it itself parses as semantic (Codex/Cursor
|
|
468
|
+
// may write a semantic raw id).
|
|
469
|
+
// A legacy v1 entry (no `semantic_session_id`, UUID `session_id`) matches
|
|
470
|
+
// neither and is dropped — a UUID is never reintroduced as a candidate.
|
|
471
|
+
const semanticLabelOf = (entry) => {
|
|
472
|
+
for (const candidate of [entry?.semantic_session_id, entry?.session_id]) {
|
|
473
|
+
if (parseSessionId(candidate)?.format === 'semantic') return candidate;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
};
|
|
477
|
+
const registrySessions = registryEntries.flatMap((r) => {
|
|
478
|
+
const label = semanticLabelOf(r);
|
|
479
|
+
return label === null ? [] : [{ sessionId: label, mode: r.mode ?? 'session' }];
|
|
480
|
+
});
|
|
394
481
|
const activeSessions = [...localSessions, ...registrySessions];
|
|
395
482
|
|
|
396
483
|
return await resolveSemanticSessionId({
|
|
@@ -467,6 +554,60 @@ async function emitHostBanner(projectRoot) {
|
|
|
467
554
|
}
|
|
468
555
|
}
|
|
469
556
|
|
|
557
|
+
/**
|
|
558
|
+
* The one-time telemetry-consent instruction handed to the coordinator (#1138).
|
|
559
|
+
*
|
|
560
|
+
* Deliberately terse: it is injected into the model context on EVERY session
|
|
561
|
+
* start until the operator decides, so every character is a recurring cost.
|
|
562
|
+
* It names the phase rather than restating it, so the AUQ wording stays
|
|
563
|
+
* single-sourced in `skills/session-start/SKILL.md` § Phase 6.8.
|
|
564
|
+
*/
|
|
565
|
+
const CONSENT_NUDGE =
|
|
566
|
+
'Telemetrie-Consent ist auf diesem Host unentschieden. Stelle JETZT, vor jeder anderen Arbeit, '
|
|
567
|
+
+ 'genau einmal die consent-neutrale AskUserQuestion aus skills/session-start/SKILL.md § Phase 6.8 '
|
|
568
|
+
+ '(zwei Optionen, keine "(Recommended)"-Markierung; Codex/Cursor: nummerierte Liste) und rufe danach '
|
|
569
|
+
+ 'grantConsent() bzw. denyConsent() aus scripts/lib/telemetry/consent.mjs. Danach nie wieder fragen.';
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* #1138 — queue the consent nudge when, and only when, this host has no
|
|
573
|
+
* telemetry decision on record.
|
|
574
|
+
*
|
|
575
|
+
* WHY A HOOK AT ALL. Phase 6.8 was prose only: it fired when the coordinator
|
|
576
|
+
* happened to reach line ~1060 of a 1230-line skill, after 24 other phases.
|
|
577
|
+
* Measured 2026-08-23: 0 records from any host other than the author's.
|
|
578
|
+
*
|
|
579
|
+
* WHY `isCiEnv` AND NOT `isHeadless`. `isHeadless()` answers "can THIS process
|
|
580
|
+
* prompt on a TTY?" — and this process is a hook whose stdout is a pipe, so it
|
|
581
|
+
* would answer `true` unconditionally and the nudge could never fire. Measured
|
|
582
|
+
* in a plain non-TTY subprocess on this host, 2026-08-23:
|
|
583
|
+
* `isHeadless()=true isCiEnv()=false stdout.isTTY=undefined`
|
|
584
|
+
* The question that actually matters here is a different one: "is there an
|
|
585
|
+
* operator who can answer an AskUserQuestion?" The prompting happens in the
|
|
586
|
+
* live session, not in this process, so TTY-ness of the hook is irrelevant and
|
|
587
|
+
* `isCiEnv()` — the fail-closed "this is unattended automation" probe — is the
|
|
588
|
+
* correct gate. (The same trap sits in the Phase 6.8 prose, which passes
|
|
589
|
+
* `interactive: !isHeadless()` from a `node -e` subprocess whose stdout is also
|
|
590
|
+
* a pipe: it would have resolved `prompt:false` every time even if executed.)
|
|
591
|
+
*
|
|
592
|
+
* `resolveConsent` remains the single decision point: `prompt` is true only for
|
|
593
|
+
* a host with no stored decision, no env override, and no fleet flag.
|
|
594
|
+
*
|
|
595
|
+
* Never throws — a failure here must not affect session start.
|
|
596
|
+
*/
|
|
597
|
+
function maybeQueueConsentNudge() {
|
|
598
|
+
try {
|
|
599
|
+
if (isCiEnv(process.env)) return;
|
|
600
|
+
const consent = resolveConsent({
|
|
601
|
+
env: process.env,
|
|
602
|
+
ownerConfig: loadOwnerConfig().config,
|
|
603
|
+
state: readTelemetryState().record,
|
|
604
|
+
interactive: true,
|
|
605
|
+
});
|
|
606
|
+
if (consent.prompt !== true) return;
|
|
607
|
+
setAdditionalContext(CONSENT_NUDGE);
|
|
608
|
+
} catch { /* best effort — the nudge is informational, never blocks */ }
|
|
609
|
+
}
|
|
610
|
+
|
|
470
611
|
// ---------------------------------------------------------------------------
|
|
471
612
|
// Main
|
|
472
613
|
// ---------------------------------------------------------------------------
|
|
@@ -699,16 +840,74 @@ async function main() {
|
|
|
699
840
|
const allActive = await discoverActiveSessions(projectRoot);
|
|
700
841
|
const mechanicalPeers = allActive.filter((s) => s.sessionId !== sessionId);
|
|
701
842
|
if (mechanicalPeers.length > 0) {
|
|
843
|
+
// #1137 part 1 — say WHERE, not just how many. discoverActiveSessions()
|
|
844
|
+
// walks EVERY path `git worktree list` reports, which includes worktrees
|
|
845
|
+
// another coordinator parked outside this working copy (e.g. under
|
|
846
|
+
// /private/tmp/…). Measured 2026-08-23: all 4 reported "peers" were the
|
|
847
|
+
// 4 registered worktrees, none of them in this checkout — and the old
|
|
848
|
+
// wording ("active in same repo") read as 4 sessions competing for THIS
|
|
849
|
+
// directory. The surface is deliberately unchanged (a foreign-worktree
|
|
850
|
+
// session IS a peer of this repo); only the label and the per-peer
|
|
851
|
+
// detail are, so the operator can tell the two apart at a glance.
|
|
702
852
|
const summary = mechanicalPeers
|
|
703
853
|
.slice(0, 3)
|
|
704
|
-
.map((p) =>
|
|
854
|
+
.map((p) => {
|
|
855
|
+
const where = p.worktreePath ? path.basename(p.worktreePath) : '?';
|
|
856
|
+
// #1151 — carry the peer's MODE when the source recorded one.
|
|
857
|
+
// `worktree:id` says where a peer sits but not what it is doing,
|
|
858
|
+
// and the operator's next decision (wait / promote / proceed)
|
|
859
|
+
// turns on exactly that: a `deep` peer holding this worktree set
|
|
860
|
+
// is a different situation from a `discovery` one. Guarded rather
|
|
861
|
+
// than assumed — sessionFromLock() passes `lock.mode` verbatim, so
|
|
862
|
+
// a lock written without the field yields undefined here, while
|
|
863
|
+
// sessionFromRegistryEntry() always defaults it to 'session'.
|
|
864
|
+
const mode = typeof p.mode === 'string' && p.mode.length > 0 ? `:${p.mode}` : '';
|
|
865
|
+
return `${where}:${shortSessionId(p.sessionId)}${mode}`;
|
|
866
|
+
})
|
|
705
867
|
.join(', ');
|
|
706
868
|
const overflow = mechanicalPeers.length > 3 ? ` +${mechanicalPeers.length - 3} more` : '';
|
|
707
|
-
pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in
|
|
869
|
+
pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in this repo's worktree set (${summary}${overflow})`);
|
|
708
870
|
}
|
|
709
871
|
} catch { /* best effort — banner is informational, never blocks */ }
|
|
710
872
|
}
|
|
711
873
|
|
|
874
|
+
// Phase 4 measurement probes — the mechanical caller (#1128).
|
|
875
|
+
//
|
|
876
|
+
// `skills/session-start/SKILL.md` § Phase 4 names 18 probes with module paths
|
|
877
|
+
// and entry functions. Measured 2026-08-23 at `4f6404e`, not one of them had
|
|
878
|
+
// a caller anywhere in hooks/, npm scripts, .gitlab-ci.yml or .husky/ — the
|
|
879
|
+
// only caller was the prose itself, and across 336 recorded session starts
|
|
880
|
+
// there was no event proving any of them had ever run. Built, documented,
|
|
881
|
+
// never wired.
|
|
882
|
+
//
|
|
883
|
+
// Placement is deliberate: AFTER the backfill (so `sessions-staleness` and
|
|
884
|
+
// `sessions-integrity` measure the reconstructed ledger, not the stale one)
|
|
885
|
+
// and BEFORE flushBanner() (so probe findings ride the single systemMessage
|
|
886
|
+
// envelope instead of adding a second stdout object Claude Code would
|
|
887
|
+
// discard — see the bannerLines docstring and HR-106).
|
|
888
|
+
//
|
|
889
|
+
// Best-effort exactly like `backfillOnSessionStart` above: the runner has no
|
|
890
|
+
// rejecting path of its own, and this try/catch is defence-in-depth so a
|
|
891
|
+
// future regression inside it still cannot block a session start.
|
|
892
|
+
// Escape hatch: SO_DISABLE_STARTUP_PROBES=1.
|
|
893
|
+
//
|
|
894
|
+
// `enable-host-banner: false` silences the DISPLAY, never the MEASUREMENT.
|
|
895
|
+
// The documented opt-out ("a user who silenced session-start banners has
|
|
896
|
+
// opted out of ALL session-start banners") governs `pushBanner` and nothing
|
|
897
|
+
// else: the probes still run and `orchestrator.probes.completed` is still
|
|
898
|
+
// written, because the defect this wiring repairs is precisely that nobody
|
|
899
|
+
// could tell whether the probes had ever run (HR-105). Gating the run on a
|
|
900
|
+
// display preference would rebuild that blind spot behind a config key.
|
|
901
|
+
if (process.env.SO_DISABLE_STARTUP_PROBES !== '1') {
|
|
902
|
+
try {
|
|
903
|
+
const { runSessionStartProbes } = await import('../scripts/lib/session-start-probes.mjs');
|
|
904
|
+
const probeRun = await runSessionStartProbes({ repoRoot: projectRoot });
|
|
905
|
+
if (bannerData) {
|
|
906
|
+
for (const line of probeRun.bannerLines) pushBanner(line);
|
|
907
|
+
}
|
|
908
|
+
} catch { /* hook must remain non-blocking */ }
|
|
909
|
+
}
|
|
910
|
+
|
|
712
911
|
const payload = {
|
|
713
912
|
platform,
|
|
714
913
|
project: projectName,
|
|
@@ -738,6 +937,10 @@ async function main() {
|
|
|
738
937
|
}
|
|
739
938
|
await emitEvent('orchestrator.session.started', payload);
|
|
740
939
|
|
|
940
|
+
// #1138 — one-time telemetry-consent nudge. Queued (never written) here so it
|
|
941
|
+
// rides the single stdout envelope below.
|
|
942
|
+
maybeQueueConsentNudge();
|
|
943
|
+
|
|
741
944
|
// Single flush — see the bannerLines docstring for why this must stay the
|
|
742
945
|
// only stdout write in the hook.
|
|
743
946
|
flushBanner();
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* JSONL format (`.orchestrator/metrics/events.jsonl`) — emitted via the canonical
|
|
17
17
|
* `emitEvent()` so the JSONL record and the optional Clank webhook always carry the
|
|
18
18
|
* SAME dotted event name (was: bare `stop`/`subagent_stop` in JSONL vs dotted in webhook):
|
|
19
|
-
* Stop: {"timestamp":<ISO>,"event":"orchestrator.session.stopped","session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>}
|
|
19
|
+
* Stop: {"timestamp":<ISO>,"event":"orchestrator.session.stopped","session_id":"...","semantic_session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>}
|
|
20
|
+
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.)
|
|
20
21
|
* SubagentStop: {"timestamp":<ISO>,"event":"orchestrator.agent.stopped","agent":"<name>"}
|
|
21
22
|
*/
|
|
22
23
|
|
|
@@ -29,6 +30,7 @@ if (!shouldRunHook('on-stop')) process.exit(0);
|
|
|
29
30
|
|
|
30
31
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
31
32
|
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
33
|
+
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
32
34
|
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
33
35
|
import { updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
34
36
|
|
|
@@ -216,17 +218,39 @@ async function readWaveNumber(projectRoot) {
|
|
|
216
218
|
// ---------------------------------------------------------------------------
|
|
217
219
|
|
|
218
220
|
/**
|
|
219
|
-
* Resolve this session's id
|
|
220
|
-
*
|
|
221
|
-
*
|
|
221
|
+
* Resolve this session's id + its attested semantic id. A stdin payload id wins
|
|
222
|
+
* ONLY when it parses as a UUID. If Claude Code did not pass one (Codex /
|
|
223
|
+
* Cursor paths, or older harnesses) — or passed something that is not a UUID —
|
|
224
|
+
* fall back to `.orchestrator/current-session.json` which on-session-start.mjs
|
|
225
|
+
* writes.
|
|
226
|
+
*
|
|
227
|
+
* #1091 / Kanevry#66 — WRITER/READER SYMMETRY, same defect class as
|
|
228
|
+
* `hooks/on-session-end.mjs`: `on-session-start.mjs` accepts a stdin raw id
|
|
229
|
+
* only when `parseSessionId(fromStdin)?.format === 'uuid'` and otherwise mints
|
|
230
|
+
* a `randomUUID()`, so every artifact this hook then addresses by id (the host
|
|
231
|
+
* registry entry, `session.lock`) is keyed by a UUID. Passing a non-UUID stdin
|
|
232
|
+
* string through meant `heartbeat()` and `updateHeartbeat()` addressed a key
|
|
233
|
+
* that was never written: the registry entry silently went un-refreshed (and
|
|
234
|
+
* aged into the zombie sweep) while the lock heartbeat no-oped on an ownership
|
|
235
|
+
* mismatch.
|
|
236
|
+
*
|
|
237
|
+
* `semanticSessionId` (#1068 AC1) is read from the SAME file and is deliberately
|
|
238
|
+
* gated on the resolved id BEING the recorded one — `current-session.json` is a
|
|
239
|
+
* single repo-global file describing whichever session most recently ran
|
|
240
|
+
* SessionStart, so an unrelated window's turn-end must not inherit a live
|
|
241
|
+
* peer's semantic identity (the #863 defect (c) contamination shape, mirrored
|
|
242
|
+
* here from `hooks/on-session-end.mjs`).
|
|
222
243
|
*
|
|
223
244
|
* @param {object|null} input
|
|
224
245
|
* @param {string} projectRoot
|
|
225
|
-
* @returns {Promise<string|null>}
|
|
246
|
+
* @returns {Promise<{sessionId: string|null, semanticSessionId: string|null}>}
|
|
226
247
|
*/
|
|
227
248
|
async function resolveSessionId(input, projectRoot) {
|
|
228
249
|
const fromStdin = input?.session_id ?? input?.sessionId ?? null;
|
|
229
|
-
|
|
250
|
+
let sessionId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
|
|
251
|
+
|
|
252
|
+
let recordedId = null;
|
|
253
|
+
let semanticSessionId = null;
|
|
230
254
|
try {
|
|
231
255
|
const raw = await fs.readFile(
|
|
232
256
|
path.join(projectRoot, '.orchestrator', 'current-session.json'),
|
|
@@ -234,10 +258,20 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
234
258
|
);
|
|
235
259
|
const parsed = JSON.parse(raw);
|
|
236
260
|
if (typeof parsed.session_id === 'string' && parsed.session_id.length > 0) {
|
|
237
|
-
|
|
261
|
+
recordedId = parsed.session_id;
|
|
262
|
+
}
|
|
263
|
+
if (typeof parsed.semantic_session_id === 'string' && parsed.semantic_session_id.length > 0) {
|
|
264
|
+
semanticSessionId = parsed.semantic_session_id;
|
|
238
265
|
}
|
|
239
266
|
} catch { /* missing or unparseable is fine */ }
|
|
240
|
-
|
|
267
|
+
|
|
268
|
+
if (sessionId === null) sessionId = recordedId;
|
|
269
|
+
|
|
270
|
+
const isRecordedSession = sessionId !== null && sessionId === recordedId;
|
|
271
|
+
return {
|
|
272
|
+
sessionId,
|
|
273
|
+
semanticSessionId: isRecordedSession ? semanticSessionId : null,
|
|
274
|
+
};
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
/**
|
|
@@ -250,7 +284,7 @@ async function handleStop(input) {
|
|
|
250
284
|
const wave = await readWaveNumber(projectRoot);
|
|
251
285
|
const { commit, branch } = await gitInfo(projectRoot);
|
|
252
286
|
|
|
253
|
-
const sessionId = await resolveSessionId(input, projectRoot);
|
|
287
|
+
const { sessionId, semanticSessionId } = await resolveSessionId(input, projectRoot);
|
|
254
288
|
|
|
255
289
|
// v3.1.0 multi-session registry (#169), corrected in #1047 — REFRESH the
|
|
256
290
|
// registry entry here; never remove it.
|
|
@@ -323,8 +357,13 @@ async function handleStop(input) {
|
|
|
323
357
|
// Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
|
|
324
358
|
// JSONL record AND fires the optional Clank webhook with the SAME event name — no
|
|
325
359
|
// more bare-`stop` (JSONL) vs dotted-`stopped` (webhook) divergence.
|
|
360
|
+
// #1068 AC1 — carry the attested semantic id alongside the raw UUID so a
|
|
361
|
+
// turn-end outcome is joinable by identity from events.jsonl alone. Omitted
|
|
362
|
+
// (never `""`/`null`) when unattested: an unresolved identity stays visibly
|
|
363
|
+
// unresolved rather than becoming a guessed id.
|
|
326
364
|
await emitEvent('orchestrator.session.stopped', {
|
|
327
365
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
366
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
328
367
|
wave,
|
|
329
368
|
...(branch !== null ? { branch } : {}),
|
|
330
369
|
...(commit !== null ? { commit } : {}),
|
|
@@ -30,6 +30,19 @@
|
|
|
30
30
|
* Why read the transcript: the SubagentStop stdin payload has NO output_text
|
|
31
31
|
* field. The agent's text lives in `input.transcript_path`.
|
|
32
32
|
*
|
|
33
|
+
* Output channels — THREE writes, TWO different recipients:
|
|
34
|
+
* - `discovery_validator_violation` in .orchestrator/metrics/events.jsonl,
|
|
35
|
+
* and the stderr WARN → the COORDINATOR's only copy of the finding. PSA-006
|
|
36
|
+
* makes REJECTING the unverified claim his duty, so these two are the
|
|
37
|
+
* channels that carry the rule's enforcement path.
|
|
38
|
+
* - stdout `hookSpecificOutput.additionalContext` → the STOPPING SUBAGENT
|
|
39
|
+
* (which continues and may self-correct), NOT the coordinator — a hook is
|
|
40
|
+
* structurally unable to address him. See the measured delivery note at the
|
|
41
|
+
* hookSpecificOutput write below for the shipped-binary evidence.
|
|
42
|
+
* All three are pinned together by the "all three channels fire" test in
|
|
43
|
+
* tests/hooks/post-subagent-discovery-validator.test.mjs — do not collapse
|
|
44
|
+
* the coordinator-visible pair into additionalContext.
|
|
45
|
+
*
|
|
33
46
|
* Exit codes: 0 always (informational, never blocking).
|
|
34
47
|
*/
|
|
35
48
|
|
|
@@ -594,12 +607,30 @@ async function main() {
|
|
|
594
607
|
}
|
|
595
608
|
|
|
596
609
|
if (alreadyWarned) {
|
|
597
|
-
// Events logged above; suppress
|
|
610
|
+
// Events logged above; suppress the repeat additionalContext for this
|
|
611
|
+
// already-warned real context. (Recipient is the stopping subagent, not the
|
|
612
|
+
// coordinator — see the delivery note at the hookSpecificOutput write below.)
|
|
598
613
|
return;
|
|
599
614
|
}
|
|
600
615
|
|
|
601
|
-
// v2.1.163+ additionalContext:
|
|
602
|
-
//
|
|
616
|
+
// v2.1.163+ additionalContext: surface the finding inline, not just in
|
|
617
|
+
// stderr + events.jsonl.
|
|
618
|
+
//
|
|
619
|
+
// The recipient is the SUBAGENT that just stopped — NOT the coordinator.
|
|
620
|
+
// Measured in the shipped binary (Claude Code 2.1.241, 2026-08-23): the
|
|
621
|
+
// SubagentStop hookSpecificOutput schema reads "additionalContext is
|
|
622
|
+
// non-error feedback delivered to the subagent; the subagent continues so it
|
|
623
|
+
// can act on it", and the emitter picks its target with
|
|
624
|
+
// `i.agentId ? "SubagentStop" : "Stop"`, yielding a `hook_additional_context`
|
|
625
|
+
// message into that agent's own loop. So this text lands in the transcript of
|
|
626
|
+
// the agent whose claims it is about, one moment after that agent is done.
|
|
627
|
+
//
|
|
628
|
+
// Worth knowing before "fixing" the path: PSA-006 asks the COORDINATOR to
|
|
629
|
+
// reject unverified distributional claims, and this channel never reaches
|
|
630
|
+
// him. His copy of the finding is events.jsonl + stderr, not this write.
|
|
631
|
+
// Whether he SHOULD receive it is a product question (#1116) — changing the
|
|
632
|
+
// route is not a comment edit.
|
|
633
|
+
//
|
|
603
634
|
// Non-blocking — exit 0 always. Decision:"block" must never be set here.
|
|
604
635
|
process.stdout.write(JSON.stringify({
|
|
605
636
|
hookSpecificOutput: {
|
|
@@ -41,6 +41,7 @@ if (!shouldRunHook('post-tool-batch-wave-signal')) process.exit(0);
|
|
|
41
41
|
|
|
42
42
|
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
43
43
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
44
|
+
import { findScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
44
45
|
|
|
45
46
|
// ---------------------------------------------------------------------------
|
|
46
47
|
// Helpers
|
|
@@ -135,17 +136,25 @@ async function resolveSessionIdForHeartbeat(input, sessionFile) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* Resolve the current wave number from
|
|
139
|
+
* Resolve the current wave number from the wave-scope manifest's `.wave`.
|
|
139
140
|
* Returns 0 when the file is absent or unparseable, mirroring the
|
|
140
141
|
* pre-bash-memory-propose-audit.mjs G5 precedent ("wave defaults to 0 when
|
|
141
142
|
* wave-scope.json absent"). The file is deleted mid-session at Quality phase
|
|
142
143
|
* transitions and final cleanup, so absence is an expected, non-error state.
|
|
143
144
|
*
|
|
145
|
+
* The manifest is located via `findScopeFile()` (#1082), the same precedence
|
|
146
|
+
* every other consumer uses — `.pi` > `.cursor` > `.codex` > `.claude`. The
|
|
147
|
+
* previous hard-coded `.claude/` path made this hook's whole wave-lifecycle
|
|
148
|
+
* fallback structurally dead on Codex CLI, Cursor and pi: the manifest exists,
|
|
149
|
+
* it just is not under `.claude/`, so every batch read 0 and no
|
|
150
|
+
* `orchestrator.wave.started`/`completed` event was ever emitted there.
|
|
151
|
+
*
|
|
144
152
|
* @param {string} projectDir
|
|
145
153
|
* @returns {Promise<number>}
|
|
146
154
|
*/
|
|
147
155
|
async function resolveWaveNumber(projectDir) {
|
|
148
|
-
const waveFile =
|
|
156
|
+
const waveFile = findScopeFile(projectDir);
|
|
157
|
+
if (waveFile === null) return 0;
|
|
149
158
|
try {
|
|
150
159
|
const raw = await readFile(waveFile, 'utf8');
|
|
151
160
|
const data = JSON.parse(raw);
|