@akira-tl/forgerelay 1.1.2 → 1.2.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 (170) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +14 -0
  3. package/dist/activity/runtime/mcp-query-tools.js +15 -5
  4. package/dist/cli/config/inspect.js +331 -0
  5. package/dist/cli/config/migrate.js +351 -0
  6. package/dist/cli/init/setup-config.js +41 -0
  7. package/dist/cli/init.js +128 -130
  8. package/dist/cli/maintenance.js +15 -5
  9. package/dist/cli/mcp/external-mcp.js +14 -10
  10. package/dist/cli/mcp/status.js +11 -6
  11. package/dist/cli.js +40 -7
  12. package/dist/lsp/language-server-config.js +66 -144
  13. package/dist/lsp/runtime/manager.js +35 -4
  14. package/dist/mcp/hooks/config.js +141 -0
  15. package/dist/mcp/hooks/external-mcp-transform.js +22 -13
  16. package/dist/mcp/hooks/hook-cli.js +62 -52
  17. package/dist/mcp/hooks/hooks.js +111 -85
  18. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
  19. package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
  20. package/dist/mcp/server/core/capability-support.js +7 -0
  21. package/dist/runtime/config/config.js +142 -64
  22. package/dist/runtime/config/definition/catalog.js +12 -0
  23. package/dist/runtime/config/definition/definition.js +115 -0
  24. package/dist/runtime/config/definition/external-mcp.js +58 -0
  25. package/dist/runtime/config/definition/general-config.js +258 -0
  26. package/dist/runtime/config/definition/language-servers.js +116 -0
  27. package/dist/runtime/config/definition/schema.js +113 -0
  28. package/dist/runtime/config/definition/types.js +2 -0
  29. package/dist/runtime/config/external-mcp-auth-store.js +105 -29
  30. package/dist/runtime/config/external-mcp-config.js +39 -0
  31. package/dist/runtime/config/external-mcp-registry.js +284 -106
  32. package/dist/runtime/config/inspection/live.js +77 -0
  33. package/dist/runtime/config/resolution/general.js +93 -0
  34. package/dist/runtime/config/resolution/hooks.js +315 -0
  35. package/dist/runtime/config/resolution/language-servers.js +237 -0
  36. package/dist/runtime/config/resolution/project-sources.js +159 -0
  37. package/dist/runtime/config/resolution/resolver.js +485 -0
  38. package/dist/runtime/config/resolution/types.js +1 -0
  39. package/dist/runtime/config/runtime/config-runtime.js +74 -0
  40. package/dist/runtime/config/runtime/source-refresh.js +263 -0
  41. package/dist/runtime/config/user-config.js +38 -31
  42. package/dist/runtime/security/project-execution-trust.js +34 -0
  43. package/dist/runtime/testing/server-fixture.js +9 -8
  44. package/dist/server.js +10 -6
  45. package/dist/subagents/profiles.js +336 -53
  46. package/dist/subagents/sessions/execution.js +6 -35
  47. package/dist/subagents/sessions/manager.js +14 -2
  48. package/dist/ui/.vite/manifest.json +275 -275
  49. package/dist/ui/activity-panel-app.html +1 -1
  50. package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
  51. package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
  52. package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
  53. package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
  54. package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
  55. package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
  56. package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
  57. package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
  58. package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
  59. package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
  60. package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
  61. package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
  62. package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
  63. package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
  64. package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
  65. package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
  66. package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
  67. package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
  68. package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
  69. package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
  70. package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
  71. package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
  72. package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
  73. package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
  74. package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
  75. package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
  76. package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
  77. package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
  78. package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
  79. package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
  80. package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
  81. package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
  82. package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
  83. package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
  84. package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
  85. package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
  86. package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
  87. package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
  88. package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
  89. package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
  90. package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
  91. package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
  92. package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
  93. package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
  94. package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
  95. package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
  96. package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
  97. package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
  98. package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
  99. package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
  100. package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
  101. package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
  102. package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
  103. package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
  104. package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
  105. package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
  106. package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
  107. package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
  108. package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
  109. package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
  110. package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
  111. package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
  112. package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
  113. package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
  114. package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
  115. package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
  116. package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
  117. package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
  118. package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
  119. package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
  120. package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
  121. package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
  122. package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
  123. package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
  124. package/dist/workspaces/conversation-test-support.js +3 -11
  125. package/dist/workspaces/sessions.js +2 -0
  126. package/dist/workspaces/state/project-context.js +221 -0
  127. package/dist/workspaces.js +3 -1
  128. package/docs/chatgpt-coding-workflow.md +8 -3
  129. package/docs/configuration.md +99 -41
  130. package/docs/gotchas.md +24 -5
  131. package/docs/security.md +14 -3
  132. package/docs/setup.md +12 -7
  133. package/package.json +12 -3
  134. package/schemas/v1/config.project-local.schema.json +14 -0
  135. package/schemas/v1/config.project.schema.json +14 -0
  136. package/schemas/v1/config.user.schema.json +538 -0
  137. package/schemas/v1/hooks.project-local.schema.json +118 -0
  138. package/schemas/v1/hooks.project.schema.json +118 -0
  139. package/schemas/v1/hooks.user.schema.json +118 -0
  140. package/schemas/v1/language-servers.project-local.schema.json +230 -0
  141. package/schemas/v1/language-servers.project.schema.json +230 -0
  142. package/schemas/v1/language-servers.user.schema.json +230 -0
  143. package/schemas/v1/mcp.project-local.schema.json +143 -0
  144. package/schemas/v1/mcp.project.schema.json +143 -0
  145. package/schemas/v1/mcp.user.schema.json +143 -0
  146. package/scripts/ci/architecture.mjs +54 -4
  147. package/scripts/ci/cmd-acceptance.mjs +9 -9
  148. package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
  149. package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
  150. package/scripts/ci/gates/core-contract.mjs +27 -0
  151. package/scripts/ci/gates/platform-tests.mjs +38 -0
  152. package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
  153. package/scripts/ci/gates/test-shard.mjs +71 -0
  154. package/scripts/ci/powershell51-acceptance.mjs +9 -9
  155. package/scripts/ci/pwsh-acceptance.mjs +9 -9
  156. package/scripts/ci/verify.mjs +2 -0
  157. package/scripts/ci/windows-product-acceptance.mjs +9 -10
  158. package/scripts/config/schema-text.mjs +4 -0
  159. package/scripts/config/schema-text.test.mjs +18 -0
  160. package/scripts/config/schema.mjs +74 -0
  161. package/scripts/lsp/interop.mjs +4 -1
  162. package/scripts/release/pack.mjs +64 -4
  163. package/scripts/release/release-gate.test.mjs +93 -27
  164. package/scripts/release-parity.mjs +19 -2
  165. package/templates/instructions/cmd.md +12 -0
  166. package/templates/instructions/fish.md +11 -0
  167. package/templates/instructions/powershell.md +14 -0
  168. package/templates/instructions/pwsh.md +14 -0
  169. package/templates/instructions/zsh.md +11 -0
  170. package/dist/runtime/instructions/powershell-skill.js +0 -65
