claude-code-session-manager 0.75.3 → 0.77.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.
Files changed (164) hide show
  1. package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
  2. package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
  3. package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
  4. package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
  5. package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
  6. package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
  7. package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
  9. package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
  10. package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
  11. package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
  12. package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
  13. package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
  15. package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
  16. package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
  18. package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
  20. package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
  21. package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
  22. package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
  23. package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
  24. package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
  25. package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
  26. package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
  27. package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/lib/activeSessions.cjs +116 -6
  32. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  33. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  34. package/scripts/render-project-pages.cjs +70 -0
  35. package/scripts/scheduler-mcp-server.cjs +269 -96
  36. package/scripts/validate-project-pages-summary.cjs +62 -0
  37. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  38. package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
  39. package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
  40. package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
  41. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  42. package/src/main/__tests__/prdCreate.test.cjs +247 -0
  43. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  44. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  45. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  46. package/src/main/__tests__/projectPages.test.cjs +73 -1
  47. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  48. package/src/main/__tests__/runVerify.test.cjs +94 -0
  49. package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
  50. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  51. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
  52. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  54. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  55. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  56. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  57. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  58. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  59. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  60. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  61. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
  62. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  63. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
  64. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  65. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  66. package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
  67. package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
  68. package/src/main/bilkoHost.cjs +4 -3
  69. package/src/main/chatRunner.cjs +6 -1
  70. package/src/main/config.cjs +25 -33
  71. package/src/main/health.cjs +153 -2
  72. package/src/main/index.cjs +64 -5
  73. package/src/main/ipcSchemas.cjs +69 -1
  74. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
  75. package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
  76. package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
  77. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
  78. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
  79. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  80. package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
  81. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
  82. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  83. package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
  84. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  85. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  86. package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
  87. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
  88. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  89. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
  90. package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
  91. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  92. package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
  93. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  94. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  95. package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
  96. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
  97. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
  98. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  99. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
  100. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  101. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  102. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
  103. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  104. package/src/main/lib/activeIndexMerge.cjs +15 -0
  105. package/src/main/lib/activeIndexRebuild.cjs +133 -0
  106. package/src/main/lib/agentModelResolve.cjs +58 -0
  107. package/src/main/lib/buildTarget.cjs +3 -2
  108. package/src/main/lib/childWithLog.cjs +69 -2
  109. package/src/main/lib/claudeBin.cjs +54 -1
  110. package/src/main/lib/crossProjectFeedback.cjs +8 -1
  111. package/src/main/lib/definitionOfDone.cjs +3 -2
  112. package/src/main/lib/delegationReadiness.cjs +514 -26
  113. package/src/main/lib/ephemeralCwd.cjs +78 -0
  114. package/src/main/lib/epicDelegationStats.cjs +2 -1
  115. package/src/main/lib/epicMint.cjs +17 -1
  116. package/src/main/lib/epicStatusMirror.cjs +95 -0
  117. package/src/main/lib/epicValidationHook.cjs +2 -1
  118. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  119. package/src/main/lib/fixChainDepth.cjs +45 -0
  120. package/src/main/lib/gitWorktree.cjs +520 -21
  121. package/src/main/lib/jobWorktree.cjs +2 -0
  122. package/src/main/lib/landedSinceRun.cjs +55 -0
  123. package/src/main/lib/launchFailure.cjs +357 -0
  124. package/src/main/lib/loadGate.cjs +134 -0
  125. package/src/main/lib/mcpToolCatalog.cjs +370 -0
  126. package/src/main/lib/opsErrorLog.cjs +12 -1
  127. package/src/main/lib/opsOwnership.cjs +106 -0
  128. package/src/main/lib/prdAdminRoutes.cjs +43 -3
  129. package/src/main/lib/prdAgentType.cjs +84 -0
  130. package/src/main/lib/prdCreate.cjs +103 -15
  131. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  132. package/src/main/lib/prdFrontmatter.cjs +17 -3
  133. package/src/main/lib/prdLocations.cjs +13 -6
  134. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  135. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  136. package/src/main/lib/projectRootResolve.cjs +134 -0
  137. package/src/main/lib/promptSessionSchema.cjs +7 -0
  138. package/src/main/lib/queueHealth.cjs +38 -0
  139. package/src/main/lib/queueStore.cjs +40 -7
  140. package/src/main/lib/quietMachineLease.cjs +48 -0
  141. package/src/main/lib/rcaReport.cjs +54 -4
  142. package/src/main/lib/reaperHelpers.cjs +64 -1
  143. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  144. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  145. package/src/main/lib/schedulerBatch.cjs +301 -55
  146. package/src/main/lib/schedulerConfig.cjs +99 -0
  147. package/src/main/projectBrief.cjs +3 -2
  148. package/src/main/projectPages.cjs +162 -3
  149. package/src/main/promptSessionTranscript.cjs +0 -0
  150. package/src/main/pty.cjs +5 -0
  151. package/src/main/queueOps.cjs +15 -8
  152. package/src/main/runVerify.cjs +50 -9
  153. package/src/main/scheduler/prdParser.cjs +18 -1
  154. package/src/main/scheduler.cjs +1701 -130
  155. package/src/main/seedAgentPersonas.cjs +62 -21
  156. package/src/main/seedSchedulerMcp.cjs +58 -4
  157. package/src/main/templates/project-pages-catalog.json +741 -0
  158. package/src/main/templates/project-pages-pipeline.md +417 -0
  159. package/src/preload/api.d.ts +187 -3
  160. package/src/preload/index.cjs +9 -0
  161. package/src/seed/agents/project-home-builder.md +59 -0
  162. package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
  163. package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
  164. package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // Bash-invocable CLI wrapping the Stage 0 renderer (render.tsx →
