claude-code-session-manager 0.123.1 → 0.124.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-T14aM8ie.js → AgentLibrary-qmAgadiS.js} +1 -1
- package/dist/assets/{Button-Dz1kV8Z_.js → Button-CkuaU6cA.js} +1 -1
- package/dist/assets/{DataModel-DdYRn2Ec.js → DataModel-DEVHxW5I.js} +1 -1
- package/dist/assets/{History-zQPvqwE7.js → History-C-qw4Gx4.js} +1 -1
- package/dist/assets/{Hooks-BTrbT742.js → Hooks-DzAnCyjJ.js} +1 -1
- package/dist/assets/{Library-BwQ6GRHR.js → Library-DExZ29wM.js} +1 -1
- package/dist/assets/{ListDetail-CunxAhRC.js → ListDetail-DiQk-Hf9.js} +1 -1
- package/dist/assets/{McpServers-DpoyxXaO.js → McpServers-C0nqX36_.js} +1 -1
- package/dist/assets/{Memory-KDtGYMc3.js → Memory-D4KAWPAC.js} +1 -1
- package/dist/assets/{Panel-xXmUBo8l.js → Panel-xv9ToSjl.js} +1 -1
- package/dist/assets/{Permissions-lIzTUrW2.js → Permissions-0yEyqwZU.js} +1 -1
- package/dist/assets/{Plugins-BA1Zqk69.js → Plugins-CB28SGGJ.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CmeJG2Kw.js → ProvenanceBadge-9i4gXDvI.js} +1 -1
- package/dist/assets/{SaveBar-BTJg_9Uc.js → SaveBar-mSVRXpml.js} +1 -1
- package/dist/assets/{Scheduler-CISTDNMU.js → Scheduler-BoUetIC0.js} +1 -1
- package/dist/assets/{ScopeSwitcher-C6NWvzoj.js → ScopeSwitcher-B8tdQIKs.js} +1 -1
- package/dist/assets/{Settings-BBoGJ34v.js → Settings-D3jfKwaK.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-IJv60QAJ.js → SkillReferenceGraph-DJ4JJRt3.js} +1 -1
- package/dist/assets/{Skills-L6nGcNF1.js → Skills-P9HHxDCR.js} +1 -1
- package/dist/assets/{SystemPrompt-Cm7I2gJz.js → SystemPrompt-B_tnbBZ2.js} +1 -1
- package/dist/assets/{TagLibrary-BxPwoPE6.js → TagLibrary-CI8NHRKi.js} +1 -1
- package/dist/assets/{TiptapBody-CR-Zr3Pz.js → TiptapBody-9LktXPiD.js} +1 -1
- package/dist/assets/{Toggle-BFhlpZ-n.js → Toggle-BwsUsAA7.js} +1 -1
- package/dist/assets/{ViewTabs-CuGlZ9Ed.js → ViewTabs-CY5gMZ_N.js} +1 -1
- package/dist/assets/{historyTotals-Dxv9Fzrs.js → historyTotals-_dIaxmPX.js} +1 -1
- package/dist/assets/{index-Dx8owDeQ.js → index-d4i3X1x3.js} +419 -419
- package/dist/assets/{settingsSchema-DaqFSAQH.js → settingsSchema-Dg0Xa9RR.js} +1 -1
- package/dist/assets/{skillFrontmatter-BIJddxVB.js → skillFrontmatter-CpoFkSyj.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-1hjZ4knk.js → useAgentsDirWatch-CFPprfWG.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-web/assets/app.css +32 -0
- package/dist-web/assets/app.js +1479 -0
- package/dist-web/index.html +16 -0
- package/package.json +6 -4
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +28 -10
- package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -6
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +68 -9
- package/src/main/config.cjs +23 -29
- package/src/main/docEdit.cjs +2 -5
- package/src/main/files.cjs +16 -4
- package/src/main/index.cjs +26 -4
- package/src/main/ipcSchemas.cjs +6 -2
- package/src/main/lib/epic/activeIndexMerge.cjs +3 -5
- package/src/main/lib/epic/epicReopen.cjs +161 -0
- package/src/main/lib/epic/epicSpawnPlan.cjs +9 -1
- package/src/main/lib/epic/epicTranscriptRestore.cjs +109 -0
- package/src/main/lib/epic/epicValidationHook.cjs +1 -1
- package/src/main/lib/epic/epicWorktreeMint.cjs +50 -3
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +2 -1
- package/src/main/lib/feedback-remote/remoteAssets.cjs +98 -0
- package/src/main/lib/feedback-remote/remoteIpcDispatch.cjs +216 -0
- package/src/main/lib/insideHome.cjs +102 -23
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +27 -0
- package/src/main/lib/prd/prdWorktreeReach.cjs +9 -1
- package/src/main/lib/scheduler/rcaReport.cjs +1 -13
- package/src/main/lib/scheduler/reaperHelpers.cjs +82 -77
- package/src/main/lib/scheduler/reviewNotice.cjs +0 -2
- package/src/main/lib/scheduler/scheduleJobTransitions.cjs +1 -1
- package/src/main/lib/scheduler/schedulerBatch.cjs +1 -1
- package/src/main/lib/sendToRenderer.cjs +22 -2
- package/src/main/lib/smfileHeaders.cjs +17 -1
- package/src/main/pluginInstall.cjs +1 -1
- package/src/main/promptSessionEvents.cjs +2 -5
- package/src/main/pty.cjs +5 -4
- package/src/main/runVerify.cjs +6 -41
- package/src/main/scheduler/budget.cjs +4 -4
- package/src/main/scheduler/prdParser.cjs +2 -3
- package/src/main/scheduler/reviewLadder.cjs +0 -9
- package/src/main/scheduler.cjs +72 -25
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/main/voiceHotkey.cjs +3 -2
- package/src/main/webRemote.cjs +161 -1
- package/src/preload/api.d.ts +8 -1
- package/src/preload/apiSurface.cjs +19 -0
- package/src/preload/apiSurface.json +1689 -0
- package/src/preload/index.cjs +2 -0
- package/src/seed/agents/architect.md +2 -2
- package/src/seed/agents/blender-artist.md +76 -0
|
@@ -12,72 +12,78 @@ const path = require('node:path');
|
|
|
12
12
|
const { readTail } = require('../util/fileTail.cjs');
|
|
13
13
|
const { detectRateLimitInLog } = require('../claude/rateLimitDetect.cjs');
|
|
14
14
|
const { pidAlive } = require('../process/pidAlive.cjs');
|
|
15
|
+
const { smArgv0 } = require('../process/procName.cjs');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* isClaudeArgv(argv) — strict claude-executor identity. Pure.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
* Judges argv[0] (and argv[1] only for a node/bun-launched CLI) — never a
|
|
21
|
+
* substring of the whole command line. 2026-10-25: a stray
|
|
22
|
+
* `tail -F /tmp/claude-1000/.../x.log` matched /\bclaude\b/ and was adopted as
|
|
23
|
+
* the executor of two pidless rows for up to 25 hours.
|
|
24
|
+
*/
|
|
25
|
+
function isClaudeArgv(argv) {
|
|
26
|
+
if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== 'string') return false;
|
|
27
|
+
const base = path.basename(argv[0]);
|
|
28
|
+
if (base === 'claude' || base.startsWith('sm-claude-')) return true;
|
|
29
|
+
if (base === 'node' || base === 'bun') {
|
|
30
|
+
const a1 = typeof argv[1] === 'string' ? argv[1] : '';
|
|
31
|
+
// A shebang-script claude launched through an sm-claude-* alias shows the
|
|
32
|
+
// alias path as argv[1] (the kernel rewrites script argv), so accept it too.
|
|
33
|
+
return /\/claude(\.js|\.mjs|\.cjs)?$/.test(a1) || a1.includes('/@anthropic-ai/claude-code/')
|
|
34
|
+
|| path.basename(a1).startsWith('sm-claude-');
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read /proc/<pid>/cmdline as an argv array; null when unreadable. */
|
|
40
|
+
function readArgv(pid) {
|
|
41
|
+
try {
|
|
42
|
+
const parts = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0');
|
|
43
|
+
if (parts.length && parts[parts.length - 1] === '') parts.pop();
|
|
44
|
+
return parts;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return true if pid is alive AND its argv looks like a claude process
|
|
52
|
+
* (isClaudeArgv). Guards against PID recycling. Where /proc/<pid>/cmdline
|
|
53
|
+
* cannot be read (macOS, permission denied) or on win32, conservatively
|
|
54
|
+
* returns true — never false-reap a live PID we cannot identify.
|
|
29
55
|
*
|
|
30
|
-
*
|
|
31
|
-
* far worse than a late reap.
|
|
56
|
+
* Existence check delegates to the shared pidAlive() so EPERM counts as alive.
|
|
32
57
|
*/
|
|
33
58
|
function claudePidAlive(pid, { platform = process.platform } = {}) {
|
|
34
59
|
if (!pid || typeof pid !== 'number' || pid <= 1) return false;
|
|
35
60
|
if (!pidAlive(pid)) return false;
|
|
36
|
-
// Windows has no /proc — cannot verify identity, so never call it dead.
|
|
37
61
|
if (platform === 'win32') return true;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch {
|
|
42
|
-
// Can't read cmdline (macOS, permission denied) → assume alive.
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
62
|
+
const argv = readArgv(pid);
|
|
63
|
+
if (argv === null) return true;
|
|
64
|
+
return isClaudeArgv(argv);
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
/**
|
|
48
|
-
* findLiveProcessForJob(job, { worktreeDir
|
|
68
|
+
* findLiveProcessForJob(job, { worktreeDir }) → pid | null
|
|
49
69
|
*
|
|
50
|
-
* Positive liveness scan for a 'running' row whose `runtime.pid` is missing
|
|
51
|
-
*
|
|
52
|
-
* incident: 234-uranus-eight-tails-ox marked failed/never_ran while PID
|
|
53
|
-
* 2174739 was a live `claude -p` still writing to that job's own worktree).
|
|
70
|
+
* Positive liveness scan for a 'running' row whose `runtime.pid` is missing
|
|
71
|
+
* (2026-09-06: a pidless row was reaped while its claude was still alive).
|
|
54
72
|
*
|
|
55
|
-
* Linux-`/proc` only.
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* worktreeDir to match against.
|
|
62
|
-
* - `/proc/<pid>/cwd` resolves to `runCwd` (or nested under it) AND
|
|
63
|
-
* `/proc/<pid>/cmdline` contains `claude` (2026-09-13 incident:
|
|
64
|
-
* runId 2026-09-13T16-15-56-818Z ran in-place in the shared tree — no
|
|
65
|
-
* worktree, and dispatch never puts the slug into argv — so neither of
|
|
66
|
-
* the above could ever match a live pid 3138458). `runCwd` is deliberately
|
|
67
|
-
* NEVER matched alone: several concurrent jobs commonly share one tree's
|
|
68
|
-
* cwd, so a bare cwd match without the process-identity check would
|
|
69
|
-
* cross-match the wrong job's process.
|
|
73
|
+
* Linux-`/proc` only. A candidate must pass isClaudeArgv AND either:
|
|
74
|
+
* - `/proc/<pid>/cwd` is `worktreeDir` or nested under it, or
|
|
75
|
+
* - argv[0] === smArgv0('job', job.slug) (every scheduler job spawn carries
|
|
76
|
+
* the slug in argv[0]).
|
|
77
|
+
* There is deliberately no runCwd-nested match: it adopted a stray `tail -F`
|
|
78
|
+
* on 2026-10-25 and is redundant now that argv[0] carries the slug.
|
|
70
79
|
*
|
|
71
|
-
* Returns the first matching pid, or null
|
|
72
|
-
*
|
|
73
|
-
* Safe fallback by construction: on any platform without `/proc` (macOS,
|
|
74
|
-
* Windows) `fs.readdirSync('/proc')` throws and this returns null immediately
|
|
75
|
-
* — i.e. exactly today's behaviour (fail toward terminalizing), never a hang
|
|
76
|
-
* or a thrown error propagating to the caller.
|
|
80
|
+
* Returns the first matching pid, or null. On platforms without `/proc` it
|
|
81
|
+
* returns null immediately.
|
|
77
82
|
*/
|
|
78
|
-
function findLiveProcessForJob(job, { worktreeDir,
|
|
83
|
+
function findLiveProcessForJob(job, { worktreeDir, platform = process.platform } = {}) {
|
|
79
84
|
if (platform === 'win32') return null; // no /proc — skip every read
|
|
80
85
|
const slug = job?.slug;
|
|
86
|
+
const want = slug ? smArgv0('job', slug) : null;
|
|
81
87
|
let entries;
|
|
82
88
|
try {
|
|
83
89
|
entries = fs.readdirSync('/proc');
|
|
@@ -89,34 +95,15 @@ function findLiveProcessForJob(job, { worktreeDir, runCwd, platform = process.pl
|
|
|
89
95
|
const pid = Number(name);
|
|
90
96
|
if (!pid || pid <= 1) continue;
|
|
91
97
|
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
try {
|
|
95
|
-
cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
96
|
-
} catch {
|
|
97
|
-
// Process exited mid-scan, or permission denied — try the argv
|
|
98
|
-
// fallback below before giving up on this pid.
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (worktreeDir && cwdLink && (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep))) {
|
|
103
|
-
return pid;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let cmd = null;
|
|
107
|
-
if (slug || (runCwd && cwdLink)) {
|
|
108
|
-
try {
|
|
109
|
-
cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
|
|
110
|
-
} catch {
|
|
111
|
-
// Same as above — process gone or unreadable, keep scanning.
|
|
112
|
-
}
|
|
113
|
-
}
|
|
98
|
+
const argv = readArgv(pid);
|
|
99
|
+
if (!argv || !isClaudeArgv(argv)) continue;
|
|
114
100
|
|
|
115
|
-
if (
|
|
101
|
+
if (want && argv[0] === want) return pid;
|
|
116
102
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
if (worktreeDir) {
|
|
104
|
+
let cwdLink = null;
|
|
105
|
+
try { cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`); } catch { /* exited / denied */ }
|
|
106
|
+
if (cwdLink && (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep))) return pid;
|
|
120
107
|
}
|
|
121
108
|
}
|
|
122
109
|
return null;
|
|
@@ -413,6 +400,11 @@ function selectReapableJobs(jobs, now, {
|
|
|
413
400
|
const reapable = [];
|
|
414
401
|
const warnings = [];
|
|
415
402
|
const recovered = [];
|
|
403
|
+
/** pid -> slug that records/was recovered with it */
|
|
404
|
+
const claimedPids = new Map();
|
|
405
|
+
for (const r of jobs ?? []) {
|
|
406
|
+
if (r.status === 'running' && r.runtime?.pid) claimedPids.set(r.runtime.pid, r.slug);
|
|
407
|
+
}
|
|
416
408
|
for (const j of jobs ?? []) {
|
|
417
409
|
if (j.status !== 'running') continue;
|
|
418
410
|
const pid = j.runtime?.pid;
|
|
@@ -448,10 +440,22 @@ function selectReapableJobs(jobs, now, {
|
|
|
448
440
|
continue;
|
|
449
441
|
}
|
|
450
442
|
|
|
451
|
-
|
|
443
|
+
// A row with no run log never reached the spawn — a /proc scan can only
|
|
444
|
+
// misattribute a stray process to it.
|
|
445
|
+
const livePid = (typeof findLiveProcess === 'function' && Number.isFinite(logMtimeMs))
|
|
446
|
+
? findLiveProcess(j) : null;
|
|
452
447
|
if (livePid) {
|
|
453
|
-
|
|
454
|
-
|
|
448
|
+
const holder = claimedPids.get(livePid);
|
|
449
|
+
if (holder && holder !== j.slug) {
|
|
450
|
+
warnings.push({
|
|
451
|
+
slug: j.slug,
|
|
452
|
+
reason: `pid ${livePid} not recovered for ${j.slug} — already claimed by ${holder}`,
|
|
453
|
+
});
|
|
454
|
+
} else {
|
|
455
|
+
claimedPids.set(livePid, j.slug);
|
|
456
|
+
recovered.push({ slug: j.slug, pid: livePid });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
455
459
|
}
|
|
456
460
|
reapable.push({
|
|
457
461
|
slug: j.slug,
|
|
@@ -520,6 +524,7 @@ function resolveCommitGuardOutcome(guardVerdict, satisfyingCommits) {
|
|
|
520
524
|
|
|
521
525
|
module.exports = {
|
|
522
526
|
claudePidAlive,
|
|
527
|
+
isClaudeArgv,
|
|
523
528
|
classifyRunOutcome,
|
|
524
529
|
mapOutcomeToGateOutcome,
|
|
525
530
|
ORPHAN_REQUEUE_CAP,
|
|
@@ -153,8 +153,6 @@ function selectDueReviewNotices(jobs = [], { now = Date.now(), holdMs = DEFAULT_
|
|
|
153
153
|
const isDue = (j) => {
|
|
154
154
|
if (j.status === 'skipped' && j.needsReviewAutoResolvedSkip) return true;
|
|
155
155
|
if (j.status !== 'needs_review') return false;
|
|
156
|
-
// A question for the human has nothing for the ladder to heal — no hold.
|
|
157
|
-
if (j.verifierVerdict === 'needs_human') return true;
|
|
158
156
|
const firstParkedAt = j.reviewNotice.firstParkedAt;
|
|
159
157
|
if (!firstParkedAt) return false;
|
|
160
158
|
const parkedMs = Date.parse(firstParkedAt);
|
|
@@ -116,7 +116,7 @@ const LEGAL_TRANSITIONS = {
|
|
|
116
116
|
pending: ['running', 'completed', 'failed', 'skipped'],
|
|
117
117
|
running: ['completed', 'failed', 'needs_review', 'skipped', 'pending'],
|
|
118
118
|
investigating: ['failed', 'needs_review', 'completed', 'pending', 'skipped'],
|
|
119
|
-
failed: ['investigating', 'pending', 'completed', 'needs_review'],
|
|
119
|
+
failed: ['investigating', 'pending', 'completed', 'needs_review', 'skipped'],
|
|
120
120
|
needs_review: ['investigating', 'pending', 'completed', 'skipped', 'running'],
|
|
121
121
|
completed: ['pending'],
|
|
122
122
|
skipped: ['pending'],
|
|
@@ -360,7 +360,7 @@ function pickForProject(projectJobs, runningSlugsInProject, slots, heldSlugs = n
|
|
|
360
360
|
for (const [rootSlug, { rootStatus, slugs }] of heldByRoot) {
|
|
361
361
|
const names = [...slugs].sort().join(', ');
|
|
362
362
|
if (rootStatus === 'needs_review') {
|
|
363
|
-
reasons.push(`holding ${slugs.size} job(s) transitively behind root blocker ${rootSlug} (needs_review) [${names}].
|
|
363
|
+
reasons.push(`holding ${slugs.size} job(s) transitively behind root blocker ${rootSlug} (needs_review) [${names}]. the chain resumes when the recovery ladder resolves ${rootSlug} (or it is retired or reset).`);
|
|
364
364
|
} else if (rootStatus === 'failed') {
|
|
365
365
|
reasons.push(`holding ${slugs.size} job(s) transitively behind root blocker ${rootSlug} (failed) [${names}]. Reset or archive ${rootSlug} to unblock the chain.`);
|
|
366
366
|
} else if (rootStatus === 'skipped') {
|
|
@@ -20,9 +20,21 @@
|
|
|
20
20
|
* webContents state and wrap the send in try/catch.
|
|
21
21
|
*/
|
|
22
22
|
function sendIfAlive(window, channel, payload) {
|
|
23
|
+
sendToWindow(window, channel, payload);
|
|
24
|
+
// Remote fanout runs even when the window is gone (web clients outlive it) and never throws.
|
|
25
|
+
if (_remoteFanout) {
|
|
26
|
+
try {
|
|
27
|
+
_remoteFanout(channel, payload);
|
|
28
|
+
} catch {
|
|
29
|
+
// A broken remote subscriber must never break the Electron send path.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendToWindow(window, channel, payload) {
|
|
23
35
|
if (!window || window.isDestroyed()) return;
|
|
24
36
|
const wc = window.webContents;
|
|
25
|
-
if (!wc || wc.isDestroyed() || wc.isCrashed()) return;
|
|
37
|
+
if (!wc || wc.isDestroyed() || wc.isCrashed?.()) return;
|
|
26
38
|
try {
|
|
27
39
|
wc.send(channel, payload);
|
|
28
40
|
} catch {
|
|
@@ -31,4 +43,12 @@ function sendIfAlive(window, channel, payload) {
|
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
/** @type {((channel: string, payload: unknown) => void) | null} */
|
|
47
|
+
let _remoteFanout = null;
|
|
48
|
+
|
|
49
|
+
/** Register (or clear with null) the fanout that mirrors every push to E2E web clients. */
|
|
50
|
+
function setRemoteFanout(fn) {
|
|
51
|
+
_remoteFanout = typeof fn === 'function' ? fn : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { sendIfAlive, setRemoteFanout };
|
|
@@ -63,4 +63,20 @@ function shouldBlockFrameNavigation({ isMainFrame, currentUrl, targetUrl }) {
|
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Convert an smfile:// URL pathname (still percent-encoded) into a filesystem
|
|
68
|
+
* path. On win32 the pathname is `/C:/Users/...`; the leading '/' before the
|
|
69
|
+
* drive letter is stripped and the result is path.win32-normalized. Posix input
|
|
70
|
+
* is only decoded.
|
|
71
|
+
* @param {string} pathname
|
|
72
|
+
* @param {string} [platform]
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
function smfileUrlToPath(pathname, platform = process.platform) {
|
|
76
|
+
const decoded = decodeURIComponent(pathname);
|
|
77
|
+
if (platform !== 'win32') return decoded;
|
|
78
|
+
const stripped = /^[\\/][A-Za-z]:/.test(decoded) ? decoded.slice(1) : decoded;
|
|
79
|
+
return require('node:path').win32.normalize(stripped);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { DEMO_VIDEO_CSP_HEADER, smfileResponseHeaders, shouldBlockFrameNavigation, smfileUrlToPath };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Channels:
|
|
9
9
|
* ipcMain.handle('plugins:install', { slug }) -> { ok, exitCode }
|
|
10
|
-
*
|
|
10
|
+
* sendIfAlive('plugins:install-progress', { slug, line })
|
|
11
11
|
*
|
|
12
12
|
* Safety:
|
|
13
13
|
* - slug must match /^[a-z0-9\-/]+$/ (lowercase, hyphen, slash for owner/name).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Main-process counterpart to promptSessions.ts's event chain, used ONLY by
|
|
@@ -30,11 +31,7 @@ let mainWindow = null;
|
|
|
30
31
|
function attachWindow(win) { mainWindow = win; }
|
|
31
32
|
|
|
32
33
|
function broadcast(channel, payload) {
|
|
33
|
-
|
|
34
|
-
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
|
|
35
|
-
mainWindow.webContents.send(channel, payload);
|
|
36
|
-
}
|
|
37
|
-
} catch { /* render frame may be gone */ }
|
|
34
|
+
sendIfAlive(mainWindow, channel, payload);
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
let seq = 0;
|
package/src/main/pty.cjs
CHANGED
|
@@ -16,10 +16,10 @@ const { ipcMain } = require('electron');
|
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const os = require('node:os');
|
|
18
18
|
const fs = require('node:fs');
|
|
19
|
-
const { addAllowedRoot } = require('./config.cjs');
|
|
19
|
+
const { addAllowedRoot, getAllowedRoots } = require('./config.cjs');
|
|
20
20
|
const { aliasedShellBin } = require('./lib/process/smProcNames.cjs');
|
|
21
21
|
const { cleanChildEnv, pathWithUserBins } = require('./lib/process/cleanEnv.cjs');
|
|
22
|
-
const {
|
|
22
|
+
const { checkInsideHomeOrRoots } = require('./lib/insideHome.cjs');
|
|
23
23
|
const { defaultShell } = require('./lib/process/defaultShell.cjs');
|
|
24
24
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
25
25
|
const opsErrorLog = require('./lib/telemetry/opsErrorLog.cjs');
|
|
@@ -151,9 +151,10 @@ class PtyManager {
|
|
|
151
151
|
return { pid: null, cwd, reattached: false, error: 'node-pty unavailable' };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Validate that cwd is inside homedir
|
|
154
|
+
// Validate that cwd is inside homedir or an already-registered project root
|
|
155
|
+
// (never a renderer-supplied one) before widening the allowed-root set.
|
|
155
156
|
if (cwd) {
|
|
156
|
-
const r =
|
|
157
|
+
const r = checkInsideHomeOrRoots(cwd, getAllowedRoots());
|
|
157
158
|
if (!r.ok) throw new Error(`pty ${r.error}`);
|
|
158
159
|
addAllowedRoot(r.realPath);
|
|
159
160
|
}
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -269,21 +269,22 @@ function checkDeps(queueEntry, allJobs) {
|
|
|
269
269
|
// ─── sentinel scanner ─────────────────────────────────────────────────────────
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* Scan for a `SCHEDULER_VERDICT: PASS|FAIL|BLOCKED_BY_FOREIGN_WIP
|
|
272
|
+
* Scan for a `SCHEDULER_VERDICT: PASS|FAIL|BLOCKED_BY_FOREIGN_WIP` sentinel (a legacy NEEDS_HUMAN line is classed as FAIL)
|
|
273
273
|
* line in the run output.
|
|
274
274
|
*
|
|
275
275
|
* Checks `resultEvent.resultText` first (the agent's final message), then the
|
|
276
276
|
* last tool_result content. Anchored to line-start so prose mentioning the
|
|
277
277
|
* string in mid-sentence does not match.
|
|
278
278
|
*
|
|
279
|
-
* Returns 'pass', 'fail', 'blocked_by_foreign_wip',
|
|
279
|
+
* Returns 'pass', 'fail', 'blocked_by_foreign_wip', or null.
|
|
280
280
|
*/
|
|
281
281
|
function scanSentinel(resultEvent, events) {
|
|
282
282
|
const RE = /^SCHEDULER_VERDICT:\s*(PASS|FAIL|BLOCKED_BY_FOREIGN_WIP|NEEDS_HUMAN)\b/m;
|
|
283
|
+
const norm = (v) => { const l = v.toLowerCase(); return l === 'needs_human' ? 'fail' : l; };
|
|
283
284
|
|
|
284
285
|
if (resultEvent) {
|
|
285
286
|
const m = resultEvent.resultText.match(RE);
|
|
286
|
-
if (m) return m[1]
|
|
287
|
+
if (m) return norm(m[1]);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
let lastToolResult = null;
|
|
@@ -292,7 +293,7 @@ function scanSentinel(resultEvent, events) {
|
|
|
292
293
|
}
|
|
293
294
|
if (lastToolResult && lastToolResult.content) {
|
|
294
295
|
const m = lastToolResult.content.match(RE);
|
|
295
|
-
if (m) return m[1]
|
|
296
|
+
if (m) return norm(m[1]);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
return null;
|
|
@@ -327,29 +328,7 @@ function scanHaltLine(text) {
|
|
|
327
328
|
* as scanSentinel). Trimmed, capped at 300 chars; '' when absent.
|
|
328
329
|
*/
|
|
329
330
|
function scanSentinelReason(resultEvent, events) {
|
|
330
|
-
const RE = /^SCHEDULER_VERDICT:\s*FAIL\b[ \t]*([^\n]*)/m;
|
|
331
|
-
const clip = (m) => m[1].trim().slice(0, 300);
|
|
332
|
-
if (resultEvent && resultEvent.resultText) {
|
|
333
|
-
const m = resultEvent.resultText.match(RE);
|
|
334
|
-
if (m) return clip(m);
|
|
335
|
-
}
|
|
336
|
-
let lastToolResult = null;
|
|
337
|
-
for (const ev of events) {
|
|
338
|
-
if (ev.kind === 'tool_result') lastToolResult = ev;
|
|
339
|
-
}
|
|
340
|
-
if (lastToolResult && lastToolResult.content) {
|
|
341
|
-
const m = lastToolResult.content.match(RE);
|
|
342
|
-
if (m) return clip(m);
|
|
343
|
-
}
|
|
344
|
-
return '';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* One-line question after `SCHEDULER_VERDICT: NEEDS_HUMAN` (same two-source
|
|
349
|
-
* scan order as scanSentinel). Trimmed, capped at 300 chars; '' when absent.
|
|
350
|
-
*/
|
|
351
|
-
function scanHumanQuestion(resultEvent, events) {
|
|
352
|
-
const RE = /^SCHEDULER_VERDICT:\s*NEEDS_HUMAN\b[ \t]*([^\n]*)/m;
|
|
331
|
+
const RE = /^SCHEDULER_VERDICT:\s*(?:FAIL|NEEDS_HUMAN)\b[ \t]*([^\n]*)/m;
|
|
353
332
|
const clip = (m) => m[1].trim().slice(0, 300);
|
|
354
333
|
if (resultEvent && resultEvent.resultText) {
|
|
355
334
|
const m = resultEvent.resultText.match(RE);
|
|
@@ -816,19 +795,6 @@ async function verifyRun({
|
|
|
816
795
|
);
|
|
817
796
|
}
|
|
818
797
|
|
|
819
|
-
// The executor needs a decision/action only the human can take. Parks as
|
|
820
|
-
// needs_review with the question; every automatic retry rung skips it.
|
|
821
|
-
if (sentinel === 'needs_human') {
|
|
822
|
-
const humanQuestion = scanHumanQuestion(resultEvent, events);
|
|
823
|
-
const kept = [...annotations, ...issues.map((i) => ({ verdict: i.verdict, reason: i.reason }))];
|
|
824
|
-
return conclude(
|
|
825
|
-
'needs_human',
|
|
826
|
-
'executor reported SCHEDULER_VERDICT: NEEDS_HUMAN' + (humanQuestion ? ': ' + humanQuestion : ''),
|
|
827
|
-
'needs_review',
|
|
828
|
-
{ sentinel, humanQuestion, ...(kept.length ? { annotations: kept } : {}) },
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
798
|
const sentinelFields = sentinel ? { sentinel } : {};
|
|
833
799
|
const extras = (annotations.length || sentinel)
|
|
834
800
|
? { ...(annotations.length ? { annotations } : {}), ...sentinelFields }
|
|
@@ -1050,7 +1016,6 @@ module.exports = {
|
|
|
1050
1016
|
scanSentinel,
|
|
1051
1017
|
scanHaltLine,
|
|
1052
1018
|
scanSentinelReason,
|
|
1053
|
-
scanHumanQuestion,
|
|
1054
1019
|
scanForeignWipPathsClaim,
|
|
1055
1020
|
isMergeMainSlug,
|
|
1056
1021
|
extractMergeMainPrNumber,
|
|
@@ -225,12 +225,12 @@ function classifyBudgetKill(res, landedCommitEvidence) {
|
|
|
225
225
|
/**
|
|
226
226
|
* isJobBudgetExempt(job) → boolean
|
|
227
227
|
*
|
|
228
|
-
* Pure. `
|
|
229
|
-
*
|
|
230
|
-
*
|
|
228
|
+
* Pure. Only an explicit `budgetExempt: true` opt-out has no wall-clock kill
|
|
229
|
+
* ceiling. `quietMachine` jobs are NOT exempt: they hold an exclusive
|
|
230
|
+
* machine-wide lease, so they must always be killed by the normal budget.
|
|
231
231
|
*/
|
|
232
232
|
function isJobBudgetExempt(job) {
|
|
233
|
-
return job?.
|
|
233
|
+
return job?.budgetExempt === true;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/** Pure predicate the budget watchdog's shouldFire calls — single source of
|
|
@@ -137,9 +137,8 @@ async function parsePrdRaw(filePath) {
|
|
|
137
137
|
quietMachine: fm.quietMachine === 'true',
|
|
138
138
|
// Explicit opt-out of the wall-clock budget kill (scheduler.cjs's budget
|
|
139
139
|
// watchdog) — only a literal `true` opts out, matching quietMachine's
|
|
140
|
-
// parse semantics above. quietMachine jobs are exempt
|
|
141
|
-
//
|
|
142
|
-
// needs no wall-clock kill ceiling.
|
|
140
|
+
// parse semantics above. quietMachine jobs are NOT exempt; this is the
|
|
141
|
+
// only way to get no wall-clock kill ceiling.
|
|
143
142
|
budgetExempt: fm.budgetExempt === 'true',
|
|
144
143
|
body: body.trim(),
|
|
145
144
|
};
|
|
@@ -34,7 +34,6 @@ const { DEFAULT_PROJECT_CWD } = require('../lib/scheduler/schedulerBatch.cjs');
|
|
|
34
34
|
* Kill-switch: SM_RESUME_RECOVERY_DISABLE=1 restores today's behaviour
|
|
35
35
|
* exactly (always returns null), mirroring SM_RCA_DISABLE/SM_DOD_DISABLE.
|
|
36
36
|
*/
|
|
37
|
-
/** A row parked on a question only the human can answer — no automatic rung may touch it. */
|
|
38
37
|
// PRD refire-cap: a row parked by the post-run re-fire cap stays parked for the
|
|
39
38
|
// Epic's grouped notice. Every automated rung (reset, re-verify, auto-fix,
|
|
40
39
|
// auto-skip) consults this ONE predicate.
|
|
@@ -42,14 +41,10 @@ function isRefireCapped(job) {
|
|
|
42
41
|
return !!job && job.verifierVerdict === 'refire_capped';
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
function isNeedsHuman(job) {
|
|
46
|
-
return !!job && job.verifierVerdict === 'needs_human';
|
|
47
|
-
}
|
|
48
44
|
|
|
49
45
|
function selectResumeRecoveryTarget(job) {
|
|
50
46
|
if (process.env.SM_RESUME_RECOVERY_DISABLE === '1') return null;
|
|
51
47
|
if (!job || job.status !== 'needs_review') return null;
|
|
52
|
-
if (isNeedsHuman(job)) return null;
|
|
53
48
|
if (job.verifierVerdict !== 'uncommitted_changes') return null;
|
|
54
49
|
if (typeof job.sessionId !== 'string' || job.sessionId.length === 0) return null;
|
|
55
50
|
if (job.resumeRecoveryAttempted === true) return null;
|
|
@@ -113,7 +108,6 @@ function isMechanicalRecoveryFutile(job, currentHeadSha) {
|
|
|
113
108
|
function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
|
|
114
109
|
if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
|
|
115
110
|
if (!job || job.status !== 'needs_review') return null;
|
|
116
|
-
if (isNeedsHuman(job)) return null;
|
|
117
111
|
if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
|
|
118
112
|
if (job.mechanicalRecoveryAttempted === true) return null;
|
|
119
113
|
if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
|
|
@@ -827,7 +821,6 @@ function selectExhaustedNeedsReviewTargets(jobs, now, thresholdMs) {
|
|
|
827
821
|
for (const j of jobs ?? []) {
|
|
828
822
|
if (isCancelledJob(j)) continue;
|
|
829
823
|
if (j.status !== 'needs_review') continue;
|
|
830
|
-
if (isNeedsHuman(j)) continue;
|
|
831
824
|
if (!isEligibleForNeedsReviewAutoResolve(j, jobs)) continue;
|
|
832
825
|
if ((j.exhaustedResolveAttempts ?? 0) > NEEDS_REVIEW_RESOLVE_CAP) continue;
|
|
833
826
|
const history = j.statusHistory || [];
|
|
@@ -872,7 +865,6 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
|
|
|
872
865
|
if (isCancelledJob(job)) return false;
|
|
873
866
|
if (isRefireCapped(job)) return false;
|
|
874
867
|
if (job.status !== 'needs_review') return false;
|
|
875
|
-
if (isNeedsHuman(job)) return false;
|
|
876
868
|
// A job parked here because it was blocked by a sibling's foreign WIP
|
|
877
869
|
// three times in a row (never its own regression) has nothing for a
|
|
878
870
|
// fix-plan investigation to diagnose — there is no code defect to
|
|
@@ -1013,7 +1005,6 @@ function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false, ba
|
|
|
1013
1005
|
}
|
|
1014
1006
|
|
|
1015
1007
|
module.exports = {
|
|
1016
|
-
isNeedsHuman,
|
|
1017
1008
|
isRefireCapped,
|
|
1018
1009
|
isCancelledJob,
|
|
1019
1010
|
selectResumeRecoveryTarget,
|