claude-code-session-manager 0.35.3 → 0.35.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{TiptapBody-DmCekkUQ.js → TiptapBody-CUO_twO8.js} +1 -1
- package/dist/assets/index-BHyeZfve.css +32 -0
- package/dist/assets/index-DIVOtBGO.js +3558 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +14 -5
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +48 -35
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +20 -16
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +4 -4
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +32 -16
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +137 -0
- package/src/main/__tests__/mcpStatus.test.cjs +61 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +50 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +19 -1
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +51 -0
- package/src/main/browserView.cjs +2 -2
- package/src/main/chatRunner.cjs +218 -21
- package/src/main/index.cjs +19 -0
- package/src/main/ipcSchemas.cjs +7 -0
- package/src/main/mcpStatus.cjs +93 -0
- package/src/main/scheduler.cjs +221 -73
- package/src/preload/api.d.ts +34 -0
- package/src/preload/index.cjs +14 -0
- package/dist/assets/index-CHKMzzCM.js +0 -3558
- package/dist/assets/index-DVqmrWP3.css +0 -32
package/src/main/scheduler.cjs
CHANGED
|
@@ -1151,6 +1151,86 @@ function isPromotableOriginal(status) {
|
|
|
1151
1151
|
return status === 'failed' || status === 'needs_review';
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Pure helper: given a completed fix-plan slug (e.g. '451-fix-foo'), find the
|
|
1156
|
+
* SPECIFIC promotable original job it heals. Matches on the full
|
|
1157
|
+
* numeric-prefixed slug ('451-foo'), not just the base ('foo') — two
|
|
1158
|
+
* needs_review jobs can share a base slug with different NN prefixes
|
|
1159
|
+
* (e.g. '451-foo' and '453-foo'), and a fix plan must only heal the
|
|
1160
|
+
* lineage whose NN it was authored against. Exported for tests.
|
|
1161
|
+
*/
|
|
1162
|
+
function healTargetForFix(fixSlug, jobs) {
|
|
1163
|
+
const originalSlug = fixSlug.replace(/^(\d+)-fix-/, '$1-');
|
|
1164
|
+
return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status)) || null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Build the Opus investigation prompt. Pure/hermetic so its content can be
|
|
1169
|
+
* unit-tested (no spawn, no fs). Inputs are the already-resolved values that
|
|
1170
|
+
* spawnInvestigation computes.
|
|
1171
|
+
*/
|
|
1172
|
+
function buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group }) {
|
|
1173
|
+
return `You are investigating a failed scheduled job in the session-manager queue. Your ONLY job is to write a fix-plan PRD file. Do NOT attempt the fix yourself.
|
|
1174
|
+
|
|
1175
|
+
# Failed job
|
|
1176
|
+
- Slug: ${failedJob.slug}
|
|
1177
|
+
- Title: ${failedJob.title}
|
|
1178
|
+
- cwd: ${cwd}
|
|
1179
|
+
- Exit code: ${failedJob.exitCode}
|
|
1180
|
+
- Full failure log: ${failedLogPath}
|
|
1181
|
+
|
|
1182
|
+
# Original PRD body (this is what the job was trying to do)
|
|
1183
|
+
\`\`\`
|
|
1184
|
+
${originalBody}
|
|
1185
|
+
\`\`\`
|
|
1186
|
+
|
|
1187
|
+
# Last ~16KB of the failure log (stream-json format from \`claude -p\`)
|
|
1188
|
+
\`\`\`
|
|
1189
|
+
${logTail}
|
|
1190
|
+
\`\`\`
|
|
1191
|
+
|
|
1192
|
+
# Your task
|
|
1193
|
+
1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
|
|
1194
|
+
2. Read source files in ${cwd} as needed to understand the context.
|
|
1195
|
+
3. Identify the root cause of the failure.
|
|
1196
|
+
4. Write a NEW fix-plan PRD file at exactly this path:
|
|
1197
|
+
|
|
1198
|
+
${fixPath}
|
|
1199
|
+
|
|
1200
|
+
5. The frontmatter MUST be exactly this format (no extra keys):
|
|
1201
|
+
\`\`\`
|
|
1202
|
+
---
|
|
1203
|
+
title: Fix: <short summary of the fix>
|
|
1204
|
+
cwd: ${cwd}
|
|
1205
|
+
parallelGroup: ${group}
|
|
1206
|
+
estimateMinutes: <your time estimate>
|
|
1207
|
+
---
|
|
1208
|
+
\`\`\`
|
|
1209
|
+
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
1210
|
+
- Root-cause analysis (what went wrong and why)
|
|
1211
|
+
- Concrete fix steps (specific files / commands / edits)
|
|
1212
|
+
- Verification command(s) the next agent should run to confirm the fix
|
|
1213
|
+
- Acceptance criteria
|
|
1214
|
+
- Before writing the fix-plan PRD body, read
|
|
1215
|
+
\`plugins/session-manager-dev/skills/develop/standards.md\` (resolve it relative to the failed
|
|
1216
|
+
job's repo root — for session-manager-authored PRDs that is this cwd). Inline its
|
|
1217
|
+
\`## Execution discipline (headless runs)\` section VERBATIM into the new fix-plan PRD under an
|
|
1218
|
+
\`## Engineering standards\` heading, exactly as the \`/develop\` skill's Phase 1 does for normal
|
|
1219
|
+
PRDs. Reuse that convention — do not invent a second, paraphrased set of warnings. The headless
|
|
1220
|
+
Sonnet executor that runs your fix-plan sees no skills and no conversation, so inlining these
|
|
1221
|
+
rules verbatim is the only way they reach it.
|
|
1222
|
+
- Known failure class — "delegated instead of executed": if the failure log tail above shows the
|
|
1223
|
+
failed run invoked the \`Skill\` tool with a \`session-manager-dev:develop\` or
|
|
1224
|
+
\`session-manager-dev:process-feedback\` argument, and/or called \`ScheduleWakeup\`, and then exited
|
|
1225
|
+
0 without producing the diff/tests its PRD demanded, recognize this as a self-delegation failure.
|
|
1226
|
+
In that case LEAD the fix-plan PRD by quoting VERBATIM the canonical rule from standards.md's
|
|
1227
|
+
Execution discipline section — "You ARE the executor — never re-queue or self-schedule" — rather
|
|
1228
|
+
than authoring new prose. Instruct the fix-plan's executor plainly that a queued PRD is the task,
|
|
1229
|
+
not evidence of completion, and that the deliverable is the code diff.
|
|
1230
|
+
|
|
1231
|
+
DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1154
1234
|
/**
|
|
1155
1235
|
* Spawn an Opus investigation session for a failed job. The investigator's job
|
|
1156
1236
|
* is to read the failure log + original PRD, identify the root cause, and write
|
|
@@ -1223,49 +1303,7 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
1223
1303
|
console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
1224
1304
|
cwd = DEFAULT_PROJECT_CWD;
|
|
1225
1305
|
}
|
|
1226
|
-
const prompt =
|
|
1227
|
-
|
|
1228
|
-
# Failed job
|
|
1229
|
-
- Slug: ${failedJob.slug}
|
|
1230
|
-
- Title: ${failedJob.title}
|
|
1231
|
-
- cwd: ${cwd}
|
|
1232
|
-
- Exit code: ${failedJob.exitCode}
|
|
1233
|
-
- Full failure log: ${failedLogPath}
|
|
1234
|
-
|
|
1235
|
-
# Original PRD body (this is what the job was trying to do)
|
|
1236
|
-
\`\`\`
|
|
1237
|
-
${originalBody}
|
|
1238
|
-
\`\`\`
|
|
1239
|
-
|
|
1240
|
-
# Last ~16KB of the failure log (stream-json format from \`claude -p\`)
|
|
1241
|
-
\`\`\`
|
|
1242
|
-
${logTail}
|
|
1243
|
-
\`\`\`
|
|
1244
|
-
|
|
1245
|
-
# Your task
|
|
1246
|
-
1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
|
|
1247
|
-
2. Read source files in ${cwd} as needed to understand the context.
|
|
1248
|
-
3. Identify the root cause of the failure.
|
|
1249
|
-
4. Write a NEW fix-plan PRD file at exactly this path:
|
|
1250
|
-
|
|
1251
|
-
${fixPath}
|
|
1252
|
-
|
|
1253
|
-
5. The frontmatter MUST be exactly this format (no extra keys):
|
|
1254
|
-
\`\`\`
|
|
1255
|
-
---
|
|
1256
|
-
title: Fix: <short summary of the fix>
|
|
1257
|
-
cwd: ${cwd}
|
|
1258
|
-
parallelGroup: ${group}
|
|
1259
|
-
estimateMinutes: <your time estimate>
|
|
1260
|
-
---
|
|
1261
|
-
\`\`\`
|
|
1262
|
-
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
1263
|
-
- Root-cause analysis (what went wrong and why)
|
|
1264
|
-
- Concrete fix steps (specific files / commands / edits)
|
|
1265
|
-
- Verification command(s) the next agent should run to confirm the fix
|
|
1266
|
-
- Acceptance criteria
|
|
1267
|
-
|
|
1268
|
-
DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
|
|
1306
|
+
const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
|
|
1269
1307
|
|
|
1270
1308
|
// Phase 1: open log fd for pre-spawn diagnostics.
|
|
1271
1309
|
const { fd, safeLog, closeFd } = openLog(investigationLogPath);
|
|
@@ -1329,6 +1367,13 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
|
|
|
1329
1367
|
console.log(`[scheduler] investigation produced fix plan: ${fixSlug}`);
|
|
1330
1368
|
} else {
|
|
1331
1369
|
console.log(`[scheduler] investigation finished WITHOUT producing fix plan (slug=${failedJob.slug}, code=${exitCode})`);
|
|
1370
|
+
// Record the no-plan outcome so selectAutoFixTargets can offer one
|
|
1371
|
+
// bounded retry instead of permanently dead-ending behind the
|
|
1372
|
+
// 1-attempt cap (autoFixAttempted stays true).
|
|
1373
|
+
mutate((s) => {
|
|
1374
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
1375
|
+
if (j) j.autoFixOutcome = 'no-plan';
|
|
1376
|
+
}).catch(() => {});
|
|
1332
1377
|
}
|
|
1333
1378
|
// Trigger a tick so the new fix plan is reconciled into the queue and fired.
|
|
1334
1379
|
tickQueue().catch(() => {});
|
|
@@ -1525,17 +1570,16 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1525
1570
|
// this, the queue stalls indefinitely behind a stale failure even
|
|
1526
1571
|
// though the auto-recovery did its job.
|
|
1527
1572
|
if (effectiveStatus === 'completed' && isFixPlanSlug(job.slug)) {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
s.jobs[orig].completedBy = job.slug;
|
|
1573
|
+
const orig = healTargetForFix(job.slug, s.jobs);
|
|
1574
|
+
if (orig) {
|
|
1575
|
+
const priorStatus = orig.status;
|
|
1576
|
+
console.log(`[scheduler] auto-promote: ${orig.slug} (${priorStatus}) → completed because ${job.slug} succeeded`);
|
|
1577
|
+
orig.status = 'completed';
|
|
1578
|
+
orig.exitCode = 0;
|
|
1579
|
+
orig.error = null;
|
|
1580
|
+
orig.completedBy = job.slug;
|
|
1537
1581
|
if (priorStatus === 'needs_review') {
|
|
1538
|
-
delete
|
|
1582
|
+
delete orig.verifierVerdict;
|
|
1539
1583
|
}
|
|
1540
1584
|
}
|
|
1541
1585
|
}
|
|
@@ -1881,16 +1925,57 @@ function selectHistoryJobs(jobs, limit) {
|
|
|
1881
1925
|
// heal a genuinely-unfinished job.
|
|
1882
1926
|
const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable']);
|
|
1883
1927
|
|
|
1928
|
+
/**
|
|
1929
|
+
* Backfill a job's missing runId by scanning RUNS_DIR for a run directory
|
|
1930
|
+
* whose contents reference this job's slug (a '<slug>.log' file inside it).
|
|
1931
|
+
* A needs_review job can lose its runId (e.g. an old queue-schema gap) and
|
|
1932
|
+
* become invisible to every auto-resolution path that requires job.runId
|
|
1933
|
+
* truthy. O(number of run dirs) — a single readdir + per-dir existsSync
|
|
1934
|
+
* check, no nested loop over user-scaled data. Dir names are ISO timestamps,
|
|
1935
|
+
* so lexical-descending sort picks the newest match. Exported for tests.
|
|
1936
|
+
*/
|
|
1937
|
+
function resolveRunId(job, { runsDir = RUNS_DIR } = {}) {
|
|
1938
|
+
if (!job || job.runId) return job?.runId || null;
|
|
1939
|
+
if (!job.slug) return null;
|
|
1940
|
+
let dirs;
|
|
1941
|
+
try {
|
|
1942
|
+
dirs = fs.readdirSync(runsDir);
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
const matches = dirs.filter((d) => {
|
|
1947
|
+
try {
|
|
1948
|
+
return fs.existsSync(path.join(runsDir, d, `${job.slug}.log`));
|
|
1949
|
+
} catch {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
if (!matches.length) return null;
|
|
1954
|
+
matches.sort().reverse();
|
|
1955
|
+
return matches[0];
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/**
|
|
1959
|
+
* Pure predicate: a needs_review job with no runId and no backfillable run
|
|
1960
|
+
* directory can never self-heal or get an auto-fix investigation — it must
|
|
1961
|
+
* be surfaced for a human rather than silently dead-ended. Exported for
|
|
1962
|
+
* tests.
|
|
1963
|
+
*/
|
|
1964
|
+
function isUnresolvableNeedsReview(job, { hasRunDir }) {
|
|
1965
|
+
return !!job && job.status === 'needs_review' && !job.runId && !hasRunDir;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1884
1968
|
/**
|
|
1885
1969
|
* Pure predicate: is this job eligible for the boot re-verify self-heal? Only
|
|
1886
|
-
* needs_review jobs with a run log
|
|
1887
|
-
* EXCLUDES 'uncommitted_changes' (git
|
|
1888
|
-
* so re-scanning it would falsely
|
|
1970
|
+
* needs_review jobs with a run log (own or backfilled via resolveRunId) AND a
|
|
1971
|
+
* transcript-scan verdict. Crucially EXCLUDES 'uncommitted_changes' (git
|
|
1972
|
+
* commit-guard) — verifyRun can't see git, so re-scanning it would falsely
|
|
1973
|
+
* heal an unfinished job. Exported for tests.
|
|
1889
1974
|
*/
|
|
1890
1975
|
function isRescanCandidate(job) {
|
|
1891
1976
|
return !!job
|
|
1892
1977
|
&& job.status === 'needs_review'
|
|
1893
|
-
&& !!job.runId
|
|
1978
|
+
&& !!(job.runId || resolveRunId(job))
|
|
1894
1979
|
&& RESCANNABLE_VERDICTS.has(job.verifierVerdict);
|
|
1895
1980
|
}
|
|
1896
1981
|
|
|
@@ -1906,23 +1991,32 @@ function isRescanCandidate(job) {
|
|
|
1906
1991
|
* @returns {Promise<{rescanned:number, healed:string[]}>}
|
|
1907
1992
|
*/
|
|
1908
1993
|
/**
|
|
1909
|
-
* Pure helper — no I/O
|
|
1910
|
-
*
|
|
1994
|
+
* Pure helper — no I/O by default (resolveJobRunId is injectable; production
|
|
1995
|
+
* caller passes resolveRunId, which does touch disk). Returns the subset of
|
|
1996
|
+
* jobs eligible for automatic fix-plan authoring after a reverify pass
|
|
1997
|
+
* leaves them still in needs_review.
|
|
1911
1998
|
*
|
|
1912
1999
|
* Exclusion rules (all must pass):
|
|
1913
2000
|
* - status === 'needs_review'
|
|
1914
|
-
* -
|
|
1915
|
-
* - autoFixAttempted !== true (1-attempt cap)
|
|
2001
|
+
* - a runId, own or backfilled via resolveJobRunId (need a run log to investigate)
|
|
1916
2002
|
* - not itself a fix-plan slug (avoids infinite recursion)
|
|
2003
|
+
* - autoFixAttempted cap: a job with no prior attempt is eligible; a job
|
|
2004
|
+
* whose prior attempt outcome was 'no-plan' gets ONE bounded retry
|
|
2005
|
+
* (autoFixRetries < 1); any other attempted job (or an exhausted
|
|
2006
|
+
* no-plan retry) is excluded
|
|
1917
2007
|
* - no fix sibling on disk (fixSlugExists) or already in the queue
|
|
1918
2008
|
*/
|
|
1919
|
-
function selectAutoFixTargets(jobs, { fixSlugExists }) {
|
|
2009
|
+
function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRunId }) {
|
|
1920
2010
|
const slugsInQueue = new Set(jobs.map((j) => j.slug));
|
|
1921
2011
|
return jobs.filter((job) => {
|
|
1922
2012
|
if (job.status !== 'needs_review') return false;
|
|
1923
|
-
|
|
1924
|
-
if (
|
|
2013
|
+
const runId = job.runId || resolveJobRunId(job);
|
|
2014
|
+
if (!runId) return false;
|
|
1925
2015
|
if (isFixPlanSlug(job.slug)) return false;
|
|
2016
|
+
if (job.autoFixAttempted) {
|
|
2017
|
+
if (job.autoFixOutcome !== 'no-plan') return false;
|
|
2018
|
+
if ((job.autoFixRetries ?? 0) >= 1) return false;
|
|
2019
|
+
}
|
|
1926
2020
|
const fixSlug = `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
|
|
1927
2021
|
if (fixSlugExists(fixSlug)) return false;
|
|
1928
2022
|
if (slugsInQueue.has(fixSlug)) return false;
|
|
@@ -1936,7 +2030,7 @@ async function reverifyNeedsReview() {
|
|
|
1936
2030
|
const healed = [];
|
|
1937
2031
|
const leftForReview = [];
|
|
1938
2032
|
for (const job of candidates) {
|
|
1939
|
-
const runDir = path.join(RUNS_DIR, job.runId);
|
|
2033
|
+
const runDir = path.join(RUNS_DIR, job.runId || resolveRunId(job));
|
|
1940
2034
|
const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
|
|
1941
2035
|
// Derive committedDuringRun from the recorded run window. The live
|
|
1942
2036
|
// commit-guard uses gitHead() (before/after HEAD diff); here the run is
|
|
@@ -1978,24 +2072,72 @@ async function reverifyNeedsReview() {
|
|
|
1978
2072
|
console.log(`[scheduler] boot reverify: left for review: ${detail}`);
|
|
1979
2073
|
}
|
|
1980
2074
|
|
|
2075
|
+
// Surface needs_review jobs that can never self-heal or get an auto-fix
|
|
2076
|
+
// investigation (no runId, no backfillable run dir) instead of leaving
|
|
2077
|
+
// them silently stranded. Also surface no-plan auto-fix jobs whose one
|
|
2078
|
+
// bounded retry is already exhausted. Both are annotated, never looped on.
|
|
2079
|
+
const afterHealForAnnotate = await readQueue();
|
|
2080
|
+
const unresolvable = afterHealForAnnotate.jobs.filter(
|
|
2081
|
+
(j) => isUnresolvableNeedsReview(j, { hasRunDir: !!resolveRunId(j) }) && j.verifierVerdict !== 'no_run_artifacts',
|
|
2082
|
+
);
|
|
2083
|
+
const exhaustedNoPlan = afterHealForAnnotate.jobs.filter(
|
|
2084
|
+
(j) => j.status === 'needs_review'
|
|
2085
|
+
&& j.autoFixAttempted && j.autoFixOutcome === 'no-plan' && (j.autoFixRetries ?? 0) >= 1
|
|
2086
|
+
&& j.verifierVerdict !== 'autofix_no_plan',
|
|
2087
|
+
);
|
|
2088
|
+
if (unresolvable.length || exhaustedNoPlan.length) {
|
|
2089
|
+
const unresolvableSet = new Set(unresolvable.map((j) => j.slug));
|
|
2090
|
+
const exhaustedSet = new Set(exhaustedNoPlan.map((j) => j.slug));
|
|
2091
|
+
await mutate((s) => {
|
|
2092
|
+
for (const j of s.jobs) {
|
|
2093
|
+
if (unresolvableSet.has(j.slug)) {
|
|
2094
|
+
j.verifierVerdict = 'no_run_artifacts';
|
|
2095
|
+
j.error = 'no run artifacts — manual review';
|
|
2096
|
+
} else if (exhaustedSet.has(j.slug)) {
|
|
2097
|
+
j.verifierVerdict = 'autofix_no_plan';
|
|
2098
|
+
j.error = 'auto-fix investigation produced no fix plan after retry — manual review';
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
if (unresolvable.length) {
|
|
2103
|
+
console.log(`[scheduler] boot reverify: no run artifacts for ${unresolvable.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
2104
|
+
}
|
|
2105
|
+
if (exhaustedNoPlan.length) {
|
|
2106
|
+
console.log(`[scheduler] boot reverify: auto-fix retry exhausted for ${exhaustedNoPlan.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
2107
|
+
}
|
|
2108
|
+
await broadcast();
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1981
2111
|
// Auto-fix: spawn a fix-plan investigation for each job still in
|
|
1982
2112
|
// needs_review after the heal pass (kill-switch: SM_AUTOFIX_DISABLE=1).
|
|
2113
|
+
// spawnInvestigation early-returns once investigationsInFlight reaches
|
|
2114
|
+
// MAX_CONCURRENT_INVESTIGATIONS (queues the rest for retry), so this loop
|
|
2115
|
+
// cannot fan out past the cap regardless of how many targets are selected.
|
|
1983
2116
|
if (process.env.SM_AUTOFIX_DISABLE !== '1') {
|
|
1984
2117
|
const afterHeal = await readQueue();
|
|
1985
2118
|
const targets = selectAutoFixTargets(afterHeal.jobs, {
|
|
1986
2119
|
fixSlugExists: (s) => fs.existsSync(path.join(PRDS_DIR, `${s}.md`)),
|
|
1987
2120
|
});
|
|
1988
2121
|
for (const job of targets) {
|
|
1989
|
-
const
|
|
2122
|
+
const runId = job.runId || resolveRunId(job);
|
|
2123
|
+
const runDir = path.join(RUNS_DIR, runId);
|
|
2124
|
+
const isNoPlanRetry = job.autoFixAttempted && job.autoFixOutcome === 'no-plan';
|
|
1990
2125
|
// Persist the attempt BEFORE spawning — a crash mid-investigation still
|
|
1991
2126
|
// counts it (mirrors orphanRetries). Safe even when the slot is busy: the
|
|
1992
2127
|
// investigation is queued and drained as slots free, so it is genuinely
|
|
1993
2128
|
// attempted rather than silently dropped.
|
|
1994
2129
|
await mutate((s) => {
|
|
1995
2130
|
const j = s.jobs.find((x) => x.slug === job.slug);
|
|
1996
|
-
if (j)
|
|
2131
|
+
if (j) {
|
|
2132
|
+
j.autoFixAttempted = true;
|
|
2133
|
+
if (!j.runId && runId) j.runId = runId;
|
|
2134
|
+
if (isNoPlanRetry) {
|
|
2135
|
+
j.autoFixRetries = (j.autoFixRetries ?? 0) + 1;
|
|
2136
|
+
delete j.autoFixOutcome;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1997
2139
|
});
|
|
1998
|
-
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (1/1)`);
|
|
2140
|
+
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (${isNoPlanRetry ? 'retry' : '1/1'})`);
|
|
1999
2141
|
spawnInvestigation(job, runDir).catch((e) => {
|
|
2000
2142
|
console.error('[scheduler] auto-fix spawnInvestigation error', job.slug, e);
|
|
2001
2143
|
});
|
|
@@ -2335,10 +2477,16 @@ async function init() {
|
|
|
2335
2477
|
// Periodic self-heal: re-run the verifier over stale needs_review jobs so a
|
|
2336
2478
|
// job whose work actually landed (committed in-window, no FAIL sentinel)
|
|
2337
2479
|
// auto-clears WITHOUT waiting for the next app restart. Cheap-guarded — the
|
|
2338
|
-
// log scan only runs when something is actually flagged.
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2480
|
+
// log scan only runs when something is actually flagged. Kill-switch:
|
|
2481
|
+
// SM_REVERIFY_PERIODIC_DISABLE=1 (boot reverify above stays always-on).
|
|
2482
|
+
// reverifyNeedsReview's auto-fix loop is capped downstream by
|
|
2483
|
+
// MAX_CONCURRENT_INVESTIGATIONS (spawnInvestigation queues/early-returns
|
|
2484
|
+
// past it), so this interval firing cannot fan out investigations.
|
|
2485
|
+
if (process.env.SM_REVERIFY_PERIODIC_DISABLE !== '1') {
|
|
2486
|
+
const s = readQueueSync();
|
|
2487
|
+
if (s.jobs.some((j) => j.status === 'needs_review')) {
|
|
2488
|
+
reverifyNeedsReview().catch(() => {});
|
|
2489
|
+
}
|
|
2342
2490
|
}
|
|
2343
2491
|
}, 10 * 60_000);
|
|
2344
2492
|
|
|
@@ -2510,4 +2658,4 @@ const remote = {
|
|
|
2510
2658
|
},
|
|
2511
2659
|
};
|
|
2512
2660
|
|
|
2513
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets };
|
|
2661
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -198,6 +198,21 @@ export type BillingFetchResult =
|
|
|
198
198
|
| { kind: 'transient'; message: string; httpStatus: number | null }
|
|
199
199
|
| { kind: 'config'; message: string };
|
|
200
200
|
|
|
201
|
+
// ── MCP server live connection probe (`claude mcp list`)
|
|
202
|
+
export interface McpServerStatus {
|
|
203
|
+
name: string;
|
|
204
|
+
target: string;
|
|
205
|
+
transport: 'stdio' | 'http' | 'sse' | 'ws' | 'unknown';
|
|
206
|
+
status: 'connected' | 'failed' | 'needs-auth' | 'pending' | 'unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface McpStatusResult {
|
|
210
|
+
ok: boolean;
|
|
211
|
+
servers: McpServerStatus[];
|
|
212
|
+
error?: string;
|
|
213
|
+
checkedAt: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
201
216
|
// ── Usage matrix (AgOps dashboard, main-side aggregator)
|
|
202
217
|
export type UsageMatrixState = 'idle' | 'executing' | 'plan' | 'background';
|
|
203
218
|
export type UsageMatrixIntensity = 'idle' | 'low' | 'medium' | 'critical';
|
|
@@ -978,6 +993,12 @@ export interface ChatRunErrorEvent {
|
|
|
978
993
|
message: string;
|
|
979
994
|
}
|
|
980
995
|
|
|
996
|
+
export interface ChatRunNoticeEvent {
|
|
997
|
+
tabId: string;
|
|
998
|
+
sessionId: string;
|
|
999
|
+
message: string;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
981
1002
|
export interface SessionManagerAPI {
|
|
982
1003
|
app: {
|
|
983
1004
|
version: () => Promise<string>;
|
|
@@ -1058,6 +1079,8 @@ export interface SessionManagerAPI {
|
|
|
1058
1079
|
| { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
|
|
1059
1080
|
| { ok: false; error: string }
|
|
1060
1081
|
>;
|
|
1082
|
+
/** Fired when the embedded page tries to open a new window (target="_blank", window.open, ctrl/cmd-click) — the main process denies the native popup and forwards the URL here. */
|
|
1083
|
+
onOpenTabRequest: (handler: (payload: { url: string }) => void) => () => void;
|
|
1061
1084
|
};
|
|
1062
1085
|
transcripts: {
|
|
1063
1086
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
@@ -1076,6 +1099,9 @@ export interface SessionManagerAPI {
|
|
|
1076
1099
|
billing: {
|
|
1077
1100
|
fetch: () => Promise<BillingFetchResult>;
|
|
1078
1101
|
};
|
|
1102
|
+
mcp: {
|
|
1103
|
+
status: () => Promise<McpStatusResult>;
|
|
1104
|
+
};
|
|
1079
1105
|
usageMatrix: {
|
|
1080
1106
|
snapshot: () => Promise<UsageMatrixSnapshot>;
|
|
1081
1107
|
onTick: (handler: (snap: UsageMatrixSnapshot) => void) => () => void;
|
|
@@ -1226,6 +1252,13 @@ export interface SessionManagerAPI {
|
|
|
1226
1252
|
| { ok: true; path: string; bytes: number }
|
|
1227
1253
|
| { ok: false; empty?: true; error?: string }
|
|
1228
1254
|
>;
|
|
1255
|
+
/** Ctrl+V text paste — reads OS clipboard text via Electron's native API
|
|
1256
|
+
* (renderer's navigator.clipboard.readText() is denied by the
|
|
1257
|
+
* permission-request handler, which only allows media permissions). */
|
|
1258
|
+
pasteText: () => Promise<
|
|
1259
|
+
| { ok: true; text: string }
|
|
1260
|
+
| { ok: false; error?: string }
|
|
1261
|
+
>;
|
|
1229
1262
|
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1230
1263
|
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1231
1264
|
};
|
|
@@ -1311,6 +1344,7 @@ export interface SessionManagerAPI {
|
|
|
1311
1344
|
onNeedsInput: (handler: (e: ChatRunNeedsInputEvent) => void) => () => void;
|
|
1312
1345
|
onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
|
|
1313
1346
|
onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
|
|
1347
|
+
onNotice: (handler: (e: ChatRunNoticeEvent) => void) => () => void;
|
|
1314
1348
|
};
|
|
1315
1349
|
exchanges: {
|
|
1316
1350
|
/** Durable per-exchange log entries for a project, newest-first (max 100 by default). */
|
package/src/preload/index.cjs
CHANGED
|
@@ -107,6 +107,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
107
107
|
return () => ipcRenderer.removeListener(channel, listener);
|
|
108
108
|
},
|
|
109
109
|
capture: (payload) => ipcRenderer.invoke('browser:capture', payload),
|
|
110
|
+
onOpenTabRequest: (handler) => {
|
|
111
|
+
const listener = (_e, payload) => handler(payload);
|
|
112
|
+
ipcRenderer.on('browser:open-tab-request', listener);
|
|
113
|
+
return () => ipcRenderer.removeListener('browser:open-tab-request', listener);
|
|
114
|
+
},
|
|
110
115
|
},
|
|
111
116
|
transcripts: {
|
|
112
117
|
subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
|
|
@@ -129,6 +134,9 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
129
134
|
billing: {
|
|
130
135
|
fetch: () => ipcRenderer.invoke('billing:fetch'),
|
|
131
136
|
},
|
|
137
|
+
mcp: {
|
|
138
|
+
status: () => ipcRenderer.invoke('mcp:status'),
|
|
139
|
+
},
|
|
132
140
|
usageMatrix: {
|
|
133
141
|
snapshot: () => ipcRenderer.invoke('usage:matrix:snapshot'),
|
|
134
142
|
onTick: (handler) => {
|
|
@@ -279,6 +287,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
279
287
|
},
|
|
280
288
|
clipboard: {
|
|
281
289
|
pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
|
|
290
|
+
pasteText: () => ipcRenderer.invoke('clipboard:paste-text'),
|
|
282
291
|
copyImage: (dataUrl) => ipcRenderer.invoke('browser:copy-image', { dataUrl }),
|
|
283
292
|
},
|
|
284
293
|
memory: {
|
|
@@ -406,6 +415,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
406
415
|
ipcRenderer.on('chat:run:error', listener);
|
|
407
416
|
return () => ipcRenderer.removeListener('chat:run:error', listener);
|
|
408
417
|
},
|
|
418
|
+
onNotice: (handler) => {
|
|
419
|
+
const listener = (_e, payload) => handler(payload);
|
|
420
|
+
ipcRenderer.on('chat:run:notice', listener);
|
|
421
|
+
return () => ipcRenderer.removeListener('chat:run:notice', listener);
|
|
422
|
+
},
|
|
409
423
|
},
|
|
410
424
|
exchanges: {
|
|
411
425
|
/** List exchanges for a project (durable chat-run log), newest-first.
|