claude-code-session-manager 0.122.1 → 0.123.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/{AgentLibrary-C2qUX9WU.js → AgentLibrary-T14aM8ie.js} +2 -2
- package/dist/assets/{Button-ssBzvFha.js → Button-Dz1kV8Z_.js} +1 -1
- package/dist/assets/DataModel-DdYRn2Ec.js +1 -0
- package/dist/assets/{History-BRdRp1J_.js → History-zQPvqwE7.js} +2 -2
- package/dist/assets/{Hooks--hiGPBQy.js → Hooks-BTrbT742.js} +3 -3
- package/dist/assets/{Library-0XYvkhh2.js → Library-BwQ6GRHR.js} +7 -7
- package/dist/assets/ListDetail-CunxAhRC.js +1 -0
- package/dist/assets/{McpServers-CCjHSwFm.js → McpServers-DpoyxXaO.js} +2 -2
- package/dist/assets/{Memory-C7RXoVO4.js → Memory-KDtGYMc3.js} +8 -8
- package/dist/assets/{Panel-C92O_aAH.js → Panel-xXmUBo8l.js} +1 -1
- package/dist/assets/{Permissions-DuTJ1FUb.js → Permissions-lIzTUrW2.js} +3 -3
- package/dist/assets/{Plugins-DSXsP4hM.js → Plugins-BA1Zqk69.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CWdDkU1m.js → ProvenanceBadge-CmeJG2Kw.js} +1 -1
- package/dist/assets/{SaveBar-ob2dJJjH.js → SaveBar-BTJg_9Uc.js} +1 -1
- package/dist/assets/{Scheduler-Du4HgPKY.js → Scheduler-CISTDNMU.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Dv_6M_Ci.js → ScopeSwitcher-C6NWvzoj.js} +1 -1
- package/dist/assets/Settings-BBoGJ34v.js +4 -0
- package/dist/assets/{SkillReferenceGraph-Dx1Qs8Y0.js → SkillReferenceGraph-IJv60QAJ.js} +1 -1
- package/dist/assets/Skills-L6nGcNF1.js +2 -0
- package/dist/assets/SystemPrompt-Cm7I2gJz.js +1 -0
- package/dist/assets/TagLibrary-BxPwoPE6.js +1 -0
- package/dist/assets/{TiptapBody-Cihe5er3.js → TiptapBody-CR-Zr3Pz.js} +1 -1
- package/dist/assets/{Toggle-CpYVC8bF.js → Toggle-BFhlpZ-n.js} +1 -1
- package/dist/assets/{ViewTabs-C3FN6P_C.js → ViewTabs-CuGlZ9Ed.js} +1 -1
- package/dist/assets/{historyTotals-C4Rs37Cd.js → historyTotals-Dxv9Fzrs.js} +1 -1
- package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
- package/dist/assets/{index-S-F5jp_h.js → index-Dx8owDeQ.js} +151 -151
- package/dist/assets/{settingsSchema-BZcdzHTQ.js → settingsSchema-DaqFSAQH.js} +1 -1
- package/dist/assets/{skillFrontmatter-EfNERt4b.js → skillFrontmatter-BIJddxVB.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-BfbrFvB3.js → useAgentsDirWatch-1hjZ4knk.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -25
- package/scripts/scheduler-mcp-server.cjs +124 -74
- package/src/main/agentLibrary.cjs +8 -3
- package/src/main/build-info.json +4 -4
- package/src/main/crashDiagnostics.cjs +24 -0
- package/src/main/historyAggregator.cjs +41 -7
- package/src/main/index.cjs +13 -3
- package/src/main/ipcSchemas.cjs +30 -22
- package/src/main/lib/activeSessions.cjs +93 -32
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
- package/src/main/lib/epic/epicMint.cjs +1 -1
- package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
- package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
- package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
- package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
- package/src/main/lib/git/gitWorktree.cjs +9 -4
- package/src/main/lib/git/worktreeDeps.cjs +13 -2
- package/src/main/lib/git/worktreeSetup.cjs +212 -0
- package/src/main/lib/mcpToolCatalog.cjs +31 -2
- package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
- package/src/main/lib/prd/prdAgentType.cjs +28 -0
- package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
- package/src/main/lib/prd/prdCreate.cjs +205 -32
- package/src/main/lib/prd/prdGateFiles.cjs +31 -0
- package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
- package/src/main/lib/scheduler/queueStore.cjs +5 -4
- package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
- package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
- package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
- package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
- package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
- package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
- package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
- package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
- package/src/main/pty.cjs +0 -53
- package/src/main/runVerify.cjs +38 -11
- package/src/main/scheduler/prdParser.cjs +3 -0
- package/src/main/scheduler/prdPaths.cjs +48 -2
- package/src/main/scheduler.cjs +124 -29
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +8 -4
- package/src/main/webRemote.cjs +38 -132
- package/src/preload/api.d.ts +4 -52
- package/src/preload/index.cjs +0 -24
- package/src/seed/agents/dev-lead.md +2 -1
- package/src/seed/agents/validator.md +2 -1
- package/dist/assets/DataModel-CENoTvc2.js +0 -1
- package/dist/assets/ListDetail-CkKXAM59.js +0 -1
- package/dist/assets/RemoteMachines-BFmbIGpW.js +0 -3
- package/dist/assets/Settings-DMoI0cqB.js +0 -4
- package/dist/assets/Skills-DYWQTQB1.js +0 -2
- package/dist/assets/SystemPrompt-Df0IfOuI.js +0 -1
- package/dist/assets/TagLibrary-BuMsGx-2.js +0 -1
- package/dist/assets/useRemotePeers-CKVakQPc.js +0 -1
- package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
- package/src/main/remoteClient.cjs +0 -334
- package/src/main/remoteClientIpc.cjs +0 -133
|
@@ -27,6 +27,7 @@ const { app, powerSaveBlocker, powerMonitor, crashReporter } = require('electron
|
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
29
|
const { reportCrash, classifyUncleanShutdown, reportChildProcessGone } = require('./lib/telemetry/crashTelemetry.cjs');
|
|
30
|
+
const { maybeCaptureMainHeap } = require('./lib/telemetry/mainHeapSnapshot.cjs');
|
|
30
31
|
const { createLoopDelayMonitor, stallVerdict } = require('./lib/util/loopDelay.cjs');
|
|
31
32
|
|
|
32
33
|
let logs = null;
|
|
@@ -124,6 +125,7 @@ function sampleMemory() {
|
|
|
124
125
|
main: {
|
|
125
126
|
rssMb: Math.round(mu.rss / 1048576),
|
|
126
127
|
heapUsedMb: Math.round(mu.heapUsed / 1048576),
|
|
128
|
+
heapTotalMb: Math.round(mu.heapTotal / 1048576),
|
|
127
129
|
externalMb: Math.round(mu.external / 1048576),
|
|
128
130
|
},
|
|
129
131
|
powerBlockerHeld: (() => {
|
|
@@ -152,6 +154,25 @@ function persistSentinel(clean) {
|
|
|
152
154
|
} catch { /* best-effort */ }
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
let heapSkipLogged = false;
|
|
158
|
+
function tryMainHeapSnapshot() {
|
|
159
|
+
try {
|
|
160
|
+
const t0 = Date.now();
|
|
161
|
+
const heapUsedMb = process.memoryUsage().heapUsed / 1048576;
|
|
162
|
+
const r = maybeCaptureMainHeap({ heapUsedMb });
|
|
163
|
+
if (r.captured) {
|
|
164
|
+
logs?.writeLine({ scope: 'crash-diag', level: 'warn', message: '[crash-diag] main heap snapshot',
|
|
165
|
+
meta: { filePath: r.filePath, ms: Date.now() - t0, heapUsedMb: Math.round(heapUsedMb) } });
|
|
166
|
+
} else if (r.reason !== 'below-threshold' && r.reason !== 'already-captured' && !heapSkipLogged) {
|
|
167
|
+
heapSkipLogged = true;
|
|
168
|
+
logs?.writeLine({ scope: 'crash-diag', level: 'warn', message: '[crash-diag] main heap snapshot skipped',
|
|
169
|
+
meta: { reason: r.reason, heapUsedMb: Math.round(heapUsedMb) } });
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
try { logs?.writeLine({ scope: 'crash-diag', level: 'error', message: `[crash-diag] main heap snapshot failed: ${err && err.message}` }); } catch { /* ignore */ }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
function heartbeat() {
|
|
156
177
|
lastSample = sampleMemory();
|
|
157
178
|
const loop = loopMonitor ? loopMonitor.snapshot() : null;
|
|
@@ -167,11 +188,14 @@ function heartbeat() {
|
|
|
167
188
|
meta: {
|
|
168
189
|
totalMb: lastSample.totalMb,
|
|
169
190
|
mainRssMb: lastSample.main.rssMb,
|
|
191
|
+
mainHeapUsedMb: lastSample.main.heapUsedMb,
|
|
192
|
+
mainHeapTotalMb: lastSample.main.heapTotalMb,
|
|
170
193
|
powerBlockerHeld: lastSample.powerBlockerHeld,
|
|
171
194
|
top: top.map((p) => `${p.type}:${p.mb}MB`),
|
|
172
195
|
...loop,
|
|
173
196
|
},
|
|
174
197
|
});
|
|
198
|
+
tryMainHeapSnapshot();
|
|
175
199
|
const stall = loop && stallVerdict(loop);
|
|
176
200
|
if (stall) logs?.writeLine({ scope: 'crash-diag', ...stall, meta: { ...loop } });
|
|
177
201
|
}
|
|
@@ -553,10 +553,30 @@ function rollupMapToRows(map, finalizedDates) {
|
|
|
553
553
|
* opts.dryRun (optional): computes what would be finalized without writing
|
|
554
554
|
* anything to the rollup file.
|
|
555
555
|
*/
|
|
556
|
-
async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
556
|
+
async function finalizeClosedDays({ budgetMs, dryRun = false, cursorPath } = {}) {
|
|
557
557
|
const deadline = typeof budgetMs === 'number' ? Date.now() + budgetMs : null;
|
|
558
558
|
const today = localDate(new Date());
|
|
559
559
|
|
|
560
|
+
// Resumable cursor: { cycleStartedAt, lastDir, dates }. `dates` carries the
|
|
561
|
+
// closed days seen by earlier partial calls of this cycle so the completing
|
|
562
|
+
// call can stamp all of them. Missing/corrupt file => fresh cycle.
|
|
563
|
+
const atomicFs = require('./lib/util/atomicFs.cjs');
|
|
564
|
+
let resolvedCursor = cursorPath;
|
|
565
|
+
if (!resolvedCursor) {
|
|
566
|
+
try { resolvedCursor = require('./lib/scheduler/schedulerPaths.cjs').historyRollupCursorPath(); } catch { resolvedCursor = null; }
|
|
567
|
+
}
|
|
568
|
+
let cycleStartedAt = Date.now();
|
|
569
|
+
let lastDir = '';
|
|
570
|
+
const dates = new Set();
|
|
571
|
+
if (resolvedCursor) {
|
|
572
|
+
const c = atomicFs.readJsonOrSync(resolvedCursor, null);
|
|
573
|
+
if (c && typeof c === 'object' && typeof c.lastDir === 'string' && c.lastDir) {
|
|
574
|
+
lastDir = c.lastDir;
|
|
575
|
+
if (typeof c.cycleStartedAt === 'number') cycleStartedAt = c.cycleStartedAt;
|
|
576
|
+
if (Array.isArray(c.dates)) for (const d of c.dates) if (typeof d === 'string') dates.add(d);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
560
580
|
let projectDirs;
|
|
561
581
|
try {
|
|
562
582
|
projectDirs = await fsp.readdir(PROJECTS_DIR, { withFileTypes: true });
|
|
@@ -566,10 +586,12 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
|
566
586
|
|
|
567
587
|
projectDirs = projectDirs
|
|
568
588
|
.filter((e) => e.isDirectory())
|
|
569
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
589
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
590
|
+
.filter((e) => !lastDir || e.name.localeCompare(lastDir) > 0);
|
|
570
591
|
|
|
571
592
|
const buckets = new Map();
|
|
572
593
|
let partial = false;
|
|
594
|
+
let walkedLast = lastDir;
|
|
573
595
|
|
|
574
596
|
for (const projEntry of projectDirs) {
|
|
575
597
|
if (deadline !== null && Date.now() >= deadline) {
|
|
@@ -581,7 +603,7 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
|
581
603
|
const projectDir = path.join(PROJECTS_DIR, encodedCwd);
|
|
582
604
|
|
|
583
605
|
let files;
|
|
584
|
-
try { files = await fsp.readdir(projectDir, { withFileTypes: true }); } catch { continue; }
|
|
606
|
+
try { files = await fsp.readdir(projectDir, { withFileTypes: true }); } catch { walkedLast = encodedCwd; continue; }
|
|
585
607
|
|
|
586
608
|
for (const fileEntry of files) {
|
|
587
609
|
if (!fileEntry.name.endsWith('.jsonl')) continue;
|
|
@@ -600,23 +622,35 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
|
600
622
|
if (!buckets.has(key)) buckets.set(key, emptyBucket(sessionDate, encodedCwd));
|
|
601
623
|
foldParsedIntoBucket(buckets.get(key), parsed);
|
|
602
624
|
}
|
|
625
|
+
walkedLast = encodedCwd; // advance only after the directory is fully walked
|
|
603
626
|
}
|
|
604
627
|
|
|
605
|
-
const dates = new Set();
|
|
606
628
|
const entries = [];
|
|
607
629
|
for (const b of buckets.values()) {
|
|
608
630
|
dates.add(b.date);
|
|
609
631
|
entries.push(...bucketToRollupLines(b));
|
|
610
632
|
}
|
|
611
|
-
// Only stamp dates as finalized (safe to trust with no live re-scan)
|
|
612
|
-
//
|
|
613
|
-
// may be missing contributions from directories not yet visited.
|
|
633
|
+
// Only stamp dates as finalized (safe to trust with no live re-scan) once a
|
|
634
|
+
// full cycle has covered every project directory — possibly across calls.
|
|
614
635
|
if (!partial) {
|
|
615
636
|
for (const date of dates) entries.push(finalizedMarkerLine(date));
|
|
616
637
|
}
|
|
617
638
|
|
|
618
639
|
if (!dryRun) {
|
|
619
640
|
await appendRollupAndNotify(entries);
|
|
641
|
+
if (resolvedCursor) {
|
|
642
|
+
try {
|
|
643
|
+
if (partial) {
|
|
644
|
+
if (walkedLast !== lastDir) {
|
|
645
|
+
await atomicFs.writeJsonAtomic(resolvedCursor, { cycleStartedAt, lastDir: walkedLast, dates: Array.from(dates) });
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
await fsp.rm(resolvedCursor, { force: true });
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
// Cursor is an optimisation; a failed write just means a re-walk.
|
|
652
|
+
}
|
|
653
|
+
}
|
|
620
654
|
}
|
|
621
655
|
return { finalizedDates: partial ? [] : Array.from(dates), partial };
|
|
622
656
|
}
|
package/src/main/index.cjs
CHANGED
|
@@ -122,7 +122,6 @@ const heapSnapshot = require('./heapSnapshot.cjs');
|
|
|
122
122
|
const filesIpc = require('./files.cjs');
|
|
123
123
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
124
124
|
const webRemote = require('./webRemote.cjs');
|
|
125
|
-
const { registerRemoteClientHandlers } = require('./remoteClientIpc.cjs');
|
|
126
125
|
const { registerAgentsHandlers } = require('./agentsIpc.cjs');
|
|
127
126
|
const { registerTelemetryHandlers } = require('./telemetryIpc.cjs');
|
|
128
127
|
const { registerUpdaterIpc } = require('./updaterIpc.cjs');
|
|
@@ -627,8 +626,20 @@ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadiness
|
|
|
627
626
|
try {
|
|
628
627
|
await ensureGuardsInstalled(cwd);
|
|
629
628
|
} catch { /* auto-install must never block the readiness probe itself */ }
|
|
629
|
+
const first = await checkDelegationReadiness({ ...payload, cwd });
|
|
630
|
+
const failing = (first?.checks || []).some((c) => !c.ok && (c.id === 'dev-plugin' || c.id === 'scheduler-mcp'));
|
|
631
|
+
if (!failing) return first;
|
|
632
|
+
// Repair at point of use (at most once per check per app run) — the boot seeders give up after 3 attempts.
|
|
633
|
+
try {
|
|
634
|
+
await require('./lib/epic/delegationSelfHeal.cjs').selfHealDelegation({
|
|
635
|
+
checks: first.checks,
|
|
636
|
+
homeDir: os.homedir(),
|
|
637
|
+
deps: { awaitBootSeeding: () => bootSeedingPromise, cwd },
|
|
638
|
+
});
|
|
639
|
+
} catch { /* self-heal never blocks the probe */ }
|
|
630
640
|
return checkDelegationReadiness({ ...payload, cwd });
|
|
631
641
|
}));
|
|
642
|
+
let bootSeedingPromise = Promise.resolve();
|
|
632
643
|
|
|
633
644
|
// Every guard-install attempt is logged from MAIN, not the renderer — a
|
|
634
645
|
// press that never reaches this process (e.g. a renderer talking to an
|
|
@@ -1041,7 +1052,6 @@ git.register(ipcMain);
|
|
|
1041
1052
|
filesIpc.registerFilesHandlers();
|
|
1042
1053
|
registerDocEditHandlers();
|
|
1043
1054
|
webRemote.registerRemoteHandlers();
|
|
1044
|
-
registerRemoteClientHandlers(() => mainWindow);
|
|
1045
1055
|
registerAgentsHandlers({ ipcMain, getWindow: () => mainWindow });
|
|
1046
1056
|
try {
|
|
1047
1057
|
registerUpdaterIpc({ ipcMain, getWindow: () => mainWindow, ptyManager });
|
|
@@ -1455,7 +1465,7 @@ app.whenReady().then(async () => {
|
|
|
1455
1465
|
// Seeders run through ONE serial chain (runSeeders.cjs): plugin install and
|
|
1456
1466
|
// `mcp add --scope user` both write Claude's config files, and concurrent
|
|
1457
1467
|
// writes can drop an entry. Fire-and-forget relative to window creation.
|
|
1458
|
-
runSeedersSerially([
|
|
1468
|
+
bootSeedingPromise = runSeedersSerially([
|
|
1459
1469
|
{ name: 'dev-plugin', run: () => seedDevPlugin({ logger: console, writeLog: logs.writeLine }) },
|
|
1460
1470
|
{ name: 'agent-personas', run: () => seedAgentPersonas({ logger: console, writeLog: logs.writeLine }) },
|
|
1461
1471
|
{ name: 'scheduler-mcp', run: () => seedSchedulerMcp({ logger: console, writeLog: logs.writeLine }) },
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -450,6 +450,18 @@ const schedulerCreatePrd = z.object({
|
|
|
450
450
|
// computed target (from the SAME backlog tree the UI renders — see
|
|
451
451
|
// lib/backlogTree.ts); the main-process handler only validates the rewrite
|
|
452
452
|
// is safe (prdDisposition.cjs), never re-derives "the" terminal itself.
|
|
453
|
+
// Batch create (createPrdBatch): up to 40 entries, each the exact single-create
|
|
454
|
+
// shape. Cross-entry rules (cycles, duplicate slugs, dependsOn naming siblings)
|
|
455
|
+
// live in prdCreate.cjs; this only bounds the envelope.
|
|
456
|
+
const PRD_BATCH_MAX = 40;
|
|
457
|
+
const adminCreatePrds = z.object({
|
|
458
|
+
prds: z.array(schedulerCreatePrd).min(1).max(PRD_BATCH_MAX),
|
|
459
|
+
cwd: z.string().min(1).max(4096).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
460
|
+
originClaudeSessionId: z.string().min(1).max(200).optional(),
|
|
461
|
+
originProjectRoot: z.string().min(1).max(4096).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
462
|
+
sourcePromptId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
463
|
+
});
|
|
464
|
+
|
|
453
465
|
const scheduleSetPrdDisposition = z.object({
|
|
454
466
|
slug: z.string().regex(SCHEDULE_SLUG_RE),
|
|
455
467
|
cwd: z.string().min(1).max(4096).optional(),
|
|
@@ -554,9 +566,18 @@ const adminUpdatePrd = z.object({
|
|
|
554
566
|
(s) => Buffer.byteLength(s, 'utf8') <= PRD_WRITE_MAX_BYTES,
|
|
555
567
|
`body must be ≤ ${PRD_WRITE_MAX_BYTES} bytes`,
|
|
556
568
|
).optional(),
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
569
|
+
// Structured section patches, applied via prdBodyPatch.cjs (re-validated and
|
|
570
|
+
// re-rendered). Mutually exclusive with `body` (enforced in updatePrd too).
|
|
571
|
+
gate: z.array(z.string().min(1).max(2000)).min(1).max(50).optional(),
|
|
572
|
+
files: z.array(z.string().min(1).max(1000)).min(1).max(200).optional(),
|
|
573
|
+
acceptanceCriteria: z.array(z.string().min(1).max(2000)).min(1).max(100).optional(),
|
|
574
|
+
}).strict().refine(
|
|
575
|
+
(it) => (it.frontmatter && Object.keys(it.frontmatter).length > 0) || it.body !== undefined
|
|
576
|
+
|| it.gate !== undefined || it.files !== undefined || it.acceptanceCriteria !== undefined,
|
|
577
|
+
'at least one of frontmatter, body, gate, files or acceptanceCriteria is required',
|
|
578
|
+
).refine(
|
|
579
|
+
(it) => it.body === undefined || (it.gate === undefined && it.files === undefined && it.acceptanceCriteria === undefined),
|
|
580
|
+
'body cannot be combined with gate, files or acceptanceCriteria',
|
|
560
581
|
);
|
|
561
582
|
|
|
562
583
|
const adminCancelJob = z.object({
|
|
@@ -1188,7 +1209,7 @@ function validated(schema, handler) {
|
|
|
1188
1209
|
// Commands are split into three tiers:
|
|
1189
1210
|
// READ_COMMANDS — return data; allowed when remoteEnabled=true.
|
|
1190
1211
|
// SAS_GATED_READS — return sensitive user data (sessions, PRDs, logs,
|
|
1191
|
-
// transcript summaries); additionally require
|
|
1212
|
+
// transcript summaries, machine hostname); additionally require
|
|
1192
1213
|
// _e2eAuthenticated=true (SAS confirmed by user).
|
|
1193
1214
|
// A compromised relay cannot exfiltrate this data from
|
|
1194
1215
|
// a session that has not been SAS-confirmed.
|
|
@@ -1198,22 +1219,17 @@ function validated(schema, handler) {
|
|
|
1198
1219
|
// ALLOWED_COMMANDS is the union, kept for existing import compatibility.
|
|
1199
1220
|
//
|
|
1200
1221
|
// Ungated READ_COMMANDS (justify each):
|
|
1201
|
-
// cmd:app:version — exposes only the app semver string; no user data.
|
|
1202
|
-
// cmd:app:info — exposes only app semver + OS platform string; no user data.
|
|
1203
1222
|
// cmd:session:unsubscribe — teardown lifecycle; returns nothing sensitive.
|
|
1204
1223
|
const READ_COMMANDS = new Set([
|
|
1205
|
-
'cmd:app:version',
|
|
1206
|
-
'cmd:app:info',
|
|
1207
1224
|
// v2 mobile: unsubscribe is a teardown lifecycle call with no data payload.
|
|
1208
1225
|
'cmd:session:unsubscribe',
|
|
1209
1226
|
]);
|
|
1210
1227
|
|
|
1211
1228
|
// Sensitive reads — return user data; require SAS confirmation same as MUTATE.
|
|
1212
1229
|
const SAS_GATED_READS = new Set([
|
|
1213
|
-
'
|
|
1214
|
-
//
|
|
1215
|
-
'cmd:
|
|
1216
|
-
'cmd:history:aggregate',
|
|
1230
|
+
// Web app's multi-desktop picker: { version, platform, hostname }. The
|
|
1231
|
+
// hostname identifies the machine, so this is SAS-gated.
|
|
1232
|
+
'cmd:app:info',
|
|
1217
1233
|
// subscribe initiates a live stream of session state/summary — sensitive.
|
|
1218
1234
|
'cmd:session:subscribe',
|
|
1219
1235
|
// Picker data (projects, personas, tags, macros) for the phone's New Session.
|
|
@@ -1238,17 +1254,8 @@ const SAS_GATED_READS = new Set([
|
|
|
1238
1254
|
]);
|
|
1239
1255
|
|
|
1240
1256
|
const MUTATE_COMMANDS = new Set([
|
|
1241
|
-
'cmd:sessions:save',
|
|
1242
|
-
'cmd:pty:spawn',
|
|
1243
|
-
'cmd:pty:write',
|
|
1244
|
-
// pty:kill terminates a live session; pty:resize drives the geometry of the
|
|
1245
|
-
// user's interactive PTY — both write live process state, so they are gated
|
|
1246
|
-
// behind remoteControlEnabled + SAS like every other mutation. A read-only
|
|
1247
|
-
// mobile mirror has no business killing or resizing the desktop's session.
|
|
1248
|
-
'cmd:pty:kill',
|
|
1249
|
-
'cmd:pty:resize',
|
|
1250
1257
|
// Pushes a prompt into an open tab's chat queue — a real mutation of that
|
|
1251
|
-
// tab's session,
|
|
1258
|
+
// tab's session, a mutation.
|
|
1252
1259
|
'cmd:chat:send',
|
|
1253
1260
|
// Starts a new Session on the desktop via the renderer's launchSession.
|
|
1254
1261
|
'cmd:session:create',
|
|
@@ -1299,6 +1306,7 @@ module.exports = {
|
|
|
1299
1306
|
scheduleReadLog,
|
|
1300
1307
|
scheduleWritePrd,
|
|
1301
1308
|
schedulerCreatePrd,
|
|
1309
|
+
adminCreatePrds,
|
|
1302
1310
|
scheduleSetPrdDisposition,
|
|
1303
1311
|
scheduleArchivePrd,
|
|
1304
1312
|
scheduleRetagPrd,
|
|
@@ -14,7 +14,8 @@ const os = require('node:os');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('./git/gitWorktree.cjs');
|
|
16
16
|
const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
17
|
-
const
|
|
17
|
+
const cwdClassify = require('./cwdClassify.cjs');
|
|
18
|
+
const { worktreeMainRootOf } = cwdClassify;
|
|
18
19
|
|
|
19
20
|
const HOME = os.homedir();
|
|
20
21
|
const TMPDIR = os.tmpdir();
|
|
@@ -55,7 +56,7 @@ const TAIL_BYTES = 64 * 1024;
|
|
|
55
56
|
// and leave the tail with no complete `cwd` row. Only on that miss the tail
|
|
56
57
|
// grows (x16 per step) up to MAX_TAIL_BYTES, then up to MAX_FALLBACK_FILES
|
|
57
58
|
// newest transcripts are tried before the project is given up on.
|
|
58
|
-
const MAX_TAIL_BYTES =
|
|
59
|
+
const MAX_TAIL_BYTES = 1024 * 1024;
|
|
59
60
|
const MAX_FALLBACK_FILES = 5;
|
|
60
61
|
const TAIL_GROWTH = 16;
|
|
61
62
|
|
|
@@ -87,11 +88,45 @@ const CACHE_TTL_MS = 120_000;
|
|
|
87
88
|
// Bounds how many project dirs the async rescan reads concurrently.
|
|
88
89
|
const RESCAN_CONCURRENCY = 32;
|
|
89
90
|
const EMPTY_CWDS = Object.freeze([]);
|
|
90
|
-
|
|
91
|
+
// projectsDir -> { dirMtimeMs, cachedAt, records, dirs, refreshPromise }
|
|
92
|
+
// `dirs` is Map<projectDirName, {rawCwd, newestMtimeMs, newestFile, newestSize}> — the
|
|
93
|
+
// per-dir record cache that lets a changed projectsDir rescan only the added dirs.
|
|
94
|
+
const rawScanCache = new Map();
|
|
95
|
+
|
|
96
|
+
// rawCwd -> classifyCwd result. Never holds kind 'unknown' (an unresolvable
|
|
97
|
+
// .git may resolve later). Bounded; cleared when exceeded or on a full first scan.
|
|
98
|
+
const CLASSIFY_MEMO_MAX = 20000;
|
|
99
|
+
const classifyMemo = new Map();
|
|
100
|
+
function classifyMemoized(rawCwd) {
|
|
101
|
+
const hit = classifyMemo.get(rawCwd);
|
|
102
|
+
if (hit) return hit;
|
|
103
|
+
const classified = cwdClassify.classifyCwd(rawCwd);
|
|
104
|
+
if (classified.kind !== 'unknown') {
|
|
105
|
+
if (classifyMemo.size >= CLASSIFY_MEMO_MAX) classifyMemo.clear();
|
|
106
|
+
classifyMemo.set(rawCwd, classified);
|
|
107
|
+
}
|
|
108
|
+
return classified;
|
|
109
|
+
}
|
|
91
110
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Forces the next activeProjectCwds/allProjectCwds call to revalidate. Keeps the
|
|
113
|
+
* per-dir Map and classify memo (a full rescan of thousands of dirs is the cost
|
|
114
|
+
* this exists to avoid): entries are only marked stale so the next scan takes
|
|
115
|
+
* the incremental path. `deep` also re-lists/re-stats every KNOWN dir, tail-reading
|
|
116
|
+
* only those whose newest transcript changed.
|
|
117
|
+
*/
|
|
118
|
+
function bustProjectCwdCache({ deep = false } = {}) {
|
|
119
|
+
for (const entry of rawScanCache.values()) {
|
|
120
|
+
entry.dirMtimeMs = NaN;
|
|
121
|
+
entry.cachedAt = 0;
|
|
122
|
+
if (deep) entry.forceDeep = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Test isolation: the old full clear (cache + classify memo). */
|
|
127
|
+
function __resetProjectCwdCacheForTests() {
|
|
94
128
|
rawScanCache.clear();
|
|
129
|
+
classifyMemo.clear();
|
|
95
130
|
}
|
|
96
131
|
|
|
97
132
|
// The ops-root folder name. A transcript's `cwd` can point INSIDE it whenever
|
|
@@ -119,7 +154,7 @@ const OPS_DIRNAME = OPS_ROOT_DIR;
|
|
|
119
154
|
* resolution) lives in cwdClassify.cjs.
|
|
120
155
|
*/
|
|
121
156
|
function projectRootOf(cwd) {
|
|
122
|
-
return classifyCwd(cwd).projectRoot ?? cwd;
|
|
157
|
+
return cwdClassify.classifyCwd(cwd).projectRoot ?? cwd;
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
/**
|
|
@@ -160,11 +195,19 @@ function lastCwdInLines(lines) {
|
|
|
160
195
|
return null;
|
|
161
196
|
}
|
|
162
197
|
|
|
198
|
+
/** True when the cached entry's newest transcript is unchanged (name, mtime, size). */
|
|
199
|
+
function sameNewest(prev, newest) {
|
|
200
|
+
return !!prev && prev.newestFile === newest.tf && prev.newestMtimeMs === newest.mtimeMs
|
|
201
|
+
&& prev.newestSize === newest.size;
|
|
202
|
+
}
|
|
203
|
+
|
|
163
204
|
/**
|
|
164
|
-
* scanOneProjectDirSync(projDir) → {rawCwd, newestMtimeMs} | null
|
|
205
|
+
* scanOneProjectDirSync(projDir, prev?) → {rawCwd, newestMtimeMs} | null
|
|
165
206
|
* Newest *.jsonl's mtime plus its last parseable `cwd`, for one project dir.
|
|
207
|
+
* With `prev` (a cached entry), the tail read is skipped entirely when the newest
|
|
208
|
+
* transcript's name, mtime and size are unchanged.
|
|
166
209
|
*/
|
|
167
|
-
function scanOneProjectDirSync(projDir) {
|
|
210
|
+
function scanOneProjectDirSync(projDir, prev) {
|
|
168
211
|
let entries;
|
|
169
212
|
try {
|
|
170
213
|
entries = fs.readdirSync(projDir).filter((f) => f.endsWith('.jsonl'));
|
|
@@ -173,22 +216,26 @@ function scanOneProjectDirSync(projDir) {
|
|
|
173
216
|
const files = [];
|
|
174
217
|
for (const tf of entries) {
|
|
175
218
|
const fp = path.join(projDir, tf);
|
|
176
|
-
try { files.push({ fp, mtimeMs:
|
|
219
|
+
try { const st = fs.statSync(fp); files.push({ fp, tf, mtimeMs: st.mtimeMs, size: st.size }); } catch { continue; }
|
|
177
220
|
}
|
|
178
|
-
if (!files.length) return null;
|
|
221
|
+
if (!files.length) return { rawCwd: null, newestFile: null, newestMtimeMs: 0, newestSize: 0 };
|
|
179
222
|
files.sort((x, y) => y.mtimeMs - x.mtimeMs);
|
|
180
223
|
const newestMtimeMs = files[0].mtimeMs;
|
|
224
|
+
const newestFile = files[0].tf;
|
|
225
|
+
const newestSize = files[0].size;
|
|
226
|
+
if (sameNewest(prev, files[0])) return { ...prev, newestMtimeMs };
|
|
181
227
|
|
|
182
228
|
for (const { fp } of files.slice(0, MAX_FALLBACK_FILES)) {
|
|
183
229
|
let size = TAIL_BYTES;
|
|
184
230
|
for (;;) {
|
|
185
231
|
const rawCwd = lastCwdInLines(readTailLines(fp, size));
|
|
186
|
-
if (rawCwd) return { rawCwd, newestMtimeMs };
|
|
232
|
+
if (rawCwd) return { rawCwd, newestMtimeMs, newestFile, newestSize };
|
|
187
233
|
if (size >= MAX_TAIL_BYTES || size >= fileSizeSync(fp)) break;
|
|
188
234
|
size = Math.min(size * TAIL_GROWTH, MAX_TAIL_BYTES);
|
|
189
235
|
}
|
|
190
236
|
}
|
|
191
|
-
|
|
237
|
+
// No cwd found: cache the negative result under the same newest-file fingerprint.
|
|
238
|
+
return { rawCwd: null, newestMtimeMs, newestFile, newestSize };
|
|
192
239
|
}
|
|
193
240
|
|
|
194
241
|
/**
|
|
@@ -237,14 +284,21 @@ function scanProjectsDir(projectsDir) {
|
|
|
237
284
|
let slugs;
|
|
238
285
|
try { slugs = fs.readdirSync(projectsDir); } catch { rawScanCache.delete(projectsDir); return null; }
|
|
239
286
|
|
|
240
|
-
|
|
287
|
+
// Incremental: reuse every already-known dir's entry, scan only added names,
|
|
288
|
+
// drop names that vanished.
|
|
289
|
+
if (!cached) classifyMemo.clear();
|
|
290
|
+
const prevDirs = cached ? cached.dirs : null;
|
|
291
|
+
const deep = !!(cached && cached.forceDeep);
|
|
292
|
+
const dirs = new Map();
|
|
241
293
|
for (const slug of slugs) {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
294
|
+
const known = prevDirs && prevDirs.get(slug);
|
|
295
|
+
if (known && !deep) { dirs.set(slug, known); continue; }
|
|
296
|
+
const rec = scanOneProjectDirSync(path.join(projectsDir, slug), known || undefined);
|
|
297
|
+
if (rec) dirs.set(slug, rec);
|
|
244
298
|
}
|
|
245
299
|
|
|
246
|
-
Object.freeze(
|
|
247
|
-
rawScanCache.set(projectsDir, { dirMtimeMs, cachedAt: now0, records, refreshPromise: null });
|
|
300
|
+
const records = Object.freeze([...dirs.values()].filter((d) => d.rawCwd));
|
|
301
|
+
rawScanCache.set(projectsDir, { dirMtimeMs, cachedAt: now0, records, dirs, refreshPromise: null });
|
|
248
302
|
return records;
|
|
249
303
|
}
|
|
250
304
|
|
|
@@ -271,10 +325,10 @@ async function readTailLinesAsync(filePath, maxBytes) {
|
|
|
271
325
|
}
|
|
272
326
|
|
|
273
327
|
/**
|
|
274
|
-
* scanOneProjectDirAsync(projDir) → Promise<{rawCwd, newestMtimeMs} | null>
|
|
328
|
+
* scanOneProjectDirAsync(projDir, prev?) → Promise<{rawCwd, newestMtimeMs} | null>
|
|
275
329
|
* fs.promises counterpart of scanOneProjectDirSync, used only by the async rescan.
|
|
276
330
|
*/
|
|
277
|
-
async function scanOneProjectDirAsync(projDir) {
|
|
331
|
+
async function scanOneProjectDirAsync(projDir, prev) {
|
|
278
332
|
let entries;
|
|
279
333
|
try {
|
|
280
334
|
entries = (await fsp.readdir(projDir)).filter((f) => f.endsWith('.jsonl'));
|
|
@@ -283,24 +337,28 @@ async function scanOneProjectDirAsync(projDir) {
|
|
|
283
337
|
const files = [];
|
|
284
338
|
for (const tf of entries) {
|
|
285
339
|
const fp = path.join(projDir, tf);
|
|
286
|
-
try { files.push({ fp, mtimeMs:
|
|
340
|
+
try { const st = await fsp.stat(fp); files.push({ fp, tf, mtimeMs: st.mtimeMs, size: st.size }); } catch { continue; }
|
|
287
341
|
}
|
|
288
|
-
if (!files.length) return null;
|
|
342
|
+
if (!files.length) return { rawCwd: null, newestFile: null, newestMtimeMs: 0, newestSize: 0 };
|
|
289
343
|
files.sort((x, y) => y.mtimeMs - x.mtimeMs);
|
|
290
344
|
const newestMtimeMs = files[0].mtimeMs;
|
|
345
|
+
const newestFile = files[0].tf;
|
|
346
|
+
const newestSize = files[0].size;
|
|
347
|
+
if (sameNewest(prev, files[0])) return { ...prev, newestMtimeMs };
|
|
291
348
|
|
|
292
349
|
for (const { fp } of files.slice(0, MAX_FALLBACK_FILES)) {
|
|
293
350
|
let size = TAIL_BYTES;
|
|
294
351
|
for (;;) {
|
|
295
352
|
const rawCwd = lastCwdInLines(await readTailLinesAsync(fp, size));
|
|
296
|
-
if (rawCwd) return { rawCwd, newestMtimeMs };
|
|
353
|
+
if (rawCwd) return { rawCwd, newestMtimeMs, newestFile, newestSize };
|
|
297
354
|
let fileSize = 0;
|
|
298
355
|
try { fileSize = (await fsp.stat(fp)).size; } catch { /* gone: stop growing */ }
|
|
299
356
|
if (size >= MAX_TAIL_BYTES || size >= fileSize) break;
|
|
300
357
|
size = Math.min(size * TAIL_GROWTH, MAX_TAIL_BYTES);
|
|
301
358
|
}
|
|
302
359
|
}
|
|
303
|
-
|
|
360
|
+
// No cwd found: cache the negative result under the same newest-file fingerprint.
|
|
361
|
+
return { rawCwd: null, newestMtimeMs, newestFile, newestSize };
|
|
304
362
|
}
|
|
305
363
|
|
|
306
364
|
/**
|
|
@@ -311,25 +369,27 @@ async function scanOneProjectDirAsync(projDir) {
|
|
|
311
369
|
* (missing dir, unreadable) is left to throw/reject — startAsyncRefresh's
|
|
312
370
|
* caller catches it, logs once, and keeps the old cached record.
|
|
313
371
|
*/
|
|
314
|
-
async function rescanProjectsDirAsync(projectsDir) {
|
|
372
|
+
async function rescanProjectsDirAsync(projectsDir, prevDirs) {
|
|
315
373
|
const dirMtimeMs = (await fsp.stat(projectsDir)).mtimeMs;
|
|
316
374
|
const slugs = await fsp.readdir(projectsDir);
|
|
317
375
|
|
|
318
|
-
const
|
|
376
|
+
const dirs = new Map();
|
|
319
377
|
let nextIdx = 0;
|
|
320
378
|
async function worker() {
|
|
321
379
|
for (;;) {
|
|
322
380
|
const i = nextIdx++;
|
|
323
381
|
if (i >= slugs.length) return;
|
|
324
|
-
const rec = await scanOneProjectDirAsync(
|
|
325
|
-
|
|
382
|
+
const rec = await scanOneProjectDirAsync(
|
|
383
|
+
path.join(projectsDir, slugs[i]), prevDirs && prevDirs.get(slugs[i]),
|
|
384
|
+
);
|
|
385
|
+
if (rec) dirs.set(slugs[i], rec);
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
const workerCount = Math.min(RESCAN_CONCURRENCY, slugs.length);
|
|
329
389
|
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
330
390
|
|
|
331
|
-
Object.freeze(
|
|
332
|
-
return { dirMtimeMs, records };
|
|
391
|
+
const records = Object.freeze([...dirs.values()].filter((d) => d.rawCwd));
|
|
392
|
+
return { dirMtimeMs, records, dirs };
|
|
333
393
|
}
|
|
334
394
|
|
|
335
395
|
/**
|
|
@@ -345,12 +405,12 @@ function startAsyncRefresh(projectsDir) {
|
|
|
345
405
|
if (!entry) return Promise.resolve();
|
|
346
406
|
if (entry.refreshPromise) return entry.refreshPromise;
|
|
347
407
|
|
|
348
|
-
const promise = rescanProjectsDirAsync(projectsDir)
|
|
408
|
+
const promise = rescanProjectsDirAsync(projectsDir, entry.dirs)
|
|
349
409
|
.then((result) => {
|
|
350
410
|
const current = rawScanCache.get(projectsDir);
|
|
351
411
|
if (!current) return; // busted mid-flight; nothing to swap into
|
|
352
412
|
rawScanCache.set(projectsDir, {
|
|
353
|
-
dirMtimeMs: result.dirMtimeMs, cachedAt: Date.now(), records: result.records, refreshPromise: null,
|
|
413
|
+
dirMtimeMs: result.dirMtimeMs, cachedAt: Date.now(), records: result.records, dirs: result.dirs, refreshPromise: null,
|
|
354
414
|
});
|
|
355
415
|
})
|
|
356
416
|
.catch((err) => {
|
|
@@ -394,7 +454,7 @@ function deriveCwds(records, maxAgeMin, maxCwds, tmpDropRoots) {
|
|
|
394
454
|
// Normalize an ops-internal cwd up to its project root BEFORE any of the
|
|
395
455
|
// checks below — the stray-ops-root incident (see OPS_DIRNAME above) got
|
|
396
456
|
// through precisely because such a path is absolute and does exist.
|
|
397
|
-
const classified =
|
|
457
|
+
const classified = classifyMemoized(rawCwd);
|
|
398
458
|
// `unknown` (a .git file we cannot resolve) is non-registrable: registering
|
|
399
459
|
// it would hand every consumer a project whose ops writes are refused.
|
|
400
460
|
if (classified.kind === 'unknown') return;
|
|
@@ -487,5 +547,6 @@ module.exports = {
|
|
|
487
547
|
worktreeMainRootOf,
|
|
488
548
|
isSstHarnessCwd,
|
|
489
549
|
bustProjectCwdCache,
|
|
550
|
+
__resetProjectCwdCacheForTests,
|
|
490
551
|
__waitForPendingRefresh,
|
|
491
552
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point-of-use repair for the two delegation preconditions the boot seeders
|
|
3
|
+
* give up on after MAX_ATTEMPTS: the session-manager-dev plugin and the
|
|
4
|
+
* session-manager-scheduler MCP registration. Called by the
|
|
5
|
+
* app:delegation-readiness handler when either check is failing. Each check id
|
|
6
|
+
* is attempted at most once per app process, regardless of the seed marker.
|
|
7
|
+
* Never throws; failures go to opsErrorLog (scope 'delegationSelfHeal').
|
|
8
|
+
*/
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
|
|
13
|
+
const attempted = new Set();
|
|
14
|
+
|
|
15
|
+
function readSettings(homeDir) {
|
|
16
|
+
try { return JSON.parse(fs.readFileSync(path.join(homeDir, '.claude', 'settings.json'), 'utf8')); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultDeps(homeDir) {
|
|
20
|
+
const dev = require('../../seedDevPlugin.cjs');
|
|
21
|
+
const mcp = require('../../seedSchedulerMcp.cjs');
|
|
22
|
+
return {
|
|
23
|
+
install: (opts) => require('../../pluginInstall.cjs').install(opts),
|
|
24
|
+
mcpRepair: async () => {
|
|
25
|
+
await mcp.runClaudeMcpRemove(); // best-effort: add refuses when a stale entry exists
|
|
26
|
+
return mcp.runClaudeMcpAdd(mcp.serverScriptPath());
|
|
27
|
+
},
|
|
28
|
+
writeMarker: (id, state) => (id === 'dev-plugin' ? dev.writeMarker(state, homeDir) : mcp.writeMarker(state)),
|
|
29
|
+
readSettings,
|
|
30
|
+
log: (entry) => require('../telemetry/opsErrorLog.cjs').appendError(entry),
|
|
31
|
+
awaitBootSeeding: async () => {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ checks: Array<{id:string, ok:boolean}>, homeDir?: string, deps?: object }} args
|
|
37
|
+
*/
|
|
38
|
+
async function selfHealDelegation({ checks, homeDir = os.homedir(), deps = {} } = {}) {
|
|
39
|
+
const d = { ...defaultDeps(homeDir), ...deps };
|
|
40
|
+
const failing = new Set((checks || []).filter((c) => c && !c.ok).map((c) => c.id));
|
|
41
|
+
const todo = ['dev-plugin', 'scheduler-mcp'].filter((id) => failing.has(id) && !attempted.has(id));
|
|
42
|
+
if (!todo.length) return { attempted: [] };
|
|
43
|
+
for (const id of todo) attempted.add(id); // claim synchronously: concurrent probes can't double-run
|
|
44
|
+
try { await d.awaitBootSeeding?.(); } catch { /* boot seeding failures are logged elsewhere */ }
|
|
45
|
+
|
|
46
|
+
const done = [];
|
|
47
|
+
for (const id of todo) {
|
|
48
|
+
try {
|
|
49
|
+
let r;
|
|
50
|
+
if (id === 'dev-plugin') {
|
|
51
|
+
const enabled = d.readSettings(homeDir)?.enabledPlugins?.['session-manager-dev@session-manager'];
|
|
52
|
+
if (enabled === false) continue; // deliberate disable stays
|
|
53
|
+
r = await d.install({ slug: 'session-manager-dev', marketplace: { add: 'bundled', name: 'session-manager' } });
|
|
54
|
+
} else {
|
|
55
|
+
r = await d.mcpRepair();
|
|
56
|
+
}
|
|
57
|
+
done.push(id);
|
|
58
|
+
if (r?.ok) d.writeMarker(id, { done: true, attempts: 0 });
|
|
59
|
+
else throw new Error(r?.error || `exit ${r?.exitCode}`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
try {
|
|
62
|
+
d.log({
|
|
63
|
+
cwd: d.cwd,
|
|
64
|
+
scope: 'delegationSelfHeal',
|
|
65
|
+
level: 'error',
|
|
66
|
+
message: `${id} self-heal failed: ${err?.message ?? err}`,
|
|
67
|
+
meta: { check: id },
|
|
68
|
+
});
|
|
69
|
+
} catch { /* logging is best-effort */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { attempted: done };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _resetForTests() { attempted.clear(); }
|
|
76
|
+
|
|
77
|
+
module.exports = { selfHealDelegation, _resetForTests };
|
|
@@ -382,7 +382,7 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
|
|
|
382
382
|
|
|
383
383
|
const prdDir = resolveEpicPrdWriteDir(cwd, epicId);
|
|
384
384
|
fs.mkdirSync(prdDir, { recursive: true });
|
|
385
|
-
telemetryCounters.trackEpicCreate();
|
|
385
|
+
telemetryCounters.trackEpicCreate({ tag, agentType });
|
|
386
386
|
return { epicId, prdDir, created: true };
|
|
387
387
|
});
|
|
388
388
|
}
|