claude-code-session-manager 0.119.1 → 0.121.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-BpW0pGgk.js → AgentLibrary-FO2ei2wB.js} +1 -1
- package/dist/assets/{Button-DZX5guWb.js → Button-CeIOXUPU.js} +1 -1
- package/dist/assets/{DataModel-WG4pG_4z.js → DataModel-D0p__Hil.js} +1 -1
- package/dist/assets/{History-D58whvn_.js → History-DVqQ5azN.js} +1 -1
- package/dist/assets/{Hooks-DCI9ezg4.js → Hooks-DkTnRHyT.js} +1 -1
- package/dist/assets/{Library-YqMi-vsA.js → Library-l7KS6pkE.js} +1 -1
- package/dist/assets/{ListDetail-ap4W5z_Z.js → ListDetail-DDw8fuRT.js} +1 -1
- package/dist/assets/{McpServers-CT0pBTF8.js → McpServers-UUNIxk_3.js} +1 -1
- package/dist/assets/{Memory-BODDUBy4.js → Memory-CNYUoQ8C.js} +1 -1
- package/dist/assets/{Panel-CeOfUS9B.js → Panel-Bb54soc2.js} +1 -1
- package/dist/assets/{Permissions-DyOqpLQx.js → Permissions-D-dNdV3f.js} +1 -1
- package/dist/assets/{Plugins-DBXzKjL2.js → Plugins-DhcIXRhQ.js} +2 -2
- package/dist/assets/{ProvenanceBadge-C47IX1Q6.js → ProvenanceBadge-DEAkDNjA.js} +1 -1
- package/dist/assets/{RemoteMachines-CtkAJvkZ.js → RemoteMachines-xCEgvTGU.js} +1 -1
- package/dist/assets/{SaveBar-YMtzcCzP.js → SaveBar-BSEs7ecA.js} +1 -1
- package/dist/assets/{Scheduler-BNiYg0YD.js → Scheduler-BIKJIU-C.js} +5 -5
- package/dist/assets/{ScopeSwitcher-Dgi8CiDi.js → ScopeSwitcher-DcaFyAV-.js} +1 -1
- package/dist/assets/Settings-vcdWy_Jx.js +4 -0
- package/dist/assets/{SkillReferenceGraph-CaFsccNI.js → SkillReferenceGraph-CLrJWTPd.js} +1 -1
- package/dist/assets/{Skills-BeCMBQ3l.js → Skills-CYM4rBEC.js} +1 -1
- package/dist/assets/{SystemPrompt-C3WmcPEF.js → SystemPrompt-BWNIu-iY.js} +1 -1
- package/dist/assets/{TagLibrary-DP4GH7Dr.js → TagLibrary-CYdwHK6m.js} +1 -1
- package/dist/assets/{TiptapBody-CXCMvRr_.js → TiptapBody-Kt90lHrD.js} +1 -1
- package/dist/assets/{Toggle-TY-Wa8hv.js → Toggle-D1OXWYOz.js} +1 -1
- package/dist/assets/{ViewTabs-CaacRnp6.js → ViewTabs-BeQR3CKh.js} +1 -1
- package/dist/assets/{historyTotals-C28IlVVQ.js → historyTotals-B0qafrRo.js} +1 -1
- package/dist/assets/{index-DdEMkmbi.js → index-Bq9uH08R.js} +201 -201
- package/dist/assets/{index-DAMBm3UN.css → index-Dd7UQabV.css} +1 -1
- package/dist/assets/{settingsSchema-7Yg6dNNB.js → settingsSchema-0ZksSAPB.js} +1 -1
- package/dist/assets/{skillFrontmatter-Dcuys1km.js → skillFrontmatter-C0nh8yfa.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-B0H4M-TT.js → useAgentsDirWatch-DPkoBybS.js} +1 -1
- package/dist/assets/{useRemotePeers-Zp9jchCS.js → useRemotePeers-BDJMUihy.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 +54 -14
- 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 +18 -4
- 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/persona-macro/macroLibrary.cjs +75 -0
- package/src/main/lib/prd/prdCreate.cjs +25 -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/remoteTranscriptTail.cjs +69 -2
- 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 +82 -91
- package/src/preload/api.d.ts +41 -3
- package/src/preload/index.cjs +9 -0
- package/dist/assets/Settings-B4LuoRnd.js +0 -4
package/src/main/scheduler.cjs
CHANGED
|
@@ -57,6 +57,7 @@ const billing = require('./usage.cjs');
|
|
|
57
57
|
const { cleanChildEnv, pathWithUserBins } = require('./lib/process/cleanEnv.cjs');
|
|
58
58
|
const { ensureTimeoutShim, withTimeoutShimOnPath } = require('./lib/process/timeoutShim.cjs');
|
|
59
59
|
const supervisor = require('./supervisor.cjs');
|
|
60
|
+
const { processGroupRssMb } = require('./lib/process/procGroupRss.cjs');
|
|
60
61
|
const { resolveClaudeBin, claudeSpawnTarget, probeClaudeVersion } = require('./lib/claude/claudeBin.cjs');
|
|
61
62
|
const { headlessPermissionArgs, ensureCliCapsProbed } = require('./lib/claude/claudeCliCaps.cjs');
|
|
62
63
|
// Fire-and-forget priming — by the time the first job dispatches, the cached
|
|
@@ -81,6 +82,10 @@ const { stripAppOwnedChurn } = require('./lib/scheduler/jobDirtFilter.cjs');
|
|
|
81
82
|
const { GATE_AUTHORITY_VERDICTS, decideGateAuthority } = require('./lib/scheduler/gateAuthority.cjs');
|
|
82
83
|
const { decideBaselineAuthority } = require('./lib/scheduler/baselineGate.cjs');
|
|
83
84
|
const { linkDependencyDirs } = require('./lib/git/worktreeDeps.cjs');
|
|
85
|
+
const { checkMainDependencyHealth, repairMainDependencies } = require('./lib/git/depHealth.cjs');
|
|
86
|
+
// cwd -> last main-checkout dependency repair attempt (ms); caps repair to once per cooldown.
|
|
87
|
+
const mainDepsRepairAt = new Map();
|
|
88
|
+
const MAIN_DEPS_REPAIR_COOLDOWN_MS = 10 * 60_000;
|
|
84
89
|
const { resolveBindingRateLimitReset } = require('./lib/claude/rateLimitWindow.cjs');
|
|
85
90
|
const { isResetFresh, bindingWindow, degradedBudget, meterFailureBudget } = require('./lib/claude/usageCircuit.cjs');
|
|
86
91
|
const { computeQueueHealth } = require('./lib/scheduler/queueHealth.cjs');
|
|
@@ -146,6 +151,7 @@ const queueOps = require('./queueOps.cjs');
|
|
|
146
151
|
const { listEpicPrdDirs, listArchivedPrdDirs, deriveProjectCwdFromPrdPath } = require('./lib/prd/prdLocations.cjs');
|
|
147
152
|
const { ensureEpic, appendPrdCreatedEvent, readActiveIndex } = require('./lib/epic/epicMint.cjs');
|
|
148
153
|
const agentModelResolve = require('./lib/claude/agentModelResolve.cjs');
|
|
154
|
+
const { buildPersonaToolScope, personaToolScopeArgs } = require('./lib/claude/personaToolScope.cjs');
|
|
149
155
|
const { resolveEpicEffort, effortArgs } = require('./lib/claude/agentEffortResolve.cjs');
|
|
150
156
|
const { transitionJob, STATUS_HISTORY_CAP, LEGAL_TRANSITIONS } = require('./lib/scheduler/scheduleJobTransitions.cjs');
|
|
151
157
|
const { buildContextDigest, composeExecutorPrompt } = require('./lib/epic/epicContextDigest.cjs');
|
|
@@ -411,7 +417,9 @@ const HEARTBEAT_MAX_BYTES = 1024 * 1024;
|
|
|
411
417
|
// DEFAULT_PROJECT_CWD imported from lib/scheduler/schedulerBatch.cjs (single source of truth).
|
|
412
418
|
|
|
413
419
|
// Each headless claude -p job can shell out to tsc/vite/pytest and grow well
|
|
414
|
-
// past 1 GB at peak; reserve 2.5 GB per running+pending slot.
|
|
420
|
+
// past 1 GB at peak; reserve 2.5 GB of PEAK headroom per running+pending slot.
|
|
421
|
+
// A running job's already-resident memory is credited back (consumedMb in
|
|
422
|
+
// memoryLimitedBatchSize) because MemAvailable is already net of it. Raised from 1.5 GB
|
|
415
423
|
// after the 2026-06-16 OOM: 3 concurrent cross-project jobs + their build
|
|
416
424
|
// subprocesses pushed a 24 GB host ~10 GB into swap and the OOM killer took
|
|
417
425
|
// Electron — every pty got SIGHUP (code=0 signal=1).
|
|
@@ -478,10 +486,13 @@ function getAvailableMemMb() {
|
|
|
478
486
|
* drop available memory below MIN_FREE_MB_PER_JOB per active slot.
|
|
479
487
|
* Exported for unit tests.
|
|
480
488
|
*/
|
|
481
|
-
function memoryLimitedBatchSize(availableMb, minPerJob, runningCount, batchLen) {
|
|
489
|
+
function memoryLimitedBatchSize(availableMb, minPerJob, runningCount, batchLen, consumedMb = 0) {
|
|
482
490
|
if (availableMb === Infinity) return batchLen;
|
|
491
|
+
// Credit is bounded by the running jobs' own reservations: never negative,
|
|
492
|
+
// never more permissive than a 0-running machine.
|
|
493
|
+
const credit = Math.min(Math.max(Number(consumedMb) || 0, 0), minPerJob * runningCount);
|
|
483
494
|
let allowed = batchLen;
|
|
484
|
-
while (allowed > 0 && availableMb < minPerJob * (runningCount + allowed)) {
|
|
495
|
+
while (allowed > 0 && availableMb < minPerJob * (runningCount + allowed) - credit) {
|
|
485
496
|
allowed--;
|
|
486
497
|
}
|
|
487
498
|
return allowed;
|
|
@@ -522,6 +533,26 @@ function biasJobOomScore(pid) {
|
|
|
522
533
|
// source and a search fallback. Live PRDs resolve per-job to
|
|
523
534
|
// `<job.cwd>/session-manager-operations/scheduler/prds/` via prdLocations.cjs.
|
|
524
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Last-line guard before any 'prd-missing' retire: bust the project-cwd cache
|
|
538
|
+
* and re-check every job-scoped dir plus findPrdDirForJob for
|
|
539
|
+
* `<dir>/<slug>.md`. Returns the first existing absolute path, or null. A
|
|
540
|
+
* stale allProjectCwds scan must never retire a PRD whose file exists.
|
|
541
|
+
*/
|
|
542
|
+
async function recheckPrdSourceForJob(job) {
|
|
543
|
+
const slug = job && job.slug;
|
|
544
|
+
if (typeof slug !== 'string' || !slug || path.basename(slug) !== slug) return null;
|
|
545
|
+
try { require('./lib/activeSessions.cjs').bustProjectCwdCache(); } catch { /* best-effort */ }
|
|
546
|
+
const dirs = [...(await prdPaths.jobScopedPrdDirs(job))];
|
|
547
|
+
const found = await prdPaths.findPrdDirForJob(job);
|
|
548
|
+
if (found) dirs.push(found);
|
|
549
|
+
for (const dir of dirs) {
|
|
550
|
+
const candidate = path.join(dir, `${slug}.md`);
|
|
551
|
+
try { await fsp.access(candidate); return candidate; } catch { /* not here */ }
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
525
556
|
/**
|
|
526
557
|
* Build the non-failure result + run meta for a queued job whose PRD source
|
|
527
558
|
* is gone. Shared by both PRD-read failure exits in executeJob so the
|
|
@@ -2665,7 +2696,12 @@ async function clearPause(source) {
|
|
|
2665
2696
|
*/
|
|
2666
2697
|
function resetJobFields(job, errorMsg, opts = {}) {
|
|
2667
2698
|
if ((job.status === 'completed' || job.status === 'skipped') && opts.force !== true) return false;
|
|
2699
|
+
// Durable cancel (2026-10-19 git-viewer incident): a human-cancelled row is
|
|
2700
|
+
// revived only by an explicit human reset, never by an automated pass.
|
|
2701
|
+
if (reviewLadder.isCancelledJob(job) && opts.source !== 'remote:resetJob' && opts.source !== 'ipc:schedule:reset-job') return false;
|
|
2668
2702
|
if (!transitionJob(job, 'pending', { reason: errorMsg ?? 'reset to pending', source: opts.source ?? 'resetJobFields' })) return false;
|
|
2703
|
+
delete job.cancelledAt;
|
|
2704
|
+
delete job.cancelledRunId;
|
|
2669
2705
|
job.runId = null;
|
|
2670
2706
|
job.startedAt = null;
|
|
2671
2707
|
job.finishedAt = null;
|
|
@@ -3093,6 +3129,13 @@ async function notifyOriginatingTab(job, {
|
|
|
3093
3129
|
+ (failures.length ? `. Pre-existing failures: ${shown}${more}` : '')
|
|
3094
3130
|
+ '. Check Scheduler for details.';
|
|
3095
3131
|
}
|
|
3132
|
+
if (job.status === 'skipped' && Array.isArray(job.blockedDependents) && job.blockedDependents.length > 0) {
|
|
3133
|
+
const deps = job.blockedDependents;
|
|
3134
|
+
const shown = deps.slice(0, 10).join(', ');
|
|
3135
|
+
const more = deps.length > 10 ? ` (+${deps.length - 10} more)` : '';
|
|
3136
|
+
message = `PRD ${job.slug} was skipped: its PRD source could not be found on disk. Blocked dependents: ${shown}${more}.`
|
|
3137
|
+
+ ' If the PRD file exists, run scheduler_reset_job with force:true; if it was intentionally removed, archive it with scheduler_archive_prd.';
|
|
3138
|
+
}
|
|
3096
3139
|
|
|
3097
3140
|
// The EPIC this check-in belongs to. Deliberately does NOT consider
|
|
3098
3141
|
// `sourceTabId` — that is a Terminal tab id, not an Epic id, and
|
|
@@ -4142,16 +4185,21 @@ const HEADLESS_DISALLOWED_TOOLS = Object.freeze([
|
|
|
4142
4185
|
* persona body, resolved by agentModelResolve.cjs's resolvePrdPersonaForSpawn),
|
|
4143
4186
|
* is passed as `--append-system-prompt` so the executor IS that persona at
|
|
4144
4187
|
* launch rather than being asked in prose to adopt one.
|
|
4188
|
+
* `toolScope` (buildPersonaToolScope of the persona's `tools:` list; null =
|
|
4189
|
+
* unscoped) adds `--agents/--agent` so the persona's tool list is enforced —
|
|
4190
|
+
* `--allowedTools` is a no-op under --dangerously-skip-permissions. It is
|
|
4191
|
+
* inserted BEFORE that flag, never between `--disallowedTools` and its value.
|
|
4145
4192
|
* `--disallowedTools` takes a variadic list, so another flag must always
|
|
4146
4193
|
* follow it to end that list — `--output-format` does, right after. Never
|
|
4147
4194
|
* put `--disallowedTools` last or right before the prompt.
|
|
4148
4195
|
*/
|
|
4149
|
-
function buildClaudeSpawnArgs({ prompt, model, effort, sessionId, resume, systemPrompt }) {
|
|
4196
|
+
function buildClaudeSpawnArgs({ prompt, model, effort, sessionId, resume, systemPrompt, toolScope }) {
|
|
4150
4197
|
return [
|
|
4151
4198
|
'-p', prompt,
|
|
4152
4199
|
'--model', model,
|
|
4153
4200
|
...effortArgs(effort),
|
|
4154
4201
|
...(systemPrompt ? ['--append-system-prompt', systemPrompt] : []),
|
|
4202
|
+
...personaToolScopeArgs(toolScope),
|
|
4155
4203
|
'--dangerously-skip-permissions',
|
|
4156
4204
|
...headlessPermissionArgs(),
|
|
4157
4205
|
'--disallowedTools', HEADLESS_DISALLOWED_TOOLS.join(','),
|
|
@@ -4289,6 +4337,26 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4289
4337
|
// then an ancestor-folder Epic walk above job.cwd) first, so the common
|
|
4290
4338
|
// case — a live Epic-scoped PRD — is a first-try hit instead of probing
|
|
4291
4339
|
// the retired flat dir and only then falling back.
|
|
4340
|
+
// Shared by both 'prd-missing' exits: resolves to null (truly missing), the
|
|
4341
|
+
// string 'resolved' (file found + parsed; prompt/prdPath/parsedPrdMeta set),
|
|
4342
|
+
// or a ready-to-return { exitCode: -1, error } result.
|
|
4343
|
+
const recheckBeforeMissingSkip = async () => {
|
|
4344
|
+
const found = await recheckPrdSourceForJob(job);
|
|
4345
|
+
if (!found) return null;
|
|
4346
|
+
try {
|
|
4347
|
+
const parsed = await parsePrd(found);
|
|
4348
|
+
prompt = parsed.body;
|
|
4349
|
+
prdPath = found;
|
|
4350
|
+
parsedPrdMeta = parsed;
|
|
4351
|
+
} catch (err) {
|
|
4352
|
+
const error = `PRD exists at ${found} but could not be read: ${err?.message}`;
|
|
4353
|
+
safeLog(`[scheduler] ${error}\n`);
|
|
4354
|
+
closeFd();
|
|
4355
|
+
return { exitCode: -1, durationMs: 0, error, sessionId };
|
|
4356
|
+
}
|
|
4357
|
+
safeLog(`[scheduler] PRD resolved on re-check at ${found}\n`);
|
|
4358
|
+
return 'resolved';
|
|
4359
|
+
};
|
|
4292
4360
|
const resolvedDir = await prdPaths.findPrdDirForJob(job);
|
|
4293
4361
|
prdPath = resolvedDir ? path.join(resolvedDir, `${job.slug}.md`) : prdPaths.prdPathForJob(job);
|
|
4294
4362
|
try {
|
|
@@ -4324,11 +4392,14 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4324
4392
|
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
4325
4393
|
}
|
|
4326
4394
|
if (e2 && e2.code === 'ENOENT') {
|
|
4327
|
-
|
|
4395
|
+
const rc = await recheckBeforeMissingSkip();
|
|
4396
|
+
if (rc && rc !== 'resolved') return rc;
|
|
4397
|
+
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
4398
|
+
} else {
|
|
4399
|
+
safeLog(`[scheduler] failed to read PRD: ${e2?.message}\n`);
|
|
4400
|
+
closeFd();
|
|
4401
|
+
return { exitCode: -1, durationMs: 0, error: e2?.message };
|
|
4328
4402
|
}
|
|
4329
|
-
safeLog(`[scheduler] failed to read PRD: ${e2?.message}\n`);
|
|
4330
|
-
closeFd();
|
|
4331
|
-
return { exitCode: -1, durationMs: 0, error: e2?.message };
|
|
4332
4403
|
}
|
|
4333
4404
|
} else {
|
|
4334
4405
|
// No candidate dir has the slug at all. Case A: it was archived —
|
|
@@ -4341,11 +4412,14 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4341
4412
|
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
4342
4413
|
}
|
|
4343
4414
|
if (e && e.code === 'ENOENT') {
|
|
4344
|
-
|
|
4415
|
+
const rc = await recheckBeforeMissingSkip();
|
|
4416
|
+
if (rc && rc !== 'resolved') return rc;
|
|
4417
|
+
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
4418
|
+
} else {
|
|
4419
|
+
safeLog(`[scheduler] failed to read PRD: ${e?.message}\n`);
|
|
4420
|
+
closeFd();
|
|
4421
|
+
return { exitCode: -1, durationMs: 0, error: e?.message };
|
|
4345
4422
|
}
|
|
4346
|
-
safeLog(`[scheduler] failed to read PRD: ${e?.message}\n`);
|
|
4347
|
-
closeFd();
|
|
4348
|
-
return { exitCode: -1, durationMs: 0, error: e?.message };
|
|
4349
4423
|
}
|
|
4350
4424
|
}
|
|
4351
4425
|
} // end resumeTarget ? preamble : normal-PRD-read
|
|
@@ -4437,7 +4511,8 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4437
4511
|
// and is logged once by resolvePrdPersonaForSpawn itself.
|
|
4438
4512
|
const personaResolution = await agentModelResolve.resolvePrdPersonaForSpawn({ cwd, agentType: job.agentType });
|
|
4439
4513
|
const personaEffort = resolveEpicEffort({ cwd, agentType: job.agentType }).effort;
|
|
4440
|
-
|
|
4514
|
+
const personaToolScope = buildPersonaToolScope(personaResolution.tools, { disallowed: HEADLESS_DISALLOWED_TOOLS });
|
|
4515
|
+
safeLog(`[scheduler] agentType=${job.agentType || '(none)'} persona=${personaResolution.personaPath || '(fallback — no persona applied)'} model=${personaResolution.model}${personaEffort ? ` effort=${personaEffort}` : ''} ${personaToolScope ? `tools=${personaToolScope.tools.length} scoped` : 'tools=unscoped'}\n`);
|
|
4441
4516
|
|
|
4442
4517
|
// Authoritative guarantee that buildClaudeSpawnArgs (a pure sync helper,
|
|
4443
4518
|
// called below via withChildAndLog) sees a resolved --permission-prompts
|
|
@@ -4646,6 +4721,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4646
4721
|
sessionId,
|
|
4647
4722
|
resume: !!resumeTarget,
|
|
4648
4723
|
systemPrompt: personaResolution.systemPrompt,
|
|
4724
|
+
toolScope: personaToolScope,
|
|
4649
4725
|
});
|
|
4650
4726
|
const jobSpawn = claudeSpawnTarget('job', job.slug, claudeBin, jobArgs);
|
|
4651
4727
|
|
|
@@ -4836,7 +4912,7 @@ function isPromotableOriginal(status) {
|
|
|
4836
4912
|
*/
|
|
4837
4913
|
function healTargetForFix(fixSlug, jobs) {
|
|
4838
4914
|
const originalSlug = fixSlug.replace(/^(\d+)-fix-/, '$1-');
|
|
4839
|
-
return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status)) || null;
|
|
4915
|
+
return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status) && !reviewLadder.isCancelledJob(x)) || null;
|
|
4840
4916
|
}
|
|
4841
4917
|
|
|
4842
4918
|
/**
|
|
@@ -5179,8 +5255,8 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
|
|
|
5179
5255
|
await mutate((s) => {
|
|
5180
5256
|
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
5181
5257
|
if (!j) return;
|
|
5182
|
-
const statusEligible = j.status === 'needs_review' || j.status === 'failed';
|
|
5183
|
-
const transitioned = transitionJob(j, 'investigating', { reason: 'spawning investigation probe', source: 'spawnInvestigation:start' });
|
|
5258
|
+
const statusEligible = (j.status === 'needs_review' || j.status === 'failed') && !reviewLadder.isCancelledJob(j);
|
|
5259
|
+
const transitioned = statusEligible && transitionJob(j, 'investigating', { reason: 'spawning investigation probe', source: 'spawnInvestigation:start' });
|
|
5184
5260
|
liveCheck = { ok: statusEligible && transitioned, status: j.status };
|
|
5185
5261
|
});
|
|
5186
5262
|
if (!liveCheck.ok) {
|
|
@@ -5530,10 +5606,11 @@ function reportSchedulerError(message, slug, e) {
|
|
|
5530
5606
|
* worktreeIntegrationFailure: string|null, worktreeIntegrationDetail: object|null,
|
|
5531
5607
|
* mergeAutoResolved: string|null, mergeAutoResolvedPaths: string[]|null}>}
|
|
5532
5608
|
*/
|
|
5533
|
-
async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPaths, deps = {} }) {
|
|
5609
|
+
async function finalizeJobWorktree({ job, runId = null, runDir, worktree, guardCwd, carriedPaths, deps = {} }) {
|
|
5534
5610
|
const jw = deps.jobWorktree || jobWorktree;
|
|
5535
5611
|
const uncommitted = deps.uncommittedChanges || gitTreeGuard.uncommittedChanges;
|
|
5536
5612
|
const report = deps.reportSchedulerError || reportSchedulerError;
|
|
5613
|
+
const runCancelled = deps.isRunCancelled || isRunCancelled;
|
|
5537
5614
|
let worktreeLeftoverDirty = [];
|
|
5538
5615
|
let salvagePatch = null;
|
|
5539
5616
|
let worktreeIntegrationFailure = null;
|
|
@@ -5556,6 +5633,12 @@ async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPat
|
|
|
5556
5633
|
console.log(`[scheduler] ${job.slug}: salvaged ${salvage.bytes} byte(s) of uncommitted worktree diff to ${salvagePath}`);
|
|
5557
5634
|
}
|
|
5558
5635
|
}
|
|
5636
|
+
if (await runCancelled(job.slug, runId)) {
|
|
5637
|
+
// Cancelled mid-run: keep the commit on the job branch only.
|
|
5638
|
+
appendAuditEvent('job_cancel_integration_skipped', { slug: job.slug, runId, branch: worktree.branch });
|
|
5639
|
+
await jw.cleanupJobWorktree({ cwd: guardCwd, dir: worktree.dir, branch: worktree.branch, keepBranch: true });
|
|
5640
|
+
return { worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha: null, cancelled: true };
|
|
5641
|
+
}
|
|
5559
5642
|
const integration = await jw.integrateJobBranch({ cwd: guardCwd, branch: worktree.branch, slug: job.slug, carriedPaths, baseBranch: worktree.baseBranch });
|
|
5560
5643
|
if (integration.ok && integration.reason === 'carried-wip-only') {
|
|
5561
5644
|
console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} touched only carried base WIP paths — skipping merge (carried-wip-only)`);
|
|
@@ -5587,7 +5670,25 @@ async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPat
|
|
|
5587
5670
|
await jw.cleanupJobWorktree({ cwd: guardCwd, dir: worktree.dir, branch: worktree.branch, keepBranch: true });
|
|
5588
5671
|
} catch { /* already reported above */ }
|
|
5589
5672
|
}
|
|
5590
|
-
return { worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha };
|
|
5673
|
+
return { worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha, cancelled: false };
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
/**
|
|
5677
|
+
* True when the queue row for `slug` carries a durable cancel stamp for this
|
|
5678
|
+
* exact run (`cancelledAt` + matching `cancelledRunId`). Incident 2026-10-19
|
|
5679
|
+
* (git-viewer): a cancelled running PRD's commit still landed on main because
|
|
5680
|
+
* finalizeJobWorktree integrated unconditionally. A read error returns false so
|
|
5681
|
+
* a flaky read never strands every normal run's work. Known residual: a cancel
|
|
5682
|
+
* arriving after integrateJobBranch already completed cannot un-land the commit.
|
|
5683
|
+
*/
|
|
5684
|
+
async function isRunCancelled(slug, runId) {
|
|
5685
|
+
try {
|
|
5686
|
+
const state = await readQueue();
|
|
5687
|
+
const row = state.jobs.find((j) => j.slug === slug);
|
|
5688
|
+
return !!(row && row.cancelledAt && (row.cancelledRunId ?? null) === (runId ?? null));
|
|
5689
|
+
} catch {
|
|
5690
|
+
return false;
|
|
5691
|
+
}
|
|
5591
5692
|
}
|
|
5592
5693
|
|
|
5593
5694
|
/**
|
|
@@ -6016,7 +6117,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6016
6117
|
} finally {
|
|
6017
6118
|
if (worktree.ok) {
|
|
6018
6119
|
({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha } =
|
|
6019
|
-
await finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPaths }));
|
|
6120
|
+
await finalizeJobWorktree({ job, runId, runDir, worktree, guardCwd, carriedPaths }));
|
|
6020
6121
|
} else {
|
|
6021
6122
|
// In-place run (non-git cwd, cap reached, env-disabled, or a carry-over
|
|
6022
6123
|
// failure) — there is no throwaway checkout to diff, so salvage only
|
|
@@ -6139,6 +6240,8 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6139
6240
|
// 20 never-executed sigma PRDs read as shipped (2026-08-30 incident).
|
|
6140
6241
|
if (res.skipped) {
|
|
6141
6242
|
const targetStatus = res.skipped === 'prd-missing' ? 'skipped' : 'completed';
|
|
6243
|
+
/** @type {string[]} */
|
|
6244
|
+
let blockedDependents = [];
|
|
6142
6245
|
await mutate((s) => {
|
|
6143
6246
|
const idx = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
6144
6247
|
if (idx >= 0) {
|
|
@@ -6147,9 +6250,25 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6147
6250
|
s.jobs[idx].exitCode = 0;
|
|
6148
6251
|
s.jobs[idx].error = null;
|
|
6149
6252
|
delete s.jobs[idx].runtime;
|
|
6253
|
+
if (res.skipped === 'prd-missing') {
|
|
6254
|
+
// Dependents wait on this slug forever once it is skipped; record
|
|
6255
|
+
// them so the Epic can be told what is blocked.
|
|
6256
|
+
blockedDependents = s.jobs
|
|
6257
|
+
.filter((x) => x.slug !== job.slug
|
|
6258
|
+
&& x.cwd === job.cwd
|
|
6259
|
+
&& Array.isArray(x.dependsOn) && x.dependsOn.includes(job.slug)
|
|
6260
|
+
&& (x.status == null || x.status === 'pending' || x.status === 'needs_review'))
|
|
6261
|
+
.map((x) => x.slug);
|
|
6262
|
+
s.jobs[idx].blockedDependents = blockedDependents;
|
|
6263
|
+
}
|
|
6150
6264
|
}
|
|
6151
6265
|
});
|
|
6152
6266
|
await broadcast({ flush: true });
|
|
6267
|
+
if (res.skipped === 'prd-missing' && blockedDependents.length > 0) {
|
|
6268
|
+
notifyOriginatingTab({ ...job, status: 'skipped', blockedDependents }).catch((e) => {
|
|
6269
|
+
console.error('[scheduler] notifyOriginatingTab error', job.slug, e);
|
|
6270
|
+
});
|
|
6271
|
+
}
|
|
6153
6272
|
return;
|
|
6154
6273
|
}
|
|
6155
6274
|
|
|
@@ -7342,22 +7461,34 @@ async function tickBody(gen, { bypassLoadGate }) {
|
|
|
7342
7461
|
const jobBudgetMb = availableForJobs(availableMb, RESERVED_HOST_MB);
|
|
7343
7462
|
// The slot pool already bounded `batch` (it was passed as freeSlots to
|
|
7344
7463
|
// pickNextBatch above), so only the memory gate can narrow it further.
|
|
7464
|
+
// Credit each running job's already-resident RSS (capped at its reservation)
|
|
7465
|
+
// — MemAvailable is already net of it. Unknown pid / 0 MB read = no credit.
|
|
7466
|
+
let consumedMb = 0;
|
|
7467
|
+
if (availableMb !== Infinity) {
|
|
7468
|
+
for (const j of state.jobs) {
|
|
7469
|
+
if (!runningSet.has(j.slug)) continue;
|
|
7470
|
+
const pid = j.runtime?.pid ?? j.pid;
|
|
7471
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
7472
|
+
consumedMb += Math.min(processGroupRssMb(pid), MIN_FREE_MB_PER_JOB);
|
|
7473
|
+
}
|
|
7474
|
+
consumedMb = Math.min(consumedMb, MIN_FREE_MB_PER_JOB * runningSet.size);
|
|
7475
|
+
}
|
|
7345
7476
|
const allowed = memoryLimitedBatchSize(
|
|
7346
|
-
jobBudgetMb, MIN_FREE_MB_PER_JOB, runningSet.size, batch.length,
|
|
7477
|
+
jobBudgetMb, MIN_FREE_MB_PER_JOB, runningSet.size, batch.length, consumedMb,
|
|
7347
7478
|
);
|
|
7348
7479
|
if (allowed === 0) {
|
|
7349
|
-
const threshold = RESERVED_HOST_MB + MIN_FREE_MB_PER_JOB * (runningSet.size + 1);
|
|
7350
|
-
console.log(`[scheduler] memory gate: available=${availableMb} MB < threshold=${threshold} MB (host reserve ${RESERVED_HOST_MB} + ${MIN_FREE_MB_PER_JOB}/job × ${runningSet.size + 1}) — deferring ${batch.length} job(s)`);
|
|
7351
|
-
lastMemGate = { availableMb, threshold, deferred: true, at: new Date().toISOString() };
|
|
7480
|
+
const threshold = RESERVED_HOST_MB + MIN_FREE_MB_PER_JOB * (runningSet.size + 1) - consumedMb;
|
|
7481
|
+
console.log(`[scheduler] memory gate: available=${availableMb} MB < threshold=${threshold} MB (host reserve ${RESERVED_HOST_MB} + ${MIN_FREE_MB_PER_JOB}/job × ${runningSet.size + 1} - consumed=${consumedMb} MB) — deferring ${batch.length} job(s)`);
|
|
7482
|
+
lastMemGate = { availableMb, threshold, consumedMb, deferred: true, at: new Date().toISOString() };
|
|
7352
7483
|
return recordTick(
|
|
7353
|
-
{ fired: false, reason: 'memory-deferred', deferredCount: batch.length, availableMb, threshold },
|
|
7354
|
-
{ detail: `memory gate: ${availableMb} MB free, need ${threshold} MB`, holds },
|
|
7484
|
+
{ fired: false, reason: 'memory-deferred', deferredCount: batch.length, availableMb, threshold, consumedMb },
|
|
7485
|
+
{ detail: `memory gate: ${availableMb} MB free, need ${threshold} MB (consumed=${consumedMb} MB credited)`, holds },
|
|
7355
7486
|
);
|
|
7356
7487
|
}
|
|
7357
7488
|
const gatedBatch = batch.slice(0, allowed);
|
|
7358
7489
|
if (gatedBatch.length < batch.length) {
|
|
7359
|
-
console.log(`[scheduler] memory gate: available=${availableMb} MB — clamped batch ${batch.length} → ${gatedBatch.length} (host reserve ${RESERVED_HOST_MB} + ${MIN_FREE_MB_PER_JOB}/job)`);
|
|
7360
|
-
lastMemGate = { availableMb, threshold: RESERVED_HOST_MB + MIN_FREE_MB_PER_JOB * (runningSet.size + gatedBatch.length), deferred: false, clamped: true, at: new Date().toISOString() };
|
|
7490
|
+
console.log(`[scheduler] memory gate: available=${availableMb} MB — clamped batch ${batch.length} → ${gatedBatch.length} (host reserve ${RESERVED_HOST_MB} + ${MIN_FREE_MB_PER_JOB}/job, consumed=${consumedMb} MB)`);
|
|
7491
|
+
lastMemGate = { availableMb, threshold: RESERVED_HOST_MB + MIN_FREE_MB_PER_JOB * (runningSet.size + gatedBatch.length) - consumedMb, consumedMb, deferred: false, clamped: true, at: new Date().toISOString() };
|
|
7361
7492
|
} else {
|
|
7362
7493
|
// Ungated full batch: clear stale gate snapshot so status doesn't show
|
|
7363
7494
|
// a stale deferral from a previous tick.
|
|
@@ -8499,7 +8630,7 @@ function selectHistoryJobs(jobs, limit, historyEntries = []) {
|
|
|
8499
8630
|
* guard-parked row was never "exhausted auto-fix" and must never claim to be.
|
|
8500
8631
|
*/
|
|
8501
8632
|
function applyNeedsReviewAutoResolve(j, jobsInProject = []) {
|
|
8502
|
-
if (!j || j.status !== 'needs_review' || !reviewLadder.isEligibleForNeedsReviewAutoResolve(j, jobsInProject)) return null;
|
|
8633
|
+
if (!j || j.status !== 'needs_review' || reviewLadder.isCancelledJob(j) || !reviewLadder.isEligibleForNeedsReviewAutoResolve(j, jobsInProject)) return null;
|
|
8503
8634
|
if ((j.exhaustedResolveAttempts ?? 0) > reviewLadder.NEEDS_REVIEW_RESOLVE_CAP) return null;
|
|
8504
8635
|
const originIsGuardParked = !reviewLadder.isExhaustedAutoFix(j) && reviewLadder.isGuardParkedWithoutAutoFix(j);
|
|
8505
8636
|
|
|
@@ -8744,12 +8875,31 @@ async function runGateShadow(job) {
|
|
|
8744
8875
|
const dirtBefore = await gateTreeDirt(job.cwd);
|
|
8745
8876
|
const gate = resolveGate(prdText);
|
|
8746
8877
|
let outcome;
|
|
8878
|
+
let mainDeps = null;
|
|
8879
|
+
let repaired = false;
|
|
8747
8880
|
if (gate.source === 'none') outcome = { status: 'unavailable', reason: 'gate-opt-out', results: [] };
|
|
8748
8881
|
else if (!gate.sequence.length) outcome = { status: 'unavailable', reason: 'no-parseable-gate', results: [] };
|
|
8749
8882
|
else {
|
|
8750
|
-
|
|
8751
|
-
if (
|
|
8752
|
-
|
|
8883
|
+
let health = checkMainDependencyHealth(job.cwd);
|
|
8884
|
+
if (!health.ok) {
|
|
8885
|
+
const last = mainDepsRepairAt.get(job.cwd) ?? 0;
|
|
8886
|
+
if (Date.now() - last >= MAIN_DEPS_REPAIR_COOLDOWN_MS) {
|
|
8887
|
+
mainDepsRepairAt.set(job.cwd, Date.now());
|
|
8888
|
+
const rep = await repairMainDependencies(job.cwd);
|
|
8889
|
+
repaired = rep.ok;
|
|
8890
|
+
console.warn(`[scheduler] main deps broken in ${job.cwd}: ${health.reason} — repair ${rep.ok ? 'ok' : 'failed'}`);
|
|
8891
|
+
health = checkMainDependencyHealth(job.cwd);
|
|
8892
|
+
}
|
|
8893
|
+
mainDeps = { ok: health.ok, reason: health.reason ?? null, missing: health.missing ?? [], repaired };
|
|
8894
|
+
}
|
|
8895
|
+
if (!health.ok) {
|
|
8896
|
+
const names = health.missing && health.missing.length ? `: ${health.missing.join(', ')}` : '';
|
|
8897
|
+
outcome = { status: 'unavailable', reason: `main-deps-broken: ${health.reason}${names}`, results: [] };
|
|
8898
|
+
} else {
|
|
8899
|
+
const r = await runGateSequence(gate.sequence, { cwd: job.cwd });
|
|
8900
|
+
if (r.status === 'busy') return null;
|
|
8901
|
+
outcome = r;
|
|
8902
|
+
}
|
|
8753
8903
|
}
|
|
8754
8904
|
const headAfter = await gitTreeGuard.gitHead(job.cwd);
|
|
8755
8905
|
const dirtAfter = await gateTreeDirt(job.cwd);
|
|
@@ -8761,7 +8911,7 @@ async function runGateShadow(job) {
|
|
|
8761
8911
|
// about done-ness, so it must stay eligible for retry once the PRD gains a
|
|
8762
8912
|
// real gate. `busy` never reaches here (returns early above).
|
|
8763
8913
|
const definitive = headStable && cleanOk && (outcome.status === 'green' || outcome.status === 'red');
|
|
8764
|
-
const gateShadow = { ...outcome, head: headBefore, source: gate.source, ranAt: new Date().toISOString(), definitive };
|
|
8914
|
+
const gateShadow = { ...outcome, head: headBefore, source: gate.source, ranAt: new Date().toISOString(), definitive, ...(mainDeps ? { mainDeps } : {}) };
|
|
8765
8915
|
|
|
8766
8916
|
// Gate authority: a green re-run may complete the row outright, but only
|
|
8767
8917
|
// for transcript-noise verdicts with a landed commit — every other park
|
|
@@ -9930,7 +10080,7 @@ function rescheduleIntervalTick() {
|
|
|
9930
10080
|
mutate(async (ms) => {
|
|
9931
10081
|
for (const target of autoResetTargets) {
|
|
9932
10082
|
const j = ms.jobs.find((x) => x.slug === target.slug);
|
|
9933
|
-
if (!j || j.status !== 'failed' || (j.failedAutoResetAttempts ?? 0) >= reviewLadder.FAILED_AUTORESET_CAP) continue; // race guard
|
|
10083
|
+
if (!j || j.status !== 'failed' || reviewLadder.isCancelledJob(j) || (j.failedAutoResetAttempts ?? 0) >= reviewLadder.FAILED_AUTORESET_CAP) continue; // race guard
|
|
9934
10084
|
const attempt = (j.failedAutoResetAttempts ?? 0) + 1;
|
|
9935
10085
|
j.failedAutoResetAttempts = attempt;
|
|
9936
10086
|
const reason = `auto-reset after ${Math.round(reviewLadder.FAILED_AUTORESET_MS / 60_000)}m failed (attempt ${attempt}/${reviewLadder.FAILED_AUTORESET_CAP})`;
|
|
@@ -10823,9 +10973,6 @@ const remote = {
|
|
|
10823
10973
|
}
|
|
10824
10974
|
const wasRunning = job.status === 'running';
|
|
10825
10975
|
const pid = job.runtime?.pid;
|
|
10826
|
-
if (wasRunning && pid) {
|
|
10827
|
-
killOrphanClaudePid(pid);
|
|
10828
|
-
}
|
|
10829
10976
|
// Evidence-before-failure guard, scoped to an actually-running job being
|
|
10830
10977
|
// killed here (a 'pending' cancel has no live process, so nothing new
|
|
10831
10978
|
// could have landed since its last stamp — and 'needs_review' is not
|
|
@@ -10845,19 +10992,49 @@ const remote = {
|
|
|
10845
10992
|
const cancelReason = confirmedLandedCommit
|
|
10846
10993
|
? `cancelled via admin API, but landedCommit ${confirmedLandedCommit} resolves — verify before treating as done`
|
|
10847
10994
|
: 'cancelled via admin API';
|
|
10995
|
+
const cancelledAt = new Date().toISOString();
|
|
10848
10996
|
await mutate((s) => {
|
|
10849
10997
|
const idx = s.jobs.findIndex((j) => j.slug === slug);
|
|
10850
10998
|
if (idx < 0) return;
|
|
10851
10999
|
const j = s.jobs[idx];
|
|
10852
11000
|
transitionJob(j, targetStatus, { reason: cancelReason, source: 'remote:cancelJob' });
|
|
10853
11001
|
j.error = cancelReason;
|
|
11002
|
+
j.cancelledAt = cancelledAt;
|
|
11003
|
+
if (wasRunning) j.cancelledRunId = job.runId ?? null;
|
|
10854
11004
|
j.finishedAt = new Date().toISOString();
|
|
10855
11005
|
j.exitCode = j.exitCode ?? null;
|
|
10856
11006
|
if (confirmedLandedCommit) j.verifierVerdict = 'cancelled_with_landed_commit';
|
|
10857
11007
|
delete j.runtime;
|
|
10858
11008
|
});
|
|
11009
|
+
// Stamped before SIGTERM so finalizeJobWorktree (run on executor exit) sees it.
|
|
11010
|
+
if (wasRunning && pid) killOrphanClaudePid(pid);
|
|
10859
11011
|
await broadcast({ flush: true });
|
|
10860
|
-
|
|
11012
|
+
const result = { ok: true, slug, status: targetStatus, wasRunning, cwd: job.cwd ?? null, cancelledAt };
|
|
11013
|
+
if (wasRunning) {
|
|
11014
|
+
// Whatever the executor committed before SIGTERM stays on its job
|
|
11015
|
+
// branch (never integrated); tell the caller where. Known residual: a
|
|
11016
|
+
// cancel arriving after integration already finished reports
|
|
11017
|
+
// landed:false with jobBranch:null (branch deleted after the merge).
|
|
11018
|
+
let jobBranch = null;
|
|
11019
|
+
let jobBranchSha = null;
|
|
11020
|
+
try {
|
|
11021
|
+
const branch = jobWorktree.branchNameFor(slug);
|
|
11022
|
+
const stdout = await execGitAt(
|
|
11023
|
+
resolveProjectRoot(job.cwd || DEFAULT_PROJECT_CWD),
|
|
11024
|
+
['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`],
|
|
11025
|
+
{ timeout: 10_000 },
|
|
11026
|
+
);
|
|
11027
|
+
const sha = String(stdout || '').trim();
|
|
11028
|
+
if (sha) { jobBranch = branch; jobBranchSha = sha; }
|
|
11029
|
+
} catch { /* branch absent or git errored → nulls */ }
|
|
11030
|
+
Object.assign(result, {
|
|
11031
|
+
landed: confirmedLandedCommit != null,
|
|
11032
|
+
landedCommit: confirmedLandedCommit ?? null,
|
|
11033
|
+
jobBranch,
|
|
11034
|
+
jobBranchSha,
|
|
11035
|
+
});
|
|
11036
|
+
}
|
|
11037
|
+
return result;
|
|
10861
11038
|
},
|
|
10862
11039
|
|
|
10863
11040
|
// Exposes the module-level allocateParallelGroup (PRD 548) to callers that
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- PRD_AUTHORING.md
|
|
1
|
+
<!-- PRD_AUTHORING.md v7 -->
|
|
2
2
|
# PRD Authoring Guide — Scheduler Safety Rules
|
|
3
3
|
|
|
4
4
|
Rule: every PRD you queue follows the rules below.
|
|
@@ -125,7 +125,7 @@ A short Epic-context digest is prepended automatically for orientation only —
|
|
|
125
125
|
|
|
126
126
|
The executor can't ask you anything: no `ScheduleWakeup`, Cron, Monitor, `AskUserQuestion`, plan mode, or worktree tools; background tasks are off (a `timeout` command is always on PATH — the app installs a shim on macOS). It must never stop to ask a question — it makes the safest reasonable call and reports it, so write the PRD so that call is obvious.
|
|
127
127
|
|
|
128
|
-
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus symlinked `node_modules
|
|
128
|
+
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `.venv`/`venv` (and `node_modules` for non-pnpm projects) symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store. Why: a pnpm install through the symlink purged the main checkout's node_modules (Bilko-Game-Academy 2026-10-18). A step that needs gitignored runtime state (databases, `downloads/`, browser profiles, logs) or a live side effect must address the main checkout by absolute path (e.g. `$HOME/Projects/<repo>/...`), be artifact-only (a deliverable artifact + `artifactPaths`, no tracked edits), and `dependsOn` the PRD that lands its tracked edits. Host crontab installs stay human-only (see §15).
|
|
129
129
|
Why: burrow PRD 1516 halted because its relative `.venv` python against `downloads/fb-tracker.db`, `scripts/install-cron.sh` and `pipeline-cron.sh --now` resolved inside the worktree (no runtime state; install-cron would bake a temporary worktree path into crontab), while sibling 1494 succeeded the same day using an absolute main-checkout path and artifact-only output. `scheduler_create_prd` warns on relative gitignored paths.
|
|
130
130
|
|
|
131
131
|
---
|