@qodeca/xezar 0.13.1 → 0.14.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 (250) hide show
  1. package/README.md +45 -15
  2. package/dist/agent-config/account-identity.d.ts +2 -8
  3. package/dist/agent-config/account-identity.js +54 -10
  4. package/dist/agent-config/account-identity.js.map +1 -1
  5. package/dist/agent-config/catalog.d.ts +49 -7
  6. package/dist/agent-config/catalog.js +107 -0
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/model-settings/pi.d.ts +12 -6
  9. package/dist/agent-config/model-settings/pi.js +12 -6
  10. package/dist/agent-config/model-settings/pi.js.map +1 -1
  11. package/dist/agent-config/seed.js +10 -0
  12. package/dist/agent-config/seed.js.map +1 -1
  13. package/dist/contract/github.d.ts +15 -0
  14. package/dist/contract/index.d.ts +9 -0
  15. package/dist/contract/index.js +1527 -895
  16. package/dist/contract/mcp-api-reference.d.ts +174 -0
  17. package/dist/contract/mcp-audit.d.ts +103 -0
  18. package/dist/contract/mcp-discovery.d.ts +257 -0
  19. package/dist/contract/mcp-event-catalog.d.ts +121 -0
  20. package/dist/contract/mcp-event-catalog.test.d.ts +1 -0
  21. package/dist/contract/mcp-idempotency.d.ts +200 -0
  22. package/dist/contract/mcp-journal.d.ts +268 -0
  23. package/dist/contract/mcp-leader.d.ts +88 -0
  24. package/dist/contract/mcp-ownership.d.ts +78 -0
  25. package/dist/contract/mcp-versioning.d.ts +139 -0
  26. package/dist/contract/runs.d.ts +81 -0
  27. package/dist/core/agent-profiles.d.ts +15 -4
  28. package/dist/core/agent-profiles.js +22 -8
  29. package/dist/core/agent-profiles.js.map +1 -1
  30. package/dist/core/opencode-server-runner.js +71 -11
  31. package/dist/core/opencode-server-runner.js.map +1 -1
  32. package/dist/core/secret-redaction.d.ts +7 -1
  33. package/dist/core/secret-redaction.js +44 -10
  34. package/dist/core/secret-redaction.js.map +1 -1
  35. package/dist/index.js +138 -27
  36. package/dist/index.js.map +1 -1
  37. package/dist/machine-identity.d.ts +66 -0
  38. package/dist/machine-identity.js +96 -0
  39. package/dist/machine-identity.js.map +1 -0
  40. package/dist/mcp/adapters/claude-code.d.ts +41 -0
  41. package/dist/mcp/adapters/claude-code.js +40 -0
  42. package/dist/mcp/adapters/claude-code.js.map +1 -0
  43. package/dist/mcp/adapters/codex.d.ts +140 -0
  44. package/dist/mcp/adapters/codex.js +323 -0
  45. package/dist/mcp/adapters/codex.js.map +1 -0
  46. package/dist/mcp/adapters/opencode.d.ts +148 -0
  47. package/dist/mcp/adapters/opencode.js +528 -0
  48. package/dist/mcp/adapters/opencode.js.map +1 -0
  49. package/dist/mcp/adapters/pi-link.d.ts +102 -0
  50. package/dist/mcp/adapters/pi-link.js +235 -0
  51. package/dist/mcp/adapters/pi-link.js.map +1 -0
  52. package/dist/mcp/adapters/pi.d.ts +188 -0
  53. package/dist/mcp/adapters/pi.js +418 -0
  54. package/dist/mcp/adapters/pi.js.map +1 -0
  55. package/dist/mcp/api-reference.d.ts +26 -0
  56. package/dist/mcp/api-reference.js +133 -0
  57. package/dist/mcp/api-reference.js.map +1 -0
  58. package/dist/mcp/audit-trail.d.ts +162 -0
  59. package/dist/mcp/audit-trail.js +306 -0
  60. package/dist/mcp/audit-trail.js.map +1 -0
  61. package/dist/mcp/bridge.d.ts +81 -0
  62. package/dist/mcp/bridge.js +407 -0
  63. package/dist/mcp/bridge.js.map +1 -0
  64. package/dist/mcp/connection-file.d.ts +49 -0
  65. package/dist/mcp/connection-file.js +63 -0
  66. package/dist/mcp/connection-file.js.map +1 -0
  67. package/dist/mcp/echo-guard.d.ts +94 -0
  68. package/dist/mcp/echo-guard.js +160 -0
  69. package/dist/mcp/echo-guard.js.map +1 -0
  70. package/dist/mcp/event-catalog.d.ts +120 -0
  71. package/dist/mcp/event-catalog.js +306 -0
  72. package/dist/mcp/event-catalog.js.map +1 -0
  73. package/dist/mcp/event-controller.d.ts +230 -0
  74. package/dist/mcp/event-controller.js +596 -0
  75. package/dist/mcp/event-controller.js.map +1 -0
  76. package/dist/mcp/event-journal.d.ts +57 -0
  77. package/dist/mcp/event-journal.js +423 -0
  78. package/dist/mcp/event-journal.js.map +1 -0
  79. package/dist/mcp/index.d.ts +63 -0
  80. package/dist/mcp/index.js +386 -0
  81. package/dist/mcp/index.js.map +1 -0
  82. package/dist/mcp/ipc.d.ts +202 -0
  83. package/dist/mcp/ipc.js +206 -0
  84. package/dist/mcp/ipc.js.map +1 -0
  85. package/dist/mcp/leader-delivery.d.ts +119 -0
  86. package/dist/mcp/leader-delivery.js +450 -0
  87. package/dist/mcp/leader-delivery.js.map +1 -0
  88. package/dist/mcp/operation-receipts.d.ts +210 -0
  89. package/dist/mcp/operation-receipts.js +723 -0
  90. package/dist/mcp/operation-receipts.js.map +1 -0
  91. package/dist/mcp/project-catalogs.d.ts +23 -0
  92. package/dist/mcp/project-catalogs.js +25 -0
  93. package/dist/mcp/project-catalogs.js.map +1 -0
  94. package/dist/mcp/project-leaders.d.ts +26 -0
  95. package/dist/mcp/project-leaders.js +17 -0
  96. package/dist/mcp/project-leaders.js.map +1 -0
  97. package/dist/mcp/protocol.d.ts +50 -0
  98. package/dist/mcp/protocol.js +47 -0
  99. package/dist/mcp/protocol.js.map +1 -0
  100. package/dist/mcp/reconnect.d.ts +297 -0
  101. package/dist/mcp/reconnect.js +465 -0
  102. package/dist/mcp/reconnect.js.map +1 -0
  103. package/dist/mcp/resource-ownership.d.ts +228 -0
  104. package/dist/mcp/resource-ownership.js +390 -0
  105. package/dist/mcp/resource-ownership.js.map +1 -0
  106. package/dist/mcp/service-adapter.d.ts +16488 -0
  107. package/dist/mcp/service-adapter.js +148 -0
  108. package/dist/mcp/service-adapter.js.map +1 -0
  109. package/dist/mcp/service.d.ts +83 -0
  110. package/dist/mcp/service.js +233 -0
  111. package/dist/mcp/service.js.map +1 -0
  112. package/dist/mcp/session-binding.d.ts +105 -0
  113. package/dist/mcp/session-binding.js +163 -0
  114. package/dist/mcp/session-binding.js.map +1 -0
  115. package/dist/mcp/stale-write.d.ts +145 -0
  116. package/dist/mcp/stale-write.js +219 -0
  117. package/dist/mcp/stale-write.js.map +1 -0
  118. package/dist/mcp/tool.d.ts +48 -0
  119. package/dist/mcp/tool.js +24 -0
  120. package/dist/mcp/tool.js.map +1 -0
  121. package/dist/mcp/tools/discovery.d.ts +40 -0
  122. package/dist/mcp/tools/discovery.js +232 -0
  123. package/dist/mcp/tools/discovery.js.map +1 -0
  124. package/dist/mcp/tools/execution-control.d.ts +171 -0
  125. package/dist/mcp/tools/execution-control.js +513 -0
  126. package/dist/mcp/tools/execution-control.js.map +1 -0
  127. package/dist/mcp/tools/handoff-git.d.ts +26 -0
  128. package/dist/mcp/tools/handoff-git.js +544 -0
  129. package/dist/mcp/tools/handoff-git.js.map +1 -0
  130. package/dist/mcp/tools/index.d.ts +2 -0
  131. package/dist/mcp/tools/index.js +23 -0
  132. package/dist/mcp/tools/index.js.map +1 -0
  133. package/dist/mcp/tools/leader-events.d.ts +46 -0
  134. package/dist/mcp/tools/leader-events.js +126 -0
  135. package/dist/mcp/tools/leader-events.js.map +1 -0
  136. package/dist/mcp/tools/local-handoff.d.ts +102 -0
  137. package/dist/mcp/tools/local-handoff.js +306 -0
  138. package/dist/mcp/tools/local-handoff.js.map +1 -0
  139. package/dist/mcp/tools/project-config.d.ts +340 -0
  140. package/dist/mcp/tools/project-config.js +1075 -0
  141. package/dist/mcp/tools/project-config.js.map +1 -0
  142. package/dist/mcp/tools/results-evidence.d.ts +112 -0
  143. package/dist/mcp/tools/results-evidence.js +1045 -0
  144. package/dist/mcp/tools/results-evidence.js.map +1 -0
  145. package/dist/mcp/tools/task-create.d.ts +70 -0
  146. package/dist/mcp/tools/task-create.js +473 -0
  147. package/dist/mcp/tools/task-create.js.map +1 -0
  148. package/dist/mcp/tools/task-reads.d.ts +102 -0
  149. package/dist/mcp/tools/task-reads.js +689 -0
  150. package/dist/mcp/tools/task-reads.js.map +1 -0
  151. package/dist/mcp/tools/work-organisation.d.ts +2 -0
  152. package/dist/mcp/tools/work-organisation.js +502 -0
  153. package/dist/mcp/tools/work-organisation.js.map +1 -0
  154. package/dist/paths.d.ts +17 -7
  155. package/dist/paths.js +18 -8
  156. package/dist/paths.js.map +1 -1
  157. package/dist/runs/project-writer.js +76 -11
  158. package/dist/runs/project-writer.js.map +1 -1
  159. package/dist/runs/retention.d.ts +24 -1
  160. package/dist/runs/retention.js +62 -1
  161. package/dist/runs/retention.js.map +1 -1
  162. package/dist/server/forge/github.js +44 -0
  163. package/dist/server/forge/github.js.map +1 -1
  164. package/dist/server/forge/types.d.ts +16 -0
  165. package/dist/server/project-context.d.ts +81 -8
  166. package/dist/server/project-context.js +143 -35
  167. package/dist/server/project-context.js.map +1 -1
  168. package/dist/server/server.d.ts +1050 -53
  169. package/dist/server/server.js +326 -120
  170. package/dist/server/server.js.map +1 -1
  171. package/dist/server/validators.d.ts +26 -6
  172. package/dist/server/validators.js +17 -0
  173. package/dist/server/validators.js.map +1 -1
  174. package/dist/skills-remote.js +64 -6
  175. package/dist/skills-remote.js.map +1 -1
  176. package/dist/workflows/run.d.ts +277 -12
  177. package/dist/workflows/run.js +554 -59
  178. package/dist/workflows/run.js.map +1 -1
  179. package/dist/workspace/agent-profiles.d.ts +30 -3
  180. package/dist/workspace/agent-profiles.js +39 -4
  181. package/dist/workspace/agent-profiles.js.map +1 -1
  182. package/dist/workspace/config.js +25 -4
  183. package/dist/workspace/config.js.map +1 -1
  184. package/dist/workspace/project-owner.d.ts +210 -0
  185. package/dist/workspace/project-owner.js +518 -0
  186. package/dist/workspace/project-owner.js.map +1 -0
  187. package/package.json +5 -3
  188. package/scripts/pi-leader-extension.ts +420 -0
  189. package/scripts/sync-readme.mjs +83 -2
  190. package/web/dist/assets/{alert-dialog-BVsVcbFy.js → alert-dialog-D6rP6d39.js} +1 -1
  191. package/web/dist/assets/arrow-down-DMOfFIzW.js +1 -0
  192. package/web/dist/assets/arrow-left-C4Ec2BUg.js +1 -0
  193. package/web/dist/assets/{centered-state-Djgly6Ni.js → centered-state-B0ZqrPaa.js} +12 -12
  194. package/web/dist/assets/chevron-right-mHJ-crvn.js +1 -0
  195. package/web/dist/assets/{collapsible-DZ4D-DHG.js → collapsible-DchEHgUM.js} +1 -1
  196. package/web/dist/assets/{commit-list-B-z1JL9G.js → commit-list-BpMim1Rv.js} +1 -1
  197. package/web/dist/assets/{compare-variants-D62m4j3B.js → compare-variants-D5ItQbAq.js} +1 -1
  198. package/web/dist/assets/{diff-BKew9rQX.js → diff-HqqZ_WDR.js} +2 -2
  199. package/web/dist/assets/{diff-stat-D0mc5V6Z.js → diff-stat-NsU-3ecr.js} +1 -1
  200. package/web/dist/assets/{diff-view-xpIcuLIC.js → diff-view-B7c11Xux.js} +1 -1
  201. package/web/dist/assets/{dropdown-menu-CEQziH7X.js → dropdown-menu-CtTpnyod.js} +1 -1
  202. package/web/dist/assets/{editable-title-BCX3T2BY.js → editable-title-BXtuGQry.js} +1 -1
  203. package/web/dist/assets/{ellipsis-vertical-ByVdDc4z.js → ellipsis-vertical-MHtGZSH6.js} +1 -1
  204. package/web/dist/assets/{file-xBSB5hXO.js → file-DKLw5SOb.js} +1 -1
  205. package/web/dist/assets/{folder-CldgbZ5A.js → folder-Drw6-ufH.js} +1 -1
  206. package/web/dist/assets/{git-toolbar-DJANqg7Y.js → git-toolbar-BQH8bORV.js} +1 -1
  207. package/web/dist/assets/github-ynnpKFCJ.js +1 -0
  208. package/web/dist/assets/{image-preview-Cv5mZr0n.js → image-preview-DLhMN_R9.js} +1 -1
  209. package/web/dist/assets/index-BEsrNdXt.js +29 -0
  210. package/web/dist/assets/index-Da8DOBau.css +2 -0
  211. package/web/dist/assets/{markdown-CgEEE2ND.js → markdown-q365eN6Y.js} +1 -1
  212. package/web/dist/assets/{new-task-form-1r2bgsdD.js → new-task-form-CW-hNIWQ.js} +1 -1
  213. package/web/dist/assets/{pill-q-x22_ej.js → pill-B_TFulXa.js} +1 -1
  214. package/web/dist/assets/{project-router-VDN4jQM8.js → project-router-CLPc-ImZ.js} +1 -1
  215. package/web/dist/assets/{prompt-templates-BmZtCe-L.js → prompt-templates-CLc4RxyO.js} +1 -1
  216. package/web/dist/assets/{repo-git-btG0sdbO.js → repo-git-Is7R6-Tj.js} +1 -1
  217. package/web/dist/assets/{run-diff-vguEMJzh.js → run-diff-DHMvDlVo.js} +2 -2
  218. package/web/dist/assets/run-header-DvZJbQ8I.js +1 -0
  219. package/web/dist/assets/{search-x-QmhaAbg5.js → search-x-Ce2MMetu.js} +1 -1
  220. package/web/dist/assets/{skill-empty-hint-BrzUwByZ.js → skill-empty-hint-C_xCSvzB.js} +1 -1
  221. package/web/dist/assets/skills-CivqD4si.js +1 -0
  222. package/web/dist/assets/{sparkles-CP8QDOgX.js → sparkles-BCqPoj5g.js} +1 -1
  223. package/web/dist/assets/{square-terminal-CGypshSD.js → square-terminal-ikj5g-oK.js} +1 -1
  224. package/web/dist/assets/{tab-link-D4lFzzI1.js → tab-link-VsEjFV1Y.js} +1 -1
  225. package/web/dist/assets/{task-changes-BtZId14z.js → task-changes-CH-E81-_.js} +1 -1
  226. package/web/dist/assets/{task-commits-BoeyJXrv.js → task-commits-DqfkMFxp.js} +1 -1
  227. package/web/dist/assets/{task-files-DIy2oUnj.js → task-files-D_nkHZ_a.js} +2 -2
  228. package/web/dist/assets/task-thread-bxQompxJ.js +9 -0
  229. package/web/dist/assets/{textarea-CyBCyKIk.js → textarea-xajuW1L_.js} +1 -1
  230. package/web/dist/assets/{thread-loading-Cuq2jchC.js → thread-loading-C3JQZbIE.js} +1 -1
  231. package/web/dist/assets/{trash-2-B6Dp3ZNs.js → trash-2-hJrBhpUD.js} +1 -1
  232. package/web/dist/assets/{triangle-alert-xBya8FUp.js → triangle-alert-D1u82y1a.js} +1 -1
  233. package/web/dist/assets/{upload-D6MXEB9p.js → upload-Bn6FBZ59.js} +1 -1
  234. package/web/dist/assets/{use-desktop-CPBwzQ7M.js → use-desktop-DOIMfHfY.js} +1 -1
  235. package/web/dist/assets/{use-submit-shortcut-JTpNzU-u.js → use-submit-shortcut-Dj4DHTdE.js} +1 -1
  236. package/web/dist/assets/utils-YwBpOoRN.js +64 -0
  237. package/web/dist/assets/{workflows-DG115scU.js → workflows-CTaAKz2m.js} +3 -3
  238. package/web/dist/assets/{zoomable-image-GTYwLtkS.js → zoomable-image-C1-6P3tg.js} +1 -1
  239. package/web/dist/index.html +23 -23
  240. package/web/dist/assets/arrow-down-CYgAlZtW.js +0 -1
  241. package/web/dist/assets/arrow-left-BOqE0a3k.js +0 -1
  242. package/web/dist/assets/chevron-right-Cc9BWnbN.js +0 -1
  243. package/web/dist/assets/github-D6vK0jFn.js +0 -1
  244. package/web/dist/assets/index-CkmR6h7s.css +0 -2
  245. package/web/dist/assets/index-DVDqF4bM.js +0 -9
  246. package/web/dist/assets/refresh-cw-DcNCj2nu.js +0 -1
  247. package/web/dist/assets/run-header-DM7MiKj9.js +0 -1
  248. package/web/dist/assets/skills-DMfxQtrD.js +0 -1
  249. package/web/dist/assets/task-thread-CbMPmrWq.js +0 -9
  250. package/web/dist/assets/utils--z9hRtl3.js +0 -64
