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
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
32
|
+
import { writeSync } from 'node:fs';
|
|
32
33
|
import { execFile } from 'node:child_process';
|
|
33
34
|
import { promisify } from 'node:util';
|
|
34
35
|
import { randomUUID } from 'node:crypto';
|
|
@@ -47,11 +48,132 @@ import {
|
|
|
47
48
|
detectPeers,
|
|
48
49
|
sweepZombies,
|
|
49
50
|
logSweepEvent,
|
|
51
|
+
repoPathHash,
|
|
50
52
|
} from '../scripts/lib/session-registry.mjs';
|
|
51
53
|
import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detector.mjs';
|
|
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';
|
|
52
57
|
|
|
53
58
|
const execFileAsync = promisify(execFile);
|
|
54
59
|
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Banner buffer (#1089 / #1052 A3a)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
//
|
|
64
|
+
// MEASURED 2026-08-21: this hook emitted FIVE independent
|
|
65
|
+
// `console.log(JSON.stringify({ systemMessage }))` calls, and Claude Code
|
|
66
|
+
// surfaces only the FIRST JSON object a SessionStart hook writes to stdout.
|
|
67
|
+
// A live run against a copied 5-peer registry produced four stdout lines of
|
|
68
|
+
// which the operator saw exactly one — the host/resource line. The three that
|
|
69
|
+
// were silently discarded included:
|
|
70
|
+
//
|
|
71
|
+
// ⚠️ 👥 Peers: 5 active (… session-orchestrator:main:wave-0 …)
|
|
72
|
+
// 🔍 Mechanical peer-detection: 1 active in same repo (…)
|
|
73
|
+
//
|
|
74
|
+
// i.e. the two lines whose entire job is to warn that ANOTHER SESSION HOLDS
|
|
75
|
+
// THIS WORKING COPY. The information was computed correctly, formatted
|
|
76
|
+
// correctly, and thrown away by the transport — the "built but not wired"
|
|
77
|
+
// class, with the wiring defect one layer below where anyone was looking.
|
|
78
|
+
//
|
|
79
|
+
// The fix is structural: every banner line goes into one buffer and leaves as
|
|
80
|
+
// ONE systemMessage. Adding a sixth banner in future therefore cannot
|
|
81
|
+
// re-introduce the bug.
|
|
82
|
+
const bannerLines = [];
|
|
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
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Queue one or more banner lines for the single end-of-hook flush.
|
|
99
|
+
* @param {string|null|undefined} line — multi-line strings are pushed verbatim
|
|
100
|
+
*/
|
|
101
|
+
function pushBanner(line) {
|
|
102
|
+
if (typeof line !== 'string' || line.length === 0) return;
|
|
103
|
+
bannerLines.push(line);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
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.
|
|
147
|
+
*
|
|
148
|
+
* Uses `fs.writeSync(1, …)` rather than `console.log`: the top-level guard
|
|
149
|
+
* calls `process.exit(0)`, which discards anything still sitting in libuv's
|
|
150
|
+
* async write queue when stdout is a pipe (see
|
|
151
|
+
* `.claude/rules/anti-pattern-console-log-process-exit-drops-stdout…`). The
|
|
152
|
+
* banner is far below the 64 KiB pipe buffer today, but a synchronous write
|
|
153
|
+
* costs nothing and removes the failure mode rather than staying under it.
|
|
154
|
+
*
|
|
155
|
+
* Idempotent: a second call after a flush is a no-op.
|
|
156
|
+
*/
|
|
157
|
+
function flushBanner() {
|
|
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
|
+
}
|
|
170
|
+
bannerLines.length = 0;
|
|
171
|
+
pendingAdditionalContext = null;
|
|
172
|
+
try {
|
|
173
|
+
writeSync(1, `${JSON.stringify(envelope)}\n`);
|
|
174
|
+
} catch { /* stdout closed — the hook is informational and never blocks */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
55
177
|
// ---------------------------------------------------------------------------
|
|
56
178
|
// Helpers
|
|
57
179
|
// ---------------------------------------------------------------------------
|
|
@@ -191,92 +313,43 @@ async function readStdinJson(timeoutMs = 500) {
|
|
|
191
313
|
*/
|
|
192
314
|
async function resolveSessionId(input, projectRoot) {
|
|
193
315
|
const fromStdin = (input && (input.session_id || input.sessionId)) ?? null;
|
|
316
|
+
const parsedStdinId = parseSessionId(fromStdin);
|
|
317
|
+
const rawStdinSessionId = parsedStdinId?.format === 'uuid' ? fromStdin : null;
|
|
194
318
|
|
|
195
|
-
// Mode normalization is shared by both branches
|
|
196
|
-
//
|
|
197
|
-
//
|
|
319
|
+
// Mode normalization is shared by both branches so semantic attribution is
|
|
320
|
+
// derived independently of the raw session identity. A semantic id is always
|
|
321
|
+
// descriptive metadata, never the physical lock/registry session_id.
|
|
198
322
|
const rawMode = (input && (input.mode || input.session_type)) || 'session';
|
|
199
323
|
const normalizedMode =
|
|
200
324
|
String(rawMode).toLowerCase().replace(/[^a-z-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'session';
|
|
201
325
|
|
|
202
326
|
let sessionId;
|
|
203
327
|
let source;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// when Claude Code's stdin payload provided a UUID-v4. Defaults to null
|
|
207
|
-
// and is populated below; the lock-bootstrap helper falls back to mirroring
|
|
208
|
-
// sessionId when this stays null.
|
|
328
|
+
// The lock and downstream consumers surface this independently-derived
|
|
329
|
+
// semantic attribution alongside the physical raw session id.
|
|
209
330
|
let semanticSessionId = null;
|
|
210
331
|
|
|
211
|
-
if (
|
|
212
|
-
sessionId =
|
|
332
|
+
if (rawStdinSessionId !== null) {
|
|
333
|
+
sessionId = rawStdinSessionId;
|
|
213
334
|
source = 'stdin';
|
|
214
|
-
|
|
215
|
-
// D4 #587: even on the stdin-UUID path, ALSO compute a semantic
|
|
216
|
-
// session-id so the lock and registry can surface it downstream.
|
|
217
|
-
// Best-effort: any failure leaves semanticSessionId = null which is
|
|
218
|
-
// handled gracefully by the lock-bootstrap helper.
|
|
219
|
-
try {
|
|
220
|
-
const semCandidate = await deriveSemanticCandidate({
|
|
221
|
-
projectRoot,
|
|
222
|
-
mode: normalizedMode,
|
|
223
|
-
});
|
|
224
|
-
if (semCandidate) semanticSessionId = semCandidate;
|
|
225
|
-
} catch { /* best effort — leave semanticSessionId = null */ }
|
|
226
335
|
} else {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
mode: normalizedMode,
|
|
233
|
-
});
|
|
234
|
-
if (!candidate) throw new Error('semantic-derivation-empty');
|
|
235
|
-
|
|
236
|
-
// Race-free host-wide uniqueness: try to atomically claim the registry slot
|
|
237
|
-
// for this sessionId via O_CREAT|O_EXCL. If another process won the race
|
|
238
|
-
// (EEXIST), fall back to UUID-v4 to guarantee distinct sessionIds across
|
|
239
|
-
// parallel hook invocations. The empty file is immediately overwritten by
|
|
240
|
-
// registerSelf() with the real entry — _validEntry() in readers filters
|
|
241
|
-
// any zero-content artifact during the millisecond gap.
|
|
242
|
-
try {
|
|
243
|
-
// entryPath() is NOT exported from session-registry.mjs (it is an
|
|
244
|
-
// internal helper); replicate its body inline so the race-free slot
|
|
245
|
-
// claim actually succeeds. Without this inline build, the
|
|
246
|
-
// destructuring on `entryPath` returns undefined and calling it
|
|
247
|
-
// throws TypeError → silent UUID-fallback, defeating the semantic-id
|
|
248
|
-
// generation path entirely. Pre-existing bug pre-dating Epic #583 P3.
|
|
249
|
-
const { activeDir } = await import('../scripts/lib/session-registry.mjs');
|
|
250
|
-
const fsAsync = await import('node:fs/promises');
|
|
251
|
-
await fsAsync.mkdir(activeDir(), { recursive: true });
|
|
252
|
-
const claimPath = path.join(activeDir(), `${candidate}.json`);
|
|
253
|
-
const handle = await fsAsync.open(claimPath, 'wx'); // O_CREAT|O_EXCL — fails with EEXIST on collision
|
|
254
|
-
await handle.close();
|
|
255
|
-
sessionId = candidate;
|
|
256
|
-
semanticSessionId = candidate;
|
|
257
|
-
source = 'generated-semantic';
|
|
258
|
-
} catch (claimErr) {
|
|
259
|
-
if (claimErr.code === 'EEXIST') {
|
|
260
|
-
// Collision with another parallel hook — fall back to UUID-v4 silently.
|
|
261
|
-
// Hook is informational-only; never write to stderr per existing convention.
|
|
262
|
-
sessionId = randomUUID();
|
|
263
|
-
source = 'generated-uuid-fallback-collision';
|
|
264
|
-
// We still surface the semantic candidate as semanticSessionId so the
|
|
265
|
-
// lock reflects what this session would have been called — useful for
|
|
266
|
-
// forensics even when the UUID won the registry-slot race.
|
|
267
|
-
semanticSessionId = candidate;
|
|
268
|
-
} else {
|
|
269
|
-
throw claimErr; // surfaces to outer catch → UUID fallback
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} catch {
|
|
273
|
-
// Semantic id generation failed for any reason — silently fall back to UUID-v4.
|
|
274
|
-
// Hook is informational-only; never write to stderr per existing convention.
|
|
275
|
-
sessionId = randomUUID();
|
|
276
|
-
source = 'generated-uuid-fallback';
|
|
277
|
-
}
|
|
336
|
+
// A missing, malformed, or semantic stdin value is not a trustworthy raw
|
|
337
|
+
// identity. Generate the physical id locally; semantic derivation below is
|
|
338
|
+
// attribution only and must never become the lock/registry key.
|
|
339
|
+
sessionId = randomUUID();
|
|
340
|
+
source = 'generated-uuid';
|
|
278
341
|
}
|
|
279
342
|
|
|
343
|
+
// Derive a descriptive semantic label for either raw-id source. Best-effort:
|
|
344
|
+
// a failure leaves it null without changing the physical raw session_id.
|
|
345
|
+
try {
|
|
346
|
+
const semCandidate = await deriveSemanticCandidate({
|
|
347
|
+
projectRoot,
|
|
348
|
+
mode: normalizedMode,
|
|
349
|
+
});
|
|
350
|
+
if (semCandidate) semanticSessionId = semCandidate;
|
|
351
|
+
} catch { /* best effort — leave semanticSessionId = null */ }
|
|
352
|
+
|
|
280
353
|
try {
|
|
281
354
|
const dir = path.join(projectRoot, '.orchestrator');
|
|
282
355
|
await mkdir(dir, { recursive: true });
|
|
@@ -341,6 +414,10 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
341
414
|
*
|
|
342
415
|
* Returns the candidate string on success, null on any failure. Never throws.
|
|
343
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
|
+
*
|
|
344
421
|
* Used by both branches of resolveSessionId() so the semantic_session_id
|
|
345
422
|
* field on the session.lock is consistently populated regardless of whether
|
|
346
423
|
* the SessionStart stdin payload provided a UUID (Claude Code) or nothing
|
|
@@ -351,7 +428,7 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
351
428
|
*/
|
|
352
429
|
async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
353
430
|
try {
|
|
354
|
-
const { resolveSemanticSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
431
|
+
const { resolveSemanticSessionId, parseSessionId } = await import('../scripts/lib/session-id.mjs');
|
|
355
432
|
const { discoverActiveSessions } = await import('../scripts/lib/session-discovery.mjs');
|
|
356
433
|
const { execSync } = await import('node:child_process');
|
|
357
434
|
|
|
@@ -379,10 +456,28 @@ async function deriveSemanticCandidate({ projectRoot, mode }) {
|
|
|
379
456
|
discoverActiveSessions(projectRoot).catch(() => []),
|
|
380
457
|
readRegistry().catch(() => []),
|
|
381
458
|
]);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
});
|
|
386
481
|
const activeSessions = [...localSessions, ...registrySessions];
|
|
387
482
|
|
|
388
483
|
return await resolveSemanticSessionId({
|
|
@@ -415,15 +510,43 @@ async function emitHostBanner(projectRoot) {
|
|
|
415
510
|
if (!host || !resources) return null;
|
|
416
511
|
|
|
417
512
|
const hostLine = `🖥️ Host: ${host.host_class} · ${host.ram_total_gb} GB RAM · ${host.platform ?? 'unknown'} · ${host.is_ssh ? 'ssh' : 'local'}`;
|
|
418
|
-
|
|
513
|
+
|
|
514
|
+
// #1089: report the memory number a human can ACT on.
|
|
515
|
+
//
|
|
516
|
+
// The old line printed `ram_free_gb`, which on Darwin is `os.freemem()` =
|
|
517
|
+
// `Pages free` only. Median across 1477 measured session starts: 0.4 GB —
|
|
518
|
+
// on hosts with 24-128 GB installed. So this banner spent four months
|
|
519
|
+
// telling the operator the machine was seconds from death while the same
|
|
520
|
+
// machine reported 40%+ memory free and ran full waves clean. Six repos
|
|
521
|
+
// logged that false alarm as a learning; one capped agents for five
|
|
522
|
+
// consecutive sessions off this number.
|
|
523
|
+
//
|
|
524
|
+
// Precedence mirrors evaluate()'s memorySignal(): pressure > available >
|
|
525
|
+
// free. `free` survives only as the last resort — on Linux/Windows it is
|
|
526
|
+
// genuinely accurate, which is exactly where nothing better is published.
|
|
527
|
+
const memLine = (() => {
|
|
528
|
+
if (resources.memory_pressure_pct_free !== null && resources.memory_pressure_pct_free !== undefined) {
|
|
529
|
+
return `${resources.memory_pressure_pct_free}% memory free (OS pressure)`;
|
|
530
|
+
}
|
|
531
|
+
if (resources.ram_available_gb !== null && resources.ram_available_gb !== undefined) {
|
|
532
|
+
return `${resources.ram_available_gb.toFixed(1)} GB available`;
|
|
533
|
+
}
|
|
534
|
+
return `${resources.ram_free_gb.toFixed(1)} GB free`;
|
|
535
|
+
})();
|
|
536
|
+
|
|
537
|
+
// Peer SESSIONS, not Claude PROCESSES. Measured ratio 6.0:1 — the old
|
|
538
|
+
// suffix read "17 Claude processes running" on a host carrying 3 sessions,
|
|
539
|
+
// which is alarming and means nothing actionable.
|
|
540
|
+
const peerSuffix = resources.peer_sessions_count === null || resources.peer_sessions_count === undefined
|
|
419
541
|
? ''
|
|
420
|
-
: ` · ${resources.
|
|
421
|
-
const resourceLine = `📊 Resources: ${
|
|
422
|
-
const banner = `${hostLine}\n${resourceLine}`;
|
|
542
|
+
: ` · ${resources.peer_sessions_count} peer session${resources.peer_sessions_count === 1 ? '' : 's'}`;
|
|
543
|
+
const resourceLine = `📊 Resources: ${memLine} · CPU ${resources.cpu_load_pct}%${peerSuffix}`;
|
|
423
544
|
|
|
424
|
-
// systemMessage envelope is the Claude Code hook contract;
|
|
425
|
-
//
|
|
426
|
-
|
|
545
|
+
// systemMessage envelope is the Claude Code hook contract; buffered so all
|
|
546
|
+
// banner lines leave as ONE object (see flushBanner — Claude Code reads
|
|
547
|
+
// only the first).
|
|
548
|
+
pushBanner(hostLine);
|
|
549
|
+
pushBanner(resourceLine);
|
|
427
550
|
|
|
428
551
|
return { host, resources };
|
|
429
552
|
} catch {
|
|
@@ -431,6 +554,60 @@ async function emitHostBanner(projectRoot) {
|
|
|
431
554
|
}
|
|
432
555
|
}
|
|
433
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
|
+
|
|
434
611
|
// ---------------------------------------------------------------------------
|
|
435
612
|
// Main
|
|
436
613
|
// ---------------------------------------------------------------------------
|
|
@@ -464,11 +641,7 @@ async function main() {
|
|
|
464
641
|
// Config / STATE.md / git, and only then the tool. The banner carries the
|
|
465
642
|
// ORDER, not an absolute; full routing + exceptions in
|
|
466
643
|
// .claude/rules/ask-via-tool.md.
|
|
467
|
-
|
|
468
|
-
console.log(JSON.stringify({
|
|
469
|
-
systemMessage: '🎯 Decide: operator verb (/go) > derive+report > AUQ if blocking (.claude/rules/ask-via-tool.md).',
|
|
470
|
-
}));
|
|
471
|
-
} catch { /* best effort */ }
|
|
644
|
+
pushBanner('🎯 Decide: operator verb (/go) > derive+report > AUQ if blocking (.claude/rules/ask-via-tool.md).');
|
|
472
645
|
}
|
|
473
646
|
|
|
474
647
|
// F1.3 cold-start abandonment fix (PRD 2026-05-21). Emit a one-shot
|
|
@@ -490,11 +663,7 @@ async function main() {
|
|
|
490
663
|
enabled: coldStartCfg.enabled !== false,
|
|
491
664
|
});
|
|
492
665
|
if (decision.shouldEmit) {
|
|
493
|
-
|
|
494
|
-
console.log(JSON.stringify({
|
|
495
|
-
systemMessage: decision.bannerLines.join('\n'),
|
|
496
|
-
}));
|
|
497
|
-
} catch { /* best effort — stdout may be closed */ }
|
|
666
|
+
pushBanner(decision.bannerLines.join('\n'));
|
|
498
667
|
if (decision.markerPath) {
|
|
499
668
|
await consumeMarker(decision.markerPath).catch(() => false);
|
|
500
669
|
}
|
|
@@ -592,17 +761,61 @@ async function main() {
|
|
|
592
761
|
// Append a peer line to the host banner when a banner was already emitted
|
|
593
762
|
// and at least one peer is live on this host.
|
|
594
763
|
if (bannerData && peers.length > 0) {
|
|
764
|
+
// #1052 A3a — split peers by the axis that actually decides behaviour.
|
|
765
|
+
//
|
|
766
|
+
// `.claude/rules/parallel-sessions.md` defines the operator-session axis by
|
|
767
|
+
// the WORKING COPY, not by reachability: a peer in this checkout contends
|
|
768
|
+
// for one git index, one filesystem, one STATE.md, and can hold the
|
|
769
|
+
// wave-scope guard. A peer in another repo contends only for host capacity.
|
|
770
|
+
// Those are different problems and the old single-line summary blurred them
|
|
771
|
+
// into one comma-separated list sorted by nothing.
|
|
772
|
+
//
|
|
773
|
+
// Correlate on `repo_path_hash` rather than `repo_name`: a sibling worktree
|
|
774
|
+
// of this repo carries a DIFFERENT working copy (and a different hash) while
|
|
775
|
+
// often sharing a similar name, and it is the checkout — not the name — that
|
|
776
|
+
// can collide. Falls back to name comparison only if the hash is missing.
|
|
777
|
+
let selfRepoHash = null;
|
|
778
|
+
try { selfRepoHash = repoPathHash(projectRoot); } catch { /* best effort */ }
|
|
779
|
+
const sameCopy = peers.filter((p) =>
|
|
780
|
+
selfRepoHash && p.repo_path_hash ? p.repo_path_hash === selfRepoHash : false);
|
|
781
|
+
const elsewhere = peers.filter((p) => !sameCopy.includes(p));
|
|
782
|
+
|
|
595
783
|
const threshold = await peerWarnThreshold(projectRoot);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
784
|
+
// WARN on any peer in THIS working copy — one is already enough to collide
|
|
785
|
+
// — or on host-wide count crossing the configured threshold.
|
|
786
|
+
const icon = sameCopy.length > 0 || peers.length >= threshold ? '⚠️ ' : '';
|
|
787
|
+
|
|
788
|
+
// Semantic ids are NOT unique — several live sessions routinely share e.g.
|
|
789
|
+
// `main-2026-08-21-session-2`, which makes a bare semantic label unusable
|
|
790
|
+
// for addressing a specific peer. Append a short uuid discriminator so two
|
|
791
|
+
// rows can be told apart (and so the id can be pasted into a lookup).
|
|
792
|
+
const fmt = (p) => {
|
|
793
|
+
const label = p.semantic_session_id ?? p.session_id ?? 'unknown';
|
|
794
|
+
const disc = p.session_id ? `#${String(p.session_id).slice(0, 8)}` : '';
|
|
795
|
+
return `${label}${disc}:${p.branch ?? 'unknown'}:wave-${p.current_wave ?? 0}`;
|
|
796
|
+
};
|
|
797
|
+
pushBanner(`${icon}👥 Peers: ${peers.length} live on this host`);
|
|
798
|
+
if (sameCopy.length > 0) {
|
|
799
|
+
const list = sameCopy.slice(0, 3).map(fmt).join(', ');
|
|
800
|
+
const more = sameCopy.length > 3 ? ` +${sameCopy.length - 3} more` : '';
|
|
801
|
+
// This is the line that matters. A peer here means PSA-002 territory:
|
|
802
|
+
// it may hold .claude/wave-scope.json, carry uncommitted work in files
|
|
803
|
+
// you are about to edit, or switch the branch under you.
|
|
804
|
+
pushBanner(` ⚠️ ${sameCopy.length} in THIS working copy — coordinate before editing (${list}${more})`);
|
|
805
|
+
}
|
|
806
|
+
if (elsewhere.length > 0) {
|
|
807
|
+
const list = elsewhere
|
|
808
|
+
.slice(0, 4)
|
|
809
|
+
.map((p) => `${p.repo_name ?? 'unknown'}:${p.branch ?? 'unknown'}`)
|
|
810
|
+
.join(', ');
|
|
811
|
+
const more = elsewhere.length > 4 ? ` +${elsewhere.length - 4} more` : '';
|
|
812
|
+
pushBanner(` other repos (host capacity only): ${list}${more}`);
|
|
813
|
+
}
|
|
814
|
+
// #1052 A3a acceptance criterion: state the limit, never imply a status we
|
|
815
|
+
// cannot read. `ListAgents` is a MODEL-side tool; this hook is a Node
|
|
816
|
+
// process and structurally cannot call it, so busy/waiting/idle is
|
|
817
|
+
// unavailable here. The coordinator half (A3b) overlays it later.
|
|
818
|
+
pushBanner(' (registry view: repo/branch/wave. Liveness (busy/idle) is model-side — not available in this hook.)');
|
|
606
819
|
}
|
|
607
820
|
|
|
608
821
|
// Epic #583 W3-P3 — Mechanical peer-detection banner (independent of the
|
|
@@ -627,19 +840,74 @@ async function main() {
|
|
|
627
840
|
const allActive = await discoverActiveSessions(projectRoot);
|
|
628
841
|
const mechanicalPeers = allActive.filter((s) => s.sessionId !== sessionId);
|
|
629
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.
|
|
630
852
|
const summary = mechanicalPeers
|
|
631
853
|
.slice(0, 3)
|
|
632
|
-
.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
|
+
})
|
|
633
867
|
.join(', ');
|
|
634
868
|
const overflow = mechanicalPeers.length > 3 ? ` +${mechanicalPeers.length - 3} more` : '';
|
|
635
|
-
|
|
636
|
-
try {
|
|
637
|
-
console.log(JSON.stringify({ systemMessage: mechanicalLine }));
|
|
638
|
-
} catch { /* best effort */ }
|
|
869
|
+
pushBanner(`🔍 Mechanical peer-detection: ${mechanicalPeers.length} active in this repo's worktree set (${summary}${overflow})`);
|
|
639
870
|
}
|
|
640
871
|
} catch { /* best effort — banner is informational, never blocks */ }
|
|
641
872
|
}
|
|
642
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
|
+
|
|
643
911
|
const payload = {
|
|
644
912
|
platform,
|
|
645
913
|
project: projectName,
|
|
@@ -652,9 +920,31 @@ async function main() {
|
|
|
652
920
|
payload.ram_free_gb = bannerData.resources.ram_free_gb;
|
|
653
921
|
payload.cpu_load_pct = bannerData.resources.cpu_load_pct;
|
|
654
922
|
payload.claude_processes_count = bannerData.resources.claude_processes_count;
|
|
923
|
+
// #1089 — record the signals the verdict is actually computed from, so the
|
|
924
|
+
// firing rate of each rule class is measurable AFTER the fact.
|
|
925
|
+
//
|
|
926
|
+
// This is the defect that let the old rule set fire on 99.0% of starts for
|
|
927
|
+
// four months undetected: `resource_verdict` was written to sessions.jsonl
|
|
928
|
+
// for exactly 15 of 1734 sessions, all inside one week in April 2026, and
|
|
929
|
+
// the fields logged here were the two MISLEADING ones (`ram_free_gb`,
|
|
930
|
+
// `claude_processes_count`) — so even the surviving telemetry could not
|
|
931
|
+
// have falsified the thresholds. The three added below are the ones the
|
|
932
|
+
// rules now judge on. `.claude/rules/host-resources.md` HR-005 turns them
|
|
933
|
+
// into a standing 10%-firing-rate audit.
|
|
934
|
+
payload.ram_available_gb = bannerData.resources.ram_available_gb ?? null;
|
|
935
|
+
payload.memory_pressure_pct_free = bannerData.resources.memory_pressure_pct_free ?? null;
|
|
936
|
+
payload.peer_sessions_count = bannerData.resources.peer_sessions_count ?? null;
|
|
655
937
|
}
|
|
656
938
|
await emitEvent('orchestrator.session.started', payload);
|
|
657
939
|
|
|
940
|
+
// #1138 — one-time telemetry-consent nudge. Queued (never written) here so it
|
|
941
|
+
// rides the single stdout envelope below.
|
|
942
|
+
maybeQueueConsentNudge();
|
|
943
|
+
|
|
944
|
+
// Single flush — see the bannerLines docstring for why this must stay the
|
|
945
|
+
// only stdout write in the hook.
|
|
946
|
+
flushBanner();
|
|
947
|
+
|
|
658
948
|
// Size-based rotation of events.jsonl (#251). Session-start is the single
|
|
659
949
|
// rotation trigger — per-append overhead is rejected design. Any failure
|
|
660
950
|
// is swallowed: rotation must NEVER block the hook.
|
|
@@ -681,6 +971,10 @@ async function main() {
|
|
|
681
971
|
}
|
|
682
972
|
|
|
683
973
|
// Top-level guard — always exit 0 (non-blocking informational hook).
|
|
974
|
+
// flushBanner() runs here too so a throw partway through main() still surfaces
|
|
975
|
+
// whatever was already collected; it is idempotent, so the normal path (which
|
|
976
|
+
// flushes at the end of main) does not double-emit.
|
|
684
977
|
main().catch(() => {}).finally(() => {
|
|
978
|
+
flushBanner();
|
|
685
979
|
process.exit(0);
|
|
686
980
|
});
|