claude-code-session-manager 0.94.0 → 0.96.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/dist/assets/{DataModel-B0LDnnSL.js → DataModel-T0D00BpK.js} +1 -1
- package/dist/assets/History-DuAovTr7.js +2 -0
- package/dist/assets/{Hooks-CwLnp0Z_.js → Hooks-CgP9jsAN.js} +3 -3
- package/dist/assets/{HostBilko-CwEHEKYk.js → HostBilko-DHfz3Mxa.js} +1 -1
- package/dist/assets/{Library-BU9np05E.js → Library--poVP-d1.js} +1 -1
- package/dist/assets/{MarkdownEditor-B74ogK2f.js → MarkdownEditor-C56-Pc4c.js} +1 -1
- package/dist/assets/{McpServers-BLM3ftzP.js → McpServers-D-ykP4Vi.js} +2 -2
- package/dist/assets/Memory-BrkJjRL8.js +8 -0
- package/dist/assets/{Permissions-3pGLJmxQ.js → Permissions-BMpAaxJu.js} +3 -3
- package/dist/assets/Plugins-OuEe5S_T.js +2 -0
- package/dist/assets/{ProvenanceBadge-BnjBQiV9.js → ProvenanceBadge-4eNbDYe8.js} +1 -1
- package/dist/assets/{SaveBar-Zq2NfcPf.js → SaveBar-BQ8y9Lkv.js} +1 -1
- package/dist/assets/Scheduler-DcMT5sT_.js +16 -0
- package/dist/assets/{ScopeSwitcher-Cnojv22D.js → ScopeSwitcher-XdxtVKP-.js} +1 -1
- package/dist/assets/Settings-FdaPUq2t.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BRDiE6Hv.js → SkillReferenceGraph-CJzvvc0_.js} +1 -1
- package/dist/assets/Skills-Wvm9OE1e.js +3 -0
- package/dist/assets/{SystemPrompt-tFr19Od6.js → SystemPrompt--LYw_69j.js} +1 -1
- package/dist/assets/TagLibrary-Be6HbMMx.js +1 -0
- package/dist/assets/{TiptapBody-DGzSg3BO.js → TiptapBody-DzFlWMgO.js} +1 -1
- package/dist/assets/{Toggle-DzoROibf.js → Toggle-BxtAFE1i.js} +1 -1
- package/dist/assets/index-XkrLB67D.js +3079 -0
- package/dist/assets/{settingsSchema-BYKVe1WM.js → settingsSchema-BIvDZ1Cj.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +10 -2
- package/plugins/CLAUDE.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +83 -67
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
- package/scripts/README.md +4 -0
- package/scripts/hooks/guard-destructive-git.cjs +9 -374
- package/scripts/hooks/guard-inline-implementation.cjs +9 -128
- package/scripts/hooks/guard-prd-writes.cjs +9 -74
- package/scripts/hooks/guard-self-schedule.cjs +9 -61
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
- package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
- package/scripts/scheduler-mcp-server.cjs +8 -1
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
- package/src/main/__tests__/exchanges.test.cjs +41 -96
- package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/health-credentials.test.cjs +81 -0
- package/src/main/__tests__/health-tick-liveness.test.cjs +12 -0
- package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
- package/src/main/__tests__/planValidator.test.cjs +71 -0
- package/src/main/__tests__/prdCreate.test.cjs +3 -1
- package/src/main/__tests__/prdSizing.test.cjs +106 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +15 -0
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +89 -0
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
- package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
- package/src/main/__tests__/telemetryClient.test.cjs +2 -2
- package/src/main/__tests__/telemetryContract.test.cjs +2 -2
- package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
- package/src/main/__tests__/validationSentinels.test.cjs +84 -0
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +26 -14
- package/src/main/crashDiagnostics.cjs +9 -0
- package/src/main/exchanges.cjs +26 -21
- package/src/main/health.cjs +91 -5
- package/src/main/index.cjs +1 -1
- package/src/main/ipcSchemas.cjs +5 -1
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
- package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
- package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
- package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
- package/src/main/lib/__tests__/loopDelay.test.cjs +68 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
- package/src/main/lib/__tests__/usageCircuit.test.cjs +86 -17
- package/src/main/lib/agentModelResolve.cjs +1 -0
- package/src/main/lib/credentials.cjs +62 -0
- package/src/main/lib/cwdClassify.cjs +15 -0
- package/src/main/lib/definitionOfDone.cjs +9 -13
- package/src/main/lib/effectiveModelInfo.cjs +2 -9
- package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
- package/src/main/lib/loadGate.cjs +29 -5
- package/src/main/lib/loopDelay.cjs +44 -0
- package/src/main/lib/opsOwnership.cjs +23 -0
- package/src/main/lib/planValidator.cjs +28 -0
- package/src/main/lib/prdCreate.cjs +8 -4
- package/src/main/lib/prdSizing.cjs +83 -0
- package/src/main/lib/runLogRetention.cjs +2 -8
- package/src/main/lib/telemetryBacklog.cjs +9 -3
- package/src/main/lib/telemetryClient.cjs +12 -3
- package/src/main/lib/telemetrySettings.cjs +8 -2
- package/src/main/lib/usageCircuit.cjs +40 -12
- package/src/main/lib/validationSentinels.cjs +29 -0
- package/src/main/memoryAggregate.cjs +36 -11
- package/src/main/otelSettings.cjs +8 -2
- package/src/main/scheduler.cjs +264 -34
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/sessionsStore.cjs +8 -2
- package/src/main/supervisor.cjs +3 -3
- package/src/main/templates/PRD_AUTHORING.md +9 -9
- package/src/main/usage.cjs +9 -1
- package/src/main/voiceSettings.cjs +8 -2
- package/src/preload/api.d.ts +20 -5
- package/src/preload/index.cjs +5 -5
- package/src/seed/agents/dev-lead.md +24 -15
- package/src/seed/agents/validator.md +33 -0
- package/dist/assets/History-C685Kytt.js +0 -2
- package/dist/assets/Memory-DAP1t8bA.js +0 -8
- package/dist/assets/Plugins-Q1KoLEzn.js +0 -2
- package/dist/assets/Scheduler-D488ebKm.js +0 -16
- package/dist/assets/Settings-D5Lhj7ga.js +0 -3
- package/dist/assets/Skills-BQDqp-EN.js +0 -3
- package/dist/assets/TagLibrary-BjyrnaRE.js +0 -1
- package/dist/assets/index-CCl4tz-u.js +0 -3079
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* epicWorktreeProjectConfig.cjs — per-project UI toggle for Epic worktree
|
|
5
|
-
* isolation (PRD 1035, final link of the epic-worktree-isolation chain
|
|
5
|
+
* isolation (PRD 1035, final link of the epic-worktree-isolation chain;
|
|
6
|
+
* moved off the machine-wide global map onto the per-project ops root by
|
|
7
|
+
* PRD 1390).
|
|
6
8
|
*
|
|
7
9
|
* `SM_EPIC_WORKTREE_DISABLE` (gitWorktree.cjs) is an env var, checked once at
|
|
8
10
|
* process start — there was previously no way to turn the feature off for
|
|
9
11
|
* one project from the app itself. This module is that per-project knob:
|
|
10
|
-
*
|
|
12
|
+
* `<cwd>/session-manager-operations/prompt-sessions/epic-worktree-config.json`
|
|
13
|
+
* holding `{ disabled: boolean }` for that one project, read by
|
|
11
14
|
* gitWorktree.cjs's `isWorktreeDisabled('epic', cwd)` on every worktree
|
|
12
15
|
* creation attempt, and read/written by Settings.tsx's per-project toggle
|
|
13
16
|
* over the two IPC handlers registered below.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* By shape the old single global file (`{ "<project-cwd>": true|false }` under
|
|
19
|
+
* `~/.claude/session-manager/`) was per-project config keyed by project path —
|
|
20
|
+
* the wrong tier. It now lives in the `prompt-sessions` ops namespace, already
|
|
21
|
+
* owned by writer 'epics' (opsOwnership.cjs), the same as every other Epic
|
|
22
|
+
* record. Migration is lossless and one-shot: on first read for a cwd whose
|
|
23
|
+
* new per-project file doesn't exist yet, the legacy global map is checked for
|
|
24
|
+
* that cwd's key; if present (the map only ever stores `true` — `false` is
|
|
25
|
+
* represented by key absence), the new file is seeded with it. The legacy file
|
|
26
|
+
* itself is left in place — untouched, never deleted — so any OTHER project's
|
|
27
|
+
* still-unmigrated key keeps resolving correctly until that project is itself
|
|
28
|
+
* touched.
|
|
20
29
|
*
|
|
21
30
|
* Plain Node (no Electron deps in the read/write helpers) so gitWorktree.cjs
|
|
22
31
|
* — itself Electron-free — can require this lazily without pulling Electron
|
|
@@ -26,19 +35,22 @@
|
|
|
26
35
|
const fs = require('node:fs');
|
|
27
36
|
const path = require('node:path');
|
|
28
37
|
const os = require('node:os');
|
|
38
|
+
const { assertOpsWrite, opsPath } = require('./opsOwnership.cjs');
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
// The old machine-wide map. Read-only from here on (migration source only) —
|
|
41
|
+
// never written again, and deliberately never deleted so unmigrated projects
|
|
42
|
+
// keep working. Resolved per-call (not a frozen const) so tests can point
|
|
43
|
+
// this at a throwaway tmpdir file via SM_EPIC_WORKTREE_PROJECT_CONFIG_PATH
|
|
44
|
+
// instead of touching the real machine-level file.
|
|
45
|
+
const DEFAULT_LEGACY_CONFIG_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'epic-worktree-project-config.json');
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// mutating the real machine-level file at DEFAULT_CONFIG_PATH.
|
|
35
|
-
function configPath() {
|
|
36
|
-
return process.env.SM_EPIC_WORKTREE_PROJECT_CONFIG_PATH || DEFAULT_CONFIG_PATH;
|
|
47
|
+
function legacyConfigPath() {
|
|
48
|
+
return process.env.SM_EPIC_WORKTREE_PROJECT_CONFIG_PATH || DEFAULT_LEGACY_CONFIG_PATH;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
function
|
|
51
|
+
function readLegacyConfig() {
|
|
40
52
|
try {
|
|
41
|
-
const raw = fs.readFileSync(
|
|
53
|
+
const raw = fs.readFileSync(legacyConfigPath(), 'utf8');
|
|
42
54
|
const parsed = JSON.parse(raw);
|
|
43
55
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
44
56
|
} catch {
|
|
@@ -46,31 +58,62 @@ function readConfig() {
|
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
/** `<cwd>/session-manager-operations/prompt-sessions/epic-worktree-config.json` */
|
|
62
|
+
function perProjectConfigPath(cwd) {
|
|
63
|
+
return opsPath(cwd, 'prompt-sessions', 'epic-worktree-config.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readPerProjectConfigFile(cwd) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(perProjectConfigPath(cwd), 'utf8');
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writePerProjectConfig(cwd, config, writer = 'epics') {
|
|
77
|
+
const file = perProjectConfigPath(cwd);
|
|
78
|
+
assertOpsWrite(file, writer);
|
|
51
79
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
80
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
53
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2));
|
|
81
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
|
|
54
82
|
fs.renameSync(tmp, file);
|
|
55
83
|
}
|
|
56
84
|
|
|
85
|
+
/**
|
|
86
|
+
* One-shot migration: when the per-project file doesn't exist yet, seed it
|
|
87
|
+
* from the legacy global map's entry for this cwd (present only when it was
|
|
88
|
+
* explicitly disabled — the legacy map deletes the key on re-enable). Never
|
|
89
|
+
* touches the legacy file itself.
|
|
90
|
+
*/
|
|
91
|
+
function readOrMigratePerProjectConfig(cwd) {
|
|
92
|
+
const existing = readPerProjectConfigFile(cwd);
|
|
93
|
+
if (existing) return existing;
|
|
94
|
+
const legacyValue = readLegacyConfig()[cwd];
|
|
95
|
+
if (legacyValue !== true) return { disabled: false };
|
|
96
|
+
const seeded = { disabled: true };
|
|
97
|
+
writePerProjectConfig(cwd, seeded);
|
|
98
|
+
return seeded;
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
/** True when the project at `cwd` has explicitly turned Epic worktree isolation off. */
|
|
58
102
|
function isEpicWorktreeDisabledForProject(cwd) {
|
|
59
103
|
if (!cwd || typeof cwd !== 'string') return false;
|
|
60
|
-
|
|
104
|
+
try {
|
|
105
|
+
return readOrMigratePerProjectConfig(cwd).disabled === true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
61
109
|
}
|
|
62
110
|
|
|
63
|
-
/** Persists
|
|
64
|
-
function setEpicWorktreeDisabledForProject(cwd, disabled) {
|
|
111
|
+
/** Persists the per-project toggle. */
|
|
112
|
+
function setEpicWorktreeDisabledForProject(cwd, disabled, writer = 'epics') {
|
|
65
113
|
if (!cwd || typeof cwd !== 'string') throw new Error('setEpicWorktreeDisabledForProject: cwd is required');
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
delete config[cwd];
|
|
71
|
-
}
|
|
72
|
-
writeConfig(config);
|
|
73
|
-
return disabled;
|
|
114
|
+
const value = !!disabled;
|
|
115
|
+
writePerProjectConfig(cwd, { disabled: value }, writer);
|
|
116
|
+
return value;
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
function registerEpicWorktreeProjectConfigHandlers() {
|
|
@@ -94,8 +137,9 @@ function registerEpicWorktreeProjectConfigHandlers() {
|
|
|
94
137
|
}
|
|
95
138
|
|
|
96
139
|
module.exports = {
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
DEFAULT_LEGACY_CONFIG_PATH,
|
|
141
|
+
legacyConfigPath,
|
|
142
|
+
perProjectConfigPath,
|
|
99
143
|
isEpicWorktreeDisabledForProject,
|
|
100
144
|
setEpicWorktreeDisabledForProject,
|
|
101
145
|
registerEpicWorktreeProjectConfigHandlers,
|
|
@@ -57,12 +57,36 @@ function isLoadGated(loadavg1, cores, threshold) {
|
|
|
57
57
|
return loadavg1 / cores > threshold;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Best-effort top-N CPU consumers, for the audit line the scheduler warn-logs
|
|
62
|
+
* when the load gate has held jobs past its escalation window. Diagnostic only
|
|
63
|
+
* — nothing downstream branches on the result, so it fails to [] rather than
|
|
64
|
+
* throwing. Supported on Linux AND macOS (the two platforms this app targets);
|
|
65
|
+
* [] anywhere else or on any error.
|
|
66
|
+
*
|
|
67
|
+
* The `ps` invocation is platform-specific and NOT interchangeable:
|
|
68
|
+
* - Linux (GNU/procps): `ps -eo pid,pcpu,comm --sort=-pcpu`. `--sort` is a
|
|
69
|
+
* GNU long option.
|
|
70
|
+
* - macOS (BSD ps): has NO `--sort` (`ps: illegal option -- -`, which threw
|
|
71
|
+
* and left the gate audit logging `top CPU: n/a` on every Mac). BSD sorts
|
|
72
|
+
* with `-r` (by CPU usage) and takes `-A` for all processes, `-c` for the
|
|
73
|
+
* executable comm without its path. `ps -Aco pid,pcpu,comm -r` is the
|
|
74
|
+
* column-and-sort equivalent.
|
|
75
|
+
* Both print a `PID %CPU COMM` header we skip, so downstream parsing is shared.
|
|
76
|
+
* `execImpl` is injected only by the unit test; production uses execFileSync.
|
|
77
|
+
*/
|
|
78
|
+
function topCpuConsumers(n = 3, { execImpl = execFileSync, platform = process.platform } = {}) {
|
|
79
|
+
let args;
|
|
80
|
+
if (platform === 'linux') {
|
|
81
|
+
args = ['-eo', 'pid,pcpu,comm', '--sort=-pcpu'];
|
|
82
|
+
} else if (platform === 'darwin') {
|
|
83
|
+
args = ['-Aco', 'pid,pcpu,comm', '-r'];
|
|
84
|
+
} else {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
63
87
|
try {
|
|
64
|
-
const out =
|
|
65
|
-
return out.split('\n').slice(1, 1 + n).map((l) => l.trim()).filter(Boolean);
|
|
88
|
+
const out = execImpl('ps', args, { encoding: 'utf8', timeout: 2000 });
|
|
89
|
+
return String(out).split('\n').slice(1, 1 + n).map((l) => l.trim()).filter(Boolean);
|
|
66
90
|
} catch {
|
|
67
91
|
return [];
|
|
68
92
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event-loop delay probe for the main-process memory heartbeat. Wraps a
|
|
5
|
+
* perf_hooks IntervalHistogram (injectable for tests): enable() at init,
|
|
6
|
+
* snapshot() once per heartbeat returns the interval just ended in ms and
|
|
7
|
+
* reset()s, disable() rides the heartbeat teardown. No timer of its own.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { monitorEventLoopDelay } = require('node:perf_hooks');
|
|
11
|
+
|
|
12
|
+
const LOOP_DELAY_RESOLUTION_MS = 20;
|
|
13
|
+
// A single beat whose worst loop delay exceeds this is a visible main-thread stall.
|
|
14
|
+
const LOOP_STALL_WARN_MS = 250;
|
|
15
|
+
|
|
16
|
+
const NS_PER_MS = 1e6;
|
|
17
|
+
const toMs = (ns) => Math.round(ns / NS_PER_MS * 10) / 10;
|
|
18
|
+
|
|
19
|
+
function createLoopDelayMonitor(createHistogram = monitorEventLoopDelay) {
|
|
20
|
+
const h = createHistogram({ resolution: LOOP_DELAY_RESOLUTION_MS });
|
|
21
|
+
return {
|
|
22
|
+
enable() { h.enable(); },
|
|
23
|
+
disable() { h.disable(); },
|
|
24
|
+
/** Percentiles for the interval since the last snapshot; resets the histogram. */
|
|
25
|
+
snapshot() {
|
|
26
|
+
const out = {
|
|
27
|
+
loopDelayP50Ms: toMs(h.percentile(50)),
|
|
28
|
+
loopDelayP99Ms: toMs(h.percentile(99)),
|
|
29
|
+
loopDelayMaxMs: toMs(h.max),
|
|
30
|
+
};
|
|
31
|
+
h.reset();
|
|
32
|
+
return out;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Log level + message for a heartbeat given its loop-delay snapshot. */
|
|
38
|
+
function stallVerdict(snap) {
|
|
39
|
+
return snap.loopDelayMaxMs > LOOP_STALL_WARN_MS
|
|
40
|
+
? { level: 'warn', message: 'main loop stall' }
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { createLoopDelayMonitor, stallVerdict, LOOP_STALL_WARN_MS, LOOP_DELAY_RESOLUTION_MS };
|
|
@@ -60,6 +60,15 @@ const OWNERS = Object.freeze({
|
|
|
60
60
|
// bilko-host-publisher Epic authors beyond dist/ is agent-Write-tool
|
|
61
61
|
// output, same unenforceable-by-construction class as project-pages/output.
|
|
62
62
|
'bilko-host': 'bilko-host',
|
|
63
|
+
// Memory Clusters owns its own regenerable per-project cache (PRD 1389) —
|
|
64
|
+
// memoryAggregate.cjs's clustering result, rebuilt on any explicit
|
|
65
|
+
// refresh:true. See memory-clusters/README.md.
|
|
66
|
+
'memory-clusters': 'memory-clusters',
|
|
67
|
+
// Per-project UI state (PRD 1398) — small renderer prefs that used to live
|
|
68
|
+
// in cwd-scoped or (worse) global localStorage keys. Written directly by
|
|
69
|
+
// the renderer through the generic config:write-json IPC with writer:
|
|
70
|
+
// 'ui-prefs'; no dedicated main-process module. See ui-prefs/README.md.
|
|
71
|
+
'ui-prefs': 'ui-prefs',
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
/**
|
|
@@ -170,6 +179,20 @@ function assertOpsWrite(absPath, writer) {
|
|
|
170
179
|
// worktree-cwd hazard (PRD 1082; incidents 2026-08-30, 2026-09-01).
|
|
171
180
|
const { inOps } = parseOpsPath(absPath);
|
|
172
181
|
if (inOps) {
|
|
182
|
+
// classifyCwd fails closed on a non-absolute absPath (returns
|
|
183
|
+
// innermostOpsRoot: null) — path.dirname(null) throws a raw, untagged
|
|
184
|
+
// TypeError, which would crash this "last line of defense" instead of
|
|
185
|
+
// producing the graceful, catchable refusal every other branch here
|
|
186
|
+
// gives. A caller that skips the absolute-path helpers (opsPath/
|
|
187
|
+
// resolveProjectRoot) and hands assertOpsWrite a relative fragment
|
|
188
|
+
// containing an OPS_ROOT_DIR segment must still fail closed, not crash.
|
|
189
|
+
if (typeof absPath !== 'string' || !path.isAbsolute(absPath)) {
|
|
190
|
+
const err = new Error(
|
|
191
|
+
`refusing to write ${OPS_ROOT_DIR}/ state: absPath must be absolute, got "${absPath}"`,
|
|
192
|
+
);
|
|
193
|
+
err.unknownCwd = true;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
173
196
|
// innermostOpsRoot (lastIndexOf) is the fact THIS gate keys on; activeSessions
|
|
174
197
|
// truncates at the outermost instead — cwdClassify returns both, unified in
|
|
175
198
|
// neither direction.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* planValidator.cjs — pure predicate telling a work-item's dispatch whether a
|
|
5
|
+
* plan-level `validator` job (PRD 1405/1407) will re-review its diff once the
|
|
6
|
+
* plan finishes, so the per-job finish protocol and per-PRD validation prompt
|
|
7
|
+
* (PRD 986) can both yield to it instead of duplicating the same review. No
|
|
8
|
+
* filesystem or scheduler knowledge — the caller supplies the queue rows.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* hasDownstreamValidator(job, jobs) → boolean
|
|
13
|
+
*
|
|
14
|
+
* True iff `jobs` contains a row with `agentType === 'validator'`, status
|
|
15
|
+
* `pending` or `running`, whose `dependsOn` names `job.slug`. A validator job
|
|
16
|
+
* itself never has a downstream validator (it IS the plan's validation pass).
|
|
17
|
+
*/
|
|
18
|
+
function hasDownstreamValidator(job, jobs) {
|
|
19
|
+
if (!job || !Array.isArray(jobs)) return false;
|
|
20
|
+
if (job.agentType === 'validator') return false;
|
|
21
|
+
return jobs.some((row) => row
|
|
22
|
+
&& row.agentType === 'validator'
|
|
23
|
+
&& (row.status === 'pending' || row.status === 'running')
|
|
24
|
+
&& Array.isArray(row.dependsOn)
|
|
25
|
+
&& row.dependsOn.includes(job.slug));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { hasDownstreamValidator };
|
|
@@ -31,6 +31,7 @@ const { DEFAULT_PRD_AGENT_TYPE, assertAgentTypeWritable } = require('./prdAgentT
|
|
|
31
31
|
const { resolveDepSlug, findNearMatches } = require('./depSlugResolve.cjs');
|
|
32
32
|
const { isFixPlanSlug } = require('./fixPlanSlug.cjs');
|
|
33
33
|
const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
|
|
34
|
+
const { sizingWarnings } = require('./prdSizing.cjs');
|
|
34
35
|
|
|
35
36
|
// A caller-supplied slug that already starts with its own `NN-` (e.g.
|
|
36
37
|
// "254-perf-x") used to silently become the double-prefixed row
|
|
@@ -133,10 +134,10 @@ function buildPrdBody(input) {
|
|
|
133
134
|
const oosLines = oosSource.map((line) => `- ${line}`).join('\n');
|
|
134
135
|
|
|
135
136
|
const standardsPointer = [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
'
|
|
139
|
-
'
|
|
137
|
+
'Your system prompt carries the ordered run contract.',
|
|
138
|
+
`\`${STANDARDS_PATH}\` holds the reasoning behind each contract line (Performance, Debugging,`,
|
|
139
|
+
'API reuse, TDD, Execution discipline) — read the section a line points at when it is unclear;',
|
|
140
|
+
'do not re-read the whole file every run.',
|
|
140
141
|
].join('\n');
|
|
141
142
|
|
|
142
143
|
const bodyLines = [
|
|
@@ -496,6 +497,8 @@ async function createPrd(input, remote) {
|
|
|
496
497
|
epicId: writeResult.epicId ?? null,
|
|
497
498
|
enqueued: false,
|
|
498
499
|
note: 'PRD file written; the queue row is derived by the next scheduler reconcile pass, not created here',
|
|
500
|
+
// Advisory only (PRD 1403) — never blocks the write. See prdSizing.cjs.
|
|
501
|
+
warnings: sizingWarnings(input),
|
|
499
502
|
};
|
|
500
503
|
}
|
|
501
504
|
|
|
@@ -541,6 +544,7 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
541
544
|
epicId: result.epicId ?? null,
|
|
542
545
|
enqueued: false,
|
|
543
546
|
note: result.note,
|
|
547
|
+
warnings: result.warnings,
|
|
544
548
|
});
|
|
545
549
|
});
|
|
546
550
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdSizing.cjs — pure PRD-size warning rules (PRD 1403).
|
|
3
|
+
*
|
|
4
|
+
* The measured actual/estimate ratio across scheduler runs sits at 0.24 (p50) with 60% of
|
|
5
|
+
* jobs finishing in <=10 min, yet nothing at the authoring gate flags an oversized PRD before
|
|
6
|
+
* it lands. `sizingWarnings` is the single rule set; prdCreate.cjs's createPrd() surfaces its
|
|
7
|
+
* output in the create-prd response, and scheduler-mcp-server.cjs's scheduler_create_prd
|
|
8
|
+
* handler prints it as a "Sizing warnings:" block. Warnings never block a write.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const SIZING_LIMITS = {
|
|
13
|
+
estimateMinutes: 15,
|
|
14
|
+
acLines: 8,
|
|
15
|
+
acLineChars: 400,
|
|
16
|
+
bodyChars: 7000,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Open-ended search-and-fix shape, e.g. "grep the codebase and update every call site." Matches
|
|
20
|
+
// across sentence boundaries within one AC line ([\s\S]*, not [^.]*) — a natural two-sentence
|
|
21
|
+
// phrasing like "Search the codebase for X. Update every call site." is exactly the shape this
|
|
22
|
+
// rule exists to catch, and a period must not be enough to dodge it.
|
|
23
|
+
const OPEN_ENDED_RE = /\b(grep|search|scan|audit|find)\b[\s\S]*\b(and|then)\b[\s\S]*\b(update|fix|adjust|migrate|change)\b/i;
|
|
24
|
+
const TIMEOUT_CMD_RE = /\btimeout\s+\d+/gi;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pure — no I/O. Returns one human-readable warning per triggered rule, `[]` when nothing
|
|
28
|
+
* is triggered. `input` mirrors createPrd()'s own input shape; missing/non-array fields are
|
|
29
|
+
* treated as empty/absent rather than thrown on, since this is advisory only.
|
|
30
|
+
*/
|
|
31
|
+
function sizingWarnings(input) {
|
|
32
|
+
const { estimateMinutes, goal, implementationNotes } = input || {};
|
|
33
|
+
const acceptanceCriteria = Array.isArray(input?.acceptanceCriteria) ? input.acceptanceCriteria : [];
|
|
34
|
+
const acLines = acceptanceCriteria.filter((line) => typeof line === 'string');
|
|
35
|
+
|
|
36
|
+
const warnings = [];
|
|
37
|
+
|
|
38
|
+
if (typeof estimateMinutes === 'number' && estimateMinutes > SIZING_LIMITS.estimateMinutes) {
|
|
39
|
+
warnings.push(
|
|
40
|
+
`estimateMinutes (${estimateMinutes}) exceeds the ${SIZING_LIMITS.estimateMinutes}-minute sizing limit — split this PRD into smaller work items.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (acceptanceCriteria.length > SIZING_LIMITS.acLines) {
|
|
45
|
+
warnings.push(
|
|
46
|
+
`acceptanceCriteria has ${acceptanceCriteria.length} lines, exceeding the ${SIZING_LIMITS.acLines}-line sizing limit — split into smaller, single-purpose PRDs.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (acLines.some((line) => line.length > SIZING_LIMITS.acLineChars)) {
|
|
51
|
+
warnings.push(
|
|
52
|
+
`One or more acceptance criteria lines exceed ${SIZING_LIMITS.acLineChars} characters — break the line up into smaller, single-purpose criteria.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (acLines.some((line) => OPEN_ENDED_RE.test(line))) {
|
|
57
|
+
warnings.push(
|
|
58
|
+
'One or more acceptance criteria lines describe an open-ended search-and-fix task (e.g. "grep ... and update ...") — scope it to a concrete, bounded change instead.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (acLines.some((line) => {
|
|
63
|
+
const matches = line.match(TIMEOUT_CMD_RE);
|
|
64
|
+
return matches && matches.length >= 2;
|
|
65
|
+
})) {
|
|
66
|
+
warnings.push(
|
|
67
|
+
'One or more acceptance criteria lines run two or more `timeout <n>` commands — split into separate criteria, one command each.',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bodyChars = String(goal || '').length
|
|
72
|
+
+ String(implementationNotes || '').length
|
|
73
|
+
+ acLines.join('\n').length;
|
|
74
|
+
if (bodyChars > SIZING_LIMITS.bodyChars) {
|
|
75
|
+
warnings.push(
|
|
76
|
+
`Combined goal + implementation notes + acceptance criteria is ${bodyChars} characters, exceeding the ${SIZING_LIMITS.bodyChars}-character sizing limit — split into smaller PRDs.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return warnings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { SIZING_LIMITS, sizingWarnings };
|
|
@@ -43,13 +43,8 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
const fs = require('node:fs');
|
|
46
|
-
const os = require('node:os');
|
|
47
46
|
const path = require('node:path');
|
|
48
|
-
|
|
49
|
-
const DEFAULT_RUNS_DIR = path.join(
|
|
50
|
-
os.homedir(),
|
|
51
|
-
'.claude', 'session-manager', 'scheduled-plans', 'runs'
|
|
52
|
-
);
|
|
47
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
53
48
|
|
|
54
49
|
// Any status that is not yet a terminal outcome. Mirrors the status literals
|
|
55
50
|
// used throughout scheduler.cjs (see e.g. its DOD_SLUG_RE-adjacent status
|
|
@@ -436,14 +431,13 @@ function applyRetention(runsDir, settings, opts) {
|
|
|
436
431
|
* startup timer, alongside finalizeClosedDays) — never on its own timer.
|
|
437
432
|
*/
|
|
438
433
|
function runBootSweep(opts) {
|
|
439
|
-
const runsDir = (opts && opts.runsDir) ||
|
|
434
|
+
const runsDir = (opts && opts.runsDir) || schedulerPaths.runsDir();
|
|
440
435
|
const queueStore = require('./queueStore.cjs');
|
|
441
436
|
const state = queueStore.readMergedSync();
|
|
442
437
|
return applyRetention(runsDir, state.config || {}, { jobs: state.jobs || [] });
|
|
443
438
|
}
|
|
444
439
|
|
|
445
440
|
module.exports = {
|
|
446
|
-
DEFAULT_RUNS_DIR,
|
|
447
441
|
LIVE_STATUSES,
|
|
448
442
|
isLiveJob,
|
|
449
443
|
liveKeysFromJobs,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* a conservative, delivery-confirmed offset) at once — see the two-phase
|
|
22
22
|
* design below.
|
|
23
23
|
*
|
|
24
|
-
* TWO-PHASE WATERMARK. Per file, ~/.
|
|
24
|
+
* TWO-PHASE WATERMARK. Per file, ~/.claude/session-manager/telemetry-
|
|
25
25
|
* watermarks.json tracks:
|
|
26
26
|
* - bytesEnqueued: how far we've read + handed lines to telemetryClient.
|
|
27
27
|
* Advances the instant a line has been processed
|
|
@@ -85,8 +85,14 @@ function lastRunSummary() {
|
|
|
85
85
|
return lastSummary ? { ...lastSummary } : null;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
let migrated = false;
|
|
88
89
|
function watermarksPath() {
|
|
89
|
-
|
|
90
|
+
const p = path.join(os.homedir(), '.claude', 'session-manager', 'telemetry-watermarks.json');
|
|
91
|
+
if (!migrated) {
|
|
92
|
+
migrated = true;
|
|
93
|
+
config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'telemetry-watermarks.json'), p);
|
|
94
|
+
}
|
|
95
|
+
return p;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function resolveDeps(deps = {}) {
|
|
@@ -154,7 +160,7 @@ function entryFor(wm, key) {
|
|
|
154
160
|
|
|
155
161
|
/**
|
|
156
162
|
* Known projects come from the same source of truth the renderer restores
|
|
157
|
-
* tabs from (sessionsStore.cjs's ~/.
|
|
163
|
+
* tabs from (sessionsStore.cjs's ~/.claude/session-manager/tabs.json — TAB =
|
|
158
164
|
* cwd = Main Project), deduped by normalized cwd, with ephemeral (worktree /
|
|
159
165
|
* tmpdir) cwds excluded before normalization even runs, since a worktree cwd
|
|
160
166
|
* would otherwise resolve back to its real project and be scanned twice
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* through the same queue/dedup/backoff machinery but sent one record at a
|
|
8
8
|
* time via sendSingle() rather than sendBatch().
|
|
9
9
|
*
|
|
10
|
-
* Records accumulate durably in ~/.
|
|
10
|
+
* Records accumulate durably in ~/.claude/session-manager/telemetry-queue.jsonl
|
|
11
11
|
* the instant they're accepted, and are only ever sent by flush(reason) — on
|
|
12
12
|
* a deliberate cadence (boot / daily / version-change / quit / manual), never
|
|
13
13
|
* on a short interval. Every record is idempotent by recordId and stamped at
|
|
@@ -111,12 +111,21 @@ function logWarn(message, meta) {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* SM_TELEMETRY_SPOOL, when set, overrides the spool directory in place of
|
|
114
|
-
* `~/.
|
|
114
|
+
* `~/.claude/session-manager` — the explicit opt-in a test that genuinely
|
|
115
115
|
* needs to exercise real queue/sent-file I/O uses to prove it isn't about to
|
|
116
116
|
* write into a real user's spool (see isTestEnvironment() below).
|
|
117
117
|
*/
|
|
118
|
+
let spoolMigrated = false;
|
|
118
119
|
function spoolDir() {
|
|
119
|
-
|
|
120
|
+
if (process.env.SM_TELEMETRY_SPOOL) return process.env.SM_TELEMETRY_SPOOL;
|
|
121
|
+
const dir = path.join(os.homedir(), '.claude', 'session-manager');
|
|
122
|
+
if (!spoolMigrated) {
|
|
123
|
+
spoolMigrated = true;
|
|
124
|
+
const oldDir = path.join(os.homedir(), '.config', 'session-manager');
|
|
125
|
+
config.migrateLegacyHomeFile(path.join(oldDir, 'telemetry-queue.jsonl'), path.join(dir, 'telemetry-queue.jsonl'));
|
|
126
|
+
config.migrateLegacyHomeFile(path.join(oldDir, 'telemetry-sent.json'), path.join(dir, 'telemetry-sent.json'));
|
|
127
|
+
}
|
|
128
|
+
return dir;
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
function queuePath() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* telemetrySettings — persists product-telemetry consent/config + install identity.
|
|
3
3
|
*
|
|
4
|
-
* Storage: ~/.
|
|
4
|
+
* Storage: ~/.claude/session-manager/telemetry.json
|
|
5
5
|
* Shape: {
|
|
6
6
|
* enabled: boolean, // ON by default — hard kill switch is SM_TELEMETRY=0
|
|
7
7
|
* installId: string, // crypto.randomUUID(), minted once, the only identity
|
|
@@ -45,8 +45,14 @@ const DEFAULTS = Object.freeze({
|
|
|
45
45
|
|
|
46
46
|
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
47
47
|
|
|
48
|
+
let migrated = false;
|
|
48
49
|
function storePath() {
|
|
49
|
-
|
|
50
|
+
const p = path.join(os.homedir(), '.claude', 'session-manager', 'telemetry.json');
|
|
51
|
+
if (!migrated) {
|
|
52
|
+
migrated = true;
|
|
53
|
+
config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'telemetry.json'), p);
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
function isValid(cfg) {
|
|
@@ -50,19 +50,44 @@ function isResetFresh(iso, now) {
|
|
|
50
50
|
return resetMs > nowMs;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Flat sibling window that mirrors a `limits[]` entry's `kind`, for resets_at backfill. */
|
|
54
|
+
const FLAT_WINDOW_BY_KIND = { session: 'five_hour', weekly_all: 'seven_day' };
|
|
55
|
+
|
|
56
|
+
/** An entry's utilization: real `percent`, else the flat-shape `utilization`. */
|
|
57
|
+
function entryPercent(l) {
|
|
58
|
+
return Number.isFinite(l.percent) ? l.percent : l.utilization;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
/**
|
|
54
|
-
* Which window is actually binding dispatch.
|
|
55
|
-
* `
|
|
56
|
-
* `
|
|
62
|
+
* Which window is actually binding dispatch. The real /api/oauth/usage
|
|
63
|
+
* `limits[]` entries carry `kind` / `group` / `percent` / `severity` /
|
|
64
|
+
* `resets_at` / `scope` / `is_active` (percent is 0-100, may exceed 100).
|
|
65
|
+
* Only UNSCOPED entries (`scope == null`) are candidates — a scoped entry
|
|
66
|
+
* (e.g. `weekly_scoped` for one model) does not bind dispatch generally. Among
|
|
67
|
+
* them the HIGHEST finite percent wins ("binding" = closest to blocking us);
|
|
68
|
+
* `is_active` only breaks ties, because the API sets it on group precedence
|
|
69
|
+
* and trusting it alone would pick weekly_all 64 over session 95. With no
|
|
70
|
+
* usable unscoped entry, falls back to the flat `five_hour`. Pure, no I/O.
|
|
57
71
|
*/
|
|
58
72
|
function bindingWindow(payload) {
|
|
59
|
-
if (payload && Array.isArray(payload.limits)
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
if (payload && Array.isArray(payload.limits)) {
|
|
74
|
+
let best = null;
|
|
75
|
+
let bestPct = -Infinity;
|
|
76
|
+
for (const l of payload.limits) {
|
|
77
|
+
if (!l || l.scope != null) continue;
|
|
78
|
+
const pct = entryPercent(l);
|
|
79
|
+
if (!Number.isFinite(pct)) continue;
|
|
80
|
+
if (pct > bestPct || (pct === bestPct && l.is_active === true && !(best && best.is_active === true))) {
|
|
81
|
+
best = l;
|
|
82
|
+
bestPct = pct;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (best) {
|
|
86
|
+
const flat = payload[FLAT_WINDOW_BY_KIND[best.kind]];
|
|
62
87
|
return {
|
|
63
|
-
name:
|
|
64
|
-
utilization:
|
|
65
|
-
resets_at:
|
|
88
|
+
name: best.kind || best.type || best.name || 'unknown',
|
|
89
|
+
utilization: bestPct,
|
|
90
|
+
resets_at: best.resets_at ?? (flat ? flat.resets_at ?? null : null),
|
|
66
91
|
};
|
|
67
92
|
}
|
|
68
93
|
}
|
|
@@ -88,15 +113,18 @@ function degradedConcurrencyCap(configuredCap) {
|
|
|
88
113
|
|
|
89
114
|
/**
|
|
90
115
|
* Conservative budget to run on while the meter is down. utilization is
|
|
91
|
-
* carried forward from the last known-good BINDING window
|
|
92
|
-
*
|
|
116
|
+
* carried forward from the last known-good BINDING window (a genuine 0% stays
|
|
117
|
+
* 0); with no payload ever received it is 100, never a blind 0 that would read
|
|
118
|
+
* as "plenty of headroom" instead of "we don't know." A fresh
|
|
93
119
|
* executor-observed 429 (its own window not yet passed) pins utilization to
|
|
94
120
|
* 100 regardless of the stale cached value. concurrencyCap never exceeds 2
|
|
95
121
|
* (or SM_USAGE_DEGRADED_CAP, if set).
|
|
96
122
|
*/
|
|
97
123
|
function degradedBudget(lastGoodPayload, executorEvidence = {}) {
|
|
124
|
+
// "Never a blind 0" means NO payload ever received -> 100. A payload whose
|
|
125
|
+
// binding window genuinely reads 0% carries forward as 0.
|
|
98
126
|
const window = bindingWindow(lastGoodPayload);
|
|
99
|
-
let utilization = Number.isFinite(window.utilization)
|
|
127
|
+
let utilization = lastGoodPayload && Number.isFinite(window.utilization)
|
|
100
128
|
? window.utilization
|
|
101
129
|
: 100;
|
|
102
130
|
|