claude-code-session-manager 0.122.1 → 0.123.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentLibrary-C2qUX9WU.js → AgentLibrary-T14aM8ie.js} +2 -2
- package/dist/assets/{Button-ssBzvFha.js → Button-Dz1kV8Z_.js} +1 -1
- package/dist/assets/DataModel-DdYRn2Ec.js +1 -0
- package/dist/assets/{History-BRdRp1J_.js → History-zQPvqwE7.js} +2 -2
- package/dist/assets/{Hooks--hiGPBQy.js → Hooks-BTrbT742.js} +3 -3
- package/dist/assets/{Library-0XYvkhh2.js → Library-BwQ6GRHR.js} +7 -7
- package/dist/assets/ListDetail-CunxAhRC.js +1 -0
- package/dist/assets/{McpServers-CCjHSwFm.js → McpServers-DpoyxXaO.js} +2 -2
- package/dist/assets/{Memory-C7RXoVO4.js → Memory-KDtGYMc3.js} +8 -8
- package/dist/assets/{Panel-C92O_aAH.js → Panel-xXmUBo8l.js} +1 -1
- package/dist/assets/{Permissions-DuTJ1FUb.js → Permissions-lIzTUrW2.js} +3 -3
- package/dist/assets/{Plugins-DSXsP4hM.js → Plugins-BA1Zqk69.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CWdDkU1m.js → ProvenanceBadge-CmeJG2Kw.js} +1 -1
- package/dist/assets/{SaveBar-ob2dJJjH.js → SaveBar-BTJg_9Uc.js} +1 -1
- package/dist/assets/{Scheduler-Du4HgPKY.js → Scheduler-CISTDNMU.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Dv_6M_Ci.js → ScopeSwitcher-C6NWvzoj.js} +1 -1
- package/dist/assets/Settings-BBoGJ34v.js +4 -0
- package/dist/assets/{SkillReferenceGraph-Dx1Qs8Y0.js → SkillReferenceGraph-IJv60QAJ.js} +1 -1
- package/dist/assets/Skills-L6nGcNF1.js +2 -0
- package/dist/assets/SystemPrompt-Cm7I2gJz.js +1 -0
- package/dist/assets/TagLibrary-BxPwoPE6.js +1 -0
- package/dist/assets/{TiptapBody-Cihe5er3.js → TiptapBody-CR-Zr3Pz.js} +1 -1
- package/dist/assets/{Toggle-CpYVC8bF.js → Toggle-BFhlpZ-n.js} +1 -1
- package/dist/assets/{ViewTabs-C3FN6P_C.js → ViewTabs-CuGlZ9Ed.js} +1 -1
- package/dist/assets/{historyTotals-C4Rs37Cd.js → historyTotals-Dxv9Fzrs.js} +1 -1
- package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
- package/dist/assets/{index-S-F5jp_h.js → index-Dx8owDeQ.js} +151 -151
- package/dist/assets/{settingsSchema-BZcdzHTQ.js → settingsSchema-DaqFSAQH.js} +1 -1
- package/dist/assets/{skillFrontmatter-EfNERt4b.js → skillFrontmatter-BIJddxVB.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-BfbrFvB3.js → useAgentsDirWatch-1hjZ4knk.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -25
- package/scripts/scheduler-mcp-server.cjs +124 -74
- package/src/main/agentLibrary.cjs +8 -3
- package/src/main/build-info.json +4 -4
- package/src/main/crashDiagnostics.cjs +24 -0
- package/src/main/historyAggregator.cjs +41 -7
- package/src/main/index.cjs +13 -3
- package/src/main/ipcSchemas.cjs +30 -22
- package/src/main/lib/activeSessions.cjs +93 -32
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
- package/src/main/lib/epic/epicMint.cjs +1 -1
- package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
- package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
- package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
- package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
- package/src/main/lib/git/gitWorktree.cjs +9 -4
- package/src/main/lib/git/worktreeDeps.cjs +13 -2
- package/src/main/lib/git/worktreeSetup.cjs +212 -0
- package/src/main/lib/mcpToolCatalog.cjs +31 -2
- package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
- package/src/main/lib/prd/prdAgentType.cjs +28 -0
- package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
- package/src/main/lib/prd/prdCreate.cjs +205 -32
- package/src/main/lib/prd/prdGateFiles.cjs +31 -0
- package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
- package/src/main/lib/scheduler/queueStore.cjs +5 -4
- package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
- package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
- package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
- package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
- package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
- package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
- package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
- package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
- package/src/main/pty.cjs +0 -53
- package/src/main/runVerify.cjs +38 -11
- package/src/main/scheduler/prdParser.cjs +3 -0
- package/src/main/scheduler/prdPaths.cjs +48 -2
- package/src/main/scheduler.cjs +124 -29
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +8 -4
- package/src/main/webRemote.cjs +38 -132
- package/src/preload/api.d.ts +4 -52
- package/src/preload/index.cjs +0 -24
- package/src/seed/agents/dev-lead.md +2 -1
- package/src/seed/agents/validator.md +2 -1
- package/dist/assets/DataModel-CENoTvc2.js +0 -1
- package/dist/assets/ListDetail-CkKXAM59.js +0 -1
- package/dist/assets/RemoteMachines-BFmbIGpW.js +0 -3
- package/dist/assets/Settings-DMoI0cqB.js +0 -4
- package/dist/assets/Skills-DYWQTQB1.js +0 -2
- package/dist/assets/SystemPrompt-Df0IfOuI.js +0 -1
- package/dist/assets/TagLibrary-BuMsGx-2.js +0 -1
- package/dist/assets/useRemotePeers-CKVakQPc.js +0 -1
- package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
- package/src/main/remoteClient.cjs +0 -334
- package/src/main/remoteClientIpc.cjs +0 -133
package/src/main/runVerify.cjs
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Downgrade policy:
|
|
23
23
|
* clean → null (caller stamps 'completed')
|
|
24
|
-
* halt → 'pending' (re-fires automatically next slot
|
|
24
|
+
* halt → 'pending' (re-fires automatically next slot; only for
|
|
25
|
+
* a run that printed no SCHEDULER_VERDICT line)
|
|
25
26
|
* deps_unmet → 'pending' (re-fires when deps clear)
|
|
26
27
|
* transcript_errors → historical rows only; no longer emitted
|
|
27
28
|
* verify_unavailable → 'needs_review' (unreadable log / verifier threw)
|
|
@@ -297,6 +298,30 @@ function scanSentinel(resultEvent, events) {
|
|
|
297
298
|
return null;
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
/**
|
|
302
|
+
* First line that starts at column 0 with the exact uppercase halt marker
|
|
303
|
+
* (`HALT:`) outside a fenced block, else null. Line-anchored and fence-aware
|
|
304
|
+
* because reports quote the marker in prose and paste tool output in fences,
|
|
305
|
+
* and PRD_AUTHORING.md §4/§9/§10 tell scripts to print it on failure — neither
|
|
306
|
+
* is the executor's own stop signal.
|
|
307
|
+
* @param {unknown} text
|
|
308
|
+
* @returns {string | null}
|
|
309
|
+
*/
|
|
310
|
+
function scanHaltLine(text) {
|
|
311
|
+
if (typeof text !== 'string') return null;
|
|
312
|
+
let inFence = false;
|
|
313
|
+
for (const raw of text.split('\n')) {
|
|
314
|
+
const line = raw.replace(/\r$/, '').trimEnd();
|
|
315
|
+
const t = line.trimStart();
|
|
316
|
+
if (t.startsWith('```') || t.startsWith('~~~')) {
|
|
317
|
+
inFence = !inFence;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!inFence && line.startsWith('HALT:')) return line;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
300
325
|
/**
|
|
301
326
|
* One-line reason after `SCHEDULER_VERDICT: FAIL` (same two-source scan order
|
|
302
327
|
* as scanSentinel). Trimmed, capped at 300 chars; '' when absent.
|
|
@@ -742,13 +767,17 @@ async function verifyRun({
|
|
|
742
767
|
else if (commitEvidence) commitEvidenceSource = 'transcript';
|
|
743
768
|
}
|
|
744
769
|
|
|
745
|
-
// ── 3. HALT detection
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
770
|
+
// ── 3. HALT detection (fallback only) ─────────────────────────────────
|
|
771
|
+
// The SCHEDULER_VERDICT line is the executor's final say (FINISH_PROTOCOL
|
|
772
|
+
// VERDICT LINE step); the halt check is a fallback for runs that printed
|
|
773
|
+
// none. Incident: Rabbits-Game 163-attack-scorecard, 2026-10-24 — a PASS
|
|
774
|
+
// report quoting the marker in backticks was re-queued as a halt.
|
|
775
|
+
const sentinel = scanSentinel(resultEvent, events);
|
|
776
|
+
if (resultEvent && sentinel === null) {
|
|
777
|
+
const haltLine = scanHaltLine(resultEvent.resultText);
|
|
778
|
+
if (haltLine) {
|
|
779
|
+
const rt = resultEvent.resultText;
|
|
780
|
+
let reason = `Agent HALTed: ${haltLine.slice(0, 200)}`;
|
|
752
781
|
// Embed machine-parseable floor date if present in HALT message.
|
|
753
782
|
const dateMatch = rt.match(/~?(\d{4}-\d{2}-\d{2})/);
|
|
754
783
|
if (dateMatch) reason += ` (eligible from: ${dateMatch[1]})`;
|
|
@@ -773,9 +802,6 @@ async function verifyRun({
|
|
|
773
802
|
}
|
|
774
803
|
}
|
|
775
804
|
|
|
776
|
-
// Scan for the SCHEDULER_VERDICT sentinel emitted by the finish protocol.
|
|
777
|
-
const sentinel = scanSentinel(resultEvent, events);
|
|
778
|
-
|
|
779
805
|
// An explicit executor FAIL is always blocking — outranks every other
|
|
780
806
|
// verdict and every override below (early return). Transcript issues are
|
|
781
807
|
// kept as annotations so nothing is lost.
|
|
@@ -1022,6 +1048,7 @@ module.exports = {
|
|
|
1022
1048
|
checkDeps,
|
|
1023
1049
|
parseLog,
|
|
1024
1050
|
scanSentinel,
|
|
1051
|
+
scanHaltLine,
|
|
1025
1052
|
scanSentinelReason,
|
|
1026
1053
|
scanHumanQuestion,
|
|
1027
1054
|
scanForeignWipPathsClaim,
|
|
@@ -19,6 +19,7 @@ const os = require('node:os');
|
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const { splitFrontmatter } = require('../lib/prd/prdFrontmatter.cjs');
|
|
21
21
|
const { deriveEpicIdFromPrdPath } = require('../lib/prd/prdLocations.cjs');
|
|
22
|
+
const { parseFilesSection } = require('../lib/prd/prdGateFiles.cjs');
|
|
22
23
|
const { reportDanglingAgentTypeOnce } = require('../lib/prd/prdAgentType.cjs');
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -100,6 +101,8 @@ async function parsePrdRaw(filePath) {
|
|
|
100
101
|
// queue row is completed (a slug with no row is treated as already
|
|
101
102
|
// done/archived, matching retireCompletedSlugs semantics).
|
|
102
103
|
dependsOn: parseDependsOn(fm.dependsOn),
|
|
104
|
+
// Declared touch-list from the body's `# Files` section ([] when absent).
|
|
105
|
+
files: parseFilesSection(body),
|
|
103
106
|
// Provenance stamp (PRD-authoring lockdown): set only by prdCreate.cjs's
|
|
104
107
|
// buildPrdBody (create) or the update-prd route's legacy-adopt patch
|
|
105
108
|
// (migration/manual adopt). A PRD discovered on disk with no value here
|
|
@@ -16,6 +16,7 @@ const { DEFAULT_PROJECT_CWD } = require('../lib/scheduler/schedulerBatch.cjs');
|
|
|
16
16
|
const { resolvePrdsDirs, resolveArchivedPrdsDirs, resolvePrdWriteDir, listEpicPrdDirs, resolveEpicPrdWriteDir, isSafeEpicId, listArchivedPrdDirs, ancestorEpicPrdDirs, ancestorEpicArchivedPrdDirs } = require('../lib/prd/prdLocations.cjs');
|
|
17
17
|
const { migratePrds, consolidateFlatPrds, legacyAdoptExistingPrds } = require('../lib/prd/prdMigration.cjs');
|
|
18
18
|
const { opsPath } = require('../lib/opsOwnership.cjs');
|
|
19
|
+
const { bareSlug, resolveDepSlug } = require('../lib/prd/depSlugResolve.cjs');
|
|
19
20
|
const { allProjectCwds, projectRootOf } = require('../lib/activeSessions.cjs');
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -268,6 +269,50 @@ async function safeSlugPath(slug) {
|
|
|
268
269
|
return module.exports.safeSlugPathIn(dir, slug);
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Resolve a BARE slug (no `NN-` prefix) to the unique NN-prefixed PRD file —
|
|
274
|
+
* for a caller whose create response was lost and never learned the NN.
|
|
275
|
+
* Scans the live PRD dirs, the archive dirs, and one level of
|
|
276
|
+
* `prds-archived/<ts>/`, matching with depSlugResolve's rule. `cwd` narrows to
|
|
277
|
+
* one project; omitted, it searches every candidate dir. Returns
|
|
278
|
+
* `{ ok: true, path, dir }`, `{ ok: false, reason: 'not-found' }`, or
|
|
279
|
+
* `{ ok: false, reason: 'ambiguous', files, error }` (never guesses).
|
|
280
|
+
*/
|
|
281
|
+
async function resolveBareSlugFile(slug, cwd) {
|
|
282
|
+
if (typeof slug !== 'string' || !slug || bareSlug(slug) !== slug) return { ok: false, reason: 'not-found' };
|
|
283
|
+
const live = cwd ? [module.exports.prdDirForCwd(cwd), ...listEpicPrdDirs(cwd)] : module.exports.candidatePrdsDirs();
|
|
284
|
+
const archived = cwd ? listArchivedPrdDirs(cwd) : module.exports.candidateArchivedPrdsDirs();
|
|
285
|
+
const dirs = [...live];
|
|
286
|
+
for (const a of archived) {
|
|
287
|
+
dirs.push(a);
|
|
288
|
+
const subs = await fsp.readdir(a, { withFileTypes: true }).catch(() => []);
|
|
289
|
+
for (const ent of subs) if (ent.isDirectory()) dirs.push(path.join(a, ent.name));
|
|
290
|
+
}
|
|
291
|
+
const found = []; // { slug, dir, file }
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
for (const dir of dirs) {
|
|
294
|
+
const names = await fsp.readdir(dir).catch(() => []);
|
|
295
|
+
for (const n of names) {
|
|
296
|
+
if (!n.endsWith('.md')) continue;
|
|
297
|
+
const file = path.join(dir, n);
|
|
298
|
+
if (seen.has(file)) continue;
|
|
299
|
+
seen.add(file);
|
|
300
|
+
found.push({ slug: n.slice(0, -3), dir, file });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const matched = new Set(resolveDepSlug(slug, found.map((f) => f.slug)));
|
|
304
|
+
const hits = found.filter((f) => matched.has(f.slug));
|
|
305
|
+
if (hits.length === 0) return { ok: false, reason: 'not-found' };
|
|
306
|
+
if (hits.length > 1) {
|
|
307
|
+
const files = hits.map((h) => h.file);
|
|
308
|
+
return { ok: false, reason: 'ambiguous', files, error: `ambiguous slug "${slug}": matches ${files.join(' and ')} — use the full NN-prefixed slug` };
|
|
309
|
+
}
|
|
310
|
+
const only = hits[0];
|
|
311
|
+
const safe = module.exports.safeSlugPathIn(only.dir, only.slug);
|
|
312
|
+
if (!safe) return { ok: false, reason: 'not-found' };
|
|
313
|
+
return { ok: true, path: safe, dir: only.dir };
|
|
314
|
+
}
|
|
315
|
+
|
|
271
316
|
/**
|
|
272
317
|
* The two distinct failure modes safeSlugPath collapses into one nullable
|
|
273
318
|
* return (the defect this fixes — see the PRD that added this helper's
|
|
@@ -291,10 +336,10 @@ async function resolveSlugOrReason(slug, cwd) {
|
|
|
291
336
|
return { ok: true, path: p };
|
|
292
337
|
} catch { /* not in this dir — try the next candidate */ }
|
|
293
338
|
}
|
|
294
|
-
return
|
|
339
|
+
return module.exports.resolveBareSlugFile(slug, cwd);
|
|
295
340
|
}
|
|
296
341
|
const dir = await module.exports.findPrdDir(slug);
|
|
297
|
-
if (!dir) return
|
|
342
|
+
if (!dir) return module.exports.resolveBareSlugFile(slug, cwd);
|
|
298
343
|
const p = module.exports.safeSlugPathIn(dir, slug);
|
|
299
344
|
if (!p) return { ok: false, reason: 'not-found' };
|
|
300
345
|
return { ok: true, path: p };
|
|
@@ -522,6 +567,7 @@ module.exports = {
|
|
|
522
567
|
safeSlugPathIn,
|
|
523
568
|
realPrdsDir,
|
|
524
569
|
safeSlugPath,
|
|
570
|
+
resolveBareSlugFile,
|
|
525
571
|
resolveSlugOrReason,
|
|
526
572
|
jobMatchesSlugAndCwd,
|
|
527
573
|
unknownSlugMessage,
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -82,6 +82,7 @@ const { detectRateLimitInLog } = require('./lib/claude/rateLimitDetect.cjs');
|
|
|
82
82
|
const { stripAppOwnedChurn } = require('./lib/scheduler/jobDirtFilter.cjs');
|
|
83
83
|
const { GATE_AUTHORITY_VERDICTS, decideGateAuthority } = require('./lib/scheduler/gateAuthority.cjs');
|
|
84
84
|
const { decideBaselineAuthority } = require('./lib/scheduler/baselineGate.cjs');
|
|
85
|
+
const { formatSetupOutcome: formatWorktreeSetupOutcome, isSetupSettled: isWorktreeSetupSettled } = require('./lib/git/worktreeSetup.cjs');
|
|
85
86
|
const { linkDependencyDirs } = require('./lib/git/worktreeDeps.cjs');
|
|
86
87
|
const { checkMainDependencyHealth, repairMainDependencies } = require('./lib/git/depHealth.cjs');
|
|
87
88
|
// cwd -> last main-checkout dependency repair attempt (ms); caps repair to once per cooldown.
|
|
@@ -107,6 +108,8 @@ const { appendResponseEventIfKnown, appendResponseEventWithReason } = require('.
|
|
|
107
108
|
const { buildReviewNotice, selectDueReviewNotices, formatReviewNotice, holdMsFromEnv } = require('./lib/scheduler/reviewNotice.cjs');
|
|
108
109
|
const epicValidationHook = require('./lib/epic/epicValidationHook.cjs');
|
|
109
110
|
const prdCreate = require('./lib/prd/prdCreate.cjs');
|
|
111
|
+
const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
|
|
112
|
+
const { expandHome } = require('./lib/util/expandHome.cjs');
|
|
110
113
|
const { parseValidationSentinels } = require('./lib/scheduler/validationSentinels.cjs');
|
|
111
114
|
const { hasDownstreamValidator } = require('./lib/prd/planValidator.cjs');
|
|
112
115
|
const promptSessionTranscript = require('./promptSessionTranscript.cjs');
|
|
@@ -187,6 +190,8 @@ const queueStore = require('./lib/scheduler/queueStore.cjs');
|
|
|
187
190
|
const supervisorRecord = require('./lib/scheduler/jobSupervisorRecord.cjs');
|
|
188
191
|
const adoptedRunSupervisor = require('./lib/scheduler/adoptedRunSupervisor.cjs');
|
|
189
192
|
const { splitFrontmatter, parsePrdFile, serializePrdFile } = require('./lib/prd/prdFrontmatter.cjs');
|
|
193
|
+
const { applyStructuredPrdPatch } = require('./lib/prd/prdBodyPatch.cjs');
|
|
194
|
+
const { resolvePersonaDefaultEstimate, DEFAULT_PRD_AGENT_TYPE } = require('./lib/prd/prdAgentType.cjs');
|
|
190
195
|
const { resolveDepSlug, findNearMatches } = require('./lib/prd/depSlugResolve.cjs');
|
|
191
196
|
const { computeDispositionRewrite, mintPlanId, resolveInheritedPlanId } = require('./lib/prd/prdDisposition.cjs');
|
|
192
197
|
const { allProjectCwds } = require('./lib/activeSessions.cjs');
|
|
@@ -539,7 +544,7 @@ function biasJobOomScore(pid) {
|
|
|
539
544
|
async function recheckPrdSourceForJob(job) {
|
|
540
545
|
const slug = job && job.slug;
|
|
541
546
|
if (typeof slug !== 'string' || !slug || path.basename(slug) !== slug) return null;
|
|
542
|
-
try { require('./lib/activeSessions.cjs').bustProjectCwdCache(); } catch { /* best-effort */ }
|
|
547
|
+
try { require('./lib/activeSessions.cjs').bustProjectCwdCache({ deep: true }); } catch { /* best-effort */ }
|
|
543
548
|
const dirs = [...(await prdPaths.jobScopedPrdDirs(job))];
|
|
544
549
|
const found = await prdPaths.findPrdDirForJob(job);
|
|
545
550
|
if (found) dirs.push(found);
|
|
@@ -563,7 +568,7 @@ async function recheckPrdSourceForJob(job) {
|
|
|
563
568
|
* fixture that leaked into the live queue and was cleaned up by its own
|
|
564
569
|
* `finally` block). Retire the row rather than fail it.
|
|
565
570
|
*/
|
|
566
|
-
function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, reason = 'prd-archived') {
|
|
571
|
+
function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, reason = 'prd-archived', extraMeta = {}) {
|
|
567
572
|
const msg = reason === 'prd-missing'
|
|
568
573
|
? 'PRD source no longer exists on disk — retiring stale queue entry'
|
|
569
574
|
: (() => {
|
|
@@ -575,7 +580,7 @@ function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd,
|
|
|
575
580
|
const finishedAt = Date.now();
|
|
576
581
|
config.writeJsonSync(metaPath, {
|
|
577
582
|
slug: job.slug, cwd, sessionId, exitCode: 0, skipped: reason,
|
|
578
|
-
note: msg, startedAt, finishedAt, durationMs: 0, ...SCHEDULER_META_IDENTITY,
|
|
583
|
+
note: msg, startedAt, finishedAt, durationMs: 0, ...SCHEDULER_META_IDENTITY, ...extraMeta,
|
|
579
584
|
});
|
|
580
585
|
return { exitCode: 0, durationMs: 0, skipped: reason, note: msg, sessionId };
|
|
581
586
|
}
|
|
@@ -1604,6 +1609,7 @@ async function reconcile(state) {
|
|
|
1604
1609
|
// membership, so moving the file between Epic dirs must re-point the row.
|
|
1605
1610
|
epicId: p.epicId ?? job.epicId ?? null,
|
|
1606
1611
|
dependsOn: p.dependsOn,
|
|
1612
|
+
files: p.files ?? [],
|
|
1607
1613
|
disposition: p.disposition ?? null,
|
|
1608
1614
|
planId: p.planId ?? null,
|
|
1609
1615
|
quietMachine: p.quietMachine === true,
|
|
@@ -1727,6 +1733,7 @@ async function reconcile(state) {
|
|
|
1727
1733
|
sourceTabId: p.sourceTabId ?? inv.row?.sourceTabId ?? null,
|
|
1728
1734
|
epicId: p.epicId ?? inv.row?.epicId ?? null,
|
|
1729
1735
|
dependsOn: p.dependsOn,
|
|
1736
|
+
files: p.files ?? [],
|
|
1730
1737
|
disposition: p.disposition ?? null,
|
|
1731
1738
|
planId: p.planId ?? null,
|
|
1732
1739
|
quietMachine: p.quietMachine === true,
|
|
@@ -1862,6 +1869,7 @@ async function reconcile(state) {
|
|
|
1862
1869
|
sourceTabId: p.sourceTabId,
|
|
1863
1870
|
epicId: p.epicId ?? null,
|
|
1864
1871
|
dependsOn: p.dependsOn,
|
|
1872
|
+
files: p.files ?? [],
|
|
1865
1873
|
disposition: p.disposition ?? null,
|
|
1866
1874
|
planId: p.planId ?? null,
|
|
1867
1875
|
quietMachine: p.quietMachine === true,
|
|
@@ -4205,13 +4213,14 @@ function ensureTimeoutShimOnce() {
|
|
|
4205
4213
|
* Watchdogs are declared as an array; the result-tailer's exit-code mapping
|
|
4206
4214
|
* (success+killedBySignal → 0) is scheduler-specific and lives in onExit.
|
|
4207
4215
|
*/
|
|
4208
|
-
async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null, reviewInRun = true) {
|
|
4216
|
+
async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null, reviewInRun = true, worktreeSetup = null) {
|
|
4209
4217
|
// First statement — stamps the 'exec-entered' dispatch-phase breadcrumb
|
|
4210
4218
|
// before openLog below, so a hang inside openLog/spawn itself still shows
|
|
4211
4219
|
// execution reached this function (see spawnJob's dispatchPhase comment).
|
|
4212
4220
|
if (onPhase) await onPhase('exec-entered');
|
|
4213
4221
|
const logPath = path.join(runDir, `${job.slug}.log`);
|
|
4214
4222
|
const metaPath = path.join(runDir, `${job.slug}.meta.json`);
|
|
4223
|
+
const setupMeta = worktreeSetup ? { worktreeSetup } : {};
|
|
4215
4224
|
// `cwd` stays the MAIN tree throughout — PRD lookup (findPrdDir/prdPathForJob)
|
|
4216
4225
|
// and every git call below key off this value, never the worktree. Only the
|
|
4217
4226
|
// spawned child's own process cwd (spawnCwd) may point at a job worktree —
|
|
@@ -4237,6 +4246,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4237
4246
|
|
|
4238
4247
|
safeLog(`[scheduler] starting ${job.slug} at ${new Date().toISOString()}\n[scheduler] cwd=${cwd}` +
|
|
4239
4248
|
(spawnCwd !== cwd ? ` (running isolated in worktree ${spawnCwd})` : '') + '\n\n');
|
|
4249
|
+
if (worktreeSetup) {
|
|
4250
|
+
try {
|
|
4251
|
+
const setupLines = formatWorktreeSetupOutcome(worktreeSetup);
|
|
4252
|
+
if (setupLines.length) safeLog(setupLines.join('\n') + '\n\n');
|
|
4253
|
+
} catch { /* logging must never break a run */ }
|
|
4254
|
+
}
|
|
4240
4255
|
|
|
4241
4256
|
// Dead-cwd guard: verify the target directory exists and is traversable
|
|
4242
4257
|
// before handing it to the child process. Checks spawnCwd (where the child
|
|
@@ -4249,7 +4264,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4249
4264
|
// Sync write: this is an early-exit error path inside an async function,
|
|
4250
4265
|
// so we could await, but using the sync variant keeps the error path
|
|
4251
4266
|
// ordering identical to the spawn-failed branch below (also sync).
|
|
4252
|
-
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY });
|
|
4267
|
+
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY, ...setupMeta });
|
|
4253
4268
|
return { exitCode: -1, durationMs: 0, error: errMsg, sessionId };
|
|
4254
4269
|
}
|
|
4255
4270
|
|
|
@@ -4327,12 +4342,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4327
4342
|
// real failure. Anything else (malformed/unreadable PRD) is a real
|
|
4328
4343
|
// failure and keeps exitCode: -1.
|
|
4329
4344
|
if (await prdPaths.archivedTwinExists(job)) {
|
|
4330
|
-
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
4345
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived', setupMeta);
|
|
4331
4346
|
}
|
|
4332
4347
|
if (e2 && e2.code === 'ENOENT') {
|
|
4333
4348
|
const rc = await recheckBeforeMissingSkip();
|
|
4334
4349
|
if (rc && rc !== 'resolved') return rc;
|
|
4335
|
-
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
4350
|
+
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing', setupMeta);
|
|
4336
4351
|
} else {
|
|
4337
4352
|
safeLog(`[scheduler] failed to read PRD: ${e2?.message}\n`);
|
|
4338
4353
|
closeFd();
|
|
@@ -4347,12 +4362,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4347
4362
|
// than a hard failure. Anything else (a real read/parse error) still
|
|
4348
4363
|
// fails the job.
|
|
4349
4364
|
if (await prdPaths.archivedTwinExists(job)) {
|
|
4350
|
-
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
4365
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived', setupMeta);
|
|
4351
4366
|
}
|
|
4352
4367
|
if (e && e.code === 'ENOENT') {
|
|
4353
4368
|
const rc = await recheckBeforeMissingSkip();
|
|
4354
4369
|
if (rc && rc !== 'resolved') return rc;
|
|
4355
|
-
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
4370
|
+
if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing', setupMeta);
|
|
4356
4371
|
} else {
|
|
4357
4372
|
safeLog(`[scheduler] failed to read PRD: ${e?.message}\n`);
|
|
4358
4373
|
closeFd();
|
|
@@ -4437,7 +4452,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4437
4452
|
if (!promptCheck.ok) {
|
|
4438
4453
|
safeLog(`[scheduler] ${promptCheck.error}\n`);
|
|
4439
4454
|
closeFd();
|
|
4440
|
-
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: promptCheck.error, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY });
|
|
4455
|
+
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: promptCheck.error, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY, ...setupMeta });
|
|
4441
4456
|
return { exitCode: -1, durationMs: 0, error: promptCheck.error, sessionId };
|
|
4442
4457
|
}
|
|
4443
4458
|
|
|
@@ -4603,7 +4618,8 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4603
4618
|
// is calibrated against). `quietMachine` jobs and any PRD with an
|
|
4604
4619
|
// explicit `budgetExempt: true` opt out entirely — logged once here so
|
|
4605
4620
|
// an unbounded job is never silently unbounded.
|
|
4606
|
-
const jobBudgetMs = budget.computeJobBudgetMs(
|
|
4621
|
+
const jobBudgetMs = budget.computeJobBudgetMs(
|
|
4622
|
+
job.estimateMinutes ?? resolvePersonaDefaultEstimate(job.cwd, job.agentType || DEFAULT_PRD_AGENT_TYPE));
|
|
4607
4623
|
const budgetExempt = budget.isJobBudgetExempt(job);
|
|
4608
4624
|
if (budgetExempt) {
|
|
4609
4625
|
safeLog(`[scheduler] wall-clock budget watchdog EXEMPT for ${job.slug} ` +
|
|
@@ -4701,7 +4717,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4701
4717
|
sl(`\n[scheduler] ${errMsg}\n`);
|
|
4702
4718
|
// Sync write: inside a Promise executor callback; must flush meta
|
|
4703
4719
|
// before resolve() so the spawnJob mutate() that follows sees it.
|
|
4704
|
-
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked, ...SCHEDULER_META_IDENTITY, originSessionId, contextDigestApplied });
|
|
4720
|
+
config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked, ...SCHEDULER_META_IDENTITY, ...setupMeta, originSessionId, contextDigestApplied });
|
|
4705
4721
|
resolve({ exitCode: -1, durationMs, error: errMsg, leakedDescendants: leaked, sessionId });
|
|
4706
4722
|
return;
|
|
4707
4723
|
}
|
|
@@ -4763,7 +4779,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4763
4779
|
startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked,
|
|
4764
4780
|
agentResultSubtype, mappedFromSignal: mappedToSuccess ? signal || `code=${exitCode}` : null,
|
|
4765
4781
|
killedByWatchdog: effectiveKilledByWatchdog, budgetKillReason,
|
|
4766
|
-
...SCHEDULER_META_IDENTITY,
|
|
4782
|
+
...SCHEDULER_META_IDENTITY, ...setupMeta,
|
|
4767
4783
|
originSessionId, contextDigestApplied,
|
|
4768
4784
|
});
|
|
4769
4785
|
resolve({
|
|
@@ -5722,6 +5738,23 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
5722
5738
|
await broadcast({ flush: true });
|
|
5723
5739
|
return;
|
|
5724
5740
|
}
|
|
5741
|
+
// Early returns after the preflight worktree exists must not leak the
|
|
5742
|
+
// checkout or its cap count (retries would also re-run the whole setup).
|
|
5743
|
+
const releasePreflightWorktree = async () => {
|
|
5744
|
+
if (!preflightWorktree.ok) return;
|
|
5745
|
+
try {
|
|
5746
|
+
await jobWorktree.cleanupJobWorktree({
|
|
5747
|
+
cwd: job.cwd || defaultCwd,
|
|
5748
|
+
dir: preflightWorktree.dir,
|
|
5749
|
+
branch: preflightWorktree.branch,
|
|
5750
|
+
keepBranch: false,
|
|
5751
|
+
});
|
|
5752
|
+
} catch (err) {
|
|
5753
|
+
try {
|
|
5754
|
+
reportSchedulerError("preflight worktree cleanup failed", job.slug, err);
|
|
5755
|
+
} catch { /* reporting must never throw out of spawnJob */ }
|
|
5756
|
+
}
|
|
5757
|
+
};
|
|
5725
5758
|
// Launch circuit breaker (lib/scheduler/launchFailure.cjs, issue #11). Re-evaluated
|
|
5726
5759
|
// here, not just in tickQueue, because the block can change between the
|
|
5727
5760
|
// pick and this dispatch (another job's probe just failed). 'blocked' →
|
|
@@ -5761,6 +5794,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
5761
5794
|
});
|
|
5762
5795
|
if (launchGate.state === 'blocked') {
|
|
5763
5796
|
console.log(`[scheduler] ${job.slug}: deferring — ${launchGate.reason}`);
|
|
5797
|
+
await releasePreflightWorktree();
|
|
5764
5798
|
await broadcast({ flush: true });
|
|
5765
5799
|
return;
|
|
5766
5800
|
}
|
|
@@ -5872,7 +5906,10 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
5872
5906
|
}
|
|
5873
5907
|
});
|
|
5874
5908
|
await broadcast({ flush: true });
|
|
5875
|
-
if (dispatchSkippedAlreadyCompleted)
|
|
5909
|
+
if (dispatchSkippedAlreadyCompleted) {
|
|
5910
|
+
await releasePreflightWorktree();
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5876
5913
|
|
|
5877
5914
|
await mutate((s) => {
|
|
5878
5915
|
const idx = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
@@ -5940,6 +5977,19 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
5940
5977
|
const worktree = preflightWorktree;
|
|
5941
5978
|
if (worktree.ok) {
|
|
5942
5979
|
console.log(`[scheduler] ${job.slug}: isolated in worktree ${worktree.dir} (branch ${worktree.branch})`);
|
|
5980
|
+
if (worktree.setup && !isWorktreeSetupSettled(worktree.setup)) {
|
|
5981
|
+
try {
|
|
5982
|
+
const cmds = (Array.isArray(worktree.setup.commands) ? worktree.setup.commands : [])
|
|
5983
|
+
.map((c) => `${c && c.cmd}: exit ${c && c.code} in ${(c && c.ms) || 0}ms`);
|
|
5984
|
+
appendError({
|
|
5985
|
+
cwd: job.cwd || defaultCwd,
|
|
5986
|
+
scope: 'scheduler',
|
|
5987
|
+
level: 'warn',
|
|
5988
|
+
message: `${job.slug}: worktree setup did not pass (${worktree.setup.reason})${cmds.length ? ' — ' + cmds.join('; ') : ''}`,
|
|
5989
|
+
meta: { slug: job.slug, cwd: job.cwd || defaultCwd, reason: worktree.setup.reason, commands: (worktree.setup.commands || []).map((c) => ({ cmd: c && c.cmd, code: c && c.code, ms: c && c.ms })) },
|
|
5990
|
+
});
|
|
5991
|
+
} catch { /* durable logging must never break dispatch */ }
|
|
5992
|
+
}
|
|
5943
5993
|
} else {
|
|
5944
5994
|
console.log(`[scheduler] ${job.slug}: running in main tree (worktree not used: ${worktree.reason})`);
|
|
5945
5995
|
// A job losing worktree isolation must never be a silent downgrade
|
|
@@ -6045,7 +6095,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6045
6095
|
s.jobs[idx].dispatchPhaseAt = new Date().toISOString();
|
|
6046
6096
|
}
|
|
6047
6097
|
});
|
|
6048
|
-
}, reviewInRun);
|
|
6098
|
+
}, reviewInRun, worktree.ok ? worktree.setup : { ran: false, ok: false, reason: 'in-place', waitedMs: 0, ms: 0, commands: [] });
|
|
6049
6099
|
} finally {
|
|
6050
6100
|
if (worktree.ok) {
|
|
6051
6101
|
({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha } =
|
|
@@ -8894,15 +8944,23 @@ async function runGateShadow(job) {
|
|
|
8894
8944
|
} catch { ancestorOk = false; }
|
|
8895
8945
|
const memo = job.gateShadow?.baseline;
|
|
8896
8946
|
let baseOutcome = memo && memo.head === headBefore && memo.outcome ? memo.outcome : null;
|
|
8947
|
+
let baseSetup = baseOutcome ? (memo.setup ?? null) : null;
|
|
8897
8948
|
if (!baseOutcome) {
|
|
8898
8949
|
const scratch = path.join(os.tmpdir(), `sm-baseline-gate-${process.pid}-${Date.now()}`);
|
|
8899
8950
|
let added = false;
|
|
8900
8951
|
try {
|
|
8901
8952
|
await execGitAt(job.cwd, ['worktree', 'add', '--detach', scratch, job.guardHeadBefore], { timeout: 120_000 });
|
|
8902
8953
|
added = true;
|
|
8903
|
-
await linkDependencyDirs({ mainCwd: job.cwd, worktreeCwd: scratch });
|
|
8904
|
-
|
|
8905
|
-
|
|
8954
|
+
const deps = await linkDependencyDirs({ mainCwd: job.cwd, worktreeCwd: scratch });
|
|
8955
|
+
baseSetup = deps && deps.setup ? deps.setup : null;
|
|
8956
|
+
if (!isWorktreeSetupSettled(baseSetup)) {
|
|
8957
|
+
// A declared setup that did not pass can leave a broken cache (cold Godot
|
|
8958
|
+
// import); a base gate failing for that reason must not excuse HEAD's failures.
|
|
8959
|
+
baseOutcome = { status: 'unavailable', reason: `base-setup-${baseSetup?.reason ?? 'error'}`, results: [] };
|
|
8960
|
+
} else {
|
|
8961
|
+
const r = await runGateSequence(gate.sequence, { cwd: scratch });
|
|
8962
|
+
baseOutcome = r.status === 'busy' ? { status: 'unavailable', reason: 'busy', results: [] } : r;
|
|
8963
|
+
}
|
|
8906
8964
|
} catch (e) {
|
|
8907
8965
|
baseOutcome = { status: 'unavailable', reason: `base-gate-error: ${e && e.message ? e.message : e}`, results: [] };
|
|
8908
8966
|
} finally {
|
|
@@ -8914,7 +8972,7 @@ async function runGateShadow(job) {
|
|
|
8914
8972
|
}
|
|
8915
8973
|
}
|
|
8916
8974
|
baselineDecision = decideBaselineAuthority({ job, headOutcome: outcome, baseOutcome, evidenceOk, ancestorOk, cleanOk, headStable });
|
|
8917
|
-
gateShadow.baseline = { head: headBefore, base: job.guardHeadBefore, outcome: baseOutcome, ...baselineDecision };
|
|
8975
|
+
gateShadow.baseline = { head: headBefore, base: job.guardHeadBefore, outcome: baseOutcome, setup: baseSetup, ...baselineDecision };
|
|
8918
8976
|
}
|
|
8919
8977
|
|
|
8920
8978
|
const runId = job.runId || reviewLadder.resolveRunId(job);
|
|
@@ -10358,12 +10416,20 @@ function toCompactPrdEntry(entry) {
|
|
|
10358
10416
|
* `schedule:list-prds` IPC handler and the admin HTTP `GET
|
|
10359
10417
|
* /admin/scheduler/prds` route (PRD 1024) — neither re-implements this scan.
|
|
10360
10418
|
*/
|
|
10361
|
-
|
|
10419
|
+
function normalizeListCwd(cwd) {
|
|
10420
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return cwd;
|
|
10421
|
+
// Same normalization createPrd applies to frontmatter cwd: tilde expansion,
|
|
10422
|
+
// trailing-slash strip, Epic-worktree path -> main checkout.
|
|
10423
|
+
return resolveProjectContext({ cwd: expandHome(cwd) }).cwd || cwd;
|
|
10424
|
+
}
|
|
10425
|
+
|
|
10426
|
+
async function listPrdsInternal({ cwd: rawCwd } = {}) {
|
|
10362
10427
|
ensureDirs();
|
|
10428
|
+
const cwd = normalizeListCwd(rawCwd);
|
|
10363
10429
|
const out = [];
|
|
10364
10430
|
const seenSlugs = new Set();
|
|
10365
10431
|
|
|
10366
|
-
async function readDirInto(dir, { archived }) {
|
|
10432
|
+
async function readDirInto(dir, { archived, nested = false }) {
|
|
10367
10433
|
let entries;
|
|
10368
10434
|
try {
|
|
10369
10435
|
entries = await fsp.readdir(dir);
|
|
@@ -10373,9 +10439,20 @@ async function listPrdsInternal({ cwd } = {}) {
|
|
|
10373
10439
|
}
|
|
10374
10440
|
return;
|
|
10375
10441
|
}
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10442
|
+
const files = entries.filter((n) => n.endsWith('.md') && !n.startsWith('.')).map((n) => path.join(dir, n));
|
|
10443
|
+
if (archived && !nested) {
|
|
10444
|
+
// prds-archived/<ISO-ts>/*.md — exactly one level down (mirrors
|
|
10445
|
+
// budget.cjs computeDepHistorySatisfaction); never recurse further.
|
|
10446
|
+
for (const name of entries) {
|
|
10447
|
+
if (name.endsWith('.md') || name.startsWith('.')) continue;
|
|
10448
|
+
const sub = path.join(dir, name);
|
|
10449
|
+
let st;
|
|
10450
|
+
try { st = await fsp.lstat(sub); } catch { continue; }
|
|
10451
|
+
if (st.isDirectory()) await readDirInto(sub, { archived, nested: true });
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10454
|
+
for (const filePath of files) {
|
|
10455
|
+
const name = path.basename(filePath);
|
|
10379
10456
|
try {
|
|
10380
10457
|
const parsed = await parsePrd(filePath);
|
|
10381
10458
|
// A slug can't be both live and archived at once, but a duplicate
|
|
@@ -10667,9 +10744,10 @@ const remote = {
|
|
|
10667
10744
|
// paging by offset can't skip or duplicate an entry between requests as
|
|
10668
10745
|
// long as the underlying PRD set doesn't change mid-page.
|
|
10669
10746
|
async listPrds(filter = {}) {
|
|
10670
|
-
const
|
|
10747
|
+
const filterCwd = normalizeListCwd(filter.cwd);
|
|
10748
|
+
const all = await listPrdsInternal({ cwd: filterCwd });
|
|
10671
10749
|
const filtered = all.filter((entry) => {
|
|
10672
|
-
if (
|
|
10750
|
+
if (filterCwd && entry.cwd !== filterCwd) return false;
|
|
10673
10751
|
if (filter.epicId && entry.epicId !== filter.epicId) return false;
|
|
10674
10752
|
if (filter.status && entry.status !== filter.status) return false;
|
|
10675
10753
|
return true;
|
|
@@ -10757,7 +10835,15 @@ const remote = {
|
|
|
10757
10835
|
// prdFrontmatter.cjs's parsePrdFile/serializePrdFile round-trip pair (PRD
|
|
10758
10836
|
// 1024) so unrecognized keys (e.g. dependsOn) and untouched recognized
|
|
10759
10837
|
// keys' original line formatting survive unchanged.
|
|
10760
|
-
async updatePrd({ slug, cwd, frontmatter, body }) {
|
|
10838
|
+
async updatePrd({ slug, cwd, frontmatter, body, gate, files, acceptanceCriteria }) {
|
|
10839
|
+
const structured = {};
|
|
10840
|
+
if (gate !== undefined) structured.gate = gate;
|
|
10841
|
+
if (files !== undefined) structured.files = files;
|
|
10842
|
+
if (acceptanceCriteria !== undefined) structured.acceptanceCriteria = acceptanceCriteria;
|
|
10843
|
+
const hasStructured = Object.keys(structured).length > 0;
|
|
10844
|
+
if (hasStructured && body !== undefined) {
|
|
10845
|
+
return { ok: false, error: 'Pass either body (full replacement) or the structured fields gate/files/acceptanceCriteria, not both.' };
|
|
10846
|
+
}
|
|
10761
10847
|
const job = await this.getJob(slug);
|
|
10762
10848
|
const EDITABLE_JOB_STATUSES = new Set(['pending', 'quarantined', 'needs_review', 'failed', 'skipped']);
|
|
10763
10849
|
if (job && !EDITABLE_JOB_STATUSES.has(job.status)) {
|
|
@@ -10837,13 +10923,22 @@ const remote = {
|
|
|
10837
10923
|
fm[key] = frontmatter[key];
|
|
10838
10924
|
}
|
|
10839
10925
|
}
|
|
10840
|
-
|
|
10926
|
+
let newBody = body !== undefined ? body : origBody;
|
|
10927
|
+
let patchWarnings = [];
|
|
10928
|
+
if (hasStructured) {
|
|
10929
|
+
const patched = applyStructuredPrdPatch(origBody, structured);
|
|
10930
|
+
if (!patched.ok) return { ok: false, error: patched.error };
|
|
10931
|
+
newBody = patched.body;
|
|
10932
|
+
patchWarnings = patched.warnings || [];
|
|
10933
|
+
}
|
|
10841
10934
|
const newRaw = serializePrdFile(fm, newBody);
|
|
10842
10935
|
|
|
10843
10936
|
try {
|
|
10844
10937
|
await config.writeTextAtomic(filePath, newRaw, { writer: 'scheduler' });
|
|
10845
10938
|
const stat = await fsp.stat(filePath);
|
|
10846
|
-
return
|
|
10939
|
+
return patchWarnings.length
|
|
10940
|
+
? { ok: true, slug, bytesWritten: stat.size, warnings: patchWarnings }
|
|
10941
|
+
: { ok: true, slug, bytesWritten: stat.size };
|
|
10847
10942
|
} catch (e) {
|
|
10848
10943
|
return { ok: false, error: e?.message ?? 'write failed' };
|
|
10849
10944
|
}
|
|
@@ -198,4 +198,4 @@ async function seedDevPlugin({
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
module.exports = { seedDevPlugin, upgradeIfStale, marketplaceIsStale, compareSemver, markerPath, MAX_ATTEMPTS };
|
|
201
|
+
module.exports = { seedDevPlugin, writeMarker, PLUGIN_SLUG, MARKETPLACE, ENABLED_KEY, upgradeIfStale, marketplaceIsStale, compareSemver, markerPath, MAX_ATTEMPTS };
|
|
@@ -209,4 +209,4 @@ async function seedSchedulerMcp({ logger = console, addFn = runClaudeMcpAdd, rem
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
module.exports = { seedSchedulerMcp, mcpAddArgv, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
|
|
212
|
+
module.exports = { seedSchedulerMcp, writeMarker, runClaudeMcpAdd, runClaudeMcpRemove, mcpAddArgv, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- PRD_AUTHORING.md
|
|
1
|
+
<!-- PRD_AUTHORING.md v13 -->
|
|
2
2
|
# PRD Authoring Guide — Scheduler Safety Rules
|
|
3
3
|
|
|
4
4
|
Rule: every PRD you queue follows the rules below.
|
|
@@ -69,7 +69,7 @@ if (!found) {
|
|
|
69
69
|
Rule: every PRD needs these fields.
|
|
70
70
|
1. `title` — one line, plain English.
|
|
71
71
|
2. `cwd` — the Epic's project root, as an absolute path or `~/…`. The API always sets it to the Epic's project, whatever you pass. To work in another project, open an Epic in that project. The folder must exist when you queue.
|
|
72
|
-
3. `estimateMinutes` — optional; omit it. A run without one gets the 45-minute
|
|
72
|
+
3. `estimateMinutes` — optional; omit it. A run without one gets its persona's `defaultEstimateMinutes`, else the 45-minute floor. Pass one only if the run legitimately needs longer. Most PRDs take 5–10 minutes (§8).
|
|
73
73
|
|
|
74
74
|
`parallelGroup` is deprecated and ignored. `dependsOn: [<slug>, ...]` is the only ordering primitive.
|
|
75
75
|
|
|
@@ -100,12 +100,13 @@ Both fields are required in every `scheduler_create_prd` call.
|
|
|
100
100
|
7. Keep each entry on one line, at most 500 chars, with plain spaces between words.
|
|
101
101
|
8. Use `["none"]` only for docs or config with no runnable check. Write exactly `none`. Never mix `none` with commands.
|
|
102
102
|
9. Put `NAME=value` words before `timeout`, never after it, for example `CI=1 timeout 300 npm test`. A leading `TMPDIR=$(mktemp -d) ` is allowed but not needed: the scheduler gives each gate its own TMPDIR.
|
|
103
|
+
10. A gate command must not create or modify tracked files; write outputs to the per-gate TMPDIR or a gitignored path. Why: a dirtied tree fails the clean-tree check.
|
|
103
104
|
|
|
104
105
|
### files
|
|
105
106
|
|
|
106
107
|
1. 1–50 repo-relative paths. A folder ends with `/`.
|
|
107
108
|
2. No absolute paths, no `~` at the start, no `..` or `.` segments, no `*` or `?`, no `\` or backticks, no leading `-`. Use `/` between folders.
|
|
108
|
-
3.
|
|
109
|
+
3. The scheduler never runs two PRDs with overlapping files at once (held as `files-overlap`); `dependsOn` is for real output or order dependencies only.
|
|
109
110
|
|
|
110
111
|
### What the API writes
|
|
111
112
|
|
|
@@ -125,7 +126,7 @@ A short Epic-context digest is prepended automatically for orientation only —
|
|
|
125
126
|
|
|
126
127
|
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
128
|
|
|
128
|
-
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus
|
|
129
|
+
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `node_modules` and non-uv `.venv`/`venv` symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store, and uv projects (uv.lock) get a worktree-local env built by an offline frozen `uv sync`, not a symlink. 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. A project may declare `worktreeSetup` gate-syntax commands in a tracked `.session-manager.json` at the repo root; they run once per fresh job worktree and baseline scratch worktree, before the executor starts, with cwd = the worktree root (a project in a subfolder names it, e.g. `timeout 600 godot --headless --path game --import`). Setups run one at a time per project and are never skipped; they must write only inside the worktree (node_modules and non-uv venvs are symlinks into the main checkout). The outcome is the `[scheduler] worktree setup:` lines at the top of the run log plus `worktreeSetup` in the run's meta.json. Host crontab installs stay human-only (see §14).
|
|
129
130
|
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
131
|
|
|
131
132
|
---
|
|
@@ -150,6 +151,8 @@ npm test || { echo "HALT: npm test failed — see above"; exit 1; }
|
|
|
150
151
|
|
|
151
152
|
Note: a `rateLimited` exit-1 is the scheduler's own benign auto-pause (it resumes at the next 5h reset) — don't engineer retry logic for it.
|
|
152
153
|
|
|
154
|
+
Note: `HALT:` in a script's output, a PRD, or a report is plain text to the scheduler. Only the verdict line decides the outcome; a report line that starts with `HALT:` re-queues the job only when the report has no `SCHEDULER_VERDICT` line. Why: the verifier used to match `HALT:` anywhere in a report, so finished PASS runs that only described a tool's failure path were re-queued and parked (Rabbits-Game `163-attack-scorecard`, 2026-10-24).
|
|
155
|
+
|
|
153
156
|
---
|
|
154
157
|
|
|
155
158
|
## §10 Negative-assertion checks must exit 0 on the clean case
|
|
@@ -224,3 +227,4 @@ Before queueing a new PRD, verify each of these:
|
|
|
224
227
|
- [ ] **§11 End green:** the gate runs last; any intentional failure runs early and is captured.
|
|
225
228
|
- [ ] **Gate baseline:** every gate command ran green once at HEAD in the main checkout; a red suite is scoped to the PRD's own test files and the PRD notes say why. Why: a gate already red on base parks as `sentinel_fail`.
|
|
226
229
|
- [ ] **No human-only gates:** no gate or criterion needs a human or the passage of time (cron install, live post wait, live payment, dashboard action); those end `SCHEDULER_VERDICT: NEEDS_HUMAN <question>`. Why: no executor can pass them.
|
|
230
|
+
- [ ] **Gate purity:** a gate command must not create or modify tracked files; outputs go to the per-gate TMPDIR or a gitignored path. Why: a dirtied tree fails the clean-tree check.
|