claude-code-session-manager 0.119.1 → 0.121.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/{AgentLibrary-BpW0pGgk.js → AgentLibrary-FO2ei2wB.js} +1 -1
- package/dist/assets/{Button-DZX5guWb.js → Button-CeIOXUPU.js} +1 -1
- package/dist/assets/{DataModel-WG4pG_4z.js → DataModel-D0p__Hil.js} +1 -1
- package/dist/assets/{History-D58whvn_.js → History-DVqQ5azN.js} +1 -1
- package/dist/assets/{Hooks-DCI9ezg4.js → Hooks-DkTnRHyT.js} +1 -1
- package/dist/assets/{Library-YqMi-vsA.js → Library-l7KS6pkE.js} +1 -1
- package/dist/assets/{ListDetail-ap4W5z_Z.js → ListDetail-DDw8fuRT.js} +1 -1
- package/dist/assets/{McpServers-CT0pBTF8.js → McpServers-UUNIxk_3.js} +1 -1
- package/dist/assets/{Memory-BODDUBy4.js → Memory-CNYUoQ8C.js} +1 -1
- package/dist/assets/{Panel-CeOfUS9B.js → Panel-Bb54soc2.js} +1 -1
- package/dist/assets/{Permissions-DyOqpLQx.js → Permissions-D-dNdV3f.js} +1 -1
- package/dist/assets/{Plugins-DBXzKjL2.js → Plugins-DhcIXRhQ.js} +2 -2
- package/dist/assets/{ProvenanceBadge-C47IX1Q6.js → ProvenanceBadge-DEAkDNjA.js} +1 -1
- package/dist/assets/{RemoteMachines-CtkAJvkZ.js → RemoteMachines-xCEgvTGU.js} +1 -1
- package/dist/assets/{SaveBar-YMtzcCzP.js → SaveBar-BSEs7ecA.js} +1 -1
- package/dist/assets/{Scheduler-BNiYg0YD.js → Scheduler-BIKJIU-C.js} +5 -5
- package/dist/assets/{ScopeSwitcher-Dgi8CiDi.js → ScopeSwitcher-DcaFyAV-.js} +1 -1
- package/dist/assets/Settings-vcdWy_Jx.js +4 -0
- package/dist/assets/{SkillReferenceGraph-CaFsccNI.js → SkillReferenceGraph-CLrJWTPd.js} +1 -1
- package/dist/assets/{Skills-BeCMBQ3l.js → Skills-CYM4rBEC.js} +1 -1
- package/dist/assets/{SystemPrompt-C3WmcPEF.js → SystemPrompt-BWNIu-iY.js} +1 -1
- package/dist/assets/{TagLibrary-DP4GH7Dr.js → TagLibrary-CYdwHK6m.js} +1 -1
- package/dist/assets/{TiptapBody-CXCMvRr_.js → TiptapBody-Kt90lHrD.js} +1 -1
- package/dist/assets/{Toggle-TY-Wa8hv.js → Toggle-D1OXWYOz.js} +1 -1
- package/dist/assets/{ViewTabs-CaacRnp6.js → ViewTabs-BeQR3CKh.js} +1 -1
- package/dist/assets/{historyTotals-C28IlVVQ.js → historyTotals-B0qafrRo.js} +1 -1
- package/dist/assets/{index-DdEMkmbi.js → index-Bq9uH08R.js} +201 -201
- package/dist/assets/{index-DAMBm3UN.css → index-Dd7UQabV.css} +1 -1
- package/dist/assets/{settingsSchema-7Yg6dNNB.js → settingsSchema-0ZksSAPB.js} +1 -1
- package/dist/assets/{skillFrontmatter-Dcuys1km.js → skillFrontmatter-C0nh8yfa.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-B0H4M-TT.js → useAgentsDirWatch-DPkoBybS.js} +1 -1
- package/dist/assets/{useRemotePeers-Zp9jchCS.js → useRemotePeers-BDJMUihy.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +54 -14
- package/src/main/crashDiagnostics.cjs +18 -13
- package/src/main/health.cjs +129 -4
- package/src/main/index.cjs +57 -18
- package/src/main/ipcSchemas.cjs +18 -4
- package/src/main/lib/activeSessions.cjs +51 -34
- package/src/main/lib/claude/agentModelResolve.cjs +5 -3
- package/src/main/lib/claude/personaToolScope.cjs +68 -0
- package/src/main/lib/epic/archivedTranscript.cjs +91 -0
- package/src/main/lib/epic/archivedTranscriptMigration.cjs +84 -0
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/epicSettle.cjs +21 -5
- package/src/main/lib/epic/epicValidationHook.cjs +7 -0
- package/src/main/lib/git/depHealth.cjs +161 -0
- package/src/main/lib/git/worktreeDeps.cjs +158 -5
- package/src/main/lib/machineProfile.cjs +2 -0
- package/src/main/lib/mcpToolCatalog.cjs +3 -1
- package/src/main/lib/opsRetentionSettings.cjs +183 -0
- package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
- package/src/main/lib/prd/prdCreate.cjs +25 -0
- package/src/main/lib/prd/prdWorktreeReach.cjs +2 -2
- package/src/main/lib/prd/workTypeLibrary.cjs +7 -3
- package/src/main/lib/process/procGroupRss.cjs +61 -0
- package/src/main/lib/remoteTranscriptTail.cjs +69 -2
- package/src/main/lib/scheduler/queueHistory.cjs +68 -9
- package/src/main/lib/scheduler/runLogRetention.cjs +129 -43
- package/src/main/lib/scheduler/schedulerBatch.cjs +7 -3
- package/src/main/lib/scheduler/staleTmpSweep.cjs +117 -0
- package/src/main/lib/telemetry/auditLog.cjs +51 -6
- package/src/main/lib/telemetry/crashTelemetry.cjs +49 -1
- package/src/main/lib/telemetry/opsErrorLog.cjs +30 -10
- package/src/main/lib/telemetry/telemetryClient.cjs +5 -2
- package/src/main/scheduler/prdParser.cjs +50 -0
- package/src/main/scheduler/prdPaths.cjs +61 -16
- package/src/main/scheduler/reviewLadder.cjs +24 -1
- package/src/main/scheduler.cjs +214 -37
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/main/webRemote.cjs +82 -91
- package/src/preload/api.d.ts +41 -3
- package/src/preload/index.cjs +9 -0
- package/dist/assets/Settings-B4LuoRnd.js +0 -4
package/src/main/chatRunner.cjs
CHANGED
|
@@ -497,7 +497,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
497
497
|
// run settled without one.
|
|
498
498
|
let terminalSent = false;
|
|
499
499
|
let lineOverflowLogged = false;
|
|
500
|
-
const emitTerminal = (channel, payload) => {
|
|
500
|
+
const emitTerminal = (channel, payload, diag = {}) => {
|
|
501
501
|
if (terminalSent) return;
|
|
502
502
|
terminalSent = true;
|
|
503
503
|
// Every terminal error for this tab, logged once, regardless of the
|
|
@@ -507,6 +507,8 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
507
507
|
opsErrorLog.appendError({
|
|
508
508
|
cwd,
|
|
509
509
|
scope: 'chatRunner',
|
|
510
|
+
level: diag.level || 'error',
|
|
511
|
+
code: diag.code || payload?.code,
|
|
510
512
|
tabId,
|
|
511
513
|
tags: silent ? ['silent-probe'] : [],
|
|
512
514
|
message: payload?.message ?? 'chat run failed',
|
|
@@ -677,7 +679,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
677
679
|
`Killed after ${Math.round(elapsedMs / 60_000)}m (ceiling ${KILL_CEILING_MIN}m). ` +
|
|
678
680
|
`Last actions: ${lastActionsText}. External side effects may have completed — verify ` +
|
|
679
681
|
`before retrying.`,
|
|
680
|
-
});
|
|
682
|
+
}, { level: 'warn', code: 'ceiling-kill' });
|
|
681
683
|
cancelFn();
|
|
682
684
|
}, KILL_CEILING_MS);
|
|
683
685
|
if (killTimer.unref) killTimer.unref();
|
|
@@ -707,7 +709,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
707
709
|
tabId,
|
|
708
710
|
sessionId,
|
|
709
711
|
message: `spawn failed: ${err?.message ?? String(err)}`,
|
|
710
|
-
});
|
|
712
|
+
}, { code: 'spawn-failed' });
|
|
711
713
|
settle();
|
|
712
714
|
return;
|
|
713
715
|
}
|
|
@@ -803,7 +805,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
803
805
|
tabId,
|
|
804
806
|
sessionId,
|
|
805
807
|
message: `run ended with result subtype: ${event.subtype ?? 'unknown'}`,
|
|
806
|
-
});
|
|
808
|
+
}, { code: 'result-error' });
|
|
807
809
|
}
|
|
808
810
|
}
|
|
809
811
|
};
|
|
@@ -823,6 +825,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
823
825
|
opsErrorLog.appendError({
|
|
824
826
|
cwd,
|
|
825
827
|
scope: 'chatRunner',
|
|
828
|
+
code: 'stdout-line-overflow',
|
|
826
829
|
tabId,
|
|
827
830
|
tags: silent ? ['silent-probe'] : [],
|
|
828
831
|
message: `stdout line exceeded ${LINE_BUFFER_MAX} bytes without a newline; buffer dropped`,
|
|
@@ -843,7 +846,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
843
846
|
tabId,
|
|
844
847
|
sessionId,
|
|
845
848
|
message: err?.message ?? String(err),
|
|
846
|
-
});
|
|
849
|
+
}, { code: 'child-error' });
|
|
847
850
|
settle();
|
|
848
851
|
});
|
|
849
852
|
|
|
@@ -893,7 +896,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
893
896
|
tabId,
|
|
894
897
|
sessionId,
|
|
895
898
|
message: 'run cancelled',
|
|
896
|
-
});
|
|
899
|
+
}, { level: 'info', code: 'cancelled' });
|
|
897
900
|
} else if (SESSION_IN_USE_RE.test(stderrText()) || NO_CONVERSATION_RE.test(stderrText())) {
|
|
898
901
|
// A session-flag rejection that survived the one-shot swap (or was
|
|
899
902
|
// not eligible for it). The raw stderr dump names neither the flag,
|
|
@@ -911,7 +914,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
911
914
|
+ `Spawn cwd: ${execCwd}. Transcript: ${plan.transcriptPath ?? 'no transcript found on disk'}. `
|
|
912
915
|
+ `App version: ${appVersion}. If this version is behind the published latest, update the app first — `
|
|
913
916
|
+ 'the session-flag resolution logic lives in main, and a stale build resolves it with older rules.',
|
|
914
|
-
});
|
|
917
|
+
}, { code: 'session-flag-rejected' });
|
|
915
918
|
} else {
|
|
916
919
|
const preview = stderrBuffer.preview();
|
|
917
920
|
const errDetail = preview ? `: ${preview}` : '';
|
|
@@ -919,7 +922,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
919
922
|
tabId,
|
|
920
923
|
sessionId,
|
|
921
924
|
message: `process exited without a result event (code=${code} signal=${signal})${errDetail}`,
|
|
922
|
-
});
|
|
925
|
+
}, { code: 'exit-no-result' });
|
|
923
926
|
}
|
|
924
927
|
}
|
|
925
928
|
settle();
|
|
@@ -966,16 +969,48 @@ function cancel(tabId) {
|
|
|
966
969
|
// directly, reusing send()'s existing queued-vs-immediate branching rather
|
|
967
970
|
// than duplicating it here.
|
|
968
971
|
|
|
972
|
+
const EXTERNAL_SEND_ACK_TIMEOUT_MS = 8000;
|
|
973
|
+
const pendingExternalSends = new Map(); // requestId -> { resolve, timer }
|
|
974
|
+
|
|
975
|
+
function hasLiveWindow() {
|
|
976
|
+
try {
|
|
977
|
+
return !!(mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed());
|
|
978
|
+
} catch { return false; }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Resolves { disposition, reason? } once the renderer reports what it did, or
|
|
983
|
+
* { disposition: 'unconfirmed', reason: 'timeout' | 'no-window' }. Never rejects.
|
|
984
|
+
*/
|
|
969
985
|
function enqueueExternalPrompt(tabId, prompt, extra = {}) {
|
|
970
986
|
const { sessionId, cwd } = extra ?? {};
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
987
|
+
if (!hasLiveWindow()) return Promise.resolve({ disposition: 'unconfirmed', reason: 'no-window' });
|
|
988
|
+
const requestId = require('node:crypto').randomUUID();
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const timer = setTimeout(() => {
|
|
991
|
+
pendingExternalSends.delete(requestId);
|
|
992
|
+
resolve({ disposition: 'unconfirmed', reason: 'timeout' });
|
|
993
|
+
}, EXTERNAL_SEND_ACK_TIMEOUT_MS);
|
|
994
|
+
timer.unref?.();
|
|
995
|
+
pendingExternalSends.set(requestId, { resolve, timer });
|
|
996
|
+
broadcast('chat:external-send', {
|
|
997
|
+
requestId,
|
|
998
|
+
tabId,
|
|
999
|
+
prompt,
|
|
1000
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1001
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
1002
|
+
});
|
|
976
1003
|
});
|
|
977
1004
|
}
|
|
978
1005
|
|
|
1006
|
+
function settleExternalSend({ requestId, disposition, reason }) {
|
|
1007
|
+
const entry = pendingExternalSends.get(requestId);
|
|
1008
|
+
if (!entry) return;
|
|
1009
|
+
clearTimeout(entry.timer);
|
|
1010
|
+
pendingExternalSends.delete(requestId);
|
|
1011
|
+
entry.resolve({ disposition, ...(reason !== undefined ? { reason } : {}) });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
979
1014
|
// Asks the renderer to start a new Session (same launchSession a macro press
|
|
980
1015
|
// uses) on behalf of the paired phone. Payload is pre-validated by the caller.
|
|
981
1016
|
function requestExternalNewSession(payload) {
|
|
@@ -1007,7 +1042,7 @@ function registerAdminRoute(adminHttp) {
|
|
|
1007
1042
|
sendJson(res, 400, { ok: false, error: e?.message ?? 'invalid body' });
|
|
1008
1043
|
return;
|
|
1009
1044
|
}
|
|
1010
|
-
enqueueExternalPrompt(body.tabId, body.prompt);
|
|
1045
|
+
void enqueueExternalPrompt(body.tabId, body.prompt);
|
|
1011
1046
|
sendJson(res, 200, { ok: true });
|
|
1012
1047
|
});
|
|
1013
1048
|
}
|
|
@@ -1049,6 +1084,10 @@ function registerChatHandlers() {
|
|
|
1049
1084
|
return classifyPromptTicket(text);
|
|
1050
1085
|
}));
|
|
1051
1086
|
|
|
1087
|
+
ipcMain.handle('chat:external-send-result', validated(schemas.chatExternalSendResult, async (payload) => {
|
|
1088
|
+
settleExternalSend(payload);
|
|
1089
|
+
}));
|
|
1090
|
+
|
|
1052
1091
|
ipcMain.handle('chat:cancel', async (_e, payload) => {
|
|
1053
1092
|
let tabId;
|
|
1054
1093
|
try { tabId = schemas.chatCancel.parse(payload).tabId; } catch { return; }
|
|
@@ -1079,6 +1118,7 @@ module.exports = {
|
|
|
1079
1118
|
WARN_CEILING_MS,
|
|
1080
1119
|
__setExecutor,
|
|
1081
1120
|
enqueueExternalPrompt,
|
|
1121
|
+
EXTERNAL_SEND_ACK_TIMEOUT_MS,
|
|
1082
1122
|
requestExternalNewSession,
|
|
1083
1123
|
registerAdminRoute,
|
|
1084
1124
|
};
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
const { app, powerSaveBlocker, powerMonitor, crashReporter } = require('electron');
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
|
-
const { reportCrash } = require('./lib/telemetry/crashTelemetry.cjs');
|
|
29
|
+
const { reportCrash, classifyUncleanShutdown, reportChildProcessGone } = require('./lib/telemetry/crashTelemetry.cjs');
|
|
30
30
|
const { createLoopDelayMonitor, stallVerdict } = require('./lib/util/loopDelay.cjs');
|
|
31
31
|
|
|
32
32
|
let logs = null;
|
|
@@ -37,6 +37,7 @@ let loopMonitor = null;
|
|
|
37
37
|
let sentinelPath = null;
|
|
38
38
|
let startedAtIso = null;
|
|
39
39
|
let lastSample = null;
|
|
40
|
+
let lastLoop = null;
|
|
40
41
|
|
|
41
42
|
// Warn threshold for total working set across all Electron processes. The OOM
|
|
42
43
|
// journal showed a single renderer hitting ~900 MB anon-rss before the kernel
|
|
@@ -140,6 +141,7 @@ function persistSentinel(clean) {
|
|
|
140
141
|
startedAt: startedAtIso,
|
|
141
142
|
closedAt: clean ? new Date().toISOString() : undefined,
|
|
142
143
|
lastSample,
|
|
144
|
+
loop: lastLoop ? { loopDelayMaxMs: lastLoop.loopDelayMaxMs, loopDelayP99Ms: lastLoop.loopDelayP99Ms } : null,
|
|
143
145
|
};
|
|
144
146
|
try {
|
|
145
147
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
@@ -152,11 +154,12 @@ function persistSentinel(clean) {
|
|
|
152
154
|
|
|
153
155
|
function heartbeat() {
|
|
154
156
|
lastSample = sampleMemory();
|
|
157
|
+
const loop = loopMonitor ? loopMonitor.snapshot() : null;
|
|
158
|
+
lastLoop = loop;
|
|
155
159
|
// Persist every tick so the final pre-crash sample survives a SIGKILL.
|
|
156
160
|
persistSentinel(false);
|
|
157
161
|
const top = [...lastSample.procs].sort((a, b) => b.mb - a.mb).slice(0, 4);
|
|
158
162
|
const level = lastSample.totalMb >= MEM_WARN_MB ? 'warn' : 'debug';
|
|
159
|
-
const loop = loopMonitor ? loopMonitor.snapshot() : null;
|
|
160
163
|
logs?.writeLine({
|
|
161
164
|
scope: 'crash-diag',
|
|
162
165
|
level,
|
|
@@ -183,10 +186,14 @@ function checkPreviousRun() {
|
|
|
183
186
|
if (!prev || prev.open !== true) return;
|
|
184
187
|
const s = prev.lastSample;
|
|
185
188
|
const lastTop = s && Array.isArray(s.procs) ? [...s.procs].sort((a, b) => b.mb - a.mb).slice(0, 4).map((p) => `${p.type}:${p.mb}MB`) : undefined;
|
|
189
|
+
const lastLoopDelayMaxMs = prev.loop?.loopDelayMaxMs;
|
|
190
|
+
const sampleMs = s?.ts ? Date.parse(s.ts) : NaN;
|
|
191
|
+
const silentGapSec = Number.isFinite(sampleMs) ? Math.round((Date.now() - sampleMs) / 1000) : undefined;
|
|
192
|
+
const likelyCause = classifyUncleanShutdown({ lastLoopDelayMaxMs, lastTotalMb: s?.totalMb, silentGapSec });
|
|
186
193
|
logs?.writeLine({
|
|
187
194
|
scope: 'crash-diag',
|
|
188
195
|
level: 'error',
|
|
189
|
-
message:
|
|
196
|
+
message: `previous session ended UNCLEANLY (likelyCause=${likelyCause}; no graceful quit)`,
|
|
190
197
|
meta: {
|
|
191
198
|
prevPid: prev.pid,
|
|
192
199
|
startedAt: prev.startedAt,
|
|
@@ -195,7 +202,10 @@ function checkPreviousRun() {
|
|
|
195
202
|
lastMainRssMb: s?.main?.rssMb,
|
|
196
203
|
lastTop,
|
|
197
204
|
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
198
|
-
|
|
205
|
+
lastLoopDelayMaxMs,
|
|
206
|
+
silentGapSec,
|
|
207
|
+
likelyCause,
|
|
208
|
+
hint: 'Check `journalctl -k | grep -i oom` around lastSampleTs to confirm the kernel OOM-killer, and the "main-loop stall" heartbeats before lastSampleTs to confirm a wedged main thread.',
|
|
199
209
|
},
|
|
200
210
|
});
|
|
201
211
|
// The unclean-shutdown postmortem is the report most likely to be the ONLY
|
|
@@ -209,6 +219,9 @@ function checkPreviousRun() {
|
|
|
209
219
|
lastMainRssMb: s?.main?.rssMb,
|
|
210
220
|
lastTop,
|
|
211
221
|
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
222
|
+
lastLoopDelayMaxMs,
|
|
223
|
+
silentGapSec,
|
|
224
|
+
likelyCause,
|
|
212
225
|
},
|
|
213
226
|
});
|
|
214
227
|
}
|
|
@@ -244,15 +257,7 @@ function registerCrashHooks() {
|
|
|
244
257
|
lastTotalMb: lastSample?.totalMb,
|
|
245
258
|
},
|
|
246
259
|
});
|
|
247
|
-
|
|
248
|
-
reason: details?.reason || 'unknown',
|
|
249
|
-
meta: {
|
|
250
|
-
type: details?.type,
|
|
251
|
-
name: details?.name || details?.serviceName,
|
|
252
|
-
exitCode: details?.exitCode,
|
|
253
|
-
lastTotalMb: lastSample?.totalMb,
|
|
254
|
-
},
|
|
255
|
-
});
|
|
260
|
+
reportChildProcessGone({ details, lastTotalMb: lastSample?.totalMb });
|
|
256
261
|
});
|
|
257
262
|
|
|
258
263
|
// powerMonitor ties the suspend story to the crash timeline. If we ever log
|
package/src/main/health.cjs
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('node:path');
|
|
|
10
10
|
const os = require('node:os');
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
const { POLL_INTERVAL_MS, loadGateThreshold } = require('./lib/scheduler/schedulerConfig.cjs');
|
|
13
|
-
const { opsPath } = require('./lib/opsOwnership.cjs');
|
|
13
|
+
const { opsPath, OPS_ROOT_DIR } = require('./lib/opsOwnership.cjs');
|
|
14
14
|
const { effectivePaused } = require('./lib/scheduler/upgradeDrain.cjs');
|
|
15
15
|
const { checkPersonaImports } = require('./lib/persona-macro/personaImportHealth.cjs');
|
|
16
16
|
const { checkDelegationReadiness } = require('./lib/epic/delegationReadiness.cjs');
|
|
@@ -24,6 +24,7 @@ const { classifyQueueStarvation, launchBlockedSlugs, STARVE_ESCALATION_MS } = re
|
|
|
24
24
|
const { REVERIFY_INTERVAL_MS } = require('./scheduler/reviewLadder.cjs');
|
|
25
25
|
const { findStarvedProjects, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('./lib/scheduler/schedulerBatch.cjs');
|
|
26
26
|
const { auditLogPath, readTail } = require('./lib/telemetry/auditLog.cjs');
|
|
27
|
+
const { checkMainDependencyHealth } = require('./lib/git/depHealth.cjs');
|
|
27
28
|
const { resolveBuildIdentity } = require('./lib/buildIdentity.cjs');
|
|
28
29
|
const { computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/scheduler/runLogRetention.cjs');
|
|
29
30
|
const { allProjectCwds } = require('./lib/activeSessions.cjs');
|
|
@@ -399,6 +400,41 @@ function evaluateEpicIndexHealth(cwds) {
|
|
|
399
400
|
return { component: { ok: !anyOrphanRows, byProject }, issues };
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
// Read-only scan for stray `session-manager-operations` dirs nested under a
|
|
404
|
+
// project root (symptom of a writer joining an agent cwd without
|
|
405
|
+
// projectRootOf/opsPath). The top-level one is legitimate; node_modules, .git
|
|
406
|
+
// and .claude/worktrees are skipped. Bounded depth, sync readdir, never writes.
|
|
407
|
+
const OPS_DIR_NAME = OPS_ROOT_DIR;
|
|
408
|
+
function findNestedOpsRoots(projectRoot, { maxDepth = 3 } = {}) {
|
|
409
|
+
const found = [];
|
|
410
|
+
const walk = (dir, depth) => {
|
|
411
|
+
let entries;
|
|
412
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
413
|
+
for (const e of entries) {
|
|
414
|
+
if (!e.isDirectory()) continue; // symlinks are not followed
|
|
415
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
416
|
+
const full = path.join(dir, e.name);
|
|
417
|
+
if (e.name === '.claude') {
|
|
418
|
+
// only .claude/worktrees is skipped; other .claude children still scan
|
|
419
|
+
if (depth + 1 < maxDepth) {
|
|
420
|
+
try {
|
|
421
|
+
for (const c of fs.readdirSync(full, { withFileTypes: true })) {
|
|
422
|
+
if (c.isDirectory() && c.name !== 'worktrees') walk(path.join(full, c.name), depth + 2);
|
|
423
|
+
}
|
|
424
|
+
} catch { /* unreadable */ }
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (e.name === OPS_DIR_NAME) {
|
|
429
|
+
if (depth > 0) { found.push(full); continue; } // stray; don't descend
|
|
430
|
+
}
|
|
431
|
+
if (depth + 1 < maxDepth) walk(full, depth + 1);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
walk(projectRoot, 0);
|
|
435
|
+
return found;
|
|
436
|
+
}
|
|
437
|
+
|
|
402
438
|
// Pure evaluator over ~/.claude/session-manager/scheduler-state.json's parsed
|
|
403
439
|
// content (the sidecar scheduler.cjs's pollLoop maintains for the billing
|
|
404
440
|
// usage-meter / rate-limit-window poller — see rateLimitPollerStreak tests).
|
|
@@ -619,14 +655,56 @@ function latestStarveEscalationReasons(auditLogPath) {
|
|
|
619
655
|
* repo HEAD differs from the installed build. Both are reported, neither is
|
|
620
656
|
* critical (never flips status.ok).
|
|
621
657
|
*/
|
|
622
|
-
|
|
658
|
+
const WORKTREE_SAFETY_PATHS = [
|
|
659
|
+
'src/main/lib/git/worktreeDeps.cjs',
|
|
660
|
+
'src/main/lib/git/depHealth.cjs',
|
|
661
|
+
'src/main/lib/git/gitWorktree.cjs',
|
|
662
|
+
'src/main/lib/git/jobWorktree.cjs',
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
function evaluateBuildFreshness({ running = null, installedOnDisk = null, repoHead = null, safetyCommitsMissing = [] } = {}) {
|
|
623
666
|
const differs = (a, b) => !!a && !!b && !(a.startsWith(b) || b.startsWith(a));
|
|
624
667
|
const restartNeeded = differs(running, installedOnDisk);
|
|
625
668
|
const publishNeeded = differs(repoHead, installedOnDisk);
|
|
626
669
|
const notes = [];
|
|
627
670
|
if (restartNeeded) notes.push(`restart needed: running ${running}, installed ${installedOnDisk}`);
|
|
628
671
|
if (publishNeeded) notes.push(`publish needed: repo HEAD ${repoHead}, installed ${installedOnDisk}`);
|
|
629
|
-
|
|
672
|
+
let safetyIssue;
|
|
673
|
+
if (safetyCommitsMissing.length) {
|
|
674
|
+
const listed = safetyCommitsMissing.slice(0, 3).map((c) => `${c.sha} ${c.subject}`).join('; ');
|
|
675
|
+
const more = safetyCommitsMissing.length > 3 ? `; +${safetyCommitsMissing.length - 3} more` : '';
|
|
676
|
+
safetyIssue = `running build ${running} predates ${safetyCommitsMissing.length} worktree-safety commit(s): ${listed}${more} — publish a new release and restart Session Manager`;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
ok: true, running, installedOnDisk, repoHead, restartNeeded, publishNeeded,
|
|
680
|
+
safetyCommitsMissing, ...(safetyIssue ? { safetyIssue } : {}),
|
|
681
|
+
...(notes.length ? { note: notes.join('; ') } : {}),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* evaluateMainDepLinks(results) → { component, issues }
|
|
687
|
+
*
|
|
688
|
+
* Pure over `Array<{ cwd, health }>` (health = checkMainDependencyHealth's
|
|
689
|
+
* return). One issue per project whose node_modules links point into a job
|
|
690
|
+
* worktree or dangle. Non-critical: component.ok is always true.
|
|
691
|
+
*/
|
|
692
|
+
function evaluateMainDepLinks(results = []) {
|
|
693
|
+
const list = Array.isArray(results) ? results : [];
|
|
694
|
+
const broken = [];
|
|
695
|
+
const issues = [];
|
|
696
|
+
for (const { cwd, health } of list) {
|
|
697
|
+
if (!health || health.ok !== false) continue;
|
|
698
|
+
if (health.reason !== 'links-into-worktree' && health.reason !== 'dangling-links') continue;
|
|
699
|
+
const missing = Array.isArray(health.missing) ? health.missing : [];
|
|
700
|
+
broken.push({ cwd, reason: health.reason, missing });
|
|
701
|
+
const shown = missing.slice(0, 5).join(', ');
|
|
702
|
+
const more = missing.length > 5 ? `, +${missing.length - 5} more` : '';
|
|
703
|
+
issues.push(
|
|
704
|
+
`${cwd}: node_modules ${health.reason}${shown ? ` (${shown}${more})` : ''} — fix: cd ${cwd} && CI=1 pnpm install --offline --frozen-lockfile`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return { component: { ok: true, checked: list.length, broken }, issues };
|
|
630
708
|
}
|
|
631
709
|
|
|
632
710
|
/**
|
|
@@ -1234,6 +1312,18 @@ async function check(opts = {}) {
|
|
|
1234
1312
|
status.components.epic_transcripts = { ok: true, error: e.message };
|
|
1235
1313
|
}
|
|
1236
1314
|
|
|
1315
|
+
// 6.665. Nested ops roots (NON-fatal WARN): stray session-manager-operations
|
|
1316
|
+
// dirs under this project. Read-only; never deletes.
|
|
1317
|
+
try {
|
|
1318
|
+
const nested = findNestedOpsRoots(PROJECT_ROOT, { maxDepth: 3 });
|
|
1319
|
+
status.components.nested_ops_roots = { ok: true, count: nested.length, paths: nested };
|
|
1320
|
+
if (nested.length > 0) {
|
|
1321
|
+
status.issues.push(`Warning: ${nested.length} nested session-manager-operations dir(s): ${nested.join(', ')}`);
|
|
1322
|
+
}
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
status.components.nested_ops_roots = { ok: true, error: e.message };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1237
1327
|
// 6.7. Build freshness (informational): running heartbeat vs installed
|
|
1238
1328
|
// build-info vs repo HEAD.
|
|
1239
1329
|
try {
|
|
@@ -1243,15 +1333,47 @@ async function check(opts = {}) {
|
|
|
1243
1333
|
timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
1244
1334
|
}).trim() || null;
|
|
1245
1335
|
} catch { /* not a git checkout */ }
|
|
1336
|
+
const running = readFreshHeartbeat(schedulerPaths.heartbeatPath())?.build?.codeSha ?? null;
|
|
1337
|
+
let safetyCommitsMissing = [];
|
|
1338
|
+
if (typeof running === 'string' && running && typeof repoHead === 'string' && repoHead) {
|
|
1339
|
+
try {
|
|
1340
|
+
safetyCommitsMissing = execFileSync(
|
|
1341
|
+
'git',
|
|
1342
|
+
['-C', PROJECT_ROOT, 'log', '--format=%h%x09%s', `${running}..HEAD`, '--', ...WORKTREE_SAFETY_PATHS],
|
|
1343
|
+
{ timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
1344
|
+
).split('\n').filter(Boolean).map((line) => {
|
|
1345
|
+
const i = line.indexOf('\t');
|
|
1346
|
+
return { sha: line.slice(0, i), subject: line.slice(i + 1) };
|
|
1347
|
+
});
|
|
1348
|
+
} catch { safetyCommitsMissing = []; }
|
|
1349
|
+
}
|
|
1246
1350
|
status.components.build = evaluateBuildFreshness({
|
|
1247
|
-
running
|
|
1351
|
+
running,
|
|
1248
1352
|
installedOnDisk: resolveBuildIdentity().codeSha ?? null,
|
|
1249
1353
|
repoHead,
|
|
1354
|
+
safetyCommitsMissing,
|
|
1250
1355
|
});
|
|
1356
|
+
if (status.components.build.safetyIssue) status.issues.push(status.components.build.safetyIssue);
|
|
1251
1357
|
} catch (e) {
|
|
1252
1358
|
status.components.build = { ok: true, error: e.message };
|
|
1253
1359
|
}
|
|
1254
1360
|
|
|
1361
|
+
// 6.75. Main-checkout dependency links: node_modules entries pointing into a
|
|
1362
|
+
// job worktree or dangling, for every cwd the queue knows. Non-critical.
|
|
1363
|
+
if (queueState) {
|
|
1364
|
+
try {
|
|
1365
|
+
const cwds = [...new Set(Object.values(queueState.jobs || {})
|
|
1366
|
+
.map((j) => j && j.cwd).filter((c) => typeof c === 'string' && c))].slice(0, 50);
|
|
1367
|
+
const results = cwds.filter((cwd) => fs.existsSync(cwd))
|
|
1368
|
+
.map((cwd) => ({ cwd, health: checkMainDependencyHealth(cwd) }));
|
|
1369
|
+
const { component, issues } = evaluateMainDepLinks(results);
|
|
1370
|
+
status.components.main_dep_links = component;
|
|
1371
|
+
status.issues.push(...issues);
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
status.components.main_dep_links = { ok: true, error: e.message };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1255
1377
|
// 6.8. Credential health (informational-but-loud): is the Claude OAuth token
|
|
1256
1378
|
// usable? An expired/absent token silently voids every headless `claude -p`
|
|
1257
1379
|
// job — the queue "runs" but nothing authenticates. refreshIfNeeded() reads
|
|
@@ -1297,6 +1419,7 @@ module.exports = {
|
|
|
1297
1419
|
evaluateCredentialHealth,
|
|
1298
1420
|
computeEpicIndexDrift,
|
|
1299
1421
|
evaluateEpicIndexHealth,
|
|
1422
|
+
findNestedOpsRoots,
|
|
1300
1423
|
computeProjectProblemCounts,
|
|
1301
1424
|
evaluateWorktreeCapBlocked,
|
|
1302
1425
|
evaluatePerProjectStall,
|
|
@@ -1309,6 +1432,8 @@ module.exports = {
|
|
|
1309
1432
|
evaluateBlockingParkHealth,
|
|
1310
1433
|
evaluateStarveEscalationHealth,
|
|
1311
1434
|
evaluateBuildFreshness,
|
|
1435
|
+
evaluateMainDepLinks,
|
|
1436
|
+
WORKTREE_SAFETY_PATHS,
|
|
1312
1437
|
latestStarveEscalationReasons,
|
|
1313
1438
|
DISPATCH_STALL_THRESHOLD_MS,
|
|
1314
1439
|
DISPATCH_WARN_THRESHOLD_MS,
|
package/src/main/index.cjs
CHANGED
|
@@ -1007,6 +1007,7 @@ pluginInstall.registerPluginInstallHandlers();
|
|
|
1007
1007
|
memoryTool.registerMemoryHandlers();
|
|
1008
1008
|
activeIndexMerge.registerActiveIndexMergeHandlers();
|
|
1009
1009
|
promptSessionsCreateEpic.registerPromptSessionsCreateEpicHandlers();
|
|
1010
|
+
require('./lib/epic/archivedTranscript.cjs').registerIpc(ipcMain);
|
|
1010
1011
|
customerFeedbackIpc.registerCustomerFeedbackHandlers();
|
|
1011
1012
|
epicWorktreeMint.registerEpicWorktreeMintHandlers();
|
|
1012
1013
|
epicWorktreeMerge.registerEpicWorktreeMergeHandlers();
|
|
@@ -1044,6 +1045,11 @@ try {
|
|
|
1044
1045
|
} catch (e) {
|
|
1045
1046
|
logs.writeLine({ scope: 'main', level: 'error', message: 'updater IPC registration failed', meta: { error: e?.message } });
|
|
1046
1047
|
}
|
|
1048
|
+
try {
|
|
1049
|
+
require('./lib/opsRetentionSettings.cjs').registerIpc(ipcMain);
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
logs.writeLine({ scope: 'main', level: 'error', message: 'ops retention IPC registration failed', meta: { error: e?.message } });
|
|
1052
|
+
}
|
|
1047
1053
|
chatRunner.registerChatHandlers();
|
|
1048
1054
|
|
|
1049
1055
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
|
@@ -1110,9 +1116,11 @@ const SMFILE_MIME = {
|
|
|
1110
1116
|
// E2E tests also skip so playwright.electron.launch can run multiple specs
|
|
1111
1117
|
// back-to-back without the prior app's lock causing the next launch to quit.
|
|
1112
1118
|
const isDev = process.env.SM_DEV === '1' || process.env.SM_E2E === '1';
|
|
1119
|
+
let lostInstanceLock = false;
|
|
1113
1120
|
if (!isDev) {
|
|
1114
1121
|
const gotLock = app.requestSingleInstanceLock();
|
|
1115
1122
|
if (!gotLock) {
|
|
1123
|
+
lostInstanceLock = true;
|
|
1116
1124
|
terminateLosingInstance(app);
|
|
1117
1125
|
} else {
|
|
1118
1126
|
app.on('second-instance', () => {
|
|
@@ -1125,6 +1133,31 @@ if (!isDev) {
|
|
|
1125
1133
|
}
|
|
1126
1134
|
}
|
|
1127
1135
|
|
|
1136
|
+
function runRunLogRetentionPass() {
|
|
1137
|
+
if (lostInstanceLock) return;
|
|
1138
|
+
try {
|
|
1139
|
+
const schedulerPaths = require('./lib/scheduler/schedulerPaths.cjs');
|
|
1140
|
+
const staleTmpSweep = require('./lib/scheduler/staleTmpSweep.cjs');
|
|
1141
|
+
const result = runLogRetention.runBootSweep();
|
|
1142
|
+
const plansRoot = schedulerPaths.scheduledPlansRoot();
|
|
1143
|
+
const tmp = staleTmpSweep.sweepStaleTmp(plansRoot);
|
|
1144
|
+
const archive = staleTmpSweep.pruneRunsArchive(path.join(plansRoot, 'runs-archive'));
|
|
1145
|
+
logs.writeLine({
|
|
1146
|
+
scope: 'run-log-retention',
|
|
1147
|
+
level: 'info',
|
|
1148
|
+
message: result.deleted ? 'sweep applied' : 'sweep dry-run',
|
|
1149
|
+
meta: {
|
|
1150
|
+
deleted: result.deleted,
|
|
1151
|
+
removedFiles: (result.removedFiles || 0) + tmp.removedFiles + archive.removedFiles,
|
|
1152
|
+
freedBytes: (result.freedBytes || 0) + tmp.freedBytes + archive.freedBytes,
|
|
1153
|
+
errorCount: (result.errors ? result.errors.length : 0) + tmp.errors.length + archive.errors.length,
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
logs.writeLine({ scope: 'run-log-retention', level: 'error', message: 'sweep failed', meta: { error: e?.message } });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1128
1161
|
// Global webContents hardening — applies to DevTools, future webContents, etc.
|
|
1129
1162
|
// Must be registered before app.whenReady so it captures the first window too.
|
|
1130
1163
|
app.on('web-contents-created', (_e, wc) => {
|
|
@@ -1454,25 +1487,31 @@ app.whenReady().then(async () => {
|
|
|
1454
1487
|
logs.writeLine({ scope: 'history-rollup', level: 'error', message: 'finalizeClosedDays failed', meta: { error: e?.message } });
|
|
1455
1488
|
});
|
|
1456
1489
|
runIntradayRefresh();
|
|
1457
|
-
// Run-log retention sweep:
|
|
1458
|
-
//
|
|
1490
|
+
// Run-log retention sweep: runs once 30s after boot, then every 24h (a
|
|
1491
|
+
// long-lived app would otherwise never trim). Skipped when this process
|
|
1492
|
+
// lost the single-instance lock (lostInstanceLock, set beside
|
|
1493
|
+
// terminateLosingInstance above). Read-only unless config.schedulerRunLogRetention
|
|
1459
1494
|
// opts in (see runLogRetention.cjs's safety model) — non-fatal on error.
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1495
|
+
runRunLogRetentionPass();
|
|
1496
|
+
const retentionTimer = setInterval(runRunLogRetentionPass, 24 * 60 * 60 * 1000);
|
|
1497
|
+
if (retentionTimer.unref) retentionTimer.unref();
|
|
1498
|
+
// Archived-transcript migration: move embedded `transcript` strings into gzip
|
|
1499
|
+
// sidecars, <=50 files per project per boot, yielding between files. Uses the
|
|
1500
|
+
// cached allProjectCwds() list (never a fresh ~/.claude/projects scan).
|
|
1501
|
+
(async () => {
|
|
1502
|
+
try {
|
|
1503
|
+
const { migrateProject } = require('./lib/epic/archivedTranscriptMigration.cjs');
|
|
1504
|
+
for (const projectCwd of allProjectCwds()) {
|
|
1505
|
+
const r = await migrateProject(projectCwd, { limit: 50, yieldFn: () => new Promise((res) => setImmediate(res)) });
|
|
1506
|
+
if (r.migrated || r.errors.length) {
|
|
1507
|
+
logs.writeLine({ scope: 'main', level: r.errors.length ? 'warn' : 'info', message: 'archived transcript migration', meta: { projectCwd, migrated: r.migrated, freedBytes: r.freedBytes, errors: r.errors.length } });
|
|
1508
|
+
}
|
|
1509
|
+
await new Promise((res) => setImmediate(res));
|
|
1510
|
+
}
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
logs.writeLine({ scope: 'main', level: 'error', message: 'archived transcript migration failed', meta: { error: e?.message } });
|
|
1513
|
+
}
|
|
1514
|
+
})();
|
|
1476
1515
|
}, 30_000);
|
|
1477
1516
|
// Keep TODAY's rollup line current so the History dashboard never needs to
|
|
1478
1517
|
// fall back to a live transcript scan. Shares the same O_EXCL lock file as
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -13,7 +13,7 @@ const { PromptSessionSchema, EpicTagSchema, EpicSourceSchema, EpicIntakeSectionS
|
|
|
13
13
|
const { PrdWorkTypeSchema } = require('./lib/prd/workTypeLibrary.cjs');
|
|
14
14
|
const { PERSONA_NAME_RE } = require('./agentLibrary.cjs');
|
|
15
15
|
const { AgentPersonaSaveSchema } = require('./lib/persona-macro/agentPersonaSchema.cjs');
|
|
16
|
-
const { MacroSaveSchema, MacroDeleteSchema, MacroSetProjectSchema } = require('./lib/persona-macro/macroLibrary.cjs');
|
|
16
|
+
const { MacroSaveSchema, MacroDeleteSchema, MacroSetProjectSchema, MacroInputSchema } = require('./lib/persona-macro/macroLibrary.cjs');
|
|
17
17
|
|
|
18
18
|
// ──────────────────────────────────────────── PTY
|
|
19
19
|
const ptySpawn = z.object({
|
|
@@ -415,9 +415,9 @@ const schedulerCreatePrd = z.object({
|
|
|
415
415
|
// User-selected work-type tag (PRD 774; independence from the parent
|
|
416
416
|
// Epic's own tag confirmed PRD 1041) — deterministic, never LLM-classified.
|
|
417
417
|
// Epic tag and PRD tag draw on the same WorkType vocabulary
|
|
418
|
-
// (lib/prd/workTypeLibrary.cjs) but are independent values; 'discussion'
|
|
418
|
+
// (lib/prd/workTypeLibrary.cjs) but are independent values; 'discussion' and 'portfolio' are
|
|
419
419
|
// excluded here for the same reason adminPrdFrontmatterPatch excludes it
|
|
420
|
-
// below — a discussion-tagged Epic never reaches PRD authoring.
|
|
420
|
+
// below — a discussion- or portfolio-tagged Epic never reaches PRD authoring.
|
|
421
421
|
tag: PrdWorkTypeSchema.optional(),
|
|
422
422
|
// WHO executes this PRD (persona name, `~/.claude/agents/<name>.md`) —
|
|
423
423
|
// independent of `tag` above. Optional: prdCreate.cjs's buildPrdBody fills
|
|
@@ -516,7 +516,7 @@ const adminPrdFrontmatterPatch = z.object({
|
|
|
516
516
|
parallelGroup: z.number().int().min(0).max(999999).optional(),
|
|
517
517
|
sourcePromptId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
518
518
|
sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
519
|
-
// 'discussion' deliberately excluded: matches prdFrontmatter.cjs's
|
|
519
|
+
// 'discussion' and 'portfolio' deliberately excluded: matches prdFrontmatter.cjs's
|
|
520
520
|
// parsePrdFile, which never parses that value for a PRD's tag key (a
|
|
521
521
|
// discussion-tagged Epic never reaches PRD authoring) — see
|
|
522
522
|
// workTypeLibrary.cjs's PrdWorkTypeSchema.
|
|
@@ -960,6 +960,14 @@ const chatExternalSend = z.object({
|
|
|
960
960
|
),
|
|
961
961
|
});
|
|
962
962
|
|
|
963
|
+
// Renderer's report of what it did with a `chat:external-send` (mirrors
|
|
964
|
+
// ChatExternalSendResult in preload/api.d.ts).
|
|
965
|
+
const chatExternalSendResult = z.object({
|
|
966
|
+
requestId: z.string().min(1).max(64),
|
|
967
|
+
disposition: z.enum(['sent', 'queued', 'refused']),
|
|
968
|
+
reason: z.enum(['terminal-attached', 'completed', 'unknown-target', 'empty-prompt']).optional(),
|
|
969
|
+
});
|
|
970
|
+
|
|
963
971
|
// Phone drill-down reads. cwd is re-checked against the open tabs in
|
|
964
972
|
// webRemote.cjs; sessionKey becomes a transcript filename lookup so it is
|
|
965
973
|
// restricted to a session-id charset (no '/', so no traversal).
|
|
@@ -968,6 +976,7 @@ const remoteSessionTranscript = z.object({
|
|
|
968
976
|
cwd: z.string().min(1).max(4096),
|
|
969
977
|
sessionKey: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*$/),
|
|
970
978
|
limit: z.number().int().min(1).max(100).optional(),
|
|
979
|
+
before: z.number().int().min(0).max(10000).optional(),
|
|
971
980
|
});
|
|
972
981
|
// One Epic's PRD list (slug/title/status projection only, never bodies).
|
|
973
982
|
const remoteSessionPrds = z.object({
|
|
@@ -986,6 +995,10 @@ const remoteSessionCreate = z.object({
|
|
|
986
995
|
(s) => Buffer.byteLength(s, 'utf8') <= CHAT_PROMPT_MAX_BYTES,
|
|
987
996
|
`prompt must be ≤ ${CHAT_PROMPT_MAX_BYTES} bytes`,
|
|
988
997
|
),
|
|
998
|
+
macroId: MacroDeleteSchema.shape.id.optional(),
|
|
999
|
+
inputs: z.record(MacroInputSchema.shape.name, z.string().max(2000))
|
|
1000
|
+
.refine((o) => Object.keys(o).length <= 20, 'too many inputs')
|
|
1001
|
+
.optional(),
|
|
989
1002
|
});
|
|
990
1003
|
|
|
991
1004
|
// ──────────────────────────────────────────── Web Remote
|
|
@@ -1352,6 +1365,7 @@ module.exports = {
|
|
|
1352
1365
|
chatCancel,
|
|
1353
1366
|
chatClassifyTicket,
|
|
1354
1367
|
chatExternalSend,
|
|
1368
|
+
chatExternalSendResult,
|
|
1355
1369
|
remoteSessionCreate,
|
|
1356
1370
|
remoteProjectSessions,
|
|
1357
1371
|
remoteSessionTranscript,
|