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.
Files changed (87) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/{AgentLibrary-QPw9gC_o.js → AgentLibrary-CKOZzc2W.js} +1 -1
  4. package/dist/assets/{Button-8RAbgh1d.js → Button-yuqFpQsK.js} +1 -1
  5. package/dist/assets/{DataModel-BEkuOZuS.js → DataModel-Bwdc_gaJ.js} +1 -1
  6. package/dist/assets/{History-BLrNSazM.js → History-D1Z0lydo.js} +1 -1
  7. package/dist/assets/{Hooks-C8P6wQq1.js → Hooks-CyD6l4Is.js} +1 -1
  8. package/dist/assets/{Library-CWFvBZw-.js → Library-fnod5cPe.js} +1 -1
  9. package/dist/assets/{ListDetail-B1jVMgzN.js → ListDetail-BKO68SJ4.js} +1 -1
  10. package/dist/assets/{MarkdownEditor-JrnUbWjW.js → MarkdownEditor-DrYMnMBy.js} +1 -1
  11. package/dist/assets/{McpServers-BGqTxDBm.js → McpServers-CgfItWhF.js} +1 -1
  12. package/dist/assets/{Memory-h8bhRNyI.js → Memory-4M3gO3_r.js} +1 -1
  13. package/dist/assets/{Panel-srf0FT0m.js → Panel-CKbXo-LT.js} +1 -1
  14. package/dist/assets/{Permissions-BA4oWydI.js → Permissions-Bc9EpjXL.js} +1 -1
  15. package/dist/assets/{Plugins-DTuTQqUL.js → Plugins-B_-PlEIx.js} +2 -2
  16. package/dist/assets/{ProvenanceBadge-BW18EquH.js → ProvenanceBadge-Cxev7XSA.js} +1 -1
  17. package/dist/assets/{SaveBar-Cc6UukFa.js → SaveBar-DiqscG6q.js} +1 -1
  18. package/dist/assets/{Scheduler-BctPlNL1.js → Scheduler-CCcJzuu8.js} +1 -1
  19. package/dist/assets/{ScopeSwitcher-DV1OnSuc.js → ScopeSwitcher-BsM4-jHQ.js} +1 -1
  20. package/dist/assets/{Settings-Bpyt1RGa.js → Settings-C3-VL-T8.js} +1 -1
  21. package/dist/assets/{SkillReferenceGraph-B28AYo12.js → SkillReferenceGraph-B9YlOuPo.js} +1 -1
  22. package/dist/assets/{Skills-zXLI5OB_.js → Skills-Dtf-z_zN.js} +1 -1
  23. package/dist/assets/{SystemPrompt-DKoN0UAV.js → SystemPrompt-HJDn0E8C.js} +1 -1
  24. package/dist/assets/{TagLibrary-BkDOa8IM.js → TagLibrary-DLGOfauU.js} +1 -1
  25. package/dist/assets/{TiptapBody-CIY1f6mI.js → TiptapBody-Cdmv1CuM.js} +1 -1
  26. package/dist/assets/{Toggle-C139wuAK.js → Toggle-CQFhOo1F.js} +1 -1
  27. package/dist/assets/{ViewTabs-CUEfsMOe.js → ViewTabs-CxjDCHjH.js} +1 -1
  28. package/dist/assets/index-Blo38vfX.js +3077 -0
  29. package/dist/assets/{index-D89EUizB.css → index-DkJv4l6L.css} +1 -1
  30. package/dist/assets/{settingsSchema-CAFckLz6.js → settingsSchema-B0uoL_1R.js} +1 -1
  31. package/dist/assets/useAgentsDirWatch-e3TJUFPP.js +1 -0
  32. package/dist/assets/{useConfigDirWatch-QKxfZx9X.js → useConfigDirWatch-Dxv7JbCU.js} +1 -1
  33. package/dist/index.html +2 -2
  34. package/package.json +10 -12
  35. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
  36. package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
  37. package/scripts/scheduler-mcp-server.cjs +51 -17
  38. package/src/main/build-info.json +4 -4
  39. package/src/main/chatRunner.cjs +8 -6
  40. package/src/main/crashDiagnostics.cjs +39 -3
  41. package/src/main/historyAggregator.cjs +7 -1
  42. package/src/main/index.cjs +73 -4
  43. package/src/main/ipcSchemas.cjs +27 -0
  44. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  45. package/src/main/lib/appRuntime.cjs +57 -0
  46. package/src/main/lib/childWithLog.cjs +10 -10
  47. package/src/main/lib/claudeBin.cjs +58 -20
  48. package/src/main/lib/cleanEnv.cjs +33 -9
  49. package/src/main/lib/customerFeedbackAdminRoutes.cjs +94 -0
  50. package/src/main/lib/customerFeedbackClient.cjs +261 -0
  51. package/src/main/lib/customerFeedbackInbox.cjs +286 -0
  52. package/src/main/lib/customerFeedbackIpc.cjs +43 -0
  53. package/src/main/lib/defaultShell.cjs +36 -0
  54. package/src/main/lib/definitionOfDone.cjs +6 -2
  55. package/src/main/lib/delegationReadiness.cjs +13 -5
  56. package/src/main/lib/gitWorktree.cjs +6 -3
  57. package/src/main/lib/insideHome.cjs +15 -4
  58. package/src/main/lib/killTree.cjs +80 -0
  59. package/src/main/lib/loadGate.cjs +16 -0
  60. package/src/main/lib/loginShellPath.cjs +82 -0
  61. package/src/main/lib/machineProfile.cjs +4 -2
  62. package/src/main/lib/mcpStdioServer.cjs +127 -0
  63. package/src/main/lib/mcpToolCatalog.cjs +26 -1
  64. package/src/main/lib/openExternalApp.cjs +62 -11
  65. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  66. package/src/main/lib/prereqs.cjs +145 -0
  67. package/src/main/lib/procIdentity.cjs +37 -2
  68. package/src/main/lib/projectFolder.cjs +89 -0
  69. package/src/main/lib/rcaReport.cjs +1 -0
  70. package/src/main/lib/reaperHelpers.cjs +5 -2
  71. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  72. package/src/main/lib/timeoutShimScript.cjs +14 -2
  73. package/src/main/lib/updateCheck.cjs +41 -11
  74. package/src/main/lib/watchdogHelpers.cjs +23 -3
  75. package/src/main/lib/winSpawn.cjs +67 -0
  76. package/src/main/lib/winUserData.cjs +22 -0
  77. package/src/main/otel.cjs +18 -62
  78. package/src/main/pty.cjs +32 -11
  79. package/src/main/runVerify.cjs +42 -2
  80. package/src/main/scheduler.cjs +116 -29
  81. package/src/main/seedSchedulerMcp.cjs +11 -2
  82. package/src/preload/api.d.ts +82 -0
  83. package/src/preload/index.cjs +14 -0
  84. package/src/seed/agents/validator.md +2 -2
  85. package/dist/assets/index-DrvxIbmR.js +0 -3073
  86. package/dist/assets/useAgentsDirWatch-XhQEVlaK.js +0 -1
  87. 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
- child = spawn(cmd.argv[0], cmd.argv.slice(1), {
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: { ...env, ...cmd.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
- const SCHEDULER_MCP_FIX_CMD = `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- node ${SCHEDULER_MCP_SERVER_SCRIPT}`;
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: SCHEDULER_MCP_FIX_CMD,
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: SCHEDULER_MCP_FIX_CMD,
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 node ${guardShimPath(def.scriptBasename, homeDir)}${def.nudge ? ' (a nudge, not a hard gate)' : ''}`,
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 = `node ${guardShimPath(def.scriptBasename, homeDir)}`;
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
- return { ok: true, integrated: true, fastForward: true };
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
- * Linux + darwin only. No Windows path handling.
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 isContained(realPath) {
56
- return realPath === HOME_REAL || realPath.startsWith(HOME_REAL + path.sep);
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;