session-orchestrator 5.3.0 → 5.5.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/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## Why this module exists
|
|
5
5
|
*
|
|
6
|
-
* `skills/session-start/SKILL.md` § Phase 4 names
|
|
7
|
-
* with a module path and an entry function (re-measured 2026-
|
|
6
|
+
* `skills/session-start/SKILL.md` § Phase 4 names 20 measurement probes, each
|
|
7
|
+
* with a module path and an entry function (re-measured 2026-10-02 — see the
|
|
8
8
|
* registry Census below; the family stood at 18 when this module was written
|
|
9
|
-
* on 2026-08-23
|
|
10
|
-
*
|
|
9
|
+
* on 2026-08-23, at 19 on 2026-09-11, and gained `events-retention` with
|
|
10
|
+
* #1401 — update this count with every registration).
|
|
11
11
|
* Measured 2026-08-23 at `4f6404e`, NONE of them had a mechanical caller:
|
|
12
12
|
*
|
|
13
13
|
* ```
|
|
@@ -44,12 +44,14 @@
|
|
|
44
44
|
* @module scripts/lib/session-start-probes
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
-
import {
|
|
48
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
47
|
+
import { execFile } from 'node:child_process';
|
|
48
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
49
49
|
import path from 'node:path';
|
|
50
50
|
import { pathToFileURL } from 'node:url';
|
|
51
|
+
import { promisify } from 'node:util';
|
|
51
52
|
|
|
52
53
|
import { emitEvent } from './events.mjs';
|
|
54
|
+
import { ARCHIVE_DIR_NAME, ARCHIVE_NAME_RE } from './events-schema.mjs';
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Resolve a sibling module to an absolute `file:` URL.
|
|
@@ -66,6 +68,8 @@ import { emitEvent } from './events.mjs';
|
|
|
66
68
|
*/
|
|
67
69
|
const local = (rel) => pathToFileURL(path.join(import.meta.dirname, rel)).href;
|
|
68
70
|
|
|
71
|
+
const execFileAsync = promisify(execFile);
|
|
72
|
+
|
|
69
73
|
/** Fallback hint when nothing cheaper than an on-demand CLI call is available. */
|
|
70
74
|
const CI_UNKNOWN_HINT_DEFAULT = 'run `glab ci status` on demand';
|
|
71
75
|
|
|
@@ -93,24 +97,30 @@ const PUSHED_FOLLOW_UP_REASONS = new Set([
|
|
|
93
97
|
* missing). Full, not short: `checkCiStatus({ sha })` matches against GitLab's
|
|
94
98
|
* full pipeline SHAs and refuses anything shorter.
|
|
95
99
|
*
|
|
96
|
-
* NAMED CEILING (BV-004): one `git rev-parse` with a 2s timeout,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
100
|
+
* NAMED CEILING (BV-004): one `git rev-parse` with a 2s timeout, called only
|
|
101
|
+
* from {@link ciStatusFollowUp} — i.e. only when the `ci-status` probe already
|
|
102
|
+
* ran (network opt-in) and its reading needs a commit SHA.
|
|
103
|
+
*
|
|
104
|
+
* ASYNC on purpose (#1396): it runs inside the probe's budget race, and only a
|
|
105
|
+
* call that yields the event loop is (a) counted as the probe's OWN work time
|
|
106
|
+
* and (b) preemptible by the race. A synchronous spawn is subtracted from
|
|
107
|
+
* `workMs` as loop-blocked time (see {@link startLoopBlockedMeter}), so a 2s
|
|
108
|
+
* hang would never have scored `budget-exceeded`. An abandoned child is not
|
|
109
|
+
* waited for: the hook ends with `process.exit(0)`.
|
|
99
110
|
*
|
|
100
111
|
* @param {string|undefined} repoRoot
|
|
101
112
|
* @param {string[]} refs
|
|
102
|
-
* @returns {string[]|null}
|
|
113
|
+
* @returns {Promise<string[]|null>}
|
|
103
114
|
*/
|
|
104
|
-
function revParseShas(repoRoot, refs) {
|
|
115
|
+
async function revParseShas(repoRoot, refs) {
|
|
105
116
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
106
117
|
try {
|
|
107
|
-
const
|
|
118
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', ...refs], {
|
|
108
119
|
cwd: repoRoot,
|
|
109
120
|
encoding: 'utf8',
|
|
110
121
|
timeout: 2000,
|
|
111
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
112
122
|
});
|
|
113
|
-
const shas = String(
|
|
123
|
+
const shas = String(stdout).trim().split('\n').map((s) => s.trim());
|
|
114
124
|
const valid = shas.length === refs.length
|
|
115
125
|
&& shas.every((s) => /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(s));
|
|
116
126
|
return valid ? shas : null;
|
|
@@ -121,16 +131,6 @@ function revParseShas(repoRoot, refs) {
|
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
/**
|
|
125
|
-
* Full SHA of the last PUSHED commit (`@{upstream}`), or `null`.
|
|
126
|
-
*
|
|
127
|
-
* @param {string|undefined} repoRoot
|
|
128
|
-
* @returns {string|null}
|
|
129
|
-
*/
|
|
130
|
-
function lastPushedSha(repoRoot) {
|
|
131
|
-
return revParseShas(repoRoot, ['@{upstream}'])?.[0] ?? null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
134
|
/**
|
|
135
135
|
* `ci-status` follow-up (#1332): when HEAD has no CI data, ask for the verdict
|
|
136
136
|
* of the last PUSHED commit — the commit whose pipeline can actually be red
|
|
@@ -154,25 +154,28 @@ function lastPushedSha(repoRoot) {
|
|
|
154
154
|
* the local HEAD — there the pushed SHA is a different question even when it
|
|
155
155
|
* equals local HEAD.
|
|
156
156
|
*
|
|
157
|
+
* The resolved SHA is handed to the runner through `deliver` BEFORE the CLI
|
|
158
|
+
* round trip, so a requery that overruns the budget or throws still leaves
|
|
159
|
+
* the pushed SHA in the reading the renderer receives (#1396).
|
|
160
|
+
*
|
|
157
161
|
* @param {*} result The HEAD reading from `checkCiStatus`
|
|
158
162
|
* @param {{repoRoot?: string}} ctx
|
|
159
163
|
* @param {(extra: object) => Promise<*>} requery Re-invokes the probe with extra options
|
|
164
|
+
* @param {(partial: *) => void} deliver Records a partial result as the fallback
|
|
160
165
|
* @returns {Promise<*>}
|
|
161
166
|
*/
|
|
162
|
-
async function ciPushedFollowUp(result, ctx, requery) {
|
|
167
|
+
async function ciPushedFollowUp(result, ctx, requery, deliver) {
|
|
163
168
|
const reason = result?.details?.reason;
|
|
164
|
-
if (result?.status !== 'unknown' || !PUSHED_FOLLOW_UP_REASONS.has(reason)) {
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
169
|
// One spawn for both: an unresolvable `@{upstream}` fails the whole call,
|
|
168
170
|
// which is exactly the "no upstream" answer.
|
|
169
|
-
const shas = revParseShas(ctx?.repoRoot, ['HEAD', '@{upstream}']);
|
|
170
|
-
// `pushed.sha: null` records "no upstream"
|
|
171
|
+
const shas = await revParseShas(ctx?.repoRoot, ['HEAD', '@{upstream}']);
|
|
172
|
+
// `pushed.sha: null` records "no upstream" as DATA — the renderer never asks git.
|
|
171
173
|
if (!shas) return { ...result, pushed: { sha: null } };
|
|
172
174
|
const [head, sha] = shas;
|
|
173
175
|
if (reason === 'no-pipeline-for-head-sha' && sha === head) {
|
|
174
176
|
return { ...result, pushed: { sha, sameAsHead: true } };
|
|
175
177
|
}
|
|
178
|
+
deliver({ ...result, pushed: { sha } });
|
|
176
179
|
let verdict;
|
|
177
180
|
try {
|
|
178
181
|
verdict = await requery({ sha });
|
|
@@ -183,6 +186,36 @@ async function ciPushedFollowUp(result, ctx, requery) {
|
|
|
183
186
|
return { ...result, pushed: { sha, verdict } };
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
/**
|
|
190
|
+
* The `ci-status` follow-up (#1332, #1396): every git fact a `ci-status`
|
|
191
|
+
* banner names is resolved HERE, inside the probe's budget race, and carried
|
|
192
|
+
* on the result as data — the renderer stays a pure function of the result.
|
|
193
|
+
* #1396: the renderer used to run `git rev-parse` itself after a fallen-back
|
|
194
|
+
* follow-up, outside the budget, where `orchestrator.probes.completed` could
|
|
195
|
+
* never score it `timed_out`.
|
|
196
|
+
*
|
|
197
|
+
* - an `unknown` reading on a {@link PUSHED_FOLLOW_UP_REASONS} reason →
|
|
198
|
+
* {@link ciPushedFollowUp} (`pushed`)
|
|
199
|
+
* - a `green` reading beside a set-aside FAILED same-commit pipeline →
|
|
200
|
+
* `head: { sha }` for {@link droppedFailedLine} (#1390 P5)
|
|
201
|
+
*
|
|
202
|
+
* @param {*} result
|
|
203
|
+
* @param {{repoRoot?: string}} ctx
|
|
204
|
+
* @param {(extra: object) => Promise<*>} requery
|
|
205
|
+
* @param {(partial: *) => void} deliver
|
|
206
|
+
* @returns {Promise<*>}
|
|
207
|
+
*/
|
|
208
|
+
async function ciStatusFollowUp(result, ctx, requery, deliver) {
|
|
209
|
+
if (result?.status === 'unknown' && PUSHED_FOLLOW_UP_REASONS.has(result?.details?.reason)) {
|
|
210
|
+
return ciPushedFollowUp(result, ctx, requery, deliver);
|
|
211
|
+
}
|
|
212
|
+
if (result?.status === 'green' && droppedFailedCount(result) > 0) {
|
|
213
|
+
const sha = (await revParseShas(ctx?.repoRoot, ['HEAD']))?.[0] ?? null;
|
|
214
|
+
return { ...result, head: { sha } };
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
186
219
|
/**
|
|
187
220
|
* `CI <status> (#<pipeline>)` for a pushed-SHA verdict, or `null` when there
|
|
188
221
|
* is no readable verdict (the caller then falls back to the command hint).
|
|
@@ -222,18 +255,18 @@ function commitRunsCommand(gh, sha) {
|
|
|
222
255
|
*
|
|
223
256
|
* @param {object} result The probe result (possibly carrying `pushed` from
|
|
224
257
|
* {@link ciPushedFollowUp})
|
|
225
|
-
* @param {{repoRoot?: string}} [ctx]
|
|
226
258
|
* @returns {string}
|
|
227
259
|
*/
|
|
228
|
-
function ciUnknownHint(result
|
|
260
|
+
function ciUnknownHint(result) {
|
|
229
261
|
if (!PUSHED_FOLLOW_UP_REASONS.has(result?.details?.reason)) return CI_UNKNOWN_HINT_DEFAULT;
|
|
230
262
|
// The GitHub reason names the gh CLI; a glab command there would be wrong.
|
|
231
263
|
const gh = result.details.cliUsed === 'gh';
|
|
232
264
|
const onDemand = gh ? 'run `gh run list` on demand' : CI_UNKNOWN_HINT_DEFAULT;
|
|
233
265
|
const pushed = result.pushed && typeof result.pushed === 'object' ? result.pushed : null;
|
|
234
|
-
//
|
|
235
|
-
// Without `pushed` (follow-up overran or threw
|
|
236
|
-
|
|
266
|
+
// Pure (#1396): the SHA is whatever the follow-up resolved inside the budget.
|
|
267
|
+
// Without `pushed` (the follow-up overran or threw before resolving it) the
|
|
268
|
+
// hint degrades to the on-demand command — it never asks git itself.
|
|
269
|
+
const sha = pushed ? pushed.sha : null;
|
|
237
270
|
if (!sha) return onDemand;
|
|
238
271
|
const short = sha.slice(0, 8);
|
|
239
272
|
if (pushed?.sameAsHead) return `last pushed: ${short} = HEAD — ${onDemand}`;
|
|
@@ -277,22 +310,22 @@ function droppedFailedCount(r) {
|
|
|
277
310
|
* #857 ref-preference result; this only stops the failure from vanishing
|
|
278
311
|
* behind a silent green.
|
|
279
312
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* `checkCiStatus` publishes the sha it read, take it
|
|
313
|
+
* The HEAD sha comes from `r.head.sha`, resolved by {@link ciStatusFollowUp}
|
|
314
|
+
* inside the budget (#1396) — the reading carries no sha, and the GitLab path
|
|
315
|
+
* read the local HEAD, so HEAD is the commit it spoke for. Without it (the
|
|
316
|
+
* follow-up fell back) the line names the CLI's failed-run listing instead.
|
|
317
|
+
* REVISIT TRIGGER: once `checkCiStatus` publishes the sha it read, take it
|
|
318
|
+
* from the reading.
|
|
285
319
|
*
|
|
286
320
|
* Only the GitLab path publishes `droppedStatuses` today (`candidateEvidence`
|
|
287
321
|
* is called from `checkGitlab` alone); the gh form exists so a GitHub producer
|
|
288
322
|
* can never print a glab command (#1339 P1 class) — see {@link commitRunsCommand}.
|
|
289
323
|
*
|
|
290
324
|
* @param {*} r A green reading with `droppedFailedCount(r) > 0`
|
|
291
|
-
* @param {{repoRoot?: string}} [ctx]
|
|
292
325
|
* @returns {string}
|
|
293
326
|
*/
|
|
294
|
-
function droppedFailedLine(r
|
|
295
|
-
const sha =
|
|
327
|
+
function droppedFailedLine(r) {
|
|
328
|
+
const sha = typeof r.head?.sha === 'string' ? r.head.sha : null;
|
|
296
329
|
const gh = r.details.cliUsed === 'gh';
|
|
297
330
|
let check;
|
|
298
331
|
if (sha) check = commitRunsCommand(gh, sha);
|
|
@@ -432,11 +465,15 @@ function startLoopBlockedMeter() {
|
|
|
432
465
|
* — the #1157 SKILL.md-size split extracted Phase 4's procedure out of
|
|
433
466
|
* `SKILL.md` itself, so the original 2026-08-23 citation of SKILL.md lines
|
|
434
467
|
* 693-835 no longer resolves; re-run the census against the reference file):
|
|
435
|
-
* grep -oE '
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
468
|
+
* grep -oE "fn: 'check[A-Za-z]+'" scripts/lib/session-start-probes.mjs \
|
|
469
|
+
* | sed "s/fn: '//;s/'//" > fns.txt # -> 20
|
|
470
|
+
* grep -oFf fns.txt \
|
|
471
|
+
* skills/session-start/references/phase-4-ssot-environment-check.md \
|
|
472
|
+
* | sort -u | wc -l # -> 20
|
|
473
|
+
* i.e. every registered entry function is named in the reference file
|
|
474
|
+
* (2026-10-02, after `events-retention` / #1401; 19 on 2026-09-11). The older
|
|
475
|
+
* census `grep -oE 'via \`(await )?check[A-Za-z]+'` returned 0 at `86e636a9`
|
|
476
|
+
* — the prose no longer says "via" — so it is retired, not re-counted.
|
|
440
477
|
* (Prior census, 2026-08-23 at `4f6404e`: 16 + 2 = 18 — one probe was
|
|
441
478
|
* registered since without a matching update here. This count is deliberately
|
|
442
479
|
* NOT pinned by an exact-equality test against `PROBES.length` — the registry
|
|
@@ -456,11 +493,19 @@ function startLoopBlockedMeter() {
|
|
|
456
493
|
* - `precondition` optional; returns a skip-reason string to skip the probe
|
|
457
494
|
* - `render` optional; maps a result to a banner line. Default:
|
|
458
495
|
* `result.message` when severity is warn/alert.
|
|
459
|
-
* - `followUp` optional; `async (result, ctx, requery) => result'`
|
|
460
|
-
* INSIDE the budget race after the entry function, where
|
|
461
|
-
* `requery(extra)` re-invokes it with `{...args, ...extra}
|
|
462
|
-
*
|
|
463
|
-
*
|
|
496
|
+
* - `followUp` optional; `async (result, ctx, requery, deliver) => result'`
|
|
497
|
+
* run INSIDE the budget race after the entry function, where
|
|
498
|
+
* `requery(extra)` re-invokes it with `{...args, ...extra}`
|
|
499
|
+
* and `deliver(partial)` replaces the fallback result. If it
|
|
500
|
+
* throws or overruns the budget, the last delivered result
|
|
501
|
+
* stands (recorded `followUp: 'threw'|'budget-exceeded'`).
|
|
502
|
+
* - `render` is a PURE function of the result (#1396): anything it names
|
|
503
|
+
* that the entry function does not return is resolved in `followUp`,
|
|
504
|
+
* inside the budget — never in the renderer.
|
|
505
|
+
* - `telemetry` optional; PURE `(result) => object|undefined` — the numbers
|
|
506
|
+
* the probe measured, persisted as `measure` on its element
|
|
507
|
+
* of `orchestrator.probes.completed` for `ran-*` outcomes
|
|
508
|
+
* (#1489), bounded by {@link probeMeasure}.
|
|
464
509
|
*/
|
|
465
510
|
export const PROBES = [
|
|
466
511
|
{
|
|
@@ -514,8 +559,8 @@ export const PROBES = [
|
|
|
514
559
|
args: ({ repoRoot }) => ({ repoRoot }),
|
|
515
560
|
// #1332: on `no-pipeline-for-head-sha` / `no-check-runs-for-head`, query
|
|
516
561
|
// the last PUSHED commit's verdict via `checkCiStatus({ sha })` — see the
|
|
517
|
-
// ceiling on the function.
|
|
518
|
-
followUp:
|
|
562
|
+
// ceiling on the function. #1396: also resolves every SHA the renderer names.
|
|
563
|
+
followUp: ciStatusFollowUp,
|
|
519
564
|
// Bespoke shape: `{status, ok, details, …}` with no `message` field. The
|
|
520
565
|
// banner text is prescribed by SKILL.md § Phase 4.
|
|
521
566
|
//
|
|
@@ -525,7 +570,7 @@ export const PROBES = [
|
|
|
525
570
|
// probe. Without these two lines a degraded ci-status result scored `'ok'`
|
|
526
571
|
// and rendered nothing — "could not read" displayed exactly like "green",
|
|
527
572
|
// which is the confusion the probe's own migration removed one layer down.
|
|
528
|
-
render: (r
|
|
573
|
+
render: (r) => {
|
|
529
574
|
if (!r || typeof r !== 'object') return null;
|
|
530
575
|
if (r.degraded) return typeof r.message === 'string' && r.message ? r.message : null;
|
|
531
576
|
// `status: 'unknown'` is the SAME collapse one level over: HEAD carries
|
|
@@ -539,7 +584,7 @@ export const PROBES = [
|
|
|
539
584
|
// #1337: a RED pushed commit is an alert, and the banner says what the
|
|
540
585
|
// rule judges (HR-106) — same 🚨 as a red HEAD.
|
|
541
586
|
const mark = r.pushed?.verdict?.status === 'red' ? '🚨' : '⚠';
|
|
542
|
-
return `${mark} ci-status: CI status for HEAD could not be determined (${reason}) — ${ciUnknownHint(r
|
|
587
|
+
return `${mark} ci-status: CI status for HEAD could not be determined (${reason}) — ${ciUnknownHint(r)}`;
|
|
543
588
|
}
|
|
544
589
|
if (r.status === 'red') {
|
|
545
590
|
const pid = r.details?.currentPipelineId ?? '?';
|
|
@@ -556,7 +601,7 @@ export const PROBES = [
|
|
|
556
601
|
lines.push(`⚠ CI green on HEAD, but ${r.allowFailureJobs.length} allow_failure job(s) FAILED: ${names}. A pipeline reports success regardless of these.`);
|
|
557
602
|
}
|
|
558
603
|
// #1390 P5: a failed same-commit pipeline the ref preference set aside.
|
|
559
|
-
if (droppedFailedCount(r) > 0) lines.push(droppedFailedLine(r
|
|
604
|
+
if (droppedFailedCount(r) > 0) lines.push(droppedFailedLine(r));
|
|
560
605
|
// Both findings can hold at once; `pushBanner` takes a multi-line
|
|
561
606
|
// string verbatim, so neither swallows the other.
|
|
562
607
|
return lines.length > 0 ? lines.join('\n') : null;
|
|
@@ -705,6 +750,40 @@ export const PROBES = [
|
|
|
705
750
|
network: false,
|
|
706
751
|
args: ({ repoRoot, env }) => ({ repoRoot, env }),
|
|
707
752
|
},
|
|
753
|
+
{
|
|
754
|
+
// #1401 part 3: archive COUNT retention vs the TIME window the ledger's
|
|
755
|
+
// readers declare (`REQUIRED_EVENTS_WINDOW_DAYS`). Without any ledger there
|
|
756
|
+
// is nothing to measure — recorded as `skipped`, never as `ran-clean`. A
|
|
757
|
+
// bare, empty or foreign-only `_archive/` is no ledger either: the probe
|
|
758
|
+
// answers those `not-measured` at severity ok, which the runner would
|
|
759
|
+
// record as `ran-clean` (see {@link hasRotationArchive}).
|
|
760
|
+
id: 'events-retention',
|
|
761
|
+
spec: local('./events-retention-banner.mjs'),
|
|
762
|
+
fn: 'checkEventsRetention',
|
|
763
|
+
network: false,
|
|
764
|
+
precondition: ({ repoRoot }) => {
|
|
765
|
+
const metrics = path.join(repoRoot, '.orchestrator', 'metrics');
|
|
766
|
+
return existsSync(path.join(metrics, 'events.jsonl')) || hasRotationArchive(metrics)
|
|
767
|
+
? null
|
|
768
|
+
: 'no-events-ledger';
|
|
769
|
+
},
|
|
770
|
+
args: ({ repoRoot, config }) => ({ repoRoot, config }),
|
|
771
|
+
// #1489: the coverage reaches the ledger on every measured answer, warn or
|
|
772
|
+
// not — a warning that never fires is otherwise indistinguishable from a
|
|
773
|
+
// dead one (HR-105). Counts and day spans only: no path, no timestamp.
|
|
774
|
+
// A degraded answer carries no `archives`, so it persists no measure. Every
|
|
775
|
+
// measured answer has a finite `coverageDays` — a span with no stamp is
|
|
776
|
+
// degraded since #1498 — and `probeMeasure` drops a non-finite value, so a
|
|
777
|
+
// regression there reads as "not measured", never as a number.
|
|
778
|
+
telemetry: (r) => (Number.isInteger(r?.archives)
|
|
779
|
+
? {
|
|
780
|
+
coverage_days: Math.round(r.coverageDays * 1000) / 1000,
|
|
781
|
+
archives: r.archives,
|
|
782
|
+
max_backups: r.maxBackups,
|
|
783
|
+
required_days: r.requiredDays ?? null,
|
|
784
|
+
}
|
|
785
|
+
: undefined),
|
|
786
|
+
},
|
|
708
787
|
];
|
|
709
788
|
|
|
710
789
|
// ---------------------------------------------------------------------------
|
|
@@ -730,6 +809,68 @@ function pluginVersion() {
|
|
|
730
809
|
}
|
|
731
810
|
}
|
|
732
811
|
|
|
812
|
+
/**
|
|
813
|
+
* NAMED CEILING (BV-004): keys per probe `measure`. `events-retention`, the one
|
|
814
|
+
* probe with telemetry today, persists 4. Revisit when a probe needs more than
|
|
815
|
+
* 8 — the cap exists so one probe cannot inflate every SessionStart record.
|
|
816
|
+
*/
|
|
817
|
+
const MEASURE_MAX_KEYS = 8;
|
|
818
|
+
const MEASURE_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/;
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* A probe's `telemetry(result)`, bounded for the ledger (#1489): at most
|
|
822
|
+
* {@link MEASURE_MAX_KEYS} snake_case keys whose values are finite numbers or
|
|
823
|
+
* `null`; any other entry is dropped. Strings are refused outright, not
|
|
824
|
+
* vetted: `orchestrator.probes.completed` also travels over the optional
|
|
825
|
+
* webhook unredacted, and a string is how a path or a repo slug gets there.
|
|
826
|
+
* A missing, throwing or empty telemetry yields `undefined` — no `measure`
|
|
827
|
+
* key, which reads as "not measured", never as zero.
|
|
828
|
+
*
|
|
829
|
+
* Reading the returned object stays inside the `try`: `Object.entries` runs
|
|
830
|
+
* its getters and a Proxy's `ownKeys` trap, and either may throw — which,
|
|
831
|
+
* outside it, rejected the whole runner against its never-rejects contract.
|
|
832
|
+
*
|
|
833
|
+
* @param {{telemetry?: Function}} probe
|
|
834
|
+
* @param {*} result — the probe's delivered result
|
|
835
|
+
* @returns {Record<string, number|null>|undefined}
|
|
836
|
+
*/
|
|
837
|
+
function probeMeasure(probe, result) {
|
|
838
|
+
if (typeof probe.telemetry !== 'function') return undefined;
|
|
839
|
+
try {
|
|
840
|
+
const raw = probe.telemetry(result);
|
|
841
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
842
|
+
const entries = Object.entries(raw)
|
|
843
|
+
.filter(([k, v]) => MEASURE_KEY_RE.test(k) && (v === null || (typeof v === 'number' && Number.isFinite(v))))
|
|
844
|
+
.slice(0, MEASURE_MAX_KEYS);
|
|
845
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
846
|
+
} catch {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Does `<metricsDir>/_archive/` hold at least one rotated events archive? The
|
|
853
|
+
* `events-retention` precondition's second half.
|
|
854
|
+
*
|
|
855
|
+
* It lists the directory exactly as `checkEventsRetention` does (same
|
|
856
|
+
* `ARCHIVE_NAME_RE` filter), so the precondition skips precisely the inputs the
|
|
857
|
+
* probe would answer `not-measured`. The directory existing is not a ledger
|
|
858
|
+
* (#1401 review: an empty or foreign-only `_archive/` was recorded
|
|
859
|
+
* `ran-clean`). Any listing error other than ENOENT answers `true`: the probe
|
|
860
|
+
* then runs and reports the unlistable directory as `degraded`, because an
|
|
861
|
+
* unreadable source must never be skipped as absent.
|
|
862
|
+
*
|
|
863
|
+
* @param {string} metricsDir — `<repoRoot>/.orchestrator/metrics`
|
|
864
|
+
* @returns {boolean}
|
|
865
|
+
*/
|
|
866
|
+
function hasRotationArchive(metricsDir) {
|
|
867
|
+
try {
|
|
868
|
+
return readdirSync(path.join(metricsDir, ARCHIVE_DIR_NAME)).some((n) => ARCHIVE_NAME_RE.test(n));
|
|
869
|
+
} catch (err) {
|
|
870
|
+
return err?.code !== 'ENOENT';
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
733
874
|
/**
|
|
734
875
|
* Race `promise` against a budget denominated in the probe's OWN work time —
|
|
735
876
|
* wall-clock elapsed MINUS the time a synchronous sibling held the event loop
|
|
@@ -973,7 +1114,12 @@ export async function runSessionStartProbes(opts = {}, deps = {}) {
|
|
|
973
1114
|
// the race below: a timeout with `delivered` set is `delivered`.
|
|
974
1115
|
try {
|
|
975
1116
|
return {
|
|
976
|
-
__probeResult: await probe.followUp(
|
|
1117
|
+
__probeResult: await probe.followUp(
|
|
1118
|
+
first,
|
|
1119
|
+
ctx,
|
|
1120
|
+
(extra) => fn({ ...probe.args(ctx), ...extra }),
|
|
1121
|
+
(partial) => { delivered = { __probeResult: partial }; },
|
|
1122
|
+
),
|
|
977
1123
|
};
|
|
978
1124
|
} catch {
|
|
979
1125
|
followUpFailure = 'threw';
|
|
@@ -1001,24 +1147,49 @@ export async function runSessionStartProbes(opts = {}, deps = {}) {
|
|
|
1001
1147
|
if (raced.__absent || raced.__probeError?.code === 'ERR_MODULE_NOT_FOUND') {
|
|
1002
1148
|
record('skipped', { reason: 'module-absent' });
|
|
1003
1149
|
} else {
|
|
1004
|
-
|
|
1150
|
+
// The thrown value is probe-supplied: String() of an object without a
|
|
1151
|
+
// prototype (or with a throwing toString) throws itself, which would
|
|
1152
|
+
// reject the runner — the contract is that it never rejects.
|
|
1153
|
+
let reason;
|
|
1154
|
+
try {
|
|
1155
|
+
reason = String(raced.__probeError?.message ?? raced.__probeError).slice(0, 200);
|
|
1156
|
+
} catch {
|
|
1157
|
+
reason = 'probe-threw-unprintable';
|
|
1158
|
+
}
|
|
1159
|
+
record('error', { reason });
|
|
1005
1160
|
}
|
|
1006
1161
|
return;
|
|
1007
1162
|
}
|
|
1008
1163
|
|
|
1009
1164
|
const result = raced?.__probeResult;
|
|
1010
|
-
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1165
|
+
// A custom `severityOf`, a `render`, or a getter on the result the
|
|
1166
|
+
// defaults read is probe-supplied code that can throw. The runner never
|
|
1167
|
+
// rejects, so such a throw degrades this probe the way a throwing
|
|
1168
|
+
// precondition does: `error` with a fixed reason, no banner line.
|
|
1169
|
+
let severity;
|
|
1170
|
+
let line;
|
|
1171
|
+
let stage = 'severity-threw';
|
|
1172
|
+
try {
|
|
1173
|
+
severity = severityOf(result, probe);
|
|
1174
|
+
stage = 'render-threw';
|
|
1175
|
+
// The renderer gets the RESULT only (#1396): no `ctx`, so it has no
|
|
1176
|
+
// repo to spawn git against. It runs after the budget race, where
|
|
1177
|
+
// nothing it did would be measured; a repo-local fact a banner names
|
|
1178
|
+
// is resolved in the probe's `followUp`, inside the race, and carried
|
|
1179
|
+
// as data.
|
|
1180
|
+
line = typeof probe.render === 'function'
|
|
1181
|
+
? probe.render(result)
|
|
1182
|
+
: defaultRender(result, severity);
|
|
1183
|
+
} catch {
|
|
1184
|
+
record('error', { reason: stage, workMs });
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const measure = probeMeasure(probe, result);
|
|
1018
1188
|
record(severity === 'ok' ? 'ran-clean' : severity === 'warn' ? 'ran-warn' : 'ran-alert', {
|
|
1019
1189
|
severity,
|
|
1020
1190
|
workMs,
|
|
1021
1191
|
...(followUpFailure ? { followUp: followUpFailure } : {}),
|
|
1192
|
+
...(measure ? { measure } : {}),
|
|
1022
1193
|
...(line ? { line } : {}),
|
|
1023
1194
|
});
|
|
1024
1195
|
}),
|
|
@@ -1082,6 +1253,9 @@ export async function runSessionStartProbes(opts = {}, deps = {}) {
|
|
|
1082
1253
|
// A follow-up that fell back to the delivered result is otherwise
|
|
1083
1254
|
// invisible (the outcome is `ran-*`); this is its revisit trigger's input.
|
|
1084
1255
|
...(typeof r.followUp === 'string' ? { follow_up: r.followUp } : {}),
|
|
1256
|
+
// What the probe MEASURED, warn or not (#1489, HR-105): without it a
|
|
1257
|
+
// probe that never warns reads the same whether it is rare or dead.
|
|
1258
|
+
...(r.measure ? { measure: r.measure } : {}),
|
|
1085
1259
|
})),
|
|
1086
1260
|
};
|
|
1087
1261
|
|