claude-code-session-manager 0.95.0 → 0.97.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/{DataModel-DZK9QZpV.js → DataModel-D93FoaOm.js} +1 -1
- package/dist/assets/History-CXxxqNP1.js +2 -0
- package/dist/assets/{Hooks-CmK6l0uf.js → Hooks-CzeYFQjq.js} +3 -3
- package/dist/assets/{HostBilko-D8TIx-Bx.js → HostBilko-nESrTRIg.js} +1 -1
- package/dist/assets/{Library-DTxpOjx8.js → Library-D2zPkwOC.js} +1 -1
- package/dist/assets/{MarkdownEditor-BPJyrevq.js → MarkdownEditor-DbinP78A.js} +1 -1
- package/dist/assets/{McpServers-B_rvIYDs.js → McpServers-Bu_sVGLr.js} +2 -2
- package/dist/assets/Memory-BgiI6LZ_.js +8 -0
- package/dist/assets/{Permissions-DhMMp27n.js → Permissions-C1MMzZRm.js} +3 -3
- package/dist/assets/Plugins-Dhu6wAZi.js +2 -0
- package/dist/assets/{ProvenanceBadge-ptlItdCK.js → ProvenanceBadge-C3AcWKMG.js} +1 -1
- package/dist/assets/{SaveBar-CSbNSl2W.js → SaveBar-plwYm0iO.js} +1 -1
- package/dist/assets/Scheduler-BVYTeh39.js +16 -0
- package/dist/assets/{ScopeSwitcher-BJof-GWH.js → ScopeSwitcher-DFy0vv_o.js} +1 -1
- package/dist/assets/Settings-DIhGgdfs.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CGl4B89-.js → SkillReferenceGraph-DsI7hRPB.js} +1 -1
- package/dist/assets/Skills-3E748UfB.js +3 -0
- package/dist/assets/{SystemPrompt-DKj-q39J.js → SystemPrompt-BqmfUJOd.js} +1 -1
- package/dist/assets/TagLibrary-CPz6fO8Q.js +1 -0
- package/dist/assets/{TiptapBody-BbcJL9az.js → TiptapBody-D9EoBQ1P.js} +1 -1
- package/dist/assets/{Toggle-DAslvnnv.js → Toggle-BHAmZNDn.js} +1 -1
- package/dist/assets/index-tn5JIVPj.js +3079 -0
- package/dist/assets/{settingsSchema-pzp0TgvN.js → settingsSchema-Crqy_aNz.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -2
- package/plugins/CLAUDE.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +90 -67
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
- package/scripts/README.md +1 -0
- package/scripts/hooks/guard-destructive-git.cjs +9 -374
- package/scripts/hooks/guard-inline-implementation.cjs +9 -128
- package/scripts/hooks/guard-prd-writes.cjs +9 -74
- package/scripts/hooks/guard-self-schedule.cjs +9 -61
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
- package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
- package/scripts/scheduler-mcp-server.cjs +8 -1
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
- package/src/main/__tests__/exchanges.test.cjs +41 -96
- package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/health-credentials.test.cjs +81 -0
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +39 -0
- package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
- package/src/main/__tests__/planValidator.test.cjs +125 -0
- package/src/main/__tests__/prdCreate.test.cjs +3 -1
- package/src/main/__tests__/prdSizing.test.cjs +106 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
- package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
- package/src/main/__tests__/telemetryClient.test.cjs +2 -2
- package/src/main/__tests__/telemetryContract.test.cjs +2 -2
- package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
- package/src/main/__tests__/validationSentinels.test.cjs +84 -0
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +26 -14
- package/src/main/exchanges.cjs +26 -21
- package/src/main/health.cjs +66 -4
- package/src/main/index.cjs +1 -1
- package/src/main/ipcSchemas.cjs +15 -7
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
- package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
- package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
- package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
- package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
- package/src/main/lib/agentModelResolve.cjs +1 -0
- package/src/main/lib/credentials.cjs +62 -0
- package/src/main/lib/cwdClassify.cjs +15 -0
- package/src/main/lib/definitionOfDone.cjs +9 -13
- package/src/main/lib/effectiveModelInfo.cjs +2 -9
- package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
- package/src/main/lib/loadGate.cjs +29 -5
- package/src/main/lib/opsOwnership.cjs +23 -0
- package/src/main/lib/planValidator.cjs +80 -0
- package/src/main/lib/prdCreate.cjs +8 -4
- package/src/main/lib/prdSizing.cjs +83 -0
- package/src/main/lib/runLogRetention.cjs +2 -8
- package/src/main/lib/telemetryBacklog.cjs +9 -3
- package/src/main/lib/telemetryClient.cjs +12 -3
- package/src/main/lib/telemetrySettings.cjs +8 -2
- package/src/main/lib/validationSentinels.cjs +29 -0
- package/src/main/memoryAggregate.cjs +36 -11
- package/src/main/otelSettings.cjs +8 -2
- package/src/main/scheduler.cjs +215 -31
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/sessionsStore.cjs +8 -2
- package/src/main/supervisor.cjs +3 -3
- package/src/main/templates/PRD_AUTHORING.md +9 -9
- package/src/main/voiceSettings.cjs +8 -2
- package/src/preload/api.d.ts +3 -3
- package/src/preload/index.cjs +5 -5
- package/src/seed/agents/dev-lead.md +24 -15
- package/src/seed/agents/validator.md +33 -0
- package/dist/assets/History-BDyJUGtR.js +0 -2
- package/dist/assets/Memory-B-9GlHqe.js +0 -8
- package/dist/assets/Plugins-DKfAlmMY.js +0 -2
- package/dist/assets/Scheduler--rn1QBYc.js +0 -16
- package/dist/assets/Settings-Dsx6XZP5.js +0 -3
- package/dist/assets/Skills-D522Zfol.js +0 -3
- package/dist/assets/TagLibrary-BH7CG7-6.js +0 -1
- package/dist/assets/index-Yf8e7VCB.js +0 -3079
package/src/main/scheduler.cjs
CHANGED
|
@@ -80,6 +80,8 @@ const sessionsStore = require('./sessionsStore.cjs');
|
|
|
80
80
|
const { enqueueExternalPrompt } = require('./chatRunner.cjs');
|
|
81
81
|
const { appendResponseEventIfKnown } = require('./promptSessionEvents.cjs');
|
|
82
82
|
const { maybeEnqueueValidationPrompt } = require('./lib/epicValidationHook.cjs');
|
|
83
|
+
const { parseValidationSentinels } = require('./lib/validationSentinels.cjs');
|
|
84
|
+
const { hasDownstreamValidator } = require('./lib/planValidator.cjs');
|
|
83
85
|
const promptSessionTranscript = require('./promptSessionTranscript.cjs');
|
|
84
86
|
const { verifyRun, parseLog, scanSentinel, scanForeignWipPathsClaim } = require('./runVerify.cjs');
|
|
85
87
|
const { latestTerminalOutcomeForSlug, COMPLETED_EQUIVALENT_VERDICTS } = require('./lib/terminalRunOutcome.cjs');
|
|
@@ -274,7 +276,17 @@ const BASH_MAX_TIMEOUT_MS = 900_000; // 15 min — must stay below IDLE_OUTPUT_K
|
|
|
274
276
|
// centrally here (not per-PRD) so it applies to every current and future PRD.
|
|
275
277
|
// The commit step is also backstopped by the post-run commit guard below: a
|
|
276
278
|
// clean exit that leaves uncommitted changes is downgraded to needs_review.
|
|
277
|
-
|
|
279
|
+
//
|
|
280
|
+
// PRD 1408: a plan that ends in its own trailing `validator` job (PRD
|
|
281
|
+
// 1405/1407) already re-reviews every work-item's diff once, so asking EVERY
|
|
282
|
+
// work-item to also run /code-review + /security-review inline is duplicate
|
|
283
|
+
// review work — and the dominant cost tail on multi-file PRDs. buildFinishProtocol
|
|
284
|
+
// derives a `reviewInRun: false` variant that collapses those two steps into
|
|
285
|
+
// one deferral note and renumbers the rest, sharing every other word
|
|
286
|
+
// byte-for-byte with the default (`reviewInRun: true`, which is exactly
|
|
287
|
+
// FINISH_PROTOCOL) via the head/review-steps/tail pieces below.
|
|
288
|
+
function buildFinishHead(verifyStepNum) {
|
|
289
|
+
return `
|
|
278
290
|
|
|
279
291
|
---
|
|
280
292
|
# SCHEDULER FINISH PROTOCOL (mandatory — runs AFTER the work above)
|
|
@@ -283,7 +295,7 @@ Once every acceptance-criteria line above is satisfied, finish in this EXACT
|
|
|
283
295
|
sequence. Do not stop before the commit lands; committing is part of the job.
|
|
284
296
|
|
|
285
297
|
RUN VERIFICATION IN THE FOREGROUND — this applies to the whole run, not just
|
|
286
|
-
step
|
|
298
|
+
step ${verifyStepNum} below: every test/typecheck/lint/build command you run, whether while
|
|
287
299
|
implementing the AC or during VERIFY, must run SYNCHRONOUSLY and you must wait
|
|
288
300
|
for it to return. Never start a verification command as a background task
|
|
289
301
|
(no background Bash) and then call Monitor, TaskOutput, or ScheduleWakeup to
|
|
@@ -296,17 +308,26 @@ to fit inside that ceiling; if a gate command still cannot finish inside
|
|
|
296
308
|
budget, stop and emit SCHEDULER_VERDICT: FAIL with the reason instead of
|
|
297
309
|
deferring it.
|
|
298
310
|
|
|
299
|
-
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const FINISH_REVIEW_STEPS_IN_RUN = `1. CODE REVIEW — run \`/code-review --fix\` on your changes and apply the fixes it
|
|
300
315
|
surfaces (correctness first). For any finding you judge a false positive, say
|
|
301
316
|
why in your result; do not silently skip it. If \`/code-review\` is not
|
|
302
317
|
available in this environment, do an equivalent careful self-review instead.
|
|
303
318
|
2. SECURITY REVIEW — run \`/security-review\` and address every finding (or
|
|
304
319
|
justify it). If unavailable, self-review the diff for injection, secrets,
|
|
305
320
|
path traversal, and unsafe input handling.
|
|
306
|
-
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
const FINISH_REVIEW_STEP_DEFERRED = `1. REVIEW — code and security review for this plan run once, in its trailing validator job. Do NOT run /code-review or /security-review here.
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
function buildFinishTail({ verifyStepNum, commitStepNum, verdictStepNum }) {
|
|
327
|
+
return `${verifyStepNum}. VERIFY — run the project's OWN check commands (typecheck / lint / tests — the
|
|
307
328
|
project's CLAUDE.md names them; infer from the repo if not) and make them
|
|
308
329
|
pass. Do not assume npm; use whatever the target project uses.
|
|
309
|
-
|
|
330
|
+
${commitStepNum}. COMMIT — the queue can run several jobs against this SAME working tree at
|
|
310
331
|
once. Do NOT stage the whole working tree in one blanket/wildcard git-add
|
|
311
332
|
sweep — that captures whatever a concurrent sibling job is mid-writing and
|
|
312
333
|
mis-attributes its work to this commit, corrupting both jobs' verdicts.
|
|
@@ -314,12 +335,12 @@ deferring it.
|
|
|
314
335
|
commit: \`git add <path> [<path>...] && git commit -m "<type>(<scope>): <summary>"\`.
|
|
315
336
|
Your own work must still never be left uncommitted — this only changes
|
|
316
337
|
which paths get staged, never whether you commit.
|
|
317
|
-
|
|
338
|
+
${verdictStepNum}. VERDICT SENTINEL — as the LAST LINE of your final result text, emit exactly
|
|
318
339
|
one of these lines (no trailing text after it):
|
|
319
340
|
SCHEDULER_VERDICT: PASS
|
|
320
341
|
SCHEDULER_VERDICT: FAIL <one-line reason>
|
|
321
342
|
SCHEDULER_VERDICT: BLOCKED_BY_FOREIGN_WIP
|
|
322
|
-
Print PASS only when the AC gate is green AND the commit from step
|
|
343
|
+
Print PASS only when the AC gate is green AND the commit from step ${commitStepNum} landed.
|
|
323
344
|
Print FAIL (and exit 1) if the AC gate was red or the commit could not land.
|
|
324
345
|
NEVER print PASS on a red AC gate — a lying PASS turns the verifier from a
|
|
325
346
|
false-failure catcher into a silent-failure shipper. A truthful PASS + a
|
|
@@ -348,6 +369,30 @@ for review. Do NOT add work beyond the acceptance criteria — this protocol is
|
|
|
348
369
|
only post-AC work. If a review finding can't be fixed within scope, commit what
|
|
349
370
|
you have, describe the finding in the commit body, and note the follow-up in your
|
|
350
371
|
final result.`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* buildFinishProtocol({ reviewInRun }) → string
|
|
376
|
+
*
|
|
377
|
+
* `reviewInRun: true` (default) is byte-for-byte FINISH_PROTOCOL: steps 1-5
|
|
378
|
+
* are CODE REVIEW, SECURITY REVIEW, VERIFY, COMMIT, VERDICT SENTINEL.
|
|
379
|
+
* `reviewInRun: false` collapses the two review steps into one deferral note
|
|
380
|
+
* (step 1) and renumbers VERIFY/COMMIT/VERDICT SENTINEL to 2-4 — used when
|
|
381
|
+
* `hasDownstreamValidator` (lib/planValidator.cjs) finds this job's diff will
|
|
382
|
+
* be re-reviewed once by the plan's own trailing validator job, so asking
|
|
383
|
+
* every work-item to also run /code-review + /security-review inline is
|
|
384
|
+
* redundant review work.
|
|
385
|
+
*/
|
|
386
|
+
function buildFinishProtocol({ reviewInRun = true } = {}) {
|
|
387
|
+
if (reviewInRun) {
|
|
388
|
+
return buildFinishHead(3) + FINISH_REVIEW_STEPS_IN_RUN
|
|
389
|
+
+ buildFinishTail({ verifyStepNum: 3, commitStepNum: 4, verdictStepNum: 5 });
|
|
390
|
+
}
|
|
391
|
+
return buildFinishHead(2) + FINISH_REVIEW_STEP_DEFERRED
|
|
392
|
+
+ buildFinishTail({ verifyStepNum: 2, commitStepNum: 3, verdictStepNum: 4 });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const FINISH_PROTOCOL = buildFinishProtocol({ reviewInRun: true });
|
|
351
396
|
|
|
352
397
|
// Unquote a single git porcelain v1 path token. Defined once in
|
|
353
398
|
// gitWorktree.cjs (which this file already requires — the reverse would be
|
|
@@ -1513,9 +1558,19 @@ function loadSchedulerState() {
|
|
|
1513
1558
|
if (typeof s.failureStreakWarnedAt === 'number') failureStreakWarnedAt = s.failureStreakWarnedAt;
|
|
1514
1559
|
failureStreakWarnedAt = restoreFailureStreakWarnedAt(failureStreakWarned, failureStreakWarnedAt, Date.now());
|
|
1515
1560
|
if (typeof s.lastEscalationAt === 'number') lastEscalationAtMs = s.lastEscalationAt;
|
|
1561
|
+
if (typeof s.lastPollOk === 'boolean') lastPollOk = s.lastPollOk;
|
|
1562
|
+
if (typeof s.lastFailureKind === 'string' || s.lastFailureKind === null) lastFailureKind = s.lastFailureKind;
|
|
1563
|
+
if (typeof s.backoffNextAt === 'number' || s.backoffNextAt === null) backoffNextAt = s.backoffNextAt;
|
|
1564
|
+
if (typeof s.cachedUtilization === 'number' || s.cachedUtilization === null) cachedUtilization = s.cachedUtilization;
|
|
1565
|
+
if (typeof s.cachedBindingWindowName === 'string' || s.cachedBindingWindowName === null) cachedBindingWindowName = s.cachedBindingWindowName;
|
|
1516
1566
|
} catch { /* first boot or corrupt — start fresh */ }
|
|
1517
1567
|
}
|
|
1518
1568
|
|
|
1569
|
+
/** Test-only seam: the module-level fields loadSchedulerState() restores that have no other exported reader. */
|
|
1570
|
+
function getSchedulerStateSnapshot() {
|
|
1571
|
+
return { lastPollOk, lastFailureKind, backoffNextAt, cachedUtilization, cachedBindingWindowName };
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1519
1574
|
function persistSchedulerState() {
|
|
1520
1575
|
// Sync write: called from many sync hot paths (clearPause, pollLoop catch
|
|
1521
1576
|
// block) and the sidecar is tiny (<1 KB). Converting to async here would
|
|
@@ -1544,6 +1599,14 @@ function persistSchedulerState() {
|
|
|
1544
1599
|
// THESE persisted values, since it never holds the in-memory circuit.
|
|
1545
1600
|
usageCircuitState: billing.usageCircuit.state(),
|
|
1546
1601
|
usageCircuitOpenedAt: billing.usageCircuit.openedAt(),
|
|
1602
|
+
// Footer/KPI display fields — previously memory-only, so a restart showed
|
|
1603
|
+
// misleading placeholders (poll-failed, no rate-limit banner, "—" utilization)
|
|
1604
|
+
// until the next post-boot billing poll overwrote them.
|
|
1605
|
+
lastPollOk,
|
|
1606
|
+
lastFailureKind,
|
|
1607
|
+
backoffNextAt,
|
|
1608
|
+
cachedUtilization,
|
|
1609
|
+
cachedBindingWindowName,
|
|
1547
1610
|
});
|
|
1548
1611
|
} catch (e) {
|
|
1549
1612
|
console.warn('[scheduler] failed to persist scheduler state', e?.message);
|
|
@@ -4280,6 +4343,11 @@ async function notifyOriginatingTab(job, {
|
|
|
4280
4343
|
appendTranscriptTurn = promptSessionTranscript.appendTurn,
|
|
4281
4344
|
readResultFromLog = extractResultTextFromLog,
|
|
4282
4345
|
enqueueValidation = maybeEnqueueValidationPrompt,
|
|
4346
|
+
// PRD 1408: same queue rows spawnJob/tickQueue already use — read fresh
|
|
4347
|
+
// here (not threaded through the terminal-transition snapshot) since this
|
|
4348
|
+
// fires well after dispatch, when the plan may have gained/dropped a
|
|
4349
|
+
// trailing validator row since this job was spawned.
|
|
4350
|
+
loadJobs = async () => (await readQueue()).jobs || [],
|
|
4283
4351
|
} = {}) {
|
|
4284
4352
|
try {
|
|
4285
4353
|
const prd = await resolveNotifyPrd(job, parsePrdRaw);
|
|
@@ -4311,10 +4379,25 @@ async function notifyOriginatingTab(job, {
|
|
|
4311
4379
|
// fallback). Best-effort: a missing cwd/epic id, an unreadable run log,
|
|
4312
4380
|
// or an IPC error here must never block the notification below.
|
|
4313
4381
|
const epicIdForTranscript = prd?.sourcePromptId || prd?.sourceTabId || job.epicId || null;
|
|
4314
|
-
|
|
4382
|
+
// Read the job's raw result text once, up front, so both the transcript
|
|
4383
|
+
// append below and the validator-sentinel parsing further down (PRD
|
|
4384
|
+
// 1407) reuse the same read instead of hitting the run log twice. Only
|
|
4385
|
+
// read when at least one consumer needs it — an ordinary job with no
|
|
4386
|
+
// resolvable transcript target and no validator sentinels to parse must
|
|
4387
|
+
// not pay for a log-tail read it will throw away.
|
|
4388
|
+
const needsResultText = (epicIdForTranscript && job.cwd)
|
|
4389
|
+
|| (job.agentType === 'validator' && job.status === 'completed');
|
|
4390
|
+
let resultText = null;
|
|
4391
|
+
if (needsResultText) {
|
|
4315
4392
|
try {
|
|
4316
4393
|
const logPath = job.runId ? path.join(schedulerPaths.runsDir(), job.runId, `${job.slug}.log`) : null;
|
|
4317
|
-
|
|
4394
|
+
resultText = readResultFromLog(logPath);
|
|
4395
|
+
} catch (e) {
|
|
4396
|
+
console.error('[scheduler] notifyOriginatingTab readResultFromLog error', job?.slug, e);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
if (epicIdForTranscript && job.cwd) {
|
|
4400
|
+
try {
|
|
4318
4401
|
await appendTranscriptTurn(job.cwd, epicIdForTranscript, {
|
|
4319
4402
|
role: 'assistant',
|
|
4320
4403
|
text: resultText || message,
|
|
@@ -4325,7 +4408,43 @@ async function notifyOriginatingTab(job, {
|
|
|
4325
4408
|
}
|
|
4326
4409
|
}
|
|
4327
4410
|
|
|
4411
|
+
// PRD 1407: a plan-level validator job ends its run with one
|
|
4412
|
+
// `VALIDATION: <slug> VERIFIED`/`REFUTED` line per PRD it checked.
|
|
4413
|
+
// Parsed only for a completed validator job; every ordinary work-item
|
|
4414
|
+
// job gets an empty array here and behaves exactly as before.
|
|
4415
|
+
const verdicts = (job.agentType === 'validator' && job.status === 'completed')
|
|
4416
|
+
? parseValidationSentinels(resultText)
|
|
4417
|
+
: [];
|
|
4418
|
+
|
|
4419
|
+
if (!epicId && verdicts.length > 0) {
|
|
4420
|
+
// No Epic to attach these to (unresolved sourcePromptId/epicId, same
|
|
4421
|
+
// rare case the check-in message below falls back on) — unlike the
|
|
4422
|
+
// check-in message, a verdict has no chat-notification fallback, so
|
|
4423
|
+
// without this log the parsed evidence vanishes with no trace at all.
|
|
4424
|
+
console.error(
|
|
4425
|
+
`[scheduler] notifyOriginatingTab: ${verdicts.length} validator verdict(s) parsed for ${job.slug} but no epicId to attach them to — dropped`,
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4328
4429
|
if (epicId) {
|
|
4430
|
+
// PRD 1407: append one response event per parsed verdict, BEFORE the
|
|
4431
|
+
// validator job's own check-in append below — each verdict stamps
|
|
4432
|
+
// `validation: 'verified' | 'refuted'` onto the authoring Epic's
|
|
4433
|
+
// event chain, which epicDerive.ts/epicTimeline.ts already render as
|
|
4434
|
+
// the Epic traffic light and which epicValidationHook.cjs's loop
|
|
4435
|
+
// guard already ignores (only 'unvalidated' can trigger a prompt).
|
|
4436
|
+
for (const v of verdicts) {
|
|
4437
|
+
try {
|
|
4438
|
+
await appendResponseEvent(
|
|
4439
|
+
job.cwd || null,
|
|
4440
|
+
epicId,
|
|
4441
|
+
`Validation ${v.verdict.toUpperCase()}: ${v.slug}${v.reason ? ` — ${v.reason}` : ''}`,
|
|
4442
|
+
{ prdSlug: v.slug, outcome: 'completed', validation: v.verdict },
|
|
4443
|
+
);
|
|
4444
|
+
} catch (e) {
|
|
4445
|
+
console.error('[scheduler] notifyOriginatingTab verdict appendResponseEvent error', job?.slug, v.slug, e);
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4329
4448
|
// PRD 986: the check-in event is born validation:'unvalidated' — never
|
|
4330
4449
|
// 'verified' — regardless of the job's self-reported outcome. The
|
|
4331
4450
|
// check-in is a request to validate, not an assertion of done.
|
|
@@ -4348,20 +4467,40 @@ async function notifyOriginatingTab(job, {
|
|
|
4348
4467
|
// happened above). All gating (kill-switch SM_EPIC_VALIDATION_DISABLE,
|
|
4349
4468
|
// active-Epic check, once-per-(epicId, prdSlug), loop guard, slot-pool
|
|
4350
4469
|
// routing) lives in lib/epicValidationHook.cjs.
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4470
|
+
//
|
|
4471
|
+
// PRD 1407: a validator job's own check-in never enqueues a
|
|
4472
|
+
// validation prompt for itself — it IS the plan's validation pass,
|
|
4473
|
+
// not a work-item awaiting one.
|
|
4474
|
+
//
|
|
4475
|
+
// PRD 1408: nor does an ordinary work-item whose plan already ends in
|
|
4476
|
+
// a trailing validator job depending on it — that validator's own
|
|
4477
|
+
// check-in (above) is the single validation pass for the whole plan,
|
|
4478
|
+
// so a per-PRD prompt here would be a second, redundant ask into the
|
|
4479
|
+
// same (Opus/Fable-tier) authoring session.
|
|
4480
|
+
if (job.agentType !== 'validator') {
|
|
4481
|
+
let skipForDownstreamValidator = false;
|
|
4482
|
+
try {
|
|
4483
|
+
skipForDownstreamValidator = hasDownstreamValidator(job, await loadJobs());
|
|
4484
|
+
} catch (e) {
|
|
4485
|
+
console.error('[scheduler] notifyOriginatingTab loadJobs error', job?.slug, e);
|
|
4486
|
+
}
|
|
4487
|
+
if (!skipForDownstreamValidator) {
|
|
4488
|
+
try {
|
|
4489
|
+
enqueueValidation(
|
|
4490
|
+
{
|
|
4491
|
+
cwd: job.cwd || null,
|
|
4492
|
+
epicId,
|
|
4493
|
+
prdSlug: job.slug,
|
|
4494
|
+
prdPath: prd?.path || archivedPrdPathForJob(job) || null,
|
|
4495
|
+
outcome: job.status,
|
|
4496
|
+
eventValidation: 'unvalidated',
|
|
4497
|
+
},
|
|
4498
|
+
{ sendPrompt },
|
|
4499
|
+
);
|
|
4500
|
+
} catch (e) {
|
|
4501
|
+
console.error('[scheduler] notifyOriginatingTab enqueueValidation error', job?.slug, e);
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4365
4504
|
}
|
|
4366
4505
|
return;
|
|
4367
4506
|
}
|
|
@@ -5337,7 +5476,7 @@ function pickRunDir() {
|
|
|
5337
5476
|
* Watchdogs are declared as an array; the result-tailer's exit-code mapping
|
|
5338
5477
|
* (success+killedBySignal → 0) is scheduler-specific and lives in onExit.
|
|
5339
5478
|
*/
|
|
5340
|
-
async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null) {
|
|
5479
|
+
async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null, reviewInRun = true) {
|
|
5341
5480
|
// First statement — stamps the 'exec-entered' dispatch-phase breadcrumb
|
|
5342
5481
|
// before openLog below, so a hang inside openLog/spawn itself still shows
|
|
5343
5482
|
// execution reached this function (see spawnJob's dispatchPhase comment).
|
|
@@ -5523,7 +5662,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
5523
5662
|
// Always route through composeExecutorPrompt (even with an empty digest)
|
|
5524
5663
|
// so the finish protocol is appended in the prompt's tail exactly once,
|
|
5525
5664
|
// after any digest fence rather than concatenated ahead of it.
|
|
5526
|
-
prompt = composeExecutorPrompt({ prdBody: prompt, digestText, finishProtocol:
|
|
5665
|
+
prompt = composeExecutorPrompt({ prdBody: prompt, digestText, finishProtocol: buildFinishProtocol({ reviewInRun }) });
|
|
5527
5666
|
|
|
5528
5667
|
// Foreign-WIP manifest (starry-night-ships PRD 148 postmortem): the
|
|
5529
5668
|
// scheduler already knows, at spawn time, which dirty paths this job did
|
|
@@ -6744,7 +6883,15 @@ function guardedTick(fn, label) {
|
|
|
6744
6883
|
}
|
|
6745
6884
|
}
|
|
6746
6885
|
|
|
6747
|
-
async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
6886
|
+
async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, siblingJobs = null) {
|
|
6887
|
+
// PRD 1408: the queue rows in scope at dispatch (tickQueue's own
|
|
6888
|
+
// state.jobs, threaded straight down — never re-read here or inside
|
|
6889
|
+
// executeJob) decide whether this job's own finish protocol still needs to
|
|
6890
|
+
// run /code-review + /security-review inline, or can defer to a downstream
|
|
6891
|
+
// `validator` job that will re-review this diff once the plan finishes.
|
|
6892
|
+
// A caller with no rows in scope (spawnResumeRecovery) reads them itself,
|
|
6893
|
+
// once, before delegating to spawnJob — still never inside executeJob.
|
|
6894
|
+
const reviewInRun = !hasDownstreamValidator(job, siblingJobs || []);
|
|
6748
6895
|
// Session-Manager owns the machine-wide `claude -p` pool (sessionSlots.cjs)
|
|
6749
6896
|
// — the scheduler REQUESTS capacity, it doesn't own a private cap. A miss
|
|
6750
6897
|
// leaves the job pending; the next tick retries when a slot frees up.
|
|
@@ -7116,7 +7263,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7116
7263
|
s.jobs[idx].dispatchPhaseAt = new Date().toISOString();
|
|
7117
7264
|
}
|
|
7118
7265
|
});
|
|
7119
|
-
});
|
|
7266
|
+
}, reviewInRun);
|
|
7120
7267
|
} finally {
|
|
7121
7268
|
if (worktree.ok) {
|
|
7122
7269
|
({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths } =
|
|
@@ -7506,10 +7653,17 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7506
7653
|
let mechanicalRecoveryTarget = null;
|
|
7507
7654
|
let terminalNotifySnapshot = null;
|
|
7508
7655
|
const newlyCompletedPrds = [];
|
|
7656
|
+
// PRD 1408: captured inside the mutate() below (the one in-memory read of
|
|
7657
|
+
// `s.jobs` this finalize pass already does) so notifyOriginatingTab's own
|
|
7658
|
+
// hasDownstreamValidator check can reuse it instead of a second, redundant
|
|
7659
|
+
// readQueue() — that call federates queue.json shards across every known
|
|
7660
|
+
// project cwd on the machine, not just this job's own project.
|
|
7661
|
+
let jobsSnapshotForNotify = null;
|
|
7509
7662
|
// Base HEAD for the mechanical-recovery futility check — read here, never inside mutate() (no I/O).
|
|
7510
7663
|
const finalizeHeadSha = worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
|
|
7511
7664
|
? await gitHead(guardCwd) : null;
|
|
7512
7665
|
await mutate((s) => {
|
|
7666
|
+
jobsSnapshotForNotify = s.jobs.slice();
|
|
7513
7667
|
const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
7514
7668
|
// A job already moved off 'running' by someone else (namely
|
|
7515
7669
|
// remote.cancelJob, PRD 1024 — it SIGTERMs the process then finalizes
|
|
@@ -7923,7 +8077,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
|
|
|
7923
8077
|
});
|
|
7924
8078
|
|
|
7925
8079
|
if (terminalNotifySnapshot) {
|
|
7926
|
-
notifyOriginatingTab(terminalNotifySnapshot
|
|
8080
|
+
notifyOriginatingTab(terminalNotifySnapshot, {
|
|
8081
|
+
loadJobs: async () => jobsSnapshotForNotify || [],
|
|
8082
|
+
}).catch((e) => {
|
|
7927
8083
|
console.error('[scheduler] notifyOriginatingTab error', job.slug, e);
|
|
7928
8084
|
});
|
|
7929
8085
|
}
|
|
@@ -8151,7 +8307,11 @@ async function reclaimTerminalJobOrphansThrottled(state) {
|
|
|
8151
8307
|
*/
|
|
8152
8308
|
async function spawnResumeRecovery(job, resumeTarget) {
|
|
8153
8309
|
const { runId, dir: runDir } = pickRunDir();
|
|
8154
|
-
|
|
8310
|
+
// No queue rows in scope here (unlike tickQueue's own dispatch loop) — read
|
|
8311
|
+
// once, before delegating to spawnJob, so hasDownstreamValidator still
|
|
8312
|
+
// never runs inside executeJob itself.
|
|
8313
|
+
const siblingJobs = (await readQueue().catch(() => ({ jobs: [] }))).jobs || [];
|
|
8314
|
+
await spawnJob(job, runId, runDir, job.cwd || DEFAULT_PROJECT_CWD, resumeTarget, siblingJobs);
|
|
8155
8315
|
}
|
|
8156
8316
|
|
|
8157
8317
|
// Serialized ticker: prevents two concurrent tickQueue() calls from racing
|
|
@@ -8411,7 +8571,10 @@ async function tickBody(gen, { bypassLoadGate }) {
|
|
|
8411
8571
|
for (const job of gatedBatch) {
|
|
8412
8572
|
if (cancelToken.cancelled || stale()) break;
|
|
8413
8573
|
// spawnJob is fire-and-forget; it calls tickQueue() on completion.
|
|
8414
|
-
|
|
8574
|
+
// state.jobs is the queue's rows in scope at THIS dispatch (PRD 1408) —
|
|
8575
|
+
// threaded straight through so spawnJob's hasDownstreamValidator check
|
|
8576
|
+
// never re-reads the queue itself.
|
|
8577
|
+
spawnJob(job, runId, runDir, state.config.defaultCwd, null, state.jobs).catch((e) => reportSchedulerError('spawnJob dispatch rejected', job.slug, e));
|
|
8415
8578
|
}
|
|
8416
8579
|
return recordTick({ fired: true, count: gatedBatch.length, group: gatedBatch[0]?.parallelGroup }, { holds });
|
|
8417
8580
|
}
|
|
@@ -8854,7 +9017,24 @@ async function runQueueStarvationWatchdog(state, {
|
|
|
8854
9017
|
// A forced tick is machine-wide by construction (the picker considers
|
|
8855
9018
|
// every project's rows) — one call here services every starved cwd found
|
|
8856
9019
|
// this pass, not one call per cwd.
|
|
8857
|
-
|
|
9020
|
+
//
|
|
9021
|
+
// bypassLoadGate: TRUE. The watchdog only reaches here on a 'starved'
|
|
9022
|
+
// verdict: pending + dispatchable rows, ZERO running, idle >= 10 min. That
|
|
9023
|
+
// is exactly the state where the CPU load gate (scheduler.cjs's INNERMOST
|
|
9024
|
+
// launch predicate) is a false positive — a box whose loadavg is high while
|
|
9025
|
+
// *this* scheduler runs nothing is being loaded by something else, and the
|
|
9026
|
+
// load gate would then hold the last-resort tick behind the very saturation
|
|
9027
|
+
// it can't influence, freezing the queue for hours (observed 2026-09-23:
|
|
9028
|
+
// 3 dispatchable, 0 running, lastTickReason 'load-deferred' unbroken from
|
|
9029
|
+
// 07:00→13:06). The gate is advisory headroom ("leave one core free"), not
|
|
9030
|
+
// a resource guard; starting ONE batch cannot OOM the host. The slot pool
|
|
9031
|
+
// and memory gate — which DO guard real exhaustion — are unaffected by this
|
|
9032
|
+
// flag and still bound the batch. The watchdog is the automated equivalent
|
|
9033
|
+
// of a human Run-now, the other caller that legitimately bypasses load
|
|
9034
|
+
// (§2/§3, scheduler-operations.md). Latched to one forced tick per
|
|
9035
|
+
// QUEUE_STARVATION_MS per cwd, so this cannot thrash. Routed via
|
|
9036
|
+
// module.exports so tests can assert the bypass without driving real load.
|
|
9037
|
+
await module.exports.tickQueue({ bypassLoadGate: true }).catch((e) => console.error('[scheduler] starvation tick error', e));
|
|
8858
9038
|
return primary;
|
|
8859
9039
|
}
|
|
8860
9040
|
|
|
@@ -12690,6 +12870,9 @@ module.exports = {
|
|
|
12690
12870
|
shouldEscalateFailureStreak,
|
|
12691
12871
|
restoreFailureStreakWarnedAt,
|
|
12692
12872
|
persistedStreakMinutes,
|
|
12873
|
+
loadSchedulerState,
|
|
12874
|
+
persistSchedulerState,
|
|
12875
|
+
getSchedulerStateSnapshot,
|
|
12693
12876
|
computeDegradedBudget,
|
|
12694
12877
|
healRefusalReason,
|
|
12695
12878
|
writeQueue,
|
|
@@ -12701,6 +12884,7 @@ module.exports = {
|
|
|
12701
12884
|
parsePorcelain,
|
|
12702
12885
|
parsePorcelainEntries,
|
|
12703
12886
|
FINISH_PROTOCOL,
|
|
12887
|
+
buildFinishProtocol,
|
|
12704
12888
|
IDLE_OUTPUT_KILL_MS,
|
|
12705
12889
|
BASH_DEFAULT_TIMEOUT_MS,
|
|
12706
12890
|
BASH_MAX_TIMEOUT_MS,
|
|
@@ -39,7 +39,7 @@ const path = require('node:path');
|
|
|
39
39
|
const os = require('node:os');
|
|
40
40
|
const { writeJsonSync } = require('./config.cjs');
|
|
41
41
|
|
|
42
|
-
const PERSONAS = ['architect', 'dev-lead', 'project-home-builder'];
|
|
42
|
+
const PERSONAS = ['architect', 'dev-lead', 'project-home-builder', 'validator'];
|
|
43
43
|
// What `{ done: true }` (the pre-seeded-set marker format) meant: only these two personas existed
|
|
44
44
|
// in PERSONAS at the time. Fixed, not derived from the current PERSONAS array above — otherwise
|
|
45
45
|
// every future persona added to PERSONAS would retroactively count as already delivered to a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SessionsStore — persists the renderer's tab list to disk so the app can
|
|
3
3
|
* restore tabs + resume claude sessions after an electron restart.
|
|
4
4
|
*
|
|
5
|
-
* Storage: ~/.
|
|
5
|
+
* Storage: ~/.claude/session-manager/tabs.json
|
|
6
6
|
* Shape: { tabs: PersistedTab[], activeTabId: string | null, savedAt: number }
|
|
7
7
|
*
|
|
8
8
|
* Only serializable, durable fields are persisted: id, sessionId, cwd,
|
|
@@ -17,8 +17,14 @@ const os = require('node:os');
|
|
|
17
17
|
const { ipcMain } = require('electron');
|
|
18
18
|
const config = require('./config.cjs');
|
|
19
19
|
|
|
20
|
+
let migrated = false;
|
|
20
21
|
function storePath() {
|
|
21
|
-
|
|
22
|
+
const p = path.join(os.homedir(), '.claude', 'session-manager', 'tabs.json');
|
|
23
|
+
if (!migrated) {
|
|
24
|
+
migrated = true;
|
|
25
|
+
config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'tabs.json'), p);
|
|
26
|
+
}
|
|
27
|
+
return p;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
async function load() {
|
package/src/main/supervisor.cjs
CHANGED
|
@@ -18,11 +18,11 @@ const { spawn, execFileSync } = require('node:child_process');
|
|
|
18
18
|
const { ipcMain } = require('electron');
|
|
19
19
|
const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
|
|
20
20
|
const { withProcRole } = require('./lib/cleanEnv.cjs');
|
|
21
|
+
const schedulerPaths = require('./lib/schedulerPaths.cjs');
|
|
21
22
|
|
|
22
23
|
const HOME = os.homedir();
|
|
23
24
|
const SUPERVISOR_LOG_PATH = path.join(HOME, '.claude', 'session-manager', 'supervisor.log');
|
|
24
25
|
const SUPERVISOR_LOG_MAX_BYTES = 1024 * 1024;
|
|
25
|
-
const RUNS_DIR = path.join(HOME, '.claude', 'session-manager', 'scheduled-plans', 'runs');
|
|
26
26
|
|
|
27
27
|
// In-flight probe slugs — prevents duplicate probes across ticks.
|
|
28
28
|
const inFlightProbes = new Set();
|
|
@@ -371,7 +371,7 @@ async function probeJob(job, lastActivityAge) {
|
|
|
371
371
|
const startedAt = job.startedAt || new Date().toISOString();
|
|
372
372
|
const ageMinutes = Math.floor((Date.now() - Date.parse(startedAt)) / 60_000);
|
|
373
373
|
const runId = job.runtime?.runId;
|
|
374
|
-
const logPath = runId ? path.join(
|
|
374
|
+
const logPath = runId ? path.join(schedulerPaths.runsDir(), runId, `${slug}.log`) : null;
|
|
375
375
|
|
|
376
376
|
const pstreeOutput = getPstree(jobPid);
|
|
377
377
|
const childBashCmdlines = getChildBashCmdlines(jobPid);
|
|
@@ -426,7 +426,7 @@ async function supervisorTick() {
|
|
|
426
426
|
for (const job of runningJobs) {
|
|
427
427
|
if (inFlightProbes.has(job.slug)) continue;
|
|
428
428
|
const runId = job.runtime?.runId;
|
|
429
|
-
const logPath = runId ? path.join(
|
|
429
|
+
const logPath = runId ? path.join(schedulerPaths.runsDir(), runId, `${job.slug}.log`) : null;
|
|
430
430
|
if (!logPath) continue;
|
|
431
431
|
const lastActivity = getLastActivityTs(logPath);
|
|
432
432
|
const ageMs = Date.now() - lastActivity;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- PRD_AUTHORING.md
|
|
1
|
+
<!-- PRD_AUTHORING.md v3 -->
|
|
2
2
|
# PRD Authoring Guide — Scheduler Safety Rules
|
|
3
3
|
|
|
4
4
|
This guide codifies lessons from two stuck-job incidents (fizzpop poll-hang, etch-engine post-AC overrun) into enforceable rules every PRD MUST follow. Violating these rules costs real money and wastes hours waiting at a terminal.
|
|
@@ -122,14 +122,14 @@ curl -sf https://bilko.run/projects/<slug>/ > /dev/null || { echo "SMOKE TEST FA
|
|
|
122
122
|
|
|
123
123
|
## §6 Frontmatter rules
|
|
124
124
|
|
|
125
|
-
**Summary:** Required keys are `title`, `cwd` (absolute path), `estimateMinutes`. Default to letting the filename `NN-` prefix drive grouping
|
|
125
|
+
**Summary:** Required keys are `title`, `cwd` (absolute path), `estimateMinutes`. Default to letting the filename `NN-` prefix drive grouping.
|
|
126
126
|
|
|
127
127
|
**Required frontmatter:**
|
|
128
128
|
```yaml
|
|
129
129
|
---
|
|
130
130
|
title: <one line, plain English>
|
|
131
131
|
cwd: ~/Projects/<target-repo>
|
|
132
|
-
estimateMinutes:
|
|
132
|
+
estimateMinutes: 10
|
|
133
133
|
---
|
|
134
134
|
```
|
|
135
135
|
|
|
@@ -139,7 +139,7 @@ estimateMinutes: 60
|
|
|
139
139
|
- `cwd` MUST point to the target project. Prefer `~/...` for portability; only use an absolute path if you have a specific reason to pin to one machine.
|
|
140
140
|
- **`cwd` MUST already exist on disk at queue time.** The scheduler runs a dead-cwd guard (`fs.accessSync(cwd, fs.constants.X_OK)` in `src/main/scheduler.cjs:669-680`) *before* spawning the child, so a PRD whose `cwd` references a not-yet-created directory will exit with `-1: cwd no longer exists` and the body will never run — even if the first step of the body would have created the directory. If the PRD's purpose is to create a brand-new sibling project at `~/Projects/<new-slug>/`, point `cwd` at the parent (`~/Projects`) and make the first executable step `mkdir -p ~/Projects/<new-slug> && cd ~/Projects/<new-slug>`.
|
|
141
141
|
- `estimateMinutes` is used for ETA display; include a realistic estimate (note: empirical median is ~10 min, p90 ~20 min — avoid wildly inflated estimates that hide real outliers).
|
|
142
|
-
- `parallelGroup`
|
|
142
|
+
- `parallelGroup` is DEPRECATED and ignored (PRD 832) — `dependsOn: [<slug>, …]` is the only ordering primitive; numbers are unique per project.
|
|
143
143
|
|
|
144
144
|
### `planId` (API-owned — never pass it)
|
|
145
145
|
|
|
@@ -168,11 +168,11 @@ Incident: sigma PRD `816-prepare-157-copy-citation-extras-patch` (2026-09-20) wr
|
|
|
168
168
|
|
|
169
169
|
---
|
|
170
170
|
|
|
171
|
-
## §8 Scope sizing — target
|
|
171
|
+
## §8 Scope sizing — target ≤10 min, ceiling 15 (data-driven, 2026-09)
|
|
172
172
|
|
|
173
|
-
**Summary:** One PRD ≈
|
|
173
|
+
**Summary:** One PRD ≈ **≤10 wall-clock minutes** of work. Empirically (2026-09 calibration) wall p50 = **7.8 min**, 60% of runs ≤ 10 min; authored estimates ran 4× too high. **If you project >15 min, SPLIT.**
|
|
174
174
|
|
|
175
|
-
**Rule:** Split larger work into sequential PRDs; reference the dependency in `# Implementation notes`.
|
|
175
|
+
**Rule:** Split larger work into sequential PRDs; reference the dependency in `# Implementation notes`. e2e/publish work is the failure tail: **shard test suites to one spec per PRD; never run a full suite or an endpoint-polling publish in a single PRD** (§1/§5).
|
|
176
176
|
|
|
177
177
|
---
|
|
178
178
|
|
|
@@ -202,9 +202,9 @@ Before queueing a new PRD, verify each of these:
|
|
|
202
202
|
- [ ] **§3 Smoke tests + verify-before-done:** Every deploy/migration step is followed by a test command that exits 1 on failure. Run the AC test command once before declaring done; never end the run on a red test.
|
|
203
203
|
- [ ] **§4 Bounded generators:** Any search/seed loop has an explicit `MAX_ATTEMPTS` constant and surfaces failure on exhaustion.
|
|
204
204
|
- [ ] **§5 Render deploys:** Deploy waits use a live URL check, not uptime/restart signals.
|
|
205
|
-
- [ ] **§6 Frontmatter:** `title`, `cwd` (`~/Projects/<name>` preferred; path MUST exist on this machine), `estimateMinutes` present. `parallelGroup`
|
|
205
|
+
- [ ] **§6 Frontmatter:** `title`, `cwd` (`~/Projects/<name>` preferred; path MUST exist on this machine), `estimateMinutes` present. `parallelGroup` is deprecated — use `dependsOn` for ordering.
|
|
206
206
|
- [ ] **§7 Self-contained:** No references to "the conversation" or external context. Paths and identifiers are inline. Body is clean UTF-8 — **no NUL/control bytes** (paste-from-PDF crashes the spawn). Quick check: `grep -qP '\x00' file && echo BAD`.
|
|
207
|
-
- [ ] **§8 Scope:** Targets
|
|
207
|
+
- [ ] **§8 Scope:** Targets ≤10 min, ceiling 15. If projected larger, split. e2e/publish sharded to one spec per PRD.
|
|
208
208
|
- [ ] **§9 Failure surfacing:** Errors exit 1 with a diagnostic line. No silent `|| true` swallows. (`rateLimited` exit-1 is benign auto-pause, not a failure.)
|
|
209
209
|
- [ ] **§11 Negative-assertion checks:** Any "this should produce NO output / NO match" check (a `grep` that should find nothing, a "no leftover X" guard) is written as an inverted conditional that exits 0 on the clean case. A bare `grep` whose success is "no match" exits 1 and trips the verifier `transcript_errors` downgrade even when the run is perfect.
|
|
210
210
|
- [ ] **§12 End green:** The acceptance/test gate is the LAST thing the run does; any intentionally-failing step (TDD red test, expected-nonzero probe) runs EARLY, never after the gate, and is captured (`2>&1 | tail` inside a conditional) so it doesn't surface as a bare `is_error`/`Traceback` in the final portion of the transcript.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VoiceSettings — persists voice hotkey configuration to disk.
|
|
3
3
|
*
|
|
4
|
-
* Storage: ~/.
|
|
4
|
+
* Storage: ~/.claude/session-manager/voice.json
|
|
5
5
|
* Shape: {
|
|
6
6
|
* accelerator: string, mode: 'hold'|'toggle', global: boolean, schemaVersion: 1,
|
|
7
7
|
* // F5 device picker (optional, additive):
|
|
@@ -44,8 +44,14 @@ function defaultsForPlatform() {
|
|
|
44
44
|
return { accelerator: 'Ctrl+Shift+Space', mode: 'hold', global: false, schemaVersion: SCHEMA_VERSION };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
let migrated = false;
|
|
47
48
|
function storePath() {
|
|
48
|
-
|
|
49
|
+
const p = path.join(os.homedir(), '.claude', 'session-manager', 'voice.json');
|
|
50
|
+
if (!migrated) {
|
|
51
|
+
migrated = true;
|
|
52
|
+
config.migrateLegacyHomeFile(path.join(os.homedir(), '.config', 'session-manager', 'voice.json'), p);
|
|
53
|
+
}
|
|
54
|
+
return p;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// PRD F1 v2 §Chord schema. Anchored, requires ≥1 modifier so a bare
|
package/src/preload/api.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface ImportRef {
|
|
|
66
66
|
/** Declared owner ids for the single-writer law over a project's
|
|
67
67
|
* session-manager-operations/ root. Mirrors OWNERS in
|
|
68
68
|
* src/main/lib/opsOwnership.cjs — one writer per namespace; everyone reads. */
|
|
69
|
-
export type OpsWriter = 'epics' | 'scheduler' | 'project-home';
|
|
69
|
+
export type OpsWriter = 'epics' | 'scheduler' | 'project-home' | 'ui-prefs';
|
|
70
70
|
|
|
71
71
|
export interface WriteResult {
|
|
72
72
|
ok: boolean;
|
|
@@ -340,7 +340,7 @@ export interface VoiceMarkWizardCompleteResult {
|
|
|
340
340
|
/**
|
|
341
341
|
* F8 — semantic turn-detection settings. MVP shape only; the actual
|
|
342
342
|
* smart-turn-v3 ONNX model is not loaded in v1. Persisted at
|
|
343
|
-
* ~/.
|
|
343
|
+
* ~/.claude/session-manager/voice.json under `turnDetector`. See PRD F8 v2.
|
|
344
344
|
*/
|
|
345
345
|
export interface VoiceTurnDetectorState {
|
|
346
346
|
enabled: boolean;
|
|
@@ -1944,7 +1944,7 @@ export interface SessionManagerAPI {
|
|
|
1944
1944
|
/** Create a new memory entry with starter frontmatter + body. */
|
|
1945
1945
|
create: (name: string, description?: string, workspace?: string) => Promise<MemoryMutationResult>;
|
|
1946
1946
|
/** Aggregate workspace memories into semantic clusters. `refresh:true` fires a cost-gated `claude -p` pass; otherwise returns the cache only. */
|
|
1947
|
-
aggregate: (workspace: string, refresh?: boolean) => Promise<MemoryAggregateResult>;
|
|
1947
|
+
aggregate: (workspace: string, refresh?: boolean, cwd?: string) => Promise<MemoryAggregateResult>;
|
|
1948
1948
|
/** Deterministic, zero-LLM-cost staleness report. `cwd` (optional) scopes the dead-repo-ref check. */
|
|
1949
1949
|
stale: (workspace?: string, cwd?: string) => Promise<MemoryStaleResult>;
|
|
1950
1950
|
};
|
package/src/preload/index.cjs
CHANGED
|
@@ -158,13 +158,13 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
158
158
|
getHotkeyConfig: () => ipcRenderer.invoke('voice:get-hotkey-config'),
|
|
159
159
|
setHotkeyConfig: (cfg) => ipcRenderer.invoke('voice:set-hotkey', cfg),
|
|
160
160
|
setRecording: (recording) => ipcRenderer.send('voice:set-recording', !!recording),
|
|
161
|
-
// F5 device picker prefs (~/.
|
|
161
|
+
// F5 device picker prefs (~/.claude/session-manager/voice.json `device` key).
|
|
162
162
|
getDevicePref: () => ipcRenderer.invoke('voice:get-device-pref'),
|
|
163
163
|
setDevicePref: (pref) => ipcRenderer.invoke('voice:set-device-pref', pref),
|
|
164
|
-
// F7 first-run wizard state (~/.
|
|
164
|
+
// F7 first-run wizard state (~/.claude/session-manager/voice.json `wizard` key).
|
|
165
165
|
getWizardState: () => ipcRenderer.invoke('voice:wizard-state'),
|
|
166
166
|
markWizardComplete: () => ipcRenderer.invoke('voice:wizard-complete'),
|
|
167
|
-
// F8 turn-detector settings (~/.
|
|
167
|
+
// F8 turn-detector settings (~/.claude/session-manager/voice.json `turnDetector` key).
|
|
168
168
|
// MVP: settings persistence + kill switch only; no model is actually loaded in v1.
|
|
169
169
|
getTurnDetector: () => ipcRenderer.invoke('voice:get-turn-detector'),
|
|
170
170
|
setTurnDetector: (state) => ipcRenderer.invoke('voice:set-turn-detector', state),
|
|
@@ -315,8 +315,8 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
315
315
|
if (workspace) payload.workspace = workspace;
|
|
316
316
|
return ipcRenderer.invoke('memory:create', payload);
|
|
317
317
|
},
|
|
318
|
-
aggregate: (workspace, refresh) =>
|
|
319
|
-
ipcRenderer.invoke('memory:aggregate', refresh ? {
|
|
318
|
+
aggregate: (workspace, refresh, cwd) =>
|
|
319
|
+
ipcRenderer.invoke('memory:aggregate', { workspace, ...(refresh ? { refresh: true } : {}), ...(cwd ? { cwd } : {}) }),
|
|
320
320
|
stale: (workspace, cwd) =>
|
|
321
321
|
ipcRenderer.invoke('memory:stale', { ...(workspace ? { workspace } : {}), ...(cwd ? { cwd } : {}) }),
|
|
322
322
|
},
|