session-orchestrator 3.24.0 → 4.0.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/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- 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.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -67,12 +67,31 @@ import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs'
|
|
|
67
67
|
// Constants
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The three real session MODES — deliberately NOT the schema's VALID_SESSION_TYPES,
|
|
72
|
+
* which since GitLab #1234 also carries `unknown`. This set answers a narrower
|
|
73
|
+
* question: "is `gathered.mode` a MEASUREMENT?". `unknown` must never pass it, or
|
|
74
|
+
* an events record carrying `mode: 'unknown'` would be recorded as a measured type
|
|
75
|
+
* (`_session_type_inferred` absent) when it is the opposite.
|
|
76
|
+
*/
|
|
77
|
+
const MEASURED_SESSION_MODES = new Set(['feature', 'deep', 'housekeeping']);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* session_type written when nothing in events.jsonl measured the mode
|
|
81
|
+
* (GitLab #1234). Always paired with `_session_type_inferred` + `_synthetic`.
|
|
82
|
+
*/
|
|
83
|
+
const UNMEASURED_SESSION_TYPE = 'unknown';
|
|
72
84
|
|
|
73
85
|
const EVENT_STARTED = 'orchestrator.session.started';
|
|
74
86
|
const EVENT_LOCK_ACQUIRED = 'orchestrator.session.lock.acquired';
|
|
87
|
+
// Both names for one generation (GitLab #1234): `hooks/on-stop.mjs` now emits
|
|
88
|
+
// `orchestrator.turn.stopped` as the canonical name and keeps the legacy
|
|
89
|
+
// `orchestrator.session.stopped` (with `deprecated: true`) beside it until
|
|
90
|
+
// 2027-03-06. A terminal-event probe must accept EITHER, or every session that
|
|
91
|
+
// closes after the legacy name is dropped silently loses its attested end and
|
|
92
|
+
// falls back to the flagged `lastEventMs` estimate.
|
|
75
93
|
const EVENT_STOPPED = 'orchestrator.session.stopped';
|
|
94
|
+
const EVENT_TURN_STOPPED = 'orchestrator.turn.stopped';
|
|
76
95
|
const EVENT_ENDED = 'orchestrator.session.ended';
|
|
77
96
|
|
|
78
97
|
const EVENTS_REL = ['.orchestrator', 'metrics', 'events.jsonl'];
|
|
@@ -108,14 +127,24 @@ function markerName(id) {
|
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
/**
|
|
111
|
-
* Read a JSONL file into an array of parsed objects. Missing file →
|
|
112
|
-
*
|
|
130
|
+
* Read a JSONL file into an array of parsed objects. Missing file (ENOENT) →
|
|
131
|
+
* [] silently; an unreadable one (EACCES/EISDIR/…) → [] with a stderr WARN
|
|
132
|
+
* (#1210 — ENOENT and other read failures are different facts, same split as
|
|
133
|
+
* `sessions-canonical.mjs` `readCanonicalSessions`). Each malformed line is
|
|
134
|
+
* skipped rather than aborting the whole read. Never throws.
|
|
113
135
|
*/
|
|
114
136
|
function readJsonlSafe(readFileSync, filePath) {
|
|
115
137
|
let raw;
|
|
116
138
|
try {
|
|
117
139
|
raw = readFileSync(filePath, 'utf8');
|
|
118
|
-
} catch {
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (!err || err.code !== 'ENOENT') {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`⚠ readJsonlSafe: cannot read ${filePath} ` +
|
|
144
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
145
|
+
'treating as EMPTY, counts below are floors\n',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
119
148
|
return [];
|
|
120
149
|
}
|
|
121
150
|
const out = [];
|
|
@@ -207,7 +236,7 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
|
|
|
207
236
|
if (typeof ev.timestamp === 'string') startedAt = ev.timestamp;
|
|
208
237
|
if (typeof ev.branch === 'string' && ev.branch.length > 0) branch = ev.branch;
|
|
209
238
|
if (typeof ev.project === 'string') project = ev.project;
|
|
210
|
-
} else if (ev.event === EVENT_STOPPED || ev.event === EVENT_ENDED) {
|
|
239
|
+
} else if (ev.event === EVENT_STOPPED || ev.event === EVENT_TURN_STOPPED || ev.event === EVENT_ENDED) {
|
|
211
240
|
if (!Number.isNaN(ts)) {
|
|
212
241
|
lastTerminalMs = lastTerminalMs === null ? ts : Math.max(lastTerminalMs, ts);
|
|
213
242
|
}
|
|
@@ -307,9 +336,9 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
307
336
|
// Guard the same monotonic invariant as before: never earlier than started_at.
|
|
308
337
|
const completedIso = new Date(Math.max(startedMs, completedMs)).toISOString();
|
|
309
338
|
|
|
310
|
-
let sessionType =
|
|
339
|
+
let sessionType = UNMEASURED_SESSION_TYPE;
|
|
311
340
|
let inferred = true;
|
|
312
|
-
if (gathered.mode &&
|
|
341
|
+
if (gathered.mode && MEASURED_SESSION_MODES.has(gathered.mode)) {
|
|
313
342
|
sessionType = gathered.mode;
|
|
314
343
|
inferred = false;
|
|
315
344
|
}
|
|
@@ -346,7 +375,36 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
346
375
|
_backfill_incomplete_fields: incomplete,
|
|
347
376
|
};
|
|
348
377
|
if (branchFound) record.branch = gathered.branch;
|
|
349
|
-
if (inferred)
|
|
378
|
+
if (inferred) {
|
|
379
|
+
record._session_type_inferred = true;
|
|
380
|
+
// GitLab #1234 — BACKFILLER HONESTY, half landed 2026-09-06.
|
|
381
|
+
//
|
|
382
|
+
// `session_type` above is now `'unknown'`, not the old `'housekeeping'`
|
|
383
|
+
// DEFAULT that nothing in events.jsonl ever said. Measured 2026-09-06, all
|
|
384
|
+
// 1.656 `abandoned` records in the 90-day fleet window carry
|
|
385
|
+
// `_session_type_inferred: true` + `total_waves: 0`, and NO organically
|
|
386
|
+
// written `abandoned` record exists anywhere — that `housekeeping` guess is
|
|
387
|
+
// what produced the "27 % close rate" figure that turned out to be an
|
|
388
|
+
// artefact (the real rate is 21,3 %). VALID_SESSION_TYPES was widened with
|
|
389
|
+
// `unknown` in `scripts/lib/session-schema/constants.mjs` to make this
|
|
390
|
+
// sayable; historical records keep their `housekeeping` label verbatim
|
|
391
|
+
// (sessions.jsonl is append-only), so a reader wanting the honest
|
|
392
|
+
// population still filters on `_synthetic !== true` rather than on the type.
|
|
393
|
+
//
|
|
394
|
+
// The OTHER half is deliberately NOT landed: `status` stays `'abandoned'`
|
|
395
|
+
// even though `'unresolved'` is the honest word and the schema now accepts
|
|
396
|
+
// it. Six executable phantom-stub filters key on the literal `abandoned`
|
|
397
|
+
// (census + revisit trigger in `scripts/lib/session-schema/validator.mjs`
|
|
398
|
+
// § SESSION_STATUS) and none of them is in this change's file scope —
|
|
399
|
+
// flipping the emitter first would make every new stub invisible to all six
|
|
400
|
+
// and re-open the #834 phantom-in-signal class fleet-wide. Repoint those
|
|
401
|
+
// filters onto one both-accepting predicate, then flip this one line.
|
|
402
|
+
//
|
|
403
|
+
// `_synthetic: true` still carries the claim no enum can: this record was
|
|
404
|
+
// COMPOSED. A consumer that filters on `_synthetic !== true` gets only
|
|
405
|
+
// measured records without needing to know either enum.
|
|
406
|
+
record._synthetic = true;
|
|
407
|
+
}
|
|
350
408
|
if (synthetic) record._synthetic_session_id = true;
|
|
351
409
|
if (completedEstimated) record._completed_at_estimated = true;
|
|
352
410
|
// #1068 AC3/AC4 — forensic supersede marker. sessions.jsonl is append-only,
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { open, readFile } from 'node:fs/promises';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
|
|
45
|
+
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
45
46
|
import { withStateMdLock } from './session-lock.mjs';
|
|
46
47
|
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
47
48
|
import { parseStateMd } from './state-md/yaml-parser.mjs';
|
|
@@ -127,34 +128,22 @@ function isValidMode(mode) {
|
|
|
127
128
|
* - Malformed JSONL line → silently skipped (per-line try/catch).
|
|
128
129
|
* - Lines without a string `session_id` field → filtered out.
|
|
129
130
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* #1209: migrated to `readCanonicalSessions` (dedup per `session_id`, drop
|
|
132
|
+
* id-less lines — same three rules `sessions-canonical.mjs` documents).
|
|
133
|
+
* Number-neutral for THIS reader: the only consumer of its output
|
|
134
|
+
* (`resolveSemanticSessionId`) folds `candidateIds` through `Math.max` over
|
|
135
|
+
* the parsed `n` — "Duplicates are fine — Math.max handles them" (see that
|
|
136
|
+
* function's body) — so collapsing a duplicate `session_id` line to one
|
|
137
|
+
* record changes nothing observable here. Migrated anyway for one collapse
|
|
138
|
+
* implementation across all `sessions.jsonl` raw-line-scan readers (#1209).
|
|
134
139
|
*
|
|
135
140
|
* @param {string} repoRoot
|
|
136
|
-
* @returns {Promise<string[]>} Array of session_id strings (may include duplicates
|
|
141
|
+
* @returns {Promise<string[]>} Array of session_id strings (may include duplicates
|
|
142
|
+
* only across DIFFERENT ids — never the same id twice, see above).
|
|
137
143
|
*/
|
|
138
144
|
async function readSessionIdsFromHistory(repoRoot) {
|
|
139
145
|
const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
raw = await readFile(filePath, 'utf8');
|
|
143
|
-
} catch {
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
146
|
-
const ids = [];
|
|
147
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
148
|
-
const trimmed = line.trim();
|
|
149
|
-
if (trimmed === '') continue;
|
|
150
|
-
try {
|
|
151
|
-
const parsed = JSON.parse(trimmed);
|
|
152
|
-
if (typeof parsed?.session_id === 'string') ids.push(parsed.session_id);
|
|
153
|
-
} catch {
|
|
154
|
-
// Malformed line — skip silently (audit §3.1 robustness contract).
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return ids;
|
|
146
|
+
return readCanonicalSessions({ filePath }).map((rec) => rec.session_id);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
/**
|
|
@@ -26,7 +26,49 @@
|
|
|
26
26
|
* session id.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { isLockShape } from '../session-lock-shape.mjs';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read the two session ids out of `<repoRoot>/.orchestrator/session.lock`.
|
|
35
|
+
*
|
|
36
|
+
* A deliberate, behaviour-identical stand-in for `readLock()` from
|
|
37
|
+
* `../session-lock.mjs` (#1153 P7): that import drags a static closure of six
|
|
38
|
+
* modules (session-lock → exclusivity-matrix, file-lock, io, host-identity,
|
|
39
|
+
* crypto-digest-utils — measured 2026-09-04 by following its `^import` lines)
|
|
40
|
+
* into every consumer of this module, of which the live
|
|
41
|
+
* `hooks/enforce-scope.mjs` runs on EVERY Edit/Write. Two strings do not need
|
|
42
|
+
* a lock manager.
|
|
43
|
+
*
|
|
44
|
+
* Tolerance is matched to `readLock()` exactly, which collapses every non-ok
|
|
45
|
+
* outcome of `readLockDetailed()` to `null`: missing file, unreadable file,
|
|
46
|
+
* invalid JSON, and **valid JSON that fails the lock schema** all yield `{}`
|
|
47
|
+
* here. The schema check is not reproduced but IMPORTED — `isLockShape()` from
|
|
48
|
+
* `../session-lock-shape.mjs` is the same predicate `parseLock()` applies, and
|
|
49
|
+
* that module imports nothing, so sharing it costs the hook chain no closure.
|
|
50
|
+
* A copy would have been free to drift the fail-OPEN way: a relaxed
|
|
51
|
+
* `parseLock` plus an unchanged copy here drops the lock tier's ids, the own
|
|
52
|
+
* manifest reads `foreign`, and enforcement switches itself off silently.
|
|
53
|
+
*
|
|
54
|
+
* Never throws.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} repoRoot
|
|
57
|
+
* @returns {{ session_id?: string, semantic_session_id?: string }}
|
|
58
|
+
*/
|
|
59
|
+
function readLockIds(repoRoot) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(
|
|
62
|
+
path.join(repoRoot ?? process.cwd(), '.orchestrator', 'session.lock'),
|
|
63
|
+
'utf8',
|
|
64
|
+
);
|
|
65
|
+
const obj = JSON.parse(raw);
|
|
66
|
+
if (!isLockShape(obj)) return {};
|
|
67
|
+
return { session_id: obj.session_id, semantic_session_id: obj.semantic_session_id };
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
30
72
|
|
|
31
73
|
/**
|
|
32
74
|
* The set of session ids that provably name THIS session — the UNION of every
|
|
@@ -112,10 +154,10 @@ export function readOwnSessionIds(repoRoot, { hookInput = null } = {}) {
|
|
|
112
154
|
|
|
113
155
|
// Source 3 — repo-global lock file (the manifest writer's own identity).
|
|
114
156
|
try {
|
|
115
|
-
const lock =
|
|
157
|
+
const lock = readLockIds(repoRoot);
|
|
116
158
|
for (const key of ['session_id', 'semantic_session_id']) add(lock?.[key]);
|
|
117
159
|
} catch {
|
|
118
|
-
/*
|
|
160
|
+
/* readLockIds never throws by contract, but that contract is not ours to trust */
|
|
119
161
|
}
|
|
120
162
|
return ids;
|
|
121
163
|
}
|
|
@@ -190,9 +232,11 @@ export function readProcessLocalSessionIds({ env = process.env, hookInput = null
|
|
|
190
232
|
* - `'own'` — an id matched.
|
|
191
233
|
*
|
|
192
234
|
* Both id fields are consulted because they address the same session under two
|
|
193
|
-
* naming schemes: `
|
|
194
|
-
* Code), `
|
|
235
|
+
* naming schemes: `session_id` is the raw harness session id (a UUID on Claude
|
|
236
|
+
* Code), `semantic_session_id` the `<branch>-<date>-<mode>-<n>` form. A harness
|
|
195
237
|
* that resolves only the semantic one must still recognise its own manifest.
|
|
238
|
+
* The pre-#1153 spellings `session` / `semantic_session` are still READ (see
|
|
239
|
+
* {@link MANIFEST_SESSION_KEYS}).
|
|
196
240
|
*
|
|
197
241
|
* @param {unknown} scope — parsed wave-scope manifest (any shape; a non-object
|
|
198
242
|
* simply yields no ids, hence `'unknown'`).
|
|
@@ -204,14 +248,85 @@ export function readProcessLocalSessionIds({ env = process.env, hookInput = null
|
|
|
204
248
|
* latter returns a `string[]`: a bare array is NOT a Set and folds to the
|
|
205
249
|
* empty set below, yielding `'unknown'` for every manifest.
|
|
206
250
|
* @returns {{ verdict: 'own'|'foreign'|'unknown', manifestIds: string[] }}
|
|
251
|
+
* @see MANIFEST_SESSION_KEYS / {@link manifestSessionBinding} — defined
|
|
252
|
+
* immediately below rather than above this doc comment, because the three
|
|
253
|
+
* live hooks import this module on EVERY tool call: a use-before-define here
|
|
254
|
+
* throws inside the hook chain and locks every session sharing the checkout
|
|
255
|
+
* out of Edit/Write/Bash (measured 2026-09-04, ~8 minutes, #1153 P2).
|
|
207
256
|
*/
|
|
257
|
+
/**
|
|
258
|
+
* The session-binding key names of a `wave-scope.json` manifest — the ONE place
|
|
259
|
+
* these literals live (#1153 P2). Every reader imports them from here instead
|
|
260
|
+
* of repeating the strings: `scripts/materialize-wave-scope.mjs`,
|
|
261
|
+
* `scripts/validate-wave-scope.mjs`, `scripts/memory-propose.mjs`, and — via
|
|
262
|
+
* {@link classifyManifestSession} — `scripts/lib/events.mjs` plus the three
|
|
263
|
+
* live hooks.
|
|
264
|
+
*
|
|
265
|
+
* `current` are the canonical names, chosen to match the two neighbouring
|
|
266
|
+
* session artefacts a reader already knows: `.orchestrator/session.lock` and
|
|
267
|
+
* `current-session.json` both spell them `session_id` / `semantic_session_id`,
|
|
268
|
+
* and `scripts/lib/quality-gate.mjs` reads that exact pair. One session
|
|
269
|
+
* identity should not carry two spellings depending on which file names it.
|
|
270
|
+
*
|
|
271
|
+
* `legacy` are the pre-#1153 spellings, and they are **accepted on the READ
|
|
272
|
+
* side only, until the next minor release**. `wave-scope.json` is git-ignored
|
|
273
|
+
* and session-ephemeral, so the one surviving reason to read them is a package
|
|
274
|
+
* upgraded mid-wave with an old-format manifest already on disk. The writer
|
|
275
|
+
* (`scripts/wave-scope-binding.mjs`) emits `current` exclusively.
|
|
276
|
+
*
|
|
277
|
+
* @type {Readonly<{ current: readonly string[], legacy: readonly string[] }>}
|
|
278
|
+
*/
|
|
279
|
+
export const MANIFEST_SESSION_KEYS = Object.freeze({
|
|
280
|
+
current: Object.freeze(['session_id', 'semantic_session_id']),
|
|
281
|
+
legacy: Object.freeze(['session', 'semantic_session']),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Resolve the binding out of a manifest under BOTH key spellings. A non-object
|
|
286
|
+
* — including an ARRAY, which `typeof` calls `'object'` — yields no ids.
|
|
287
|
+
*
|
|
288
|
+
* **A CONFLICT yields no value for that slot, and that is fail-CLOSED.**
|
|
289
|
+
* When both spellings of one slot are present with different non-empty values,
|
|
290
|
+
* the manifest is self-contradictory (`scripts/validate-wave-scope.mjs` calls
|
|
291
|
+
* exactly this an ERROR — but no hook runs the validator, so the classifier is
|
|
292
|
+
* the only thing standing between the manifest and the guard). Preferring the
|
|
293
|
+
* current spelling let a peer DISARM this session's guard by appending
|
|
294
|
+
* `"session_id": "attacker"` beside a legitimate legacy `"session": "<me>"`:
|
|
295
|
+
* the slot then resolved to an id this session does not carry,
|
|
296
|
+
* {@link classifyManifestSession} returned `'foreign'`, and
|
|
297
|
+
* `hooks/enforce-scope.mjs` skips enforcement on `'foreign'`. Dropping the slot
|
|
298
|
+
* instead collapses the verdict to `'unknown'`, which every caller treats as
|
|
299
|
+
* "keep enforcing".
|
|
300
|
+
*
|
|
301
|
+
* Both present and EQUAL (after trim) → that value. Only one present → that
|
|
302
|
+
* value. Both present and different → the slot is omitted.
|
|
303
|
+
*
|
|
304
|
+
* @param {unknown} scope
|
|
305
|
+
* @returns {{ session_id?: string, semantic_session_id?: string }}
|
|
306
|
+
*/
|
|
307
|
+
export function manifestSessionBinding(scope) {
|
|
308
|
+
const out = {};
|
|
309
|
+
if (!scope || typeof scope !== 'object' || Array.isArray(scope)) return out;
|
|
310
|
+
MANIFEST_SESSION_KEYS.current.forEach((key, i) => {
|
|
311
|
+
const legacyKey = MANIFEST_SESSION_KEYS.legacy[i];
|
|
312
|
+
const pick = (v) => (typeof v === 'string' && v.trim() ? v.trim() : '');
|
|
313
|
+
const current = pick(scope[key]);
|
|
314
|
+
const legacy = pick(scope[legacyKey]);
|
|
315
|
+
if (current && legacy && current !== legacy) return; // conflict → no value
|
|
316
|
+
const value = current || legacy;
|
|
317
|
+
if (value) out[key] = value;
|
|
318
|
+
});
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @see the contract note above `MANIFEST_SESSION_KEYS` — the full docblock for
|
|
323
|
+
* this function sits there, separated from it only because the constants must
|
|
324
|
+
* be defined before use (#1153 P2). */
|
|
208
325
|
export function classifyManifestSession(scope, ownIds) {
|
|
209
326
|
const manifestIds = [];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (value) manifestIds.push(value);
|
|
214
|
-
}
|
|
327
|
+
const binding = manifestSessionBinding(scope);
|
|
328
|
+
for (const key of MANIFEST_SESSION_KEYS.current) {
|
|
329
|
+
if (binding[key]) manifestIds.push(binding[key]);
|
|
215
330
|
}
|
|
216
331
|
const own = ownIds instanceof Set ? ownIds : new Set();
|
|
217
332
|
if (manifestIds.length === 0 || own.size === 0) return { verdict: 'unknown', manifestIds };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-lock-shape.mjs — the ONE predicate that decides whether a parsed
|
|
3
|
+
* JSON value is a `.orchestrator/session.lock` record (#1153 P7).
|
|
4
|
+
*
|
|
5
|
+
* **Zero imports on purpose.** One of the two consumers is
|
|
6
|
+
* `scripts/lib/session-identity/own-session.mjs`, which the live
|
|
7
|
+
* `hooks/enforce-scope.mjs` loads on EVERY Edit/Write; anything this module
|
|
8
|
+
* imported would join that hook's static closure. It therefore holds six
|
|
9
|
+
* `typeof` checks and nothing else.
|
|
10
|
+
*
|
|
11
|
+
* **The two consumers, and why they must not drift apart:**
|
|
12
|
+
*
|
|
13
|
+
* - `scripts/lib/session-lock.mjs` `parseLock()` — the lock manager's own
|
|
14
|
+
* reader; a value that fails this predicate is `null` there.
|
|
15
|
+
* - `scripts/lib/session-identity/own-session.mjs` `readLockIds()` — the
|
|
16
|
+
* dependency-free stand-in that reads the same two ids out of the same
|
|
17
|
+
* file without dragging the lock manager into a hook.
|
|
18
|
+
*
|
|
19
|
+
* The drift direction is FAIL-OPEN, which is why the predicate is shared
|
|
20
|
+
* rather than repeated: if `parseLock` relaxed the shape and `readLockIds` did
|
|
21
|
+
* not, the lock tier of {@link readOwnSessionIds} would stop contributing its
|
|
22
|
+
* ids, this session's own manifest would classify `foreign`, and
|
|
23
|
+
* `hooks/enforce-scope.mjs` would silently skip enforcement for the whole wave.
|
|
24
|
+
*
|
|
25
|
+
* @param {unknown} obj — a value already parsed from JSON.
|
|
26
|
+
* @returns {boolean} true when `obj` carries all six required lock fields with
|
|
27
|
+
* the right primitive types. `semantic_session_id` and `last_heartbeat` are
|
|
28
|
+
* deliberately NOT required — both are optional by schema (v1 locks predate
|
|
29
|
+
* `last_heartbeat`, and `semantic_session_id` is absent on harnesses that
|
|
30
|
+
* resolve no semantic id).
|
|
31
|
+
*/
|
|
32
|
+
export function isLockShape(obj) {
|
|
33
|
+
return (
|
|
34
|
+
typeof obj === 'object' &&
|
|
35
|
+
obj !== null &&
|
|
36
|
+
typeof obj.session_id === 'string' &&
|
|
37
|
+
typeof obj.started_at === 'string' &&
|
|
38
|
+
typeof obj.mode === 'string' &&
|
|
39
|
+
typeof obj.pid === 'number' &&
|
|
40
|
+
typeof obj.host === 'string' &&
|
|
41
|
+
typeof obj.ttl_hours === 'number'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -43,6 +43,7 @@ import os from 'node:os';
|
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import crypto from 'node:crypto';
|
|
45
45
|
import { classifyMode } from './exclusivity-matrix.mjs';
|
|
46
|
+
import { isLockShape } from './session-lock-shape.mjs';
|
|
46
47
|
import { isPidAliveOnHost } from './file-lock.mjs';
|
|
47
48
|
import { writeJsonAtomicSync } from './io.mjs';
|
|
48
49
|
import { hostnamesMatch, lockHostCandidate, recordHostAlias, stableHostname } from './host-identity.mjs';
|
|
@@ -199,16 +200,10 @@ function heartbeatAgeMinutes(lock) {
|
|
|
199
200
|
function parseLock(raw) {
|
|
200
201
|
try {
|
|
201
202
|
const obj = JSON.parse(raw);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
typeof obj.started_at === 'string' &&
|
|
207
|
-
typeof obj.mode === 'string' &&
|
|
208
|
-
typeof obj.pid === 'number' &&
|
|
209
|
-
typeof obj.host === 'string' &&
|
|
210
|
-
typeof obj.ttl_hours === 'number'
|
|
211
|
-
) {
|
|
203
|
+
// The six-field predicate lives in ONE place (#1153 P7) — see
|
|
204
|
+
// `./session-lock-shape.mjs` for the other consumer and the fail-open
|
|
205
|
+
// drift this sharing prevents.
|
|
206
|
+
if (isLockShape(obj)) {
|
|
212
207
|
// Schema v1 → v2 normalisation: when `last_heartbeat` is absent or
|
|
213
208
|
// non-string, treat the lock as if it heartbeat-ed once at started_at.
|
|
214
209
|
const normalised = { ...obj };
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* peer-detection (F2, #168) and hooks/on-stop.mjs clean deregister + zombie
|
|
6
6
|
* sweep (F3, #169).
|
|
7
7
|
*
|
|
8
|
-
* Registry location:
|
|
9
|
-
* Sweep log:
|
|
8
|
+
* Registry location: `<private-config-dir>/sessions/active/<sessionId>.json`
|
|
9
|
+
* Sweep log: `<private-config-dir>/sessions/sweep.log` (JSONL)
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
11
|
+
* `<private-config-dir>` is `resolvePrivateConfigDir()` — `SO_CONFIG_HOME` >
|
|
12
|
+
* `XDG_CONFIG_HOME` > `~/.config/session-orchestrator`. `SO_SESSION_REGISTRY_DIR`
|
|
13
|
+
* overrides the whole `sessions/` directory (not `active/`) and outranks all
|
|
14
|
+
* three — used by tests for isolation.
|
|
13
15
|
*
|
|
14
16
|
* Heartbeat schema (per issue #167):
|
|
15
17
|
* {
|
|
@@ -27,7 +29,6 @@
|
|
|
27
29
|
* deliberately, see `skills/_shared/state-ownership.md` § Schema v1 Sunset.
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
|
-
import os from 'node:os';
|
|
31
32
|
import path from 'node:path';
|
|
32
33
|
import crypto from 'node:crypto';
|
|
33
34
|
import { promises as fs } from 'node:fs';
|
|
@@ -36,16 +37,31 @@ import { digestSha256 } from './crypto-digest-utils.mjs';
|
|
|
36
37
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
37
38
|
|
|
38
39
|
import { utcTimestamp, appendJsonl } from './common.mjs';
|
|
40
|
+
import { resolvePrivateConfigDir } from './config/private-config-dir.mjs';
|
|
39
41
|
|
|
40
42
|
// ---------------------------------------------------------------------------
|
|
41
43
|
// Paths
|
|
42
44
|
// ---------------------------------------------------------------------------
|
|
43
45
|
|
|
44
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Parent directory for all session-registry state.
|
|
48
|
+
*
|
|
49
|
+
* Precedence: `SO_SESSION_REGISTRY_DIR` (names the `sessions/` dir ITSELF —
|
|
50
|
+
* highest, and what every existing test uses for isolation) > the host-private
|
|
51
|
+
* config dir resolved by `resolvePrivateConfigDir()` (`SO_CONFIG_HOME` >
|
|
52
|
+
* `XDG_CONFIG_HOME` > `~/.config/session-orchestrator`).
|
|
53
|
+
*
|
|
54
|
+
* Hardcoding `~/.config` here was the #1223 hazard class in its registry form:
|
|
55
|
+
* a sandboxed probe run that set `SO_CONFIG_HOME=<tmp>` moved every OTHER
|
|
56
|
+
* host-private artefact but not this one, so the throwaway session registered
|
|
57
|
+
* itself in the operator's REAL registry and was then discovered as a live peer.
|
|
58
|
+
* `.trim()` guards the whitespace-only env value (`development.md` § Error
|
|
59
|
+
* Handling) — `' '` is truthy and would otherwise be returned verbatim.
|
|
60
|
+
*/
|
|
45
61
|
export function registryBaseDir() {
|
|
46
|
-
const override = process.env.SO_SESSION_REGISTRY_DIR;
|
|
47
|
-
if (override
|
|
48
|
-
return path.join(
|
|
62
|
+
const override = (process.env.SO_SESSION_REGISTRY_DIR || '').trim();
|
|
63
|
+
if (override) return override;
|
|
64
|
+
return path.join(resolvePrivateConfigDir(), 'sessions');
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/** Directory holding one JSON file per active session. */
|
|
@@ -56,8 +56,31 @@ export const SESSION_KEY_ALIASES = Object.freeze({
|
|
|
56
56
|
// Enums / required field lists
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Closed set of valid session_type values.
|
|
61
|
+
*
|
|
62
|
+
* `unknown` (GitLab #1234, added 2026-09-06) is NOT a fourth session MODE — it is
|
|
63
|
+
* the absence of a measurement, and it exists so a reconstructed record can say
|
|
64
|
+
* so instead of guessing. Measured 2026-09-06 over the 90-day fleet corpus: all
|
|
65
|
+
* 1.656 `abandoned` records carry `_session_type_inferred: true` + `total_waves: 0`
|
|
66
|
+
* and NO organically written `abandoned` record exists anywhere — i.e. every one
|
|
67
|
+
* of them was labelled `housekeeping` by `scripts/lib/session-close-backfill.mjs`
|
|
68
|
+
* because the enum left it no alternative, and that guess is what produced the
|
|
69
|
+
* fleet-wide "27 % close rate" figure (the real rate is 21,3 %).
|
|
70
|
+
*
|
|
71
|
+
* Only `synthesizeRecord()` in `scripts/lib/session-close-backfill.mjs` writes it,
|
|
72
|
+
* and only for records it also flags `_session_type_inferred` + `_synthetic`.
|
|
73
|
+
* A live session never becomes `unknown`: `/session` still resolves one of the
|
|
74
|
+
* three modes, and `scripts/lib/wave-sizing.mjs` (which THROWS on a fourth value)
|
|
75
|
+
* is only ever fed the live type, never a ledger record.
|
|
76
|
+
*
|
|
77
|
+
* Known mis-bucket, out of this change's scope: `normalizeSessionType()` in
|
|
78
|
+
* `scripts/lib/telemetry/schema.mjs:382` maps any non-empty unlisted value to
|
|
79
|
+
* `'other'` ("something WAS measured and is not one of the three modes"), which
|
|
80
|
+
* is the opposite of what `unknown` means — even though that module already
|
|
81
|
+
* defines `SESSION_TYPE_UNKNOWN = 'unknown'` for its absent-branch.
|
|
82
|
+
*/
|
|
83
|
+
export const VALID_SESSION_TYPES = Object.freeze(['feature', 'deep', 'housekeeping', 'unknown']);
|
|
61
84
|
|
|
62
85
|
/**
|
|
63
86
|
* Required fields for a schema_version=1 record. Validated by validateSession
|
|
@@ -131,4 +154,15 @@ export const OPTIONAL_FIELDS = Object.freeze([
|
|
|
131
154
|
// into a note a human reads" ⊃ "schema-valid". A record missing it is a clean
|
|
132
155
|
// vault-mirror skip, NOT a malformed record.
|
|
133
156
|
'effectiveness',
|
|
157
|
+
// PRD docs/prd/2026-09-06-ultradeep-session-profile.md — `session_profile`
|
|
158
|
+
// names a WAVE-SHAPE variant on top of an unchanged `session_type`. It is
|
|
159
|
+
// additive and optional on purpose: `ultradeep` is deliberately NOT a member
|
|
160
|
+
// of VALID_SESSION_TYPES above, because that set is mirrored in
|
|
161
|
+
// scripts/lib/telemetry/schema.mjs (an unlisted type -> 'other') and in
|
|
162
|
+
// scripts/lib/wave-sizing.mjs (an unlisted type -> TypeError), where a new
|
|
163
|
+
// MODE would be MISLABELLED rather than rejected. That reasoning is unchanged
|
|
164
|
+
// by the `unknown` member added above: `unknown` is the absence of a
|
|
165
|
+
// measurement, not a mode, and nothing dispatches on it. Every historical
|
|
166
|
+
// record lacking the field validates unchanged.
|
|
167
|
+
'session_profile',
|
|
134
168
|
]);
|
|
@@ -28,11 +28,33 @@ const EXPECTED_COST_TIERS = Object.freeze(['quick', 'standard', 'deep']);
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Valid values for the optional `status` field (Epic #724 C1).
|
|
31
|
-
* `completed`
|
|
32
|
-
* `abandoned`
|
|
33
|
-
*
|
|
31
|
+
* `completed` — record written by a normal /close flow.
|
|
32
|
+
* `abandoned` — stub backfilled by the SessionEnd hook because the session
|
|
33
|
+
* terminated without running /close.
|
|
34
|
+
* `unresolved` — the HONEST label for that same stub: "never reached /close" is
|
|
35
|
+
* observed, but "abandoned" is an interpretation of it (a session
|
|
36
|
+
* may have finished its work and merely skipped /close, or been
|
|
37
|
+
* killed). GitLab #1234, added 2026-09-06.
|
|
38
|
+
*
|
|
39
|
+
* CEILING (BV-004) — `unresolved` currently has NO writer, and the emitter must
|
|
40
|
+
* not adopt it yet. Six EXECUTABLE phantom-stub filters key on the literal
|
|
41
|
+
* `abandoned` and none is inside this change's file scope, so flipping the
|
|
42
|
+
* emitter today would make every new stub invisible to all of them and re-open
|
|
43
|
+
* the #834 phantom-in-signal class fleet-wide. Census measured 2026-09-06 via
|
|
44
|
+
* `rg -n "status\s*(!==|===|!=|==)\s*[\"']abandoned[\"']" scripts hooks --glob '!*.test.mjs'`:
|
|
45
|
+
* scripts/lib/session-schema/filters.mjs:56 (isRealSession — the shared helper,
|
|
46
|
+
* imported by sessions-staleness-banner, auto-dream, dialectic-deriver,
|
|
47
|
+
* harness-audit/categories/category4, evolve/autopilot-effectiveness)
|
|
48
|
+
* scripts/lib/sessions-canonical.mjs:149,172
|
|
49
|
+
* scripts/lib/eval/session-resolve.mjs:79
|
|
50
|
+
* scripts/mcp-server.sh:195
|
|
51
|
+
* scripts/compute-grounding-injection.sh:74
|
|
52
|
+
* REVISIT TRIGGER: once those six route through one predicate that accepts both
|
|
53
|
+
* `abandoned` and `unresolved`, switch `synthesizeRecord()` in
|
|
54
|
+
* `scripts/lib/session-close-backfill.mjs` to emit `unresolved` for inferred
|
|
55
|
+
* records — a one-line change, which is exactly why this value lands now.
|
|
34
56
|
*/
|
|
35
|
-
const SESSION_STATUS = Object.freeze(['completed', 'abandoned']);
|
|
57
|
+
const SESSION_STATUS = Object.freeze(['completed', 'abandoned', 'unresolved']);
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
60
|
* Canonical ISO-8601 UTC timestamp regex — accepts `YYYY-MM-DDTHH:MM:SSZ`
|
|
@@ -462,6 +484,18 @@ function _validateOptionalFields(entry) {
|
|
|
462
484
|
}
|
|
463
485
|
}
|
|
464
486
|
|
|
487
|
+
// `session_profile` — optional wave-shape profile (PRD
|
|
488
|
+
// docs/prd/2026-09-06-ultradeep-session-profile.md). Non-empty string or
|
|
489
|
+
// null/absent; absent means "no profile" and is NOT coerced to a string.
|
|
490
|
+
// Deliberately NOT validated against a closed set: unlike `session_type`,
|
|
491
|
+
// no consumer branches on the value, so an unrecognised profile is a
|
|
492
|
+
// readable record with an unknown shape rather than a silent mislabel.
|
|
493
|
+
if (entry.session_profile !== undefined && entry.session_profile !== null) {
|
|
494
|
+
if (typeof entry.session_profile !== 'string' || entry.session_profile.length === 0) {
|
|
495
|
+
throw new ValidationError('session_profile must be a non-empty string or null');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
465
499
|
// `_express_path_detail` — forensic sidecar written by `normalizeSession`
|
|
466
500
|
// when it collapses a legacy object `express_path` onto its boolean. Holds
|
|
467
501
|
// the pre-collapse object verbatim so the conversion stays reversible.
|
|
@@ -176,8 +176,16 @@ export const PROBES = [
|
|
|
176
176
|
args: ({ repoRoot }) => ({ repoRoot }),
|
|
177
177
|
// Bespoke shape: `{status, ok, details, …}` with no `message` field. The
|
|
178
178
|
// banner text is prescribed by SKILL.md § Phase 4.
|
|
179
|
+
//
|
|
180
|
+
// The degraded branch is NOT decoration (#1031): this entry overrides BOTH
|
|
181
|
+
// `render` and `severityOf`, so the module-level defaults that already
|
|
182
|
+
// handle a `{severity:'warn', message, degraded}` result never run for this
|
|
183
|
+
// probe. Without these two lines a degraded ci-status result scored `'ok'`
|
|
184
|
+
// and rendered nothing — "could not read" displayed exactly like "green",
|
|
185
|
+
// which is the confusion the probe's own migration removed one layer down.
|
|
179
186
|
render: (r) => {
|
|
180
187
|
if (!r || typeof r !== 'object') return null;
|
|
188
|
+
if (r.degraded) return typeof r.message === 'string' && r.message ? r.message : null;
|
|
181
189
|
if (r.status === 'red') {
|
|
182
190
|
const pid = r.details?.currentPipelineId ?? '?';
|
|
183
191
|
const green = r.lastGreen
|
|
@@ -193,7 +201,16 @@ export const PROBES = [
|
|
|
193
201
|
return null;
|
|
194
202
|
},
|
|
195
203
|
// `status: 'red'` is an alert even though the probe publishes no severity.
|
|
196
|
-
|
|
204
|
+
// A degraded result is a finding, never clean — same rule as the generic
|
|
205
|
+
// path in `severityOf()` below.
|
|
206
|
+
severityOf: (r) =>
|
|
207
|
+
r?.degraded
|
|
208
|
+
? 'warn'
|
|
209
|
+
: r?.status === 'red'
|
|
210
|
+
? 'alert'
|
|
211
|
+
: r?.status === 'green' && r?.allowFailureJobs
|
|
212
|
+
? 'warn'
|
|
213
|
+
: 'ok',
|
|
197
214
|
},
|
|
198
215
|
{
|
|
199
216
|
id: 'qg-command-drift',
|