session-orchestrator 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readdirSync, existsSync } from 'node:fs';
|
|
32
|
-
import os from 'node:os';
|
|
33
32
|
import path from 'node:path';
|
|
34
33
|
|
|
35
34
|
import { getConfinementRoot, getCrossRepoProjects } from '../config/cross-repo.mjs';
|
|
36
35
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
37
36
|
import { readLock, isLockLive } from '../session-lock.mjs';
|
|
37
|
+
import { expandTilde } from '../common.mjs';
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @typedef {Object} Candidate
|
|
@@ -97,21 +97,6 @@ function shouldDescendInto(name) {
|
|
|
97
97
|
return true;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
101
|
-
* Expand a leading `~` to the current user's home directory. Mirrors the helper
|
|
102
|
-
* in board-writer.mjs (a shared extraction is deferred to a later epic). Used to
|
|
103
|
-
* normalise config-declared cross-repo paths that may begin with `~/`.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} p
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
function expandHome(p) {
|
|
109
|
-
if (typeof p !== 'string' || p.length === 0) return p;
|
|
110
|
-
if (p === '~') return os.homedir();
|
|
111
|
-
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
112
|
-
return p;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
100
|
/**
|
|
116
101
|
* Derive free/busy status for a single repo from its session.lock lease.
|
|
117
102
|
* Mirrors board-writer.mjs collectRows semantics exactly:
|
|
@@ -322,7 +307,7 @@ export async function enumerateCandidates({ startDir, now, deps } = {}) {
|
|
|
322
307
|
if (Array.isArray(declared)) {
|
|
323
308
|
for (const raw of declared) {
|
|
324
309
|
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
325
|
-
const expanded =
|
|
310
|
+
const expanded = expandTilde(raw);
|
|
326
311
|
const abs = path.resolve(expanded);
|
|
327
312
|
// Confinement-filter against the same root as the FS scan.
|
|
328
313
|
const guard = validatePathInsideProjectFn(abs, root);
|
|
@@ -12,6 +12,54 @@
|
|
|
12
12
|
* Pure functions, no filesystem access — safe to import anywhere.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Current events.jsonl record schema version (#1177). Mirrors
|
|
17
|
+
* `subagents-schema.mjs § CURRENT_SCHEMA_VERSION`.
|
|
18
|
+
*
|
|
19
|
+
* Records written before #1177 carry NO `schema_version` key at all; an absent
|
|
20
|
+
* key therefore reads as "pre-versioned", never as version 0. Stamping is
|
|
21
|
+
* strictly additive — see `stampEventSchemaVersion()`.
|
|
22
|
+
*/
|
|
23
|
+
export const CURRENT_SCHEMA_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by `emitEvent()` when a record fails `validateEventRecord()`.
|
|
27
|
+
*
|
|
28
|
+
* Carries the individual validator messages so a CLI/hook caller can surface
|
|
29
|
+
* them without re-running the validator.
|
|
30
|
+
*/
|
|
31
|
+
export class EventValidationError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} message — human-readable summary.
|
|
34
|
+
* @param {string[]} [errors=[]] — the validator's individual error strings.
|
|
35
|
+
* @param {string} [eventType] — the offending event name, when known.
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, errors = [], eventType) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'EventValidationError';
|
|
40
|
+
this.errors = errors;
|
|
41
|
+
this.eventType = eventType;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return a shallow copy of `record` with `schema_version` stamped to
|
|
47
|
+
* `CURRENT_SCHEMA_VERSION` — but ONLY when the field is absent
|
|
48
|
+
* (`undefined`/`null`). An existing value is never overwritten, so a caller
|
|
49
|
+
* (or a migration re-writing historical records) keeps authority over its own
|
|
50
|
+
* version field.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} record
|
|
53
|
+
* @returns {object} shallow copy, `schema_version` guaranteed present.
|
|
54
|
+
*/
|
|
55
|
+
export function stampEventSchemaVersion(record) {
|
|
56
|
+
const out = { ...record };
|
|
57
|
+
if (out.schema_version === undefined || out.schema_version === null) {
|
|
58
|
+
out.schema_version = CURRENT_SCHEMA_VERSION;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
/** ISO-8601 UTC timestamp with trailing Z (e.g. 2026-05-28T14:35:13.123Z). */
|
|
16
64
|
const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
17
65
|
|
package/scripts/lib/events.mjs
CHANGED
|
@@ -7,12 +7,52 @@
|
|
|
7
7
|
* Part of v3.0.0 migration (Epic #124, issue #133).
|
|
8
8
|
* Issue #228: removed hardcoded personal-domain default URL. Clank Event Bus URL
|
|
9
9
|
* must now be supplied explicitly via CLANK_EVENT_URL when CLANK_EVENT_SECRET is set.
|
|
10
|
+
*
|
|
11
|
+
* ## Correlation envelope (#1177 FA3)
|
|
12
|
+
*
|
|
13
|
+
* Measured 2026-09-02 @ c3ab480 over 33,608 ledger records: only 22.1% carry
|
|
14
|
+
* `session_id` and 4.8% carry `wave`, because filling them was every call
|
|
15
|
+
* site's own job and 32 of 34 call sites pass no options at all. `emitEvent()`
|
|
16
|
+
* now fills those keys itself — under three hard rules:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Additive, never overriding.** The correlation keys are spread BEFORE
|
|
19
|
+
* `payload`, so any caller-supplied `session_id` / `semantic_session_id` /
|
|
20
|
+
* `wave` wins byte-for-byte. A payload that supplies EITHER session key
|
|
21
|
+
* suppresses the session fill entirely (both keys), so a caller that
|
|
22
|
+
* deliberately pins attribution elsewhere — `vault-mirror/telemetry.mjs`
|
|
23
|
+
* pins to `SO_PROJECT_DIR` and passes both keys — is untouched.
|
|
24
|
+
* 2. **Omit, never fabricate.** When attribution cannot be PROVEN, both keys
|
|
25
|
+
* are left ABSENT — never `null`, never `''`. An absent key is the only
|
|
26
|
+
* honest encoding of "not attributable" (see `sessionAttribution()`).
|
|
27
|
+
* 3. **Never a peer's id (#1123).** A shared working copy means
|
|
28
|
+
* `session.lock` can name a PEER session that won the acquire race. The
|
|
29
|
+
* lock alone therefore does not prove ownership; the fill happens only
|
|
30
|
+
* when a PROCESS-LOCAL id (`CLAUDE_CODE_SESSION_ID`, or a hook payload's
|
|
31
|
+
* `session_id`) equals the lock's raw `session_id`. STATE.md is NOT a
|
|
32
|
+
* witness here (#1177 FX1): it is a shared working-copy file written by
|
|
33
|
+
* the lock holder, so under a peer-owned lock both agreed about the peer
|
|
34
|
+
* and the union stamped the peer's ids. See {@link attributionForRecord}.
|
|
35
|
+
*
|
|
36
|
+
* The attribution root is the SAME root the ledger line is pinned to
|
|
37
|
+
* (`opts.repoRoot ?? SO_PROJECT_DIR`), never `process.cwd()`. Measured cost of
|
|
38
|
+
* the whole envelope (lock + wave manifest, 100 calls, this repo):
|
|
39
|
+
* 0.0961 ms/call.
|
|
10
40
|
*/
|
|
11
41
|
|
|
12
|
-
import { promises as fs } from 'node:fs';
|
|
42
|
+
import { promises as fs, existsSync, readFileSync } from 'node:fs';
|
|
13
43
|
import path from 'node:path';
|
|
14
44
|
import { SO_PROJECT_DIR, SO_SHARED_DIR } from './platform.mjs';
|
|
15
45
|
import { readLock } from './session-lock.mjs';
|
|
46
|
+
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
47
|
+
import {
|
|
48
|
+
classifyManifestSession,
|
|
49
|
+
readProcessLocalSessionIds,
|
|
50
|
+
} from './session-identity/own-session.mjs';
|
|
51
|
+
import {
|
|
52
|
+
EventValidationError,
|
|
53
|
+
stampEventSchemaVersion,
|
|
54
|
+
validateEventRecord,
|
|
55
|
+
} from './events-schema.mjs';
|
|
16
56
|
|
|
17
57
|
// ---------------------------------------------------------------------------
|
|
18
58
|
// Public API
|
|
@@ -69,16 +109,173 @@ export function sessionAttribution(repoRoot) {
|
|
|
69
109
|
}
|
|
70
110
|
}
|
|
71
111
|
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Correlation envelope (#1177 FA3)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/** State-dir candidates, in the same order `state-md` resolves them. */
|
|
117
|
+
const STATE_DIR_CANDIDATES = ['.claude', '.codex', '.cursor', '.pi'];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Session-correlation keys for a record pinned to `root` — `{}` when ownership
|
|
121
|
+
* is not provable.
|
|
122
|
+
*
|
|
123
|
+
* Decision, in one line: **only a PROCESS-LOCAL id may confirm the lock, and
|
|
124
|
+
* when one exists it decides alone.**
|
|
125
|
+
*
|
|
126
|
+
* - No lock (CI, a bare script) → `{}`. Nothing to attribute to.
|
|
127
|
+
* - No process-local id (`CLAUDE_CODE_SESSION_ID` absent) → `{}`. Ownership is
|
|
128
|
+
* UNPROVEN, and an unproven attribution is exactly the peer-id write #1123
|
|
129
|
+
* forbids; an absent key costs a correlation, a wrong key costs a false one.
|
|
130
|
+
* - A process-local id that equals the lock's raw `session_id` → fill BOTH
|
|
131
|
+
* keys, verbatim from the lock.
|
|
132
|
+
* - A process-local id that DISAGREES → `{}` (the lock names a peer that won
|
|
133
|
+
* the acquire race).
|
|
134
|
+
*
|
|
135
|
+
* **Why STATE.md is not a witness (#1177 FX1).** It used to be one, unioned
|
|
136
|
+
* with the env id — and the union was the bug: `.claude/STATE.md` is a SHARED
|
|
137
|
+
* working-copy artefact written by the session that OWNS the working copy,
|
|
138
|
+
* i.e. normally the lock holder. When a peer holds the lock, the peer also
|
|
139
|
+
* wrote STATE.md, so both "independent" witnesses name the PEER and a
|
|
140
|
+
* disagreeing process-local id could not veto them. Measured: lock=peer,
|
|
141
|
+
* STATE.md=peer, `CLAUDE_CODE_SESSION_ID`=me → the peer's ids were stamped on
|
|
142
|
+
* this session's records. A shared file cannot prove which PROCESS is emitting;
|
|
143
|
+
* see `readProcessLocalSessionIds()` for the tiering rationale (HR-102: a
|
|
144
|
+
* better signal replaces a worse one, it does not merely get outvoted by it).
|
|
145
|
+
*
|
|
146
|
+
* CEILING (BV-004): the comparison is against the lock's RAW `session_id`, so a
|
|
147
|
+
* harness that ROTATES its session id mid-session (see
|
|
148
|
+
* `tests/hooks/on-session-end.test.mjs` `new-rotated-uuid`) has an env id that
|
|
149
|
+
* no longer equals the lock's raw id, and BOTH keys are then omitted — honest
|
|
150
|
+
* absence, never misattribution. REVISIT when the rotation rate is measured in
|
|
151
|
+
* `events.jsonl` (count `orchestrator.session.started` against lock rewrites):
|
|
152
|
+
* if rotation is common, the lock must be refreshed on rotation rather than
|
|
153
|
+
* this comparison widened.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} [root=SO_PROJECT_DIR] — the repo the record is pinned to.
|
|
156
|
+
* @returns {{session_id?: string, semantic_session_id?: string}}
|
|
157
|
+
*/
|
|
158
|
+
export function attributionForRecord(root = SO_PROJECT_DIR) {
|
|
159
|
+
const attribution = sessionAttribution(root);
|
|
160
|
+
const lockRawId =
|
|
161
|
+
typeof attribution.session_id === 'string' ? attribution.session_id.trim() : '';
|
|
162
|
+
if (!lockRawId) return {};
|
|
163
|
+
const processLocal = readProcessLocalSessionIds();
|
|
164
|
+
if (processLocal.length === 0) return {};
|
|
165
|
+
return processLocal.includes(lockRawId) ? { ...attribution } : {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Absolute path of the active `wave-scope.json`, or `null` when none exists.
|
|
170
|
+
*
|
|
171
|
+
* The active platform's state dir is tried first (via `resolveStateMdPath()`,
|
|
172
|
+
* the repo's existing resolver), then the remaining candidates — so a Codex or
|
|
173
|
+
* Cursor run finds its own manifest rather than a stale `.claude/` one.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} root
|
|
176
|
+
* @returns {string|null}
|
|
177
|
+
*/
|
|
178
|
+
function waveScopePath(root) {
|
|
179
|
+
const dirs = [];
|
|
180
|
+
try {
|
|
181
|
+
dirs.push(path.dirname(resolveStateMdPath(root)));
|
|
182
|
+
} catch {
|
|
183
|
+
/* fall through to the fixed candidate list */
|
|
184
|
+
}
|
|
185
|
+
for (const dir of STATE_DIR_CANDIDATES) {
|
|
186
|
+
const abs = path.join(root, dir);
|
|
187
|
+
if (!dirs.includes(abs)) dirs.push(abs);
|
|
188
|
+
}
|
|
189
|
+
for (const dir of dirs) {
|
|
190
|
+
const candidate = path.join(dir, 'wave-scope.json');
|
|
191
|
+
try {
|
|
192
|
+
if (existsSync(candidate)) return candidate;
|
|
193
|
+
} catch {
|
|
194
|
+
/* unreadable candidate — try the next one */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* `{ wave }` from the live wave-scope manifest — `{}` when the manifest is
|
|
202
|
+
* missing, waveless, or belongs to another session.
|
|
203
|
+
*
|
|
204
|
+
* The manifest is a SHARED working-copy artefact (`.claude/wave-scope.json`),
|
|
205
|
+
* so a peer session's manifest is readable here and would stamp this session's
|
|
206
|
+
* events with a foreign wave number. Ownership is classified with
|
|
207
|
+
* `classifyManifestSession()` against the PROCESS-LOCAL id set plus whatever
|
|
208
|
+
* `attributionForRecord()` actually filled — the same tiering as the session
|
|
209
|
+
* keys, for the same reason (a shared file cannot prove which process emits):
|
|
210
|
+
*
|
|
211
|
+
* - manifest classified `own` → fill (as a NUMBER, see below).
|
|
212
|
+
* - anything else → omit. That includes an UNBOUND manifest (no `session` /
|
|
213
|
+
* `semantic_session`): since #1123 BOTH writers stamp the binding, so a
|
|
214
|
+
* manifest without one is a peer's or a stale artefact, never a legacy own
|
|
215
|
+
* one. It also includes `unknown` because we cannot resolve our own
|
|
216
|
+
* identity — stricter than `classifyManifestSession()`'s own `unknown`
|
|
217
|
+
* doctrine on purpose: a wave number is data on the record, not a feature
|
|
218
|
+
* gate, so "cannot tell" must not become "stamp it anyway".
|
|
219
|
+
*
|
|
220
|
+
* @param {string} root
|
|
221
|
+
* @param {{session_id?: string, semantic_session_id?: string}} attribution
|
|
222
|
+
* @returns {{wave?: number}}
|
|
223
|
+
*/
|
|
224
|
+
function waveForRecord(root, attribution) {
|
|
225
|
+
let scope;
|
|
226
|
+
try {
|
|
227
|
+
const file = waveScopePath(root);
|
|
228
|
+
if (!file) return {};
|
|
229
|
+
scope = JSON.parse(readFileSync(file, 'utf8'));
|
|
230
|
+
} catch {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
// The ledger's `wave` is numeric in 1876 of 1876 live records; a manifest
|
|
234
|
+
// carrying `"wave": "3"` used to write the STRING through verbatim and split
|
|
235
|
+
// every downstream group-by. Coerce, and omit anything that is not an integer
|
|
236
|
+
// (`"abc"`, `2.5`) rather than writing a NaN or a fraction.
|
|
237
|
+
const waveNum = Number(scope?.wave);
|
|
238
|
+
if (scope?.wave === null || scope?.wave === '' || !Number.isInteger(waveNum)) return {};
|
|
239
|
+
|
|
240
|
+
const ownIds = new Set([
|
|
241
|
+
...readProcessLocalSessionIds(),
|
|
242
|
+
...[attribution.session_id, attribution.semantic_session_id].filter(Boolean),
|
|
243
|
+
]);
|
|
244
|
+
const { verdict } = classifyManifestSession(scope, ownIds);
|
|
245
|
+
return verdict === 'own' ? { wave: waveNum } : {};
|
|
246
|
+
}
|
|
247
|
+
|
|
72
248
|
/**
|
|
73
249
|
* Append a JSONL event record and optionally POST to the Clank Event Bus webhook.
|
|
74
250
|
*
|
|
75
|
-
* Writes `{
|
|
76
|
-
* `.orchestrator/metrics/events.jsonl` (creates parent directory if needed).
|
|
251
|
+
* Writes `{timestamp, event, schema_version, ...payload}` as a single JSON line
|
|
252
|
+
* to `.orchestrator/metrics/events.jsonl` (creates parent directory if needed).
|
|
77
253
|
* If both `CLANK_EVENT_SECRET` and `CLANK_EVENT_URL` are set, fires an async
|
|
78
254
|
* fire-and-forget POST to `CLANK_EVENT_URL` with a 3-second timeout. Network
|
|
79
255
|
* errors are swallowed. Write errors propagate to the caller. No personal-domain
|
|
80
256
|
* default URL exists — both vars must be set explicitly (#228).
|
|
81
257
|
*
|
|
258
|
+
* Validation + versioning (#1177). Every record is stamped via
|
|
259
|
+
* `stampEventSchemaVersion()` (the schema module's own stamper — it fills the
|
|
260
|
+
* field only when absent, so a caller keeps authority over it) and run through
|
|
261
|
+
* `validateEventRecord()` BEFORE any side effect. An invalid record throws
|
|
262
|
+
* `EventValidationError` and produces NO ledger line and NO webhook POST —
|
|
263
|
+
* a malformed event is dropped at the producer rather than written and
|
|
264
|
+
* discovered by a downstream reader. The stamp is applied AFTER the payload
|
|
265
|
+
* spread, and still yields to it: the helper fills the field only when it is
|
|
266
|
+
* absent or null, so a caller supplying its own `schema_version` wins.
|
|
267
|
+
*
|
|
268
|
+
* Correlation envelope (#1177 FA3). When the payload carries neither session
|
|
269
|
+
* key, `session_id`/`semantic_session_id` are filled from
|
|
270
|
+
* {@link attributionForRecord}; when it carries no `wave`, `wave` is filled
|
|
271
|
+
* from the OWN wave-scope manifest. Both are additive and omitted whenever
|
|
272
|
+
* ownership is unproven — see the module header for the three rules.
|
|
273
|
+
*
|
|
274
|
+
* The webhook body deliberately stays `{ event_type, source, payload }` with the
|
|
275
|
+
* RAW payload — the wire format is a published contract with an external
|
|
276
|
+
* consumer; `schema_version` describes the JSONL record, not the webhook
|
|
277
|
+
* envelope, and is not added to it.
|
|
278
|
+
*
|
|
82
279
|
* @param {string} type — event type (e.g. "orchestrator.session.started")
|
|
83
280
|
* @param {object} [payload={}] — additional fields shallow-merged into the record
|
|
84
281
|
* @param {object} [opts={}] — emission options.
|
|
@@ -95,8 +292,44 @@ export function sessionAttribution(repoRoot) {
|
|
|
95
292
|
* @returns {Promise<void>}
|
|
96
293
|
*/
|
|
97
294
|
export async function emitEvent(type, payload = {}, opts = {}) {
|
|
98
|
-
//
|
|
99
|
-
|
|
295
|
+
// Correlation envelope (#1177 FA3) — computed against the SAME root the line
|
|
296
|
+
// is pinned to. Both fills are gated on the payload NOT already carrying the
|
|
297
|
+
// key, and both spread BEFORE `payload`, so a caller always wins twice over.
|
|
298
|
+
// A payload that supplies EITHER session key suppresses BOTH: mixing a
|
|
299
|
+
// caller's `session_id` with a lock-derived `semantic_session_id` would
|
|
300
|
+
// silently produce a record whose two id fields name different sessions.
|
|
301
|
+
const attributionRoot = opts.repoRoot ?? SO_PROJECT_DIR;
|
|
302
|
+
const correlation = {};
|
|
303
|
+
if (payload.session_id === undefined && payload.semantic_session_id === undefined) {
|
|
304
|
+
Object.assign(correlation, attributionForRecord(attributionRoot));
|
|
305
|
+
}
|
|
306
|
+
if (payload.wave === undefined) {
|
|
307
|
+
Object.assign(correlation, waveForRecord(attributionRoot, correlation));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Build the JSONL record: timestamp + event first, payload spreads last, and
|
|
311
|
+
// `stampEventSchemaVersion()` — the schema module's own stamper, which only
|
|
312
|
+
// fills an ABSENT/null field — adds the version. Routing through the helper
|
|
313
|
+
// instead of inlining `schema_version: CURRENT_SCHEMA_VERSION` keeps the
|
|
314
|
+
// stamp rule in ONE place: a caller-supplied version still wins, because the
|
|
315
|
+
// helper never overwrites a value the spread already put there.
|
|
316
|
+
const record = stampEventSchemaVersion({
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
event: type,
|
|
319
|
+
...correlation,
|
|
320
|
+
...payload,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Validate BEFORE any side effect — no line, no directory, no webhook (#1177).
|
|
324
|
+
const verdict = validateEventRecord(record);
|
|
325
|
+
if (!verdict.valid) {
|
|
326
|
+
throw new EventValidationError(
|
|
327
|
+
`invalid event record for "${String(type)}": ${verdict.errors.join('; ')}`,
|
|
328
|
+
verdict.errors,
|
|
329
|
+
typeof type === 'string' ? type : undefined,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
100
333
|
const line = JSON.stringify(record) + '\n';
|
|
101
334
|
|
|
102
335
|
// Ensure the destination directory exists before appending. Resolution order:
|
|
@@ -223,10 +223,15 @@ function readinessConfidence(autopilotSummary, judgmentSummary, score) {
|
|
|
223
223
|
/**
|
|
224
224
|
* Summarize autopilot run history plus type-8 mode effectiveness rollups.
|
|
225
225
|
*
|
|
226
|
-
* Abandoned-session filtering (#834)
|
|
227
|
-
* to `groupByMode()`, which
|
|
228
|
-
*
|
|
229
|
-
*
|
|
226
|
+
* Abandoned-session filtering (#834) AND duplicate-identity collapse (#1167):
|
|
227
|
+
* `sessions` is passed straight through to `groupByMode()`, which canonicalizes
|
|
228
|
+
* the array (one record per physical session) and then filters phantom
|
|
229
|
+
* `status: 'abandoned'` stubs before bucketing — this function inherits BOTH
|
|
230
|
+
* guarantees transitively and duplicates neither. The abandoned filter alone
|
|
231
|
+
* was not enough: a `supersedes` pair and an exact same-id duplicate are two
|
|
232
|
+
* records of one session that both survive it. See
|
|
233
|
+
* `autopilot-effectiveness.mjs` `groupByMode()` (which canonicalizes via
|
|
234
|
+
* `canonicalizeSessions(sessions, { keepUnidentified: true })` before filtering).
|
|
230
235
|
*
|
|
231
236
|
* @param {Array} autopilotRuns
|
|
232
237
|
* @param {Array} sessions
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { randomUUID } from 'node:crypto';
|
|
28
28
|
|
|
29
29
|
import { filterRealSessions } from '../session-schema.mjs';
|
|
30
|
+
import { canonicalizeSessions } from '../sessions-canonical.mjs';
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Constants
|
|
@@ -181,7 +182,23 @@ export function groupByMode(autopilotRuns, sessions) {
|
|
|
181
182
|
const out = new Map();
|
|
182
183
|
if (!Array.isArray(sessions) || sessions.length === 0) return out;
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
// Canonicalize BEFORE filtering (#1167). `filterRealSessions` drops phantom
|
|
186
|
+
// `abandoned` stubs, but two records of ONE physical session that are both
|
|
187
|
+
// real survive it — a `supersedes` pair (the backfilled stub plus the
|
|
188
|
+
// authoritative `completed` record that refutes it) and an exact same-id
|
|
189
|
+
// duplicate line both do. Each of those inflates `n_manual` / `n_autopilot`
|
|
190
|
+
// and skews every mean computed from the bucket, so the identity collapse
|
|
191
|
+
// has to happen first; the two compose in exactly this order (see
|
|
192
|
+
// `sessions-canonical.mjs` § "What this module does not do").
|
|
193
|
+
//
|
|
194
|
+
// `keepUnidentified: true` because effectiveness analysis must not LOSE the
|
|
195
|
+
// id-less rows `canonicalizeSessions` drops by default: legacy ledger rows
|
|
196
|
+
// and every in-repo fixture of the pre-id era carry `session_type` and
|
|
197
|
+
// metrics but no id, and dropping them would silently shrink `n_manual`
|
|
198
|
+
// instead of de-duplicating it.
|
|
199
|
+
const realSessions = filterRealSessions(
|
|
200
|
+
canonicalizeSessions(sessions, { keepUnidentified: true }),
|
|
201
|
+
);
|
|
185
202
|
if (realSessions.length === 0) return out;
|
|
186
203
|
|
|
187
204
|
// Optional: known autopilot_run_id set for stricter pairing. Empty set means
|