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
|
@@ -132,6 +132,7 @@ import path from 'node:path';
|
|
|
132
132
|
|
|
133
133
|
import { readLock, DEFAULT_TTL_HOURS } from './session-lock.mjs';
|
|
134
134
|
import { isRealSession } from './session-schema/filters.mjs';
|
|
135
|
+
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
135
136
|
|
|
136
137
|
/** Repo-relative path to the session ledger (one record per closed session). */
|
|
137
138
|
const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
|
|
@@ -240,20 +241,21 @@ function keepNewer(current, candidate) {
|
|
|
240
241
|
* result so the caller can say the anchor is a stub start, not a measured close;
|
|
241
242
|
* the key is omitted (`undefined`) on the genuine path.
|
|
242
243
|
*
|
|
243
|
-
*
|
|
244
|
+
* `records` is the CANONICAL (#1209b) record set — `readCanonicalSessions()`
|
|
245
|
+
* has already collapsed a duplicated `session_id` to its newest occurrence, so
|
|
246
|
+
* a since-corrected raw LINE for the same identity (e.g. one later marked
|
|
247
|
+
* `status: 'abandoned'`, or a stale `completed_at` a later record for the same
|
|
248
|
+
* id superseded) can no longer independently skew this max-reduce the way a
|
|
249
|
+
* raw-line scan over every append could.
|
|
250
|
+
*
|
|
251
|
+
* @param {object[]} records
|
|
244
252
|
* @returns {{iso: string, ms: number, stubFallback?: true}|null}
|
|
245
253
|
*/
|
|
246
|
-
function lastLedgerEntry(
|
|
254
|
+
function lastLedgerEntry(records) {
|
|
247
255
|
let newestGenuine = null; // newest genuine completed_at (or started_at floor)
|
|
248
256
|
let newestStub = null; // newest stub started_at
|
|
249
257
|
|
|
250
|
-
for (const
|
|
251
|
-
let record;
|
|
252
|
-
try {
|
|
253
|
-
record = JSON.parse(line);
|
|
254
|
-
} catch {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
258
|
+
for (const record of records) {
|
|
257
259
|
if (!record || typeof record !== 'object') continue;
|
|
258
260
|
|
|
259
261
|
if (isBackfillStub(record)) {
|
|
@@ -370,10 +372,15 @@ export function checkSessionsStaleness({ repoRoot, now = Date.now() } = {}) {
|
|
|
370
372
|
|
|
371
373
|
const nowMs = typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
|
|
372
374
|
|
|
373
|
-
const
|
|
375
|
+
const sessionsPath = path.join(repoRoot, SESSIONS_PATH);
|
|
376
|
+
const sessionLines = readJsonlLines(sessionsPath);
|
|
374
377
|
if (sessionLines === null || sessionLines.length === 0) return null;
|
|
375
378
|
|
|
376
|
-
|
|
379
|
+
// CANONICAL (#1209b) record set — readJsonlLines() above only decides
|
|
380
|
+
// missing-vs-empty (readCanonicalSessions() cannot tell those apart, see
|
|
381
|
+
// its own doc); the anchor scan itself now runs over the collapsed set.
|
|
382
|
+
const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
|
|
383
|
+
const ledger = lastLedgerEntry(sessionRecords);
|
|
377
384
|
if (ledger === null) return null;
|
|
378
385
|
|
|
379
386
|
const eventLines = readJsonlLines(path.join(repoRoot, EVENTS_PATH));
|
|
@@ -35,6 +35,7 @@ import path from 'node:path';
|
|
|
35
35
|
import { fileURLToPath } from 'node:url';
|
|
36
36
|
|
|
37
37
|
import { isRealSession } from '../session-schema/filters.mjs';
|
|
38
|
+
import { readCanonicalSessions } from '../sessions-canonical.mjs';
|
|
38
39
|
|
|
39
40
|
const DEFAULT_INVOCATIONS_PATH = path.resolve(
|
|
40
41
|
fileURLToPath(import.meta.url),
|
|
@@ -81,6 +82,15 @@ async function readJsonl(filePath) {
|
|
|
81
82
|
* it to route the join to the `abandoned` outcome bucket instead of counting
|
|
82
83
|
* a zero-signal join as `sessionsJoined`.
|
|
83
84
|
*
|
|
85
|
+
* Callers pass `sessionRecords` from `readCanonicalSessions()` (#1209b) — one
|
|
86
|
+
* record per physical session (newest-wins per `session_id`, systemic
|
|
87
|
+
* double-stub twin dropped, superseded stubs removed). Without that upstream
|
|
88
|
+
* collapse, the two-writer double-stub class (a SEPARATE synthetic
|
|
89
|
+
* `session_id` for the SAME physical session) would enter this map as a
|
|
90
|
+
* second, independent `abandoned` entry — this function's own `map.set()`
|
|
91
|
+
* overwrite only dedupes an EXACT `session_id` repeat, never two different
|
|
92
|
+
* ids for one session.
|
|
93
|
+
*
|
|
84
94
|
* @param {object[]} sessionRecords
|
|
85
95
|
* @returns {Map<string, { agentSummary: { complete: number, partial: number, failed: number, spiral: number }, real: boolean }>}
|
|
86
96
|
*/
|
|
@@ -121,10 +131,13 @@ export async function joinSkillOutcomes({
|
|
|
121
131
|
invocationsPath = DEFAULT_INVOCATIONS_PATH,
|
|
122
132
|
sessionsPath = DEFAULT_SESSIONS_PATH,
|
|
123
133
|
} = {}) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
// Invocations stay on the raw async reader (no identity-collapse concept
|
|
135
|
+
// applies to a selection-event stream). Sessions move to the CANONICAL
|
|
136
|
+
// (#1209b) reader — synchronous by contract (see sessions-canonical.mjs) —
|
|
137
|
+
// so a session_id counted twice under the two-writer double-stub bug no
|
|
138
|
+
// longer inflates `sessionsAbandoned` (see buildSessionMap doc above).
|
|
139
|
+
const invocations = await readJsonl(invocationsPath);
|
|
140
|
+
const sessionRecords = readCanonicalSessions({ filePath: sessionsPath });
|
|
128
141
|
|
|
129
142
|
const sessionMap = buildSessionMap(sessionRecords);
|
|
130
143
|
|
package/scripts/lib/state-md.mjs
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
* @see scripts/lib/state-md/body-sections.mjs readCurrentTask, appendDeviation, markExpressPathComplete, appendWhatNotToRetry, readWhatNotToRetry, readOpenQuestions, appendOpenQuestion, markOpenQuestionAnswered
|
|
10
10
|
* @see scripts/lib/state-md/mission-status.mjs parseMissionStatus, parseMissionStatusStrict, MISSION_STATUS_VALUES, writeMissionStatus, setMissionStatus, setMissionStatusDetailed, readMissionStatus, recoverFrontmatterMissionStatusDetailed, writeMissionStatusOnDisk, setMissionStatusOnDisk
|
|
11
11
|
* @see scripts/lib/state-md/recommendations.mjs parseRecommendations
|
|
12
|
+
*
|
|
13
|
+
* Plus ONE small non-re-export surface: the `session-profile` frontmatter
|
|
14
|
+
* accessors at the bottom of this file (see their docblock for why they are
|
|
15
|
+
* composed here rather than added as a fourth mutator module).
|
|
12
16
|
*/
|
|
13
17
|
|
|
18
|
+
import { parseStateMd as _parseStateMd } from './state-md/yaml-parser.mjs';
|
|
19
|
+
import { updateFrontmatterFields as _updateFrontmatterFields } from './state-md/frontmatter-mutators.mjs';
|
|
20
|
+
|
|
14
21
|
export { parseStateMd, serializeStateMd } from './state-md/yaml-parser.mjs';
|
|
15
22
|
|
|
16
23
|
export {
|
|
@@ -61,3 +68,74 @@ export {
|
|
|
61
68
|
} from './state-md/mission-status.mjs';
|
|
62
69
|
|
|
63
70
|
export { parseRecommendations } from './state-md/recommendations.mjs';
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Session profile (PRD docs/prd/2026-09-06-ultradeep-session-profile.md)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
//
|
|
76
|
+
// `session-profile` is an OPTIONAL STATE.md frontmatter scalar that names a
|
|
77
|
+
// wave-shape variant on top of an unchanged `session-type`. Today exactly one
|
|
78
|
+
// value is defined — `ultradeep` (7 waves, coordinator-direct Synthesis-Gate at
|
|
79
|
+
// wave 2) — resolved from the `/session ultradeep` argument alias in
|
|
80
|
+
// `commands/session.md`.
|
|
81
|
+
//
|
|
82
|
+
// The vocabulary is deliberately NOT a closed set here. A profile changes only
|
|
83
|
+
// how the coordinator shapes waves; unlike `session_type` (a closed set in
|
|
84
|
+
// scripts/lib/session-schema/constants.mjs, telemetry and the close-backfill),
|
|
85
|
+
// no consumer branches on the value, so an unknown one degrades to "a profile
|
|
86
|
+
// this reader does not recognise" rather than to a silent mislabel. Revisit
|
|
87
|
+
// trigger: the first consumer that BRANCHES on a specific profile value — at
|
|
88
|
+
// that point the set becomes load-bearing and belongs in a shared constant.
|
|
89
|
+
//
|
|
90
|
+
// Composed from the two existing helpers above rather than reaching into the
|
|
91
|
+
// frontmatter with a second parser: `parseStateMd` for the read,
|
|
92
|
+
// `updateFrontmatterFields` for the write (whose null/undefined semantics
|
|
93
|
+
// already mean DELETE, which is exactly "no profile").
|
|
94
|
+
|
|
95
|
+
/** Frontmatter key holding the optional session profile. */
|
|
96
|
+
export const SESSION_PROFILE_FIELD = 'session-profile';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the session profile from STATE.md contents.
|
|
100
|
+
*
|
|
101
|
+
* ABSENCE IS NEVER COERCED. Returns `null` — not `''`, not `'none'` — when the
|
|
102
|
+
* document has no frontmatter, no `session-profile` key, or a value that is not
|
|
103
|
+
* a non-empty string. Callers test `=== null` for "no profile"; they must not
|
|
104
|
+
* test truthiness of a string they assumed was always present.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} contents Full STATE.md text.
|
|
107
|
+
* @returns {string|null} The profile name, or null when no profile is set.
|
|
108
|
+
*/
|
|
109
|
+
export function readSessionProfile(contents) {
|
|
110
|
+
if (typeof contents !== 'string') return null;
|
|
111
|
+
const parsed = _parseStateMd(contents);
|
|
112
|
+
if (parsed === null) return null;
|
|
113
|
+
const value = parsed.frontmatter[SESSION_PROFILE_FIELD];
|
|
114
|
+
if (typeof value !== 'string') return null;
|
|
115
|
+
const trimmed = value.trim();
|
|
116
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set or clear the session profile in STATE.md contents.
|
|
121
|
+
*
|
|
122
|
+
* Passing `null` DELETES the key, restoring the absent (= no profile) state —
|
|
123
|
+
* it never writes a placeholder value. Every other frontmatter key, including
|
|
124
|
+
* unknown extensions, is preserved verbatim by `updateFrontmatterFields`.
|
|
125
|
+
* No-ops (returns the input unchanged) when `contents` has no frontmatter.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} contents Full STATE.md text.
|
|
128
|
+
* @param {string|null} profile Profile name, or null to clear.
|
|
129
|
+
* @returns {string} The new STATE.md text.
|
|
130
|
+
* @throws {TypeError} when `profile` is neither a non-empty string nor null.
|
|
131
|
+
*/
|
|
132
|
+
export function setSessionProfile(contents, profile) {
|
|
133
|
+
if (profile !== null && (typeof profile !== 'string' || profile.trim().length === 0)) {
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
`setSessionProfile: profile must be a non-empty string or null, got: ${JSON.stringify(profile)}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return _updateFrontmatterFields(contents, {
|
|
139
|
+
[SESSION_PROFILE_FIELD]: profile === null ? null : profile.trim(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -471,6 +471,12 @@ function collectFiles(dir) {
|
|
|
471
471
|
function isBoilerplateSite(relPath, kind, name) {
|
|
472
472
|
if (kind === 'agent') {
|
|
473
473
|
if (relPath === `agents/${name}.md`) return true;
|
|
474
|
+
// Kept for repoRoots where an authoring-spec file still lives at this path
|
|
475
|
+
// (pre-4.0.0 checkouts, template/consumer repos) — see
|
|
476
|
+
// tests/lib/sunset-walker.test.mjs "boilerplate exclusion (agents)". The
|
|
477
|
+
// session-orchestrator repo itself moved the spec to docs/agent-authoring.md,
|
|
478
|
+
// which is outside SCAN_DIRS and can never appear as a relPath here, so
|
|
479
|
+
// there is no live path to repoint this exclusion to.
|
|
474
480
|
if (relPath === 'agents/AGENTS.md') return true;
|
|
475
481
|
if (relPath === `agents/schemas/${name}.schema.json`) return true;
|
|
476
482
|
// Routing-table / validator boilerplate.
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* none and holds no mutable runtime state.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import { getPlatform, getPluginRoot } from '../platform.mjs';
|
|
39
39
|
import { enumerateSurface } from '../sunset/walker.mjs';
|
|
40
40
|
import { readPluginVersionFromPackageJson } from '../bootstrap-lock-freshness.mjs';
|
|
41
41
|
|
|
@@ -70,6 +70,52 @@ export const USAGE_PING_FIELDS = Object.freeze([
|
|
|
70
70
|
'commands',
|
|
71
71
|
]);
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* ADDITIVE optional fields (schema v1, GitLab #1234). Deliberately a SECOND
|
|
75
|
+
* list rather than an extension of USAGE_PING_FIELDS above, for two reasons:
|
|
76
|
+
*
|
|
77
|
+
* 1. USAGE_PING_FIELDS is the REQUIRED v1 contract — `tests/telemetry/parity.mjs`
|
|
78
|
+
* asserts, field by field, that the server independently REQUIRES every
|
|
79
|
+
* member. An optional field is by definition not required, so putting it in
|
|
80
|
+
* that list would force the parity guard to be weakened for all 15 fields to
|
|
81
|
+
* accommodate one.
|
|
82
|
+
* 2. The frozen-list tripwire (`projectUsagePing` drops anything unlisted)
|
|
83
|
+
* stays intact: the projection whitelist is the UNION of both lists, so a
|
|
84
|
+
* leaky field still has to be added to a reviewed list before it can reach
|
|
85
|
+
* the wire.
|
|
86
|
+
*
|
|
87
|
+
* Server tolerance: `server/ingest/validate.mjs` accepts unknown top-level
|
|
88
|
+
* fields and preserves them verbatim in `raw_json` (see its module docblock), so
|
|
89
|
+
* every member here round-trips through a server that predates it.
|
|
90
|
+
*/
|
|
91
|
+
export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
|
|
92
|
+
'fleet_self_declared',
|
|
93
|
+
'session_record',
|
|
94
|
+
'session_profile',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The full projection whitelist: required v1 fields + additive optional fields.
|
|
99
|
+
* `projectUsagePing` is driven from THIS, so neither list alone can leak a field.
|
|
100
|
+
*/
|
|
101
|
+
const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAGE_PING_OPTIONAL_FIELDS]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `session_record` provenance tokens — WHICH source the session facts
|
|
105
|
+
* (`session_type`, `duration_bucket`) in this ping came from.
|
|
106
|
+
*
|
|
107
|
+
* - `ledger` — `.orchestrator/metrics/sessions.jsonl` had a matching record.
|
|
108
|
+
* - `derived` — no ledger record; the facts were reconstructed from
|
|
109
|
+
* `events.jsonl` (`orchestrator.session.started` + last event).
|
|
110
|
+
* - `absent` — neither source produced a session type. `session_type` is then
|
|
111
|
+
* `'unknown'` and `duration_bucket` is NOT a measurement.
|
|
112
|
+
*
|
|
113
|
+
* Measured 2026-09-06 (d8 audit): this repo has NO sessions.jsonl, so 100 % of
|
|
114
|
+
* its pings took the `absent` path and were indistinguishable on the wire from
|
|
115
|
+
* a genuinely-measured `other` / `<15m` session — 32 such pings on the server.
|
|
116
|
+
*/
|
|
117
|
+
export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
|
|
118
|
+
|
|
73
119
|
/** Exact duration-bucket tokens (ASCII, stable wire values). */
|
|
74
120
|
export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
|
|
75
121
|
|
|
@@ -83,6 +129,43 @@ const VALID_PLATFORMS = Object.freeze(['claude', 'codex', 'cursor', 'pi']);
|
|
|
83
129
|
const VALID_SESSION_TYPES = Object.freeze(['housekeeping', 'feature', 'deep']);
|
|
84
130
|
const PLATFORM_OTHER = 'other';
|
|
85
131
|
const SESSION_TYPE_OTHER = 'other';
|
|
132
|
+
/**
|
|
133
|
+
* The type was never measured — distinct from `'other'`, which means "measured,
|
|
134
|
+
* but not one of the three known modes". Conflating the two is the defect this
|
|
135
|
+
* token fixes: before it existed, a ping built with NO session record silently
|
|
136
|
+
* reported `'other'`, so "we could not tell" and "we looked and it was unusual"
|
|
137
|
+
* were the same wire value.
|
|
138
|
+
*
|
|
139
|
+
* NOT an enum widening server-side: `session_type` is validated as a bounded
|
|
140
|
+
* STRING (`server/ingest/validate.mjs` `requireString` + `MAX_SESSION_TYPE`),
|
|
141
|
+
* never against a closed set, so `'unknown'` is accepted by today's server.
|
|
142
|
+
*/
|
|
143
|
+
const SESSION_TYPE_UNKNOWN = 'unknown';
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* CLOSED whitelist of PUBLIC session-profile names that may reach the wire.
|
|
147
|
+
*
|
|
148
|
+
* WHY A WHITELIST AND NOT A REGEX: `session_profile` is copied from the STATE.md
|
|
149
|
+
* frontmatter key `session-profile`, which is written per repo by whoever runs
|
|
150
|
+
* `/session <alias>` — i.e. it is the only field on the usage-ping whose VALUE is
|
|
151
|
+
* repo-authored free text. Two independent Wave-1 reviewers reproduced the leak
|
|
152
|
+
* end-to-end (2026-09-06): `session-profile: client-acme-private-repo` travelled
|
|
153
|
+
* verbatim through `buildUsagePing` → `projectUsagePing` → the ingest server's
|
|
154
|
+
* `raw_json` column. A shape regex does NOT close it — that example string is
|
|
155
|
+
* already lowercase-and-hyphens and passes any such pattern. Only an
|
|
156
|
+
* enumeration of names that are public BY CONSTRUCTION does.
|
|
157
|
+
*
|
|
158
|
+
* Today exactly one profile exists: `ultradeep`, the 7-wave variant of a `deep`
|
|
159
|
+
* session (writer: `commands/session.md` § "Argument alias: ultradeep" → STATE.md
|
|
160
|
+
* frontmatter `session-profile`; wave shape: `skills/session-plan/SKILL.md`).
|
|
161
|
+
* Adding a profile there means adding it HERE and in the server's mirror
|
|
162
|
+
* (`server/ingest/validate.mjs` `SESSION_PROFILES`) — a deliberate two-line,
|
|
163
|
+
* reviewed edit, the same contract `ACCEPTED_VERSIONS` already uses server-side.
|
|
164
|
+
*
|
|
165
|
+
* NOT a member of VALID_SESSION_TYPES: the profile is a SECOND axis. An
|
|
166
|
+
* ultradeep session is `session_type: "deep"` PLUS `session_profile: "ultradeep"`.
|
|
167
|
+
*/
|
|
168
|
+
export const VALID_SESSION_PROFILES = Object.freeze(['ultradeep']);
|
|
86
169
|
|
|
87
170
|
/**
|
|
88
171
|
* Closed sets for os/arch client-side normalization. A value outside the set —
|
|
@@ -121,7 +204,8 @@ function isNonEmptyString(v) {
|
|
|
121
204
|
// ---------------------------------------------------------------------------
|
|
122
205
|
|
|
123
206
|
/**
|
|
124
|
-
* Project an arbitrary object onto the usage-ping whitelist (USAGE_PING_FIELDS
|
|
207
|
+
* Project an arbitrary object onto the usage-ping whitelist (USAGE_PING_FIELDS
|
|
208
|
+
* PLUS USAGE_PING_OPTIONAL_FIELDS).
|
|
125
209
|
* Fully data-driven: any key not on the whitelist — paths, repo names, prompts,
|
|
126
210
|
* args, hostnames, rogue extras — is dropped. Array fields (skills, commands) are
|
|
127
211
|
* copied as NEW arrays so no caller reference leaks into the projection.
|
|
@@ -132,7 +216,7 @@ function isNonEmptyString(v) {
|
|
|
132
216
|
export function projectUsagePing(input) {
|
|
133
217
|
if (!isPlainObject(input)) return {};
|
|
134
218
|
const out = {};
|
|
135
|
-
for (const key of
|
|
219
|
+
for (const key of USAGE_PING_PROJECTED_FIELDS) {
|
|
136
220
|
if (key in input) {
|
|
137
221
|
const v = input[key];
|
|
138
222
|
out[key] = Array.isArray(v) ? [...v] : v;
|
|
@@ -163,7 +247,7 @@ export function projectUsagePing(input) {
|
|
|
163
247
|
export function loadRoster({ pluginRoot } = {}) {
|
|
164
248
|
const root = (typeof pluginRoot === 'string' && pluginRoot.trim() !== '')
|
|
165
249
|
? pluginRoot
|
|
166
|
-
:
|
|
250
|
+
: getPluginRoot();
|
|
167
251
|
|
|
168
252
|
if (!isNonEmptyString(root)) {
|
|
169
253
|
process.stderr.write(
|
|
@@ -309,8 +393,9 @@ export function deriveDurationBucket(startedAtISO, completedAtISO) {
|
|
|
309
393
|
* whose null return (missing/unparseable package.json or non-string version) maps to 'unknown'.
|
|
310
394
|
*/
|
|
311
395
|
function resolvePluginVersion() {
|
|
312
|
-
|
|
313
|
-
|
|
396
|
+
const root = getPluginRoot();
|
|
397
|
+
if (!isNonEmptyString(root)) return 'unknown';
|
|
398
|
+
return readPluginVersionFromPackageJson(root) ?? 'unknown';
|
|
314
399
|
}
|
|
315
400
|
|
|
316
401
|
/** Normalize the detected platform to the closed enum (+ 'other' fallback). */
|
|
@@ -320,7 +405,36 @@ function normalizePlatform(platform) {
|
|
|
320
405
|
|
|
321
406
|
/** Normalize the session type to the closed enum (+ 'other' fallback). */
|
|
322
407
|
function normalizeSessionType(sessionType) {
|
|
323
|
-
|
|
408
|
+
if (VALID_SESSION_TYPES.includes(sessionType)) return sessionType;
|
|
409
|
+
// ABSENT ≠ UNRECOGNISED. A missing/blank value means nothing was measured
|
|
410
|
+
// (`unknown`); a present-but-unlisted value means something WAS measured and
|
|
411
|
+
// is not one of the three modes (`other`). The old single-branch version
|
|
412
|
+
// returned `other` for both, which is how 394 pings from a host with no
|
|
413
|
+
// sessions.jsonl arrived looking like measured `other` sessions (d8, 2026-09-06).
|
|
414
|
+
return isNonEmptyString(sessionType) ? SESSION_TYPE_OTHER : SESSION_TYPE_UNKNOWN;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Normalize a session PROFILE against the closed VALID_SESSION_PROFILES
|
|
419
|
+
* whitelist. Anything else — absent, blank, or an unlisted repo-authored value —
|
|
420
|
+
* returns `null`, and the caller OMITS the field.
|
|
421
|
+
*
|
|
422
|
+
* DELIBERATELY NOT MIRRORING normalizeSessionType's two-token report. That helper
|
|
423
|
+
* can distinguish ABSENT (`'unknown'`) from UNRECOGNISED (`'other'`) because both
|
|
424
|
+
* of its answers are public constants. Here the unrecognised VALUE is precisely
|
|
425
|
+
* what must not travel, and there is no third token that carries the distinction
|
|
426
|
+
* without carrying the string: an `'other'` profile would say "this host ran a
|
|
427
|
+
* profile we do not ship" — a fact of no product use, bought with a new wire
|
|
428
|
+
* value. So both cases collapse to omission, and the wire cannot tell them apart
|
|
429
|
+
* BY DESIGN. The local `session-profile` key is untouched either way.
|
|
430
|
+
*
|
|
431
|
+
* @param {unknown} profile
|
|
432
|
+
* @returns {string|null} a whitelisted profile name, or null (⇒ field omitted).
|
|
433
|
+
*/
|
|
434
|
+
export function normalizeSessionProfile(profile) {
|
|
435
|
+
if (!isNonEmptyString(profile)) return null;
|
|
436
|
+
const trimmed = profile.trim();
|
|
437
|
+
return VALID_SESSION_PROFILES.includes(trimmed) ? trimmed : null;
|
|
324
438
|
}
|
|
325
439
|
|
|
326
440
|
/**
|
|
@@ -403,6 +517,9 @@ export function buildUsagePing({
|
|
|
403
517
|
env = process.env,
|
|
404
518
|
now = new Date().toISOString(),
|
|
405
519
|
roster,
|
|
520
|
+
consentState,
|
|
521
|
+
sessionRecordSource,
|
|
522
|
+
sessionProfile,
|
|
406
523
|
} = {}) {
|
|
407
524
|
const session = isPlainObject(sessionRecord) ? sessionRecord : {};
|
|
408
525
|
const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
|
|
@@ -431,19 +548,74 @@ export function buildUsagePing({
|
|
|
431
548
|
(kind === 'command' ? commandNames : skillNames).push(name);
|
|
432
549
|
}
|
|
433
550
|
|
|
551
|
+
// ── fleet: a statement about the OPERATOR, derived from the RESOLVED consent
|
|
552
|
+
// state, not from a raw owner.yaml read ────────────────────────────────────
|
|
553
|
+
// The old expression was `ownerConfig?.telemetry?.enabled === true` — a
|
|
554
|
+
// statement about a FILE. Measured 2026-09-06 (d8): the operator's second Mac
|
|
555
|
+
// has consent granted but no `telemetry:` block in owner.yaml, so 394 of 490
|
|
556
|
+
// server records (80,4 %) counted the operator as an external user and every
|
|
557
|
+
// week's `fleet_vs_external` was wrong.
|
|
558
|
+
//
|
|
559
|
+
// `resolveConsent()` already answers this question correctly: it returns
|
|
560
|
+
// `enabled-fleet` for the owner.yaml opt-in AND `enabled-env` for
|
|
561
|
+
// `SO_TELEMETRY=1`, both of which are operator-side postures. Callers that
|
|
562
|
+
// pass `consentState` get that answer; the ownerConfig fallback below keeps
|
|
563
|
+
// the two-argument callers (and the CLI preview) behaviourally identical.
|
|
564
|
+
//
|
|
565
|
+
// CEILING, named because the client cannot close it: this is still
|
|
566
|
+
// SELF-DECLARED — a sandbox or a host whose owner.yaml is unreachable declares
|
|
567
|
+
// `false` however honest it is. The authoritative classification is
|
|
568
|
+
// server-side (`SO_INGEST_FLEET_ANON_IDS`, server/ingest/config.mjs).
|
|
569
|
+
// `session_profile` (STATE.md frontmatter `session-profile`) is bounded by the
|
|
570
|
+
// CLOSED VALID_SESSION_PROFILES whitelist — it was the one free-text value on
|
|
571
|
+
// the wire until 2026-09-06, and a repo-authored one at that (see the
|
|
572
|
+
// whitelist's own docblock for the reproduced leak). It is still deliberately
|
|
573
|
+
// NOT routed through normalizeSessionType: that helper degrades anything
|
|
574
|
+
// outside ['housekeeping','feature','deep'] to 'other', which is exactly the
|
|
575
|
+
// silent loss the profile exists to prevent. The contract is
|
|
576
|
+
// `session_type: "deep"` PLUS `session_profile: "ultradeep"` — never
|
|
577
|
+
// `session_type: "ultradeep"`, and never a profile flattened to 'other'.
|
|
578
|
+
//
|
|
579
|
+
// ABSENT IS NOT EMPTY: with no profile — and with an UNLISTED one — the KEY IS
|
|
580
|
+
// OMITTED, matching every other optional ping field. `projectUsagePing` copies
|
|
581
|
+
// only keys that are `in` the input, so an omitted key never reaches the wire
|
|
582
|
+
// as `null`, and an unlisted profile never reaches it at all.
|
|
583
|
+
//
|
|
584
|
+
// SOURCE PRECEDENCE is resolved BEFORE normalization (explicit argument beats
|
|
585
|
+
// the session record), so an unlisted explicit profile is dropped rather than
|
|
586
|
+
// silently replaced by the record's value.
|
|
587
|
+
const rawProfile =
|
|
588
|
+
isNonEmptyString(sessionProfile) ? sessionProfile
|
|
589
|
+
: isNonEmptyString(session.session_profile) ? session.session_profile
|
|
590
|
+
: null;
|
|
591
|
+
const profile = normalizeSessionProfile(rawProfile);
|
|
592
|
+
|
|
593
|
+
const fleetSelfDeclared =
|
|
594
|
+
consentState === undefined || consentState === null
|
|
595
|
+
? ownerConfig?.telemetry?.enabled === true
|
|
596
|
+
: consentState === 'enabled-fleet' || consentState === 'enabled-env';
|
|
597
|
+
|
|
434
598
|
return {
|
|
435
599
|
record_kind: 'usage-ping',
|
|
436
600
|
schema_version: USAGE_PING_SCHEMA_VERSION,
|
|
437
601
|
sent_at: now,
|
|
438
602
|
plugin_version: resolvePluginVersion(),
|
|
439
|
-
platform: normalizePlatform(
|
|
603
|
+
platform: normalizePlatform(getPlatform()),
|
|
440
604
|
os: normalizeOs(process.platform),
|
|
441
605
|
arch: normalizeArch(process.arch),
|
|
442
606
|
node_major: parseInt(process.versions.node, 10),
|
|
443
607
|
ci: deriveCi(env),
|
|
444
|
-
fleet:
|
|
608
|
+
fleet: fleetSelfDeclared,
|
|
609
|
+
// DEPRECATED alias of `fleet_self_declared`, kept for one schema generation
|
|
610
|
+
// (removal: 2027-03-06 — see docs/telemetry.md § Schema evolution) so the
|
|
611
|
+
// server's existing `fleet` column stays comparable across the rename.
|
|
612
|
+
fleet_self_declared: fleetSelfDeclared,
|
|
445
613
|
session_type: normalizeSessionType(session.session_type),
|
|
614
|
+
...(profile !== null ? { session_profile: profile } : {}),
|
|
446
615
|
duration_bucket: deriveDurationBucket(session.started_at, session.completed_at),
|
|
616
|
+
session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
|
|
617
|
+
? sessionRecordSource
|
|
618
|
+
: 'absent',
|
|
447
619
|
skills: filterRosterNames(skillNames, rosterSkills),
|
|
448
620
|
commands: filterRosterNames(commandNames, rosterCommands),
|
|
449
621
|
};
|