claude-code-session-manager 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AgentLibrary-CiqimwWl.js +3 -0
- package/dist/assets/{History-DB-9-zwc.js → History-DbzjII2Z.js} +2 -2
- package/dist/assets/{Hooks-DnxqMRrR.js → Hooks-C0PTplI_.js} +3 -3
- package/dist/assets/{HostBilko-Cw6JocV7.js → HostBilko-vGc1FYLD.js} +1 -1
- package/dist/assets/{Library-BYSB0dmY.js → Library-4tU4rg2h.js} +1 -1
- package/dist/assets/{ListDetail-BfKqnL0r.js → ListDetail-BGIvkCwZ.js} +1 -1
- package/dist/assets/{MarkdownEditor-Cs-l5JOv.js → MarkdownEditor-ChFdpCam.js} +1 -1
- package/dist/assets/{McpServers-bBqj3hGg.js → McpServers-DsRWWpU3.js} +2 -2
- package/dist/assets/{Memory-DzPGXT5J.js → Memory-DmxoxxxY.js} +6 -6
- package/dist/assets/{Panel-BX6UZ-W0.js → Panel-KC-rv3jS.js} +1 -1
- package/dist/assets/{Permissions-Db7S4T3Z.js → Permissions-DrDjCRFl.js} +3 -3
- package/dist/assets/{Plugins-CIuey9R3.js → Plugins-KexSQwk0.js} +2 -2
- package/dist/assets/{ProvenanceBadge-FPWNWc_-.js → ProvenanceBadge-Rg-jL94y.js} +1 -1
- package/dist/assets/SaveBar-BbEo8U2e.js +1 -0
- package/dist/assets/Scheduler-C4Ti9TCc.js +14 -0
- package/dist/assets/ScopeSwitcher-Di9BmYze.js +1 -0
- package/dist/assets/Settings-CxyhPuYQ.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CprLcOCe.js → SkillReferenceGraph-0q26RHrn.js} +1 -1
- package/dist/assets/Skills-BXkVyCo5.js +3 -0
- package/dist/assets/SystemPrompt-Bwe3NTbi.js +1 -0
- package/dist/assets/{TagLibrary-q66s4N3i.js → TagLibrary-CnwEf6EI.js} +1 -1
- package/dist/assets/{TiptapBody-I22aArc2.js → TiptapBody-faCtLj9L.js} +1 -1
- package/dist/assets/{Toggle-D9sapSAv.js → Toggle-Bw7G_-RR.js} +1 -1
- package/dist/assets/{index-BRwaw_1W.js → index-B6dJ2CsU.js} +666 -665
- package/dist/assets/{index-LlWpj2VJ.css → index-CEnMgeQU.css} +1 -1
- package/dist/assets/{settingsSchema-CTc4qelV.js → settingsSchema-B5C9hZoS.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +73 -15
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +10 -0
- package/src/main/__tests__/agentLibrary.test.cjs +40 -0
- package/src/main/__tests__/develop-skill-failure-modes.test.cjs +70 -0
- package/src/main/__tests__/flatPrdTickSweep.test.cjs +110 -0
- package/src/main/__tests__/health-per-project-stall.test.cjs +82 -0
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +68 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +311 -0
- package/src/main/__tests__/prdCreate.test.cjs +7 -2
- package/src/main/__tests__/prdMigration.test.cjs +17 -0
- package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +91 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +109 -0
- package/src/main/__tests__/scheduleJobSchema.test.cjs +127 -0
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +65 -0
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +152 -0
- package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +59 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +203 -0
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +196 -0
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +108 -0
- package/src/main/agentLibrary.cjs +40 -2
- package/src/main/health.cjs +97 -2
- package/src/main/index.cjs +2 -0
- package/src/main/ipcSchemas.cjs +60 -0
- package/src/main/lib/localAdminHttp.cjs +10 -3
- package/src/main/lib/prdAdminRoutes.cjs +175 -0
- package/src/main/lib/prdCreate.cjs +37 -2
- package/src/main/lib/prdFrontmatter.cjs +179 -1
- package/src/main/lib/prdMigration.cjs +82 -5
- package/src/main/lib/queueStore.cjs +41 -7
- package/src/main/lib/scheduleJobSchema.cjs +114 -0
- package/src/main/lib/scheduleJobTransitions.cjs +164 -0
- package/src/main/lib/schedulerConfig.cjs +7 -0
- package/src/main/scheduler/prdParser.cjs +7 -0
- package/src/main/scheduler.cjs +769 -138
- package/src/preload/api.d.ts +54 -2
- package/src/preload/index.cjs +12 -0
- package/dist/assets/AgentLibrary-DYriNDGf.js +0 -1
- package/dist/assets/Scheduler-X5y252Qw.js +0 -14
- package/dist/assets/ScopeSwitcher-DU7M_q5-.js +0 -1
- package/dist/assets/Settings-DEptXcEY.js +0 -3
- package/dist/assets/Skills-CGN56X1i.js +0 -3
- package/dist/assets/SystemPrompt-DIEK7FpJ.js +0 -1
|
@@ -57,11 +57,15 @@ scheduler isn't decomposition need, it's model economics: keep the interactive m
|
|
|
57
57
|
(an expensive planner-tier model) focused on discussion and decisions, and let a cheaper executor
|
|
58
58
|
model do the implementing as a headless `claude -p` job.
|
|
59
59
|
|
|
60
|
-
**Only execution is delegated — authoring never is.**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
the
|
|
60
|
+
**Only execution is delegated — authoring never is.** Do the thinking — decomposition, scope,
|
|
61
|
+
title, goal, and acceptance criteria — yourself, in the main loop; that's what "authoring" means
|
|
62
|
+
here, and it's non-negotiable regardless of which mechanism ends up putting bytes on disk. Do
|
|
63
|
+
not spawn a subagent to draft a PRD or otherwise hand off the writing/thinking — that defeats the
|
|
64
|
+
point of keeping planning on the expensive model. Once you've composed the PRD yourself, submit
|
|
65
|
+
it through `scheduler_create_prd` (the MCP tool — see step 4's "PRD structure and location"
|
|
66
|
+
below): that tool call is the sanctioned path from your own composed content to a file on disk,
|
|
67
|
+
not a second author. The scheduled `claude -p` job remains the only step that runs on the
|
|
68
|
+
cheaper executor.
|
|
65
69
|
|
|
66
70
|
## Standards (single source of truth)
|
|
67
71
|
|
|
@@ -206,8 +210,20 @@ can't load skills.
|
|
|
206
210
|
separate PRDs into one oversized one; if the sub-task groups would each take real time on
|
|
207
211
|
their own, that's a signal to split into a chain link instead of one bloated PRD.
|
|
208
212
|
|
|
209
|
-
**
|
|
210
|
-
|
|
213
|
+
**Preflight — confirm the tool is even in your tool list before you start composing PRDs.**
|
|
214
|
+
Check for `mcp__session-manager-scheduler__scheduler_create_prd` in your available tools as
|
|
215
|
+
the very first thing you do in this step, before any drafting — catching a missing tool here
|
|
216
|
+
costs nothing; catching it after you've already composed and written PRD bodies means
|
|
217
|
+
discarding that work. If it's absent, see "Two failure modes" immediately below — case (b),
|
|
218
|
+
not the reachable-but-erroring fallback.
|
|
219
|
+
|
|
220
|
+
**`scheduler_create_prd` is the ONLY sanctioned way to author a PRD — not a preference, a
|
|
221
|
+
rule.** Every PRD reaches disk through the MCP tool
|
|
222
|
+
(`mcp__session-manager-scheduler__scheduler_create_prd`). Hand-writing the file directly is a
|
|
223
|
+
degraded, LAST-RESORT fallback (below) reserved for the single case where the tool is present
|
|
224
|
+
but errors as unreachable — never a co-equal alternative to reach for out of habit or
|
|
225
|
+
convenience, and never applicable when the tool isn't in your list at all (see "Two failure
|
|
226
|
+
modes" below). Its input
|
|
211
227
|
(`title`, `cwd`, `estimateMinutes`, `goal`, `acceptanceCriteria[]`, `implementationNotes`,
|
|
212
228
|
`outOfScope[]`) maps directly onto the sections below — pass them straight through. **Always
|
|
213
229
|
pass `sourcePromptId` explicitly, set to the `<epic-id>` resolved in the Epic-gated step
|
|
@@ -220,9 +236,38 @@ can't load skills.
|
|
|
220
236
|
`parallelGroup` is DEPRECATED and ignored — express ordering with the `dependsOn` input
|
|
221
237
|
(slugs that must complete first); independent PRDs simply omit it and may run in parallel.
|
|
222
238
|
|
|
223
|
-
**
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
**Two failure modes — do not conflate them. They have opposite correct responses.**
|
|
240
|
+
|
|
241
|
+
- **(a) Tool PRESENT but ERRORS as "app not running" / admin API unreachable.** The tool
|
|
242
|
+
shows up in your tool list (`mcp__session-manager-scheduler__scheduler_create_prd` is
|
|
243
|
+
callable), but calling it fails because the session-manager Electron app that hosts the
|
|
244
|
+
admin API isn't running right now. This is the ONLY case the manual-write fallback below
|
|
245
|
+
covers. Do not use this path when the tool is reachable but merely returned a validation
|
|
246
|
+
error (bad frontmatter, unresolvable Epic, etc.) — fix the input and retry the tool; a
|
|
247
|
+
validation error is not "the app is not running."
|
|
248
|
+
- **(b) Tool ABSENT from your tool list entirely.** You never see
|
|
249
|
+
`mcp__session-manager-scheduler__scheduler_create_prd` offered at all — there is no error to
|
|
250
|
+
catch, because the tool call is never attempted. This means the `session-manager-scheduler`
|
|
251
|
+
MCP server is not registered for the project you're running against — a **misconfiguration**,
|
|
252
|
+
not "the app is offline." **STOP. Do not write any PRD file, hand-authored or otherwise.**
|
|
253
|
+
Report to the human, by name: "the `session-manager-scheduler` MCP tool is not available in
|
|
254
|
+
this session — the server isn't registered for this project." Point them at the fix: it
|
|
255
|
+
should be registered once at USER scope (`claude mcp add session-manager-scheduler --scope
|
|
256
|
+
user -- node <path-to-session-manager-repo>/scripts/scheduler-mcp-server.cjs`, or run
|
|
257
|
+
`scripts/install-scheduler-mcp-user-scope.sh` from the session-manager repo) so every
|
|
258
|
+
project gets the tool without a per-repo `.mcp.json` edit — do not work around a missing
|
|
259
|
+
tool by hand-writing the file, and do not add a project-local `.mcp.json` entry yourself as
|
|
260
|
+
a substitute; that's the human's call and re-introduces the per-repo drift this fix removes.
|
|
261
|
+
|
|
262
|
+
**Fallback for case (a) only.** This is a deliberate bypass of the service boundary, not a
|
|
263
|
+
shortcut: using it means the frontmatter validation, atomic `NN` allocation, standards-pointer
|
|
264
|
+
insertion, and Epic-existence check that `scheduler_create_prd` normally performs did not
|
|
265
|
+
run. **You MUST call this out, visibly, in your report** — state plainly that the app wasn't
|
|
266
|
+
running, that you hand-authored the PRD file directly instead of using the tool, name the
|
|
267
|
+
exact file, and flag it for human verification (this bypass is also what
|
|
268
|
+
`scripts/audit-ops-hygiene.cjs` and the `ops-sweep` skill look for and report as a hygiene
|
|
269
|
+
finding, independent of your own report).
|
|
270
|
+
When you do use it: compute the highest in-use number deterministically yourself — never
|
|
226
271
|
eyeball or narrow-grep the `ls` (a narrowed pattern like `'^10[0-9]'` silently misses `110+`
|
|
227
272
|
and collides). PRDs are stored per-project, so `NN` allocation for a given PRD only needs
|
|
228
273
|
*that project's own* prds directory scanned — not every project's:
|
|
@@ -268,9 +313,16 @@ can't load skills.
|
|
|
268
313
|
**Anywhere else doesn't get scheduled or gets retired.** `data/prds/`, `docs/prds/`, and the
|
|
269
314
|
old global `prds/` dir under `~/.claude/session-manager/scheduled-plans/` are invisible to
|
|
270
315
|
the scheduler; the legacy flat `session-manager-operations/scheduler/prds/` dir is RETIRED —
|
|
271
|
-
anything written there is auto-consolidated into `prds-archived/`
|
|
272
|
-
|
|
273
|
-
|
|
316
|
+
anything written there is auto-consolidated into `prds-archived/` and never executed. This
|
|
317
|
+
consolidation runs at the top of every `reconcile()` call (`consolidateAllFlatPrds`, called
|
|
318
|
+
from inside `reconcile()` itself in `src/main/scheduler.cjs`, before `reconcile` scans that
|
|
319
|
+
dir for PRD sources) — not only at app boot, and not just from the tick-queue poll:
|
|
320
|
+
`reconcile()` also runs from job completion, the `schedule:state`/`schedule:rescan` IPC
|
|
321
|
+
handlers, and `rescheduleTimer()`, so the sweep is guaranteed regardless of which of those
|
|
322
|
+
triggers the next pass. A file landing in the flat dir while the app is already running is
|
|
323
|
+
swept out before it could ever be turned into a job, closing the window a boot-only pass left
|
|
324
|
+
open. PRD *source* files are per-project and per-Epic, resolved at runtime via
|
|
325
|
+
`src/main/lib/prdLocations.cjs`.
|
|
274
326
|
|
|
275
327
|
**Filename rules.** `NN` is the PRD's unique per-project number (always next free =
|
|
276
328
|
max+1 per the `ls` command above; ordering via `dependsOn` frontmatter, never via shared
|
|
@@ -423,11 +475,17 @@ single definition of "tracked to done" for both entry paths.
|
|
|
423
475
|
|
|
424
476
|
## Notes
|
|
425
477
|
|
|
426
|
-
-
|
|
478
|
+
- Submit each PRD through `scheduler_create_prd`, then confirm — don't draft them inline in chat
|
|
479
|
+
for review first. Only hand-write the file when the tool is PRESENT but ERRORS as unreachable
|
|
480
|
+
(app not running) — see the "Two failure modes" note above, including its mandatory bypass
|
|
481
|
+
warning. If the tool is ABSENT from your tool list, that's a misconfiguration, not an offline
|
|
482
|
+
app: stop and tell the human, never hand-write the file.
|
|
427
483
|
- Don't combine unrelated features into one PRD. One focused, completable unit each.
|
|
428
484
|
- Don't add a `parallelGroup` frontmatter key — the filename `NN-` prefix drives grouping.
|
|
429
485
|
- Don't write a PRD to `data/prds/`, `docs/prds/`, the project's own folder, or anywhere outside
|
|
430
|
-
the canonical path
|
|
486
|
+
the canonical path — and don't reach for a hand-written file at the canonical path either, when
|
|
487
|
+
`scheduler_create_prd` is reachable. The user has explicitly flagged this as a recurring
|
|
488
|
+
problem.
|
|
431
489
|
- Don't leave `cwd` unset hoping for the default. Be explicit.
|
|
432
490
|
- Don't skip a step-3 lens silently and don't force every request into a chain — most asks are
|
|
433
491
|
still an independent set of small PRDs; reach for the 3-5-PRD evolving chain only when the
|
|
@@ -36,6 +36,16 @@ etc.), this skill makes **no assumption about the target project's namespace
|
|
|
36
36
|
names, its OWNERS table shape, or whether it has a CLAUDE.md at all**. It
|
|
37
37
|
re-derives the source-of-truth hierarchy from what that specific project has:
|
|
38
38
|
|
|
39
|
+
**Hand-authored-PRD detection stays out of this generic script on purpose.**
|
|
40
|
+
`audit-ops-hygiene.cjs`'s Pattern E (cross-referencing a live PRD `.md` file
|
|
41
|
+
against the machine-wide `prd_create` audit log to flag a PRD that bypassed
|
|
42
|
+
`scheduler_create_prd`) requires session-manager's own scheduler/PRD/audit-log
|
|
43
|
+
concepts, which a swept project may not even have — adding it here would break
|
|
44
|
+
the "no assumption about vocabulary" guarantee above. When sweeping
|
|
45
|
+
session-manager's own repo, run `node scripts/audit-ops-hygiene.cjs` alongside
|
|
46
|
+
this skill and fold its Pattern C/D/E findings into the same report; when
|
|
47
|
+
sweeping a different project, Pattern E simply doesn't apply.
|
|
48
|
+
|
|
39
49
|
1. That project's own `CLAUDE.md` (if present) — the declared architecture.
|
|
40
50
|
2. Each namespace's own `README.md` under its `session-manager-operations/<namespace>/`.
|
|
41
51
|
3. On-disk content — folder names, and archived/processed subfolders.
|
|
@@ -213,3 +213,43 @@ test('removeOverride deletes a project overlay resolved by project name, and rej
|
|
|
213
213
|
removeOverride({ name: 'builder', projectName: 'not-open', loadSessions, validatePath: identityValidatePath }),
|
|
214
214
|
).rejects.toThrow(/project not open/);
|
|
215
215
|
});
|
|
216
|
+
|
|
217
|
+
test('projects/action/actionLabel round-trip through savePersona -> listPersonas, with newlines preserved', async () => {
|
|
218
|
+
const globalDir = await mkTmp('sm-agent-library-action-');
|
|
219
|
+
const action = '/builder\n\nCheck git vs the published package and publish anything new.';
|
|
220
|
+
await savePersona({
|
|
221
|
+
name: 'builder',
|
|
222
|
+
description: 'Drives the next publish.',
|
|
223
|
+
tools: ['Bash'],
|
|
224
|
+
model: 'inherit',
|
|
225
|
+
color: '',
|
|
226
|
+
tags: ['build'],
|
|
227
|
+
projects: ['/home/bilko/Projects/alpha', '/home/bilko/Projects/beta'],
|
|
228
|
+
action,
|
|
229
|
+
actionLabel: 'Run Build',
|
|
230
|
+
body: 'Body.',
|
|
231
|
+
globalDir,
|
|
232
|
+
validatePath: identityValidatePath,
|
|
233
|
+
writeTextAtomic: fakeWriteTextAtomic,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// The frontmatter parser is line-based, so the multi-line action must be
|
|
237
|
+
// stored escaped — a raw newline would silently truncate it on read.
|
|
238
|
+
const raw = await fsp.readFile(path.join(globalDir, 'builder.md'), 'utf8');
|
|
239
|
+
expect(raw).toContain('action: /builder\\n\\nCheck git');
|
|
240
|
+
expect(raw).toContain('projects: /home/bilko/Projects/alpha, /home/bilko/Projects/beta');
|
|
241
|
+
|
|
242
|
+
const [p] = await listPersonas({ globalDir, loadSessions: async () => ({ tabs: [] }), validatePath: identityValidatePath });
|
|
243
|
+
expect(p.projects).toEqual(['/home/bilko/Projects/alpha', '/home/bilko/Projects/beta']);
|
|
244
|
+
expect(p.action).toBe(action);
|
|
245
|
+
expect(p.actionLabel).toBe('Run Build');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('a persona with no Action fields reports them as empty/null rather than undefined', async () => {
|
|
249
|
+
const globalDir = await mkTmp('sm-agent-library-noaction-');
|
|
250
|
+
await fsp.writeFile(path.join(globalDir, 'plain.md'), '---\nname: plain\n---\nBody.\n');
|
|
251
|
+
const [p] = await listPersonas({ globalDir, loadSessions: async () => ({ tabs: [] }), validatePath: identityValidatePath });
|
|
252
|
+
expect(p.projects).toEqual([]);
|
|
253
|
+
expect(p.action).toBeNull();
|
|
254
|
+
expect(p.actionLabel).toBeNull();
|
|
255
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* develop-skill-failure-modes.test.cjs — drift guard for the two
|
|
3
|
+
* scheduler_create_prd failure-mode branches in the /develop skill.
|
|
4
|
+
*
|
|
5
|
+
* PRD 1024-1030 incident (2026-08-08): social-signals-trader's session
|
|
6
|
+
* hand-wrote 7 PRDs because scheduler_create_prd was ABSENT from its tool
|
|
7
|
+
* list (the project's .mcp.json didn't register the server yet) — not
|
|
8
|
+
* because the tool errored as unreachable. The old wording only named the
|
|
9
|
+
* "tool errors ... unreachable" case, which an absent tool never matches,
|
|
10
|
+
* so an agent in that situation had no documented path except hand-writing.
|
|
11
|
+
* This test asserts the fix (two named failure modes, with the absent-tool
|
|
12
|
+
* case explicitly forbidding hand-writing) is present and doesn't silently
|
|
13
|
+
* regress back to the single-case wording.
|
|
14
|
+
*
|
|
15
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/develop-skill-failure-modes.test.cjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
import { test, expect } from 'vitest';
|
|
24
|
+
|
|
25
|
+
const SKILL_MD = path.resolve(
|
|
26
|
+
__dirname,
|
|
27
|
+
'..',
|
|
28
|
+
'..',
|
|
29
|
+
'..',
|
|
30
|
+
'plugins',
|
|
31
|
+
'session-manager-dev',
|
|
32
|
+
'skills',
|
|
33
|
+
'develop',
|
|
34
|
+
'SKILL.md'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function readSkill() {
|
|
38
|
+
return fs.readFileSync(SKILL_MD, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('SKILL.md names both failure modes explicitly', () => {
|
|
42
|
+
const text = readSkill();
|
|
43
|
+
expect(text).toMatch(/tool PRESENT but ERRORS/i);
|
|
44
|
+
expect(text).toMatch(/tool ABSENT from your tool list/i);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the absent-tool branch explicitly forbids hand-writing and says STOP', () => {
|
|
48
|
+
const text = readSkill();
|
|
49
|
+
const absentIdx = text.search(/tool ABSENT from your tool list/i);
|
|
50
|
+
expect(absentIdx).toBeGreaterThan(-1);
|
|
51
|
+
const nearby = text.slice(absentIdx, absentIdx + 1200);
|
|
52
|
+
expect(nearby).toMatch(/STOP/);
|
|
53
|
+
expect(nearby).toMatch(/[Dd]o not write any PRD file/);
|
|
54
|
+
expect(nearby).toMatch(/misconfiguration/i);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('the present-but-erroring branch is the only one pointed at the manual-write fallback', () => {
|
|
58
|
+
const text = readSkill();
|
|
59
|
+
expect(text).toMatch(/Fallback for case \(a\) only/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('the standalone preflight check appears before PRD composition begins', () => {
|
|
63
|
+
const text = readSkill();
|
|
64
|
+
expect(text).toMatch(/Preflight — confirm the tool is even in your tool list/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a preflight-tool-registration fix is named for the human (user-scope claude mcp add)', () => {
|
|
68
|
+
const text = readSkill();
|
|
69
|
+
expect(text).toMatch(/claude mcp add session-manager-scheduler --scope\s*\n?\s*user/);
|
|
70
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flatPrdTickSweep.test.cjs — the retired flat `scheduler/prds/` dir must be
|
|
3
|
+
* TRULY inert, not just documented as such.
|
|
4
|
+
*
|
|
5
|
+
* Prior to this PRD, `runPrdMigration()`'s consolidation of the flat dir into
|
|
6
|
+
* `prds-archived/` ran ONLY at Electron boot (src/main/scheduler.cjs). But
|
|
7
|
+
* `prdLocations.cjs`'s `resolvePrdsDirs()` unconditionally keeps scanning the
|
|
8
|
+
* flat dir as a live PRD source, and `reconcile()` runs on every tick — so a
|
|
9
|
+
* PRD hand-written into the flat dir *after* boot sat there live and WOULD
|
|
10
|
+
* get reconciled into a pending job and executed (observed 2026-08-07:
|
|
11
|
+
* 1021/1022 PRDs in social-signals-trader executed once their status was
|
|
12
|
+
* repaired), contradicting the develop skill's documented claim that
|
|
13
|
+
* anything landed there is "auto-consolidated ... WITHOUT being executed."
|
|
14
|
+
*
|
|
15
|
+
* The fix: `consolidateAllFlatPrds()` now also runs at the START of every
|
|
16
|
+
* `reconcile()` call itself (not just one caller like tickQueue's poll — every
|
|
17
|
+
* caller of reconcile gets the guarantee), BEFORE reconcile scans the flat
|
|
18
|
+
* dir. A freshly hand-written PRD has no queue row yet (reconcile hasn't
|
|
19
|
+
* seen it), so it is never in LIVE_JOB_STATUSES — this sweep archives it
|
|
20
|
+
* before reconcile ever gets a chance to turn it into a job. This test
|
|
21
|
+
* proves that invariant directly against `consolidateAllFlatPrds`: a file
|
|
22
|
+
* with no matching queue job is swept into `prds-archived/` on the very next
|
|
23
|
+
* call, which prdLocations.cjs's `resolvePrdsDirs()` never scans as a live
|
|
24
|
+
* source — so it can never be reconciled into a job afterward.
|
|
25
|
+
*
|
|
26
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/flatPrdTickSweep.test.cjs
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
import { test, expect, afterEach } from 'vitest';
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const fsp = require('node:fs/promises');
|
|
34
|
+
const os = require('node:os');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
const { consolidateAllFlatPrds } = require('../scheduler.cjs');
|
|
37
|
+
const { resolvePrdWriteDir, resolvePrdsDirs } = require('../lib/prdLocations.cjs');
|
|
38
|
+
|
|
39
|
+
const tmpDirs = [];
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
while (tmpDirs.length) {
|
|
42
|
+
const d = tmpDirs.pop();
|
|
43
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function newProjectRoot() {
|
|
48
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-flat-tick-sweep-'));
|
|
49
|
+
tmpDirs.push(d);
|
|
50
|
+
return d;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('a PRD hand-written into the retired flat dir after boot is swept before it could ever be reconciled', async () => {
|
|
54
|
+
const cwd = newProjectRoot();
|
|
55
|
+
const flatDir = resolvePrdWriteDir(cwd);
|
|
56
|
+
await fsp.mkdir(flatDir, { recursive: true });
|
|
57
|
+
const filename = '999-hand-written-bypass.md';
|
|
58
|
+
await fsp.writeFile(
|
|
59
|
+
path.join(flatDir, filename),
|
|
60
|
+
`---\ntitle: Hand-written bypass\ncwd: ${cwd}\nestimateMinutes: 10\n---\n\n# Goal\n\nSomething.\n`,
|
|
61
|
+
'utf8',
|
|
62
|
+
);
|
|
63
|
+
// No queue.json at all yet — this is exactly the state of a PRD dropped on
|
|
64
|
+
// disk moments ago, before any reconcile() pass has scanned it into a job.
|
|
65
|
+
|
|
66
|
+
await consolidateAllFlatPrds([cwd]);
|
|
67
|
+
|
|
68
|
+
// Swept out of the live flat dir...
|
|
69
|
+
expect(fs.existsSync(path.join(flatDir, filename))).toBe(false);
|
|
70
|
+
// ...and into prds-archived/, which resolvePrdsDirs() never returns as a
|
|
71
|
+
// scan source — so no future reconcile() pass can find it and queue it.
|
|
72
|
+
const archivedPath = path.join(path.dirname(flatDir), 'prds-archived', filename);
|
|
73
|
+
expect(fs.existsSync(archivedPath)).toBe(true);
|
|
74
|
+
|
|
75
|
+
const liveDirs = resolvePrdsDirs(Infinity, { projectsDir: path.join(cwd, '.nonexistent') });
|
|
76
|
+
expect(liveDirs.every((d) => !d.includes('prds-archived'))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('consolidateAllFlatPrds still protects a PRD that already has a live queue job (PRD 992 invariant preserved)', async () => {
|
|
80
|
+
const cwd = newProjectRoot();
|
|
81
|
+
const flatDir = resolvePrdWriteDir(cwd);
|
|
82
|
+
await fsp.mkdir(flatDir, { recursive: true });
|
|
83
|
+
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
84
|
+
await fsp.mkdir(stateDir, { recursive: true });
|
|
85
|
+
await fsp.writeFile(
|
|
86
|
+
path.join(flatDir, '998-live.md'),
|
|
87
|
+
`---\ntitle: Live\ncwd: ${cwd}\nestimateMinutes: 10\n---\n\n# Goal\n`,
|
|
88
|
+
'utf8',
|
|
89
|
+
);
|
|
90
|
+
await fsp.writeFile(
|
|
91
|
+
path.join(stateDir, 'queue.json'),
|
|
92
|
+
JSON.stringify({ jobs: [{ slug: '998-live', status: 'running' }] }),
|
|
93
|
+
'utf8',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await consolidateAllFlatPrds([cwd]);
|
|
97
|
+
|
|
98
|
+
expect(fs.existsSync(path.join(flatDir, '998-live.md'))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('consolidateAllFlatPrds never throws when a project errors — one bad cwd does not block the sweep of the rest', async () => {
|
|
102
|
+
const good = newProjectRoot();
|
|
103
|
+
const flatDir = resolvePrdWriteDir(good);
|
|
104
|
+
await fsp.mkdir(flatDir, { recursive: true });
|
|
105
|
+
await fsp.writeFile(path.join(flatDir, '997-ok.md'), `---\ntitle: Ok\ncwd: ${good}\nestimateMinutes: 5\n---\n\n# Goal\n`, 'utf8');
|
|
106
|
+
|
|
107
|
+
await expect(consolidateAllFlatPrds([null, good])).resolves.toBeUndefined();
|
|
108
|
+
|
|
109
|
+
expect(fs.existsSync(path.join(flatDir, '997-ok.md'))).toBe(false);
|
|
110
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-per-project-stall.test.cjs — per-project problem-count rollup and
|
|
3
|
+
* per-project stall-past-threshold escalation for the scheduler_queue health
|
|
4
|
+
* check. Covers the gap where a project holding ONLY failed/needs_review/
|
|
5
|
+
* quarantined rows (0 running, 0 pending) never tripped evaluateTickLiveness
|
|
6
|
+
* (which requires actual pending work) — the exact way the burrow project's
|
|
7
|
+
* four quarantined PRDs went dark.
|
|
8
|
+
*
|
|
9
|
+
* Run: timeout 120 node --test src/main/__tests__/health-per-project-stall.test.cjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { test } = require('node:test');
|
|
15
|
+
const assert = require('node:assert/strict');
|
|
16
|
+
const {
|
|
17
|
+
computeProjectProblemCounts,
|
|
18
|
+
evaluatePerProjectStall,
|
|
19
|
+
TICK_STALL_THRESHOLD_MS,
|
|
20
|
+
} = require('../health.cjs');
|
|
21
|
+
const { computeStallSummary } = require('../scheduler.cjs');
|
|
22
|
+
|
|
23
|
+
test('computeProjectProblemCounts: breaks down failed/needs_review/quarantined by project, ignores healthy statuses', () => {
|
|
24
|
+
const jobs = [
|
|
25
|
+
{ slug: 'a', cwd: '/burrow', status: 'quarantined' },
|
|
26
|
+
{ slug: 'b', cwd: '/burrow', status: 'quarantined' },
|
|
27
|
+
{ slug: 'c', cwd: '/burrow', status: 'quarantined' },
|
|
28
|
+
{ slug: 'd', cwd: '/burrow', status: 'quarantined' },
|
|
29
|
+
{ slug: 'e', cwd: '/other', status: 'failed' },
|
|
30
|
+
{ slug: 'f', cwd: '/other', status: 'needs_review' },
|
|
31
|
+
{ slug: 'g', cwd: '/other', status: 'running' },
|
|
32
|
+
{ slug: 'h', cwd: '/other', status: 'pending' },
|
|
33
|
+
{ slug: 'i', cwd: '/other', status: 'completed' },
|
|
34
|
+
];
|
|
35
|
+
const counts = computeProjectProblemCounts(jobs);
|
|
36
|
+
assert.deepStrictEqual(counts['/burrow'], { failed: 0, needs_review: 0, quarantined: 4 });
|
|
37
|
+
assert.deepStrictEqual(counts['/other'], { failed: 1, needs_review: 1, quarantined: 0 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('evaluatePerProjectStall: fully-stalled project past threshold flags pastThreshold=true', () => {
|
|
41
|
+
const state = {
|
|
42
|
+
lastRunAt: new Date(Date.now() - (TICK_STALL_THRESHOLD_MS + 60_000)).toISOString(),
|
|
43
|
+
paused: null,
|
|
44
|
+
jobs: [
|
|
45
|
+
{ slug: 'a', cwd: '/burrow', status: 'quarantined' },
|
|
46
|
+
{ slug: 'b', cwd: '/other', status: 'running' },
|
|
47
|
+
],
|
|
48
|
+
invalidJobs: [],
|
|
49
|
+
};
|
|
50
|
+
const summary = computeStallSummary(state);
|
|
51
|
+
const result = evaluatePerProjectStall(summary, state.lastRunAt, Date.now(), TICK_STALL_THRESHOLD_MS);
|
|
52
|
+
assert.strictEqual(result['/burrow'].stalled, true);
|
|
53
|
+
assert.strictEqual(result['/burrow'].pastThreshold, true);
|
|
54
|
+
assert.strictEqual(result['/other'].stalled, false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('evaluatePerProjectStall: stalled but NOT yet past threshold flags pastThreshold=false', () => {
|
|
58
|
+
const state = {
|
|
59
|
+
lastRunAt: new Date(Date.now() - 1000).toISOString(), // just ticked
|
|
60
|
+
paused: null,
|
|
61
|
+
jobs: [{ slug: 'a', cwd: '/burrow', status: 'quarantined' }],
|
|
62
|
+
invalidJobs: [],
|
|
63
|
+
};
|
|
64
|
+
const summary = computeStallSummary(state);
|
|
65
|
+
const result = evaluatePerProjectStall(summary, state.lastRunAt, Date.now(), TICK_STALL_THRESHOLD_MS);
|
|
66
|
+
assert.strictEqual(result['/burrow'].stalled, true);
|
|
67
|
+
assert.strictEqual(result['/burrow'].pastThreshold, false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('evaluatePerProjectStall: no lastRunAt yet — stalled but not asserted past threshold (caveat, not a false RED)', () => {
|
|
71
|
+
const state = {
|
|
72
|
+
lastRunAt: null,
|
|
73
|
+
paused: null,
|
|
74
|
+
jobs: [{ slug: 'a', cwd: '/burrow', status: 'quarantined' }],
|
|
75
|
+
invalidJobs: [],
|
|
76
|
+
};
|
|
77
|
+
const summary = computeStallSummary(state);
|
|
78
|
+
const result = evaluatePerProjectStall(summary, state.lastRunAt, Date.now(), TICK_STALL_THRESHOLD_MS);
|
|
79
|
+
assert.strictEqual(result['/burrow'].stalled, true);
|
|
80
|
+
assert.strictEqual(result['/burrow'].pastThreshold, false);
|
|
81
|
+
assert.strictEqual(result['/burrow'].caveat, 'no-lastRunAt');
|
|
82
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdAdminRouteParity.test.cjs — capability-parity gate for PRD 1024 ("lock
|
|
3
|
+
* the PRD entity behind the scheduler service boundary, step 1").
|
|
4
|
+
*
|
|
5
|
+
* Enumerates every PRD/job mutation the codebase can perform (the full set
|
|
6
|
+
* of `schedule:*` IPC channels that read/write a PRD file or a queue job,
|
|
7
|
+
* plus create-prd and cancel-job which have no IPC twin) and asserts each
|
|
8
|
+
* one has a corresponding admin HTTP route registered by scheduler.cjs's
|
|
9
|
+
* registerAdminRoutes, prdCreate.cjs's registerAdminRoute, or
|
|
10
|
+
* prdAdminRoutes.cjs's registerAdminRoute. A future PRD-lifecycle capability
|
|
11
|
+
* added only as an IPC handler (filesystem-only from the admin API's point
|
|
12
|
+
* of view) fails this test until an admin route is added for it too — that
|
|
13
|
+
* is the whole point: the admin API must never fall behind what's actually
|
|
14
|
+
* possible against a PRD/job.
|
|
15
|
+
*
|
|
16
|
+
* Route registration is captured via a fake `adminHttp` stub (just
|
|
17
|
+
* `registerRoute`) — no real HTTP server or Electron boot needed, matching
|
|
18
|
+
* how prdCreate.test.cjs already verifies wiring.
|
|
19
|
+
*
|
|
20
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdAdminRouteParity.test.cjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
import { test, expect } from 'vitest';
|
|
26
|
+
const scheduler = require('../scheduler.cjs');
|
|
27
|
+
const prdCreate = require('../lib/prdCreate.cjs');
|
|
28
|
+
const prdAdminRoutes = require('../lib/prdAdminRoutes.cjs');
|
|
29
|
+
|
|
30
|
+
// Every PRD-lifecycle capability the codebase can perform, mapped to the
|
|
31
|
+
// admin HTTP route that must exist for it. `ipcChannel` is informational
|
|
32
|
+
// (documents the renderer-facing twin, if any) — the assertion is only
|
|
33
|
+
// against `adminRoute`.
|
|
34
|
+
const REQUIRED_CAPABILITIES = [
|
|
35
|
+
{ capability: 'create a PRD', ipcChannel: null, adminRoute: 'POST /admin/scheduler/create-prd' },
|
|
36
|
+
{ capability: 'list PRDs (live + archived)', ipcChannel: 'schedule:list-prds', adminRoute: 'GET /admin/scheduler/prds' },
|
|
37
|
+
{ capability: 'read one PRD (body + frontmatter)', ipcChannel: 'schedule:read-prd', adminRoute: 'GET /admin/scheduler/prd' },
|
|
38
|
+
{ capability: 'edit a not-yet-running PRD', ipcChannel: 'schedule:write-prd', adminRoute: 'POST /admin/scheduler/update-prd' },
|
|
39
|
+
{ capability: 'archive a PRD', ipcChannel: 'schedule:archive-prd', adminRoute: 'POST /admin/scheduler/archive-prd' },
|
|
40
|
+
{ capability: 'retag a PRD (parallelGroup/estimateMinutes)', ipcChannel: 'schedule:retag-prd', adminRoute: 'POST /admin/scheduler/retag-prd' },
|
|
41
|
+
{ capability: 'list scheduler jobs', ipcChannel: null, adminRoute: 'GET /admin/scheduler/jobs' },
|
|
42
|
+
{ capability: 'reset a stuck job', ipcChannel: 'schedule:reset-job', adminRoute: 'POST /admin/scheduler/reset-job' },
|
|
43
|
+
{ capability: 'cancel a not-yet-terminal job', ipcChannel: null, adminRoute: 'POST /admin/scheduler/cancel-job' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function captureRegisteredRoutes() {
|
|
47
|
+
const registered = new Set();
|
|
48
|
+
const fakeAdminHttp = {
|
|
49
|
+
registerRoute(method, url) {
|
|
50
|
+
registered.add(`${method} ${url}`);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
scheduler.registerAdminRoutes(fakeAdminHttp, scheduler.remote);
|
|
54
|
+
prdCreate.registerAdminRoute(fakeAdminHttp, scheduler.remote);
|
|
55
|
+
prdAdminRoutes.registerAdminRoute(fakeAdminHttp, scheduler.remote);
|
|
56
|
+
return registered;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('every enumerated PRD/job mutation capability has a registered admin route', () => {
|
|
60
|
+
const registered = captureRegisteredRoutes();
|
|
61
|
+
const missing = REQUIRED_CAPABILITIES.filter((c) => !registered.has(c.adminRoute));
|
|
62
|
+
expect(missing).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.each(REQUIRED_CAPABILITIES)('capability "$capability" → $adminRoute is registered', ({ adminRoute }) => {
|
|
66
|
+
const registered = captureRegisteredRoutes();
|
|
67
|
+
expect(registered.has(adminRoute)).toBe(true);
|
|
68
|
+
});
|