claude-code-session-manager 0.73.2 → 0.75.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-SpzptNyq.js +3 -0
- package/dist/assets/DataModel-C4M9xVLC.js +1 -0
- package/dist/assets/{History-Dnx86P4n.js → History-92FYrZKS.js} +2 -2
- package/dist/assets/{Hooks-CAYlpqBp.js → Hooks-DlnavO1l.js} +2 -2
- package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
- package/dist/assets/{Library-DVbXlboP.js → Library-Brhxe7xa.js} +1 -1
- package/dist/assets/ListDetail-DmIweYTN.js +1 -0
- package/dist/assets/MarkdownEditor-DA4xqWWi.js +1 -0
- package/dist/assets/{McpServers-BxUk1aR3.js → McpServers-DWXQGzVc.js} +2 -2
- package/dist/assets/{Memory-DVbPQFZw.js → Memory-HCx9Arao.js} +4 -4
- package/dist/assets/Panel-B20C6kN8.js +1 -0
- package/dist/assets/{Permissions-CSLxAoTL.js → Permissions-C4sPgLpA.js} +3 -3
- package/dist/assets/Plugins-CaRenCBg.js +2 -0
- package/dist/assets/ProvenanceBadge-DfrXzTeh.js +1 -0
- package/dist/assets/SaveBar-BmpUx2Yh.js +1 -0
- package/dist/assets/{Scheduler-CrBVVXN1.js → Scheduler-xUD8pQ6q.js} +7 -7
- package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
- package/dist/assets/Settings-C457mxOK.js +3 -0
- package/dist/assets/{SkillReferenceGraph-B1DaeT2R.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
- package/dist/assets/{Skills-CSdxEC-f.js → Skills-BNDlbftC.js} +2 -2
- package/dist/assets/SystemPrompt-d-ykNRec.js +1 -0
- package/dist/assets/TagLibrary-D3JWkQK-.js +1 -0
- package/dist/assets/{TiptapBody-4hzD0jjv.js → TiptapBody-B1Ox6D6E.js} +1 -1
- package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
- package/dist/assets/{index-DJ6xdB0r.js → index-CEeLxNCz.js} +659 -655
- package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
- package/dist/assets/{settingsSchema-DVblsXFp.js → settingsSchema-DUkd45M5.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +21 -1
- package/scripts/scheduler-mcp-server.cjs +471 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +19 -0
- package/src/main/__tests__/crossProjectFeedback.test.cjs +7 -0
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +7 -0
- package/src/main/__tests__/epicMint.test.cjs +50 -2
- package/src/main/__tests__/health-claude-md-budget.test.cjs +57 -0
- package/src/main/__tests__/prdCreate.test.cjs +17 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +7 -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/__tests__/workTypeLibrary.test.cjs +89 -0
- package/src/main/agentLibrary.cjs +43 -0
- package/src/main/health.cjs +51 -1
- package/src/main/index.cjs +44 -3
- package/src/main/ipcSchemas.cjs +46 -9
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +67 -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/agentModelResolve.cjs +32 -1
- package/src/main/lib/agentPersonaSchema.cjs +82 -0
- package/src/main/lib/delegationReadiness.cjs +141 -0
- package/src/main/lib/epicDelegationStats.cjs +124 -0
- package/src/main/lib/epicMint.cjs +74 -1
- package/src/main/lib/prdFrontmatter.cjs +6 -5
- package/src/main/lib/promptSessionSchema.cjs +7 -15
- package/src/main/lib/workTypeLibrary.cjs +48 -0
- 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 +47 -3
- 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-D0frFrHG.js +0 -3
- package/dist/assets/HostBilko-CXykS2ap.js +0 -1
- package/dist/assets/ListDetail-DXd5rB2F.js +0 -1
- package/dist/assets/MarkdownEditor-D0mO1TUZ.js +0 -1
- package/dist/assets/Panel-DigZB9Yd.js +0 -1
- package/dist/assets/Plugins-DOTiP9sQ.js +0 -2
- package/dist/assets/ProvenanceBadge-PsNd-CBq.js +0 -1
- package/dist/assets/SaveBar-C4bgDklX.js +0 -1
- package/dist/assets/ScopeSwitcher-RMAwgmj4.js +0 -1
- package/dist/assets/Settings-CM4lZxnn.js +0 -3
- package/dist/assets/SystemPrompt-j-WYOC5f.js +0 -1
- package/dist/assets/TagLibrary-Fj0zrPru.js +0 -1
- package/dist/assets/Toggle-H6pNcv3l.js +0 -1
|
@@ -60,6 +60,20 @@ no matter how old.
|
|
|
60
60
|
process/routing rule for a specific skill → that skill's `SKILL.md`). Write it into the
|
|
61
61
|
target file first, in that file's own voice and section, then delete the memory. Two
|
|
62
62
|
overlapping memories covering the same territory fold into ONE rule, not two.
|
|
63
|
+
|
|
64
|
+
**FOLD MUST NOT GROW AN ALWAYS-LOADED FILE.** A `CLAUDE.md` is read on every turn of every
|
|
65
|
+
session; this skill is the only process that routinely writes *into* one, which makes it a
|
|
66
|
+
one-way ratchet — memory gets a sanitation pass, `CLAUDE.md` becomes its landfill, and nothing
|
|
67
|
+
ever evicts from the landfill. So:
|
|
68
|
+
- If the target `CLAUDE.md` declares a **size budget**, honour it. Check `wc -c` first. If the
|
|
69
|
+
fold would breach it, fold the *rule* into `CLAUDE.md` as one line and its rationale into
|
|
70
|
+
whichever linked reference doc that file points at — never append the full memory body.
|
|
71
|
+
- Prefer the **narrowest** always-loaded target: a specific `SKILL.md` over project
|
|
72
|
+
`CLAUDE.md`, and project over global. Only a genuinely cross-project rule earns the global file.
|
|
73
|
+
- A fold that adds more than ~2 lines to a `CLAUDE.md` is a signal you're moving narrative, not
|
|
74
|
+
a law. Compress to the imperative rule and drop the history.
|
|
75
|
+
- Report the net char delta per instruction file you touched in the step-7 summary, so a run
|
|
76
|
+
that quietly added 3k to an always-loaded file is visible rather than counted as "cleanup".
|
|
63
77
|
- **TRIM.** The memory mixes durable facts with dated, closed narrative (a deploy log, a
|
|
64
78
|
PRD-tracking blow-by-blow, "verified live" status updates). Keep only what's still true
|
|
65
79
|
and not written down elsewhere; cut the rest. Re-verify any code claim you keep (grep it).
|
|
@@ -101,10 +115,16 @@ caller can parse it without scraping prose:
|
|
|
101
115
|
"deleted": [{ "name": "feedback_fix_dont_ask", "reason": "baked into CLAUDE.md + develop/SKILL.md" }],
|
|
102
116
|
"folded": [{ "name": "feedback_nav_microservices", "into": "CLAUDE.md ## Avoid", "reason": "merged with feedback_extend_dont_add_tabs" }],
|
|
103
117
|
"trimmed": [{ "name": "web_remote_v2_mobile", "reason": "PRDs landed; cut deploy-log narrative" }],
|
|
104
|
-
"kept": [{ "name": "no_schedule_self_e2e", "reason": "active, tracks open PRD" }]
|
|
118
|
+
"kept": [{ "name": "no_schedule_self_e2e", "reason": "active, tracks open PRD" }],
|
|
119
|
+
"instructionFileDelta": [{ "file": "CLAUDE.md", "charsBefore": 11576, "charsAfter": 11702, "delta": 126, "withinBudget": true }]
|
|
105
120
|
}
|
|
106
121
|
```
|
|
107
122
|
|
|
123
|
+
`instructionFileDelta` is required whenever the run wrote to any always-loaded instruction file
|
|
124
|
+
(`CLAUDE.md`, a `SKILL.md`). It is what makes a FOLD-heavy run that grew an always-loaded file by
|
|
125
|
+
3k visible instead of counted as "cleanup" — see the FOLD rule above. Set `withinBudget: false`
|
|
126
|
+
(and say so in the prose summary) if the file declares a size budget and the run breached it.
|
|
127
|
+
|
|
108
128
|
Still print the normal human-readable summary too — the JSON block is additive, not a
|
|
109
129
|
replacement.
|
|
110
130
|
|
|
@@ -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
|
+
});
|
|
@@ -117,3 +117,22 @@ test('readPersonaModel returns null (never throws) for a path-traversal agentTyp
|
|
|
117
117
|
};
|
|
118
118
|
expect(readPersonaModel('../../../etc/passwd', { globalDir, validatePath: realValidatePath })).toBeNull();
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
// READ side of the agentType FK: a persona that was valid when the Epic was
|
|
122
|
+
// created but got deleted afterward (rename/deletion in Agent Library) must
|
|
123
|
+
// still resolve to null, not throw — the WRITE side (epicMint.cjs's
|
|
124
|
+
// ensureEpic) is what refuses a bad reference at creation time.
|
|
125
|
+
test('readPersonaModel returns null (does not throw) for a persona deleted after the Epic was created, and logs it once via opsErrorLog', async () => {
|
|
126
|
+
const cwd = await mkTmpDir('sm-agentmodel-cwd-');
|
|
127
|
+
const globalDir = await mkTmpDir('sm-agentmodel-agents-'); // never written to — simulates a deleted persona
|
|
128
|
+
|
|
129
|
+
expect(() => readPersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).not.toThrow();
|
|
130
|
+
expect(readPersonaModel('deleted-persona', { globalDir, validatePath: noopValidatePath, cwd })).toBeNull();
|
|
131
|
+
|
|
132
|
+
const { todayFile } = require('../lib/opsErrorLog.cjs');
|
|
133
|
+
const lines = fs.readFileSync(todayFile(cwd), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
134
|
+
const matches = lines.filter((l) => l.message.includes('deleted-persona'));
|
|
135
|
+
// Logged once despite two readPersonaModel calls above — dedup key is (cwd, agentType).
|
|
136
|
+
expect(matches).toHaveLength(1);
|
|
137
|
+
expect(matches[0].level).toBe('warn');
|
|
138
|
+
});
|
|
@@ -45,6 +45,13 @@ async function mkProject({ managed = true } = {}) {
|
|
|
45
45
|
config.addAllowedRoot(cwd);
|
|
46
46
|
tmpDirs.push(cwd);
|
|
47
47
|
if (managed) fs.mkdirSync(path.join(cwd, 'session-manager-operations'), { recursive: true });
|
|
48
|
+
// Project-overlay fixture for the DEFAULT_FEEDBACK_AGENT ('architect') —
|
|
49
|
+
// ensureEpic's write-time persona-existence check (epicMint.cjs) now
|
|
50
|
+
// requires a real persona file to mint against; this keeps these tests
|
|
51
|
+
// hermetic instead of depending on the host's ~/.claude/agents/.
|
|
52
|
+
const agentsDir = path.join(cwd, '.claude', 'agents');
|
|
53
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(path.join(agentsDir, 'architect.md'), '---\nname: architect\n---\nFixture persona for tests.\n');
|
|
48
55
|
return fs.realpathSync(cwd);
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -62,6 +62,13 @@ async function mkProject({ managed = true } = {}) {
|
|
|
62
62
|
config.addAllowedRoot(cwd);
|
|
63
63
|
tmpDirs.push(cwd);
|
|
64
64
|
if (managed) fs.mkdirSync(path.join(cwd, 'session-manager-operations'), { recursive: true });
|
|
65
|
+
// Project-overlay fixture for the DEFAULT_FEEDBACK_AGENT ('architect') —
|
|
66
|
+
// ensureEpic's write-time persona-existence check (epicMint.cjs) now
|
|
67
|
+
// requires a real persona file to mint against; this keeps these tests
|
|
68
|
+
// hermetic instead of depending on the host's ~/.claude/agents/.
|
|
69
|
+
const agentsDir = path.join(cwd, '.claude', 'agents');
|
|
70
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(agentsDir, 'architect.md'), '---\nname: architect\n---\nFixture persona for tests.\n');
|
|
65
72
|
return fs.realpathSync(cwd);
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -30,8 +30,8 @@ async function mkCwd() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** The New Epic UI's call shape — the only caller allowed to mint. */
|
|
33
|
-
function mint(cwd, opts = {}) {
|
|
34
|
-
return ensureEpic(cwd, { mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI, ...opts });
|
|
33
|
+
function mint(cwd, opts = {}, deps = {}) {
|
|
34
|
+
return ensureEpic(cwd, { mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI, ...opts }, deps);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function writeIndex(cwd, index) {
|
|
@@ -282,3 +282,51 @@ test('ensureEpic refuses the mint and appends an epic_mint_refused audit event w
|
|
|
282
282
|
expect(records[0].kind).toBe('epic_mint_refused');
|
|
283
283
|
expect(records[0].reason).toMatch(/forced test failure/);
|
|
284
284
|
});
|
|
285
|
+
|
|
286
|
+
// ─── AGENTTYPE → PERSONA FK (write-time existence check) ────────────────────
|
|
287
|
+
// "throw on write, report on read" — a mint whose agentType names a persona
|
|
288
|
+
// that doesn't resolve is refused outright; a persona that goes dangling
|
|
289
|
+
// LATER (renamed/deleted after the Epic was minted) is legitimate history,
|
|
290
|
+
// left to agentModelResolve.cjs's readPersonaModel (stays permissive) and the
|
|
291
|
+
// ops-hygiene sweep to report.
|
|
292
|
+
|
|
293
|
+
test('minting with an unknown agentType throws and names the two paths checked', async () => {
|
|
294
|
+
const cwd = await mkCwd();
|
|
295
|
+
|
|
296
|
+
await expect(mint(cwd, { goalText: 'bad persona ref', agentType: 'no-such-persona' }))
|
|
297
|
+
.rejects.toThrow(/no-such-persona/);
|
|
298
|
+
await expect(mint(cwd, { goalText: 'bad persona ref', agentType: 'no-such-persona' }))
|
|
299
|
+
.rejects.toThrow(new RegExp(path.join(cwd, '.claude', 'agents', 'no-such-persona.md').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
300
|
+
|
|
301
|
+
expect(Object.keys(readActiveIndex(cwd).sessions)).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('minting with a valid (globally-resolvable) agentType succeeds via an injected personaExists dep', async () => {
|
|
305
|
+
const cwd = await mkCwd();
|
|
306
|
+
|
|
307
|
+
const minted = await mint(cwd, {
|
|
308
|
+
goalText: 'good persona ref',
|
|
309
|
+
agentType: 'architect',
|
|
310
|
+
}, { personaExists: () => true });
|
|
311
|
+
|
|
312
|
+
expect(minted.created).toBe(true);
|
|
313
|
+
expect(readActiveIndex(cwd).sessions[minted.epicId].agentType).toBe('architect');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('minting with a project-overlay-only persona succeeds', async () => {
|
|
317
|
+
const cwd = await mkCwd();
|
|
318
|
+
const overlayDir = path.join(cwd, '.claude', 'agents');
|
|
319
|
+
fs.mkdirSync(overlayDir, { recursive: true });
|
|
320
|
+
fs.writeFileSync(path.join(overlayDir, 'overlay-only.md'), '---\nname: overlay-only\n---\nbody\n');
|
|
321
|
+
|
|
322
|
+
const minted = await mint(cwd, { goalText: 'overlay persona ref', agentType: 'overlay-only' });
|
|
323
|
+
|
|
324
|
+
expect(minted.created).toBe(true);
|
|
325
|
+
expect(readActiveIndex(cwd).sessions[minted.epicId].agentType).toBe('overlay-only');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('minting with no agentType at all is unaffected by the persona check', async () => {
|
|
329
|
+
const cwd = await mkCwd();
|
|
330
|
+
const minted = await mint(cwd, { goalText: 'no persona at all' });
|
|
331
|
+
expect(minted.created).toBe(true);
|
|
332
|
+
});
|