claude-code-session-manager 0.104.2 → 0.106.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/README.md +15 -3
- package/bin/cli.cjs +0 -5
- package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-CKOZzc2W.js} +1 -1
- package/dist/assets/{Button-8RAbgh1d.js → Button-yuqFpQsK.js} +1 -1
- package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-Bwdc_gaJ.js} +1 -1
- package/dist/assets/{History-BLrNSazM.js → History-D1Z0lydo.js} +1 -1
- package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-CyD6l4Is.js} +1 -1
- package/dist/assets/{Library-CWFvBZw-.js → Library-fnod5cPe.js} +1 -1
- package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-BKO68SJ4.js} +1 -1
- package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-DrYMnMBy.js} +1 -1
- package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-CgfItWhF.js} +1 -1
- package/dist/assets/{Memory-h8bhRNyI.js → Memory-4M3gO3_r.js} +1 -1
- package/dist/assets/{Panel-srf0FT0m.js → Panel-CKbXo-LT.js} +1 -1
- package/dist/assets/{Permissions-BA4oWydI.js → Permissions-Bc9EpjXL.js} +1 -1
- package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-B_-PlEIx.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-Cxev7XSA.js} +1 -1
- package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-DiqscG6q.js} +1 -1
- package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-CCcJzuu8.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BsM4-jHQ.js} +1 -1
- package/dist/assets/{Settings-Bpyt1RGa.js → Settings-C3-VL-T8.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-B9YlOuPo.js} +1 -1
- package/dist/assets/{Skills-zXLI5OB_.js → Skills-Dtf-z_zN.js} +1 -1
- package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-HJDn0E8C.js} +1 -1
- package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-DLGOfauU.js} +1 -1
- package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-Cdmv1CuM.js} +1 -1
- package/dist/assets/{Toggle-C139wuAK.js → Toggle-CQFhOo1F.js} +1 -1
- package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-CxjDCHjH.js} +1 -1
- package/dist/assets/index-Blo38vfX.js +3077 -0
- package/dist/assets/{index-D89EUizB.css → index-DkJv4l6L.css} +1 -1
- package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-B0uoL_1R.js} +1 -1
- package/dist/assets/useAgentsDirWatch-e3TJUFPP.js +1 -0
- package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-Dxv7JbCU.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -12
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
- package/scripts/scheduler-mcp-server.cjs +51 -17
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +8 -6
- package/src/main/crashDiagnostics.cjs +39 -3
- package/src/main/historyAggregator.cjs +7 -1
- package/src/main/index.cjs +73 -4
- package/src/main/ipcSchemas.cjs +27 -0
- package/src/main/lib/agentPersonaSchema.cjs +8 -1
- package/src/main/lib/appRuntime.cjs +57 -0
- package/src/main/lib/childWithLog.cjs +10 -10
- package/src/main/lib/claudeBin.cjs +58 -20
- package/src/main/lib/cleanEnv.cjs +33 -9
- package/src/main/lib/customerFeedbackAdminRoutes.cjs +94 -0
- package/src/main/lib/customerFeedbackClient.cjs +261 -0
- package/src/main/lib/customerFeedbackInbox.cjs +286 -0
- package/src/main/lib/customerFeedbackIpc.cjs +43 -0
- package/src/main/lib/defaultShell.cjs +36 -0
- package/src/main/lib/definitionOfDone.cjs +6 -2
- package/src/main/lib/delegationReadiness.cjs +13 -5
- package/src/main/lib/gitWorktree.cjs +6 -3
- package/src/main/lib/insideHome.cjs +15 -4
- package/src/main/lib/killTree.cjs +80 -0
- package/src/main/lib/loadGate.cjs +16 -0
- package/src/main/lib/loginShellPath.cjs +82 -0
- package/src/main/lib/machineProfile.cjs +4 -2
- package/src/main/lib/mcpStdioServer.cjs +127 -0
- package/src/main/lib/mcpToolCatalog.cjs +26 -1
- package/src/main/lib/openExternalApp.cjs +62 -11
- package/src/main/lib/otlpTraceExporter.cjs +147 -0
- package/src/main/lib/prereqs.cjs +145 -0
- package/src/main/lib/procIdentity.cjs +37 -2
- package/src/main/lib/projectFolder.cjs +89 -0
- package/src/main/lib/rcaReport.cjs +1 -0
- package/src/main/lib/reaperHelpers.cjs +5 -2
- package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
- package/src/main/lib/timeoutShimScript.cjs +14 -2
- package/src/main/lib/updateCheck.cjs +41 -11
- package/src/main/lib/watchdogHelpers.cjs +23 -3
- package/src/main/lib/winSpawn.cjs +67 -0
- package/src/main/lib/winUserData.cjs +22 -0
- package/src/main/otel.cjs +18 -62
- package/src/main/pty.cjs +32 -11
- package/src/main/runVerify.cjs +42 -2
- package/src/main/scheduler.cjs +116 -29
- package/src/main/seedSchedulerMcp.cjs +11 -2
- package/src/preload/api.d.ts +82 -0
- package/src/preload/index.cjs +14 -0
- package/src/seed/agents/validator.md +2 -2
- package/dist/assets/index-DrvxIbmR.js +0 -3073
- package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
- package/scripts/postinstall.cjs +0 -52
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* customerFeedbackInbox — OWNER side of customer feedback.
|
|
4
|
+
*
|
|
5
|
+
* With an owner token configured (env SM_FEEDBACK_OWNER_TOKEN or
|
|
6
|
+
* ~/.claude/session-manager/customer-feedback-owner.json) this machine incrementally pulls every
|
|
7
|
+
* submission for the session-manager slug from bilko.run into a local mirror, sets item status on
|
|
8
|
+
* the host, and records which local Epic an item became. No token = customer machine (ownerMode
|
|
9
|
+
* false). The token is only ever sent as a bearer header and never appears in errors or the mirror.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const fsp = require('node:fs/promises');
|
|
15
|
+
const atomicFs = require('./atomicFs.cjs');
|
|
16
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
17
|
+
const { resolveFeedbackBase, fromWireType, FEEDBACK_SLUG } = require('./customerFeedbackClient.cjs');
|
|
18
|
+
|
|
19
|
+
const STATUSES = /** @type {const} */ (['open', 'in_progress', 'resolved', 'wontfix']);
|
|
20
|
+
const MAX_NOTE = 500;
|
|
21
|
+
const MAX_ITEMS = 2000;
|
|
22
|
+
const PAGE_LIMIT = 500;
|
|
23
|
+
const MAX_PAGES = 10;
|
|
24
|
+
const TIMEOUT_MS = 15000;
|
|
25
|
+
const SCHEMA_VERSION = 1;
|
|
26
|
+
const NO_TOKEN = 'Feedback owner token not configured';
|
|
27
|
+
const NO_STATUS_SUPPORT = 'Host does not support feedback status yet (or unknown id)';
|
|
28
|
+
const HIDDEN = new Set(['archived', 'deleted']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {{ id: string, receivedAt: string|number|null, tag: string, title: string, body: string,
|
|
32
|
+
* clientVersion: string|null, clientPlatform: string|null, moderation: string|null, status: string,
|
|
33
|
+
* statusNote: string|null, statusAt: string|number|null, epicId: string|null }} InboxItem
|
|
34
|
+
* @typedef {{ items: InboxItem[], nextSince: string|null, nextModeratedSince: string|null }} Store
|
|
35
|
+
* @typedef {{ fetchImpl?: typeof fetch, storeFile?: string, ownerFile?: string,
|
|
36
|
+
* env?: Record<string, string|undefined>, now?: () => number }} Deps
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @param {Deps} [deps] */
|
|
40
|
+
function ctx(deps) {
|
|
41
|
+
const d = deps || {};
|
|
42
|
+
return {
|
|
43
|
+
fetchImpl: d.fetchImpl || globalThis.fetch,
|
|
44
|
+
storeFile: d.storeFile || path.join(schedulerPaths.schedulerHome(), 'customer-feedback-inbox.json'),
|
|
45
|
+
ownerFile: d.ownerFile || path.join(schedulerPaths.schedulerHome(), 'customer-feedback-owner.json'),
|
|
46
|
+
env: d.env || process.env,
|
|
47
|
+
now: d.now || Date.now,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {Deps} [deps] @returns {{ token: string, projectCwd: string|null }|null} */
|
|
52
|
+
function loadOwnerConfig(deps) {
|
|
53
|
+
const c = ctx(deps);
|
|
54
|
+
const envToken = (c.env.SM_FEEDBACK_OWNER_TOKEN || '').trim();
|
|
55
|
+
if (envToken) return { token: envToken, projectCwd: c.env.SM_FEEDBACK_OWNER_PROJECT_CWD || null };
|
|
56
|
+
try {
|
|
57
|
+
const j = JSON.parse(require('node:fs').readFileSync(c.ownerFile, 'utf8'));
|
|
58
|
+
const token = j && typeof j.token === 'string' ? j.token.trim() : '';
|
|
59
|
+
if (token) return { token, projectCwd: typeof j.projectCwd === 'string' && j.projectCwd ? j.projectCwd : null };
|
|
60
|
+
} catch { /* missing or corrupt = not an owner */ }
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Never includes the token. @param {Deps} [deps] */
|
|
65
|
+
function getOwnerInfo(deps) {
|
|
66
|
+
const cfg = loadOwnerConfig(deps);
|
|
67
|
+
return { ownerMode: !!cfg, projectCwd: cfg ? cfg.projectCwd : null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {string} file @returns {Promise<Store>} */
|
|
71
|
+
async function readStore(file) {
|
|
72
|
+
try {
|
|
73
|
+
const j = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
74
|
+
if (j && Array.isArray(j.items)) {
|
|
75
|
+
return {
|
|
76
|
+
items: j.items.filter((/** @type {unknown} */ i) => i && typeof i === 'object'),
|
|
77
|
+
nextSince: typeof j.nextSince === 'string' ? j.nextSince : null,
|
|
78
|
+
nextModeratedSince: typeof j.nextModeratedSince === 'string' ? j.nextModeratedSince : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
} catch { /* missing or corrupt reads as empty */ }
|
|
82
|
+
return { items: [], nextSince: null, nextModeratedSince: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let writeQueue = Promise.resolve();
|
|
86
|
+
/**
|
|
87
|
+
* Serialized read-modify-write over the mirror.
|
|
88
|
+
* @template T
|
|
89
|
+
* @param {string} file
|
|
90
|
+
* @param {(s: Store) => T} mutate mutates the store in place, returns a result
|
|
91
|
+
* @returns {Promise<T>}
|
|
92
|
+
*/
|
|
93
|
+
function mutateStore(file, mutate) {
|
|
94
|
+
const run = async () => {
|
|
95
|
+
const store = await readStore(file);
|
|
96
|
+
const result = mutate(store);
|
|
97
|
+
store.items = sortItems(store.items).slice(0, MAX_ITEMS);
|
|
98
|
+
await atomicFs.writeJsonAtomic(file, { schemaVersion: SCHEMA_VERSION, ...store }, { mode: 0o600 });
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
const tail = writeQueue.then(run, run);
|
|
102
|
+
writeQueue = tail.then(() => {}, () => {});
|
|
103
|
+
return tail;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {InboxItem[]} items */
|
|
107
|
+
function sortItems(items) {
|
|
108
|
+
const t = (/** @type {InboxItem} */ i) => (i.receivedAt == null ? 0 : (typeof i.receivedAt === 'number' ? i.receivedAt : Date.parse(i.receivedAt) || 0));
|
|
109
|
+
return items.slice().sort((a, b) => t(b) - t(a));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {string} s @param {string} token */
|
|
113
|
+
function redact(s, token) {
|
|
114
|
+
return token ? s.split(token).join('[redacted]') : s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {unknown} e @param {string} token */
|
|
118
|
+
function netError(e, token) {
|
|
119
|
+
return `Could not reach the feedback service: ${redact(e instanceof Error ? e.message : String(e), token)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {Response} res @param {string} fallback */
|
|
123
|
+
async function errorText(res, fallback) {
|
|
124
|
+
try {
|
|
125
|
+
/** @type {any} */
|
|
126
|
+
const j = await res.json();
|
|
127
|
+
if (j && typeof j.error === 'string' && j.error) return j.error;
|
|
128
|
+
} catch { /* no body */ }
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {number} status */
|
|
133
|
+
function httpError(status) {
|
|
134
|
+
if (status === 401) return 'Feedback owner token was rejected (401).';
|
|
135
|
+
if (status === 503) return 'Host has no feedback owner token configured (503).';
|
|
136
|
+
return `Feedback service returned ${status}.`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {any} w @param {InboxItem|undefined} prev @returns {InboxItem} */
|
|
140
|
+
function toItem(w, prev) {
|
|
141
|
+
const client = w.client && typeof w.client === 'object' ? w.client : {};
|
|
142
|
+
const st = w.status && typeof w.status === 'object' ? w.status : null;
|
|
143
|
+
return {
|
|
144
|
+
id: w.id,
|
|
145
|
+
receivedAt: w.receivedAt ?? null,
|
|
146
|
+
tag: fromWireType(String(w.type)),
|
|
147
|
+
title: String(w.title ?? ''),
|
|
148
|
+
body: String(w.description ?? ''),
|
|
149
|
+
clientVersion: typeof client.version === 'string' ? client.version : null,
|
|
150
|
+
clientPlatform: typeof client.platform === 'string' ? client.platform : null,
|
|
151
|
+
moderation: w.moderation && typeof w.moderation.action === 'string' ? w.moderation.action : null,
|
|
152
|
+
status: st && typeof st.value === 'string' ? st.value : 'open',
|
|
153
|
+
statusNote: st && typeof st.note === 'string' ? st.note : null,
|
|
154
|
+
statusAt: st ? (st.at ?? null) : null,
|
|
155
|
+
epicId: prev && prev.epicId ? prev.epicId : null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {Deps} [deps]
|
|
161
|
+
* @returns {Promise<{ ok: true, fetched: number, pages: number } | { ok: false, error: string }>}
|
|
162
|
+
*/
|
|
163
|
+
async function pull(deps) {
|
|
164
|
+
const cfg = loadOwnerConfig(deps);
|
|
165
|
+
if (!cfg) return { ok: false, error: NO_TOKEN };
|
|
166
|
+
const c = ctx(deps);
|
|
167
|
+
const base = `${resolveFeedbackBase()}/api/projects/${FEEDBACK_SLUG}/feedback`;
|
|
168
|
+
const prior = await readStore(c.storeFile);
|
|
169
|
+
let since = prior.nextSince;
|
|
170
|
+
let moderatedSince = prior.nextModeratedSince;
|
|
171
|
+
/** @type {any[]} */
|
|
172
|
+
const fetched = [];
|
|
173
|
+
let pages = 0;
|
|
174
|
+
try {
|
|
175
|
+
for (; pages < MAX_PAGES;) {
|
|
176
|
+
const qs = new URLSearchParams();
|
|
177
|
+
if (since) qs.set('since', since);
|
|
178
|
+
if (moderatedSince) qs.set('moderatedSince', moderatedSince);
|
|
179
|
+
qs.set('images', 'none');
|
|
180
|
+
qs.set('limit', String(PAGE_LIMIT));
|
|
181
|
+
const res = await c.fetchImpl(`${base}?${qs.toString()}`, {
|
|
182
|
+
headers: { authorization: `Bearer ${cfg.token}` },
|
|
183
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
184
|
+
});
|
|
185
|
+
if (!res.ok) return { ok: false, error: await errorText(res, httpError(res.status)) };
|
|
186
|
+
/** @type {any} */
|
|
187
|
+
const j = await res.json().catch(() => null);
|
|
188
|
+
if (!j || !Array.isArray(j.items)) return { ok: false, error: 'Feedback service returned an unexpected response.' };
|
|
189
|
+
pages++;
|
|
190
|
+
fetched.push(...j.items.filter((/** @type {any} */ i) => i && typeof i.id === 'string' && i.id));
|
|
191
|
+
if (typeof j.nextSince === 'string') since = j.nextSince;
|
|
192
|
+
if (typeof j.nextModeratedSince === 'string') moderatedSince = j.nextModeratedSince;
|
|
193
|
+
if (j.items.length < PAGE_LIMIT) break;
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return { ok: false, error: netError(e, cfg.token) };
|
|
197
|
+
}
|
|
198
|
+
const finalSince = since;
|
|
199
|
+
const finalModerated = moderatedSince;
|
|
200
|
+
await mutateStore(c.storeFile, (store) => {
|
|
201
|
+
const byId = new Map(store.items.map((i) => [i.id, i]));
|
|
202
|
+
for (const w of fetched) byId.set(w.id, toItem(w, byId.get(w.id)));
|
|
203
|
+
store.items = [...byId.values()];
|
|
204
|
+
store.nextSince = finalSince;
|
|
205
|
+
store.nextModeratedSince = finalModerated;
|
|
206
|
+
});
|
|
207
|
+
return { ok: true, fetched: fetched.length, pages };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {{ includeHidden?: boolean }} [opts] @param {Deps} [deps]
|
|
212
|
+
* @returns {Promise<{ ok: true, items: InboxItem[] } | { ok: false, error: string }>}
|
|
213
|
+
*/
|
|
214
|
+
async function list(opts, deps) {
|
|
215
|
+
if (!loadOwnerConfig(deps)) return { ok: false, error: NO_TOKEN };
|
|
216
|
+
const store = await readStore(ctx(deps).storeFile);
|
|
217
|
+
const includeHidden = !!(opts && opts.includeHidden);
|
|
218
|
+
const items = sortItems(store.items).filter((i) => includeHidden || !(i.moderation && HIDDEN.has(i.moderation)));
|
|
219
|
+
return { ok: true, items };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} id @param {string} status @param {string} [note] @param {Deps} [deps]
|
|
224
|
+
* @returns {Promise<{ ok: true, item: InboxItem|null } | { ok: false, error: string }>}
|
|
225
|
+
*/
|
|
226
|
+
async function setStatus(id, status, note, deps) {
|
|
227
|
+
const cfg = loadOwnerConfig(deps);
|
|
228
|
+
if (!cfg) return { ok: false, error: NO_TOKEN };
|
|
229
|
+
if (typeof id !== 'string' || !id) return { ok: false, error: 'Feedback id is required.' };
|
|
230
|
+
if (!STATUSES.includes(/** @type {any} */ (status))) return { ok: false, error: `Status must be one of: ${STATUSES.join(', ')}.` };
|
|
231
|
+
if (note !== undefined && note !== null && (typeof note !== 'string' || note.length > MAX_NOTE)) {
|
|
232
|
+
return { ok: false, error: `Note must be ${MAX_NOTE} characters or fewer.` };
|
|
233
|
+
}
|
|
234
|
+
const c = ctx(deps);
|
|
235
|
+
try {
|
|
236
|
+
const res = await c.fetchImpl(`${resolveFeedbackBase()}/api/projects/${FEEDBACK_SLUG}/feedback/${encodeURIComponent(id)}/status`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${cfg.token}` },
|
|
239
|
+
body: JSON.stringify(note ? { status, note } : { status }),
|
|
240
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
241
|
+
});
|
|
242
|
+
if (res.status === 404) return { ok: false, error: NO_STATUS_SUPPORT };
|
|
243
|
+
if (res.status !== 200) return { ok: false, error: await errorText(res, httpError(res.status)) };
|
|
244
|
+
/** @type {any} */
|
|
245
|
+
const j = await res.json().catch(() => null);
|
|
246
|
+
const statusAt = j && j.statusAt != null ? j.statusAt : c.now();
|
|
247
|
+
const item = await mutateStore(c.storeFile, (store) => {
|
|
248
|
+
const it = store.items.find((i) => i.id === id);
|
|
249
|
+
if (!it) return null;
|
|
250
|
+
it.status = status;
|
|
251
|
+
it.statusNote = note || null;
|
|
252
|
+
it.statusAt = statusAt;
|
|
253
|
+
return it;
|
|
254
|
+
});
|
|
255
|
+
return { ok: true, item };
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return { ok: false, error: netError(e, cfg.token) };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Local-only: record which Epic an item was turned into.
|
|
263
|
+
* @param {string} id @param {string} epicId @param {Deps} [deps]
|
|
264
|
+
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
265
|
+
*/
|
|
266
|
+
async function linkEpic(id, epicId, deps) {
|
|
267
|
+
if (!loadOwnerConfig(deps)) return { ok: false, error: NO_TOKEN };
|
|
268
|
+
if (typeof epicId !== 'string' || !epicId) return { ok: false, error: 'Epic id is required.' };
|
|
269
|
+
const found = await mutateStore(ctx(deps).storeFile, (store) => {
|
|
270
|
+
const it = store.items.find((i) => i.id === id);
|
|
271
|
+
if (!it) return false;
|
|
272
|
+
it.epicId = epicId;
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
return found ? { ok: true } : { ok: false, error: 'Unknown feedback id.' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
STATUSES,
|
|
280
|
+
loadOwnerConfig,
|
|
281
|
+
getOwnerInfo,
|
|
282
|
+
pull,
|
|
283
|
+
list,
|
|
284
|
+
setStatus,
|
|
285
|
+
linkEpic,
|
|
286
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const REFRESH_DELAY_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renderer-facing IPC for the customer-feedback client + owner inbox.
|
|
8
|
+
* Handlers return the libs' `{ ok, ... }` results unchanged.
|
|
9
|
+
* @param {{ ipcMain?: any, client?: any, inbox?: any, setTimeoutImpl?: typeof setTimeout }} [deps]
|
|
10
|
+
*/
|
|
11
|
+
function registerCustomerFeedbackHandlers(deps = {}) {
|
|
12
|
+
const ipcMain = deps.ipcMain || require('electron').ipcMain;
|
|
13
|
+
const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
|
|
14
|
+
const client = deps.client || require('./customerFeedbackClient.cjs');
|
|
15
|
+
const inbox = deps.inbox || require('./customerFeedbackInbox.cjs');
|
|
16
|
+
const timer = deps.setTimeoutImpl || setTimeout;
|
|
17
|
+
|
|
18
|
+
ipcMain.handle('customerFeedback:submit', v(s.customerFeedbackSubmit, (p) => client.submit(p)));
|
|
19
|
+
ipcMain.handle('customerFeedback:list', () => client.list());
|
|
20
|
+
ipcMain.handle('customerFeedback:refresh-status', () => client.refreshStatuses());
|
|
21
|
+
ipcMain.handle('customerFeedback:mark-seen', v(s.customerFeedbackMarkSeen, ({ ids }) => client.markSeen(ids)));
|
|
22
|
+
ipcMain.handle('customerFeedback:owner-info', () => inbox.getOwnerInfo());
|
|
23
|
+
ipcMain.handle('customerFeedback:inbox-pull', () => inbox.pull());
|
|
24
|
+
ipcMain.handle('customerFeedback:inbox-list', v(s.customerFeedbackInboxList, ({ includeHidden }) => inbox.list({ includeHidden })));
|
|
25
|
+
ipcMain.handle('customerFeedback:inbox-set-status', v(s.customerFeedbackInboxSetStatus, ({ id, status, note }) => inbox.setStatus(id, status, note)));
|
|
26
|
+
ipcMain.handle('customerFeedback:inbox-link-epic', v(s.customerFeedbackInboxLinkEpic, ({ id, epicId }) => inbox.linkEpic(id, epicId)));
|
|
27
|
+
|
|
28
|
+
// One-shot deferred refresh so users see resolutions without opening anything.
|
|
29
|
+
try {
|
|
30
|
+
const t = timer(() => {
|
|
31
|
+
Promise.resolve()
|
|
32
|
+
.then(() => client.refreshStatuses())
|
|
33
|
+
.catch((/** @type {any} */ e) => {
|
|
34
|
+
console.warn('[customerFeedback] boot status refresh failed:', e && e.name ? e.name : 'error');
|
|
35
|
+
});
|
|
36
|
+
}, REFRESH_DELAY_MS);
|
|
37
|
+
if (t && typeof t.unref === 'function') t.unref();
|
|
38
|
+
} catch {
|
|
39
|
+
/* never throw at boot */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { registerCustomerFeedbackHandlers, REFRESH_DELAY_MS };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pick the interactive shell for a terminal tab. Pure: every host fact comes in
|
|
7
|
+
* through `platform` / `env` / `exists` so it is testable for any OS.
|
|
8
|
+
*
|
|
9
|
+
* win32: PowerShell 7 (`pwsh.exe`) when found on PATH or under
|
|
10
|
+
* %ProgramFiles%\PowerShell\7, else the Windows PowerShell 5.1 that ships with
|
|
11
|
+
* every install. POSIX: `$SHELL` (or /bin/bash) as an interactive login shell.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ platform: string, env: Record<string, string | undefined>, exists: (p: string) => boolean }} opts
|
|
14
|
+
* @returns {{ file: string, args: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
function defaultShell({ platform, env, exists }) {
|
|
17
|
+
if (platform !== 'win32') {
|
|
18
|
+
return { file: env.SHELL || '/bin/bash', args: ['-il'] };
|
|
19
|
+
}
|
|
20
|
+
const w = path.win32;
|
|
21
|
+
const pathDirs = (env.PATH || env.Path || '').split(';').filter(Boolean);
|
|
22
|
+
const programFiles = env.ProgramFiles || 'C:\\Program Files';
|
|
23
|
+
const candidates = [
|
|
24
|
+
...pathDirs.map((d) => w.join(d, 'pwsh.exe')),
|
|
25
|
+
w.join(programFiles, 'PowerShell', '7', 'pwsh.exe'),
|
|
26
|
+
];
|
|
27
|
+
const pwsh = candidates.find((p) => exists(p));
|
|
28
|
+
if (pwsh) return { file: pwsh, args: ['-NoLogo'] };
|
|
29
|
+
const systemRoot = env.SystemRoot || env.windir || 'C:\\Windows';
|
|
30
|
+
return {
|
|
31
|
+
file: w.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
|
|
32
|
+
args: ['-NoLogo'],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { defaultShell };
|
|
@@ -16,6 +16,7 @@ const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
|
16
16
|
const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
17
17
|
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
18
18
|
const atomicFs = require('./atomicFs.cjs');
|
|
19
|
+
const { resolveSpawn } = require('./winSpawn.cjs');
|
|
19
20
|
|
|
20
21
|
// Regex identifying meta/dod slugs that must NOT influence the batchKey.
|
|
21
22
|
// This is the load-bearing loop-avoidance filter: when the gate job itself
|
|
@@ -771,10 +772,13 @@ function runOneGateCommand(cmd, { cwd, env }) {
|
|
|
771
772
|
const label = cmd.argv.join(' ');
|
|
772
773
|
let child;
|
|
773
774
|
try {
|
|
774
|
-
|
|
775
|
+
const childEnv = { ...env, ...cmd.env };
|
|
776
|
+
const sp = resolveSpawn(cmd.argv[0], cmd.argv.slice(1), { env: childEnv });
|
|
777
|
+
child = spawn(sp.command, sp.args, {
|
|
775
778
|
cwd,
|
|
776
|
-
env:
|
|
779
|
+
env: childEnv,
|
|
777
780
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
781
|
+
...(sp.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
778
782
|
// No shell:true — the sequence was tokenized by tokenizeNoShell.
|
|
779
783
|
});
|
|
780
784
|
} catch {
|
|
@@ -24,6 +24,7 @@ const { writeJson, addAllowedRoot } = require('../config.cjs');
|
|
|
24
24
|
const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
|
|
25
25
|
const { shimPath: guardShimPath, resolveShimTarget, ensureGuardShimsOrError } = require('./guardShims.cjs');
|
|
26
26
|
const { resolveProjectRoot } = require('./opsOwnership.cjs');
|
|
27
|
+
const { nodeShellCommand } = require('./appRuntime.cjs');
|
|
27
28
|
|
|
28
29
|
const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
|
|
29
30
|
const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
|
|
@@ -49,7 +50,14 @@ function readJsonSafe(absPath, fallback) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
/** Lazy (not a module-load constant) so tests can toggle packaged mode. */
|
|
54
|
+
function schedulerMcpFixCmd() {
|
|
55
|
+
const cmd = nodeShellCommand(SCHEDULER_MCP_SERVER_SCRIPT);
|
|
56
|
+
const m = /^ELECTRON_RUN_AS_NODE=1 (.*)$/.exec(cmd);
|
|
57
|
+
return m
|
|
58
|
+
? `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -e ELECTRON_RUN_AS_NODE=1 -- ${m[1]}`
|
|
59
|
+
: `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- ${cmd}`;
|
|
60
|
+
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
55
63
|
* `ok` must mean "an agent in this project can actually reach a working
|
|
@@ -79,7 +87,7 @@ function checkSchedulerMcp({ cwd, homeDir }) {
|
|
|
79
87
|
detail: !isAbsolute
|
|
80
88
|
? `user-scope ${SCHEDULER_MCP_NAME} registration points at a non-absolute script path (${scriptArg})`
|
|
81
89
|
: `user-scope ${SCHEDULER_MCP_NAME} registration points at a script that no longer exists (${scriptArg})`,
|
|
82
|
-
fix:
|
|
90
|
+
fix: schedulerMcpFixCmd(),
|
|
83
91
|
};
|
|
84
92
|
}
|
|
85
93
|
return {
|
|
@@ -106,7 +114,7 @@ function checkSchedulerMcp({ cwd, homeDir }) {
|
|
|
106
114
|
label: 'Scheduler MCP server registered',
|
|
107
115
|
ok: false,
|
|
108
116
|
detail: `no ${SCHEDULER_MCP_NAME} entry in ~/.claude.json or ${cwd}/.mcp.json`,
|
|
109
|
-
fix:
|
|
117
|
+
fix: schedulerMcpFixCmd(),
|
|
110
118
|
};
|
|
111
119
|
}
|
|
112
120
|
|
|
@@ -476,7 +484,7 @@ function checkGuard(def, { cwd, homeDir = os.homedir() }) {
|
|
|
476
484
|
detail,
|
|
477
485
|
fix: ok
|
|
478
486
|
? null
|
|
479
|
-
: `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching ${def.matcher} that runs
|
|
487
|
+
: `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching ${def.matcher} that runs ${nodeShellCommand(guardShimPath(def.scriptBasename, homeDir))}${def.nudge ? ' (a nudge, not a hard gate)' : ''}`,
|
|
480
488
|
fixAction: ok ? null : def.fixActionId,
|
|
481
489
|
};
|
|
482
490
|
}
|
|
@@ -549,7 +557,7 @@ async function installGuard(def, { cwd, homeDir = os.homedir(), skipShimEnsure =
|
|
|
549
557
|
}
|
|
550
558
|
const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
|
|
551
559
|
|
|
552
|
-
const command =
|
|
560
|
+
const command = nodeShellCommand(guardShimPath(def.scriptBasename, homeDir));
|
|
553
561
|
const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
|
|
554
562
|
const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
|
|
555
563
|
|
|
@@ -1422,14 +1422,16 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
|
|
|
1422
1422
|
|
|
1423
1423
|
try {
|
|
1424
1424
|
await execGit(['merge', '--ff-only', branch], { cwd, timeout: 30_000 });
|
|
1425
|
-
|
|
1425
|
+
// The branch tip we fast-forwarded to, read from the branch ref (not HEAD,
|
|
1426
|
+
// which a concurrent writer may already have moved).
|
|
1427
|
+
return { ok: true, integrated: true, fastForward: true, sha: await tryResolveCommit(cwd, `${branch}^{commit}`) };
|
|
1426
1428
|
} catch {
|
|
1427
1429
|
// Main tree advanced since the worktree branched (a sibling job/Epic
|
|
1428
1430
|
// merged first) — a real merge commit still lands the branch's commit(s).
|
|
1429
1431
|
}
|
|
1430
1432
|
try {
|
|
1431
1433
|
await execGit(['merge', '--no-ff', '--no-edit', '-m', mergeMessage, branch], { cwd, timeout: 30_000 });
|
|
1432
|
-
return { ok: true, integrated: true, mergeCommit: true };
|
|
1434
|
+
return { ok: true, integrated: true, mergeCommit: true, sha: await readHeadSha(cwd) };
|
|
1433
1435
|
} catch (e) {
|
|
1434
1436
|
const stderrText = (e && (e.stderrText || e.message)) || String(e);
|
|
1435
1437
|
// git refuses this class of merge before ever touching MERGE_HEAD, so
|
|
@@ -1452,6 +1454,7 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
|
|
|
1452
1454
|
ok: true,
|
|
1453
1455
|
integrated: true,
|
|
1454
1456
|
mergeCommit: true,
|
|
1457
|
+
sha: await readHeadSha(cwd),
|
|
1455
1458
|
autoResolved: 'identical_working_tree_duplicates',
|
|
1456
1459
|
resolvedPaths: allPaths,
|
|
1457
1460
|
};
|
|
@@ -1479,7 +1482,7 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranc
|
|
|
1479
1482
|
await resolveByConcatenation({ cwd, paths });
|
|
1480
1483
|
await execGit(['add', ...paths], { cwd, timeout: 30_000 });
|
|
1481
1484
|
await execGit(['commit', '-m', mergeMessage], { cwd, timeout: 30_000 });
|
|
1482
|
-
return { ok: true, integrated: true, mergeCommit: true, autoResolved: 'pure_addition_concat', resolvedPaths: paths };
|
|
1485
|
+
return { ok: true, integrated: true, mergeCommit: true, sha: await readHeadSha(cwd), autoResolved: 'pure_addition_concat', resolvedPaths: paths };
|
|
1483
1486
|
}
|
|
1484
1487
|
} catch { /* fall through to the abort */ }
|
|
1485
1488
|
}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* resolved at startup we keep going with the literal value — boot-time
|
|
20
20
|
* self-check in index.cjs surfaces that to the user.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* win32: containment compares lower-cased, path.win32-normalized paths; UNC
|
|
23
|
+
* (`\\server\share`) and `\\?\` paths are always outside home.
|
|
23
24
|
*/
|
|
24
25
|
'use strict';
|
|
25
26
|
|
|
@@ -52,8 +53,18 @@ function resolveTarget(p) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function
|
|
56
|
-
|
|
56
|
+
function isContainedWin32(target, home) {
|
|
57
|
+
// UNC and \\?\ / \\.\ prefixes can alias any location — never inside home.
|
|
58
|
+
if (/^[\\/]{2}/.test(target)) return false;
|
|
59
|
+
const strip = (v) => path.win32.normalize(v).toLowerCase().replace(/[\\/]+$/, '');
|
|
60
|
+
const t = strip(target);
|
|
61
|
+
const h = strip(home);
|
|
62
|
+
return t === h || t.startsWith(h + '\\');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isContained(realPath, home = HOME_REAL, platform = process.platform) {
|
|
66
|
+
if (platform === 'win32') return isContainedWin32(realPath, home);
|
|
67
|
+
return realPath === home || realPath.startsWith(home + path.posix.sep);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
function assertInsideHome(p) {
|
|
@@ -88,4 +99,4 @@ function checkInsideHome(p) {
|
|
|
88
99
|
return { ok: true, realPath };
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
module.exports = { assertInsideHome, checkInsideHome };
|
|
102
|
+
module.exports = { assertInsideHome, checkInsideHome, isContained };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* killTree.cjs — cross-platform "kill this child and its descendants" plus the
|
|
5
|
+
* matching spawn options for children we intend to kill that way.
|
|
6
|
+
*
|
|
7
|
+
* POSIX: children are spawned `detached: true` so they lead their own process
|
|
8
|
+
* group, and `process.kill(-pid, sig)` signals the whole group.
|
|
9
|
+
* win32: no process groups; `taskkill /PID <pid> /T /F` kills the tree.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execFile: nodeExecFile } = require('node:child_process');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} KillTreeOpts
|
|
16
|
+
* @property {string} [platform] defaults to process.platform
|
|
17
|
+
* @property {(pid: number, signal?: string | number) => unknown} [kill] defaults to process.kill
|
|
18
|
+
* @property {(file: string, args: string[], options: object, cb: (err: Error | null) => void) => unknown} [execFile]
|
|
19
|
+
* @property {(msg: string) => void} [log] receives taskkill failure messages
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Kill `pid` and its descendants. Never throws.
|
|
24
|
+
*
|
|
25
|
+
* Return contract (deliberately synchronous on every platform so POSIX callers
|
|
26
|
+
* keep their existing control flow):
|
|
27
|
+
* - POSIX: true when a signal was delivered (group first, then the bare pid if
|
|
28
|
+
* the group kill threw ESRCH/EPERM), false otherwise.
|
|
29
|
+
* - win32: taskkill is started fire-and-forget and true is returned
|
|
30
|
+
* immediately when it was launched; its eventual non-zero exit is reported
|
|
31
|
+
* through `opts.log`, not the return value. The signal is ignored — taskkill
|
|
32
|
+
* /F is always forceful.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} pid
|
|
35
|
+
* @param {string | number} [signal]
|
|
36
|
+
* @param {KillTreeOpts} [opts]
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function killTree(pid, signal, opts = {}) {
|
|
40
|
+
const platform = opts.platform || process.platform;
|
|
41
|
+
const log = typeof opts.log === 'function' ? opts.log : () => {};
|
|
42
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
43
|
+
|
|
44
|
+
if (platform === 'win32') {
|
|
45
|
+
const execFile = opts.execFile || nodeExecFile;
|
|
46
|
+
try {
|
|
47
|
+
execFile('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true }, (err) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
try { log(`[killTree] taskkill /PID ${pid} failed: ${err.message}\n`); } catch { /* log must not throw */ }
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return true;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
try { log(`[killTree] taskkill spawn failed: ${e && e.message}\n`); } catch { /* log must not throw */ }
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const kill = opts.kill || process.kill.bind(process);
|
|
60
|
+
try { kill(-pid, signal); return true; } catch (e) {
|
|
61
|
+
const code = e && e.code;
|
|
62
|
+
if (code !== 'ESRCH' && code !== 'EPERM') return false;
|
|
63
|
+
}
|
|
64
|
+
try { kill(pid, signal); return true; } catch { return false; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* spawn() options for a child that killTree() will later kill.
|
|
69
|
+
* `detached` makes it a process-group leader on POSIX; on win32 it would open a
|
|
70
|
+
* console window unless `windowsHide` is set.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ platform?: string }} [opts]
|
|
73
|
+
* @returns {{ detached: true, windowsHide?: true }}
|
|
74
|
+
*/
|
|
75
|
+
function detachedSpawnOpts(opts = {}) {
|
|
76
|
+
const platform = opts.platform || process.platform;
|
|
77
|
+
return platform === 'win32' ? { detached: true, windowsHide: true } : { detached: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { killTree, detachedSpawnOpts };
|
|
@@ -116,6 +116,7 @@ function createLoadGate({
|
|
|
116
116
|
auditIntervalMs = AUDIT_INTERVAL_MS,
|
|
117
117
|
escalateAfterMs = JOB_OVERRUN_FLOOR_MS,
|
|
118
118
|
releaseWindowMs = RELEASE_WINDOW_MS,
|
|
119
|
+
platform = process.platform,
|
|
119
120
|
} = {}) {
|
|
120
121
|
let lastAuditAt = null; // null = never audited; the first gated tick always audits
|
|
121
122
|
let gatedSince = null;
|
|
@@ -124,6 +125,21 @@ function createLoadGate({
|
|
|
124
125
|
|
|
125
126
|
function evaluate({ bypass = false } = {}) {
|
|
126
127
|
const t = now();
|
|
128
|
+
if (platform === 'win32') {
|
|
129
|
+
// os.loadavg() is always [0,0,0] on Windows — the gate is open, not computed.
|
|
130
|
+
last = {
|
|
131
|
+
gated: false,
|
|
132
|
+
bypassed: false,
|
|
133
|
+
ratio: 0,
|
|
134
|
+
threshold: typeof threshold === 'function' ? threshold() : threshold,
|
|
135
|
+
loadavg1: null,
|
|
136
|
+
cores: cores(),
|
|
137
|
+
gatedSinceMs: 0,
|
|
138
|
+
reason: 'loadavg-unavailable-win32',
|
|
139
|
+
at: new Date(t).toISOString(),
|
|
140
|
+
};
|
|
141
|
+
return { ...last, shouldAudit: false, escalate: false };
|
|
142
|
+
}
|
|
127
143
|
const [l1] = loadavg();
|
|
128
144
|
const c = cores();
|
|
129
145
|
const th = typeof threshold === 'function' ? threshold() : threshold;
|