claude-code-session-manager 0.38.7 → 0.39.1
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-C74mBmMU.js → TiptapBody-CtuLATFR.js} +1 -1
- package/dist/assets/{index-BGTU12rl.css → index-B6JNrbpD.css} +1 -1
- package/dist/assets/{index-DPSmbyU_.js → index-CefVSjmm.js} +559 -559
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +24 -17
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
- package/src/main/__tests__/prdLocations.test.cjs +56 -0
- package/src/main/__tests__/prdMigration.test.cjs +126 -0
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +8 -8
- package/src/main/config.cjs +9 -0
- package/src/main/health.cjs +26 -20
- package/src/main/ipcSchemas.cjs +3 -0
- package/src/main/lib/__tests__/importReferences.spec.cjs +56 -0
- package/src/main/lib/definitionOfDone.cjs +8 -9
- package/src/main/lib/importReferences.cjs +34 -0
- package/src/main/lib/prdCreate.cjs +3 -3
- package/src/main/lib/prdLocations.cjs +46 -0
- package/src/main/lib/prdMigration.cjs +83 -0
- package/src/main/lib/rcaFeedbackHook.cjs +5 -5
- package/src/main/queueOps.cjs +44 -15
- package/src/main/scheduler.cjs +191 -60
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/preload/api.d.ts +16 -0
- package/src/preload/index.cjs +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdLocations.cjs — resolves per-project PRD-source directories.
|
|
3
|
+
*
|
|
4
|
+
* PRD 808 (1 of the 808→809→810→811 chain moving PRD storage out of the
|
|
5
|
+
* single global `~/.claude/session-manager/scheduled-plans/prds/` into each
|
|
6
|
+
* active project's own `<cwd>/session-manager-operations/scheduler/prds/`,
|
|
7
|
+
* for consistency with how feedback and HUMAN_LEARN already scope
|
|
8
|
+
* per-project operational state under `session-manager-operations/`.
|
|
9
|
+
*
|
|
10
|
+
* Global scheduler bookkeeping (queue.json, history.jsonl, runs/) is
|
|
11
|
+
* untouched by this module — it only resolves where PRD *source* .md files
|
|
12
|
+
* live. Active-project discovery reuses activeSessions.cjs's
|
|
13
|
+
* activeProjectCwds (the same discovery watchdogHelpers.cjs's sweep() uses)
|
|
14
|
+
* rather than re-implementing transcript scanning here.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { activeProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
|
|
20
|
+
|
|
21
|
+
const PRD_SUBPATH = ['session-manager-operations', 'scheduler', 'prds'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* resolvePrdWriteDir(cwd) → `<cwd>/session-manager-operations/scheduler/prds`
|
|
25
|
+
* Pure path join, no I/O. Throws on a missing/non-string cwd so a caller
|
|
26
|
+
* never silently resolves a project-scoped path against `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
function resolvePrdWriteDir(cwd) {
|
|
29
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
30
|
+
throw new Error('resolvePrdWriteDir: cwd is required');
|
|
31
|
+
}
|
|
32
|
+
return path.join(cwd, ...PRD_SUBPATH);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* resolvePrdsDirs(maxAgeMin?, opts?) → string[]
|
|
37
|
+
* One `<cwd>/session-manager-operations/scheduler/prds` dir per active
|
|
38
|
+
* project cwd (activeProjectCwds' default window is 90 minutes). `opts` is
|
|
39
|
+
* forwarded to activeProjectCwds (e.g. `projectsDir` override for tests).
|
|
40
|
+
*/
|
|
41
|
+
function resolvePrdsDirs(maxAgeMin, opts) {
|
|
42
|
+
const cwds = activeProjectCwds(maxAgeMin, opts);
|
|
43
|
+
return cwds.map(resolvePrdWriteDir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { resolvePrdWriteDir, resolvePrdsDirs, PRD_SUBPATH };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdMigration.cjs — one-time, idempotent move of PRD source .md files out
|
|
3
|
+
* of the legacy global `~/.claude/session-manager/scheduled-plans/prds/`
|
|
4
|
+
* into each PRD's own project's `<cwd>/session-manager-operations/scheduler/prds/`
|
|
5
|
+
* (PRD 808, first of the 808→809→810→811 chain).
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: once a file has been moved out of legacyPrdsDir it is simply
|
|
8
|
+
* absent on the next run — nothing left to move, so a repeat run is a
|
|
9
|
+
* no-op fs.readdir + early return. Files whose frontmatter has no parseable
|
|
10
|
+
* `cwd` are left in place (never silently dropped) and reported back as
|
|
11
|
+
* `strandedCount` so the caller can log a warning — see `migratePrds`'s
|
|
12
|
+
* `unresolved` return field, surfaced by scheduler.cjs's boot safety-net.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const fsp = require('node:fs/promises');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
20
|
+
const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Move every `.md` file in legacyPrdsDir whose frontmatter `cwd` resolves to
|
|
24
|
+
* an on-disk directory into that project's PRDs dir (renamed, not copied).
|
|
25
|
+
* Files with missing/unparseable/non-existent `cwd` are left untouched.
|
|
26
|
+
*
|
|
27
|
+
* Returns { moved, skipped, unresolved } where:
|
|
28
|
+
* - moved: number of files successfully relocated
|
|
29
|
+
* - skipped: number of files already gone (race with a concurrent mover)
|
|
30
|
+
* - unresolved: [{ file, reason }] for every .md left behind in legacyPrdsDir
|
|
31
|
+
*/
|
|
32
|
+
async function migratePrds(legacyPrdsDir) {
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await fsp.readdir(legacyPrdsDir);
|
|
36
|
+
} catch {
|
|
37
|
+
return { moved: 0, skipped: 0, unresolved: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let moved = 0;
|
|
41
|
+
let skipped = 0;
|
|
42
|
+
const unresolved = [];
|
|
43
|
+
|
|
44
|
+
for (const name of entries) {
|
|
45
|
+
if (!name.endsWith('.md') || name.startsWith('.')) continue;
|
|
46
|
+
const src = path.join(legacyPrdsDir, name);
|
|
47
|
+
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await fsp.readFile(src, 'utf8');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e?.code === 'ENOENT') { skipped++; continue; }
|
|
53
|
+
unresolved.push({ file: name, reason: `read failed: ${e?.message}` });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { fm } = splitFrontmatter(raw);
|
|
58
|
+
const cwd = fm.cwd && fm.cwd.trim();
|
|
59
|
+
if (!cwd) {
|
|
60
|
+
unresolved.push({ file: name, reason: 'no cwd in frontmatter' });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!fs.existsSync(cwd)) {
|
|
64
|
+
unresolved.push({ file: name, reason: `cwd does not exist on disk: ${cwd}` });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const destDir = resolvePrdWriteDir(cwd);
|
|
69
|
+
const dst = path.join(destDir, name);
|
|
70
|
+
try {
|
|
71
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
72
|
+
await fsp.rename(src, dst);
|
|
73
|
+
moved++;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e?.code === 'ENOENT') { skipped++; continue; }
|
|
76
|
+
unresolved.push({ file: name, reason: `move failed: ${e?.message}` });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { moved, skipped, unresolved };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { migratePrds };
|
|
@@ -30,8 +30,7 @@ const path = require('node:path');
|
|
|
30
30
|
const config = require('../config.cjs');
|
|
31
31
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
32
32
|
const { readTail } = require('./fileTail.cjs');
|
|
33
|
-
|
|
34
|
-
const PRDS_DIR = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'prds');
|
|
33
|
+
const { resolvePrdWriteDir } = require('./prdLocations.cjs');
|
|
35
34
|
|
|
36
35
|
// The one project this hook always knows how to reach, used as the fallback
|
|
37
36
|
// destination when a job's own cwd has no feedback inbox.
|
|
@@ -214,9 +213,10 @@ function readRunMeta(runDir, slug) {
|
|
|
214
213
|
}
|
|
215
214
|
}
|
|
216
215
|
|
|
217
|
-
function readPrdBody(slug) {
|
|
216
|
+
function readPrdBody(cwd, slug) {
|
|
217
|
+
if (!cwd) return '';
|
|
218
218
|
try {
|
|
219
|
-
const raw = fs.readFileSync(path.join(
|
|
219
|
+
const raw = fs.readFileSync(path.join(resolvePrdWriteDir(cwd), `${slug}.md`), 'utf8');
|
|
220
220
|
return splitFrontmatter(raw).body;
|
|
221
221
|
} catch {
|
|
222
222
|
return '';
|
|
@@ -311,7 +311,7 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
|
|
|
311
311
|
const meta = readRunMeta(runDir, job.slug);
|
|
312
312
|
const logPath = runDir ? path.join(runDir, `${job.slug}.log`) : null;
|
|
313
313
|
const logTail = logPath ? readTail(logPath, 64 * 1024) : '';
|
|
314
|
-
const acText = extractAcceptanceCriteria(readPrdBody(job.slug));
|
|
314
|
+
const acText = extractAcceptanceCriteria(readPrdBody(job.cwd, job.slug));
|
|
315
315
|
const failureClass = classifyFailure({ verdict, logTail });
|
|
316
316
|
|
|
317
317
|
const markdown = buildRcaMarkdown({
|
package/src/main/queueOps.cjs
CHANGED
|
@@ -128,6 +128,27 @@ const INTERACTIVE_AC_PATTERNS = [
|
|
|
128
128
|
];
|
|
129
129
|
|
|
130
130
|
const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
|
|
131
|
+
const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
|
|
132
|
+
|
|
133
|
+
// PRD 808: PRDS_DIR is now the LEGACY global dir — kept as a migration
|
|
134
|
+
// source and search fallback for not-yet-migrated files. Live PRDs resolve
|
|
135
|
+
// per-project via prdLocations.cjs's resolvePrdsDirs(). candidatePrdsDirs()
|
|
136
|
+
// aggregates both so the linter/archive/retag operations still see every
|
|
137
|
+
// PRD regardless of which dir it currently lives in.
|
|
138
|
+
function candidatePrdsDirs() {
|
|
139
|
+
return [PRDS_DIR, ...resolvePrdsDirs()];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Search every candidate PRD dir for `<slug>.md`; returns the containing dir or null. */
|
|
143
|
+
async function findPrdDir(slug) {
|
|
144
|
+
for (const dir of candidatePrdsDirs()) {
|
|
145
|
+
try {
|
|
146
|
+
await fsp.access(path.join(dir, `${slug}.md`));
|
|
147
|
+
return dir;
|
|
148
|
+
} catch { /* not here — try the next candidate dir */ }
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
131
152
|
|
|
132
153
|
/**
|
|
133
154
|
* Lint a single PRD asynchronously. Returns { slug, findings: [...] }.
|
|
@@ -237,16 +258,18 @@ function lintParsed(slug, raw) {
|
|
|
237
258
|
* so Promise.all never rejects under normal use.
|
|
238
259
|
*/
|
|
239
260
|
async function lintAll() {
|
|
240
|
-
let entries = [];
|
|
241
|
-
try {
|
|
242
|
-
entries = await fsp.readdir(PRDS_DIR);
|
|
243
|
-
} catch {
|
|
244
|
-
return { reports: [], scannedAt: Date.now() };
|
|
245
|
-
}
|
|
246
261
|
const paths = [];
|
|
247
|
-
for (const
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
for (const dir of candidatePrdsDirs()) {
|
|
263
|
+
let entries;
|
|
264
|
+
try {
|
|
265
|
+
entries = await fsp.readdir(dir);
|
|
266
|
+
} catch {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
for (const name of entries) {
|
|
270
|
+
if (!name.endsWith('.md') || name.startsWith('.')) continue;
|
|
271
|
+
paths.push(path.join(dir, name));
|
|
272
|
+
}
|
|
250
273
|
}
|
|
251
274
|
const reports = await Promise.all(paths.map(async (filePath) => {
|
|
252
275
|
try {
|
|
@@ -270,8 +293,10 @@ async function lintAll() {
|
|
|
270
293
|
*/
|
|
271
294
|
async function archiveOne(slug, archiveDir) {
|
|
272
295
|
if (!SLUG_RE.test(slug)) return { ok: false, slug, error: 'invalid slug' };
|
|
273
|
-
const
|
|
274
|
-
if (!
|
|
296
|
+
const srcDir = await findPrdDir(slug);
|
|
297
|
+
if (!srcDir) return { ok: false, slug, error: 'not found in any PRDs dir' };
|
|
298
|
+
const src = path.resolve(path.join(srcDir, `${slug}.md`));
|
|
299
|
+
if (!src.startsWith(srcDir + path.sep)) return { ok: false, slug, error: 'path escape (src)' };
|
|
275
300
|
const dst = path.resolve(path.join(archiveDir, `${slug}.md`));
|
|
276
301
|
if (!dst.startsWith(PRDS_ARCHIVE_DIR + path.sep)) return { ok: false, slug, error: 'path escape (dst)' };
|
|
277
302
|
try {
|
|
@@ -407,8 +432,10 @@ async function autoArchiveCompleted(state, { nowMs } = {}) {
|
|
|
407
432
|
*/
|
|
408
433
|
async function retagOne({ slug, parallelGroup, estimateMinutes }) {
|
|
409
434
|
if (!SLUG_RE.test(slug)) return { ok: false, slug, error: 'invalid slug' };
|
|
410
|
-
const
|
|
411
|
-
if (!
|
|
435
|
+
const dir = await findPrdDir(slug);
|
|
436
|
+
if (!dir) return { ok: false, slug, error: 'not found in any PRDs dir' };
|
|
437
|
+
const src = path.resolve(path.join(dir, `${slug}.md`));
|
|
438
|
+
if (!src.startsWith(dir + path.sep)) return { ok: false, slug, error: 'path escape' };
|
|
412
439
|
|
|
413
440
|
let raw;
|
|
414
441
|
try {
|
|
@@ -470,8 +497,8 @@ async function retagOne({ slug, parallelGroup, estimateMinutes }) {
|
|
|
470
497
|
}
|
|
471
498
|
if (!SLUG_RE.test(newSlug)) return { ok: false, slug, error: 'new slug would be invalid' };
|
|
472
499
|
}
|
|
473
|
-
const dst = path.resolve(path.join(
|
|
474
|
-
if (!dst.startsWith(
|
|
500
|
+
const dst = path.resolve(path.join(dir, `${newSlug}.md`));
|
|
501
|
+
if (!dst.startsWith(dir + path.sep)) return { ok: false, slug, error: 'new path escape' };
|
|
475
502
|
|
|
476
503
|
// Atomic write via shared helper. If slug changed, write at the new path
|
|
477
504
|
// and unlink the old slug.
|
|
@@ -551,4 +578,6 @@ module.exports = {
|
|
|
551
578
|
retagMany,
|
|
552
579
|
PRDS_DIR,
|
|
553
580
|
PRDS_ARCHIVE_DIR,
|
|
581
|
+
candidatePrdsDirs,
|
|
582
|
+
findPrdDir,
|
|
554
583
|
};
|