@@ -0,0 +1,1075 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { basename, dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
4
+ import { automationCheckInputSchema, automationEventSchema, automationLogResultSchema, mcpExpectedVersionSchema, operationIdSchema, runIdParamSchema, saveWorkflowInputSchema, setAgentConfigInputSchema, setConfigInputSchema, uiStateSchema, updateAutomationInputSchema, updateProjectInputSchema, workflowStepDefSchema, } from '../../contract/index.js';
5
+ import { hc } from 'hono/client';
6
+ import { parse as parseToml } from 'smol-toml';
7
+ import { parse as parseYaml } from 'yaml';
8
+ import { z } from 'zod';
9
+ import { findConfigFile } from '../../agent-config/catalog.js';
10
+ import { stripJsonComments } from '../../agent-config/validate.js';
11
+ import { agentHomePaths } from '../../paths.js';
12
+ import { slugify } from '../../planner.js';
13
+ import { projectWorkflowsDir } from '../../workflows/load.js';
14
+ import { MCP_ORIGIN } from '../service-adapter.js';
15
+ import { staleRejectionIn } from '../stale-write.js';
16
+ import { defineTool, errorResult, textResult } from '../tool.js';
17
+ /** Every action a leader may take. Each is one cockpit route (or a narrowed read of one). */
18
+ export const PROJECT_CONFIG_ACTIONS = [
19
+ 'get_config',
20
+ 'set_config',
21
+ 'get_project',
22
+ 'set_project',
23
+ 'get_prompt_templates',
24
+ 'set_prompt_templates',
25
+ 'get_limits',
26
+ 'get_capabilities',
27
+ 'get_account',
28
+ 'list_agent_config',
29
+ 'read_agent_config',
30
+ 'write_agent_config',
31
+ 'list_workflows',
32
+ 'parse_workflow',
33
+ 'save_workflow',
34
+ 'delete_workflow',
35
+ 'list_skills',
36
+ 'get_skill',
37
+ 'list_importable_skills',
38
+ 'refresh_skills',
39
+ 'check_skill_updates',
40
+ 'list_automations',
41
+ 'get_automation',
42
+ 'create_automation',
43
+ 'update_automation',
44
+ 'delete_automation',
45
+ 'enable_automation',
46
+ 'pause_automation',
47
+ 'check_automation',
48
+ 'get_automation_check',
49
+ 'get_automation_log',
50
+ 'retry_automation_receipt',
51
+ 'list_worktrees',
52
+ 'reclaim_worktrees',
53
+ 'remove_worktree',
54
+ ];
55
+ const BOUNDARY_LABEL = {
56
+ 'project-binding': 'project binding',
57
+ 'workspace-settings': 'workspace-wide setting',
58
+ 'agent-accounts': 'global agent accounts',
59
+ 'account-identity': 'account identity',
60
+ 'home-file': 'home file shared by every project',
61
+ 'outside-project': 'file outside this project',
62
+ 'host-filesystem': 'host filesystem',
63
+ 'host-process': 'host process',
64
+ 'project-registry': 'project registry',
65
+ 'quality-gate': 'quality gate',
66
+ secret: 'secret',
67
+ };
68
+ /**
69
+ * The `excluded` rows of the classification a leader might plausibly ask for, each answered with
70
+ * its boundary. None of them dispatches anything. Listed as actions (rather than left out of the
71
+ * enum) so the answer is an understandable reason, not a schema error.
72
+ */
73
+ export const REFUSED_ACTIONS = {
74
+ set_provider_enabled: {
75
+ boundary: 'workspace-settings',
76
+ reason: 'turning a provider on or off changes it for every project on this machine. Read provider status with get_capabilities; a person changes it in the cockpit.',
77
+ },
78
+ connect_provider: {
79
+ boundary: 'host-process',
80
+ reason: 'connecting a provider opens a login terminal on the host machine. A person does this in the cockpit.',
81
+ },
82
+ retry_provider: {
83
+ boundary: 'workspace-settings',
84
+ reason: 'clearing a provider authentication incident clears it for every project. Report the blocker instead; a person clears it in the cockpit.',
85
+ },
86
+ create_account: {
87
+ boundary: 'agent-accounts',
88
+ reason: 'agent accounts are global and administered by a person, not by a project leader.',
89
+ },
90
+ update_account: {
91
+ boundary: 'agent-accounts',
92
+ reason: 'agent accounts are global and administered by a person, not by a project leader.',
93
+ },
94
+ remove_account: {
95
+ boundary: 'agent-accounts',
96
+ reason: 'agent accounts are global and administered by a person, not by a project leader.',
97
+ },
98
+ select_account: {
99
+ boundary: 'agent-accounts',
100
+ reason: 'the account a project uses is stored with the global accounts. Read the effective account with get_account; a person changes it in the cockpit.',
101
+ },
102
+ check_account_status: {
103
+ boundary: 'agent-accounts',
104
+ reason: 'probing one named account is global account administration. Provider status is in get_capabilities.',
105
+ },
106
+ get_account_details: {
107
+ boundary: 'account-identity',
108
+ reason: 'account identity is never served to a project leader.',
109
+ },
110
+ open_account_file: {
111
+ boundary: 'host-process',
112
+ reason: 'opening an account folder launches an application on the host machine.',
113
+ },
114
+ set_workspace_config: {
115
+ boundary: 'workspace-settings',
116
+ reason: 'workspace settings and limits apply to every project. Read the effective limits with get_limits; this project’s own settings are set_config and set_project.',
117
+ },
118
+ set_workspace_ui_state: {
119
+ boundary: 'workspace-settings',
120
+ reason: 'the workspace preference bag describes the person at the keyboard and is shared by every project.',
121
+ },
122
+ browse_folders: {
123
+ boundary: 'host-filesystem',
124
+ reason: 'browsing folders lists host directories outside this project.',
125
+ },
126
+ add_project: {
127
+ boundary: 'project-registry',
128
+ reason: 'registering projects manages the workspace, not this project.',
129
+ },
130
+ clone_project: {
131
+ boundary: 'project-registry',
132
+ reason: 'cloning creates and registers a checkout outside this project.',
133
+ },
134
+ remove_project: {
135
+ boundary: 'project-registry',
136
+ reason: 'removing a project deregisters it from the workspace and ends this connection’s binding.',
137
+ },
138
+ apply_skill_updates: {
139
+ boundary: 'workspace-settings',
140
+ reason: 'applying skill updates rewrites globally installed skills every project reads. check_skill_updates reports what is available.',
141
+ },
142
+ import_skills: {
143
+ boundary: 'workspace-settings',
144
+ reason: 'the imported-skills list is a workspace-wide preference. list_skills shows this project’s effective catalog.',
145
+ },
146
+ get_launch_key: {
147
+ boundary: 'secret',
148
+ reason: 'the launch key is a credential and never enters a tool response.',
149
+ },
150
+ open_in_app: {
151
+ boundary: 'host-process',
152
+ reason: 'opening the project launches a desktop application on the host machine.',
153
+ },
154
+ };
155
+ const REFUSED_ACTION_NAMES = Object.keys(REFUSED_ACTIONS);
156
+ const FIELDS = [
157
+ 'config',
158
+ 'project',
159
+ 'promptTemplates',
160
+ 'fileId',
161
+ 'content',
162
+ 'version',
163
+ 'yaml',
164
+ 'workflow',
165
+ 'name',
166
+ 'wait',
167
+ 'refresh',
168
+ 'automationId',
169
+ 'automation',
170
+ 'update',
171
+ 'mode',
172
+ 'checkId',
173
+ 'receiptId',
174
+ 'logQuery',
175
+ 'runId',
176
+ 'expectedVersion',
177
+ 'operationId',
178
+ ];
179
+ const none = { required: [], optional: [] };
180
+ /**
181
+ * Which arguments each action takes. Anything else is refused rather than silently dropped.
182
+ *
183
+ * `operationId` (D-06 § 5.2, #264) is required by every action that CHANGES something and refused by
184
+ * every action that only reads: a read has no effect to deduplicate, and a receipt over one would
185
+ * answer the next identical read with the receipt instead of the settings, the skill or the log.
186
+ * Two entries are worth their own sentence:
187
+ * - `refresh_skills` applies the pending skill updates, so it is a change, not a read;
188
+ * - `check_automation` takes one for BOTH modes. `execute` launches tasks, and `preview` is only a
189
+ * count — but a mode is not a second identity, and one rule per action is what keeps the
190
+ * published guard table honest. A leader that wants a fresh preview sends a fresh key, which is
191
+ * exactly D-06's "deliberately new identical work uses a new identity".
192
+ * The refusal-only actions (`REFUSED_ACTIONS`) dispatch nothing and are checked by neither branch:
193
+ * the refinement returns before it reaches this table.
194
+ */
195
+ const ACTION_FIELDS = {
196
+ get_config: none,
197
+ set_config: { required: ['config', 'operationId'], optional: [] },
198
+ get_project: none,
199
+ set_project: { required: ['project', 'operationId'], optional: [] },
200
+ get_prompt_templates: none,
201
+ set_prompt_templates: { required: ['promptTemplates', 'operationId'], optional: [] },
202
+ get_limits: none,
203
+ get_capabilities: { required: [], optional: ['refresh'] },
204
+ get_account: none,
205
+ list_agent_config: none,
206
+ read_agent_config: { required: ['fileId'], optional: [] },
207
+ // `version` is required but may be `null` ("I expect no file yet"), so presence is checked.
208
+ write_agent_config: { required: ['fileId', 'content', 'version', 'operationId'], optional: [] },
209
+ list_workflows: none,
210
+ parse_workflow: { required: ['yaml'], optional: [] },
211
+ save_workflow: { required: ['workflow', 'operationId'], optional: [] },
212
+ delete_workflow: { required: ['name', 'operationId'], optional: [] },
213
+ list_skills: { required: [], optional: ['wait'] },
214
+ get_skill: { required: ['name'], optional: ['wait'] },
215
+ list_importable_skills: { required: [], optional: ['wait'] },
216
+ refresh_skills: { required: ['operationId'], optional: [] },
217
+ check_skill_updates: none,
218
+ list_automations: none,
219
+ get_automation: { required: ['automationId'], optional: [] },
220
+ create_automation: { required: ['automation', 'operationId'], optional: [] },
221
+ update_automation: { required: ['automationId', 'update', 'operationId'], optional: [] },
222
+ delete_automation: { required: ['automationId', 'operationId'], optional: [] },
223
+ enable_automation: { required: ['automationId', 'operationId'], optional: [] },
224
+ pause_automation: { required: ['automationId', 'operationId'], optional: [] },
225
+ check_automation: { required: ['automationId', 'mode', 'operationId'], optional: [] },
226
+ get_automation_check: { required: ['checkId'], optional: [] },
227
+ get_automation_log: { required: [], optional: ['logQuery'] },
228
+ retry_automation_receipt: { required: ['receiptId', 'operationId'], optional: [] },
229
+ list_worktrees: none,
230
+ reclaim_worktrees: { required: ['operationId'], optional: [] },
231
+ // Removing a task's worktree changes that task, so it needs its version (#250, N-03).
232
+ remove_worktree: { required: ['runId', 'expectedVersion', 'operationId'], optional: [] },
233
+ };
234
+ const isRefused = (action) => Object.hasOwn(REFUSED_ACTIONS, action);
235
+ /** A path segment the cockpit's routes would read as ONE segment: no slash, no dot segment. */
236
+ const PATH_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
237
+ const validPathId = (id) => id !== '.' && id !== '..' && PATH_ID_RE.test(id);
238
+ // The repo config's own `maxParallel` is INERT (§ 4.14): the scheduler reads the registry entry,
239
+ // which is `set_project`. Accepting it here would report a change that never happens.
240
+ const projectConfigWriteSchema = setConfigInputSchema.omit({ maxParallel: true }).strict();
241
+ const projectRegistryWriteSchema = z.strictObject(updateProjectInputSchema.shape);
242
+ const promptTemplatesSchema = uiStateSchema.shape.promptTemplates.unwrap();
243
+ // Agent steps only — `command` and the check step's `onFail` are not keys here, and the object is
244
+ // strict, so a check step is refused rather than silently turned into something else.
245
+ const { command: _command, onFail: _onFail, ...agentStepShape } = workflowStepDefSchema.shape;
246
+ const agentStepSchema = z.strictObject(agentStepShape).refine((step) => Boolean(step.prompt ?? step.skill), {
247
+ message: 'a step needs a prompt or a skill; check steps (shell commands) cannot be saved through MCP',
248
+ });
249
+ const { steps: _steps, ...saveWorkflowShape } = saveWorkflowInputSchema.shape;
250
+ const agentWorkflowSchema = z
251
+ .strictObject({ ...saveWorkflowShape, steps: z.array(agentStepSchema).min(1).max(8).optional() })
252
+ .refine((body) => Boolean(body.steps) !== Boolean(body.skills), { message: 'provide either "steps" or "skills", not both' });
253
+ /** The cockpit form's fixed values (`automations.tsx` `AutomationEditor`, create). D-97: matched, never widened. */
254
+ const AUTOMATION_FORM_FIXED = {
255
+ events: ['issue.opened'],
256
+ intervalSeconds: 300,
257
+ filters: { lookbackDays: 7, maxRecords: 25 },
258
+ workflow: 'quick-task',
259
+ };
260
+ const automationCreateFormSchema = z.strictObject({
261
+ name: z.string().min(1),
262
+ prompt: z.string().min(1),
263
+ enable: z.boolean().optional().describe('Enable from a current-time baseline (existing matches will not launch). Default false.'),
264
+ });
265
+ const automationEditFormSchema = z
266
+ .strictObject({
267
+ name: z.string().min(1).optional(),
268
+ prompt: z.string().min(1).optional(),
269
+ expectedRevision: updateAutomationInputSchema.shape.expectedRevision,
270
+ })
271
+ .refine((edit) => edit.name !== undefined || edit.prompt !== undefined, { message: 'change name, prompt or both' });
272
+ const automationLogQueryInputSchema = z.strictObject({
273
+ automationId: z.string().min(1).max(128).optional(),
274
+ result: automationLogResultSchema.optional(),
275
+ event: automationEventSchema.optional(),
276
+ since: z.string().datetime().optional().describe('ISO timestamp; only newer rows.'),
277
+ cursor: z.number().int().positive().optional().describe('Row sequence to continue from.'),
278
+ limit: z.number().int().min(1).max(100).optional(),
279
+ });
280
+ export const projectConfigInputSchema = z
281
+ .object({
282
+ action: z
283
+ .enum([...PROJECT_CONFIG_ACTIONS, ...REFUSED_ACTION_NAMES])
284
+ .describe('What to do in the project this connection is bound to. Actions outside the project boundary (workspace settings, accounts, the project registry, host folders) are answered with a refusal that names the boundary.'),
285
+ projectId: z
286
+ .unknown()
287
+ .optional()
288
+ .describe('Never accepted: the project is the one this connection is bound to, and a call that names one is refused.'),
289
+ config: projectConfigWriteSchema
290
+ .optional()
291
+ .describe("set_config: the project's own settings to change. null clears a key back to its default."),
292
+ project: projectRegistryWriteSchema
293
+ .optional()
294
+ .describe("set_project: this project's concurrency cap (maxParallel, null inherits the workspace cap) and/or its tags (whole list)."),
295
+ promptTemplates: promptTemplatesSchema
296
+ .optional()
297
+ .describe('set_prompt_templates: the WHOLE list of follow-up prompt templates; [] is an empty list.'),
298
+ fileId: z.string().min(1).max(128).optional().describe('An agent config file id from list_agent_config — never a path.'),
299
+ content: setAgentConfigInputSchema.shape.content.optional().describe('write_agent_config: the full new file content.'),
300
+ version: setAgentConfigInputSchema.shape.version
301
+ .optional()
302
+ .describe('write_agent_config: the version from the read you based the edit on; null when the file does not exist yet.'),
303
+ yaml: z.string().min(1).max(100_000).optional().describe('parse_workflow: workflow YAML to validate and normalise.'),
304
+ workflow: agentWorkflowSchema
305
+ .optional()
306
+ .describe('save_workflow: name plus exactly one of steps (agent steps: prompt or skill) or skills. Check steps (shell commands) are not accepted. An existing file is refused unless overwrite is true.'),
307
+ name: z.string().min(1).max(200).optional().describe('delete_workflow / get_skill: the workflow or skill name.'),
308
+ wait: z.boolean().optional().describe('Skill reads: wait for a cold team-skill cache to load first.'),
309
+ refresh: z.boolean().optional().describe('get_capabilities: probe provider status now instead of serving the cached answer.'),
310
+ automationId: z.string().min(1).max(128).optional(),
311
+ automation: automationCreateFormSchema
312
+ .optional()
313
+ .describe('create_automation: the cockpit form — name, prompt template, enable. Trigger: new issue, every 5 minutes, last 7 days, at most 25 records; task workflow quick-task.'),
314
+ update: automationEditFormSchema
315
+ .optional()
316
+ .describe('update_automation: a new name and/or prompt plus expectedRevision from your last read; everything else is kept.'),
317
+ mode: automationCheckInputSchema.shape.mode.optional().describe('check_automation: preview counts matches; execute launches them.'),
318
+ checkId: z.string().min(1).max(128).optional(),
319
+ receiptId: z.string().min(1).max(128).optional(),
320
+ logQuery: automationLogQueryInputSchema.optional(),
321
+ runId: z.string().min(1).max(128).optional().describe("remove_worktree: the task's run id."),
322
+ expectedVersion: mcpExpectedVersionSchema
323
+ .optional()
324
+ .describe('remove_worktree: the `version` task_read returned for the task. If the task changed since, nothing is removed.'),
325
+ operationId: operationIdSchema
326
+ .optional()
327
+ .describe('Client-generated key for this operation (8–128 chars). Required by every action that changes something and refused by every action that only reads. Reuse it only to repeat the same operation: a repeat returns the first answer and changes nothing twice.'),
328
+ })
329
+ .strict()
330
+ .superRefine((args, ctx) => {
331
+ // A refusal is answered whatever else was sent — it must never become a schema essay.
332
+ if (isRefused(args.action))
333
+ return;
334
+ const allowed = ACTION_FIELDS[args.action];
335
+ for (const field of allowed.required) {
336
+ if (args[field] === undefined)
337
+ ctx.addIssue({ code: 'custom', path: [field], message: `${args.action} needs ${field}` });
338
+ }
339
+ for (const field of FIELDS) {
340
+ if (args[field] !== undefined && !allowed.required.includes(field) && !allowed.optional.includes(field)) {
341
+ ctx.addIssue({ code: 'custom', path: [field], message: `${field} is not used by ${args.action}` });
342
+ }
343
+ }
344
+ });
345
+ // ---- the in-process client ----------------------------------------------------------------------
346
+ const IN_PROCESS_BASE = 'http://127.0.0.1';
347
+ const IN_PROCESS_HOST = '127.0.0.1';
348
+ /** The same in-process client the shared adapter builds (`service-adapter.ts`): loopback host, no
349
+ * Origin — the request never leaves the process. */
350
+ const buildClient = (service) => hc(IN_PROCESS_BASE, {
351
+ fetch: async (input, init) => {
352
+ const headers = new Headers(init?.headers);
353
+ headers.set('host', IN_PROCESS_HOST);
354
+ headers.delete('origin');
355
+ const url = input instanceof Request ? input.url : String(input);
356
+ return service.request(url, { ...init, headers });
357
+ },
358
+ }).api.v1;
359
+ /** Map a service answer. A success without a JSON body is an error, never an empty value — except
360
+ * a 204, whose whole meaning is "done, nothing to say". */
361
+ async function settle(pending, success) {
362
+ const res = await pending;
363
+ const body = await res.json().catch(() => undefined);
364
+ if (success.includes(res.status)) {
365
+ if (res.status === 204)
366
+ return { ok: true, status: 204, value: undefined };
367
+ if (body === undefined)
368
+ return { ok: false, status: 502, error: `service answered ${res.status} without a body`, body };
369
+ return { ok: true, status: res.status, value: body };
370
+ }
371
+ const error = body && typeof body === 'object' && typeof body.error === 'string'
372
+ ? body.error
373
+ : `service answered ${res.status}`;
374
+ return { ok: false, status: res.status, error, body };
375
+ }
376
+ function ok(action, result) {
377
+ const payload = { action, origin: MCP_ORIGIN, result: result ?? null };
378
+ return textResult(JSON.stringify(payload, null, 2), payload);
379
+ }
380
+ function failed(action, answer, root) {
381
+ // The task changed after the leader read it (#250): an ordinary conflict, nothing applied — D-06
382
+ // § 4.4's rejection verbatim, not an error the leader must correct.
383
+ const stale = staleRejectionIn(answer.body);
384
+ if (stale) {
385
+ const payload = { action, origin: MCP_ORIGIN, ...stale };
386
+ return textResult(JSON.stringify(payload, null, 2), payload);
387
+ }
388
+ const error = scrubPaths(answer.error, root);
389
+ const exists = answer.body && typeof answer.body === 'object' && answer.body.exists === true;
390
+ return errorResult(`${action} was refused by xezar (${answer.status}): ${error}`, {
391
+ action,
392
+ origin: MCP_ORIGIN,
393
+ status: answer.status,
394
+ error,
395
+ ...(exists ? { exists: true } : {}),
396
+ });
397
+ }
398
+ function refused(action, boundary, reason) {
399
+ return errorResult(`Refused (${BOUNDARY_LABEL[boundary]}): ${action} — ${reason} Nothing was changed.`, {
400
+ action,
401
+ origin: MCP_ORIGIN,
402
+ refused: true,
403
+ boundary,
404
+ });
405
+ }
406
+ /** The next legitimate action after a quality-gate refusal. It offers no waiver, to anyone (A-22). */
407
+ export const QUALITY_GATE_NEXT_ACTION = 'This is a blocker. A check step is a quality gate and cannot be removed or weakened from MCP. Keep the step, ' +
408
+ 'save the workflow under a new name, or report this blocker so a person can change the gate in the cockpit.';
409
+ /**
410
+ * The check steps a workflow save or delete would take away (F-22, #262), read from the FILES on
411
+ * disk — never from what the caller says is there. A file is at risk when it is the one the save
412
+ * route writes (`<slug>.yaml`, only when `overwrite` lets the route replace it) or when it carries
413
+ * the same workflow `name` (a save would shadow it, a delete removes it). The replacement can
414
+ * carry no check step at all (the MCP step schema is agent-only), so every check step of an
415
+ * at-risk file is lost — deleting it, emptying its command and turning it into an agent step are
416
+ * all the same loss here. A target file that cannot be parsed cannot be shown to hold no check
417
+ * step, so it is reported too: unknown evidence is never a pass.
418
+ */
419
+ async function checkStepsAtRisk(root, name, target) {
420
+ const dir = projectWorkflowsDir(root);
421
+ const steps = [];
422
+ const unreadable = [];
423
+ let entries = [];
424
+ try {
425
+ entries = await readdir(dir);
426
+ }
427
+ catch {
428
+ return { steps, unreadable };
429
+ }
430
+ for (const entry of entries.sort()) {
431
+ const ext = extname(entry).toLowerCase();
432
+ if (ext !== '.yaml' && ext !== '.yml')
433
+ continue;
434
+ const file = projectRelative(root, join(dir, entry)) ?? entry;
435
+ // Lower-cased: on a case-insensitive disk the route's `gated.yaml` write replaces `Gated.yaml`.
436
+ const isTarget = target?.file === entry.toLowerCase();
437
+ let doc;
438
+ try {
439
+ doc = parseYaml(await readFile(join(dir, entry), 'utf8'));
440
+ }
441
+ catch {
442
+ if (isTarget)
443
+ unreadable.push(file);
444
+ continue;
445
+ }
446
+ const record = doc && typeof doc === 'object' ? doc : {};
447
+ if (!isTarget && (typeof record.name !== 'string' || record.name.trim() !== name.trim()))
448
+ continue;
449
+ for (const step of Array.isArray(record.steps) ? record.steps : []) {
450
+ if (!step || typeof step !== 'object' || !('command' in step))
451
+ continue;
452
+ const { id, name: label } = step;
453
+ steps.push({ file, id: typeof id === 'string' ? id : '(no id)', ...(typeof label === 'string' ? { name: label } : {}) });
454
+ }
455
+ }
456
+ return { steps, unreadable };
457
+ }
458
+ function qualityGateRefusal(action, risk) {
459
+ const named = risk.steps.map((step) => `"${step.id}"${step.name ? ` (${step.name})` : ''} in ${step.file}`);
460
+ const reason = risk.steps.length > 0
461
+ ? `it would remove the check step ${named.join(', ')}. A check step is a quality gate, and MCP neither adds nor removes one.`
462
+ : `${risk.unreadable.join(', ')} could not be read, so it cannot be shown to hold no check step.`;
463
+ return errorResult(`Refused (${BOUNDARY_LABEL['quality-gate']}): ${action} — ${reason} ${QUALITY_GATE_NEXT_ACTION} Nothing was changed.`, {
464
+ action,
465
+ origin: MCP_ORIGIN,
466
+ refused: true,
467
+ blocker: true,
468
+ boundary: 'quality-gate',
469
+ checkSteps: risk.steps,
470
+ ...(risk.unreadable.length > 0 ? { unreadable: risk.unreadable } : {}),
471
+ nextAction: QUALITY_GATE_NEXT_ACTION,
472
+ });
473
+ }
474
+ function invalid(action, message) {
475
+ return errorResult(`${action}: ${message}`, { action, origin: MCP_ORIGIN, status: 400, error: message });
476
+ }
477
+ // ---- paths --------------------------------------------------------------------------------------
478
+ /** A path inside the project, relative to its root; anything else is not reported at all. */
479
+ function projectRelative(root, path) {
480
+ if (!path)
481
+ return undefined;
482
+ const rel = relative(root, path);
483
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel))
484
+ return undefined;
485
+ return rel;
486
+ }
487
+ /** Error text is the service's own, but it can quote an absolute path: report the project's as
488
+ * `.`, and never name the home directory. */
489
+ function scrubPaths(text, root) {
490
+ let out = text.split(root).join('.');
491
+ const home = process.env.HOME;
492
+ if (home && home.length > 1)
493
+ out = out.split(home).join('~');
494
+ return out;
495
+ }
496
+ /**
497
+ * Whether `path` really lands inside `root` once every symlink on the way is followed. The
498
+ * cockpit's writer goes THROUGH a symlink (`files.ts`), so an in-repo `.claude/settings.json`
499
+ * linked into a home folder would turn a project write into a home write. A path that does not
500
+ * exist yet is judged by its nearest existing ancestor — the directory the write would create it in.
501
+ */
502
+ export function resolvesInside(root, path) {
503
+ let realRoot;
504
+ try {
505
+ realRoot = realpathSync(root);
506
+ }
507
+ catch {
508
+ return false;
509
+ }
510
+ let existing = path;
511
+ const rest = [];
512
+ while (!existsSync(existing)) {
513
+ const parent = dirname(existing);
514
+ if (parent === existing)
515
+ return false;
516
+ rest.unshift(basename(existing));
517
+ existing = parent;
518
+ }
519
+ let real;
520
+ try {
521
+ real = join(realpathSync(existing), ...rest);
522
+ }
523
+ catch {
524
+ return false;
525
+ }
526
+ return real === realRoot || real.startsWith(realRoot + sep);
527
+ }
528
+ // ---- agent config -------------------------------------------------------------------------------
529
+ /**
530
+ * The gate every agent-config action passes BEFORE anything is dispatched: a known catalog id,
531
+ * not a `scope: 'user'` file (keyed off the catalog field — D-03-1), and a file that stays inside
532
+ * the project. The user-scope refusal names no path and never looks at the disk, so it reads the
533
+ * same whether or not the file exists.
534
+ */
535
+ function agentConfigGate(action, fileId, root) {
536
+ const def = findConfigFile(fileId);
537
+ if (!def)
538
+ return { result: invalid(action, `unknown agent config file id: ${JSON.stringify(fileId)} — list_agent_config names them`) };
539
+ if (def.scope === 'user') {
540
+ return {
541
+ result: refused(action, 'home-file', `${fileId} is a user-scope file in a home folder, shared by every project on this machine. Only this project’s own files are served.`),
542
+ };
543
+ }
544
+ if (!resolvesInside(root, def.resolve(root, agentHomePaths(process.env)))) {
545
+ return {
546
+ result: refused(action, 'outside-project', `${fileId} resolves outside this project folder, so it is not served here.`),
547
+ };
548
+ }
549
+ return { def };
550
+ }
551
+ /** Deep enough to show server names and their fields, never deep enough to show a value. */
552
+ const STRUCTURE_DEPTH = 3;
553
+ const STRUCTURE_MAX_KEYS = 200;
554
+ /** The kind of a value, never the value itself. */
555
+ function kindOf(value) {
556
+ if (value === null)
557
+ return 'null';
558
+ if (Array.isArray(value))
559
+ return `array(${value.length})`;
560
+ if (typeof value === 'object')
561
+ return `object(${Object.keys(value).length})`;
562
+ return typeof value;
563
+ }
564
+ /** Key names and value kinds down to `STRUCTURE_DEPTH`. Every scalar — command strings, args,
565
+ * env values, headers, URLs, tokens — is reported as its kind (ruling 12, F-15). */
566
+ export function structureOf(value, depth = 1) {
567
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || depth > STRUCTURE_DEPTH)
568
+ return kindOf(value);
569
+ const out = {};
570
+ for (const key of Object.keys(value).slice(0, STRUCTURE_MAX_KEYS)) {
571
+ out[key] = structureOf(value[key], depth + 1);
572
+ }
573
+ return out;
574
+ }
575
+ function parseForStructure(content, format) {
576
+ if (format === 'json')
577
+ return JSON.parse(content);
578
+ if (format === 'jsonc')
579
+ return JSON.parse(stripJsonComments(content));
580
+ if (format === 'toml')
581
+ return parseToml(content);
582
+ throw new Error('no structure for this format');
583
+ }
584
+ /** The bound project's registry row, never another — or null when the registry does not list it. */
585
+ async function boundEntry(s) {
586
+ const answer = await settle(s.api.projects.$get(), [200]);
587
+ if (!answer.ok)
588
+ return answer;
589
+ return { ok: true, status: 200, value: answer.value.projects.find((p) => p.id === s.projectId) ?? null };
590
+ }
591
+ /** Hosted mode trims the root to a basename, the rule `/health` already follows (§ 4.6). */
592
+ async function localHandoff(s) {
593
+ const health = await settle(s.api.health.$get(), [200]);
594
+ return health.ok ? health.value.capabilities.localHandoff : false;
595
+ }
596
+ async function projectFacts(s, entry) {
597
+ const local = await localHandoff(s);
598
+ return {
599
+ id: entry.id,
600
+ name: entry.name,
601
+ root: local ? entry.root : basename(entry.root),
602
+ status: entry.status,
603
+ ...(entry.branch !== undefined ? { branch: entry.branch } : {}),
604
+ source: entry.source,
605
+ addedAt: entry.addedAt,
606
+ lastOpenedAt: entry.lastOpenedAt,
607
+ ...(entry.forge !== undefined ? { forge: entry.forge } : {}),
608
+ ...(entry.repoUrl !== undefined ? { repoUrl: entry.repoUrl } : {}),
609
+ /** null = inherits the workspace cap. */
610
+ maxParallel: entry.maxParallel ?? null,
611
+ tags: entry.tags ?? [],
612
+ };
613
+ }
614
+ /** The repo config minus the inert per-repo `maxParallel` (§ 4.14). */
615
+ function projectConfig(config) {
616
+ const { maxParallel: _inert, ...rest } = config;
617
+ return rest;
618
+ }
619
+ function skillEntry(root, skill, withBody) {
620
+ const path = projectRelative(root, skill.path);
621
+ return {
622
+ name: skill.name,
623
+ ...(skill.description !== undefined ? { description: skill.description } : {}),
624
+ ...(skill.interactive ? { interactive: true } : {}),
625
+ source: skill.source,
626
+ ...(path !== undefined ? { path } : {}),
627
+ ...(skill.team
628
+ ? {
629
+ team: {
630
+ repo: skill.team.repo,
631
+ ref: skill.team.ref,
632
+ path: skill.team.path,
633
+ ...(skill.team.commit !== undefined ? { commit: skill.team.commit } : {}),
634
+ },
635
+ }
636
+ : {}),
637
+ ...(withBody ? { body: skill.body } : {}),
638
+ };
639
+ }
640
+ /** An account label is user text; one that looks like an email is an identity and is withheld. */
641
+ const looksLikeIdentity = (label) => label.includes('@');
642
+ async function run(args, s) {
643
+ const { action } = args;
644
+ const scope = { projectId: s.projectId };
645
+ const fail = (answer) => failed(action, answer, s.root);
646
+ switch (action) {
647
+ case 'get_config': {
648
+ const answer = await settle(s.api.p[':projectId'].config.$get({ param: scope }), [200]);
649
+ return answer.ok ? ok(action, projectConfig(answer.value)) : fail(answer);
650
+ }
651
+ case 'set_config': {
652
+ const answer = await settle(s.api.p[':projectId'].config.$put({ param: scope, json: args.config }), [200]);
653
+ return answer.ok ? ok(action, projectConfig(answer.value)) : fail(answer);
654
+ }
655
+ case 'get_project': {
656
+ const entry = await boundEntry(s);
657
+ if (!entry.ok)
658
+ return fail(entry);
659
+ if (!entry.value)
660
+ return invalid(action, 'this project is not in the workspace registry listing');
661
+ return ok(action, await projectFacts(s, entry.value));
662
+ }
663
+ case 'set_project': {
664
+ // The registry family is workspace-level and single-mount, so its `:projectId` carries no
665
+ // binding of its own: the BOUND id is substituted here and nothing a client sends reaches it.
666
+ const answer = await settle(s.api.projects[':projectId'].$patch({ param: scope, json: args.project }), [200]);
667
+ return answer.ok ? ok(action, await projectFacts(s, answer.value.project)) : fail(answer);
668
+ }
669
+ case 'get_prompt_templates': {
670
+ const answer = await settle(s.api.p[':projectId']['ui-state'].$get({ param: scope }), [200]);
671
+ // null = never edited here: the cockpit's built-in defaults apply.
672
+ return answer.ok ? ok(action, { promptTemplates: answer.value.promptTemplates ?? null }) : fail(answer);
673
+ }
674
+ case 'set_prompt_templates': {
675
+ // Whole-list replace (§ 4.5): the route merges shallowly at the top level, so the one key
676
+ // sent is the only key touched, and a per-item write would clobber the rest of the list.
677
+ const answer = await settle(s.api.p[':projectId']['ui-state'].$put({ param: scope, json: { promptTemplates: args.promptTemplates } }), [200]);
678
+ return answer.ok ? ok(action, { promptTemplates: answer.value.promptTemplates ?? null }) : fail(answer);
679
+ }
680
+ case 'get_limits': {
681
+ const [workspace, entry, config] = await Promise.all([
682
+ settle(s.api.workspace.config.$get(), [200]),
683
+ boundEntry(s),
684
+ settle(s.api.p[':projectId'].config.$get({ param: scope }), [200]),
685
+ ]);
686
+ if (!workspace.ok)
687
+ return fail(workspace);
688
+ if (!entry.ok)
689
+ return fail(entry);
690
+ if (!config.ok)
691
+ return fail(config);
692
+ const w = workspace.value;
693
+ // § 4.9: every workspace key is a READ, and nothing about another project, the host's
694
+ // environment, its folders or its machine-wide agent defaults is part of it.
695
+ return ok(action, {
696
+ workspace: {
697
+ resources: { ...w.resources },
698
+ followups: { effective: w.effectiveFollowups, inherited: w.followups === null },
699
+ agentEnvPassthrough: { effectiveNames: w.effectiveAgentEnvPassthrough, inherited: w.agentEnvPassthrough === null },
700
+ composerDefaults: {
701
+ autonomous: w.composerDefaults.autonomous ?? w.composerDefaults.inheritedAutonomous,
702
+ autonomousInherited: w.composerDefaults.autonomous === null,
703
+ worktree: w.composerDefaults.worktree ?? w.composerDefaults.inheritedWorktree,
704
+ worktreeInherited: w.composerDefaults.worktree === null,
705
+ },
706
+ skillsAutoUpdate: { effective: w.effectiveSkillsAutoUpdate, inherited: w.skillsAutoUpdate === null },
707
+ },
708
+ project: {
709
+ /** null = this project inherits the workspace cap. */
710
+ maxParallel: entry.value?.maxParallel ?? null,
711
+ memoryLimitMb: config.value.memoryLimitMb,
712
+ worktreeRetention: config.value.worktreeRetention,
713
+ },
714
+ });
715
+ }
716
+ case 'get_capabilities': {
717
+ const [health, providers] = await Promise.all([
718
+ settle(s.api.health.$get(), [200]),
719
+ settle(s.api.providers.status.$get({ query: args.refresh ? { refresh: '1' } : {} }), [200]),
720
+ ]);
721
+ if (!health.ok)
722
+ return fail(health);
723
+ if (!providers.ok)
724
+ return fail(providers);
725
+ return ok(action, {
726
+ capabilities: health.value.capabilities,
727
+ // Availability and version only: never an install path, a home folder or raw CLI output.
728
+ tools: health.value.checks.map((check) => ({
729
+ name: check.name,
730
+ available: check.available,
731
+ ...(check.version !== undefined ? { version: check.version } : {}),
732
+ })),
733
+ // Coarse state and the understandable reason (F-03); never a credential, an account, a
734
+ // login command or an incident id.
735
+ providers: providers.value.providers.map((row) => ({
736
+ provider: row.provider,
737
+ status: row.status,
738
+ ...(row.enabled !== undefined ? { enabled: row.enabled } : {}),
739
+ ...(row.hint !== undefined ? { hint: row.hint } : {}),
740
+ })),
741
+ });
742
+ }
743
+ case 'get_account': {
744
+ const answer = await settle(s.api.workspace['agent-profiles'].$get(), [200]);
745
+ if (!answer.ok)
746
+ return fail(answer);
747
+ const listing = answer.value;
748
+ if (!listing.editable) {
749
+ return ok(action, { available: false, reason: 'account information is not served in hosted mode' });
750
+ }
751
+ // D-03-3: the effective handle and display label for THIS project only — the selection
752
+ // rule `selectionFor` applies (repo first, then the machine default), and nothing else.
753
+ const selection = listing.selections[s.root] ?? {};
754
+ const providers = [...new Set(listing.profiles.map((p) => p.provider))];
755
+ return ok(action, {
756
+ available: true,
757
+ accounts: providers.map((provider) => {
758
+ const handle = selection[provider] ?? listing.defaults[provider] ?? 'default';
759
+ const profile = listing.profiles.find((p) => p.provider === provider && p.id === handle);
760
+ const label = profile?.label;
761
+ return {
762
+ provider,
763
+ handle,
764
+ ...(label && !looksLikeIdentity(label) ? { label } : {}),
765
+ };
766
+ }),
767
+ });
768
+ }
769
+ case 'list_agent_config': {
770
+ const answer = await settle(s.api.p[':projectId']['agent-config'].$get({ param: scope }), [200]);
771
+ if (!answer.ok)
772
+ return fail(answer);
773
+ const files = answer.value.files.flatMap((file) => {
774
+ const def = findConfigFile(file.id);
775
+ // Keyed off the CATALOG's scope, never the row's or a path (E-SCOPE-USER).
776
+ if (!def || def.scope === 'user')
777
+ return [];
778
+ const inside = resolvesInside(s.root, def.resolve(s.root, agentHomePaths(process.env)));
779
+ return [
780
+ {
781
+ id: file.id,
782
+ runners: file.runners,
783
+ kind: file.kind,
784
+ scope: file.scope,
785
+ label: file.label,
786
+ format: file.format,
787
+ tracked: file.tracked,
788
+ seeded: file.seeded,
789
+ holdsMcp: file.holdsMcp,
790
+ precedence: file.precedence,
791
+ ...(file.hotReload !== undefined ? { hotReload: file.hotReload } : {}),
792
+ docsUrl: file.docsUrl,
793
+ ...(inside
794
+ ? { exists: file.exists, size: file.size, version: file.version }
795
+ : { withheld: 'resolves outside this project folder' }),
796
+ writable: file.writable && inside,
797
+ ...(file.readOnlyReason !== undefined ? { readOnlyReason: file.readOnlyReason } : {}),
798
+ },
799
+ ];
800
+ });
801
+ // `userMcp` (the names in ~/.claude.json) is a home file: never part of this answer.
802
+ return ok(action, { editable: answer.value.editable, files });
803
+ }
804
+ case 'read_agent_config': {
805
+ const gate = agentConfigGate(action, args.fileId, s.root);
806
+ if ('result' in gate)
807
+ return gate.result;
808
+ const { def } = gate;
809
+ const answer = await settle(s.api.p[':projectId']['agent-config'][':id'].$get({ param: { ...scope, id: def.id } }), [200]);
810
+ if (!answer.ok)
811
+ return fail(answer);
812
+ const file = answer.value;
813
+ const head = { id: def.id, label: def.label, format: def.format, exists: file.exists, version: file.version };
814
+ if (!def.holdsMcp)
815
+ return ok(action, { ...head, content: file.content });
816
+ // Ruling 12: an MCP-holding file is read as STRUCTURE, built from the parsed file and
817
+ // never from its bytes — every value (command, args, env, headers, URLs, tokens) withheld.
818
+ let structure = null;
819
+ if (file.exists) {
820
+ try {
821
+ structure = structureOf(parseForStructure(file.content, def.format));
822
+ }
823
+ catch {
824
+ return ok(action, { ...head, contentWithheld: true, structure: null, parseError: `the file does not parse as ${def.format}` });
825
+ }
826
+ }
827
+ return ok(action, { ...head, contentWithheld: true, structure });
828
+ }
829
+ case 'write_agent_config': {
830
+ const gate = agentConfigGate(action, args.fileId, s.root);
831
+ if ('result' in gate)
832
+ return gate.result;
833
+ const { def } = gate;
834
+ // Through the cockpit's own route, so its hosted-mode 409 (E-409-LOCAL), its format check,
835
+ // its stale-version 409 and its byte-exact write all apply unchanged.
836
+ const answer = await settle(s.api.p[':projectId']['agent-config'][':id'].$put({
837
+ param: { ...scope, id: def.id },
838
+ json: { content: args.content, version: args.version ?? null },
839
+ }), [200]);
840
+ if (!answer.ok)
841
+ return fail(answer);
842
+ // The echo carries the content; the leader already has it, and an MCP file's must not travel.
843
+ return ok(action, { id: def.id, label: def.label, exists: answer.value.exists, version: answer.value.version });
844
+ }
845
+ case 'list_workflows': {
846
+ const answer = await settle(s.api.p[':projectId'].workflows.$get({ param: scope }), [200]);
847
+ if (!answer.ok)
848
+ return fail(answer);
849
+ return ok(action, {
850
+ workflows: answer.value.workflows.map(({ path, ...workflow }) => {
851
+ const rel = projectRelative(s.root, path);
852
+ return { ...workflow, ...(rel !== undefined ? { path: rel } : {}) };
853
+ }),
854
+ issues: answer.value.issues.map((issue) => ({
855
+ path: projectRelative(s.root, issue.path) ?? basename(issue.path),
856
+ message: scrubPaths(issue.message, s.root),
857
+ })),
858
+ });
859
+ }
860
+ case 'parse_workflow': {
861
+ const answer = await settle(s.api.p[':projectId'].workflows.parse.$post({ param: scope, json: { yaml: args.yaml } }), [200]);
862
+ return answer.ok ? ok(action, answer.value) : fail(answer);
863
+ }
864
+ case 'save_workflow': {
865
+ const workflow = args.workflow;
866
+ // The file the route writes is `<slug>.yaml`, replaced only under `overwrite` (server.ts).
867
+ const target = workflow.overwrite ? { file: `${slugify(workflow.name) || 'chain'}.yaml` } : null;
868
+ const risk = await checkStepsAtRisk(s.root, workflow.name, target);
869
+ if (risk.steps.length > 0 || risk.unreadable.length > 0)
870
+ return qualityGateRefusal(action, risk);
871
+ const answer = await settle(s.api.p[':projectId'].workflows.$post({ param: scope, json: args.workflow }), [201]);
872
+ if (!answer.ok)
873
+ return fail(answer);
874
+ return ok(action, { name: answer.value.name, path: projectRelative(s.root, answer.value.path) ?? null });
875
+ }
876
+ case 'delete_workflow': {
877
+ const name = args.name;
878
+ if (name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
879
+ return invalid(action, `not a workflow name: ${JSON.stringify(name)}`);
880
+ }
881
+ const risk = await checkStepsAtRisk(s.root, name, null);
882
+ if (risk.steps.length > 0)
883
+ return qualityGateRefusal(action, risk);
884
+ const answer = await settle(s.api.p[':projectId'].workflows[':name'].$delete({ param: { ...scope, name } }), [200]);
885
+ if (!answer.ok)
886
+ return fail(answer);
887
+ return ok(action, { deleted: true, name, path: projectRelative(s.root, answer.value.path) ?? null });
888
+ }
889
+ case 'list_skills':
890
+ case 'get_skill':
891
+ case 'refresh_skills': {
892
+ const query = args.wait ? { wait: '1' } : {};
893
+ const answer = action === 'refresh_skills'
894
+ ? await settle(s.api.p[':projectId'].skills.refresh.$post({ param: scope }), [200])
895
+ : await settle(s.api.p[':projectId'].skills.$get({ param: scope, query }), [200]);
896
+ if (!answer.ok)
897
+ return fail(answer);
898
+ if (action === 'get_skill') {
899
+ const skill = answer.value.find((entry) => entry.name === args.name);
900
+ if (!skill)
901
+ return invalid(action, `no skill named ${JSON.stringify(args.name)} in this project's catalog`);
902
+ return ok(action, skillEntry(s.root, skill, true));
903
+ }
904
+ return ok(action, { skills: answer.value.map((skill) => skillEntry(s.root, skill, false)) });
905
+ }
906
+ case 'list_importable_skills': {
907
+ const query = args.wait ? { wait: '1' } : {};
908
+ const answer = await settle(s.api.p[':projectId'].skills.importable.$get({ param: scope, query }), [200]);
909
+ return answer.ok ? ok(action, { skills: answer.value }) : fail(answer);
910
+ }
911
+ case 'check_skill_updates': {
912
+ // The family is workspace-level and takes a project id in the body: the BOUND id is
913
+ // substituted (E-BIND), and the answer is narrowed to this project's scope plus the
914
+ // global availability flag and its reason (§ 4.10, ruling 6).
915
+ const answer = await settle(s.api.workspace['skills-update'].check.$post({ json: { projectId: s.projectId } }), [200]);
916
+ if (!answer.ok)
917
+ return fail(answer);
918
+ const state = answer.value;
919
+ const project = state.scopes.find((entry) => entry.scope === 'project');
920
+ const global = state.scopes.find((entry) => entry.scope === 'global');
921
+ return ok(action, {
922
+ status: state.status,
923
+ available: state.available,
924
+ autoUpdateEnabled: state.autoUpdateEnabled,
925
+ autoUpdateInherited: state.inherited,
926
+ checkedAt: state.checkedAt,
927
+ ...(project
928
+ ? {
929
+ project: {
930
+ status: project.status,
931
+ available: project.available,
932
+ skills: project.skills,
933
+ checkedAt: project.checkedAt,
934
+ ...(project.reason !== undefined ? { reason: scrubPaths(project.reason, s.root) } : {}),
935
+ },
936
+ }
937
+ : {}),
938
+ ...(global
939
+ ? {
940
+ global: {
941
+ available: global.available,
942
+ ...(global.reason !== undefined ? { reason: scrubPaths(global.reason, s.root) } : {}),
943
+ },
944
+ }
945
+ : {}),
946
+ });
947
+ }
948
+ case 'list_automations': {
949
+ const answer = await settle(s.api.p[':projectId'].automations.$get({ param: scope }), [200]);
950
+ return answer.ok ? ok(action, answer.value) : fail(answer);
951
+ }
952
+ case 'create_automation': {
953
+ const { name, prompt, enable } = args.automation;
954
+ const { workflow, ...trigger } = AUTOMATION_FORM_FIXED;
955
+ const json = { name, ...trigger, events: [...trigger.events], filters: { ...trigger.filters }, task: { prompt, workflow }, enable: enable ?? false };
956
+ const answer = await settle(s.api.p[':projectId'].automations.$post({ param: scope, json }), [201]);
957
+ return answer.ok ? ok(action, answer.value) : fail(answer);
958
+ }
959
+ case 'update_automation': {
960
+ const id = args.automationId;
961
+ if (!validPathId(id))
962
+ return invalid(action, `not an automation id: ${JSON.stringify(id)}`);
963
+ const param = { ...scope, id };
964
+ const one = s.api.p[':projectId'].automations[':id'];
965
+ const current = await settle(one.$get({ param }), [200]);
966
+ if (!current.ok)
967
+ return fail(current);
968
+ // The edit form's body: name and prompt change, everything else is the stored value. The
969
+ // revision is the CALLER's, so an edit based on a stale read still answers the route's 409.
970
+ const a = current.value.automation;
971
+ const edit = args.update;
972
+ const json = {
973
+ name: edit.name ?? a.name,
974
+ ...(a.description !== undefined ? { description: a.description } : {}),
975
+ events: a.events,
976
+ intervalSeconds: a.intervalSeconds,
977
+ filters: a.filters,
978
+ task: { ...a.task, prompt: edit.prompt ?? a.task.prompt },
979
+ enabled: a.enabled,
980
+ expectedRevision: edit.expectedRevision,
981
+ };
982
+ const answer = await settle(one.$put({ param, json: json }), [200]);
983
+ return answer.ok ? ok(action, answer.value) : fail(answer);
984
+ }
985
+ case 'get_automation':
986
+ case 'delete_automation':
987
+ case 'enable_automation':
988
+ case 'pause_automation':
989
+ case 'check_automation': {
990
+ const id = args.automationId;
991
+ if (!validPathId(id))
992
+ return invalid(action, `not an automation id: ${JSON.stringify(id)}`);
993
+ const param = { ...scope, id };
994
+ const one = s.api.p[':projectId'].automations[':id'];
995
+ const answer = action === 'get_automation'
996
+ ? await settle(one.$get({ param }), [200])
997
+ : action === 'delete_automation'
998
+ ? await settle(one.$delete({ param }), [204])
999
+ : action === 'enable_automation'
1000
+ ? await settle(one.enable.$post({ param }), [200])
1001
+ : action === 'pause_automation'
1002
+ ? await settle(one.pause.$post({ param }), [200])
1003
+ : await settle(one.check.$post({ param, json: { mode: args.mode } }), [202]);
1004
+ if (!answer.ok)
1005
+ return fail(answer);
1006
+ return ok(action, action === 'delete_automation' ? { deleted: true, automationId: id } : answer.value);
1007
+ }
1008
+ case 'get_automation_check': {
1009
+ const checkId = args.checkId;
1010
+ if (!validPathId(checkId))
1011
+ return invalid(action, `not a check id: ${JSON.stringify(checkId)}`);
1012
+ const check = await settle(s.api['automation-checks'][':checkId'].$get({ param: { checkId } }), [200]);
1013
+ if (!check.ok)
1014
+ return fail(check);
1015
+ // The check family is workspace-level (a server-memory map). M-18: it is served only when
1016
+ // its automation belongs to THIS project — otherwise it is exactly as unknown as a bad id.
1017
+ const owner = await settle(s.api.p[':projectId'].automations[':id'].$get({ param: { ...scope, id: check.value.automationId } }), [200]);
1018
+ if (!owner.ok)
1019
+ return failed(action, { ok: false, status: 404, error: 'not found', body: undefined }, s.root);
1020
+ return ok(action, check.value);
1021
+ }
1022
+ case 'get_automation_log': {
1023
+ const q = args.logQuery ?? {};
1024
+ const query = Object.fromEntries(Object.entries(q)
1025
+ .filter(([, value]) => value !== undefined)
1026
+ .map(([key, value]) => [key, String(value)]));
1027
+ const answer = await settle(s.api.p[':projectId']['automation-log'].$get({ param: scope, query: query }), [200]);
1028
+ return answer.ok ? ok(action, answer.value) : fail(answer);
1029
+ }
1030
+ case 'retry_automation_receipt': {
1031
+ const receiptId = args.receiptId;
1032
+ if (!validPathId(receiptId))
1033
+ return invalid(action, `not a receipt id: ${JSON.stringify(receiptId)}`);
1034
+ const answer = await settle(s.api.p[':projectId']['automation-log'][':receiptId'].retry.$post({ param: { ...scope, receiptId } }), [202]);
1035
+ return answer.ok ? ok(action, answer.value) : fail(answer);
1036
+ }
1037
+ case 'list_worktrees': {
1038
+ const answer = await settle(s.api.p[':projectId'].worktrees.$get({ param: scope }), [200]);
1039
+ return answer.ok ? ok(action, answer.value) : fail(answer);
1040
+ }
1041
+ case 'reclaim_worktrees': {
1042
+ const answer = await settle(s.api.p[':projectId'].worktrees.reclaim.$post({ param: scope, json: {} }), [200]);
1043
+ return answer.ok ? ok(action, answer.value) : fail(answer);
1044
+ }
1045
+ case 'remove_worktree': {
1046
+ const id = args.runId;
1047
+ if (id === '.' || id === '..' || !runIdParamSchema.safeParse({ id }).success) {
1048
+ return invalid(action, `not a run id: ${JSON.stringify(id)}`);
1049
+ }
1050
+ const answer = await settle(s.api.p[':projectId'].runs[':id']['remove-worktree'].$post({ param: { ...scope, id }, json: { expectedVersion: args.expectedVersion } }), [200]);
1051
+ return answer.ok ? ok(action, { ...answer.value, runId: id }) : fail(answer);
1052
+ }
1053
+ }
1054
+ }
1055
+ export const projectConfigTool = defineTool({
1056
+ name: 'project_config',
1057
+ title: 'Project configuration',
1058
+ description: "Read and change THIS project's own configuration: its settings (agent, models, system prompt, review gate, base branch, worktree retention, memory limit), its registry entry (concurrency cap and tags), prompt templates, in-repo agent config files, workflows, skills, GitHub automations and worktrees. Shared settings are readable only as effective limits and capabilities (get_limits, get_capabilities, get_account). Workspace-wide settings, agent accounts, account identity, home files, the project registry and host folders are outside this boundary and are refused with the reason.",
1059
+ inputSchema: projectConfigInputSchema,
1060
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1061
+ async call(args, ctx) {
1062
+ if (args.projectId !== undefined) {
1063
+ return refused(args.action, 'project-binding', `this connection is bound to project ${ctx.project.name} and acts on it alone; a project cannot be named.`);
1064
+ }
1065
+ if (isRefused(args.action)) {
1066
+ const { boundary, reason } = REFUSED_ACTIONS[args.action];
1067
+ return refused(args.action, boundary, reason);
1068
+ }
1069
+ if (!ctx.service) {
1070
+ return errorResult('project_config is unavailable: this xezar service did not hand the tool its in-process entry.');
1071
+ }
1072
+ return run({ ...args, action: args.action }, { api: buildClient(ctx.service), projectId: ctx.project.id, root: ctx.project.root });
1073
+ },
1074
+ });
1075
+ //# sourceMappingURL=project-config.js.map