3
+ // scripts/render-project-pages/dist/renderer.cjs, built by
4
+ // `npm run build:project-pages`). Requires that build to have already run —
5
+ // this script does NOT bundle on the fly, since PRD authoring standards
6
+ // forbid unbounded/slow steps inside a headless job.
7
+ //
8
+ // Usage:
9
+ // node scripts/render-project-pages.cjs <summary.json> <picks.json> <output dir> <generatedAt>
10
+ //
11
+ // generatedAt (4th arg, required) is an ISO 8601 timestamp string the CALLER
12
+ // stamps — Date.now() is unavailable in some execution contexts (e.g.
13
+ // workflow scripts) that may drive this CLI, so this script never generates
14
+ // its own timestamp. This is the "accept generatedAt as a 4th CLI arg"
15
+ // option from the PRD's two documented choices (the alternative — omitting
16
+ // generatedAt from manifest.json entirely — was not taken).
17
+ //
18
+ // Writes <output dir>/home.html, marketing.html, feature.html,
19
+ // architecture.html, brief.html, and manifest.json ({ generatedAt }).
20
+ //
21
+ // Path arguments are used as-is (no allowedRoots validation like
22
+ // config.cjs's validatePath) — this is a standalone local dev/ops script run
23
+ // by a trusted human or agent shell, not an IPC-reachable surface, so it's
24
+ // out of scope for that guard by design.
25
+ 'use strict';
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ function fail(message) {
31
+ console.error(`render-project-pages: ${message}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ function main() {
36
+ const [summaryPath, picksPath, outDir, generatedAt] = process.argv.slice(2);
37
+ if (!summaryPath || !picksPath || !outDir || !generatedAt) {
38
+ fail('usage: node scripts/render-project-pages.cjs <summary.json> <picks.json> <output dir> <generatedAt>');
39
+ }
40
+ if (Number.isNaN(Date.parse(generatedAt))) {
41
+ fail(`generatedAt must be an ISO 8601 timestamp, got: ${generatedAt}`);
42
+ }
43
+
44
+ const bundlePath = path.join(__dirname, 'render-project-pages', 'dist', 'renderer.cjs');
45
+ if (!fs.existsSync(bundlePath)) {
46
+ fail(`build bundle not found at ${bundlePath} — run "npm run build:project-pages" first`);
47
+ }
48
+
49
+ let renderProjectPages;
50
+ try {
51
+ ({ renderProjectPages } = require(bundlePath));
52
+ } catch (err) {
53
+ fail(`failed to load ${bundlePath}: ${err.message}`);
54
+ }
55
+
56
+ const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
57
+ const picks = JSON.parse(fs.readFileSync(picksPath, 'utf8'));
58
+
59
+ const pages = renderProjectPages(summary, picks);
60
+
61
+ fs.mkdirSync(outDir, { recursive: true });
62
+ for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
63
+ fs.writeFileSync(path.join(outDir, `${lens}.html`), pages[lens]);
64
+ }
65
+ fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify({ generatedAt }, null, 2));
66
+
67
+ console.log(`Wrote home.html, marketing.html, feature.html, architecture.html, brief.html, manifest.json to ${outDir}`);
68
+ }
69
+
70
+ main();
@@ -11,6 +11,11 @@
11
11
  * feedback_list_projects() -> GET /admin/feedback/targets
12
12
  * feedback_open_session({ ... }) -> POST /admin/feedback/open-session
13
13
  *
14
+ * project_home_get_contract({ cwd? }) -> GET /admin/project-home/contract
15
+ * project_home_validate_summary({ cwd?, summary }) -> POST /admin/project-home/validate-summary
16
+ * project_home_render({ cwd?, summary, picks }) -> POST /admin/project-home/render
17
+ * project_home_status({ cwd? }) -> GET /admin/project-home/status
18
+ *
14
19
  * This is a separate process from the Electron app — it only ever reaches
15
20
  * it over the token-authed loopback HTTP API in admin-api.json, never by
16
21
  * requiring scheduler.cjs/localAdminHttp.cjs directly. The admin server IS the
@@ -29,11 +34,41 @@ const {
29
34
  CallToolRequestSchema,
30
35
  } = require('@modelcontextprotocol/sdk/types.js');
31
36
  const { PRD_WORK_TYPES } = require('../src/main/lib/workTypeLibrary.cjs');
37
+ const { MCP_TOOL_CATALOG, MCP_RECIPES, composeDescription } = require('../src/main/lib/mcpToolCatalog.cjs');
38
+
39
+ const CATALOG_BY_NAME = new Map(MCP_TOOL_CATALOG.map((entry) => [entry.name, entry]));
40
+
41
+ function descriptionFor(toolName) {
42
+ const entry = CATALOG_BY_NAME.get(toolName);
43
+ if (!entry) throw new Error(`mcpToolCatalog.cjs has no entry for tool "${toolName}"`);
44
+ return composeDescription(entry);
45
+ }
32
46
 
33
47
  const TOKEN_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'admin-api.json');
34
48
 
49
+ // Defined once so every failure path points to the same next call, verbatim
50
+ // — never paste this sentence at each return site (PRD: session_manager_help).
51
+ const HELP_POINTER = ' — call session_manager_help for the correct usage';
52
+
53
+ function withPointer(text) {
54
+ const str = String(text);
55
+ return str.endsWith(HELP_POINTER) ? str : str + HELP_POINTER;
56
+ }
57
+
58
+ function errorResult(text) {
59
+ return { content: [{ type: 'text', text: withPointer(text) }], isError: true };
60
+ }
61
+
35
62
  const NOT_RUNNING_ERROR =
36
- 'session-manager app is not running (admin API unreachable) — start it first';
63
+ `session-manager app is not running (admin API unreachable) — start it first${HELP_POINTER}`;
64
+
65
+ // Same resolution fetchReadiness() below uses: a project-home-builder session
66
+ // running inside an Epic worktree still targets the real project cwd, not
67
+ // the worktree's own pwd.
68
+ function resolveCwdArg(args) {
69
+ if (args && typeof args.cwd === 'string' && args.cwd) return args.cwd;
70
+ return process.env.SM_PROJECT_ROOT || process.cwd();
71
+ }
37
72
 
38
73
  async function readAdminConfig() {
39
74
  const raw = await fsp.readFile(TOKEN_PATH, 'utf8');
@@ -68,56 +103,56 @@ async function adminRequest(method, urlPath, body) {
68
103
  return json;
69
104
  }
70
105
 
106
+ // session_manager_help's live half. The catalog/recipes below are static and
107
+ // answerable purely in-process (no admin API needed), but "is this MCP
108
+ // server actually wired up for THIS project" needs the admin API's
109
+ // checkDelegationReadiness result — that's genuinely unavailable when the
110
+ // app is down, so this must degrade to a reported-unavailable state rather
111
+ // than throwing NOT_RUNNING_ERROR and failing the whole help call.
112
+ async function fetchReadiness() {
113
+ // SM_PROJECT_ROOT (when present) is a trusted hint for the real project
114
+ // root — forwarded so a worktree/ops-internal process.cwd() still reports
115
+ // the real project's readiness. See projectRootResolve.cjs.
116
+ const cwd = process.env.SM_PROJECT_ROOT || process.cwd();
117
+ try {
118
+ const qs = new URLSearchParams({ cwd });
119
+ const result = await adminRequest('GET', `/admin/mcp/readiness?${qs.toString()}`);
120
+ if (result?.ok === false) {
121
+ return { available: false, cwd, reason: result.error ?? 'readiness check failed' };
122
+ }
123
+ return { available: true, cwd, ok: result.ready, checks: result.checks };
124
+ } catch (e) {
125
+ return { available: false, cwd, reason: e?.message ?? String(e) };
126
+ }
127
+ }
128
+
71
129
  const TOOLS = [
72
130
  {
73
131
  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.',
132
+ description: descriptionFor('scheduler_reset_job'),
77
133
  inputSchema: {
78
134
  type: 'object',
79
135
  properties: {
80
136
  slug: { type: 'string', description: 'PRD slug of the job to reset' },
81
137
  force: { type: 'boolean', description: 'Required to reset a job whose status is already "completed"' },
138
+ cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
82
139
  },
83
140
  required: ['slug'],
84
141
  },
85
142
  },
86
143
  {
87
144
  name: 'scheduler_list_jobs',
88
- description: "List scheduler jobs via the session-manager app's admin API.",
145
+ description: descriptionFor('scheduler_list_jobs'),
89
146
  inputSchema: { type: 'object', properties: {} },
90
147
  },
91
148
  {
92
149
  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.',
150
+ description: descriptionFor('scheduler_create_prd'),
116
151
  inputSchema: {
117
152
  type: 'object',
118
153
  properties: {
119
154
  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)' },
155
+ cwd: { type: 'string', description: 'Absolute path to the target project (where claude -p will run). Optional inside an Epic session — the server resolves the real project from the calling session (originClaudeSessionId/sourcePromptId) when omitted.' },
121
156
  estimateMinutes: { type: 'number', description: 'Integer wall-clock estimate in minutes' },
122
157
  goal: { type: 'string', description: '2-4 sentences: what the executor will build and why' },
123
158
  acceptanceCriteria: {
@@ -134,20 +169,23 @@ const TOOLS = [
134
169
  tag: {
135
170
  type: 'string',
136
171
  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.',
172
+ 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.',
173
+ },
174
+ agentType: {
175
+ type: 'string',
176
+ description: 'Optional: WHO executes this PRD — the persona name (a file at ~/.claude/agents/<name>.md, e.g. "dev-lead") the executor should adopt. Distinct from `tag` (what kind of work); this is who does it. Defaults to "dev-lead" when omitted. Rejected at write time if the name does not resolve to a readable persona file.',
177
+ },
178
+ quietMachine: {
179
+ type: 'boolean',
180
+ description: 'Optional: set true only when this PRD\'s acceptance criteria are wall-clock/timing measurements (frame time, performance fences) that CPU contention from concurrent jobs would invalidate. The scheduler dispatches it only once zero other jobs are running machine-wide, and holds every other job off the whole slot pool for its run — a whole-machine exclusive lease, not a per-project one. If the machine never goes quiet within the configured wait window (default 30 minutes), it dispatches anyway and is marked degraded. Opt-in only; omit for ordinary PRDs.',
138
181
  },
139
182
  },
140
- required: ['title', 'cwd', 'estimateMinutes', 'goal', 'acceptanceCriteria', 'implementationNotes'],
183
+ required: ['title', 'estimateMinutes', 'goal', 'acceptanceCriteria', 'implementationNotes'],
141
184
  },
142
185
  },
143
186
  {
144
187
  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. Results are paginated (default '
148
- + 'limit 100, max 500) sorted by slug ascending — check `hasMore`/`total` in the response before assuming you '
149
- + "received every PRD; page further with `offset`. Default fields are compact (no parallelGroup/estimateMinutes/"
150
- + 'sourcePromptId/epicId/archivedStatus) — pass fields:"full" to restore them.',
188
+ description: descriptionFor('scheduler_list_prds'),
151
189
  inputSchema: {
152
190
  type: 'object',
153
191
  properties: {
@@ -162,8 +200,7 @@ const TOOLS = [
162
200
  },
163
201
  {
164
202
  name: 'scheduler_get_prd',
165
- description: "THE ONLY SUPPORTED WAY to read one PRD's full body + parsed frontmatter (live or archived) via the "
166
- + "session-manager app's admin API.",
203
+ description: descriptionFor('scheduler_get_prd'),
167
204
  inputSchema: {
168
205
  type: 'object',
169
206
  properties: {
@@ -175,11 +212,7 @@ const TOOLS = [
175
212
  },
176
213
  {
177
214
  name: 'scheduler_update_prd',
178
- description: "THE ONLY SUPPORTED WAY to edit a NOT-yet-running PRD's frontmatter and/or body via the session-manager "
179
- + 'app\'s admin API. Refuses once a queue row exists for the slug and its status is anything but "pending" '
180
- + '(running/completed/failed/needs_review) — editing the spec under a live or already-finished executor is refused, '
181
- + 'not silently applied. Only recognized frontmatter keys (title, cwd, estimateMinutes, parallelGroup, '
182
- + 'sourcePromptId, sourceTabId, tag) may be patched; unrecognized keys (e.g. dependsOn) round-trip unchanged.',
215
+ description: descriptionFor('scheduler_update_prd'),
183
216
  inputSchema: {
184
217
  type: 'object',
185
218
  properties: {
@@ -196,6 +229,8 @@ const TOOLS = [
196
229
  sourcePromptId: { type: 'string' },
197
230
  sourceTabId: { type: 'string' },
198
231
  tag: { type: 'string', enum: PRD_WORK_TYPES },
232
+ agentType: { type: 'string', description: 'WHO executes this PRD (persona name) — patchable after creation too, see scheduler_create_prd' },
233
+ quietMachine: { type: 'boolean', description: 'Optional: the exclusive machine-wide quiet lease flag (see scheduler_create_prd) — patchable after creation too' },
199
234
  },
200
235
  },
201
236
  body: { type: 'string', description: 'Optional: full replacement body (everything after the frontmatter)' },
@@ -205,34 +240,31 @@ const TOOLS = [
205
240
  },
206
241
  {
207
242
  name: 'scheduler_archive_prd',
208
- description: "THE ONLY SUPPORTED WAY to archive one or more PRDs (move to prds-archived/) via the session-manager "
209
- + "app's admin API.",
243
+ description: descriptionFor('scheduler_archive_prd'),
210
244
  inputSchema: {
211
245
  type: 'object',
212
246
  properties: {
213
247
  slugs: { type: 'array', items: { type: 'string' }, description: 'Slugs to archive' },
248
+ cwd: { type: 'string', description: 'Optional: the PRDs\' project cwd, narrows/speeds the search for every slug in this batch' },
214
249
  },
215
250
  required: ['slugs'],
216
251
  },
217
252
  },
218
253
  {
219
254
  name: 'scheduler_cancel_job',
220
- description: "THE ONLY SUPPORTED WAY to cancel a not-yet-terminal scheduler job via the session-manager app's admin "
221
- + 'API. A running job is SIGTERM\'d; a pending job is simply retired. There is no "cancelled" job status, so a '
222
- + 'cancelled job lands as "failed" with an error naming the cause. Refuses a slug whose job is already terminal '
223
- + '(completed/failed/needs_review) — nothing to cancel.',
255
+ description: descriptionFor('scheduler_cancel_job'),
224
256
  inputSchema: {
225
257
  type: 'object',
226
258
  properties: {
227
259
  slug: { type: 'string', description: 'PRD slug of the job to cancel' },
260
+ cwd: { type: 'string', description: 'Optional: the PRD project cwd, narrows/speeds the search' },
228
261
  },
229
262
  required: ['slug'],
230
263
  },
231
264
  },
232
265
  {
233
266
  name: 'scheduler_retag_prd',
234
- description: "THE ONLY SUPPORTED WAY to rewrite a PRD's parallelGroup and/or estimateMinutes frontmatter (and, if "
235
- + "parallelGroup changes, its NN- filename prefix) via the session-manager app's admin API.",
267
+ description: descriptionFor('scheduler_retag_prd'),
236
268
  inputSchema: {
237
269
  type: 'object',
238
270
  properties: {
@@ -255,10 +287,7 @@ const TOOLS = [
255
287
  },
256
288
  {
257
289
  name: 'chat_send_prompt',
258
- description:
259
- "Push a prompt into an already-open tab's chat queue via the session-manager app's admin "
260
- + 'API. The renderer resolves the tab (must currently be open) and runs the prompt through '
261
- + 'the same queued-vs-immediate path as a manual send. No-ops if the tab is unknown/closed.',
290
+ description: descriptionFor('chat_send_prompt'),
262
291
  inputSchema: {
263
292
  type: 'object',
264
293
  properties: {
@@ -270,31 +299,12 @@ const TOOLS = [
270
299
  },
271
300
  {
272
301
  name: 'feedback_list_projects',
273
- description:
274
- 'List the OTHER projects on this machine that can receive feedback (i.e. that Session Manager '
275
- + 'already manages — they have a session-manager-operations/ directory). Call this FIRST when you '
276
- + 'need the exact `toCwd` for feedback_open_session and are not certain of it — never guess a path. '
277
- + 'A project missing from this list has simply never been opened in Session Manager; ask the human '
278
- + 'to open it once rather than inventing a path.',
302
+ description: descriptionFor('feedback_list_projects'),
279
303
  inputSchema: { type: 'object', properties: {} },
280
304
  },
281
305
  {
282
306
  name: 'feedback_open_session',
283
- description:
284
- 'THE ONLY SANCTIONED WAY to hand a finding from THIS project to a DIFFERENT project. Opens a new '
285
- + "PROPOSED session in the receiving project's own Sessions queue, carrying your report as its "
286
- + 'opening prompt and stamped with where it came from. Session Manager performs the cross-folder '
287
- + 'write; you never write another project\'s files yourself. '
288
- + 'WHAT THIS DOES NOT DO: it does not start anything, queue a PRD, or spend a token. The session '
289
- + 'lands as `proposed` and runs only if a human in the RECEIVING project presses "Approve & start". '
290
- + 'There is no callback and no reply channel — do not wait for an answer, and do not tell the user '
291
- + 'the other project has "been fixed" or "is working on it". Report only that the proposal was '
292
- + 'delivered. '
293
- + 'WHEN NOT TO USE IT: for work in the project you are ALREADY in, run /develop inside the Epic you '
294
- + 'are already in — this tool refuses toCwd === fromCwd outright. '
295
- + 'Write the report for a reader who has never seen your project: state the symptom, where you '
296
- + 'observed it, what you expected, and (if you know) the file in THEIR repo that looks responsible. '
297
- + 'Never assume they can see your code.',
307
+ description: descriptionFor('feedback_open_session'),
298
308
  inputSchema: {
299
309
  type: 'object',
300
310
  properties: {
@@ -329,6 +339,62 @@ const TOOLS = [
329
339
  required: ['toCwd', 'fromCwd', 'title', 'body'],
330
340
  },
331
341
  },
342
+ {
343
+ name: 'project_home_get_contract',
344
+ description: descriptionFor('project_home_get_contract'),
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: {
348
+ cwd: { type: 'string', description: "Optional: absolute path to the target project. Defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted." },
349
+ },
350
+ },
351
+ },
352
+ {
353
+ name: 'project_home_validate_summary',
354
+ description: descriptionFor('project_home_validate_summary'),
355
+ inputSchema: {
356
+ type: 'object',
357
+ properties: {
358
+ cwd: { type: 'string', description: "Optional: absolute path to the target project. Defaults to the calling session's own project root when omitted." },
359
+ summary: { type: 'object', description: 'ProjectPageSummary object matching the summarySchema from project_home_get_contract' },
360
+ },
361
+ required: ['summary'],
362
+ },
363
+ },
364
+ {
365
+ name: 'project_home_render',
366
+ description: descriptionFor('project_home_render'),
367
+ inputSchema: {
368
+ type: 'object',
369
+ properties: {
370
+ cwd: { type: 'string', description: "Optional: absolute path to the target project. Defaults to the calling session's own project root when omitted." },
371
+ summary: { type: 'object', description: 'ProjectPageSummary object matching the summarySchema from project_home_get_contract' },
372
+ picks: { type: 'object', description: 'ProjectPagePicks object (lensId -> slotId -> variantId) matching the picksSchema from project_home_get_contract' },
373
+ },
374
+ required: ['summary', 'picks'],
375
+ },
376
+ },
377
+ {
378
+ name: 'project_home_status',
379
+ description: descriptionFor('project_home_status'),
380
+ inputSchema: {
381
+ type: 'object',
382
+ properties: {
383
+ cwd: { type: 'string', description: "Optional: absolute path to the target project. Defaults to the calling session's own project root when omitted." },
384
+ },
385
+ },
386
+ },
387
+ {
388
+ name: 'session_manager_help',
389
+ description: descriptionFor('session_manager_help'),
390
+ inputSchema: {
391
+ type: 'object',
392
+ properties: {
393
+ tool: { type: 'string', description: 'Optional: a tool name — returns that one catalog entry, including exampleArgs' },
394
+ topic: { type: 'string', description: 'Optional: a recipe id — returns that recipe\'s step-by-step instructions' },
395
+ },
396
+ },
397
+ },
332
398
  ];
333
399
 
334
400
  const server = new Server(
@@ -338,16 +404,21 @@ const server = new Server(
338
404
 
339
405
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
340
406
 
341
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
407
+ // Named + exported (see module.exports below) so mcpToolCatalog.test.cjs's
408
+ // sibling can exercise session_manager_help's argument handling directly —
409
+ // mocking node:fs/promises + global.fetch to drive adminRequest — without
410
+ // booting a stdio transport.
411
+ async function handleCallTool(request) {
342
412
  const { name, arguments: args } = request.params;
343
413
  try {
344
414
  if (name === 'scheduler_reset_job') {
345
415
  const slug = args && typeof args.slug === 'string' ? args.slug : null;
346
416
  if (!slug) {
347
- return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
417
+ return errorResult('missing required argument: slug');
348
418
  }
349
419
  const force = args && args.force === true;
350
- const result = await adminRequest('POST', '/admin/scheduler/reset-job', { slug, force });
420
+ const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
421
+ const result = await adminRequest('POST', '/admin/scheduler/reset-job', { slug, force, cwd });
351
422
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
352
423
  }
353
424
  if (name === 'scheduler_list_jobs') {
@@ -375,7 +446,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
375
446
  if (name === 'scheduler_get_prd') {
376
447
  const slug = args && typeof args.slug === 'string' ? args.slug : null;
377
448
  if (!slug) {
378
- return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
449
+ return errorResult('missing required argument: slug');
379
450
  }
380
451
  const qs = new URLSearchParams({ slug });
381
452
  if (args?.cwd) qs.set('cwd', args.cwd);
@@ -385,7 +456,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
385
456
  if (name === 'scheduler_update_prd') {
386
457
  const slug = args && typeof args.slug === 'string' ? args.slug : null;
387
458
  if (!slug) {
388
- return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
459
+ return errorResult('missing required argument: slug');
389
460
  }
390
461
  const result = await adminRequest('POST', '/admin/scheduler/update-prd', args);
391
462
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
@@ -393,28 +464,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
393
464
  if (name === 'scheduler_archive_prd') {
394
465
  const slugs = Array.isArray(args?.slugs) ? args.slugs : null;
395
466
  if (!slugs || slugs.length === 0) {
396
- return { content: [{ type: 'text', text: 'missing required argument: slugs' }], isError: true };
467
+ return errorResult('missing required argument: slugs');
397
468
  }
398
- const result = await adminRequest('POST', '/admin/scheduler/archive-prd', { slugs });
469
+ const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
470
+ const result = await adminRequest('POST', '/admin/scheduler/archive-prd', { slugs, cwd });
399
471
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
400
472
  }
401
473
  if (name === 'scheduler_cancel_job') {
402
474
  const slug = args && typeof args.slug === 'string' ? args.slug : null;
403
475
  if (!slug) {
404
- return { content: [{ type: 'text', text: 'missing required argument: slug' }], isError: true };
476
+ return errorResult('missing required argument: slug');
405
477
  }
406
- const result = await adminRequest('POST', '/admin/scheduler/cancel-job', { slug });
478
+ const cwd = args && typeof args.cwd === 'string' ? args.cwd : undefined;
479
+ const result = await adminRequest('POST', '/admin/scheduler/cancel-job', { slug, cwd });
407
480
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
408
481
  }
409
482
  if (name === 'scheduler_retag_prd') {
410
483
  const items = Array.isArray(args?.items) ? args.items : null;
411
484
  if (!items || items.length === 0) {
412
- return { content: [{ type: 'text', text: 'missing required argument: items' }], isError: true };
485
+ return errorResult('missing required argument: items');
413
486
  }
414
487
  const result = await adminRequest('POST', '/admin/scheduler/retag-prd', { items });
415
488
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
416
489
  }
417
490
  if (name === 'scheduler_create_prd') {
491
+ // Hard refusal at the authoring boundary (issue #11, list C1): a
492
+ // headless scheduled executor performs its own acceptance criteria and
493
+ // never queues follow-on work — PRD 460 called /develop from inside its
494
+ // run, spawned a duplicate PRD, and exited 0 having done nothing. The
495
+ // standards prose already says so; this makes the harness enforce it.
496
+ // scheduler.cjs stamps SM_SCHEDULER_JOB_SLUG on every job spawn and
497
+ // SM_SCHEDULER_JOB_MAY_QUEUE=1 only for personas whose whole job is
498
+ // decomposition (architect), so a planning PRD still can.
499
+ if (process.env.SM_SCHEDULER_JOB_SLUG && process.env.SM_SCHEDULER_JOB_MAY_QUEUE !== '1') {
500
+ return errorResult(
501
+ `refused: scheduler_create_prd was called from inside headless scheduled job '${process.env.SM_SCHEDULER_JOB_SLUG}'. `
502
+ + 'A PRD executor performs its own acceptance criteria directly and never queues follow-on work (self-queue failure class). '
503
+ + 'If more work is genuinely needed, say so in your final result text — the authoring Epic decides whether to queue it.',
504
+ );
505
+ }
418
506
  // If the caller (the model) didn't pass sourcePromptId, forward this
419
507
  // process's own SM_CHAT_SESSION_ID (set by chatRunner.cjs on the
420
508
  // parent claude -p process this MCP server inherited its env from) so
@@ -425,6 +513,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
425
513
  if (!payload.sourcePromptId && process.env.SM_CHAT_SESSION_ID) {
426
514
  payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
427
515
  }
516
+ // SM_PROJECT_ROOT (chatRunner.cjs/pty.cjs/scheduler.cjs's job spawn) —
517
+ // a trusted hint for the real project cwd, forwarded so the admin route
518
+ // can resolve it even when the caller omitted cwd or passed a worktree
519
+ // pwd. See projectRootResolve.cjs's resolveProjectContext.
520
+ if (process.env.SM_PROJECT_ROOT) {
521
+ payload.originProjectRoot = process.env.SM_PROJECT_ROOT;
522
+ }
428
523
  const result = await adminRequest('POST', '/admin/scheduler/create-prd', payload);
429
524
  // Never say "queued" — this tool only writes the PRD file; the queue
430
525
  // row is derived by the scheduler's next reconcile pass, not by this
@@ -441,7 +536,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
441
536
  if (name === 'feedback_open_session') {
442
537
  for (const key of ['toCwd', 'fromCwd', 'title', 'body']) {
443
538
  if (!args || typeof args[key] !== 'string' || !args[key].trim()) {
444
- return { content: [{ type: 'text', text: `missing required argument: ${key}` }], isError: true };
539
+ return errorResult(`missing required argument: ${key}`);
445
540
  }
446
541
  }
447
542
  // Same fallback shape as scheduler_create_prd: forward this process's
@@ -452,6 +547,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
452
547
  if (!payload.fromEpicId && process.env.SM_CHAT_SESSION_ID) {
453
548
  payload.originClaudeSessionId = process.env.SM_CHAT_SESSION_ID;
454
549
  }
550
+ // See scheduler_create_prd's own SM_PROJECT_ROOT forwarding above.
551
+ if (process.env.SM_PROJECT_ROOT) {
552
+ payload.originProjectRoot = process.env.SM_PROJECT_ROOT;
553
+ }
455
554
  const result = await adminRequest('POST', '/admin/feedback/open-session', payload);
456
555
  // Never say "sent", "filed" or "fixed" — this call delivers a PROPOSAL
457
556
  // that a human in the other project must still approve.
@@ -464,23 +563,97 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
464
563
  const tabId = args && typeof args.tabId === 'string' ? args.tabId : null;
465
564
  const prompt = args && typeof args.prompt === 'string' ? args.prompt : null;
466
565
  if (!tabId || !prompt) {
467
- return { content: [{ type: 'text', text: 'missing required arguments: tabId, prompt' }], isError: true };
566
+ return errorResult('missing required arguments: tabId, prompt');
468
567
  }
469
568
  const result = await adminRequest('POST', '/admin/chat/send-prompt', { tabId, prompt });
470
569
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
471
570
  }
472
- return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
571
+ if (name === 'project_home_get_contract') {
572
+ const cwd = resolveCwdArg(args);
573
+ const qs = new URLSearchParams({ cwd });
574
+ const result = await adminRequest('GET', `/admin/project-home/contract?${qs.toString()}`);
575
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
576
+ }
577
+ if (name === 'project_home_validate_summary') {
578
+ if (!args || typeof args.summary !== 'object' || args.summary === null) {
579
+ return errorResult('missing required argument: summary');
580
+ }
581
+ const cwd = resolveCwdArg(args);
582
+ const result = await adminRequest('POST', '/admin/project-home/validate-summary', { cwd, summary: args.summary });
583
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
584
+ }
585
+ if (name === 'project_home_render') {
586
+ if (!args || typeof args.summary !== 'object' || args.summary === null) {
587
+ return errorResult('missing required argument: summary');
588
+ }
589
+ if (typeof args.picks !== 'object' || args.picks === null) {
590
+ return errorResult('missing required argument: picks');
591
+ }
592
+ const cwd = resolveCwdArg(args);
593
+ const result = await adminRequest('POST', '/admin/project-home/render', { cwd, summary: args.summary, picks: args.picks });
594
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
595
+ }
596
+ if (name === 'project_home_status') {
597
+ const cwd = resolveCwdArg(args);
598
+ const qs = new URLSearchParams({ cwd });
599
+ const result = await adminRequest('GET', `/admin/project-home/status?${qs.toString()}`);
600
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
601
+ }
602
+ if (name === 'session_manager_help') {
603
+ const toolName = args && typeof args.tool === 'string' ? args.tool : null;
604
+ const topic = args && typeof args.topic === 'string' ? args.topic : null;
605
+
606
+ const response = {};
607
+
608
+ if (toolName) {
609
+ const entry = CATALOG_BY_NAME.get(toolName);
610
+ if (!entry) {
611
+ const valid = MCP_TOOL_CATALOG.map((e) => e.name);
612
+ return errorResult(`unknown tool "${toolName}" for session_manager_help — valid tool names: ${valid.join(', ')}`);
613
+ }
614
+ response.tool = entry;
615
+ }
616
+
617
+ if (topic) {
618
+ const recipe = MCP_RECIPES.find((r) => r.id === topic);
619
+ if (!recipe) {
620
+ const valid = MCP_RECIPES.map((r) => r.id);
621
+ return errorResult(`unknown topic "${topic}" for session_manager_help — valid topic ids: ${valid.join(', ')}`);
622
+ }
623
+ response.recipe = recipe;
624
+ }
625
+
626
+ if (!toolName && !topic) {
627
+ response.tools = MCP_TOOL_CATALOG.map((e) => ({ name: e.name, group: e.group, purpose: e.purpose }));
628
+ response.recipes = MCP_RECIPES.map((r) => ({ id: r.id, title: r.title }));
629
+ }
630
+
631
+ response.readiness = await fetchReadiness();
632
+
633
+ return { content: [{ type: 'text', text: JSON.stringify(response) }] };
634
+ }
635
+ return errorResult(`unknown tool: ${name}`);
473
636
  } catch (e) {
474
- return { content: [{ type: 'text', text: e?.message ?? String(e) }], isError: true };
637
+ return errorResult(e?.message ?? String(e));
475
638
  }
476
- });
639
+ }
640
+
641
+ server.setRequestHandler(CallToolRequestSchema, handleCallTool);
477
642
 
478
643
  async function main() {
479
644
  const transport = new StdioServerTransport();
480
645
  await server.connect(transport);
481
646
  }
482
647
 
483
- main().catch((e) => {
484
- process.stderr.write(`scheduler-mcp-server fatal: ${e?.stack ?? e}\n`);
485
- process.exit(1);
486
- });
648
+ // Guarded so mcpToolCatalog.test.cjs can `require()` this file to read TOOLS
649
+ // (for both-directions name-parity) without opening a stdio transport as a
650
+ // require-time side effect — only the real `node scripts/scheduler-mcp-server.cjs`
651
+ // invocation satisfies require.main === module.
652
+ if (require.main === module) {
653
+ main().catch((e) => {
654
+ process.stderr.write(`scheduler-mcp-server fatal: ${e?.stack ?? e}\n`);
655
+ process.exit(1);
656
+ });
657
+ }
658
+
659
+ module.exports = { TOOLS, handleCallTool, HELP_POINTER, NOT_RUNNING_ERROR };