claude-code-session-manager 0.40.3 → 0.41.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/{TiptapBody-DweEtHks.js → TiptapBody-BnRle0iw.js} +1 -1
- package/dist/assets/{index-BxVBtmjA.css → index-CKY3mHgV.css} +1 -1
- package/dist/assets/{index-QRU8uTeq.js → index-H7rwsoKV.js} +631 -624
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -5
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +2 -2
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +20 -34
- package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +68 -0
- package/scripts/lib/watchdogHelpers.cjs +5 -5
- package/scripts/mint-epic.cjs +28 -0
- package/scripts/propose-epic.cjs +55 -0
- package/src/main/__tests__/epicMint.test.cjs +85 -0
- package/src/main/__tests__/prdCreate.test.cjs +54 -2
- package/src/main/__tests__/promptSessionEvents.test.cjs +56 -2
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +43 -211
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +63 -0
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +86 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +21 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +78 -0
- package/src/main/browserView.cjs +1 -1
- package/src/main/chatRunner.cjs +2 -46
- package/src/main/config.cjs +12 -6
- package/src/main/docEdit.cjs +3 -4
- package/src/main/index.cjs +12 -0
- package/src/main/ipcSchemas.cjs +51 -11
- package/src/main/lib/__tests__/opsOwnership.test.cjs +92 -0
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +74 -0
- package/src/main/lib/epicMint.cjs +133 -59
- package/src/main/lib/opsOwnership.cjs +166 -0
- package/src/main/lib/prdCreate.cjs +9 -1
- package/src/main/lib/prdLocations.cjs +21 -0
- package/src/main/lib/projectBriefCore.cjs +70 -4
- package/src/main/lib/queueStore.cjs +3 -0
- package/src/main/lib/rcaFeedbackHook.cjs +41 -55
- package/src/main/projectBrief.cjs +36 -2
- package/src/main/promptSessionEvents.cjs +24 -2
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/queueOps.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +8 -0
- package/src/main/scheduler.cjs +158 -62
- package/src/main/templates/PRD_AUTHORING.md +1 -1
- package/src/preload/api.d.ts +76 -3
- package/src/preload/index.cjs +21 -3
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +0 -140
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +0 -290
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +0 -265
|
@@ -28,6 +28,7 @@ const fs = require('node:fs');
|
|
|
28
28
|
const os = require('node:os');
|
|
29
29
|
const path = require('node:path');
|
|
30
30
|
const config = require('../config.cjs');
|
|
31
|
+
const { ensureEpic } = require('./epicMint.cjs');
|
|
31
32
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
32
33
|
const { readTail } = require('./fileTail.cjs');
|
|
33
34
|
const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
@@ -35,7 +36,6 @@ const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
|
35
36
|
// The one project this hook always knows how to reach, used as the fallback
|
|
36
37
|
// destination when a job's own cwd has no feedback inbox.
|
|
37
38
|
const SM_REPO_ROOT = path.join(os.homedir(), 'Projects', 'session-manager');
|
|
38
|
-
const SM_REPO_FEEDBACK_DIR = path.join(SM_REPO_ROOT, 'session-manager-operations', 'feedback');
|
|
39
39
|
|
|
40
40
|
// NEVER write here: ~/.claude/session-manager/feedback/ is the scheduler's
|
|
41
41
|
// auto-PRD intake (src/main/scheduler.cjs's periodic feedback sweep) — it
|
|
@@ -200,27 +200,31 @@ function extractAcceptanceCriteria(prdBody) {
|
|
|
200
200
|
// ─── Destination resolution ──────────────────────────────────────────────────
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* Resolve
|
|
204
|
-
* 1.
|
|
205
|
-
*
|
|
203
|
+
* Resolve which PROJECT the proposal is filed into.
|
|
204
|
+
* 1. the job's own cwd — Epics live in every project, so this is almost
|
|
205
|
+
* always the answer.
|
|
206
|
+
* 2. else session-manager's own repo, naming the target project in the body.
|
|
207
|
+
*
|
|
208
|
+
* The old version probed for a `session-manager-operations/feedback/`
|
|
209
|
+
* directory and fell back when it was missing; with proposals there is
|
|
210
|
+
* nothing to probe for — the Epic store is created on demand.
|
|
206
211
|
*/
|
|
207
212
|
function resolveDestination(job) {
|
|
208
213
|
const jobCwd = job?.cwd;
|
|
209
214
|
if (jobCwd) {
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
if (fs.statSync(candidate).isDirectory()) {
|
|
213
|
-
return { dir: candidate, allowlistRoot: jobCwd, targetProjectNote: null };
|
|
214
|
-
}
|
|
215
|
-
} catch {
|
|
216
|
-
// missing/inaccessible — fall through to the session-manager fallback
|
|
217
|
-
}
|
|
215
|
+
return { cwd: jobCwd, allowlistRoot: jobCwd, targetProjectNote: null };
|
|
218
216
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
:
|
|
223
|
-
|
|
217
|
+
return {
|
|
218
|
+
cwd: SM_REPO_ROOT,
|
|
219
|
+
allowlistRoot: SM_REPO_ROOT,
|
|
220
|
+
targetProjectNote: "Filed into session-manager's own project (job had no cwd on record).",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** One-line title for the proposal — goalText is what the Epics list shows,
|
|
225
|
+
* so it must stay short; the full RCA markdown rides in openingPrompt. */
|
|
226
|
+
function rcaProposalTitle(job, verdict) {
|
|
227
|
+
return `PRD ${job?.slug ?? 'unknown'} needs review: ${humanVerdict(verdict)}`;
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
function shortRunId(runId) {
|
|
@@ -228,10 +232,6 @@ function shortRunId(runId) {
|
|
|
228
232
|
return cleaned.slice(0, 12) || 'norun';
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
function rcaFileName(dateStr, slug, runId) {
|
|
232
|
-
return `${dateStr}-rca-${slug}-${shortRunId(runId)}.md`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
235
|
function lastNLines(text, n) {
|
|
236
236
|
const lines = (text || '').split('\n');
|
|
237
237
|
return lines.slice(-n).join('\n');
|
|
@@ -328,35 +328,6 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
const dest = resolveDestination(job);
|
|
331
|
-
const dateStr = (now instanceof Date ? now : new Date()).toISOString().slice(0, 10);
|
|
332
|
-
const fileName = rcaFileName(dateStr, job.slug, job.runId);
|
|
333
|
-
const destPath = path.resolve(path.join(dest.dir, fileName));
|
|
334
|
-
if (!destPath.startsWith(path.resolve(dest.dir) + path.sep)) {
|
|
335
|
-
console.log(`[rca] skip: destPath escaped dest dir (${destPath})`);
|
|
336
|
-
return { filed: false, reason: 'unsafe-path' };
|
|
337
|
-
}
|
|
338
|
-
// /process-feedback archives dispositioned items to <dest.dir>/processed/
|
|
339
|
-
// immediately at disposition time (its own README convention) — long
|
|
340
|
-
// before this hook's own run-verify/self-heal passes might touch the same
|
|
341
|
-
// (slug, runId) again. Once archived, the live-dir check alone goes false
|
|
342
|
-
// and a re-trigger would refile a duplicate straight into the live inbox.
|
|
343
|
-
const processedPath = path.resolve(path.join(dest.dir, 'processed', fileName));
|
|
344
|
-
if (!processedPath.startsWith(path.resolve(path.join(dest.dir, 'processed')) + path.sep)) {
|
|
345
|
-
console.log(`[rca] skip: processedPath escaped processed dir (${processedPath})`);
|
|
346
|
-
return { filed: false, reason: 'unsafe-path' };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const liveExists = fs.existsSync(destPath);
|
|
350
|
-
const processedExists = !liveExists && fs.existsSync(processedPath);
|
|
351
|
-
const alreadyFiled = liveExists || processedExists;
|
|
352
|
-
if (alreadyFiled && !investigationText) {
|
|
353
|
-
const existingPath = liveExists ? destPath : processedPath;
|
|
354
|
-
console.log(`[rca] skip: already filed for ${job.slug}@${job.runId}`);
|
|
355
|
-
return { filed: false, reason: 'duplicate', path: existingPath };
|
|
356
|
-
}
|
|
357
|
-
// An investigationText update must target wherever the file actually
|
|
358
|
-
// lives (live dir or processed/), not assume the live dir.
|
|
359
|
-
const targetPath = liveExists || !processedExists ? destPath : processedPath;
|
|
360
331
|
|
|
361
332
|
const meta = readRunMeta(runDir, job.slug);
|
|
362
333
|
const logPath = runDir ? path.join(runDir, `${job.slug}.log`) : null;
|
|
@@ -369,11 +340,27 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
|
|
|
369
340
|
targetProjectNote: dest.targetProjectNote, annotations,
|
|
370
341
|
});
|
|
371
342
|
|
|
343
|
+
// File it as a PROPOSED Epic rather than a markdown file in a feedback
|
|
344
|
+
// inbox. The proposal lands directly in the Epics workspace behind an
|
|
345
|
+
// Approve & start gate: nothing runs and nothing is spent until a human
|
|
346
|
+
// approves it. This replaced the feedback-folder intake (and the
|
|
347
|
+
// process-feedback triage pass that existed only to read it back out) —
|
|
348
|
+
// the proposal IS the work item, already carrying its own session and
|
|
349
|
+
// PRD directory. reuseByGoal makes a re-trigger for the same failure
|
|
350
|
+
// join the existing proposal instead of duplicating it, which is what
|
|
351
|
+
// the old live/processed existence checks were for.
|
|
372
352
|
config.addAllowedRoot(dest.allowlistRoot);
|
|
373
|
-
|
|
353
|
+
const title = rcaProposalTitle(job, verdict);
|
|
354
|
+
const { epicId, created } = await ensureEpic(dest.cwd, {
|
|
355
|
+
goalText: title,
|
|
356
|
+
tag: 'bug',
|
|
357
|
+
status: 'proposed',
|
|
358
|
+
reuseByGoal: true,
|
|
359
|
+
openingPrompt: markdown,
|
|
360
|
+
});
|
|
374
361
|
|
|
375
|
-
console.log(`[rca] ${
|
|
376
|
-
return { filed: true,
|
|
362
|
+
console.log(`[rca] ${created ? 'proposed' : 'joined'} epic ${epicId} for ${job.slug}`);
|
|
363
|
+
return { filed: true, epicId, created, updated: !created };
|
|
377
364
|
} catch (e) {
|
|
378
365
|
console.error('[rca] error filing RCA feedback', e?.message ?? String(e));
|
|
379
366
|
return { filed: false, reason: 'error', error: e?.message ?? String(e) };
|
|
@@ -382,15 +369,14 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
|
|
|
382
369
|
|
|
383
370
|
module.exports = {
|
|
384
371
|
fileRcaFeedback,
|
|
372
|
+
rcaProposalTitle,
|
|
385
373
|
// Pure helpers, exported for unit tests.
|
|
386
374
|
humanVerdict,
|
|
387
375
|
classifyFailure,
|
|
388
376
|
extractAcceptanceCriteria,
|
|
389
377
|
extractRcaBlock,
|
|
390
378
|
resolveDestination,
|
|
391
|
-
rcaFileName,
|
|
392
379
|
buildRcaMarkdown,
|
|
393
380
|
FAILURE_CLASSES,
|
|
394
381
|
VERDICT_LABELS,
|
|
395
|
-
SM_REPO_FEEDBACK_DIR,
|
|
396
382
|
};
|
|
@@ -226,8 +226,22 @@ async function gatherSrcTree(cwd) {
|
|
|
226
226
|
|
|
227
227
|
const inFlight = new Set();
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Register the project root as an allowed WRITE root before persisting
|
|
231
|
+
* brief.json. config.cjs only grants project-root writes to roots added via
|
|
232
|
+
* addAllowedRoot, and until now the sole caller was pty.cjs's spawn — so a
|
|
233
|
+
* project whose tab had no live PTY (dormant/hydrated tab, chat-only Epic)
|
|
234
|
+
* failed the write with "Write outside allowed write boundaries" *after*
|
|
235
|
+
* paying for a full synthesis. The read-side validatePath above has already
|
|
236
|
+
* proven the cwd is inside allowedRoots, so this widens nothing.
|
|
237
|
+
*/
|
|
238
|
+
function allowBriefWrites(realCwd) {
|
|
239
|
+
config.addAllowedRoot(realCwd);
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
async function refresh({ cwd }) {
|
|
230
243
|
const realCwd = config.validatePath(cwd);
|
|
244
|
+
allowBriefWrites(realCwd);
|
|
231
245
|
|
|
232
246
|
if (inFlight.has(realCwd)) {
|
|
233
247
|
return { ok: false, error: 'refresh already running' };
|
|
@@ -279,9 +293,10 @@ async function refresh({ cwd }) {
|
|
|
279
293
|
priorPinned,
|
|
280
294
|
model: BRIEF_MODEL,
|
|
281
295
|
nowIso: new Date().toISOString(),
|
|
296
|
+
priorEditedAt: priorBrief ? priorBrief.editedAt : null,
|
|
282
297
|
});
|
|
283
298
|
|
|
284
|
-
await config.writeJson(briefPath(realCwd), persisted);
|
|
299
|
+
await config.writeJson(briefPath(realCwd), persisted, { writer: 'project-home' });
|
|
285
300
|
return { ok: true, brief: persisted };
|
|
286
301
|
} finally {
|
|
287
302
|
if (token) sessionSlots.release(token);
|
|
@@ -289,12 +304,29 @@ async function refresh({ cwd }) {
|
|
|
289
304
|
}
|
|
290
305
|
}
|
|
291
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Hand-edit brief.json in place — the "maintain" half of the generate-and-
|
|
309
|
+
* maintain loop, and the reason the brief is a real file rather than a cache.
|
|
310
|
+
* Zero LLM cost. Edited pinnable blocks are auto-pinned by computeUpdate so
|
|
311
|
+
* the next refresh feeds them back in instead of rewriting them.
|
|
312
|
+
*/
|
|
313
|
+
async function update({ cwd, patch }) {
|
|
314
|
+
const realCwd = config.validatePath(cwd);
|
|
315
|
+
allowBriefWrites(realCwd);
|
|
316
|
+
const currentBrief = await readBrief(realCwd);
|
|
317
|
+
const result = core.computeUpdate(currentBrief, patch, new Date().toISOString());
|
|
318
|
+
if (!result.ok) return result;
|
|
319
|
+
await config.writeJson(briefPath(realCwd), result.brief, { writer: 'project-home' });
|
|
320
|
+
return { ok: true, brief: result.brief };
|
|
321
|
+
}
|
|
322
|
+
|
|
292
323
|
async function setPin({ cwd, block, pinned }) {
|
|
293
324
|
const realCwd = config.validatePath(cwd);
|
|
325
|
+
allowBriefWrites(realCwd);
|
|
294
326
|
const currentBrief = await readBrief(realCwd);
|
|
295
327
|
const result = core.computeSetPin(currentBrief, block, pinned);
|
|
296
328
|
if (!result.ok) return result;
|
|
297
|
-
await config.writeJson(briefPath(realCwd), result.brief);
|
|
329
|
+
await config.writeJson(briefPath(realCwd), result.brief, { writer: 'project-home' });
|
|
298
330
|
return { ok: true, brief: result.brief };
|
|
299
331
|
}
|
|
300
332
|
|
|
@@ -303,6 +335,7 @@ function registerProjectBriefIpc() {
|
|
|
303
335
|
ipcMain.handle('project-brief:get', v(s.projectBriefCwd, get));
|
|
304
336
|
ipcMain.handle('project-brief:refresh', v(s.projectBriefCwd, refresh));
|
|
305
337
|
ipcMain.handle('project-brief:set-pin', v(s.projectBriefSetPin, setPin));
|
|
338
|
+
ipcMain.handle('project-brief:update', v(s.projectBriefUpdate, update));
|
|
306
339
|
}
|
|
307
340
|
|
|
308
341
|
module.exports = {
|
|
@@ -310,4 +343,5 @@ module.exports = {
|
|
|
310
343
|
get,
|
|
311
344
|
refresh,
|
|
312
345
|
setPin,
|
|
346
|
+
update,
|
|
313
347
|
};
|
|
@@ -18,6 +18,22 @@ function promptSessionActiveIndexPath(cwd) {
|
|
|
18
18
|
return `${String(cwd).replace(/\/+$/, '')}/session-manager-operations/prompt-sessions/active-index.json`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// IPC channel broadcast whenever an event is appended to a PromptSession's
|
|
22
|
+
// chain from the main process (currently only the scheduler's response-event
|
|
23
|
+
// append below). Mirrors chatRunner.cjs's attachWindow/broadcast pattern.
|
|
24
|
+
const EVENT_APPENDED_CHANNEL = 'promptSession:event-appended';
|
|
25
|
+
|
|
26
|
+
let mainWindow = null;
|
|
27
|
+
function attachWindow(win) { mainWindow = win; }
|
|
28
|
+
|
|
29
|
+
function broadcast(channel, payload) {
|
|
30
|
+
try {
|
|
31
|
+
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
|
|
32
|
+
mainWindow.webContents.send(channel, payload);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* render frame may be gone */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
let seq = 0;
|
|
22
38
|
function mintEventId() {
|
|
23
39
|
seq += 1;
|
|
@@ -75,7 +91,8 @@ async function appendResponseEventIfKnown(cwd, sourcePromptId, text) {
|
|
|
75
91
|
text,
|
|
76
92
|
};
|
|
77
93
|
data.events[sourcePromptId] = [...events, event];
|
|
78
|
-
await config.writeJson(path, data);
|
|
94
|
+
await config.writeJson(path, data, { writer: 'scheduler' });
|
|
95
|
+
broadcast(EVENT_APPENDED_CHANNEL, { cwd, promptSessionId: sourcePromptId, event });
|
|
79
96
|
return true;
|
|
80
97
|
});
|
|
81
98
|
} catch (e) {
|
|
@@ -84,4 +101,9 @@ async function appendResponseEventIfKnown(cwd, sourcePromptId, text) {
|
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
module.exports = {
|
|
104
|
+
module.exports = {
|
|
105
|
+
appendResponseEventIfKnown,
|
|
106
|
+
promptSessionActiveIndexPath,
|
|
107
|
+
attachWindow,
|
|
108
|
+
EVENT_APPENDED_CHANNEL,
|
|
109
|
+
};
|
|
Binary file
|
package/src/main/queueOps.cjs
CHANGED
|
@@ -519,7 +519,7 @@ async function retagOne({ slug, parallelGroup, estimateMinutes }) {
|
|
|
519
519
|
// Atomic write via shared helper. If slug changed, write at the new path
|
|
520
520
|
// and unlink the old slug.
|
|
521
521
|
try {
|
|
522
|
-
await config.writeTextAtomic(dst, newRaw);
|
|
522
|
+
await config.writeTextAtomic(dst, newRaw, { writer: 'scheduler' });
|
|
523
523
|
if (dst !== src) {
|
|
524
524
|
try { await fsp.unlink(src); } catch { /* if src is already same as dst (race), fine */ }
|
|
525
525
|
}
|
|
@@ -73,6 +73,14 @@ async function parsePrdRaw(filePath) {
|
|
|
73
73
|
// dropped straight into an Epic's prds/ dir still get real linkage
|
|
74
74
|
// instead of null.
|
|
75
75
|
sourcePromptId: fm.sourcePromptId || deriveEpicIdFromPrdPath(filePath) || null,
|
|
76
|
+
// The Epic this PRD actually belongs to, derived purely from its location
|
|
77
|
+
// (epic dir name == epic id, 1:1 by design — prdLocations.deriveEpicIdFromPrdPath).
|
|
78
|
+
// Deliberately separate from sourcePromptId: that field is *intent* and
|
|
79
|
+
// can be stale or hand-written wrong in frontmatter (real rows in
|
|
80
|
+
// queue.json exist where sourcePromptId and sourceTabId disagree), while
|
|
81
|
+
// this one is *fact* on disk. Every surface that shows "which Epic is this
|
|
82
|
+
// from" reads this, so Scheduler and Epics can't drift apart.
|
|
83
|
+
epicId: deriveEpicIdFromPrdPath(filePath),
|
|
76
84
|
// Optional traceability back to the chat tab that queued this PRD (PRD
|
|
77
85
|
// 761) — read back at job completion to route a status prompt via
|
|
78
86
|
// enqueueExternalPrompt (PRD 753). Additive — absent on every PRD
|