claude-code-session-manager 0.119.0 → 0.120.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-CdRrehRI.js → AgentLibrary-LBg3mVL8.js} +1 -1
- package/dist/assets/{Button-BTANn6A_.js → Button-C4BN5ISR.js} +1 -1
- package/dist/assets/{DataModel-D4DhKYDV.js → DataModel-CHhjk61s.js} +1 -1
- package/dist/assets/{History-Dd1SM-AR.js → History-CL-T6Lv3.js} +1 -1
- package/dist/assets/{Hooks-BaSxmXkk.js → Hooks-ByWVgXIf.js} +1 -1
- package/dist/assets/{Library-DiRx14Qs.js → Library-QvYVAEmY.js} +1 -1
- package/dist/assets/{ListDetail-DD12P0m2.js → ListDetail-FvNe6fo5.js} +1 -1
- package/dist/assets/{McpServers-B5Zhq3uc.js → McpServers-C7EmPm8B.js} +1 -1
- package/dist/assets/{Memory-D8Uibw65.js → Memory-nyLR4Yw4.js} +1 -1
- package/dist/assets/{Panel-cUxN--SY.js → Panel-thElCbLX.js} +1 -1
- package/dist/assets/{Permissions-CgocoJzX.js → Permissions-FQ9GM6Ca.js} +1 -1
- package/dist/assets/{Plugins-BJPK6dlg.js → Plugins-D9KE7XTI.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Cw0cwJ1w.js → ProvenanceBadge-lAViG8yV.js} +1 -1
- package/dist/assets/{RemoteMachines-BIaTLP0C.js → RemoteMachines-AWpARk7N.js} +1 -1
- package/dist/assets/{SaveBar-DlGYYdsI.js → SaveBar-c15EEGB_.js} +1 -1
- package/dist/assets/{Scheduler-I4qOJsyy.js → Scheduler-FvduP1FG.js} +5 -5
- package/dist/assets/{ScopeSwitcher-hTMYmy6R.js → ScopeSwitcher-BLep1rtB.js} +1 -1
- package/dist/assets/Settings-D1kTUIDB.js +4 -0
- package/dist/assets/{SkillReferenceGraph-D9a_UFm6.js → SkillReferenceGraph-_40_f0t3.js} +1 -1
- package/dist/assets/{Skills-Bzn5Wasd.js → Skills-DaTkSOuU.js} +1 -1
- package/dist/assets/{SystemPrompt-D0LpRn5u.js → SystemPrompt-DxNf-soM.js} +1 -1
- package/dist/assets/{TagLibrary-CgqAdpwI.js → TagLibrary-L6KpBV-x.js} +1 -1
- package/dist/assets/{TiptapBody-u-CUpM2Q.js → TiptapBody-C_XjtJ82.js} +1 -1
- package/dist/assets/{Toggle-Bb6BJMNB.js → Toggle-H0rERKNI.js} +1 -1
- package/dist/assets/{ViewTabs-Ck2NIjLp.js → ViewTabs-ZUWphDat.js} +1 -1
- package/dist/assets/{historyTotals-vNPWEsnZ.js → historyTotals-BxFKOztb.js} +1 -1
- package/dist/assets/{index-Cq-Ppqar.js → index-B5wj0DEX.js} +8 -8
- package/dist/assets/{index-DAMBm3UN.css → index-Dd7UQabV.css} +1 -1
- package/dist/assets/{settingsSchema-D2dkl1uu.js → settingsSchema-BRcYkXJN.js} +1 -1
- package/dist/assets/{skillFrontmatter-CW0PqdDX.js → skillFrontmatter-DFGp4DST.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-C9ya0GpQ.js → useAgentsDirWatch-VvrgalfC.js} +1 -1
- package/dist/assets/{useRemotePeers-4evTc_IE.js → useRemotePeers-mvXKNEE3.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +11 -8
- package/src/main/crashDiagnostics.cjs +18 -13
- package/src/main/health.cjs +129 -4
- package/src/main/index.cjs +57 -18
- package/src/main/ipcSchemas.cjs +3 -3
- package/src/main/lib/activeSessions.cjs +51 -34
- package/src/main/lib/claude/agentModelResolve.cjs +5 -3
- package/src/main/lib/claude/personaToolScope.cjs +68 -0
- package/src/main/lib/epic/archivedTranscript.cjs +91 -0
- package/src/main/lib/epic/archivedTranscriptMigration.cjs +84 -0
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/epicSettle.cjs +21 -5
- package/src/main/lib/epic/epicValidationHook.cjs +7 -0
- package/src/main/lib/git/depHealth.cjs +161 -0
- package/src/main/lib/git/worktreeDeps.cjs +158 -5
- package/src/main/lib/machineProfile.cjs +2 -0
- package/src/main/lib/mcpToolCatalog.cjs +3 -1
- package/src/main/lib/opsRetentionSettings.cjs +183 -0
- package/src/main/lib/prd/prdWorktreeReach.cjs +2 -2
- package/src/main/lib/prd/workTypeLibrary.cjs +7 -3
- package/src/main/lib/process/procGroupRss.cjs +61 -0
- package/src/main/lib/scheduler/queueHistory.cjs +68 -9
- package/src/main/lib/scheduler/runLogRetention.cjs +129 -43
- package/src/main/lib/scheduler/schedulerBatch.cjs +7 -3
- package/src/main/lib/scheduler/staleTmpSweep.cjs +117 -0
- package/src/main/lib/telemetry/auditLog.cjs +51 -6
- package/src/main/lib/telemetry/crashTelemetry.cjs +49 -1
- package/src/main/lib/telemetry/opsErrorLog.cjs +30 -10
- package/src/main/lib/telemetry/telemetryClient.cjs +5 -2
- package/src/main/scheduler/prdParser.cjs +50 -0
- package/src/main/scheduler/prdPaths.cjs +61 -16
- package/src/main/scheduler/reviewLadder.cjs +24 -1
- package/src/main/scheduler.cjs +214 -37
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/main/webRemote.cjs +4 -27
- package/src/preload/api.d.ts +28 -3
- package/src/preload/index.cjs +7 -0
- package/dist/assets/Settings-C8AGOoqw.js +0 -4
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
const { app, powerSaveBlocker, powerMonitor, crashReporter } = require('electron');
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const path = require('node:path');
|
|
29
|
-
const { reportCrash } = require('./lib/telemetry/crashTelemetry.cjs');
|
|
29
|
+
const { reportCrash, classifyUncleanShutdown, reportChildProcessGone } = require('./lib/telemetry/crashTelemetry.cjs');
|
|
30
30
|
const { createLoopDelayMonitor, stallVerdict } = require('./lib/util/loopDelay.cjs');
|
|
31
31
|
|
|
32
32
|
let logs = null;
|
|
@@ -37,6 +37,7 @@ let loopMonitor = null;
|
|
|
37
37
|
let sentinelPath = null;
|
|
38
38
|
let startedAtIso = null;
|
|
39
39
|
let lastSample = null;
|
|
40
|
+
let lastLoop = null;
|
|
40
41
|
|
|
41
42
|
// Warn threshold for total working set across all Electron processes. The OOM
|
|
42
43
|
// journal showed a single renderer hitting ~900 MB anon-rss before the kernel
|
|
@@ -140,6 +141,7 @@ function persistSentinel(clean) {
|
|
|
140
141
|
startedAt: startedAtIso,
|
|
141
142
|
closedAt: clean ? new Date().toISOString() : undefined,
|
|
142
143
|
lastSample,
|
|
144
|
+
loop: lastLoop ? { loopDelayMaxMs: lastLoop.loopDelayMaxMs, loopDelayP99Ms: lastLoop.loopDelayP99Ms } : null,
|
|
143
145
|
};
|
|
144
146
|
try {
|
|
145
147
|
fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
|
|
@@ -152,11 +154,12 @@ function persistSentinel(clean) {
|
|
|
152
154
|
|
|
153
155
|
function heartbeat() {
|
|
154
156
|
lastSample = sampleMemory();
|
|
157
|
+
const loop = loopMonitor ? loopMonitor.snapshot() : null;
|
|
158
|
+
lastLoop = loop;
|
|
155
159
|
// Persist every tick so the final pre-crash sample survives a SIGKILL.
|
|
156
160
|
persistSentinel(false);
|
|
157
161
|
const top = [...lastSample.procs].sort((a, b) => b.mb - a.mb).slice(0, 4);
|
|
158
162
|
const level = lastSample.totalMb >= MEM_WARN_MB ? 'warn' : 'debug';
|
|
159
|
-
const loop = loopMonitor ? loopMonitor.snapshot() : null;
|
|
160
163
|
logs?.writeLine({
|
|
161
164
|
scope: 'crash-diag',
|
|
162
165
|
level,
|
|
@@ -183,10 +186,14 @@ function checkPreviousRun() {
|
|
|
183
186
|
if (!prev || prev.open !== true) return;
|
|
184
187
|
const s = prev.lastSample;
|
|
185
188
|
const lastTop = s && Array.isArray(s.procs) ? [...s.procs].sort((a, b) => b.mb - a.mb).slice(0, 4).map((p) => `${p.type}:${p.mb}MB`) : undefined;
|
|
189
|
+
const lastLoopDelayMaxMs = prev.loop?.loopDelayMaxMs;
|
|
190
|
+
const sampleMs = s?.ts ? Date.parse(s.ts) : NaN;
|
|
191
|
+
const silentGapSec = Number.isFinite(sampleMs) ? Math.round((Date.now() - sampleMs) / 1000) : undefined;
|
|
192
|
+
const likelyCause = classifyUncleanShutdown({ lastLoopDelayMaxMs, lastTotalMb: s?.totalMb, silentGapSec });
|
|
186
193
|
logs?.writeLine({
|
|
187
194
|
scope: 'crash-diag',
|
|
188
195
|
level: 'error',
|
|
189
|
-
message:
|
|
196
|
+
message: `previous session ended UNCLEANLY (likelyCause=${likelyCause}; no graceful quit)`,
|
|
190
197
|
meta: {
|
|
191
198
|
prevPid: prev.pid,
|
|
192
199
|
startedAt: prev.startedAt,
|
|
@@ -195,7 +202,10 @@ function checkPreviousRun() {
|
|
|
195
202
|
lastMainRssMb: s?.main?.rssMb,
|
|
196
203
|
lastTop,
|
|
197
204
|
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
198
|
-
|
|
205
|
+
lastLoopDelayMaxMs,
|
|
206
|
+
silentGapSec,
|
|
207
|
+
likelyCause,
|
|
208
|
+
hint: 'Check `journalctl -k | grep -i oom` around lastSampleTs to confirm the kernel OOM-killer, and the "main-loop stall" heartbeats before lastSampleTs to confirm a wedged main thread.',
|
|
199
209
|
},
|
|
200
210
|
});
|
|
201
211
|
// The unclean-shutdown postmortem is the report most likely to be the ONLY
|
|
@@ -209,6 +219,9 @@ function checkPreviousRun() {
|
|
|
209
219
|
lastMainRssMb: s?.main?.rssMb,
|
|
210
220
|
lastTop,
|
|
211
221
|
powerBlockerHeldAtDeath: s?.powerBlockerHeld,
|
|
222
|
+
lastLoopDelayMaxMs,
|
|
223
|
+
silentGapSec,
|
|
224
|
+
likelyCause,
|
|
212
225
|
},
|
|
213
226
|
});
|
|
214
227
|
}
|
|
@@ -244,15 +257,7 @@ function registerCrashHooks() {
|
|
|
244
257
|
lastTotalMb: lastSample?.totalMb,
|
|
245
258
|
},
|
|
246
259
|
});
|
|
247
|
-
|
|
248
|
-
reason: details?.reason || 'unknown',
|
|
249
|
-
meta: {
|
|
250
|
-
type: details?.type,
|
|
251
|
-
name: details?.name || details?.serviceName,
|
|
252
|
-
exitCode: details?.exitCode,
|
|
253
|
-
lastTotalMb: lastSample?.totalMb,
|
|
254
|
-
},
|
|
255
|
-
});
|
|
260
|
+
reportChildProcessGone({ details, lastTotalMb: lastSample?.totalMb });
|
|
256
261
|
});
|
|
257
262
|
|
|
258
263
|
// powerMonitor ties the suspend story to the crash timeline. If we ever log
|
package/src/main/health.cjs
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('node:path');
|
|
|
10
10
|
const os = require('node:os');
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
const { POLL_INTERVAL_MS, loadGateThreshold } = require('./lib/scheduler/schedulerConfig.cjs');
|
|
13
|
-
const { opsPath } = require('./lib/opsOwnership.cjs');
|
|
13
|
+
const { opsPath, OPS_ROOT_DIR } = require('./lib/opsOwnership.cjs');
|
|
14
14
|
const { effectivePaused } = require('./lib/scheduler/upgradeDrain.cjs');
|
|
15
15
|
const { checkPersonaImports } = require('./lib/persona-macro/personaImportHealth.cjs');
|
|
16
16
|
const { checkDelegationReadiness } = require('./lib/epic/delegationReadiness.cjs');
|
|
@@ -24,6 +24,7 @@ const { classifyQueueStarvation, launchBlockedSlugs, STARVE_ESCALATION_MS } = re
|
|
|
24
24
|
const { REVERIFY_INTERVAL_MS } = require('./scheduler/reviewLadder.cjs');
|
|
25
25
|
const { findStarvedProjects, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('./lib/scheduler/schedulerBatch.cjs');
|
|
26
26
|
const { auditLogPath, readTail } = require('./lib/telemetry/auditLog.cjs');
|
|
27
|
+
const { checkMainDependencyHealth } = require('./lib/git/depHealth.cjs');
|
|
27
28
|
const { resolveBuildIdentity } = require('./lib/buildIdentity.cjs');
|
|
28
29
|
const { computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/scheduler/runLogRetention.cjs');
|
|
29
30
|
const { allProjectCwds } = require('./lib/activeSessions.cjs');
|
|
@@ -399,6 +400,41 @@ function evaluateEpicIndexHealth(cwds) {
|
|
|
399
400
|
return { component: { ok: !anyOrphanRows, byProject }, issues };
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
// Read-only scan for stray `session-manager-operations` dirs nested under a
|
|
404
|
+
// project root (symptom of a writer joining an agent cwd without
|
|
405
|
+
// projectRootOf/opsPath). The top-level one is legitimate; node_modules, .git
|
|
406
|
+
// and .claude/worktrees are skipped. Bounded depth, sync readdir, never writes.
|
|
407
|
+
const OPS_DIR_NAME = OPS_ROOT_DIR;
|
|
408
|
+
function findNestedOpsRoots(projectRoot, { maxDepth = 3 } = {}) {
|
|
409
|
+
const found = [];
|
|
410
|
+
const walk = (dir, depth) => {
|
|
411
|
+
let entries;
|
|
412
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
413
|
+
for (const e of entries) {
|
|
414
|
+
if (!e.isDirectory()) continue; // symlinks are not followed
|
|
415
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
416
|
+
const full = path.join(dir, e.name);
|
|
417
|
+
if (e.name === '.claude') {
|
|
418
|
+
// only .claude/worktrees is skipped; other .claude children still scan
|
|
419
|
+
if (depth + 1 < maxDepth) {
|
|
420
|
+
try {
|
|
421
|
+
for (const c of fs.readdirSync(full, { withFileTypes: true })) {
|
|
422
|
+
if (c.isDirectory() && c.name !== 'worktrees') walk(path.join(full, c.name), depth + 2);
|
|
423
|
+
}
|
|
424
|
+
} catch { /* unreadable */ }
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (e.name === OPS_DIR_NAME) {
|
|
429
|
+
if (depth > 0) { found.push(full); continue; } // stray; don't descend
|
|
430
|
+
}
|
|
431
|
+
if (depth + 1 < maxDepth) walk(full, depth + 1);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
walk(projectRoot, 0);
|
|
435
|
+
return found;
|
|
436
|
+
}
|
|
437
|
+
|
|
402
438
|
// Pure evaluator over ~/.claude/session-manager/scheduler-state.json's parsed
|
|
403
439
|
// content (the sidecar scheduler.cjs's pollLoop maintains for the billing
|
|
404
440
|
// usage-meter / rate-limit-window poller — see rateLimitPollerStreak tests).
|
|
@@ -619,14 +655,56 @@ function latestStarveEscalationReasons(auditLogPath) {
|
|
|
619
655
|
* repo HEAD differs from the installed build. Both are reported, neither is
|
|
620
656
|
* critical (never flips status.ok).
|
|
621
657
|
*/
|
|
622
|
-
|
|
658
|
+
const WORKTREE_SAFETY_PATHS = [
|
|
659
|
+
'src/main/lib/git/worktreeDeps.cjs',
|
|
660
|
+
'src/main/lib/git/depHealth.cjs',
|
|
661
|
+
'src/main/lib/git/gitWorktree.cjs',
|
|
662
|
+
'src/main/lib/git/jobWorktree.cjs',
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
function evaluateBuildFreshness({ running = null, installedOnDisk = null, repoHead = null, safetyCommitsMissing = [] } = {}) {
|
|
623
666
|
const differs = (a, b) => !!a && !!b && !(a.startsWith(b) || b.startsWith(a));
|
|
624
667
|
const restartNeeded = differs(running, installedOnDisk);
|
|
625
668
|
const publishNeeded = differs(repoHead, installedOnDisk);
|
|
626
669
|
const notes = [];
|
|
627
670
|
if (restartNeeded) notes.push(`restart needed: running ${running}, installed ${installedOnDisk}`);
|
|
628
671
|
if (publishNeeded) notes.push(`publish needed: repo HEAD ${repoHead}, installed ${installedOnDisk}`);
|
|
629
|
-
|
|
672
|
+
let safetyIssue;
|
|
673
|
+
if (safetyCommitsMissing.length) {
|
|
674
|
+
const listed = safetyCommitsMissing.slice(0, 3).map((c) => `${c.sha} ${c.subject}`).join('; ');
|
|
675
|
+
const more = safetyCommitsMissing.length > 3 ? `; +${safetyCommitsMissing.length - 3} more` : '';
|
|
676
|
+
safetyIssue = `running build ${running} predates ${safetyCommitsMissing.length} worktree-safety commit(s): ${listed}${more} — publish a new release and restart Session Manager`;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
ok: true, running, installedOnDisk, repoHead, restartNeeded, publishNeeded,
|
|
680
|
+
safetyCommitsMissing, ...(safetyIssue ? { safetyIssue } : {}),
|
|
681
|
+
...(notes.length ? { note: notes.join('; ') } : {}),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* evaluateMainDepLinks(results) → { component, issues }
|
|
687
|
+
*
|
|
688
|
+
* Pure over `Array<{ cwd, health }>` (health = checkMainDependencyHealth's
|
|
689
|
+
* return). One issue per project whose node_modules links point into a job
|
|
690
|
+
* worktree or dangle. Non-critical: component.ok is always true.
|
|
691
|
+
*/
|
|
692
|
+
function evaluateMainDepLinks(results = []) {
|
|
693
|
+
const list = Array.isArray(results) ? results : [];
|
|
694
|
+
const broken = [];
|
|
695
|
+
const issues = [];
|
|
696
|
+
for (const { cwd, health } of list) {
|
|
697
|
+
if (!health || health.ok !== false) continue;
|
|
698
|
+
if (health.reason !== 'links-into-worktree' && health.reason !== 'dangling-links') continue;
|
|
699
|
+
const missing = Array.isArray(health.missing) ? health.missing : [];
|
|
700
|
+
broken.push({ cwd, reason: health.reason, missing });
|
|
701
|
+
const shown = missing.slice(0, 5).join(', ');
|
|
702
|
+
const more = missing.length > 5 ? `, +${missing.length - 5} more` : '';
|
|
703
|
+
issues.push(
|
|
704
|
+
`${cwd}: node_modules ${health.reason}${shown ? ` (${shown}${more})` : ''} — fix: cd ${cwd} && CI=1 pnpm install --offline --frozen-lockfile`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return { component: { ok: true, checked: list.length, broken }, issues };
|
|
630
708
|
}
|
|
631
709
|
|
|
632
710
|
/**
|
|
@@ -1234,6 +1312,18 @@ async function check(opts = {}) {
|
|
|
1234
1312
|
status.components.epic_transcripts = { ok: true, error: e.message };
|
|
1235
1313
|
}
|
|
1236
1314
|
|
|
1315
|
+
// 6.665. Nested ops roots (NON-fatal WARN): stray session-manager-operations
|
|
1316
|
+
// dirs under this project. Read-only; never deletes.
|
|
1317
|
+
try {
|
|
1318
|
+
const nested = findNestedOpsRoots(PROJECT_ROOT, { maxDepth: 3 });
|
|
1319
|
+
status.components.nested_ops_roots = { ok: true, count: nested.length, paths: nested };
|
|
1320
|
+
if (nested.length > 0) {
|
|
1321
|
+
status.issues.push(`Warning: ${nested.length} nested session-manager-operations dir(s): ${nested.join(', ')}`);
|
|
1322
|
+
}
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
status.components.nested_ops_roots = { ok: true, error: e.message };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1237
1327
|
// 6.7. Build freshness (informational): running heartbeat vs installed
|
|
1238
1328
|
// build-info vs repo HEAD.
|
|
1239
1329
|
try {
|
|
@@ -1243,15 +1333,47 @@ async function check(opts = {}) {
|
|
|
1243
1333
|
timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
1244
1334
|
}).trim() || null;
|
|
1245
1335
|
} catch { /* not a git checkout */ }
|
|
1336
|
+
const running = readFreshHeartbeat(schedulerPaths.heartbeatPath())?.build?.codeSha ?? null;
|
|
1337
|
+
let safetyCommitsMissing = [];
|
|
1338
|
+
if (typeof running === 'string' && running && typeof repoHead === 'string' && repoHead) {
|
|
1339
|
+
try {
|
|
1340
|
+
safetyCommitsMissing = execFileSync(
|
|
1341
|
+
'git',
|
|
1342
|
+
['-C', PROJECT_ROOT, 'log', '--format=%h%x09%s', `${running}..HEAD`, '--', ...WORKTREE_SAFETY_PATHS],
|
|
1343
|
+
{ timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
1344
|
+
).split('\n').filter(Boolean).map((line) => {
|
|
1345
|
+
const i = line.indexOf('\t');
|
|
1346
|
+
return { sha: line.slice(0, i), subject: line.slice(i + 1) };
|
|
1347
|
+
});
|
|
1348
|
+
} catch { safetyCommitsMissing = []; }
|
|
1349
|
+
}
|
|
1246
1350
|
status.components.build = evaluateBuildFreshness({
|
|
1247
|
-
running
|
|
1351
|
+
running,
|
|
1248
1352
|
installedOnDisk: resolveBuildIdentity().codeSha ?? null,
|
|
1249
1353
|
repoHead,
|
|
1354
|
+
safetyCommitsMissing,
|
|
1250
1355
|
});
|
|
1356
|
+
if (status.components.build.safetyIssue) status.issues.push(status.components.build.safetyIssue);
|
|
1251
1357
|
} catch (e) {
|
|
1252
1358
|
status.components.build = { ok: true, error: e.message };
|
|
1253
1359
|
}
|
|
1254
1360
|
|
|
1361
|
+
// 6.75. Main-checkout dependency links: node_modules entries pointing into a
|
|
1362
|
+
// job worktree or dangling, for every cwd the queue knows. Non-critical.
|
|
1363
|
+
if (queueState) {
|
|
1364
|
+
try {
|
|
1365
|
+
const cwds = [...new Set(Object.values(queueState.jobs || {})
|
|
1366
|
+
.map((j) => j && j.cwd).filter((c) => typeof c === 'string' && c))].slice(0, 50);
|
|
1367
|
+
const results = cwds.filter((cwd) => fs.existsSync(cwd))
|
|
1368
|
+
.map((cwd) => ({ cwd, health: checkMainDependencyHealth(cwd) }));
|
|
1369
|
+
const { component, issues } = evaluateMainDepLinks(results);
|
|
1370
|
+
status.components.main_dep_links = component;
|
|
1371
|
+
status.issues.push(...issues);
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
status.components.main_dep_links = { ok: true, error: e.message };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1255
1377
|
// 6.8. Credential health (informational-but-loud): is the Claude OAuth token
|
|
1256
1378
|
// usable? An expired/absent token silently voids every headless `claude -p`
|
|
1257
1379
|
// job — the queue "runs" but nothing authenticates. refreshIfNeeded() reads
|
|
@@ -1297,6 +1419,7 @@ module.exports = {
|
|
|
1297
1419
|
evaluateCredentialHealth,
|
|
1298
1420
|
computeEpicIndexDrift,
|
|
1299
1421
|
evaluateEpicIndexHealth,
|
|
1422
|
+
findNestedOpsRoots,
|
|
1300
1423
|
computeProjectProblemCounts,
|
|
1301
1424
|
evaluateWorktreeCapBlocked,
|
|
1302
1425
|
evaluatePerProjectStall,
|
|
@@ -1309,6 +1432,8 @@ module.exports = {
|
|
|
1309
1432
|
evaluateBlockingParkHealth,
|
|
1310
1433
|
evaluateStarveEscalationHealth,
|
|
1311
1434
|
evaluateBuildFreshness,
|
|
1435
|
+
evaluateMainDepLinks,
|
|
1436
|
+
WORKTREE_SAFETY_PATHS,
|
|
1312
1437
|
latestStarveEscalationReasons,
|
|
1313
1438
|
DISPATCH_STALL_THRESHOLD_MS,
|
|
1314
1439
|
DISPATCH_WARN_THRESHOLD_MS,
|
package/src/main/index.cjs
CHANGED
|
@@ -1007,6 +1007,7 @@ pluginInstall.registerPluginInstallHandlers();
|
|
|
1007
1007
|
memoryTool.registerMemoryHandlers();
|
|
1008
1008
|
activeIndexMerge.registerActiveIndexMergeHandlers();
|
|
1009
1009
|
promptSessionsCreateEpic.registerPromptSessionsCreateEpicHandlers();
|
|
1010
|
+
require('./lib/epic/archivedTranscript.cjs').registerIpc(ipcMain);
|
|
1010
1011
|
customerFeedbackIpc.registerCustomerFeedbackHandlers();
|
|
1011
1012
|
epicWorktreeMint.registerEpicWorktreeMintHandlers();
|
|
1012
1013
|
epicWorktreeMerge.registerEpicWorktreeMergeHandlers();
|
|
@@ -1044,6 +1045,11 @@ try {
|
|
|
1044
1045
|
} catch (e) {
|
|
1045
1046
|
logs.writeLine({ scope: 'main', level: 'error', message: 'updater IPC registration failed', meta: { error: e?.message } });
|
|
1046
1047
|
}
|
|
1048
|
+
try {
|
|
1049
|
+
require('./lib/opsRetentionSettings.cjs').registerIpc(ipcMain);
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
logs.writeLine({ scope: 'main', level: 'error', message: 'ops retention IPC registration failed', meta: { error: e?.message } });
|
|
1052
|
+
}
|
|
1047
1053
|
chatRunner.registerChatHandlers();
|
|
1048
1054
|
|
|
1049
1055
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
|
@@ -1110,9 +1116,11 @@ const SMFILE_MIME = {
|
|
|
1110
1116
|
// E2E tests also skip so playwright.electron.launch can run multiple specs
|
|
1111
1117
|
// back-to-back without the prior app's lock causing the next launch to quit.
|
|
1112
1118
|
const isDev = process.env.SM_DEV === '1' || process.env.SM_E2E === '1';
|
|
1119
|
+
let lostInstanceLock = false;
|
|
1113
1120
|
if (!isDev) {
|
|
1114
1121
|
const gotLock = app.requestSingleInstanceLock();
|
|
1115
1122
|
if (!gotLock) {
|
|
1123
|
+
lostInstanceLock = true;
|
|
1116
1124
|
terminateLosingInstance(app);
|
|
1117
1125
|
} else {
|
|
1118
1126
|
app.on('second-instance', () => {
|
|
@@ -1125,6 +1133,31 @@ if (!isDev) {
|
|
|
1125
1133
|
}
|
|
1126
1134
|
}
|
|
1127
1135
|
|
|
1136
|
+
function runRunLogRetentionPass() {
|
|
1137
|
+
if (lostInstanceLock) return;
|
|
1138
|
+
try {
|
|
1139
|
+
const schedulerPaths = require('./lib/scheduler/schedulerPaths.cjs');
|
|
1140
|
+
const staleTmpSweep = require('./lib/scheduler/staleTmpSweep.cjs');
|
|
1141
|
+
const result = runLogRetention.runBootSweep();
|
|
1142
|
+
const plansRoot = schedulerPaths.scheduledPlansRoot();
|
|
1143
|
+
const tmp = staleTmpSweep.sweepStaleTmp(plansRoot);
|
|
1144
|
+
const archive = staleTmpSweep.pruneRunsArchive(path.join(plansRoot, 'runs-archive'));
|
|
1145
|
+
logs.writeLine({
|
|
1146
|
+
scope: 'run-log-retention',
|
|
1147
|
+
level: 'info',
|
|
1148
|
+
message: result.deleted ? 'sweep applied' : 'sweep dry-run',
|
|
1149
|
+
meta: {
|
|
1150
|
+
deleted: result.deleted,
|
|
1151
|
+
removedFiles: (result.removedFiles || 0) + tmp.removedFiles + archive.removedFiles,
|
|
1152
|
+
freedBytes: (result.freedBytes || 0) + tmp.freedBytes + archive.freedBytes,
|
|
1153
|
+
errorCount: (result.errors ? result.errors.length : 0) + tmp.errors.length + archive.errors.length,
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
logs.writeLine({ scope: 'run-log-retention', level: 'error', message: 'sweep failed', meta: { error: e?.message } });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1128
1161
|
// Global webContents hardening — applies to DevTools, future webContents, etc.
|
|
1129
1162
|
// Must be registered before app.whenReady so it captures the first window too.
|
|
1130
1163
|
app.on('web-contents-created', (_e, wc) => {
|
|
@@ -1454,25 +1487,31 @@ app.whenReady().then(async () => {
|
|
|
1454
1487
|
logs.writeLine({ scope: 'history-rollup', level: 'error', message: 'finalizeClosedDays failed', meta: { error: e?.message } });
|
|
1455
1488
|
});
|
|
1456
1489
|
runIntradayRefresh();
|
|
1457
|
-
// Run-log retention sweep:
|
|
1458
|
-
//
|
|
1490
|
+
// Run-log retention sweep: runs once 30s after boot, then every 24h (a
|
|
1491
|
+
// long-lived app would otherwise never trim). Skipped when this process
|
|
1492
|
+
// lost the single-instance lock (lostInstanceLock, set beside
|
|
1493
|
+
// terminateLosingInstance above). Read-only unless config.schedulerRunLogRetention
|
|
1459
1494
|
// opts in (see runLogRetention.cjs's safety model) — non-fatal on error.
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1495
|
+
runRunLogRetentionPass();
|
|
1496
|
+
const retentionTimer = setInterval(runRunLogRetentionPass, 24 * 60 * 60 * 1000);
|
|
1497
|
+
if (retentionTimer.unref) retentionTimer.unref();
|
|
1498
|
+
// Archived-transcript migration: move embedded `transcript` strings into gzip
|
|
1499
|
+
// sidecars, <=50 files per project per boot, yielding between files. Uses the
|
|
1500
|
+
// cached allProjectCwds() list (never a fresh ~/.claude/projects scan).
|
|
1501
|
+
(async () => {
|
|
1502
|
+
try {
|
|
1503
|
+
const { migrateProject } = require('./lib/epic/archivedTranscriptMigration.cjs');
|
|
1504
|
+
for (const projectCwd of allProjectCwds()) {
|
|
1505
|
+
const r = await migrateProject(projectCwd, { limit: 50, yieldFn: () => new Promise((res) => setImmediate(res)) });
|
|
1506
|
+
if (r.migrated || r.errors.length) {
|
|
1507
|
+
logs.writeLine({ scope: 'main', level: r.errors.length ? 'warn' : 'info', message: 'archived transcript migration', meta: { projectCwd, migrated: r.migrated, freedBytes: r.freedBytes, errors: r.errors.length } });
|
|
1508
|
+
}
|
|
1509
|
+
await new Promise((res) => setImmediate(res));
|
|
1510
|
+
}
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
logs.writeLine({ scope: 'main', level: 'error', message: 'archived transcript migration failed', meta: { error: e?.message } });
|
|
1513
|
+
}
|
|
1514
|
+
})();
|
|
1476
1515
|
}, 30_000);
|
|
1477
1516
|
// Keep TODAY's rollup line current so the History dashboard never needs to
|
|
1478
1517
|
// fall back to a live transcript scan. Shares the same O_EXCL lock file as
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -415,9 +415,9 @@ const schedulerCreatePrd = z.object({
|
|
|
415
415
|
// User-selected work-type tag (PRD 774; independence from the parent
|
|
416
416
|
// Epic's own tag confirmed PRD 1041) — deterministic, never LLM-classified.
|
|
417
417
|
// Epic tag and PRD tag draw on the same WorkType vocabulary
|
|
418
|
-
// (lib/prd/workTypeLibrary.cjs) but are independent values; 'discussion'
|
|
418
|
+
// (lib/prd/workTypeLibrary.cjs) but are independent values; 'discussion' and 'portfolio' are
|
|
419
419
|
// excluded here for the same reason adminPrdFrontmatterPatch excludes it
|
|
420
|
-
// below — a discussion-tagged Epic never reaches PRD authoring.
|
|
420
|
+
// below — a discussion- or portfolio-tagged Epic never reaches PRD authoring.
|
|
421
421
|
tag: PrdWorkTypeSchema.optional(),
|
|
422
422
|
// WHO executes this PRD (persona name, `~/.claude/agents/<name>.md`) —
|
|
423
423
|
// independent of `tag` above. Optional: prdCreate.cjs's buildPrdBody fills
|
|
@@ -516,7 +516,7 @@ const adminPrdFrontmatterPatch = z.object({
|
|
|
516
516
|
parallelGroup: z.number().int().min(0).max(999999).optional(),
|
|
517
517
|
sourcePromptId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
518
518
|
sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
519
|
-
// 'discussion' deliberately excluded: matches prdFrontmatter.cjs's
|
|
519
|
+
// 'discussion' and 'portfolio' deliberately excluded: matches prdFrontmatter.cjs's
|
|
520
520
|
// parsePrdFile, which never parses that value for a PRD's tag key (a
|
|
521
521
|
// discussion-tagged Epic never reaches PRD authoring) — see
|
|
522
522
|
// workTypeLibrary.cjs's PrdWorkTypeSchema.
|
|
@@ -51,6 +51,13 @@ function isSstHarnessCwd(cwd) {
|
|
|
51
51
|
// Transcript reads only need the last line with a `cwd` field — a few dozen
|
|
52
52
|
// lines is always enough. 64 KB keeps peak RSS proportionate.
|
|
53
53
|
const TAIL_BYTES = 64 * 1024;
|
|
54
|
+
// A single transcript line (base64 render, big tool result) can exceed TAIL_BYTES
|
|
55
|
+
// and leave the tail with no complete `cwd` row. Only on that miss the tail
|
|
56
|
+
// grows (x16 per step) up to MAX_TAIL_BYTES, then up to MAX_FALLBACK_FILES
|
|
57
|
+
// newest transcripts are tried before the project is given up on.
|
|
58
|
+
const MAX_TAIL_BYTES = 16 * 1024 * 1024;
|
|
59
|
+
const MAX_FALLBACK_FILES = 5;
|
|
60
|
+
const TAIL_GROWTH = 16;
|
|
54
61
|
|
|
55
62
|
// Safety cap on distinct cwds to bound result set size. O(1) extra space.
|
|
56
63
|
const MAX_CWDS = 50;
|
|
@@ -139,6 +146,20 @@ function readTailLines(filePath, maxBytes) {
|
|
|
139
146
|
return buf.toString('utf8').split('\n').filter((l) => l.trim());
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
function fileSizeSync(fp) {
|
|
150
|
+
try { return fs.statSync(fp).size; } catch { return 0; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** lastCwdInLines(lines) → the `cwd` of the last parseable row that has one, or null. */
|
|
154
|
+
function lastCwdInLines(lines) {
|
|
155
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
156
|
+
let row;
|
|
157
|
+
try { row = JSON.parse(lines[i]); } catch { continue; }
|
|
158
|
+
if (row && row.cwd) return row.cwd;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
/**
|
|
143
164
|
* scanOneProjectDirSync(projDir) → {rawCwd, newestMtimeMs} | null
|
|
144
165
|
* Newest *.jsonl's mtime plus its last parseable `cwd`, for one project dir.
|
|
@@ -149,28 +170,25 @@ function scanOneProjectDirSync(projDir) {
|
|
|
149
170
|
entries = fs.readdirSync(projDir).filter((f) => f.endsWith('.jsonl'));
|
|
150
171
|
} catch { return null; }
|
|
151
172
|
|
|
152
|
-
|
|
153
|
-
let newestMtimeMs = 0;
|
|
173
|
+
const files = [];
|
|
154
174
|
for (const tf of entries) {
|
|
155
175
|
const fp = path.join(projDir, tf);
|
|
156
|
-
try {
|
|
157
|
-
const st = fs.statSync(fp);
|
|
158
|
-
if (st.mtimeMs > newestMtimeMs) {
|
|
159
|
-
newestMtimeMs = st.mtimeMs;
|
|
160
|
-
newestPath = fp;
|
|
161
|
-
}
|
|
162
|
-
} catch { continue; }
|
|
176
|
+
try { files.push({ fp, mtimeMs: fs.statSync(fp).mtimeMs }); } catch { continue; }
|
|
163
177
|
}
|
|
164
|
-
if (!
|
|
178
|
+
if (!files.length) return null;
|
|
179
|
+
files.sort((x, y) => y.mtimeMs - x.mtimeMs);
|
|
180
|
+
const newestMtimeMs = files[0].mtimeMs;
|
|
165
181
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
for (const { fp } of files.slice(0, MAX_FALLBACK_FILES)) {
|
|
183
|
+
let size = TAIL_BYTES;
|
|
184
|
+
for (;;) {
|
|
185
|
+
const rawCwd = lastCwdInLines(readTailLines(fp, size));
|
|
186
|
+
if (rawCwd) return { rawCwd, newestMtimeMs };
|
|
187
|
+
if (size >= MAX_TAIL_BYTES || size >= fileSizeSync(fp)) break;
|
|
188
|
+
size = Math.min(size * TAIL_GROWTH, MAX_TAIL_BYTES);
|
|
189
|
+
}
|
|
172
190
|
}
|
|
173
|
-
return
|
|
191
|
+
return null;
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
/**
|
|
@@ -262,28 +280,27 @@ async function scanOneProjectDirAsync(projDir) {
|
|
|
262
280
|
entries = (await fsp.readdir(projDir)).filter((f) => f.endsWith('.jsonl'));
|
|
263
281
|
} catch { return null; }
|
|
264
282
|
|
|
265
|
-
|
|
266
|
-
let newestMtimeMs = 0;
|
|
283
|
+
const files = [];
|
|
267
284
|
for (const tf of entries) {
|
|
268
285
|
const fp = path.join(projDir, tf);
|
|
269
|
-
try {
|
|
270
|
-
const st = await fsp.stat(fp);
|
|
271
|
-
if (st.mtimeMs > newestMtimeMs) {
|
|
272
|
-
newestMtimeMs = st.mtimeMs;
|
|
273
|
-
newestPath = fp;
|
|
274
|
-
}
|
|
275
|
-
} catch { continue; }
|
|
286
|
+
try { files.push({ fp, mtimeMs: (await fsp.stat(fp)).mtimeMs }); } catch { continue; }
|
|
276
287
|
}
|
|
277
|
-
if (!
|
|
288
|
+
if (!files.length) return null;
|
|
289
|
+
files.sort((x, y) => y.mtimeMs - x.mtimeMs);
|
|
290
|
+
const newestMtimeMs = files[0].mtimeMs;
|
|
278
291
|
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
292
|
+
for (const { fp } of files.slice(0, MAX_FALLBACK_FILES)) {
|
|
293
|
+
let size = TAIL_BYTES;
|
|
294
|
+
for (;;) {
|
|
295
|
+
const rawCwd = lastCwdInLines(await readTailLinesAsync(fp, size));
|
|
296
|
+
if (rawCwd) return { rawCwd, newestMtimeMs };
|
|
297
|
+
let fileSize = 0;
|
|
298
|
+
try { fileSize = (await fsp.stat(fp)).size; } catch { /* gone: stop growing */ }
|
|
299
|
+
if (size >= MAX_TAIL_BYTES || size >= fileSize) break;
|
|
300
|
+
size = Math.min(size * TAIL_GROWTH, MAX_TAIL_BYTES);
|
|
301
|
+
}
|
|
285
302
|
}
|
|
286
|
-
return
|
|
303
|
+
return null;
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
/**
|
|
@@ -212,10 +212,10 @@ function buildPersonaBodyText(rawBody, personaPath) {
|
|
|
212
212
|
* same runaway-file protection.
|
|
213
213
|
*
|
|
214
214
|
* @param {{ cwd: string, agentType: string|null, fallbackModel?: string, deps?: object }} opts
|
|
215
|
-
* @returns {Promise<{ model: string, systemPrompt: string|null, personaPath: string|null }>}
|
|
215
|
+
* @returns {Promise<{ model: string, systemPrompt: string|null, personaPath: string|null, tools: string[]|null }>}
|
|
216
216
|
*/
|
|
217
217
|
async function resolvePrdPersonaForSpawn({ cwd, agentType, fallbackModel = FALLBACK_MODEL, deps = {} } = {}) {
|
|
218
|
-
const miss = { model: fallbackModel, systemPrompt: null, personaPath: null };
|
|
218
|
+
const miss = { model: fallbackModel, systemPrompt: null, personaPath: null, tools: null };
|
|
219
219
|
if (!agentType) return miss;
|
|
220
220
|
try {
|
|
221
221
|
const getBody = deps.getPersonaBody || require('../../agentLibrary.cjs').getPersonaBody;
|
|
@@ -230,7 +230,9 @@ async function resolvePrdPersonaForSpawn({ cwd, agentType, fallbackModel = FALLB
|
|
|
230
230
|
const { fm, body } = persona.fm ? persona : splitFrontmatter(persona.text);
|
|
231
231
|
const model = fm.model && fm.model !== 'inherit' ? fm.model : fallbackModel;
|
|
232
232
|
const systemPrompt = buildPersonaBodyText(body, persona.path);
|
|
233
|
-
|
|
233
|
+
const parseTools = deps.parseTools || require('../../agentLibrary.cjs').parseTools;
|
|
234
|
+
const list = parseTools(typeof fm.tools === 'string' ? fm.tools : '');
|
|
235
|
+
return { model, systemPrompt, personaPath: persona.path, tools: list.length ? list : null };
|
|
234
236
|
} catch {
|
|
235
237
|
return miss;
|
|
236
238
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* personaToolScope.cjs — turns a persona's `tools:` list into the claude CLI
|
|
5
|
+
* args that actually ENFORCE it on a headless `claude -p` run.
|
|
6
|
+
*
|
|
7
|
+
* Verified on CLI 2.1.302: under `--dangerously-skip-permissions`,
|
|
8
|
+
* `--allowedTools` restricts nothing and `--tools` only filters built-ins (MCP
|
|
9
|
+
* tools stay). `--agents '<json>' --agent <name>` with a `tools` array enforces
|
|
10
|
+
* exact per-tool scoping, MCP tools included, while the Claude Code harness
|
|
11
|
+
* prompt, CLAUDE.md, `--append-system-prompt` and `--disallowedTools` keep
|
|
12
|
+
* working.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no fs, and no require of scheduler.cjs (cycle) — the caller passes its
|
|
15
|
+
* HEADLESS_DISALLOWED_TOOLS list in.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Always granted to a scoped run: the finish protocol runs /code-review and
|
|
19
|
+
// /security-review (Skill + Agent); MCP tools are deferred, so ToolSearch is
|
|
20
|
+
// needed to reach them; TodoWrite is the run's own planning aid.
|
|
21
|
+
const HEADLESS_TOOL_FLOOR = Object.freeze(['Agent', 'Skill', 'ToolSearch', 'TodoWrite']);
|
|
22
|
+
|
|
23
|
+
const HEADLESS_SCOPE_AGENT_NAME = 'sm-headless-scoped';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {readonly string[]|null|undefined} tools persona `tools:` list
|
|
27
|
+
* @param {{ disallowed?: readonly string[] }} [opts]
|
|
28
|
+
* @returns {{ tools: string[] }|null} null = persona declares no restriction
|
|
29
|
+
*/
|
|
30
|
+
function buildPersonaToolScope(tools, { disallowed = [] } = {}) {
|
|
31
|
+
if (!Array.isArray(tools) || tools.length === 0) return null;
|
|
32
|
+
const names = tools
|
|
33
|
+
.filter((t) => typeof t === 'string')
|
|
34
|
+
.map((t) => t.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
if (names.length === 0 || names.includes('*')) return null;
|
|
37
|
+
const blocked = new Set(disallowed);
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const name of [...names, ...HEADLESS_TOOL_FLOOR]) {
|
|
40
|
+
if (blocked.has(name) || out.includes(name)) continue;
|
|
41
|
+
out.push(name);
|
|
42
|
+
}
|
|
43
|
+
return { tools: out };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {{ tools: string[] }|null} scope result of buildPersonaToolScope
|
|
48
|
+
* @returns {string[]} argv fragment ([] when unscoped)
|
|
49
|
+
*/
|
|
50
|
+
function personaToolScopeArgs(scope) {
|
|
51
|
+
if (!scope) return [];
|
|
52
|
+
const def = {
|
|
53
|
+
description: 'Headless run with a scheduler-enforced tool scope.',
|
|
54
|
+
prompt: 'Tool scope is enforced by the scheduler.',
|
|
55
|
+
tools: scope.tools,
|
|
56
|
+
};
|
|
57
|
+
return [
|
|
58
|
+
'--agents', JSON.stringify({ [HEADLESS_SCOPE_AGENT_NAME]: def }),
|
|
59
|
+
'--agent', HEADLESS_SCOPE_AGENT_NAME,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
HEADLESS_TOOL_FLOOR,
|
|
65
|
+
HEADLESS_SCOPE_AGENT_NAME,
|
|
66
|
+
buildPersonaToolScope,
|
|
67
|
+
personaToolScopeArgs,
|
|
68
|
+
};
|