claude-code-session-manager 0.109.0 → 0.111.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-Br4Ispr4.js → AgentLibrary-rBO1daqM.js} +1 -1
- package/dist/assets/{Button-C8Mc6MEP.js → Button-DsEYRZg0.js} +1 -1
- package/dist/assets/{DataModel-Bz0MypYO.js → DataModel-D8sbvvBC.js} +1 -1
- package/dist/assets/{History-DT26Iy38.js → History-BEHLtxia.js} +1 -1
- package/dist/assets/{Hooks-e4xUGEf-.js → Hooks-CKpvlXrg.js} +3 -3
- package/dist/assets/{Library-B3rzEhvN.js → Library-CeF4qh9d.js} +1 -1
- package/dist/assets/{ListDetail-BAvci_aP.js → ListDetail-DcfmxPFk.js} +1 -1
- package/dist/assets/{MarkdownEditor-DBWTGgRz.js → MarkdownEditor-CvK1pyto.js} +1 -1
- package/dist/assets/{McpServers-C-grBN21.js → McpServers-D-h2ihzJ.js} +2 -2
- package/dist/assets/{Memory-BmM0Fz-C.js → Memory-BnF89x8v.js} +1 -1
- package/dist/assets/{Panel-Dc6F8wb4.js → Panel-CQY5zano.js} +1 -1
- package/dist/assets/{Permissions-m_NnldFh.js → Permissions-DLukmmsz.js} +3 -3
- package/dist/assets/{Plugins-BB0TLUC9.js → Plugins-0Pf0OZ4l.js} +2 -2
- package/dist/assets/{ProvenanceBadge-k9PH0S2n.js → ProvenanceBadge-vC7Tj-JP.js} +1 -1
- package/dist/assets/RemoteMachines-CWnKbFHc.js +3 -0
- package/dist/assets/{SaveBar-DSMnbUVI.js → SaveBar-DjijiUb0.js} +1 -1
- package/dist/assets/{Scheduler-CbIcJEfM.js → Scheduler-Cm3EXZvE.js} +7 -7
- package/dist/assets/{ScopeSwitcher-tHtloWbN.js → ScopeSwitcher-B-P-kSfq.js} +1 -1
- package/dist/assets/Settings-DYDlp2m_.js +4 -0
- package/dist/assets/{SkillReferenceGraph-Dndl1tzr.js → SkillReferenceGraph-Epv2GJmj.js} +1 -1
- package/dist/assets/{Skills-CGnxroik.js → Skills-DHO0N0C2.js} +1 -1
- package/dist/assets/{SystemPrompt-B3A3rHNp.js → SystemPrompt-DxeNofL9.js} +1 -1
- package/dist/assets/{TagLibrary-k4CTiAYo.js → TagLibrary-QOAs7ISO.js} +1 -1
- package/dist/assets/{TiptapBody-C2LfNQXw.js → TiptapBody-DAMO3DhO.js} +1 -1
- package/dist/assets/{Toggle-CTXTU1q5.js → Toggle-Ba_f7q_m.js} +1 -1
- package/dist/assets/{ViewTabs-CfEfOSE1.js → ViewTabs-CGX6MAhc.js} +1 -1
- package/dist/assets/{historyTotals-Db0xy2Fg.js → historyTotals-vgfk8EOL.js} +1 -1
- package/dist/assets/index-CPieAFsl.js +3078 -0
- package/dist/assets/{index-C8Smiz7z.css → index-Dutcsly-.css} +1 -1
- package/dist/assets/{settingsSchema-xxO9p4yF.js → settingsSchema-D0FxNrTy.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-DLS1_moj.js → useAgentsDirWatch-DCu6BNYN.js} +1 -1
- package/dist/assets/useRemotePeers-D4cdJDyh.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +3 -1
- package/plugins/session-manager-dev/skills/send-feedback/SKILL.md +48 -15
- package/scripts/scheduler-mcp-server.cjs +29 -2
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +7 -0
- package/src/main/index.cjs +6 -0
- package/src/main/ipcSchemas.cjs +28 -0
- package/src/main/lib/autoUpdater.cjs +190 -0
- package/src/main/lib/builtinMacros.cjs +2 -2
- package/src/main/lib/crossProjectFeedback.cjs +143 -6
- package/src/main/lib/mcpToolCatalog.cjs +12 -3
- package/src/main/lib/reaperHelpers.cjs +13 -1
- package/src/main/lib/relayClientAuth.cjs +133 -0
- package/src/main/lib/remoteClientSessions.cjs +84 -0
- package/src/main/lib/shippedPersonaSeeds.cjs +11 -0
- package/src/main/lib/updaterSettings.cjs +64 -0
- package/src/main/remoteClient.cjs +42 -13
- package/src/main/remoteClientIpc.cjs +33 -1
- package/src/main/scheduler.cjs +16 -0
- package/src/main/updaterIpc.cjs +110 -0
- package/src/main/webRemote.cjs +276 -132
- package/src/preload/api.d.ts +55 -2
- package/src/preload/index.cjs +24 -1
- package/src/seed/agents/demo-video-builder.md +62 -19
- package/dist/assets/Settings-CW3w7knp.js +0 -6
- package/dist/assets/index-fJxuoRvi.js +0 -3077
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.111.0",
|
|
3
|
+
"gitSha": "ca0cbc30d2ee72698993f0f07f878a932843c758",
|
|
4
|
+
"gitShortSha": "ca0cbc30",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-11T04:47:08.011Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -970,6 +970,12 @@ function enqueueExternalPrompt(tabId, prompt) {
|
|
|
970
970
|
broadcast('chat:external-send', { tabId, prompt });
|
|
971
971
|
}
|
|
972
972
|
|
|
973
|
+
// Asks the renderer to start a new Session (same launchSession a macro press
|
|
974
|
+
// uses) on behalf of the paired phone. Payload is pre-validated by the caller.
|
|
975
|
+
function requestExternalNewSession(payload) {
|
|
976
|
+
broadcast('chat:external-new-session', payload);
|
|
977
|
+
}
|
|
978
|
+
|
|
973
979
|
/**
|
|
974
980
|
* Registers the admin-HTTP entry point for enqueueExternalPrompt, mirroring
|
|
975
981
|
* prdCreate.cjs's registerAdminRoute pattern (auth is the injected transport's
|
|
@@ -1050,5 +1056,6 @@ module.exports = {
|
|
|
1050
1056
|
WARN_CEILING_MS,
|
|
1051
1057
|
__setExecutor,
|
|
1052
1058
|
enqueueExternalPrompt,
|
|
1059
|
+
requestExternalNewSession,
|
|
1053
1060
|
registerAdminRoute,
|
|
1054
1061
|
};
|
package/src/main/index.cjs
CHANGED
|
@@ -130,6 +130,7 @@ const filesIpc = require('./files.cjs');
|
|
|
130
130
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
131
131
|
const webRemote = require('./webRemote.cjs');
|
|
132
132
|
const { registerRemoteClientHandlers } = require('./remoteClientIpc.cjs');
|
|
133
|
+
const { registerUpdaterIpc } = require('./updaterIpc.cjs');
|
|
133
134
|
const { listExchanges } = require('./exchanges.cjs');
|
|
134
135
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
135
136
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
@@ -1100,6 +1101,11 @@ filesIpc.registerFilesHandlers();
|
|
|
1100
1101
|
registerDocEditHandlers();
|
|
1101
1102
|
webRemote.registerRemoteHandlers();
|
|
1102
1103
|
registerRemoteClientHandlers(() => mainWindow);
|
|
1104
|
+
try {
|
|
1105
|
+
registerUpdaterIpc({ ipcMain, getWindow: () => mainWindow, ptyManager });
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
logs.writeLine({ scope: 'main', level: 'error', message: 'updater IPC registration failed', meta: { error: e?.message } });
|
|
1108
|
+
}
|
|
1103
1109
|
chatRunner.registerChatHandlers();
|
|
1104
1110
|
|
|
1105
1111
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -944,6 +944,19 @@ const chatExternalSend = z.object({
|
|
|
944
944
|
),
|
|
945
945
|
});
|
|
946
946
|
|
|
947
|
+
// Phone-initiated new Session. cwd/agentName are re-checked against the open
|
|
948
|
+
// tabs / persona list in webRemote.cjs before anything touches the filesystem.
|
|
949
|
+
const remoteSessionCreate = z.object({
|
|
950
|
+
cwd: z.string().min(1).max(4096),
|
|
951
|
+
agentName: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/),
|
|
952
|
+
tag: EpicTagSchema,
|
|
953
|
+
title: z.string().min(1).max(120),
|
|
954
|
+
prompt: z.string().min(1).refine(
|
|
955
|
+
(s) => Buffer.byteLength(s, 'utf8') <= CHAT_PROMPT_MAX_BYTES,
|
|
956
|
+
`prompt must be ≤ ${CHAT_PROMPT_MAX_BYTES} bytes`,
|
|
957
|
+
),
|
|
958
|
+
});
|
|
959
|
+
|
|
947
960
|
// ──────────────────────────────────────────── Web Remote
|
|
948
961
|
// OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
|
|
949
962
|
const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
|
|
@@ -957,6 +970,11 @@ const webRemoteRevokeDevice = z.object({
|
|
|
957
970
|
deviceId: z.string().regex(DEVICE_ID_RE),
|
|
958
971
|
}).strict();
|
|
959
972
|
|
|
973
|
+
// Optional payload: omit (undefined) to confirm the single pending client.
|
|
974
|
+
const webRemoteConfirmSas = z.object({
|
|
975
|
+
clientKey: z.string().max(64).optional(),
|
|
976
|
+
}).strict().optional();
|
|
977
|
+
|
|
960
978
|
const webRemoteAuditTail = z.object({
|
|
961
979
|
lines: z.number().int().min(1).max(500).optional(),
|
|
962
980
|
}).strict();
|
|
@@ -1137,9 +1155,11 @@ function validated(schema, handler) {
|
|
|
1137
1155
|
//
|
|
1138
1156
|
// Ungated READ_COMMANDS (justify each):
|
|
1139
1157
|
// cmd:app:version — exposes only the app semver string; no user data.
|
|
1158
|
+
// cmd:app:info — exposes only app semver + OS platform string; no user data.
|
|
1140
1159
|
// cmd:session:unsubscribe — teardown lifecycle; returns nothing sensitive.
|
|
1141
1160
|
const READ_COMMANDS = new Set([
|
|
1142
1161
|
'cmd:app:version',
|
|
1162
|
+
'cmd:app:info',
|
|
1143
1163
|
// v2 mobile: unsubscribe is a teardown lifecycle call with no data payload.
|
|
1144
1164
|
'cmd:session:unsubscribe',
|
|
1145
1165
|
]);
|
|
@@ -1147,9 +1167,13 @@ const READ_COMMANDS = new Set([
|
|
|
1147
1167
|
// Sensitive reads — return user data; require SAS confirmation same as MUTATE.
|
|
1148
1168
|
const SAS_GATED_READS = new Set([
|
|
1149
1169
|
'cmd:sessions:load',
|
|
1170
|
+
// Same data as the event:session:list push (projects + their active Epics).
|
|
1171
|
+
'cmd:sessions:list',
|
|
1150
1172
|
'cmd:history:aggregate',
|
|
1151
1173
|
// subscribe initiates a live stream of session state/summary — sensitive.
|
|
1152
1174
|
'cmd:session:subscribe',
|
|
1175
|
+
// Picker data (projects, personas, tags, macros) for the phone's New Session.
|
|
1176
|
+
'cmd:session:options',
|
|
1153
1177
|
// NOTE: cmd:exchanges:list is intentionally NOT allowlisted — webRemote.cjs has
|
|
1154
1178
|
// no dispatch handler for it, so an allowlist entry would only fail closed with
|
|
1155
1179
|
// an opaque reject. Re-add here together with the handler when remote exchanges
|
|
@@ -1175,6 +1199,8 @@ const MUTATE_COMMANDS = new Set([
|
|
|
1175
1199
|
// Pushes a prompt into an open tab's chat queue — a real mutation of that
|
|
1176
1200
|
// tab's session, same tier as pty:write.
|
|
1177
1201
|
'cmd:chat:send',
|
|
1202
|
+
// Starts a new Session on the desktop via the renderer's launchSession.
|
|
1203
|
+
'cmd:session:create',
|
|
1178
1204
|
]);
|
|
1179
1205
|
|
|
1180
1206
|
const ALLOWED_COMMANDS = new Set([...READ_COMMANDS, ...SAS_GATED_READS, ...MUTATE_COMMANDS]);
|
|
@@ -1193,6 +1219,7 @@ module.exports = {
|
|
|
1193
1219
|
webRemotePair,
|
|
1194
1220
|
webRemoteRevokeDevice,
|
|
1195
1221
|
webRemoteAuditTail,
|
|
1222
|
+
webRemoteConfirmSas,
|
|
1196
1223
|
sessionSubscribe,
|
|
1197
1224
|
ptySpawn,
|
|
1198
1225
|
ptyTabId,
|
|
@@ -1284,6 +1311,7 @@ module.exports = {
|
|
|
1284
1311
|
chatCancel,
|
|
1285
1312
|
chatClassifyTicket,
|
|
1286
1313
|
chatExternalSend,
|
|
1314
|
+
remoteSessionCreate,
|
|
1287
1315
|
exchangesList,
|
|
1288
1316
|
agentsSavePersona,
|
|
1289
1317
|
macrosSave,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* autoUpdater.cjs — pure, dependency-injected state machine around electron-updater's autoUpdater.
|
|
5
|
+
*
|
|
6
|
+
* Requires neither electron nor electron-updater: the caller injects the updater instance. The feed is
|
|
7
|
+
* whatever app-update.yml electron-builder baked into the installer; this module never supplies a feed URL.
|
|
8
|
+
* Checks on start() and every 4h, downloads in the background, and installs ONLY after requestInstall() —
|
|
9
|
+
* deferring (polling isBusy every 60s) while scheduler jobs or terminals are busy.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
13
|
+
const BUSY_POLL_MS = 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/** Unsigned mac builds cannot use Squirrel.Mac, so darwin is always unsupported. */
|
|
16
|
+
function isAutoUpdateSupported({ packaged, platform, env }) {
|
|
17
|
+
const e = env || {};
|
|
18
|
+
if (packaged !== true) return false;
|
|
19
|
+
if (e.SM_AUTO_UPDATE === '0' || e.SM_E2E === '1') return false;
|
|
20
|
+
if (platform === 'win32') return true;
|
|
21
|
+
return platform === 'linux' && typeof e.APPIMAGE === 'string' && e.APPIMAGE.length > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createAutoUpdater({
|
|
25
|
+
updater, supported, currentVersion, isBusy, getEnabled,
|
|
26
|
+
setInterval: setIntervalFn = setInterval, clearInterval: clearIntervalFn = clearInterval,
|
|
27
|
+
now = Date.now, log = () => {},
|
|
28
|
+
}) {
|
|
29
|
+
let enabledOverride = null;
|
|
30
|
+
let started = false;
|
|
31
|
+
let listening = false;
|
|
32
|
+
let checkTimer = null;
|
|
33
|
+
let pollTimer = null;
|
|
34
|
+
const subscribers = new Set();
|
|
35
|
+
|
|
36
|
+
const isEnabled = () => {
|
|
37
|
+
if (enabledOverride !== null) return enabledOverride;
|
|
38
|
+
return typeof getEnabled === 'function' ? getEnabled() !== false : true;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let snapshot = {
|
|
42
|
+
state: supported ? 'idle' : 'unsupported',
|
|
43
|
+
current: currentVersion,
|
|
44
|
+
latest: null,
|
|
45
|
+
progress: null,
|
|
46
|
+
error: null,
|
|
47
|
+
busyReason: null,
|
|
48
|
+
lastCheckedAt: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function setState(patch) {
|
|
52
|
+
const next = { ...snapshot, ...patch };
|
|
53
|
+
if (Object.keys(next).every((k) => next[k] === snapshot[k])) return;
|
|
54
|
+
snapshot = next;
|
|
55
|
+
for (const fn of [...subscribers]) {
|
|
56
|
+
try { fn(snapshot); } catch (e) { log('autoUpdater subscriber threw', e); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const holding = () => snapshot.state === 'ready' || snapshot.state === 'deferred';
|
|
61
|
+
|
|
62
|
+
function fail(e) {
|
|
63
|
+
const message = e && e.message ? e.message : String(e);
|
|
64
|
+
log('autoUpdater error', message);
|
|
65
|
+
setState({ state: 'error', error: message, progress: null });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clearPoll() {
|
|
69
|
+
if (pollTimer !== null) { clearIntervalFn(pollTimer); pollTimer = null; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearChecks() {
|
|
73
|
+
if (checkTimer !== null) { clearIntervalFn(checkTimer); checkTimer = null; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function attach() {
|
|
77
|
+
if (listening) return;
|
|
78
|
+
listening = true;
|
|
79
|
+
updater.autoDownload = true;
|
|
80
|
+
updater.autoInstallOnAppQuit = true;
|
|
81
|
+
updater.allowPrerelease = false;
|
|
82
|
+
updater.allowDowngrade = false;
|
|
83
|
+
updater.on('checking-for-update', () => {
|
|
84
|
+
if (!holding()) setState({ state: 'checking', error: null });
|
|
85
|
+
});
|
|
86
|
+
updater.on('update-available', (info) => {
|
|
87
|
+
if (!holding()) setState({ state: 'downloading', latest: info && info.version ? info.version : null, progress: null, error: null });
|
|
88
|
+
});
|
|
89
|
+
updater.on('download-progress', (p) => {
|
|
90
|
+
if (!holding()) setState({ state: 'downloading', progress: p && typeof p.percent === 'number' ? p.percent : null });
|
|
91
|
+
});
|
|
92
|
+
updater.on('update-downloaded', (info) => {
|
|
93
|
+
setState({ state: 'ready', latest: info && info.version ? info.version : snapshot.latest, progress: 100, error: null, busyReason: null });
|
|
94
|
+
});
|
|
95
|
+
updater.on('update-not-available', () => {
|
|
96
|
+
if (!holding()) setState({ state: 'idle', latest: null, progress: null, error: null });
|
|
97
|
+
});
|
|
98
|
+
updater.on('error', (e) => {
|
|
99
|
+
if (!holding()) fail(e);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function checkNow() {
|
|
104
|
+
if (!supported || !isEnabled()) return;
|
|
105
|
+
if (holding() || snapshot.state === 'checking' || snapshot.state === 'downloading') return;
|
|
106
|
+
attach();
|
|
107
|
+
setState({ state: 'checking', error: null, lastCheckedAt: now() });
|
|
108
|
+
try {
|
|
109
|
+
await updater.checkForUpdates();
|
|
110
|
+
} catch (e) {
|
|
111
|
+
fail(e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function startChecks() {
|
|
116
|
+
clearChecks();
|
|
117
|
+
checkTimer = setIntervalFn(() => { checkNow(); }, CHECK_INTERVAL_MS);
|
|
118
|
+
if (checkTimer && typeof checkTimer.unref === 'function') checkTimer.unref();
|
|
119
|
+
checkNow();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function applyEnabled() {
|
|
123
|
+
if (!supported) return;
|
|
124
|
+
if (!isEnabled()) {
|
|
125
|
+
clearChecks();
|
|
126
|
+
clearPoll();
|
|
127
|
+
setState({ state: 'disabled', busyReason: null, progress: null, error: null });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (snapshot.state === 'disabled') setState({ state: 'idle' });
|
|
131
|
+
if (started) startChecks();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function start() {
|
|
135
|
+
if (!supported || started) return;
|
|
136
|
+
started = true;
|
|
137
|
+
applyEnabled();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stop() {
|
|
141
|
+
started = false;
|
|
142
|
+
clearChecks();
|
|
143
|
+
clearPoll();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function setEnabled(value) {
|
|
147
|
+
enabledOverride = !!value;
|
|
148
|
+
applyEnabled();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function install() {
|
|
152
|
+
clearPoll();
|
|
153
|
+
try {
|
|
154
|
+
updater.quitAndInstall(false, true);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
fail(e);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function requestInstall({ force } = {}) {
|
|
161
|
+
if (!supported || !holding()) return;
|
|
162
|
+
if (snapshot.state === 'deferred' && !force) return;
|
|
163
|
+
if (force) { install(); return; }
|
|
164
|
+
const b = isBusy ? isBusy() : { busy: false };
|
|
165
|
+
if (!b || !b.busy) { install(); return; }
|
|
166
|
+
setState({ state: 'deferred', busyReason: b.reason || null });
|
|
167
|
+
clearPoll();
|
|
168
|
+
pollTimer = setIntervalFn(() => {
|
|
169
|
+
const cur = isBusy ? isBusy() : { busy: false };
|
|
170
|
+
if (cur && cur.busy) { setState({ busyReason: cur.reason || null }); return; }
|
|
171
|
+
install();
|
|
172
|
+
}, BUSY_POLL_MS);
|
|
173
|
+
if (pollTimer && typeof pollTimer.unref === 'function') pollTimer.unref();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cancelDeferred() {
|
|
177
|
+
if (snapshot.state !== 'deferred') return;
|
|
178
|
+
clearPoll();
|
|
179
|
+
setState({ state: 'ready', busyReason: null });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function subscribe(fn) {
|
|
183
|
+
subscribers.add(fn);
|
|
184
|
+
return () => { subscribers.delete(fn); };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { start, stop, checkNow, requestInstall, cancelDeferred, setEnabled, status: () => snapshot, subscribe };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { isAutoUpdateSupported, createAutoUpdater, CHECK_INTERVAL_MS, BUSY_POLL_MS };
|
|
@@ -30,9 +30,9 @@ const BUILTIN_MACROS = Object.freeze([
|
|
|
30
30
|
tag: 'project-home-builder',
|
|
31
31
|
surface: 'project-home',
|
|
32
32
|
projects: ['*'],
|
|
33
|
-
builtinVersion:
|
|
33
|
+
builtinVersion: 4,
|
|
34
34
|
prompt:
|
|
35
|
-
|
|
35
|
+
"Generate a character and cute animated JavaScript video: a 30-second demo of this project as one self-contained HTML/JavaScript animation. Ground everything in what you read in the repository — invent an original mascot character whose look, name and personality match the project's theme and purpose, and tell the project's real goals and key features through it. The character, story and visuals must match the project's theme and purpose as read from the repository. Write the narration script first (hook, problem, solution, three features, call to action), then build every scene to match its line, with fun dynamic elements per the persona. Use the style named on a 'Video style: <name>' line in the additional instructions — Professional, Fun, Animated whiteboard, or Kinetic typography — otherwise pick the one that best fits the project. Narrate with Kokoro (kokoro-onnx) using a natural female (af_heart) or male (am_michael) voice per the audio policy in the persona. Save it with a single project_demo_video_write call passing htmlPath.",
|
|
36
36
|
}),
|
|
37
37
|
]);
|
|
38
38
|
|
|
@@ -83,6 +83,14 @@ const DEFAULT_FEEDBACK_AGENT = 'architect';
|
|
|
83
83
|
const MAX_TITLE_CHARS = 200;
|
|
84
84
|
const MAX_BODY_CHARS = 20000;
|
|
85
85
|
const MAX_REFERENCES = 20;
|
|
86
|
+
const MAX_MOTIVATION_CHARS = 2000;
|
|
87
|
+
const MAX_LIST_ITEMS = 20;
|
|
88
|
+
const MAX_LIST_ITEM_CHARS = 500;
|
|
89
|
+
const MAX_SERIES_KEY_CHARS = 60;
|
|
90
|
+
const MAX_SERIES_TITLE_CHARS = 120;
|
|
91
|
+
const MAX_SERIES_TOTAL = 50;
|
|
92
|
+
const MAX_ORIGIN_ASK_CHARS = 300;
|
|
93
|
+
const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
86
94
|
|
|
87
95
|
/** Bound on how many `~/.claude/projects/` folders listFeedbackTargets will
|
|
88
96
|
* resolve. That directory accumulates one folder per path the CLI was ever
|
|
@@ -127,6 +135,29 @@ function isSessionManagerProject(cwd) {
|
|
|
127
135
|
return isDirectory(path.join(cwd, OPS_ROOT_DIR));
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/**
|
|
139
|
+
* resolveOriginAsk(fromCwd, fromEpicId) → string | null
|
|
140
|
+
*
|
|
141
|
+
* The sending Epic's own human goal (single line, ≤ 300 chars), read from the
|
|
142
|
+
* sender project's active-index.json. Never throws: a missing Epic or an
|
|
143
|
+
* unreadable file is just "no origin known".
|
|
144
|
+
*/
|
|
145
|
+
function resolveOriginAsk(fromCwd, fromEpicId) {
|
|
146
|
+
if (!fromCwd || !fromEpicId) return null;
|
|
147
|
+
try {
|
|
148
|
+
const sessions = readActiveIndex(fromCwd).sessions;
|
|
149
|
+
if (!Object.prototype.hasOwnProperty.call(sessions, fromEpicId)) return null;
|
|
150
|
+
const goal = singleLine(sessions[fromEpicId] && sessions[fromEpicId].goalText);
|
|
151
|
+
return goal ? goal.slice(0, MAX_ORIGIN_ASK_CHARS) : null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function seriesLine(series) {
|
|
158
|
+
return series ? `Part ${series.index} of ${series.total} — ${singleLine(series.title)}` : '';
|
|
159
|
+
}
|
|
160
|
+
|
|
130
161
|
/**
|
|
131
162
|
* composeFeedbackIntake({ fromCwd, fromEpicId, title, body, referencePaths })
|
|
132
163
|
* → { goalText, openingPrompt, sections }
|
|
@@ -160,6 +191,12 @@ function composeFeedbackIntake({
|
|
|
160
191
|
agentName = DEFAULT_FEEDBACK_AGENT,
|
|
161
192
|
agentDescription = null,
|
|
162
193
|
referencePaths = [],
|
|
194
|
+
originAsk = null,
|
|
195
|
+
motivation = '',
|
|
196
|
+
plan = [],
|
|
197
|
+
acceptanceCriteria = [],
|
|
198
|
+
outOfScope = [],
|
|
199
|
+
series = null,
|
|
163
200
|
} = {}) {
|
|
164
201
|
const trimmedTitle = singleLine(title);
|
|
165
202
|
const trimmedBody = String(body ?? '').trim();
|
|
@@ -174,21 +211,36 @@ function composeFeedbackIntake({
|
|
|
174
211
|
});
|
|
175
212
|
}
|
|
176
213
|
|
|
214
|
+
const fromPath = singleLine(fromCwd);
|
|
215
|
+
const fromName = path.basename(fromPath) || fromPath;
|
|
177
216
|
const origin = fromEpicId
|
|
178
|
-
? `${
|
|
179
|
-
:
|
|
217
|
+
? `${fromName} (${fromPath}, session ${singleLine(fromEpicId)})`
|
|
218
|
+
: `${fromName} (${fromPath})`;
|
|
219
|
+
const askClause = originAsk ? ` It was raised while working on: ${singleLine(originAsk)}.` : '';
|
|
180
220
|
sections.push({
|
|
181
221
|
kind: 'input',
|
|
182
222
|
label: 'Input',
|
|
183
223
|
text:
|
|
184
|
-
`This session is INBOUND FEEDBACK from another project: ${origin}
|
|
224
|
+
`This session is INBOUND FEEDBACK from another project: ${origin}.${askClause} `
|
|
185
225
|
+ 'Nobody in this project wrote the report below — an agent working in that project did, and it '
|
|
186
226
|
+ 'may be wrong about this codebase. Verify the claim against the code here before acting on it, '
|
|
187
227
|
+ 'and if it does not hold, say so and close the session rather than building against it.',
|
|
188
228
|
source: fromCwd,
|
|
189
229
|
});
|
|
190
230
|
|
|
191
|
-
const
|
|
231
|
+
const trimmedMotivation = String(motivation ?? '').trim();
|
|
232
|
+
const blocks = [];
|
|
233
|
+
if (series) blocks.push(seriesLine(series));
|
|
234
|
+
if (trimmedMotivation) blocks.push(`## Why\n${trimmedMotivation}`);
|
|
235
|
+
if (plan.length) blocks.push(`## Proposed plan\n${plan.map((x, i) => `${i + 1}. ${singleLine(x)}`).join('\n')}`);
|
|
236
|
+
if (acceptanceCriteria.length) {
|
|
237
|
+
blocks.push(`## Acceptance criteria\n${acceptanceCriteria.map((x) => `- [ ] ${singleLine(x)}`).join('\n')}`);
|
|
238
|
+
}
|
|
239
|
+
if (outOfScope.length) blocks.push(`## Out of scope\n${outOfScope.map((x) => `- ${singleLine(x)}`).join('\n')}`);
|
|
240
|
+
blocks.push(`## Details\n${trimmedBody}`);
|
|
241
|
+
const structured = blocks.join('\n\n');
|
|
242
|
+
|
|
243
|
+
const goalBody = trimmedTitle ? `Goal: ${trimmedTitle}\n\n${structured}` : structured;
|
|
192
244
|
sections.push({ kind: 'goal', label: 'Goal', text: goalBody });
|
|
193
245
|
|
|
194
246
|
for (const p of referencePaths) {
|
|
@@ -203,7 +255,7 @@ function composeFeedbackIntake({
|
|
|
203
255
|
return out + sep + s.text;
|
|
204
256
|
}, '');
|
|
205
257
|
|
|
206
|
-
const bodyText = trimmedTitle ? `${trimmedTitle}\n\n${
|
|
258
|
+
const bodyText = trimmedTitle ? `${trimmedTitle}\n\n${structured}` : structured;
|
|
207
259
|
const refLines = referencePaths.map((p) => `Reference: ${singleLine(p)}`);
|
|
208
260
|
const goalText = refLines.length ? `${bodyText}\n\n${refLines.join('\n')}` : bodyText;
|
|
209
261
|
|
|
@@ -237,6 +289,66 @@ function validateFeedbackInput(input = {}) {
|
|
|
237
289
|
return bad(400, `tag must be one of ${FEEDBACK_TAGS.join(' | ')} (got ${JSON.stringify(tag)})`);
|
|
238
290
|
}
|
|
239
291
|
|
|
292
|
+
const motivation = typeof input.motivation === 'string' ? input.motivation.trim() : '';
|
|
293
|
+
if (!motivation) {
|
|
294
|
+
return bad(400, 'motivation is required — WHY this is needed, in the sending human\'s terms, so the receiving human can judge it');
|
|
295
|
+
}
|
|
296
|
+
if (motivation.length > MAX_MOTIVATION_CHARS) {
|
|
297
|
+
return bad(400, `motivation is ${motivation.length} chars; cap is ${MAX_MOTIVATION_CHARS}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const listPurpose = {
|
|
301
|
+
plan: 'the ordered steps you propose the receiving project take',
|
|
302
|
+
acceptanceCriteria: 'the checkable conditions that mean this is done',
|
|
303
|
+
outOfScope: 'what this proposal deliberately does not cover',
|
|
304
|
+
};
|
|
305
|
+
const lists = {};
|
|
306
|
+
for (const field of ['plan', 'acceptanceCriteria', 'outOfScope']) {
|
|
307
|
+
const raw = input[field];
|
|
308
|
+
if (raw === undefined || raw === null) { lists[field] = []; continue; }
|
|
309
|
+
if (!Array.isArray(raw)) return bad(400, `${field} must be an array of strings (${listPurpose[field]})`);
|
|
310
|
+
if (raw.length > MAX_LIST_ITEMS) {
|
|
311
|
+
return bad(400, `${field} has ${raw.length} items; cap is ${MAX_LIST_ITEMS} (${listPurpose[field]})`);
|
|
312
|
+
}
|
|
313
|
+
const items = [];
|
|
314
|
+
for (const item of raw) {
|
|
315
|
+
const t = typeof item === 'string' ? item.trim() : '';
|
|
316
|
+
if (!t) return bad(400, `${field} items must be non-blank strings (${listPurpose[field]})`);
|
|
317
|
+
if (t.length > MAX_LIST_ITEM_CHARS) {
|
|
318
|
+
return bad(400, `a ${field} item is ${t.length} chars; cap is ${MAX_LIST_ITEM_CHARS}`);
|
|
319
|
+
}
|
|
320
|
+
items.push(t);
|
|
321
|
+
}
|
|
322
|
+
lists[field] = items;
|
|
323
|
+
}
|
|
324
|
+
if (lists.acceptanceCriteria.length === 0) {
|
|
325
|
+
return bad(400, `acceptanceCriteria is required with at least one item — ${listPurpose.acceptanceCriteria}`);
|
|
326
|
+
}
|
|
327
|
+
if (tag === 'feature' && lists.plan.length === 0) {
|
|
328
|
+
return bad(400, `plan is required with at least one item when tag is 'feature' — ${listPurpose.plan}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let series = null;
|
|
332
|
+
if (input.series !== undefined && input.series !== null) {
|
|
333
|
+
const sr = input.series;
|
|
334
|
+
const sbad = (why) => bad(400, `series ${why} (series groups one request split across several proposals: { key, title, index, total })`);
|
|
335
|
+
if (typeof sr !== 'object' || Array.isArray(sr)) return sbad('must be an object');
|
|
336
|
+
const key = typeof sr.key === 'string' ? sr.key : '';
|
|
337
|
+
if (!key || key.length > MAX_SERIES_KEY_CHARS || !KEBAB_RE.test(key)) {
|
|
338
|
+
return sbad(`key must be kebab-case, ≤ ${MAX_SERIES_KEY_CHARS} chars`);
|
|
339
|
+
}
|
|
340
|
+
const stitle = typeof sr.title === 'string' ? sr.title.trim() : '';
|
|
341
|
+
if (!stitle || stitle.length > MAX_SERIES_TITLE_CHARS) {
|
|
342
|
+
return sbad(`title must be non-blank, ≤ ${MAX_SERIES_TITLE_CHARS} chars`);
|
|
343
|
+
}
|
|
344
|
+
if (!Number.isInteger(sr.index) || sr.index < 1) return sbad('index must be an integer ≥ 1');
|
|
345
|
+
if (!Number.isInteger(sr.total) || sr.total < 2 || sr.total > MAX_SERIES_TOTAL) {
|
|
346
|
+
return sbad(`total must be an integer 2..${MAX_SERIES_TOTAL}`);
|
|
347
|
+
}
|
|
348
|
+
if (sr.index > sr.total) return sbad('index must be ≤ total');
|
|
349
|
+
series = { key, title: stitle, index: sr.index, total: sr.total };
|
|
350
|
+
}
|
|
351
|
+
|
|
240
352
|
const referencePaths = Array.isArray(input.referencePaths) ? input.referencePaths : [];
|
|
241
353
|
if (referencePaths.length > MAX_REFERENCES) {
|
|
242
354
|
return bad(400, `too many referencePaths (${referencePaths.length}); cap is ${MAX_REFERENCES}`);
|
|
@@ -293,6 +405,11 @@ function validateFeedbackInput(input = {}) {
|
|
|
293
405
|
title,
|
|
294
406
|
body,
|
|
295
407
|
tag,
|
|
408
|
+
motivation,
|
|
409
|
+
plan: lists.plan,
|
|
410
|
+
acceptanceCriteria: lists.acceptanceCriteria,
|
|
411
|
+
outOfScope: lists.outOfScope,
|
|
412
|
+
series,
|
|
296
413
|
referencePaths: referencePaths.map((p) => String(p)),
|
|
297
414
|
fromEpicId: input.fromEpicId ? String(input.fromEpicId) : null,
|
|
298
415
|
agentType: input.agentType ? String(input.agentType) : DEFAULT_FEEDBACK_AGENT,
|
|
@@ -337,6 +454,8 @@ async function openFeedbackSession(input, deps = {}) {
|
|
|
337
454
|
} catch { /* unresolvable — the proposal still delivers, just without a receipt */ }
|
|
338
455
|
}
|
|
339
456
|
|
|
457
|
+
const originAsk = resolveOriginAsk(v.fromCwd, v.fromEpicId);
|
|
458
|
+
|
|
340
459
|
const { goalText, openingPrompt, sections } = composeFeedbackIntake({
|
|
341
460
|
fromCwd: v.fromCwd,
|
|
342
461
|
fromEpicId: v.fromEpicId,
|
|
@@ -345,6 +464,12 @@ async function openFeedbackSession(input, deps = {}) {
|
|
|
345
464
|
agentName: v.agentType,
|
|
346
465
|
agentDescription: v.agentDescription,
|
|
347
466
|
referencePaths: v.referencePaths,
|
|
467
|
+
originAsk,
|
|
468
|
+
motivation: v.motivation,
|
|
469
|
+
plan: v.plan,
|
|
470
|
+
acceptanceCriteria: v.acceptanceCriteria,
|
|
471
|
+
outOfScope: v.outOfScope,
|
|
472
|
+
series: v.series,
|
|
348
473
|
});
|
|
349
474
|
|
|
350
475
|
// The Epic's stored identity is the one-line title, not the whole report —
|
|
@@ -363,6 +488,14 @@ async function openFeedbackSession(input, deps = {}) {
|
|
|
363
488
|
producer: 'cross-project-feedback',
|
|
364
489
|
fromCwd: v.fromCwd,
|
|
365
490
|
...(v.fromEpicId ? { fromEpicId: v.fromEpicId } : {}),
|
|
491
|
+
proposal: {
|
|
492
|
+
motivation: v.motivation,
|
|
493
|
+
originAsk,
|
|
494
|
+
plan: v.plan,
|
|
495
|
+
acceptanceCriteria: v.acceptanceCriteria,
|
|
496
|
+
outOfScope: v.outOfScope,
|
|
497
|
+
series: v.series,
|
|
498
|
+
},
|
|
366
499
|
},
|
|
367
500
|
mintAuthority: MINT_AUTHORITY_CROSS_PROJECT_FEEDBACK,
|
|
368
501
|
});
|
|
@@ -377,6 +510,7 @@ async function openFeedbackSession(input, deps = {}) {
|
|
|
377
510
|
fromEpicId: v.fromEpicId,
|
|
378
511
|
tag: v.tag,
|
|
379
512
|
title: v.title,
|
|
513
|
+
series: v.series,
|
|
380
514
|
});
|
|
381
515
|
|
|
382
516
|
let receiptOnOriginEpic = false;
|
|
@@ -385,7 +519,9 @@ async function openFeedbackSession(input, deps = {}) {
|
|
|
385
519
|
receiptOnOriginEpic = await appendReceipt(
|
|
386
520
|
v.fromCwd,
|
|
387
521
|
v.fromEpicId,
|
|
388
|
-
`Feedback delivered to ${v.toCwd} as a proposed session (${minted.epicId}): "${v.title}"
|
|
522
|
+
`Feedback delivered to ${v.toCwd} as a proposed session (${minted.epicId}): "${v.title}"`
|
|
523
|
+
+ (v.series ? ` [${seriesLine(v.series)}]` : '')
|
|
524
|
+
+ '. '
|
|
389
525
|
+ 'It will not run until a human in that project approves it.',
|
|
390
526
|
);
|
|
391
527
|
} catch {
|
|
@@ -545,6 +681,7 @@ module.exports = {
|
|
|
545
681
|
resolveProjectsDir,
|
|
546
682
|
isSessionManagerProject,
|
|
547
683
|
composeFeedbackIntake,
|
|
684
|
+
resolveOriginAsk,
|
|
548
685
|
validateFeedbackInput,
|
|
549
686
|
openFeedbackSession,
|
|
550
687
|
listFeedbackTargets,
|
|
@@ -238,15 +238,24 @@ const MCP_TOOL_CATALOG = [
|
|
|
238
238
|
+ 'opening prompt and stamped with where it came from. Session Manager performs the cross-folder '
|
|
239
239
|
+ 'write; you never write another project\'s files yourself.',
|
|
240
240
|
whenToUse: 'Use when work in this project surfaces something that is genuinely another project\'s to fix. '
|
|
241
|
-
+ 'Write
|
|
242
|
-
+ '
|
|
243
|
-
+ '
|
|
241
|
+
+ 'Write for a human who must approve it and has never seen your project. Lead with WHY: `motivation` '
|
|
242
|
+
+ 'in the originating human\'s terms. Then give the full `plan` (required for tag "feature") and '
|
|
243
|
+
+ '`acceptanceCriteria`; put evidence, symptom and references in `body`. Name the file in THEIR repo '
|
|
244
|
+
+ 'that looks responsible if you know it. When one request is split into several proposals, send '
|
|
245
|
+
+ 'them ALL with one shared `series` {key, title, total} (plus each one\'s `index`) and summarise '
|
|
246
|
+
+ 'the overall plan in each motivation.',
|
|
244
247
|
whenNotToUse: 'WHEN NOT TO USE IT: for work in the project you are ALREADY in, run /develop inside the Epic you '
|
|
245
248
|
+ 'are already in — this tool refuses toCwd === fromCwd outright.',
|
|
246
249
|
exampleArgs: {
|
|
247
250
|
toCwd: '/home/bilko/Projects/other-project',
|
|
248
251
|
fromCwd: '/home/bilko/Projects/session-manager',
|
|
249
252
|
title: 'Cross-project contract mismatch in the shared admin API',
|
|
253
|
+
tag: 'feature',
|
|
254
|
+
motivation: 'The human wants X to work across both projects; today Y breaks because Z, so the shared admin API needs W.',
|
|
255
|
+
plan: ['Add field F to the admin API response', 'Update the validator to accept F', 'Cover F with a contract test'],
|
|
256
|
+
acceptanceCriteria: ['Admin API returns F', 'Contract test for F passes'],
|
|
257
|
+
outOfScope: ['Migrating existing records'],
|
|
258
|
+
series: { key: 'shared-admin-api-f', title: 'Shared admin API field F', index: 1, total: 2 },
|
|
250
259
|
body: 'Symptom: X. Observed while doing Y in session-manager. Expected: Z. Suspected file: <path in their repo>.',
|
|
251
260
|
},
|
|
252
261
|
notes: 'WHAT THIS DOES NOT DO: it does not start anything, queue a PRD, or spend a token. The session '
|
|
@@ -339,6 +339,14 @@ const ORPHAN_REQUEUE_CAP = 5;
|
|
|
339
339
|
* diverted into `recovered` exactly like a `findLiveProcess` hit.
|
|
340
340
|
* Both default to a no-op when omitted, so existing callers/tests are
|
|
341
341
|
* unaffected.
|
|
342
|
+
*
|
|
343
|
+
* `finalizeOwned` (optional, `(job) → boolean`) covers the dead-PID shape:
|
|
344
|
+
* a claude child exits BEFORE its spawnJob() has integrated the worktree
|
|
345
|
+
* branch and written the terminal row, so for that whole window the row is
|
|
346
|
+
* 'running' with a dead pid. Reaping it then races the fast-forward merge and
|
|
347
|
+
* parks landed work as reaped_without_integration (family-frame 137,
|
|
348
|
+
* 2026-10-10: park + merge in the same second). A true return skips the row
|
|
349
|
+
* with a warning; the caller bounds how long it may keep answering true.
|
|
342
350
|
*/
|
|
343
351
|
/**
|
|
344
352
|
* Pure, never-throws formatter for the dispatch-phase breadcrumb appended to
|
|
@@ -400,7 +408,7 @@ function resolvePidlessFailureOverride(job) {
|
|
|
400
408
|
}
|
|
401
409
|
|
|
402
410
|
function selectReapableJobs(jobs, now, {
|
|
403
|
-
pidAlive, grace, findLiveProcess, getLogMtimeMs, getLogPid, logFreshWindowMs,
|
|
411
|
+
pidAlive, grace, findLiveProcess, getLogMtimeMs, getLogPid, logFreshWindowMs, finalizeOwned,
|
|
404
412
|
} = {}) {
|
|
405
413
|
const reapable = [];
|
|
406
414
|
const warnings = [];
|
|
@@ -410,6 +418,10 @@ function selectReapableJobs(jobs, now, {
|
|
|
410
418
|
const pid = j.runtime?.pid;
|
|
411
419
|
if (pid) {
|
|
412
420
|
if (pidAlive(pid)) continue;
|
|
421
|
+
if (typeof finalizeOwned === 'function' && finalizeOwned(j)) {
|
|
422
|
+
warnings.push({ slug: j.slug, reason: `pid ${pid} exited but its spawnJob is still finalizing (integration in flight)` });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
413
425
|
reapable.push({ slug: j.slug, pid, pidless: false });
|
|
414
426
|
continue;
|
|
415
427
|
}
|