claude-code-session-manager 0.74.0 → 0.75.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-CGplUKxY.js +3 -0
- package/dist/assets/DataModel-DQ15KxVb.js +1 -0
- package/dist/assets/{History-ShwkUTny.js → History-v2TSC_6A.js} +2 -2
- package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-Co7s6BNB.js} +2 -2
- package/dist/assets/HostBilko-DJj8FWPK.js +1 -0
- package/dist/assets/{Library-BOL6GqFX.js → Library-DQaC9VQ1.js} +1 -1
- package/dist/assets/ListDetail-BwFRfAZm.js +1 -0
- package/dist/assets/MarkdownEditor-C155gNpc.js +1 -0
- package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-BmLgE_gS.js} +2 -2
- package/dist/assets/{Memory-BYJHDz7W.js → Memory-ISwNcS5j.js} +4 -4
- package/dist/assets/Panel-1adjEpj-.js +1 -0
- package/dist/assets/{Permissions-DhYERDVR.js → Permissions-FrCz5cGa.js} +3 -3
- package/dist/assets/Plugins-zUssKp_b.js +2 -0
- package/dist/assets/ProvenanceBadge-CAXqA_HX.js +1 -0
- package/dist/assets/SaveBar-D9cqms3Y.js +1 -0
- package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-dNNc9S11.js} +6 -6
- package/dist/assets/ScopeSwitcher-1wBTne7S.js +1 -0
- package/dist/assets/Settings-BT_expIg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-CVccq4Cu.js} +1 -1
- package/dist/assets/{Skills-BHlH2uG5.js → Skills-xFpRY0ld.js} +2 -2
- package/dist/assets/SystemPrompt-CEk_71th.js +1 -0
- package/dist/assets/TagLibrary-C2zhihQG.js +1 -0
- package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-CQKcHKaD.js} +1 -1
- package/dist/assets/Toggle-CQ-IBb8m.js +1 -0
- package/dist/assets/{index-Cs_Lq9-w.js → index-BgbV3sKX.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-D6ccSKqm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +116 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +113 -7
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +119 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
- package/src/main/__tests__/seedStatus.test.cjs +100 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/index.cjs +42 -1
- package/src/main/ipcSchemas.cjs +25 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/scheduler.cjs +136 -27
- package/src/main/seedAgentPersonas.cjs +92 -0
- package/src/main/seedDevPlugin.cjs +14 -2
- package/src/main/seedSchedulerMcp.cjs +148 -0
- package/src/main/seedStatus.cjs +59 -0
- package/src/preload/api.d.ts +40 -1
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/architect.md +64 -0
- package/src/seed/agents/dev-lead.md +42 -0
- package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
- package/src/seed/prompts/code-review/correctness-only.md +8 -0
- package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
- package/src/seed/prompts/code-review/hallucination-check.md +8 -0
- package/src/seed/prompts/code-review/public-api-compat.md +8 -0
- package/src/seed/prompts/code-review/readability-naming.md +8 -0
- package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
- package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
- package/src/seed/prompts/documentation/adr-from-change.md +8 -0
- package/src/seed/prompts/documentation/module-readme.md +8 -0
- package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
- package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
- package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
- package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
- package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
- package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
- package/src/seed/prompts/performance/complexity-audit.md +8 -0
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
- package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
- package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
- package/src/seed/prompts/qa/api-contract-tests.md +8 -0
- package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
- package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
- package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
- package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
- package/src/seed/prompts/qa/tdd-red-first.md +8 -0
- package/src/seed/prompts/qa/visual-regression-review.md +8 -0
- package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
- package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
- package/src/seed/prompts/security/authz-audit.md +8 -0
- package/src/seed/prompts/security/crypto-correctness.md +8 -0
- package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
- package/src/seed/prompts/security/dependency-audit.md +8 -0
- package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
- package/src/seed/prompts/security/secret-credential-scan.md +8 -0
- package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
- package/dist/assets/HostBilko-evd1hD8q.js +0 -1
- package/dist/assets/ListDetail-BPJb2et3.js +0 -1
- package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
- package/dist/assets/Panel-n-p8OQLc.js +0 -1
- package/dist/assets/Plugins-D1ckVgo3.js +0 -2
- package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
- package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
- package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
- package/dist/assets/Settings-DlyJjexf.js +0 -3
- package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
- package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
- package/dist/assets/Toggle-BJHhHQOq.js +0 -1
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scheduler-mcp-server.cjs — stdio MCP server wrapping PRD 448's loopback
|
|
4
|
+
* admin HTTP API (src/main/lib/localAdminHttp.cjs). Exposes two tools:
|
|
5
|
+
*
|
|
6
|
+
* scheduler_reset_job({ slug }) -> POST /admin/scheduler/reset-job
|
|
7
|
+
* scheduler_list_jobs() -> GET /admin/scheduler/jobs
|
|
8
|
+
*
|
|
9
|
+
* scheduler_create_prd({ ... }) -> POST /admin/scheduler/create-prd
|
|
10
|
+
*
|
|
11
|
+
* feedback_list_projects() -> GET /admin/feedback/targets
|
|
12
|
+
* feedback_open_session({ ... }) -> POST /admin/feedback/open-session
|
|
13
|
+
*
|
|
14
|
+
* This is a separate process from the Electron app — it only ever reaches
|
|
15
|
+
* it over the token-authed loopback HTTP API in admin-api.json, never by
|
|
16
|
+
* requiring scheduler.cjs/localAdminHttp.cjs directly. The admin server IS the
|
|
17
|
+
* security boundary; this file stays on the client side of it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const fsp = require('node:fs/promises');
|
|
25
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
26
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
27
|
+
const {
|
|
28
|
+
ListToolsRequestSchema,
|
|
29
|
+
CallToolRequestSchema,
|
|
30
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
31
|
+
const { PRD_WORK_TYPES } = require('../src/main/lib/workTypeLibrary.cjs');
|
|
32
|
+
|
|
33
|
+
const TOKEN_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'admin-api.json');
|
|
34
|
+
|
|
35
|
+
const NOT_RUNNING_ERROR =
|
|
36
|
+
'session-manager app is not running (admin API unreachable) — start it first';
|
|
37
|
+
|
|
38
|
+
async function readAdminConfig() {
|
|
39
|
+
const raw = await fsp.readFile(TOKEN_PATH, 'utf8');
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if (!parsed || typeof parsed.port !== 'number' || typeof parsed.token !== 'string') {
|
|
42
|
+
throw new Error(NOT_RUNNING_ERROR);
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function adminRequest(method, urlPath, body) {
|
|
48
|
+
let port, token;
|
|
49
|
+
try {
|
|
50
|
+
({ port, token } = await readAdminConfig());
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(NOT_RUNNING_ERROR);
|
|
53
|
+
}
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(`http://127.0.0.1:${port}${urlPath}`, {
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
61
|
+
},
|
|
62
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(NOT_RUNNING_ERROR);
|
|
66
|
+
}
|
|
67
|
+
const json = await res.json();
|
|
68
|
+
return json;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const TOOLS = [
|
|
72
|
+
{
|
|
73
|
+
name: 'scheduler_reset_job',
|
|
74
|
+
description: "Reset a stuck scheduler job by slug via the session-manager app's admin API. "
|
|
75
|
+
+ 'Refuses a job whose status is already "completed" unless force:true is passed — resetting '
|
|
76
|
+
+ 'a completed job re-executes already-shipped work.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
slug: { type: 'string', description: 'PRD slug of the job to reset' },
|
|
81
|
+
force: { type: 'boolean', description: 'Required to reset a job whose status is already "completed"' },
|
|
82
|
+
},
|
|
83
|
+
required: ['slug'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'scheduler_list_jobs',
|
|
88
|
+
description: "List scheduler jobs via the session-manager app's admin API.",
|
|
89
|
+
inputSchema: { type: 'object', properties: {} },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'scheduler_create_prd',
|
|
93
|
+
description:
|
|
94
|
+
"THE ONLY SANCTIONED WAY to author a PRD. Write a new PRD file via the session-manager "
|
|
95
|
+
+ "app's admin API. Server-side validates the frontmatter, atomically allocates the NN "
|
|
96
|
+
+ 'parallel-group number, appends the engineering standards, and writes the PRD file to '
|
|
97
|
+
+ "disk. This tool ONLY writes the file — it does not create a scheduler queue row. The "
|
|
98
|
+
+ 'queue row is derived automatically by the scheduler\'s next reconcile pass (typically '
|
|
99
|
+
+ 'within ~1 minute); the response has `enqueued: false` for exactly this reason. Every '
|
|
100
|
+
+ 'PRD must join an EXISTING, already-human-approved Epic (pass sourcePromptId) — this '
|
|
101
|
+
+ 'tool never mints a new one, and refuses the write if no Epic can be resolved. '
|
|
102
|
+
+ 'TWO DISTINCT FAILURE MODES if this tool is not usable — do not conflate them: '
|
|
103
|
+
+ '(a) this tool call is PRESENT in your tool list but ERRORS as app-not-running / admin '
|
|
104
|
+
+ 'API unreachable — that is the ONLY case where hand-authoring the PRD file directly on '
|
|
105
|
+
+ 'disk is an acceptable DEGRADED, LAST-RESORT fallback; the caller MUST say so explicitly '
|
|
106
|
+
+ 'and visibly in its report (which file, why the tool was unreachable, that it needs '
|
|
107
|
+
+ 'verification) since the server-side validation, atomic NN allocation, and '
|
|
108
|
+
+ 'Epic-existence check this tool performs did not run for that file. '
|
|
109
|
+
+ '(b) this tool is ABSENT from your tool list entirely — you were never offered it, so '
|
|
110
|
+
+ 'there is no error to catch. That means the session-manager-scheduler MCP server is not '
|
|
111
|
+
+ 'registered for this project: a MISCONFIGURATION, not an offline app. In that case DO '
|
|
112
|
+
+ 'NOT hand-write any PRD file — stop and tell the human the MCP server is not registered '
|
|
113
|
+
+ '(fix: `claude mcp add session-manager-scheduler --scope user -- node '
|
|
114
|
+
+ '<session-manager-repo>/scripts/scheduler-mcp-server.cjs`, once at user scope covers '
|
|
115
|
+
+ 'every project). See /develop.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
title: { type: 'string', description: 'One-line human-readable title' },
|
|
120
|
+
cwd: { type: 'string', description: 'Absolute path to the target project (where claude -p will run)' },
|
|
121
|
+
estimateMinutes: { type: 'number', description: 'Integer wall-clock estimate in minutes' },
|
|
122
|
+
goal: { type: 'string', description: '2-4 sentences: what the executor will build and why' },
|
|
123
|
+
acceptanceCriteria: {
|
|
124
|
+
type: 'array', items: { type: 'string' },
|
|
125
|
+
description: 'Each entry is one verifiable checklist line',
|
|
126
|
+
},
|
|
127
|
+
implementationNotes: { type: 'string', description: 'File paths, patterns, and constraints the executor needs' },
|
|
128
|
+
outOfScope: { type: 'array', items: { type: 'string' }, description: 'Optional: what NOT to build' },
|
|
129
|
+
slug: { type: 'string', description: 'Optional kebab-case slug; derived from title if omitted' },
|
|
130
|
+
parallelGroup: { type: 'number', description: 'DEPRECATED and ignored (PRD 832): numbers are strictly unique per project; use dependsOn for ordering' },
|
|
131
|
+
dependsOn: { type: 'array', items: { type: 'string' }, description: 'Optional: PRD slugs that must complete before this one becomes eligible (replaces the retired shared-NN-parallel convention)' },
|
|
132
|
+
sourcePromptId: { type: 'string', description: "Effectively required: an EXISTING, already-human-approved Epic's promptSessionId (the id shown in the Epics list / active-index.json sessions key) to join — NOT a PromptTicket.id (those are distinct fields). The server never mints a new Epic; if this is omitted and no Epic can be resolved (via the SM_CHAT_SESSION_ID fallback below), the write is refused with an error telling the caller to create/approve an Epic first." },
|
|
133
|
+
sourceTabId: { type: 'string', description: 'Optional: tab id (claudeSessionId) this PRD was queued from, so the scheduler can route a completion status prompt back to it' },
|
|
134
|
+
tag: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
enum: PRD_WORK_TYPES,
|
|
137
|
+
description: 'Optional: the work type of THIS PRD — independent of the parent Epic\'s own tag. An Epic is the plan; a PRD is one unit of work inside it, and a single plan may legitimately contain several different work types. Never derived or inherited from the Epic.',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ['title', 'cwd', 'estimateMinutes', 'goal', 'acceptanceCriteria', 'implementationNotes'],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'scheduler_list_prds',
|
|
145
|
+
description: "THE ONLY SUPPORTED WAY to list scheduled PRDs (live + archived) via the session-manager app's admin API. "
|
|
146
|
+
+ 'Each entry includes its real job status (pending/running/completed/failed/needs_review, or null if not yet '
|
|
147
|
+
+ 'queued/reconciled). Optionally filter by project cwd, Epic id, and/or status.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
cwd: { type: 'string', description: 'Optional: only PRDs targeting this project cwd' },
|
|
152
|
+
epicId: { type: 'string', description: 'Optional: only PRDs belonging to this Epic id' },
|
|
153
|
+
status: { type: 'string', description: 'Optional: only PRDs whose job status equals this value' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'scheduler_get_prd',
|
|
159
|
+
description: "THE ONLY SUPPORTED WAY to read one PRD's full body + parsed frontmatter (live or archived) via the "
|
|
160
|
+
+ "session-manager app's admin API.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
slug: { type: 'string', description: 'PRD slug to read' },
|
|
165
|
+
cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
|
|
166
|
+
},
|
|
167
|
+
required: ['slug'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'scheduler_update_prd',
|
|
172
|
+
description: "THE ONLY SUPPORTED WAY to edit a NOT-yet-running PRD's frontmatter and/or body via the session-manager "
|
|
173
|
+
+ 'app\'s admin API. Refuses once a queue row exists for the slug and its status is anything but "pending" '
|
|
174
|
+
+ '(running/completed/failed/needs_review) — editing the spec under a live or already-finished executor is refused, '
|
|
175
|
+
+ 'not silently applied. Only recognized frontmatter keys (title, cwd, estimateMinutes, parallelGroup, '
|
|
176
|
+
+ 'sourcePromptId, sourceTabId, tag) may be patched; unrecognized keys (e.g. dependsOn) round-trip unchanged.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
slug: { type: 'string', description: 'PRD slug to edit' },
|
|
181
|
+
cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
|
|
182
|
+
frontmatter: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
description: 'Partial frontmatter patch — only the keys to change',
|
|
185
|
+
properties: {
|
|
186
|
+
title: { type: 'string' },
|
|
187
|
+
cwd: { type: 'string' },
|
|
188
|
+
estimateMinutes: { type: 'number' },
|
|
189
|
+
parallelGroup: { type: 'number' },
|
|
190
|
+
sourcePromptId: { type: 'string' },
|
|
191
|
+
sourceTabId: { type: 'string' },
|
|
192
|
+
tag: { type: 'string', enum: PRD_WORK_TYPES },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
body: { type: 'string', description: 'Optional: full replacement body (everything after the frontmatter)' },
|
|
196
|
+
},
|
|
197
|
+
required: ['slug'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'scheduler_archive_prd',
|
|
202
|
+
description: "THE ONLY SUPPORTED WAY to archive one or more PRDs (move to prds-archived/) via the session-manager "
|
|
203
|
+
+ "app's admin API.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
slugs: { type: 'array', items: { type: 'string' }, description: 'Slugs to archive' },
|
|
208
|
+
},
|
|
209
|
+
required: ['slugs'],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'scheduler_cancel_job',
|
|
214
|
+
description: "THE ONLY SUPPORTED WAY to cancel a not-yet-terminal scheduler job via the session-manager app's admin "
|
|
215
|
+
+ 'API. A running job is SIGTERM\'d; a pending job is simply retired. There is no "cancelled" job status, so a '
|
|
216
|
+
+ 'cancelled job lands as "failed" with an error naming the cause. Refuses a slug whose job is already terminal '
|
|
217
|
+
+ '(completed/failed/needs_review) — nothing to cancel.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
slug: { type: 'string', description: 'PRD slug of the job to cancel' },
|
|
222
|
+
},
|
|
223
|
+
required: ['slug'],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'scheduler_retag_prd',
|
|
228
|
+
description: "THE ONLY SUPPORTED WAY to rewrite a PRD's parallelGroup and/or estimateMinutes frontmatter (and, if "
|
|
229
|
+
+ "parallelGroup changes, its NN- filename prefix) via the session-manager app's admin API.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
items: {
|
|
234
|
+
type: 'array',
|
|
235
|
+
description: 'One or more retag operations',
|
|
236
|
+
items: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
slug: { type: 'string' },
|
|
240
|
+
parallelGroup: { type: 'number' },
|
|
241
|
+
estimateMinutes: { type: 'number' },
|
|
242
|
+
},
|
|
243
|
+
required: ['slug'],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
required: ['items'],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'chat_send_prompt',
|
|
252
|
+
description:
|
|
253
|
+
"Push a prompt into an already-open tab's chat queue via the session-manager app's admin "
|
|
254
|
+
+ 'API. The renderer resolves the tab (must currently be open) and runs the prompt through '
|
|
255
|
+
+ 'the same queued-vs-immediate path as a manual send. No-ops if the tab is unknown/closed.',
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
tabId: { type: 'string', description: 'Target tab id (claudeSessionId) that must already be open in the app' },
|
|
260
|
+
prompt: { type: 'string', description: 'Prompt text to enqueue' },
|
|
261
|
+
},
|
|
262
|
+
required: ['tabId', 'prompt'],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'feedback_list_projects',
|
|
267
|
+
description:
|
|
268
|
+
'List the OTHER projects on this machine that can receive feedback (i.e. that Session Manager '
|
|
269
|
+
+ 'already manages — they have a session-manager-operations/ directory). Call this FIRST when you '
|
|
270
|
+
+ 'need the exact `toCwd` for feedback_open_session and are not certain of it — never guess a path. '
|
|
271
|
+
+ 'A project missing from this list has simply never been opened in Session Manager; ask the human '
|
|
272
|
+
+ 'to open it once rather than inventing a path.',
|
|
273
|
+
inputSchema: { type: 'object', properties: {} },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'feedback_open_session',
|
|
277
|
+
description:
|
|
278
|
+
'THE ONLY SANCTIONED WAY to hand a finding from THIS project to a DIFFERENT project. Opens a new '
|
|
279
|
+
+ "PROPOSED session in the receiving project's own Sessions queue, carrying your report as its "
|
|
280
|
+
+ 'opening prompt and stamped with where it came from. Session Manager performs the cross-folder '
|
|
281
|
+
+ 'write; you never write another project\'s files yourself. '
|
|
282
|
+
+ 'WHAT THIS DOES NOT DO: it does not start anything, queue a PRD, or spend a token. The session '
|
|
283
|
+
+ 'lands as `proposed` and runs only if a human in the RECEIVING project presses "Approve & start". '
|
|
284
|
+
+ 'There is no callback and no reply channel — do not wait for an answer, and do not tell the user '
|
|
285
|
+
+ 'the other project has "been fixed" or "is working on it". Report only that the proposal was '
|
|
286
|
+
+ 'delivered. '
|
|
287
|
+
+ 'WHEN NOT TO USE IT: for work in the project you are ALREADY in, run /develop inside the Epic you '
|
|
288
|
+
+ 'are already in — this tool refuses toCwd === fromCwd outright. '
|
|
289
|
+
+ 'Write the report for a reader who has never seen your project: state the symptom, where you '
|
|
290
|
+
+ 'observed it, what you expected, and (if you know) the file in THEIR repo that looks responsible. '
|
|
291
|
+
+ 'Never assume they can see your code.',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {
|
|
295
|
+
toCwd: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description: 'Absolute cwd of the RECEIVING project. Must already be a Session Manager project — use feedback_list_projects if unsure.',
|
|
298
|
+
},
|
|
299
|
+
fromCwd: {
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'Absolute cwd of YOUR project (the sender). Must differ from toCwd.',
|
|
302
|
+
},
|
|
303
|
+
title: { type: 'string', description: 'One line naming the finding — becomes the receiving queue row' },
|
|
304
|
+
body: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'The report itself: symptom, where observed, expected behavior, suspected cause. Self-contained — the reader cannot see your project.',
|
|
307
|
+
},
|
|
308
|
+
tag: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
enum: ['bug', 'feature', 'discussion'],
|
|
311
|
+
description: "Mission tag for the receiving session. Defaults to 'discussion' — the tag that keeps /develop available but never assumed, correct for a finding the receiving project has not yet agreed with.",
|
|
312
|
+
},
|
|
313
|
+
fromEpicId: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'Optional: your own Epic/session id, so a receipt is chained onto your session and the receiving human can see which session is asking. Auto-filled from this process when omitted.',
|
|
316
|
+
},
|
|
317
|
+
agentType: { type: 'string', description: "Optional persona for the receiving session. Defaults to 'architect'." },
|
|
318
|
+
referencePaths: {
|
|
319
|
+
type: 'array', items: { type: 'string' },
|
|
320
|
+
description: 'Optional absolute paths worth reading, appended as Reference lines',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ['toCwd', 'fromCwd', 'title', 'body'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const server = new Server(
|
|
329
|
+
{ name: 'session-manager-scheduler', version: '1.0.0' },
|
|
330
|
+
{ capabilities: { tools: {} } }
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
334
|
+
|
|
335
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
336
|
+
const { name, arguments: args } = request.params;
|
|
337
|
+
try {
|
|
338
|
+
if (name === 'scheduler_reset_job') {
|
|
339
|
+
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
340
|
+
if (!slug) {
|
|
341
|
+
return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
|
|
342
|
+
}
|
|
343
|
+
const force = args && args.force === true;
|
|
344
|
+
const result = await adminRequest('POST', '/admin/scheduler/reset-job', { slug, force });
|
|
345
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
346
|
+
}
|
|
347
|
+
if (name === 'scheduler_list_jobs') {
|
|
348
|
+
const result = await adminRequest('GET', '/admin/scheduler/jobs');
|
|
349
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
350
|
+
}
|
|
351
|
+
if (name === 'scheduler_list_prds') {
|
|
352
|
+
const qs = new URLSearchParams();
|
|
353
|
+
if (args?.cwd) qs.set('cwd', args.cwd);
|
|
354
|
+
if (args?.epicId) qs.set('epicId', args.epicId);
|
|
355
|
+
if (args?.status) qs.set('status', args.status);
|
|
356
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
|
357
|
+
const result = await adminRequest('GET', `/admin/scheduler/prds${suffix}`);
|
|
358
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
359
|
+
}
|
|
360
|
+
if (name === 'scheduler_get_prd') {
|
|
361
|
+
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
362
|
+
if (!slug) {
|
|
363
|
+
return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
|
|
364
|
+
}
|
|
365
|
+
const qs = new URLSearchParams({ slug });
|
|
366
|
+
if (args?.cwd) qs.set('cwd', args.cwd);
|
|
367
|
+
const result = await adminRequest('GET', `/admin/scheduler/prd?${qs.toString()}`);
|
|
368
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
369
|
+
}
|
|
370
|
+
if (name === 'scheduler_update_prd') {
|
|
371
|
+
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
372
|
+
if (!slug) {
|
|
373
|
+
return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
|
|
374
|
+
}
|
|
375
|
+
const result = await adminRequest('POST', '/admin/scheduler/update-prd', args);
|
|
376
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
377
|
+
}
|
|
378
|
+
if (name === 'scheduler_archive_prd') {
|
|
379
|
+
const slugs = Array.isArray(args?.slugs) ? args.slugs : null;
|
|
380
|
+
if (!slugs || slugs.length === 0) {
|
|
381
|
+
return { content: [{ type: 'text', text: 'missing required argument: slugs' }], isError: true };
|
|
382
|
+
}
|
|
383
|
+
const result = await adminRequest('POST', '/admin/scheduler/archive-prd', { slugs });
|
|
384
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
385
|
+
}
|
|
386
|
+
if (name === 'scheduler_cancel_job') {
|
|
387
|
+
const slug = args && typeof args.slug === 'string' ? args.slug : null;
|
|
388
|
+
if (!slug) {
|
|
389
|
+
return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
|
|
390
|
+
}
|
|
391
|
+
const result = await adminRequest('POST', '/admin/scheduler/cancel-job', { slug });
|
|
392
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
393
|
+
}
|
|
394
|
+
if (name === 'scheduler_retag_prd') {
|
|
395
|
+
const items = Array.isArray(args?.items) ? args.items : null;
|
|
396
|
+
if (!items || items.length === 0) {
|
|
397
|
+
return { content: [{ type: 'text', text: 'missing required argument: items' }], isError: true };
|
|
398
|
+
}
|
|
399
|
+
const result = await adminRequest('POST', '/admin/scheduler/retag-prd', { items });
|
|
400
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
401
|
+
}
|
|
402
|
+
if (name === 'scheduler_create_prd') {
|
|
403
|
+
// If the caller (the model) didn't pass sourcePromptId, forward this
|
|
404
|
+
// process's own SM_CHAT_SESSION_ID (set by chatRunner.cjs on the
|
|
405
|
+
// parent claude -p process this MCP server inherited its env from) so
|
|
406
|
+
// the admin route can auto-resolve it to the calling Epic and join
|
|
407
|
+
// that Epic instead of silently minting an unrelated sibling one — see
|
|
408
|
+
// prdCreate.cjs's resolveSourcePromptIdFromClaudeSession.
|
|
409
|
+
const payload = { ...args };
|
|
410
|
+
if (!payload.sourcePromptId && process.env.SM_CHAT_SESSION_ID) {
|
|
411
|
+
payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
|
|
412
|
+
}
|
|
413
|
+
const result = await adminRequest('POST', '/admin/scheduler/create-prd', payload);
|
|
414
|
+
// Never say "queued" — this tool only writes the PRD file; the queue
|
|
415
|
+
// row is derived by the scheduler's next reconcile pass, not by this
|
|
416
|
+
// call. Say so plainly alongside the raw JSON.
|
|
417
|
+
const note = result?.ok !== false
|
|
418
|
+
? ' — PRD file written; the queue row is derived on the next scheduler reconcile pass, not by this call.'
|
|
419
|
+
: '';
|
|
420
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) + note }] };
|
|
421
|
+
}
|
|
422
|
+
if (name === 'feedback_list_projects') {
|
|
423
|
+
const result = await adminRequest('GET', '/admin/feedback/targets');
|
|
424
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
425
|
+
}
|
|
426
|
+
if (name === 'feedback_open_session') {
|
|
427
|
+
for (const key of ['toCwd', 'fromCwd', 'title', 'body']) {
|
|
428
|
+
if (!args || typeof args[key] !== 'string' || !args[key].trim()) {
|
|
429
|
+
return { content: [{ type: 'text', text: `missing required argument: ${key}` }], isError: true };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Same fallback shape as scheduler_create_prd: forward this process's
|
|
433
|
+
// inherited claude session id so the server can resolve the SENDING
|
|
434
|
+
// Epic and chain a receipt onto it. Never invents an Epic — an
|
|
435
|
+
// unresolvable id just means no receipt.
|
|
436
|
+
const payload = { ...args };
|
|
437
|
+
if (!payload.fromEpicId && process.env.SM_CHAT_SESSION_ID) {
|
|
438
|
+
payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
|
|
439
|
+
}
|
|
440
|
+
const result = await adminRequest('POST', '/admin/feedback/open-session', payload);
|
|
441
|
+
// Never say "sent", "filed" or "fixed" — this call delivers a PROPOSAL
|
|
442
|
+
// that a human in the other project must still approve.
|
|
443
|
+
const note = result?.ok !== false
|
|
444
|
+
? ' — delivered as a PROPOSED session in the receiving project; it runs only if a human there presses Approve & start. No reply channel.'
|
|
445
|
+
: '';
|
|
446
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) + note }] };
|
|
447
|
+
}
|
|
448
|
+
if (name === 'chat_send_prompt') {
|
|
449
|
+
const tabId = args && typeof args.tabId === 'string' ? args.tabId : null;
|
|
450
|
+
const prompt = args && typeof args.prompt === 'string' ? args.prompt : null;
|
|
451
|
+
if (!tabId || !prompt) {
|
|
452
|
+
return { content: [{ type: 'text', text: 'missing required arguments: tabId, prompt' }], isError: true };
|
|
453
|
+
}
|
|
454
|
+
const result = await adminRequest('POST', '/admin/chat/send-prompt', { tabId, prompt });
|
|
455
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
456
|
+
}
|
|
457
|
+
return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return { content: [{ type: 'text', text: e?.message ?? String(e) }], isError: true };
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
async function main() {
|
|
464
|
+
const transport = new StdioServerTransport();
|
|
465
|
+
await server.connect(transport);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
main().catch((e) => {
|
|
469
|
+
process.stderr.write(`scheduler-mcp-server fatal: ${e?.stack ?? e}\n`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-autofix-outcome.test.cjs — self-heal terminal-state gap fix.
|
|
3
|
+
*
|
|
4
|
+
* spawnInvestigation's onExit used to stamp autoFixOutcome only in the
|
|
5
|
+
* no-plan branch; a job whose probe erred, spawn-failed, or produced a plan
|
|
6
|
+
* that never became a queue row ended up autoFixAttempted:true with no
|
|
7
|
+
* outcome — permanently excluded from both retry AND the exhausted-retry
|
|
8
|
+
* annotation. This file is a vitest-runnable (npm run test:unit) home for
|
|
9
|
+
* the scenarios the PRD's acceptance criteria enumerate; the pure-logic
|
|
10
|
+
* coverage in scheduler-autofix-select.test.cjs (node --test) exercises the
|
|
11
|
+
* same helpers in more granular detail.
|
|
12
|
+
*
|
|
13
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-autofix-outcome.test.cjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
import { test, expect } from 'vitest';
|
|
19
|
+
const {
|
|
20
|
+
selectAutoFixTargets, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor,
|
|
21
|
+
} = require('../scheduler.cjs');
|
|
22
|
+
|
|
23
|
+
const noSiblingOnDisk = () => false;
|
|
24
|
+
|
|
25
|
+
function makeJob(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
slug: '05-my-feature',
|
|
28
|
+
status: 'needs_review',
|
|
29
|
+
runId: '2026-06-16T10-00-00-000Z',
|
|
30
|
+
parallelGroup: 5,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isTarget(job) {
|
|
36
|
+
return selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).some((j) => j.slug === job.slug);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isAnnotatable(job, queuedSlugs = new Set()) {
|
|
40
|
+
return isExhaustedAutoFix(job) || isPlanUnqueued(job, queuedSlugs);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// (a) attempted + no outcome → eligible once, then annotated when the retry is spent.
|
|
44
|
+
test('(a) attempted with unstamped outcome and an unspent retry is eligible, not annotated', () => {
|
|
45
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 0 });
|
|
46
|
+
expect(isTarget(job)).toBe(true);
|
|
47
|
+
expect(isAnnotatable(job)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('(a) attempted with unstamped outcome and a spent retry is annotated, not eligible', () => {
|
|
51
|
+
const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
|
|
52
|
+
expect(isTarget(job)).toBe(false);
|
|
53
|
+
expect(isAnnotatable(job)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// (b) outcome 'error' → eligible once.
|
|
57
|
+
test("(b) outcome 'error' with an unspent retry is eligible", () => {
|
|
58
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 0 });
|
|
59
|
+
expect(isTarget(job)).toBe(true);
|
|
60
|
+
expect(isAnnotatable(job)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("(b) outcome 'error' with a spent retry is annotated, not eligible", () => {
|
|
64
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 });
|
|
65
|
+
expect(isTarget(job)).toBe(false);
|
|
66
|
+
expect(isAnnotatable(job)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// (c) outcome 'plan' with no queue row → annotated autofix_plan_unqueued, NOT retried.
|
|
70
|
+
test("(c) outcome 'plan' with the fix slug absent from the queue is annotated, never retried", () => {
|
|
71
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0 });
|
|
72
|
+
expect(isTarget(job)).toBe(false);
|
|
73
|
+
expect(isPlanUnqueued(job, new Set())).toBe(true);
|
|
74
|
+
expect(isExhaustedAutoFix(job)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// (d) outcome 'plan' WITH a queue row → neither retried nor annotated.
|
|
78
|
+
test("(d) outcome 'plan' with the fix slug present in the queue is neither retried nor annotated", () => {
|
|
79
|
+
const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0 });
|
|
80
|
+
const queuedSlugs = new Set([fixSlugFor(job)]);
|
|
81
|
+
expect(isTarget(job)).toBe(false);
|
|
82
|
+
expect(isAnnotatable(job, queuedSlugs)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// (invariant) every needs_review job is either an auto-fix target or
|
|
86
|
+
// annotatable (would receive a non-null verifierVerdict) — never both false.
|
|
87
|
+
test('invariant: fresh, retry-eligible, exhausted, and plan-unqueued jobs are always selected or annotatable', () => {
|
|
88
|
+
const jobs = [
|
|
89
|
+
makeJob({ slug: '01-fresh' }),
|
|
90
|
+
makeJob({ slug: '02-unstamped-retry', autoFixAttempted: true, autoFixRetries: 0 }),
|
|
91
|
+
makeJob({ slug: '03-unstamped-exhausted', autoFixAttempted: true, autoFixRetries: 1 }),
|
|
92
|
+
makeJob({ slug: '04-error-retry', autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 0 }),
|
|
93
|
+
makeJob({ slug: '05-error-exhausted', autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 }),
|
|
94
|
+
makeJob({ slug: '06-plan-unqueued', autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0 }),
|
|
95
|
+
];
|
|
96
|
+
const planJob = jobs.find((j) => j.slug === '06-plan-unqueued');
|
|
97
|
+
const planQueuedJob = makeJob({
|
|
98
|
+
slug: '07-plan-queued', parallelGroup: 7, autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0,
|
|
99
|
+
});
|
|
100
|
+
jobs.push(planQueuedJob);
|
|
101
|
+
const queuedSlugs = new Set([fixSlugFor(planQueuedJob)]);
|
|
102
|
+
|
|
103
|
+
const targets = new Set(selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk }).map((j) => j.slug));
|
|
104
|
+
for (const job of jobs) {
|
|
105
|
+
const selected = targets.has(job.slug);
|
|
106
|
+
const annotated = isAnnotatable(job, queuedSlugs);
|
|
107
|
+
if (job.slug === '07-plan-queued') {
|
|
108
|
+
// The good path: waiting on the fix-plan job itself, no action here.
|
|
109
|
+
expect(selected).toBe(false);
|
|
110
|
+
expect(annotated).toBe(false);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
expect(selected || annotated).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
expect(isPlanUnqueued(planJob, queuedSlugs)).toBe(true);
|
|
116
|
+
});
|