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.
- package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
- package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/lib/activeSessions.cjs +116 -6
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +269 -96
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +247 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +25 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +64 -5
- package/src/main/ipcSchemas.cjs +69 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +69 -2
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +514 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +520 -21
- package/src/main/lib/jobWorktree.cjs +2 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +370 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +106 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +103 -15
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +40 -7
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +54 -4
- package/src/main/lib/reaperHelpers.cjs +64 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +301 -55
- package/src/main/lib/schedulerConfig.cjs +99 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +162 -3
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1701 -130
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +187 -3
- package/src/preload/index.cjs +9 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
- package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
- package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcpToolCatalog.cjs — single source of truth for what every
|
|
3
|
+
* session-manager-scheduler MCP tool does, when to reach for it, and when
|
|
4
|
+
* NOT to. Before this file, that knowledge lived only inside hand-written
|
|
5
|
+
* `description` strings in scripts/scheduler-mcp-server.cjs's TOOLS array —
|
|
6
|
+
* invisible to the app, the Home tab, the manual, and to a human debugging
|
|
7
|
+
* why a session went off-piste. scheduler-mcp-server.cjs now BUILDS each
|
|
8
|
+
* tool's live `description` from this catalog via composeDescription(), so
|
|
9
|
+
* the two can never drift.
|
|
10
|
+
*
|
|
11
|
+
* Plain CJS, no Electron dependency — requirable from both the main process
|
|
12
|
+
* and the standalone `node scripts/scheduler-mcp-server.cjs` process, same
|
|
13
|
+
* constraint workTypeLibrary.cjs already satisfies (see that file's header).
|
|
14
|
+
*
|
|
15
|
+
* Composition rule (also asserted in mcpToolCatalog.test.cjs): a tool's live
|
|
16
|
+
* description is `[purpose, whenToUse, whenNotToUse, notes].filter(Boolean)
|
|
17
|
+
* .join(' ')` — deterministic, so a catalog edit provably reaches the live
|
|
18
|
+
* MCP tool list with no separate hand-edit required.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const { z } = require('zod');
|
|
23
|
+
|
|
24
|
+
const CatalogEntrySchema = z.object({
|
|
25
|
+
name: z.string().min(1),
|
|
26
|
+
group: z.enum(['scheduler', 'chat', 'feedback', 'help', 'project-home']),
|
|
27
|
+
purpose: z.string().min(1),
|
|
28
|
+
whenToUse: z.string().min(1),
|
|
29
|
+
whenNotToUse: z.string().min(1),
|
|
30
|
+
exampleArgs: z.record(z.string(), z.unknown()),
|
|
31
|
+
notes: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const RecipeSchema = z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
title: z.string().min(1),
|
|
37
|
+
steps: z.array(z.string().min(1)).min(1),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const MCP_TOOL_CATALOG = [
|
|
41
|
+
{
|
|
42
|
+
name: 'scheduler_reset_job',
|
|
43
|
+
group: 'scheduler',
|
|
44
|
+
purpose: "Reset a stuck scheduler job by slug via the session-manager app's admin API.",
|
|
45
|
+
whenToUse: 'Use after diagnosing why a job is stuck (e.g. via scheduler_get_prd/scheduler_list_jobs) and deciding it should re-run from pending.',
|
|
46
|
+
whenNotToUse: 'Do not use as a first move on a "needs_review" job without reading it first — reset just clears status, it does not answer the question the job raised.',
|
|
47
|
+
exampleArgs: { slug: 'add-mcp-tool-catalog', force: false },
|
|
48
|
+
notes: 'Refuses a job whose status is already "completed" unless force:true is passed — resetting a completed job re-executes already-shipped work.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'scheduler_list_jobs',
|
|
52
|
+
group: 'scheduler',
|
|
53
|
+
purpose: "List scheduler jobs via the session-manager app's admin API.",
|
|
54
|
+
whenToUse: 'Use for a quick overview of live queue-row status (pending/running/completed/failed/needs_review) across the machine.',
|
|
55
|
+
whenNotToUse: 'For PRD content (frontmatter/body) rather than just job status, or for filtering by cwd/Epic, use scheduler_list_prds instead.',
|
|
56
|
+
exampleArgs: {},
|
|
57
|
+
notes: null,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'scheduler_create_prd',
|
|
61
|
+
group: 'scheduler',
|
|
62
|
+
purpose: 'THE ONLY SANCTIONED WAY to author a PRD. Write a new PRD file via the '
|
|
63
|
+
+ "session-manager app's admin API. Server-side validates the frontmatter, "
|
|
64
|
+
+ 'atomically allocates the NN parallel-group number, appends the engineering '
|
|
65
|
+
+ 'standards, and writes the PRD file to disk. This tool ONLY writes the file — '
|
|
66
|
+
+ 'it does not create a scheduler queue row. The queue row is derived '
|
|
67
|
+
+ "automatically by the scheduler's next reconcile pass (typically within ~1 "
|
|
68
|
+
+ 'minute); the response has `enqueued: false` for exactly this reason. Every PRD '
|
|
69
|
+
+ 'must join an EXISTING, already-human-approved Epic (pass sourcePromptId) — this '
|
|
70
|
+
+ 'tool never mints a new one, and refuses the write if no Epic can be resolved. '
|
|
71
|
+
+ '`cwd` is OPTIONAL when called from inside an Epic session (chat or terminal, '
|
|
72
|
+
+ 'including from inside that Epic\'s own git worktree pwd): the server resolves '
|
|
73
|
+
+ 'the real project from the calling session\'s sourcePromptId/originClaudeSessionId, '
|
|
74
|
+
+ 'never from a worktree\'s own possibly-stale active-index.json snapshot. '
|
|
75
|
+
+ '`tag` and `agentType` are two DIFFERENT, independent fields — `tag` is WHAT KIND '
|
|
76
|
+
+ 'of work this PRD is (feature/bug/build/...); `agentType` is WHO executes it (a '
|
|
77
|
+
+ 'persona name, e.g. "dev-lead"). `agentType` defaults to "dev-lead" when omitted, '
|
|
78
|
+
+ 'and is rejected at write time if it does not name a real persona file.',
|
|
79
|
+
whenToUse: 'Use whenever new work should be queued into an already-approved Epic — this is the /develop path.',
|
|
80
|
+
whenNotToUse: 'TWO DISTINCT FAILURE MODES if this tool is not usable — do not conflate them: '
|
|
81
|
+
+ '(a) this tool call is PRESENT in your tool list but ERRORS as app-not-running / admin '
|
|
82
|
+
+ 'API unreachable — that is the ONLY case where hand-authoring the PRD file directly on '
|
|
83
|
+
+ 'disk is an acceptable DEGRADED, LAST-RESORT fallback; the caller MUST say so explicitly '
|
|
84
|
+
+ 'and visibly in its report (which file, why the tool was unreachable, that it needs '
|
|
85
|
+
+ 'verification) since the server-side validation, atomic NN allocation, and '
|
|
86
|
+
+ 'Epic-existence check this tool performs did not run for that file. '
|
|
87
|
+
+ '(b) this tool is ABSENT from your tool list entirely — you were never offered it, so '
|
|
88
|
+
+ 'there is no error to catch. That means the session-manager-scheduler MCP server is not '
|
|
89
|
+
+ 'registered for this project: a MISCONFIGURATION, not an offline app. In that case DO '
|
|
90
|
+
+ 'NOT hand-write any PRD file — stop and tell the human the MCP server is not registered '
|
|
91
|
+
+ '(fix: `claude mcp add session-manager-scheduler --scope user -- node '
|
|
92
|
+
+ '<session-manager-repo>/scripts/scheduler-mcp-server.cjs`, once at user scope covers '
|
|
93
|
+
+ 'every project).',
|
|
94
|
+
exampleArgs: {
|
|
95
|
+
title: 'Add unit tests for the retry backoff helper',
|
|
96
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
97
|
+
estimateMinutes: 30,
|
|
98
|
+
goal: 'Cover retryWithBackoff.cjs edge cases (zero retries, max-delay clamp) that currently have no test.',
|
|
99
|
+
acceptanceCriteria: ['New test file exercises zero-retry and max-delay-clamp cases', 'timeout 300 npm run typecheck passes', 'timeout 600 npm run test:unit passes'],
|
|
100
|
+
implementationNotes: 'See src/main/lib/retryWithBackoff.cjs and its existing __tests__ sibling for the pattern to extend.',
|
|
101
|
+
sourcePromptId: 'epic-id-of-an-already-approved-session',
|
|
102
|
+
},
|
|
103
|
+
notes: 'See /develop.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'scheduler_list_prds',
|
|
107
|
+
group: 'scheduler',
|
|
108
|
+
purpose: "THE ONLY SUPPORTED WAY to list scheduled PRDs (live + archived) via the session-manager app's admin API. "
|
|
109
|
+
+ 'Each entry includes its real job status (pending/running/completed/failed/needs_review, or null if not yet '
|
|
110
|
+
+ 'queued/reconciled). Optionally filter by project cwd, Epic id, and/or status. Results are paginated (default '
|
|
111
|
+
+ 'limit 100, max 500) sorted by slug ascending — check `hasMore`/`total` in the response before assuming you '
|
|
112
|
+
+ "received every PRD; page further with `offset`. Default fields are compact (no parallelGroup/estimateMinutes/"
|
|
113
|
+
+ 'sourcePromptId/epicId/archivedStatus) — pass fields:"full" to restore them.',
|
|
114
|
+
whenToUse: 'Use to survey PRDs by project/Epic/status before deciding which one to read, update, reset, or archive.',
|
|
115
|
+
whenNotToUse: 'Do not assume a page without `hasMore` is the full result set without checking `total` — page further with `offset` first.',
|
|
116
|
+
exampleArgs: { cwd: '/home/bilko/Projects/session-manager', status: 'needs_review' },
|
|
117
|
+
notes: null,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'scheduler_get_prd',
|
|
121
|
+
group: 'scheduler',
|
|
122
|
+
purpose: "THE ONLY SUPPORTED WAY to read one PRD's full body + parsed frontmatter (live or archived) via the "
|
|
123
|
+
+ "session-manager app's admin API.",
|
|
124
|
+
whenToUse: 'Use before editing/resetting a PRD, or to answer what a needs_review job actually asked.',
|
|
125
|
+
whenNotToUse: 'For a fleet-wide overview instead of one PRD, use scheduler_list_prds.',
|
|
126
|
+
exampleArgs: { slug: 'add-mcp-tool-catalog' },
|
|
127
|
+
notes: null,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'scheduler_update_prd',
|
|
131
|
+
group: 'scheduler',
|
|
132
|
+
purpose: "THE ONLY SUPPORTED WAY to edit a NOT-yet-running PRD's frontmatter and/or body via the session-manager "
|
|
133
|
+
+ 'app\'s admin API. Refuses once a queue row exists for the slug and its status is anything but "pending" '
|
|
134
|
+
+ '(running/completed/failed/needs_review) — editing the spec under a live or already-finished executor is refused, '
|
|
135
|
+
+ 'not silently applied. Only recognized frontmatter keys (title, cwd, estimateMinutes, parallelGroup, '
|
|
136
|
+
+ 'sourcePromptId, sourceTabId, tag) may be patched; unrecognized keys (e.g. dependsOn) round-trip unchanged.',
|
|
137
|
+
whenToUse: 'Use to correct a PRD scope/estimate/tag before it starts running — e.g. before resetting a needs_review job whose spec needs to change.',
|
|
138
|
+
whenNotToUse: 'Do not use once the job is running or terminal (completed/failed/needs_review) without first resetting it back to pending — the route refuses the edit.',
|
|
139
|
+
exampleArgs: { slug: 'add-mcp-tool-catalog', frontmatter: { estimateMinutes: 45 } },
|
|
140
|
+
notes: null,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'scheduler_archive_prd',
|
|
144
|
+
group: 'scheduler',
|
|
145
|
+
purpose: "THE ONLY SUPPORTED WAY to archive one or more PRDs (move to prds-archived/) via the session-manager "
|
|
146
|
+
+ "app's admin API.",
|
|
147
|
+
whenToUse: 'Use once a batch of PRDs is done and should stop showing up in default (live) listings.',
|
|
148
|
+
whenNotToUse: 'Do not archive a PRD you still expect to reset/re-run — archived PRDs are not part of the live scheduling loop.',
|
|
149
|
+
exampleArgs: { slugs: ['add-mcp-tool-catalog'] },
|
|
150
|
+
notes: null,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'scheduler_cancel_job',
|
|
154
|
+
group: 'scheduler',
|
|
155
|
+
purpose: "THE ONLY SUPPORTED WAY to cancel a not-yet-terminal scheduler job via the session-manager app's admin "
|
|
156
|
+
+ 'API. A running job is SIGTERM\'d; a pending job is simply retired. There is no "cancelled" job status, so a '
|
|
157
|
+
+ 'cancelled job lands as "failed" with an error naming the cause. Refuses a slug whose job is already terminal '
|
|
158
|
+
+ '(completed/failed/needs_review) — nothing to cancel.',
|
|
159
|
+
whenToUse: 'Use to stop a running or pending job that should not continue (e.g. it was queued in error, or scope changed underneath it).',
|
|
160
|
+
whenNotToUse: 'Do not use on an already-terminal job (completed/failed/needs_review) — use scheduler_reset_job if it should run again.',
|
|
161
|
+
exampleArgs: { slug: 'add-mcp-tool-catalog' },
|
|
162
|
+
notes: null,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'scheduler_retag_prd',
|
|
166
|
+
group: 'scheduler',
|
|
167
|
+
purpose: "THE ONLY SUPPORTED WAY to rewrite a PRD's parallelGroup and/or estimateMinutes frontmatter (and, if "
|
|
168
|
+
+ "parallelGroup changes, its NN- filename prefix) via the session-manager app's admin API.",
|
|
169
|
+
whenToUse: 'Use to correct an estimate or renumber a PRD file after it was created.',
|
|
170
|
+
whenNotToUse: 'Do not use parallelGroup as an ordering/dependency barrier — it is only a unique-per-PRD display hint; use dependsOn on scheduler_create_prd/scheduler_update_prd for real ordering.',
|
|
171
|
+
exampleArgs: { items: [{ slug: 'add-mcp-tool-catalog', estimateMinutes: 30 }] },
|
|
172
|
+
notes: null,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'chat_send_prompt',
|
|
176
|
+
group: 'chat',
|
|
177
|
+
purpose: "Push a prompt into an already-open tab's chat queue via the session-manager app's admin "
|
|
178
|
+
+ 'API. The renderer resolves the tab (must currently be open) and runs the prompt through '
|
|
179
|
+
+ 'the same queued-vs-immediate path as a manual send.',
|
|
180
|
+
whenToUse: 'Use to programmatically continue a conversation in a tab that is already open in the running app.',
|
|
181
|
+
whenNotToUse: 'Do not use to start new work in a project you are not already chatting in — that is /develop (scheduler_create_prd) or feedback_open_session.',
|
|
182
|
+
exampleArgs: { tabId: 'a1b2c3d4-tab-id', prompt: 'Please continue with the next step.' },
|
|
183
|
+
notes: 'No-ops if the tab is unknown/closed.',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'feedback_list_projects',
|
|
187
|
+
group: 'feedback',
|
|
188
|
+
purpose: 'List the OTHER projects on this machine that can receive feedback (i.e. that Session Manager '
|
|
189
|
+
+ 'already manages — they have a session-manager-operations/ directory).',
|
|
190
|
+
whenToUse: "Call this FIRST when you need the exact `toCwd` for feedback_open_session and are not certain of it — never guess a path.",
|
|
191
|
+
whenNotToUse: 'A project missing from this list has simply never been opened in Session Manager; ask the human to open it once rather than inventing a path.',
|
|
192
|
+
exampleArgs: {},
|
|
193
|
+
notes: null,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'feedback_open_session',
|
|
197
|
+
group: 'feedback',
|
|
198
|
+
purpose: 'THE ONLY SANCTIONED WAY to hand a finding from THIS project to a DIFFERENT project. Opens a new '
|
|
199
|
+
+ "PROPOSED session in the receiving project's own Sessions queue, carrying your report as its "
|
|
200
|
+
+ 'opening prompt and stamped with where it came from. Session Manager performs the cross-folder '
|
|
201
|
+
+ 'write; you never write another project\'s files yourself.',
|
|
202
|
+
whenToUse: 'Use when work in this project surfaces something that is genuinely another project\'s to fix. '
|
|
203
|
+
+ 'Write the report for a reader who has never seen your project: state the symptom, where you '
|
|
204
|
+
+ 'observed it, what you expected, and (if you know) the file in THEIR repo that looks responsible. '
|
|
205
|
+
+ 'Never assume they can see your code.',
|
|
206
|
+
whenNotToUse: 'WHEN NOT TO USE IT: for work in the project you are ALREADY in, run /develop inside the Epic you '
|
|
207
|
+
+ 'are already in — this tool refuses toCwd === fromCwd outright.',
|
|
208
|
+
exampleArgs: {
|
|
209
|
+
toCwd: '/home/bilko/Projects/other-project',
|
|
210
|
+
fromCwd: '/home/bilko/Projects/session-manager',
|
|
211
|
+
title: 'Cross-project contract mismatch in the shared admin API',
|
|
212
|
+
body: 'Symptom: X. Observed while doing Y in session-manager. Expected: Z. Suspected file: <path in their repo>.',
|
|
213
|
+
},
|
|
214
|
+
notes: 'WHAT THIS DOES NOT DO: it does not start anything, queue a PRD, or spend a token. The session '
|
|
215
|
+
+ 'lands as `proposed` and runs only if a human in the RECEIVING project presses "Approve & start". '
|
|
216
|
+
+ 'There is no callback and no reply channel — do not wait for an answer, and do not tell the user '
|
|
217
|
+
+ 'the other project has "been fixed" or "is working on it". Report only that the proposal was '
|
|
218
|
+
+ 'delivered.',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'project_home_get_contract',
|
|
222
|
+
group: 'project-home',
|
|
223
|
+
purpose: "Fetch the self-sufficient contract for generating this project's Project Home pages via the "
|
|
224
|
+
+ "session-manager app's admin API — the protocol steps, the ProjectPageSummary/ProjectPagePicks JSON "
|
|
225
|
+
+ 'schemas, the component catalog (all 5 lenses: home, marketing, feature, architecture, brief), the '
|
|
226
|
+
+ 'absolute output paths, and the full pipeline spec text. Nothing in the response requires reading '
|
|
227
|
+
+ "session-manager's own repo — this is what makes generation work on a machine with only the npm "
|
|
228
|
+
+ 'package installed.',
|
|
229
|
+
whenToUse: 'Call this FIRST, before composing any ProjectPageSummary content or picking variants, whenever '
|
|
230
|
+
+ 'a project-home-builder session starts generating Project Home pages for a project.',
|
|
231
|
+
whenNotToUse: 'Do not begin composing a ProjectPageSummary or choosing variants before calling this — it '
|
|
232
|
+
+ 'must be the FIRST call a project-home-builder session makes; every other project_home_* tool assumes '
|
|
233
|
+
+ "you already have this response's schema/catalog/paths in hand.",
|
|
234
|
+
exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
|
|
235
|
+
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'project_home_validate_summary',
|
|
239
|
+
group: 'project-home',
|
|
240
|
+
purpose: 'Validate a composed ProjectPageSummary against the schema from project_home_get_contract via the '
|
|
241
|
+
+ "session-manager app's admin API, without writing anything to disk.",
|
|
242
|
+
whenToUse: 'Use after composing (or editing) a ProjectPageSummary and before calling project_home_render — '
|
|
243
|
+
+ 'fix every returned {field, message} error and re-validate until valid:true.',
|
|
244
|
+
whenNotToUse: 'Do not skip straight to project_home_render on an unvalidated summary — render does '
|
|
245
|
+
+ 're-validate server-side and rejects with no writes on failure, but catching errors here first saves a '
|
|
246
|
+
+ 'round trip and gives per-field detail sooner.',
|
|
247
|
+
exampleArgs: { cwd: '/home/bilko/Projects/session-manager', summary: { title: 'Example Project' } },
|
|
248
|
+
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'project_home_render',
|
|
252
|
+
group: 'project-home',
|
|
253
|
+
purpose: 'Render and write all 5 Project Home lenses (home, marketing, feature, architecture, brief) plus '
|
|
254
|
+
+ "summary.json/picks.json/manifest.json via the session-manager app's admin API — the only write path "
|
|
255
|
+
+ 'for Project Home generation.',
|
|
256
|
+
whenToUse: 'Use once project_home_validate_summary reports valid:true and a variant has been picked for '
|
|
257
|
+
+ "every lens/slot in the contract's catalog.",
|
|
258
|
+
whenNotToUse: 'Must NOT be used to fabricate summary content — every field of the summary passed here must '
|
|
259
|
+
+ 'trace to something concrete about the real project (an Epic goal, a source file/dir, a convention, a '
|
|
260
|
+
+ 'git log entry). This tool re-validates the summary server-side and rejects with no writes on schema '
|
|
261
|
+
+ 'failure, but it only checks shape, never truthfulness.',
|
|
262
|
+
exampleArgs: {
|
|
263
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
264
|
+
summary: { title: 'Example Project' },
|
|
265
|
+
picks: { home: { hero: 'variant-a' } },
|
|
266
|
+
},
|
|
267
|
+
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'project_home_status',
|
|
271
|
+
group: 'project-home',
|
|
272
|
+
purpose: "Report what already exists for this project's Project Home generation via the session-manager "
|
|
273
|
+
+ "app's admin API — summary.json/picks.json/each lens file's existence + mtime, and manifest.json's "
|
|
274
|
+
+ 'generatedAt.',
|
|
275
|
+
whenToUse: 'Use before starting generation to see prior state, and again after project_home_render to '
|
|
276
|
+
+ 'confirm the new files landed.',
|
|
277
|
+
whenNotToUse: 'Do not treat an all-absent result as an error — a project that has never generated a '
|
|
278
|
+
+ 'Project Home page yet still returns a well-formed status with every exists:false rather than failing.',
|
|
279
|
+
exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
|
|
280
|
+
notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'session_manager_help',
|
|
284
|
+
group: 'help',
|
|
285
|
+
purpose: 'THE ENTRY POINT — call this tool FIRST whenever you are unsure which session-manager-scheduler '
|
|
286
|
+
+ 'tool to reach for, or a call to this server just failed. Returns the same tool catalog every other '
|
|
287
|
+
+ "tool's description is composed from, this project's multi-tool recipes, and this machine's "
|
|
288
|
+
+ 'delegation-readiness state (tells apart "not registered", "registered but dead" — the process '
|
|
289
|
+
+ 'won\'t even answer tools/list — and "registered and answering").',
|
|
290
|
+
whenToUse: 'Call with no arguments for the full grouped tool list plus recipe titles. Pass `tool` (a tool '
|
|
291
|
+
+ 'name) for that one tool\'s full entry, including its exampleArgs. Pass `topic` (a recipe id) for that '
|
|
292
|
+
+ 'recipe\'s step-by-step instructions. Pass both to get both sections in one call.',
|
|
293
|
+
whenNotToUse: 'Do not use this to actually perform scheduler/chat/feedback work — it is read-only '
|
|
294
|
+
+ "documentation and never a substitute for calling the tool it describes.",
|
|
295
|
+
exampleArgs: {},
|
|
296
|
+
notes: 'Always returns the static catalog even when the session-manager app is not running — only the '
|
|
297
|
+
+ '`readiness` section of the response depends on the admin API being reachable.',
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const MCP_RECIPES = [
|
|
302
|
+
{
|
|
303
|
+
id: 'queue-work-via-develop',
|
|
304
|
+
title: 'Queue work into an already-approved Epic (/develop)',
|
|
305
|
+
steps: [
|
|
306
|
+
'Confirm an Epic already exists for this work (proposed or active) — scheduler_create_prd never mints one.',
|
|
307
|
+
'Call scheduler_create_prd with sourcePromptId set to that Epic id, plus title/cwd/estimateMinutes/goal/acceptanceCriteria/implementationNotes.',
|
|
308
|
+
'The response has enqueued:false — the queue row is derived by the scheduler\'s next reconcile pass (~1 minute), not by this call.',
|
|
309
|
+
'Optionally call scheduler_list_prds with cwd/status to confirm the PRD picked up a job row.',
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: 'unstick-needs-review-job',
|
|
314
|
+
title: 'Unstick a job stuck in needs_review',
|
|
315
|
+
steps: [
|
|
316
|
+
'Call scheduler_list_prds with status:"needs_review" (or scheduler_list_jobs) to find the stuck slug.',
|
|
317
|
+
'Call scheduler_get_prd with that slug to read its full frontmatter + body and understand the question it raised.',
|
|
318
|
+
'If the PRD spec needs to change, call scheduler_update_prd with the slug and a frontmatter/body patch — this is only accepted while the job is not yet running or terminal.',
|
|
319
|
+
'Call scheduler_reset_job with { slug } to clear the needs_review status back to pending — force is only required if the job had already reached "completed".',
|
|
320
|
+
'The next scheduler reconcile pass re-queues the job; confirm with scheduler_list_jobs or scheduler_list_prds.',
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'hand-finding-to-another-project',
|
|
325
|
+
title: 'Hand a finding to another project',
|
|
326
|
+
steps: [
|
|
327
|
+
'Call feedback_list_projects to get the exact toCwd for the receiving project — never guess a path.',
|
|
328
|
+
'Call feedback_open_session with toCwd, fromCwd, a one-line title, and a self-contained body (symptom, where observed, expected behavior, suspected cause).',
|
|
329
|
+
'The call only delivers a PROPOSED session in the receiving project — nothing runs until a human there presses Approve & start; there is no reply channel.',
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'generate-project-home',
|
|
334
|
+
title: "Generate a project's Project Home pages",
|
|
335
|
+
steps: [
|
|
336
|
+
"Call project_home_get_contract to get the protocol, ProjectPageSummary/ProjectPagePicks schemas, "
|
|
337
|
+
+ 'component catalog, absolute output paths, and pipeline spec — the FIRST call any '
|
|
338
|
+
+ 'project-home-builder session makes.',
|
|
339
|
+
'Author a ProjectPageSummary matching the contract\'s summarySchema — every field must trace to '
|
|
340
|
+
+ 'something concrete about the real project; never fabricate content.',
|
|
341
|
+
'Call project_home_validate_summary with { cwd, summary } and fix every {field, message} error it '
|
|
342
|
+
+ 'returns, re-validating until valid:true.',
|
|
343
|
+
"For each lens and slot in the contract's catalog, choose exactly one variant id by judging the "
|
|
344
|
+
+ 'candidate variants against the summary you composed, assembling a ProjectPagePicks object '
|
|
345
|
+
+ '(lensId -> slotId -> variantId).',
|
|
346
|
+
'Call project_home_render with { cwd, summary, picks } to render all 5 lenses and write '
|
|
347
|
+
+ 'summary.json/picks.json/output files.',
|
|
348
|
+
"Call project_home_status to confirm the new files landed and read manifest.json's generatedAt.",
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
for (const entry of MCP_TOOL_CATALOG) {
|
|
354
|
+
CatalogEntrySchema.parse(entry);
|
|
355
|
+
}
|
|
356
|
+
for (const recipe of MCP_RECIPES) {
|
|
357
|
+
RecipeSchema.parse(recipe);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function composeDescription(entry) {
|
|
361
|
+
return [entry.purpose, entry.whenToUse, entry.whenNotToUse, entry.notes].filter(Boolean).join(' ');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
MCP_TOOL_CATALOG,
|
|
366
|
+
MCP_RECIPES,
|
|
367
|
+
CatalogEntrySchema,
|
|
368
|
+
RecipeSchema,
|
|
369
|
+
composeDescription,
|
|
370
|
+
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
const { assertOpsWrite } = require('./opsOwnership.cjs');
|
|
17
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
17
18
|
|
|
18
19
|
// Same redaction policy as logs.cjs's sanitizeMeta — kept independent (not
|
|
19
20
|
// shared/required) so this module has no dependency on the Electron `app`
|
|
@@ -31,7 +32,8 @@ function sanitizeMeta(meta) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function logsDir(cwd) {
|
|
34
|
-
|
|
35
|
+
const { opsPath } = require('./opsOwnership.cjs');
|
|
36
|
+
return opsPath(cwd, 'logs');
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function todayFile(cwd) {
|
|
@@ -58,6 +60,15 @@ function todayFile(cwd) {
|
|
|
58
60
|
*/
|
|
59
61
|
function appendError({ cwd, scope, level = 'error', tabId, epicId, tags = [], message, meta }) {
|
|
60
62
|
if (!cwd || typeof cwd !== 'string') return; // no project to attribute this line to — skip
|
|
63
|
+
if (isEphemeralCwd(cwd)) {
|
|
64
|
+
// A worktree is torn down when its Epic/job ends and os.tmpdir() is
|
|
65
|
+
// scratch space either way — never materialize logs there. See
|
|
66
|
+
// ephemeralCwd.cjs / queueStore.cjs's projectStateDir for the sibling
|
|
67
|
+
// refusal on the scheduler namespace (verified live 2026-09-01 as a
|
|
68
|
+
// recreated /tmp/session-manager-operations/logs/ tree).
|
|
69
|
+
console.warn(`[opsErrorLog] appendError: refusing ephemeral cwd "${cwd}" (scope=${scope || 'unknown'})`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
61
72
|
const file = todayFile(cwd);
|
|
62
73
|
try {
|
|
63
74
|
assertOpsWrite(file, 'logs');
|
|
@@ -48,6 +48,18 @@ const OWNERS = Object.freeze({
|
|
|
48
48
|
'scheduler': 'scheduler',
|
|
49
49
|
// Project Home owns the synthesized Brief (generate + hand-edit).
|
|
50
50
|
'project-brief': 'project-home',
|
|
51
|
+
// Project Home also owns the app-side render path for Project Pages
|
|
52
|
+
// (summary.json/picks.json/output/* written via the
|
|
53
|
+
// /admin/project-home/render route, config.cjs's writeJson). This is a
|
|
54
|
+
// NARROWER claim than "project-home owns everything under this folder":
|
|
55
|
+
// the SAME folder is also written directly by a project-home-builder
|
|
56
|
+
// Epic's own Write tool, which never goes through config.cjs and so is
|
|
57
|
+
// NOT governed by this table at all (see
|
|
58
|
+
// session-manager-operations/project-pages/README.md). Declaring the
|
|
59
|
+
// namespace here only closes the fail-closed gap for the app's own write
|
|
60
|
+
// path — it does not claim exclusivity over the agent's Write-tool calls,
|
|
61
|
+
// which remain outside this law by construction.
|
|
62
|
+
'project-pages': 'project-home',
|
|
51
63
|
// Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
|
|
52
64
|
'logs': 'logs',
|
|
53
65
|
// Host on Bilko.run tab's deterministic bundle prep (dist/index.html +
|
|
@@ -158,6 +170,97 @@ function checkOpsWrite(absPath, writer) {
|
|
|
158
170
|
function assertOpsWrite(absPath, writer) {
|
|
159
171
|
const verdict = checkOpsWrite(absPath, writer);
|
|
160
172
|
if (!verdict.ok) throw new Error(verdict.error);
|
|
173
|
+
// Ownership is one question; WHERE the ops root lives is another. A writer
|
|
174
|
+
// that bypasses the path helpers below and builds its own absolute path can
|
|
175
|
+
// still aim at an ephemeral project root (a worktree, os.tmpdir()) — refuse
|
|
176
|
+
// that here too so the single-writer gate is also the last line against the
|
|
177
|
+
// worktree-cwd hazard (PRD 1082; incidents 2026-08-30, 2026-09-01).
|
|
178
|
+
const { inOps } = parseOpsPath(absPath);
|
|
179
|
+
if (inOps) {
|
|
180
|
+
const segs = absPath.split(path.sep);
|
|
181
|
+
const idx = segs.lastIndexOf(OPS_ROOT_DIR);
|
|
182
|
+
const projectRoot = segs.slice(0, idx).join(path.sep) || path.sep;
|
|
183
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
184
|
+
if (isEphemeralCwd(projectRoot)) {
|
|
185
|
+
const err = new Error(
|
|
186
|
+
`refusing to write ${OPS_ROOT_DIR}/ state under an ephemeral project root `
|
|
187
|
+
+ `(tmpdir or linked git worktree): "${projectRoot}" — see lib/opsOwnership.cjs resolveProjectRoot`,
|
|
188
|
+
);
|
|
189
|
+
err.ephemeral = true;
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── THE ONE ops-root resolver (PRD 1082) ─────────────────────────────────
|
|
196
|
+
//
|
|
197
|
+
// Every namespace used to compute its own path with a bare
|
|
198
|
+
// `path.join(cwd, 'session-manager-operations', ...)`, each one independently
|
|
199
|
+
// trusting its `cwd`. The worktree / ops-internal cwd hazard therefore had to
|
|
200
|
+
// be fixed one call site at a time (activeSessions 2026-08-30, PRDs 1073,
|
|
201
|
+
// 1074, 1081). This is the single choke point every reader and writer of the
|
|
202
|
+
// ops root goes through instead — normalize what can be normalized, refuse
|
|
203
|
+
// what cannot — so the next stray cwd is caught by construction, not by
|
|
204
|
+
// incident. `scripts/ops-sweep.cjs` lints that no other file in src/main/
|
|
205
|
+
// spells the ops-root literal.
|
|
206
|
+
//
|
|
207
|
+
// Normalization is `activeSessions.projectRootOf`: an ops-internal path is
|
|
208
|
+
// truncated to the project above it, a linked git worktree is mapped to its
|
|
209
|
+
// main tree via the `.git` file's `gitdir:` pointer, and a subdirectory of a
|
|
210
|
+
// git repo resolves to that repo's root. A plain directory with no enclosing
|
|
211
|
+
// `.git` is returned unchanged (byte-identical to the old join). A nested
|
|
212
|
+
// project that IS its own git repo (e.g. Apple/01-Shapes-Foundation) keeps its
|
|
213
|
+
// own root — the walk stops at the first `.git` it meets.
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* resolveProjectRoot(cwd, { opsInternal }) → absolute project root.
|
|
217
|
+
*
|
|
218
|
+
* Throws on a missing/non-string/relative cwd (a caller that cannot name an
|
|
219
|
+
* absolute project has no business touching that project's state), and
|
|
220
|
+
* throws a tagged `err.ephemeral = true` when the normalized root is still
|
|
221
|
+
* inside os.tmpdir() or is a linked worktree root (ephemeralCwd.cjs).
|
|
222
|
+
*
|
|
223
|
+
* `opsInternal` — 'normalize' (default): a cwd inside session-manager-
|
|
224
|
+
* operations/ is truncated up to its project, the reader-friendly behaviour
|
|
225
|
+
* activeSessions already has. 'refuse': throw instead — the fail-closed
|
|
226
|
+
* posture a WRITER wants (queueStore.projectStateDir), since silently
|
|
227
|
+
* redirecting a write target is riskier than refusing it.
|
|
228
|
+
*/
|
|
229
|
+
function resolveProjectRoot(cwd, { opsInternal = 'normalize' } = {}) {
|
|
230
|
+
if (!cwd || typeof cwd !== 'string') throw new Error('resolveProjectRoot: cwd is required');
|
|
231
|
+
if (!path.isAbsolute(cwd)) {
|
|
232
|
+
throw new Error(`resolveProjectRoot: cwd must be an absolute path, got "${cwd}"`);
|
|
233
|
+
}
|
|
234
|
+
if (opsInternal === 'refuse' && cwd.split(path.sep).includes(OPS_ROOT_DIR)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`resolveProjectRoot: cwd must be a project root, not a path inside ${OPS_ROOT_DIR}/, got "${cwd}"`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
// Lazy: activeSessions → gitWorktree → (lazily) config.cjs → this module.
|
|
240
|
+
const { projectRootOf } = require('../../../scripts/lib/activeSessions.cjs');
|
|
241
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
242
|
+
const root = projectRootOf(cwd);
|
|
243
|
+
if (isEphemeralCwd(root)) {
|
|
244
|
+
const err = new Error(
|
|
245
|
+
`resolveProjectRoot: refusing ephemeral cwd (tmpdir or linked git worktree), got "${cwd}"`,
|
|
246
|
+
);
|
|
247
|
+
err.ephemeral = true;
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
return root;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** resolveOpsRoot(cwd, opts) → `<projectRoot>/session-manager-operations`. */
|
|
254
|
+
function resolveOpsRoot(cwd, opts) {
|
|
255
|
+
return path.join(resolveProjectRoot(cwd, opts), OPS_ROOT_DIR);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* opsPath(cwd, ...segments) → `<projectRoot>/session-manager-operations/<segments>`.
|
|
260
|
+
* The only sanctioned way to build a path inside a project's ops root.
|
|
261
|
+
*/
|
|
262
|
+
function opsPath(cwd, ...segments) {
|
|
263
|
+
return path.join(resolveOpsRoot(cwd), ...segments);
|
|
161
264
|
}
|
|
162
265
|
|
|
163
266
|
module.exports = {
|
|
@@ -167,4 +270,7 @@ module.exports = {
|
|
|
167
270
|
parseOpsPath,
|
|
168
271
|
checkOpsWrite,
|
|
169
272
|
assertOpsWrite,
|
|
273
|
+
resolveProjectRoot,
|
|
274
|
+
resolveOpsRoot,
|
|
275
|
+
opsPath,
|
|
170
276
|
};
|
|
@@ -29,6 +29,9 @@ const { schemas } = require('../ipcSchemas.cjs');
|
|
|
29
29
|
const { readBody, sendJson } = require('./localAdminHttp.cjs');
|
|
30
30
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
31
31
|
const queueOps = require('../queueOps.cjs');
|
|
32
|
+
const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./mcpToolCatalog.cjs');
|
|
33
|
+
const { checkDelegationReadiness } = require('./delegationReadiness.cjs');
|
|
34
|
+
const { resolveProjectContext } = require('./projectRootResolve.cjs');
|
|
32
35
|
|
|
33
36
|
function parseJsonBody(raw) {
|
|
34
37
|
try {
|
|
@@ -45,6 +48,41 @@ function parseJsonBody(raw) {
|
|
|
45
48
|
* scheduler.cjs's own registerAdminRoute(s) pattern.
|
|
46
49
|
*/
|
|
47
50
|
function registerAdminRoute(adminHttp, remote) {
|
|
51
|
+
// GET /admin/mcp/catalog — read-only surface for mcpToolCatalog.cjs, the
|
|
52
|
+
// single source of truth scheduler-mcp-server.cjs's tool descriptions are
|
|
53
|
+
// now composed from (PRD: agent-facing help + Home-tab surface). No
|
|
54
|
+
// `remote` dependency; kept in this file only because it registers
|
|
55
|
+
// "alongside the existing PRD admin routes".
|
|
56
|
+
adminHttp.registerRoute('GET', '/admin/mcp/catalog', async (req, res) => {
|
|
57
|
+
sendJson(res, 200, { ok: true, tools: MCP_TOOL_CATALOG, recipes: MCP_RECIPES });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// GET /admin/mcp/readiness?cwd=... — session_manager_help's live half: the
|
|
61
|
+
// catalog above is static and always answerable in-process, but "is this
|
|
62
|
+
// MCP server actually wired up for this project" needs the same four
|
|
63
|
+
// checks delegationReadiness.cjs already runs for the New Epic readiness
|
|
64
|
+
// banner. `cwd` is a required query param (this server has no notion of
|
|
65
|
+
// "the" project — one admin API instance serves every open tab/project).
|
|
66
|
+
adminHttp.registerRoute('GET', '/admin/mcp/readiness', async (req, res, query) => {
|
|
67
|
+
let input;
|
|
68
|
+
try {
|
|
69
|
+
input = schemas.delegationReadinessCwd.parse(Object.fromEntries(query ?? []));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
sendJson(res, 400, { ok: false, error: 'invalid query', details: e?.issues ?? e?.message });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Normalize a worktree/ops-internal cwd to its real project root first —
|
|
75
|
+
// session_manager_help called from inside an Epic's worktree must report
|
|
76
|
+
// THAT project's readiness, not a worktree checkout's own (nonexistent)
|
|
77
|
+
// config state. See projectRootResolve.cjs's header.
|
|
78
|
+
const resolved = resolveProjectContext({ cwd: input.cwd });
|
|
79
|
+
const result = await checkDelegationReadiness({ cwd: resolved.cwd || input.cwd });
|
|
80
|
+
// `ok: true` means "the route answered"; `ready`/`checks` carry the
|
|
81
|
+
// actual delegation-readiness verdict — kept distinct so a caller can't
|
|
82
|
+
// mistake "readiness is false" for "the request itself failed".
|
|
83
|
+
sendJson(res, 200, { ok: true, ready: result.ok, checks: result.checks });
|
|
84
|
+
});
|
|
85
|
+
|
|
48
86
|
// GET /admin/scheduler/prds — paginated PRD listing (PRD: 353KB/120s
|
|
49
87
|
// fix). Sort order is stable and explicit: slug ascending via
|
|
50
88
|
// String.localeCompare(..., { numeric: true }) (listPrdsInternal in
|
|
@@ -121,7 +159,7 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
121
159
|
sendJson(res, 400, { ok: false, error: 'invalid payload', details: e?.issues ?? e?.message });
|
|
122
160
|
return;
|
|
123
161
|
}
|
|
124
|
-
const result = await queueOps.archiveMany(input.slugs);
|
|
162
|
+
const result = await queueOps.archiveMany(input.slugs, input.cwd);
|
|
125
163
|
// Audit only the slugs that actually archived — archiveMany's `results`
|
|
126
164
|
// array carries the real per-slug outcome; logging every requested slug
|
|
127
165
|
// regardless of whether it succeeded would misrepresent a partial-failure
|
|
@@ -147,9 +185,11 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
147
185
|
sendJson(res, 400, { ok: false, error: 'invalid payload', details: e?.issues ?? e?.message });
|
|
148
186
|
return;
|
|
149
187
|
}
|
|
150
|
-
const result = await remote.cancelJob(input.slug);
|
|
188
|
+
const result = await remote.cancelJob(input.slug, { cwd: input.cwd });
|
|
151
189
|
if (!result.ok) {
|
|
152
|
-
const status =
|
|
190
|
+
const status = result.error === 'invalid slug' ? 400
|
|
191
|
+
: /unknown slug|not found/i.test(result.error ?? '') ? 404
|
|
192
|
+
: 409;
|
|
153
193
|
sendJson(res, status, result);
|
|
154
194
|
return;
|
|
155
195
|
}
|