@@ -1,7 +1,10 @@
1
- import { existsSync } from "node:fs";
2
- import { readdir, readFile } from "node:fs/promises";
3
1
  import { basename, join, resolve } from "node:path";
2
+ import * as z from "zod/v4";
4
3
  import { parse as parseYaml } from "yaml";
4
+ import { defineConfigDomain } from "../runtime/config/definition/definition.js";
5
+ import { resolveConfigDomain } from "../runtime/config/resolution/resolver.js";
6
+ import { resolveProjectContext } from "../workspaces/state/project-context.js";
7
+ import { projectExecutionRequirement } from "../runtime/security/project-execution-trust.js";
5
8
  export const SUBAGENT_PROVIDERS = [
6
9
  "codex",
7
10
  "claude",
@@ -11,23 +14,125 @@ export const SUBAGENT_PROVIDERS = [
11
14
  "copilot",
12
15
  ];
13
16
  const FRONTMATTER_DELIMITER = "---";
14
- const PROVIDERS = new Set(SUBAGENT_PROVIDERS);
17
+ const CANONICAL_PRIORITY = 100;
18
+ const LEGACY_PRIORITY = 0;
19
+ const LEGACY_DEPRECATION = {
20
+ since: "1.2.0",
21
+ removeIn: "1.4.0",
22
+ replacement: "subagents/",
23
+ };
24
+ const profileNameSchema = z.string().trim().min(1);
25
+ const providerSchema = z.enum(SUBAGENT_PROVIDERS);
26
+ const optionalNonEmptyString = z.string().trim().min(1).optional();
27
+ const activeProfileFrontmatterSchema = z.object({
28
+ name: profileNameSchema.optional(),
29
+ description: z.string().trim().min(1),
30
+ provider: providerSchema,
31
+ model: optionalNonEmptyString,
32
+ thinking: optionalNonEmptyString,
33
+ disabled: z.literal(false).optional(),
34
+ }).strict();
35
+ const disabledProfileFrontmatterSchema = z.object({
36
+ name: profileNameSchema.optional(),
37
+ description: optionalNonEmptyString,
38
+ provider: providerSchema.optional(),
39
+ model: optionalNonEmptyString,
40
+ thinking: optionalNonEmptyString,
41
+ disabled: z.literal(true),
42
+ }).strict();
43
+ const subagentProfileValueSchema = z.object({
44
+ description: z.string().trim().min(1),
45
+ provider: providerSchema,
46
+ model: optionalNonEmptyString,
47
+ thinking: optionalNonEmptyString,
48
+ body: z.string(),
49
+ }).strict();
50
+ const subagentProfileDisabledSchema = z.object({ disabled: z.literal(true) }).strict();
51
+ const subagentProfilesSchema = z.record(profileNameSchema, z.union([subagentProfileValueSchema, subagentProfileDisabledSchema]));
52
+ export const subagentProfilesConfigDefinition = defineConfigDomain({
53
+ domain: "subagents",
54
+ title: "ForgeRelay Subagent Profile configuration",
55
+ description: "Markdown Subagent Profiles resolved by profile name across user, Project, and Project Local scopes.",
56
+ schemaOutput: "none",
57
+ fields: {
58
+ profiles: {
59
+ schema: subagentProfilesSchema,
60
+ description: "Subagent Profiles keyed by effective profile name.",
61
+ required: true,
62
+ legalScopes: ["project-local", "project", "user"],
63
+ merge: "keyed",
64
+ reload: "hot",
65
+ sensitivity: "sensitive",
66
+ interpolation: "none",
67
+ builtIn: { kind: "none" },
68
+ executionEffect: (value) => isRecord(value) && value.disabled === true ? "none" : "process",
69
+ },
70
+ },
71
+ });
72
+ export async function resolveSubagentProfilesConfig(config, workspaceRoot) {
73
+ const project = await resolveProjectContext(config.configDir, workspaceRoot);
74
+ return resolveSubagentProfilesConfigSources({
75
+ configDir: config.configDir,
76
+ sourceRuntime: config.configRuntime.sources,
77
+ projectSharedConfigDir: project.sharedConfigDir,
78
+ projectLocalConfigDir: project.localConfigDir,
79
+ });
80
+ }
81
+ export function resolveSubagentProfilesConfigSources(input) {
82
+ const sources = [];
83
+ const refreshDiagnostics = [];
84
+ appendProfileDirectory(sources, refreshDiagnostics, input.sourceRuntime, "user", join(input.configDir, "agents"), false);
85
+ appendProfileDirectory(sources, refreshDiagnostics, input.sourceRuntime, "user", join(input.configDir, "subagents"), true);
86
+ if (input.projectSharedConfigDir) {
87
+ appendProfileDirectory(sources, refreshDiagnostics, input.sourceRuntime, "project", join(input.projectSharedConfigDir, "agents"), false);
88
+ appendProfileDirectory(sources, refreshDiagnostics, input.sourceRuntime, "project", join(input.projectSharedConfigDir, "subagents"), true);
89
+ }
90
+ if (input.projectLocalConfigDir) {
91
+ appendProfileDirectory(sources, refreshDiagnostics, input.sourceRuntime, "project-local", join(input.projectLocalConfigDir, "subagents"), true);
92
+ }
93
+ const resolution = resolveConfigDomain({
94
+ definition: subagentProfilesConfigDefinition,
95
+ sources,
96
+ });
97
+ resolution.diagnostics = mergeDiagnostics(resolution.diagnostics, refreshDiagnostics);
98
+ return resolution;
99
+ }
15
100
  export async function loadSubagentProfiles(config, workspaceRoot) {
16
101
  if (!config.subagents)
17
102
  return [];
18
- const profileDirs = [
19
- config.configAgentsDir,
20
- join(workspaceRoot, ".forgerelay", "agents"),
21
- ];
22
- const profilesByName = new Map();
23
- for (const directory of profileDirs) {
24
- for (const profile of await loadProfilesFromDirectory(directory)) {
25
- profilesByName.set(profile.name, profile);
26
- }
103
+ const project = await resolveProjectContext(config.configDir, workspaceRoot);
104
+ const resolution = resolveSubagentProfilesConfigSources({
105
+ configDir: config.configDir,
106
+ sourceRuntime: config.configRuntime.sources,
107
+ projectSharedConfigDir: project.sharedConfigDir,
108
+ projectLocalConfigDir: project.localConfigDir,
109
+ });
110
+ for (const diagnostic of resolution.diagnostics) {
111
+ if (diagnostic.severity !== "error" || diagnostic.diagnosticChanged === false)
112
+ continue;
113
+ console.warn(`Skipping invalid subagent profile ${diagnostic.source.location ?? diagnostic.source.id}: ${diagnostic.message}`);
27
114
  }
28
- return Array.from(profilesByName.values())
29
- .filter((profile) => !profile.disabled)
30
- .sort((a, b) => a.name.localeCompare(b.name));
115
+ return effectiveSubagentProfileEntries(resolution)
116
+ .map((entry) => {
117
+ const executionRequirement = projectExecutionRequirement({
118
+ projectId: project.id,
119
+ resolution,
120
+ entryKey: `profiles.${entry.name}`,
121
+ display: { kind: "subagent-profile", name: entry.name },
122
+ });
123
+ return {
124
+ name: entry.name,
125
+ description: entry.value.description,
126
+ provider: entry.value.provider,
127
+ ...(entry.value.model ? { model: entry.value.model } : {}),
128
+ ...(entry.value.thinking ? { thinking: entry.value.thinking } : {}),
129
+ filePath: entry.filePath,
130
+ body: entry.value.body,
131
+ disabled: false,
132
+ ...(executionRequirement ? { executionRequirement } : {}),
133
+ };
134
+ })
135
+ .sort((left, right) => left.name.localeCompare(right.name));
31
136
  }
32
137
  export function summarizeSubagentProfile(profile) {
33
138
  return {
@@ -46,31 +151,205 @@ export function formatAvailableSubagentProfile(profile) {
46
151
  : "";
47
152
  return `${profile.name} (${profile.provider}${model}${thinking}${availability})`;
48
153
  }
49
- async function loadProfilesFromDirectory(directory) {
154
+ function effectiveSubagentProfileEntries(resolution) {
155
+ const values = isRecord(resolution.values.profiles) ? resolution.values.profiles : {};
156
+ const result = [];
157
+ for (const [name, value] of Object.entries(values)) {
158
+ const parsed = subagentProfileValueSchema.safeParse(value);
159
+ const entry = resolution.entries[`profiles.${name}`];
160
+ if (!parsed.success || !entry || !entry.effective.source.location) {
161
+ throw new Error(`Subagent Profile resolver produced an invalid effective entry for ${name}.`);
162
+ }
163
+ result.push({
164
+ name,
165
+ value: parsed.data,
166
+ scope: entry.effective.source.scope,
167
+ filePath: entry.effective.source.location,
168
+ });
169
+ }
170
+ return result;
171
+ }
172
+ function appendProfileDirectory(sources, diagnostics, sourceRuntime, scope, directory, canonical) {
50
173
  const resolvedDirectory = resolve(directory);
51
- if (!existsSync(resolvedDirectory))
52
- return [];
53
- const entries = await readdir(resolvedDirectory, { withFileTypes: true });
54
- const profiles = [];
55
- for (const entry of entries) {
56
- if (!entry.isFile())
57
- continue;
58
- if (!entry.name.endsWith(".md"))
174
+ const refreshed = sourceRuntime.refreshDirectory({
175
+ key: `subagents:${canonical ? "canonical" : "legacy"}:${scope}:${resolvedDirectory}`,
176
+ directory: resolvedDirectory,
177
+ include: (name) => name.endsWith(".md"),
178
+ parse: (_name, content, filePath) => parseProfileSourceDocument(canonical, content, filePath),
179
+ parseIssue: (error) => ({
180
+ code: "invalid_source",
181
+ message: "Subagent Profile is invalid.",
182
+ ...(error instanceof SubagentProfileSourceError && error.keyHint
183
+ ? { keyHint: error.keyHint }
184
+ : {}),
185
+ }),
186
+ readIssue: { code: "invalid_source", message: "Subagent Profile could not be read." },
187
+ });
188
+ if (refreshed.state === "invalid" && refreshed.issue) {
189
+ diagnostics.push({
190
+ severity: "error",
191
+ code: refreshed.issue.code,
192
+ source: {
193
+ id: `${canonical ? "canonical" : "legacy"}:${scope}:subagents-directory`,
194
+ scope,
195
+ kind: "file",
196
+ location: resolvedDirectory,
197
+ priority: canonical ? CANONICAL_PRIORITY : LEGACY_PRIORITY,
198
+ },
199
+ message: refreshed.issue.message,
200
+ diagnosticChanged: refreshed.diagnosticChanged,
201
+ });
202
+ }
203
+ for (const unit of refreshed.units) {
204
+ const filePath = unit.status.path;
205
+ if (unit.value !== undefined) {
206
+ const source = profileSource(scope, filePath, unit.name, canonical, unit.value);
207
+ sources.push(source);
208
+ if (unit.status.state === "invalid" && unit.issue) {
209
+ diagnostics.push({
210
+ severity: "error",
211
+ code: unit.issue.code,
212
+ source: sourceReference(source),
213
+ message: unit.issue.message,
214
+ usingLastKnownGood: true,
215
+ diagnosticChanged: unit.status.diagnosticChanged,
216
+ });
217
+ }
59
218
  continue;
60
- const filePath = join(resolvedDirectory, entry.name);
61
- try {
62
- profiles.push(await loadProfileFile(filePath));
63
219
  }
64
- catch (error) {
65
- console.warn(`Skipping invalid subagent profile ${filePath}: ${errorMessage(error)}`);
220
+ if (unit.status.state === "invalid") {
221
+ sources.push(invalidProfileSource(scope, filePath, profileKeyHintFromIssue(unit.issue) ?? basename(unit.name, ".md"), canonical, unit.issue?.message ?? "Subagent Profile is invalid.", unit.issue?.code));
66
222
  }
67
223
  }
68
- return profiles;
69
224
  }
70
- async function loadProfileFile(filePath) {
71
- const content = await readFile(filePath, "utf8");
225
+ function profileSource(scope, filePath, fileName, canonical, profile) {
226
+ const value = profile.disabled
227
+ ? { disabled: true }
228
+ : {
229
+ description: profile.description,
230
+ provider: profile.provider,
231
+ ...(profile.model ? { model: profile.model } : {}),
232
+ ...(profile.thinking ? { thinking: profile.thinking } : {}),
233
+ body: profile.body,
234
+ };
235
+ return {
236
+ id: `${canonical ? "canonical" : "legacy"}:${scope}:subagent:${fileName}`,
237
+ scope,
238
+ kind: "file",
239
+ location: filePath,
240
+ priority: canonical ? CANONICAL_PRIORITY : LEGACY_PRIORITY,
241
+ normalized: true,
242
+ ...(canonical ? { shadowsLowerPriorityKeys: [`profiles.${profile.name}`] } : { deprecation: LEGACY_DEPRECATION }),
243
+ value: { profiles: { [profile.name]: value } },
244
+ };
245
+ }
246
+ function invalidProfileSource(scope, filePath, key, canonical, message, code = "invalid_source") {
247
+ return {
248
+ id: `${canonical ? "canonical" : "legacy"}:${scope}:subagent:${basename(filePath)}`,
249
+ scope,
250
+ kind: "file",
251
+ location: filePath,
252
+ priority: canonical ? CANONICAL_PRIORITY : LEGACY_PRIORITY,
253
+ ...(canonical ? { shadowsLowerPriorityKeys: [`profiles.${key}`] } : { deprecation: LEGACY_DEPRECATION }),
254
+ error: { code, message },
255
+ };
256
+ }
257
+ class SubagentProfileSourceError extends Error {
258
+ keyHint;
259
+ constructor(keyHint) {
260
+ super("Subagent Profile source is invalid.");
261
+ this.keyHint = keyHint;
262
+ this.name = "SubagentProfileSourceError";
263
+ }
264
+ }
265
+ function parseProfileSourceDocument(canonical, content, filePath) {
266
+ const keyHint = profileKeyHint(content, filePath);
267
+ try {
268
+ return canonical
269
+ ? canonicalProfileFromDocument(content, filePath)
270
+ : legacyProfileFromDocument(content, filePath);
271
+ }
272
+ catch {
273
+ throw new SubagentProfileSourceError(keyHint);
274
+ }
275
+ }
276
+ function canonicalProfileFromDocument(content, filePath) {
277
+ const parsed = parseFrontmatter(content, filePath);
278
+ const frontmatter = z.union([
279
+ disabledProfileFrontmatterSchema,
280
+ activeProfileFrontmatterSchema,
281
+ ]).parse(parsed.frontmatter);
282
+ const name = frontmatter.name ?? basename(filePath, ".md");
283
+ if (frontmatter.disabled === true) {
284
+ return {
285
+ name,
286
+ description: frontmatter.description ?? "Disabled Subagent Profile.",
287
+ provider: frontmatter.provider ?? "codex",
288
+ model: frontmatter.model,
289
+ thinking: frontmatter.thinking,
290
+ filePath,
291
+ body: parsed.body,
292
+ disabled: true,
293
+ };
294
+ }
295
+ return {
296
+ name,
297
+ description: frontmatter.description,
298
+ provider: frontmatter.provider,
299
+ model: frontmatter.model,
300
+ thinking: frontmatter.thinking,
301
+ filePath,
302
+ body: parsed.body,
303
+ disabled: false,
304
+ };
305
+ }
306
+ function legacyProfileFromDocument(content, filePath) {
72
307
  const parsed = parseFrontmatter(content, filePath);
73
- return profileFromFrontmatter(parsed.frontmatter, parsed.body, filePath);
308
+ return legacyProfileFromFrontmatter(parsed.frontmatter, parsed.body, filePath);
309
+ }
310
+ export function canonicalSubagentProfileDocumentFromLegacy(content, filePath) {
311
+ const profile = legacyProfileFromDocument(content, filePath);
312
+ const frontmatter = [
313
+ `name: ${JSON.stringify(profile.name)}`,
314
+ `description: ${JSON.stringify(profile.description)}`,
315
+ `provider: ${profile.provider}`,
316
+ ...(profile.model ? [`model: ${JSON.stringify(profile.model)}`] : []),
317
+ ...(profile.thinking ? [`thinking: ${JSON.stringify(profile.thinking)}`] : []),
318
+ ...(profile.disabled ? ["disabled: true"] : []),
319
+ ];
320
+ return [FRONTMATTER_DELIMITER, ...frontmatter, FRONTMATTER_DELIMITER, profile.body, ""].join("\n");
321
+ }
322
+ function legacyProfileFromFrontmatter(frontmatter, body, filePath) {
323
+ const name = readString(frontmatter, "name") ?? basename(filePath, ".md");
324
+ const description = readString(frontmatter, "description");
325
+ const provider = readProvider(frontmatter, filePath);
326
+ if (!description) {
327
+ throw new Error(`Subagent profile is missing description: ${filePath}`);
328
+ }
329
+ return {
330
+ name,
331
+ description,
332
+ provider,
333
+ model: readString(frontmatter, "model"),
334
+ thinking: readString(frontmatter, "thinking"),
335
+ filePath,
336
+ body,
337
+ disabled: frontmatter.disabled === true,
338
+ };
339
+ }
340
+ function profileKeyHint(content, filePath) {
341
+ try {
342
+ return readString(parseFrontmatter(content, filePath).frontmatter, "name");
343
+ }
344
+ catch {
345
+ return undefined;
346
+ }
347
+ }
348
+ function profileKeyHintFromIssue(issue) {
349
+ if (!issue || !("keyHint" in issue) || typeof issue.keyHint !== "string")
350
+ return undefined;
351
+ const trimmed = issue.keyHint.trim();
352
+ return trimmed || undefined;
74
353
  }
75
354
  function parseFrontmatter(content, filePath) {
76
355
  const normalized = content.replace(/^\uFEFF/, "");
@@ -100,36 +379,18 @@ function parseProfileYaml(source, filePath) {
100
379
  }
101
380
  return parsed;
102
381
  }
103
- function profileFromFrontmatter(frontmatter, body, filePath) {
104
- const name = readString(frontmatter, "name") ?? basename(filePath, ".md");
105
- const description = readString(frontmatter, "description");
106
- const provider = readProvider(frontmatter, filePath);
107
- if (!description) {
108
- throw new Error(`Subagent profile is missing description: ${filePath}`);
109
- }
110
- return {
111
- name,
112
- description,
113
- provider,
114
- model: readString(frontmatter, "model"),
115
- thinking: readString(frontmatter, "thinking"),
116
- filePath,
117
- body,
118
- disabled: frontmatter.disabled === true,
119
- };
120
- }
121
382
  function readProvider(frontmatter, filePath) {
122
383
  const provider = readString(frontmatter, "provider");
123
384
  if (!provider) {
124
385
  throw new Error(`Subagent profile is missing provider: ${filePath}`);
125
386
  }
126
- if (!PROVIDERS.has(provider)) {
387
+ if (!isSubagentProvider(provider)) {
127
388
  throw new Error(`Subagent profile provider must be codex, claude, opencode, pi, cursor, or copilot: ${filePath}`);
128
389
  }
129
390
  return provider;
130
391
  }
131
392
  export function isSubagentProvider(value) {
132
- return PROVIDERS.has(value);
393
+ return SUBAGENT_PROVIDERS.includes(value);
133
394
  }
134
395
  function readString(frontmatter, key) {
135
396
  const value = frontmatter[key];
@@ -138,6 +399,28 @@ function readString(frontmatter, key) {
138
399
  const trimmed = value.trim();
139
400
  return trimmed || undefined;
140
401
  }
402
+ function sourceReference(source) {
403
+ return {
404
+ id: source.id,
405
+ scope: source.scope,
406
+ kind: source.kind,
407
+ ...(source.location ? { location: source.location } : {}),
408
+ priority: source.priority,
409
+ };
410
+ }
411
+ function mergeDiagnostics(left, right) {
412
+ const seen = new Set();
413
+ return [...left, ...right].filter((diagnostic) => {
414
+ const key = `${diagnostic.source.id}\0${diagnostic.code}\0${diagnostic.message}\0${diagnostic.usingLastKnownGood === true}`;
415
+ if (seen.has(key))
416
+ return false;
417
+ seen.add(key);
418
+ return true;
419
+ });
420
+ }
421
+ function isRecord(value) {
422
+ return typeof value === "object" && value !== null && !Array.isArray(value);
423
+ }
141
424
  function errorMessage(error) {
142
425
  return error instanceof Error ? error.message : String(error);
143
426
  }
@@ -1,5 +1,5 @@
1
1
  import { HookRunner } from "../../mcp/hooks/hooks.js";
2
- import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
2
+ import { isSubagentProvider, } from "../profiles.js";
3
3
  import { runSubagentProvider } from "../providers/registry.js";
4
4
  import { SubagentDeliveryMailbox } from "./delivery-mailbox.js";
5
5
  import { createSubagentSessionStore, } from "./store.js";
@@ -13,7 +13,7 @@ export async function executeSubagentRun(config, input, providerRunner = runSuba
13
13
  if (record.activeRun?.id !== input.runId) {
14
14
  throw new Error(`Subagent Run ${input.runId} is not active for Session ${record.id}.`);
15
15
  }
16
- const hooks = new HookRunner(config.hooks, config.logging, process.env, undefined, config.commandShellRuntime);
16
+ const hooks = new HookRunner({}, config.logging, process.env, undefined, config.commandShellRuntime, config.configDir, config.configRuntime.sources);
17
17
  const hookInvocation = {
18
18
  workspaceId: record.workspaceId,
19
19
  workspaceRoot: record.workspaceRoot,
@@ -33,7 +33,7 @@ export async function executeSubagentRun(config, input, providerRunner = runSuba
33
33
  try {
34
34
  await hooks.run("SubagentStart", hookInvocation);
35
35
  input.signal?.throwIfAborted();
36
- result = await runSessionProvider(config, record, input.prompt, providerRunner, input.signal);
36
+ result = await runSessionProvider(record, input.prompt, providerRunner, input.signal);
37
37
  input.signal?.throwIfAborted();
38
38
  }
39
39
  catch (error) {
@@ -109,43 +109,14 @@ export async function executeSubagentSession(config, sessionId, prompt) {
109
109
  store.close();
110
110
  }
111
111
  }
112
- async function runSessionProvider(config, session, prompt, providerRunner, signal) {
112
+ async function runSessionProvider(session, prompt, providerRunner, signal) {
113
113
  if (!isSubagentProvider(session.provider)) {
114
114
  throw new Error(`Unknown subagent provider for Session ${session.id}: ${session.provider}`);
115
115
  }
116
- if (session.providerSessionId) {
117
- return providerRunner(session.provider, {
118
- prompt,
119
- workspace: session.workspaceRoot,
120
- providerSessionId: session.providerSessionId,
121
- writeMode: "allowed",
122
- model: session.model,
123
- thinking: session.thinking,
124
- signal,
125
- });
126
- }
127
- if (session.profileName === session.provider) {
128
- return providerRunner(session.provider, {
129
- prompt,
130
- workspace: session.workspaceRoot,
131
- writeMode: "allowed",
132
- model: session.model,
133
- thinking: session.thinking,
134
- signal,
135
- });
136
- }
137
- const profiles = await loadSubagentProfiles(config, session.workspaceRoot);
138
- const profile = profiles.find((candidate) => candidate.name === session.profileName);
139
- if (!profile)
140
- throw new Error(`Subagent profile not found: ${session.profileName}`);
141
- return runSubagentProfile(profile, session, prompt, providerRunner, signal);
142
- }
143
- async function runSubagentProfile(profile, session, prompt, providerRunner, signal) {
144
- const body = profile.body.trim();
145
- const firstPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
146
116
  return providerRunner(session.provider, {
147
- prompt: firstPrompt,
117
+ prompt,
148
118
  workspace: session.workspaceRoot,
119
+ ...(session.providerSessionId ? { providerSessionId: session.providerSessionId } : {}),
149
120
  writeMode: "allowed",
150
121
  model: session.model,
151
122
  thinking: session.thinking,
@@ -3,6 +3,7 @@ import { formatAvailableSubagentTargets, resolveSubagentTarget, } from "../cli-t
3
3
  import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
4
4
  import { assertSubagentProviderAvailable } from "../providers/availability.js";
5
5
  import { subagentProviderContinuationSupported } from "../providers/continuation.js";
6
+ import { compatibilityAllowProjectExecutionTrustPolicy, } from "../../runtime/security/project-execution-trust.js";
6
7
  import { createSubagentSessionStore, } from "./store.js";
7
8
  export class SubagentSessionError extends Error {
8
9
  code;
@@ -15,10 +16,12 @@ export class SubagentSessionError extends Error {
15
16
  export class SubagentSessionManager {
16
17
  config;
17
18
  launcher;
19
+ projectExecutionTrustPolicy;
18
20
  store;
19
- constructor(config, launcher) {
21
+ constructor(config, launcher, projectExecutionTrustPolicy = compatibilityAllowProjectExecutionTrustPolicy) {
20
22
  this.config = config;
21
23
  this.launcher = launcher;
24
+ this.projectExecutionTrustPolicy = projectExecutionTrustPolicy;
22
25
  this.store = createSubagentSessionStore(config);
23
26
  }
24
27
  list(scope = {}) {
@@ -33,6 +36,9 @@ export class SubagentSessionManager {
33
36
  if (!target) {
34
37
  throw new Error(`Unknown subagent profile or provider: ${input.target}. Available ${formatAvailableSubagentTargets(profiles)}`);
35
38
  }
39
+ if (target.kind === "profile" && target.profile.executionRequirement) {
40
+ await this.projectExecutionTrustPolicy.authorize(target.profile.executionRequirement);
41
+ }
36
42
  assertSubagentProviderAvailable(target.provider);
37
43
  const runId = newRunId();
38
44
  const startedAt = new Date().toISOString();
@@ -56,7 +62,7 @@ export class SubagentSessionManager {
56
62
  sessionId: session.id,
57
63
  runId,
58
64
  ...(input.activityId ? { activityId: input.activityId } : {}),
59
- prompt: input.prompt,
65
+ prompt: subagentLaunchPrompt(target, input.prompt),
60
66
  }));
61
67
  return {
62
68
  session: owned,
@@ -146,6 +152,12 @@ export class SubagentSessionManager {
146
152
  return this.store.assignActiveRunOwner(session.id, run.id, owner);
147
153
  }
148
154
  }
155
+ function subagentLaunchPrompt(target, prompt) {
156
+ if (target.kind !== "profile")
157
+ return prompt;
158
+ const body = target.profile.body.trim();
159
+ return body ? `${body}\n\nTask:\n${prompt}` : prompt;
160
+ }
149
161
  function newRunId() {
150
162
  return `run_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
151
163
  }