@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
@@ -0,0 +1,263 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ const MISSING_FINGERPRINT = "missing";
5
+ export class ConfigSourceRuntime {
6
+ units = new Map();
7
+ directories = new Map();
8
+ refreshFile(input) {
9
+ return this.refreshUnit({
10
+ ...input,
11
+ path: resolve(input.path),
12
+ });
13
+ }
14
+ refreshDirectory(input) {
15
+ const directory = resolve(input.directory);
16
+ const previousDirectory = this.directories.get(input.key);
17
+ let names;
18
+ try {
19
+ names = readdirSync(directory, { withFileTypes: true })
20
+ .filter((entry) => entry.isFile() && input.include(entry.name))
21
+ .map((entry) => entry.name)
22
+ .sort((left, right) => left.localeCompare(right));
23
+ }
24
+ catch (error) {
25
+ if (isMissingDirectory(error)) {
26
+ this.clearDeletedDirectoryUnits(input.key, previousDirectory, directory, new Set());
27
+ this.directories.set(input.key, {
28
+ names: new Set(),
29
+ observedFingerprint: MISSING_FINGERPRINT,
30
+ });
31
+ return {
32
+ directory,
33
+ state: "missing",
34
+ observedFingerprint: MISSING_FINGERPRINT,
35
+ effectiveFingerprint: fingerprintSourceUnits([]),
36
+ units: [],
37
+ diagnosticChanged: false,
38
+ };
39
+ }
40
+ const issue = {
41
+ code: "invalid_source",
42
+ message: "Configuration directory could not be read.",
43
+ };
44
+ const issueFingerprint = `directory-read-error:${errorCode(error)}`;
45
+ const diagnosticChanged = previousDirectory?.issueFingerprint !== issueFingerprint;
46
+ const units = this.previousDirectoryUnits(input.key, previousDirectory, directory, issue, issueFingerprint);
47
+ const observedFingerprint = fingerprintSourceUnits(units.map((unit) => ({ name: unit.name, fingerprint: unit.status.observedFingerprint })));
48
+ const effectiveFingerprint = fingerprintSourceUnits(units.flatMap((unit) => unit.status.effectiveFingerprint
49
+ ? [{ name: unit.name, fingerprint: unit.status.effectiveFingerprint }]
50
+ : []));
51
+ this.directories.set(input.key, {
52
+ names: new Set(units.map((unit) => unit.name)),
53
+ observedFingerprint,
54
+ issueFingerprint,
55
+ });
56
+ return {
57
+ directory,
58
+ state: "invalid",
59
+ observedFingerprint,
60
+ effectiveFingerprint,
61
+ units,
62
+ issue,
63
+ diagnosticChanged,
64
+ };
65
+ }
66
+ const currentNames = new Set(names);
67
+ this.clearDeletedDirectoryUnits(input.key, previousDirectory, directory, currentNames);
68
+ const units = names.map((name) => {
69
+ const path = join(directory, name);
70
+ const result = this.refreshUnit({
71
+ key: directoryUnitKey(input.key, name),
72
+ path,
73
+ parse: (content) => input.parse(name, content, path),
74
+ validate: input.validate ? (value) => input.validate(value, name, path) : undefined,
75
+ parseIssue: input.parseIssue
76
+ ? (error) => resolveDirectoryIssue(input.parseIssue, error, name, path, "Configuration source is invalid.")
77
+ : undefined,
78
+ readIssue: input.readIssue
79
+ ? (error) => resolveDirectoryIssue(input.readIssue, error, name, path, "Configuration source could not be read.")
80
+ : undefined,
81
+ });
82
+ return { name, ...result };
83
+ });
84
+ const observedFingerprint = fingerprintSourceUnits(units.map((unit) => ({ name: unit.name, fingerprint: unit.status.observedFingerprint })));
85
+ const effectiveFingerprint = fingerprintSourceUnits(units.flatMap((unit) => unit.status.effectiveFingerprint
86
+ ? [{ name: unit.name, fingerprint: unit.status.effectiveFingerprint }]
87
+ : []));
88
+ this.directories.set(input.key, {
89
+ names: currentNames,
90
+ observedFingerprint,
91
+ });
92
+ return {
93
+ directory,
94
+ state: units.some((unit) => unit.status.state === "invalid") ? "invalid" : "valid",
95
+ observedFingerprint,
96
+ effectiveFingerprint,
97
+ units,
98
+ diagnosticChanged: units.some((unit) => unit.status.diagnosticChanged),
99
+ };
100
+ }
101
+ refreshUnit(input) {
102
+ const path = resolve(input.path);
103
+ const previous = this.units.get(input.key);
104
+ let content;
105
+ try {
106
+ content = readFileSync(path, "utf8");
107
+ }
108
+ catch (error) {
109
+ if (isMissingFile(error)) {
110
+ const changed = previous?.observedFingerprint !== MISSING_FINGERPRINT || previous?.state !== "missing";
111
+ const missing = {
112
+ observedFingerprint: MISSING_FINGERPRINT,
113
+ state: "missing",
114
+ };
115
+ this.units.set(input.key, missing);
116
+ return {
117
+ status: {
118
+ key: input.key,
119
+ path,
120
+ state: "missing",
121
+ observedFingerprint: MISSING_FINGERPRINT,
122
+ usingLastKnownGood: false,
123
+ changed,
124
+ diagnosticChanged: false,
125
+ },
126
+ };
127
+ }
128
+ const fingerprint = `read-error:${errorCode(error)}`;
129
+ const issue = resolveIssue(input.readIssue, error, "Configuration source could not be read.");
130
+ return this.invalidUnit(input.key, path, fingerprint, previous, issue);
131
+ }
132
+ const fingerprint = contentFingerprint(content);
133
+ if (previous?.observedFingerprint === fingerprint) {
134
+ return storedResult(input.key, path, previous, false, false);
135
+ }
136
+ let value;
137
+ try {
138
+ value = input.parse(content);
139
+ }
140
+ catch (error) {
141
+ const issue = resolveIssue(input.parseIssue, error, "Configuration source is invalid.");
142
+ return this.invalidUnit(input.key, path, fingerprint, previous, issue);
143
+ }
144
+ const validationIssue = input.validate?.(value);
145
+ if (validationIssue) {
146
+ return this.invalidUnit(input.key, path, fingerprint, previous, validationIssue);
147
+ }
148
+ const valid = {
149
+ observedFingerprint: fingerprint,
150
+ effectiveFingerprint: fingerprint,
151
+ state: "valid",
152
+ value,
153
+ };
154
+ this.units.set(input.key, valid);
155
+ return storedResult(input.key, path, valid, true, false);
156
+ }
157
+ invalidUnit(key, path, observedFingerprint, previous, issue) {
158
+ if (previous?.observedFingerprint === observedFingerprint && previous.state === "invalid") {
159
+ return storedResult(key, path, previous, false, false);
160
+ }
161
+ const keepLastKnownGood = previous?.effectiveFingerprint !== undefined && previous.value !== undefined;
162
+ const invalid = {
163
+ observedFingerprint,
164
+ ...(keepLastKnownGood ? {
165
+ effectiveFingerprint: previous.effectiveFingerprint,
166
+ value: previous.value,
167
+ } : {}),
168
+ state: "invalid",
169
+ issue,
170
+ };
171
+ this.units.set(key, invalid);
172
+ return storedResult(key, path, invalid, true, true);
173
+ }
174
+ clearDeletedDirectoryUnits(directoryKey, previous, directory, currentNames) {
175
+ if (!previous)
176
+ return;
177
+ for (const name of previous.names) {
178
+ if (currentNames.has(name))
179
+ continue;
180
+ const key = directoryUnitKey(directoryKey, name);
181
+ this.units.set(key, {
182
+ observedFingerprint: MISSING_FINGERPRINT,
183
+ state: "missing",
184
+ });
185
+ }
186
+ }
187
+ previousDirectoryUnits(directoryKey, previous, directory, issue, observedFingerprint) {
188
+ if (!previous)
189
+ return [];
190
+ return [...previous.names].sort((left, right) => left.localeCompare(right)).map((name) => {
191
+ const key = directoryUnitKey(directoryKey, name);
192
+ const prior = this.units.get(key);
193
+ const invalid = {
194
+ observedFingerprint,
195
+ ...(prior?.effectiveFingerprint && prior.value !== undefined ? {
196
+ effectiveFingerprint: prior.effectiveFingerprint,
197
+ value: prior.value,
198
+ } : {}),
199
+ state: "invalid",
200
+ issue,
201
+ };
202
+ const diagnosticChanged = prior?.observedFingerprint !== observedFingerprint || prior.state !== "invalid";
203
+ this.units.set(key, invalid);
204
+ return {
205
+ name,
206
+ ...storedResult(key, join(directory, name), invalid, diagnosticChanged, diagnosticChanged),
207
+ };
208
+ });
209
+ }
210
+ }
211
+ export function fingerprintSourceUnits(units) {
212
+ const sorted = [...units]
213
+ .sort((left, right) => left.name.localeCompare(right.name))
214
+ .map(({ name, fingerprint }) => [name, fingerprint]);
215
+ return createHash("sha256").update(JSON.stringify(sorted)).digest("base64url");
216
+ }
217
+ function contentFingerprint(content) {
218
+ return createHash("sha256").update(content).digest("base64url");
219
+ }
220
+ function directoryUnitKey(directoryKey, name) {
221
+ return `${directoryKey}\0${name}`;
222
+ }
223
+ function storedResult(key, path, stored, changed, diagnosticChanged) {
224
+ return {
225
+ ...(stored.value !== undefined ? { value: stored.value } : {}),
226
+ status: {
227
+ key,
228
+ path,
229
+ state: stored.state,
230
+ observedFingerprint: stored.observedFingerprint,
231
+ ...(stored.effectiveFingerprint ? { effectiveFingerprint: stored.effectiveFingerprint } : {}),
232
+ usingLastKnownGood: stored.state === "invalid" && stored.effectiveFingerprint !== undefined && stored.value !== undefined,
233
+ changed,
234
+ diagnosticChanged,
235
+ },
236
+ ...(stored.issue ? { issue: stored.issue } : {}),
237
+ };
238
+ }
239
+ function resolveIssue(configured, error, fallbackMessage) {
240
+ if (typeof configured === "function")
241
+ return configured(error);
242
+ return configured ?? { code: "invalid_source", message: fallbackMessage };
243
+ }
244
+ function resolveDirectoryIssue(configured, error, name, path, fallbackMessage) {
245
+ if (typeof configured === "function")
246
+ return configured(error, name, path);
247
+ return configured ?? { code: "invalid_source", message: fallbackMessage };
248
+ }
249
+ function isMissingFile(error) {
250
+ return errorCode(error) === "ENOENT";
251
+ }
252
+ function isMissingDirectory(error) {
253
+ const code = errorCode(error);
254
+ return code === "ENOENT" || code === "ENOTDIR";
255
+ }
256
+ function errorCode(error) {
257
+ if (typeof error === "object" && error !== null && "code" in error) {
258
+ const code = error.code;
259
+ if (typeof code === "string" && code)
260
+ return code;
261
+ }
262
+ return "UNKNOWN";
263
+ }
@@ -1,10 +1,10 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { rename as renameAsync, rm as rmAsync, writeFile as writeFileAsync, } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import { join, resolve } from "node:path";
5
6
  import { withFileLock } from "../state/lock/file-lock.js";
6
7
  import { expandHomePath } from "../../mcp/filesystem/roots.js";
7
- import { mergeHookConfigs, parseHookFile, } from "../../mcp/hooks/hooks.js";
8
8
  export function forgerelayConfigDir(env = process.env) {
9
9
  const explicit = env.FORGERELAY_CONFIG_DIR;
10
10
  if (explicit)
@@ -26,10 +26,7 @@ export function forgerelayHooksDir(env = process.env) {
26
26
  export function forgerelaySkillsDir(env = process.env) {
27
27
  return join(forgerelayConfigDir(env), "skills");
28
28
  }
29
- export function forgerelayAgentsDir(env = process.env) {
30
- return join(forgerelayConfigDir(env), "agents");
31
- }
32
- export function loadForgeRelayFiles(env = process.env) {
29
+ export function loadForgeRelayFiles(env = process.env, options = {}) {
33
30
  const dir = forgerelayConfigDir(env);
34
31
  const configPath = join(dir, "config.json");
35
32
  const authPath = join(dir, "auth.json");
@@ -47,8 +44,7 @@ export function loadForgeRelayFiles(env = process.env) {
47
44
  hooksExists,
48
45
  config: configExists ? readJsonFile(configPath) : {},
49
46
  auth: authExists ? readJsonFile(authPath) : {},
50
- hooks: hooksExists ? readJsonFile(hooksPath) : {},
51
- hookFiles: readHookFiles(join(dir, "hooks")),
47
+ hooks: options.readLegacyHooks && hooksExists ? readJsonFile(hooksPath) : {},
52
48
  };
53
49
  }
54
50
  export function writeForgeRelayConfig(config, env = process.env) {
@@ -60,8 +56,8 @@ export function writeForgeRelayConfig(config, env = process.env) {
60
56
  export async function writeForgeRelayAuth(auth, env = process.env) {
61
57
  const filePath = forgerelayAuthPath(env);
62
58
  mkdirSync(forgerelayConfigDir(env), { recursive: true });
63
- return withFileLock(`${filePath}.lock`, () => {
64
- writeJsonFile(filePath, auth, 0o600);
59
+ return withFileLock(`${filePath}.lock`, async () => {
60
+ await writeAuthJsonFile(filePath, auth, 0o600);
65
61
  return filePath;
66
62
  });
67
63
  }
@@ -141,25 +137,6 @@ export function resolveSubagentsFlag(config, env = process.env) {
141
137
  return config.subagents;
142
138
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
143
139
  }
144
- function readHookFiles(directory) {
145
- if (!existsSync(directory))
146
- return {};
147
- let hooks = {};
148
- const entries = readdirSync(directory, { withFileTypes: true })
149
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
150
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
151
- for (const entry of entries) {
152
- const filePath = join(directory, entry.name);
153
- try {
154
- hooks = mergeHookConfigs(hooks, parseHookFile(readJsonFile(filePath), entry.name.slice(0, -5)));
155
- }
156
- catch (error) {
157
- const reason = error instanceof Error ? error.message : String(error);
158
- throw new Error(`Unable to load hook file ${filePath}: ${reason}`);
159
- }
160
- }
161
- return hooks;
162
- }
163
140
  function readJsonFile(filePath) {
164
141
  try {
165
142
  return JSON.parse(readFileSync(filePath, "utf8"));
@@ -172,12 +149,42 @@ function readJsonFile(filePath) {
172
149
  async function updateForgeRelayAuth(update, env) {
173
150
  const filePath = forgerelayAuthPath(env);
174
151
  mkdirSync(forgerelayConfigDir(env), { recursive: true });
175
- return withFileLock(`${filePath}.lock`, () => {
152
+ return withFileLock(`${filePath}.lock`, async () => {
176
153
  const auth = existsSync(filePath) ? readJsonFile(filePath) : {};
177
- writeJsonFile(filePath, update(auth), 0o600);
154
+ await writeAuthJsonFile(filePath, update(auth), 0o600);
178
155
  return filePath;
179
156
  });
180
157
  }
158
+ async function writeAuthJsonFile(filePath, value, mode) {
159
+ const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
160
+ try {
161
+ await writeFileAsync(tempPath, JSON.stringify(value, null, 2) + "\n", { mode });
162
+ await replaceAuthFile(tempPath, filePath);
163
+ }
164
+ finally {
165
+ await rmAsync(tempPath, { force: true });
166
+ }
167
+ }
168
+ async function replaceAuthFile(tempPath, filePath) {
169
+ const maxAttempts = process.platform === "win32" ? 40 : 1;
170
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
171
+ try {
172
+ await renameAsync(tempPath, filePath);
173
+ return;
174
+ }
175
+ catch (error) {
176
+ const code = error.code;
177
+ const retryable = process.platform === "win32"
178
+ && ["EACCES", "EBUSY", "EEXIST", "EPERM"].includes(code ?? "");
179
+ if (!retryable || attempt === maxAttempts)
180
+ throw error;
181
+ await delay(25);
182
+ }
183
+ }
184
+ }
185
+ function delay(ms) {
186
+ return new Promise((resolve) => setTimeout(resolve, ms));
187
+ }
181
188
  function writeJsonFile(filePath, value, mode) {
182
189
  const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
183
190
  try {
@@ -0,0 +1,34 @@
1
+ export const compatibilityAllowProjectExecutionTrustPolicy = {
2
+ async authorize(requirement) {
3
+ return {
4
+ policy: "compatibility-allow",
5
+ decision: "allow",
6
+ projectId: requirement.projectId,
7
+ executableConfigFingerprint: requirement.executableConfigFingerprint,
8
+ };
9
+ },
10
+ };
11
+ export function projectExecutionRequirement(input) {
12
+ const entry = input.resolution.entries[input.entryKey];
13
+ if (!entry
14
+ || entry.effective.source.scope !== "project"
15
+ || entry.effective.executionEffect !== "process"
16
+ || entry.tombstone === true) {
17
+ return undefined;
18
+ }
19
+ const executionFingerprint = entry.effective.executionFingerprint;
20
+ if (!executionFingerprint) {
21
+ throw new Error(`Project execution metadata is missing a fingerprint for ${entry.logicalPath}.`);
22
+ }
23
+ return {
24
+ projectId: input.projectId,
25
+ domain: input.resolution.domain,
26
+ logicalPath: entry.logicalPath,
27
+ source: {
28
+ id: entry.effective.source.id,
29
+ ...(entry.effective.source.location ? { location: entry.effective.source.location } : {}),
30
+ },
31
+ display: { ...input.display },
32
+ executableConfigFingerprint: executionFingerprint,
33
+ };
34
+ }
@@ -11,7 +11,6 @@ import { HostTurnStore } from "../../activity/history/host-turn-store.js";
11
11
  import { ActivityLifecycle } from "../../activity/runtime/lifecycle.js";
12
12
  import { ActivityQueryService } from "../../activity/history/query-service.js";
13
13
  import { loadConfig } from "../config/config.js";
14
- import { parseHookConfig } from "../../mcp/hooks/hooks.js";
15
14
  import { CodeIntelligenceManager } from "../../lsp/runtime/manager.js";
16
15
  import { ProcessManager } from "../../mcp/process/process-sessions.js";
17
16
  import { createReviewCheckpointManager } from "../../workspaces/review/review-checkpoints.js";
@@ -30,15 +29,19 @@ export async function fixture(t, options = {}) {
30
29
  const agentDir = join(root, "agent");
31
30
  const stateDir = join(root, ".state");
32
31
  const configDir = join(root, ".config");
33
- await mkdir(join(project, ".forgerelay", "agents"), { recursive: true });
32
+ await mkdir(join(project, ".forgerelay", "subagents"), { recursive: true });
34
33
  await mkdir(configDir, { recursive: true });
35
- if (options.userConfig) {
36
- await writeFile(join(configDir, "config.json"), JSON.stringify(options.userConfig, null, 2) + "\n");
34
+ const userConfig = {
35
+ ...(options.userConfig ?? {}),
36
+ ...(options.hooks ? { hooks: options.hooks } : {}),
37
+ };
38
+ if (Object.keys(userConfig).length > 0) {
39
+ await writeFile(join(configDir, "config.json"), JSON.stringify(userConfig, null, 2) + "\n");
37
40
  }
38
41
  await mkdir(agentDir, { recursive: true });
39
42
  await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
40
43
  await writeFile(join(project, "AGENTS.md"), "project instructions\n");
41
- await writeFile(join(project, ".forgerelay", "agents", "reviewer.md"), [
44
+ await writeFile(join(project, ".forgerelay", "subagents", "reviewer.md"), [
42
45
  "---",
43
46
  "name: reviewer",
44
47
  "description: Reviews project changes.",
@@ -66,9 +69,7 @@ export async function fixture(t, options = {}) {
66
69
  PORT: "1",
67
70
  ...options.env,
68
71
  });
69
- const config = options.hooks
70
- ? { ...loadedConfig, hooks: parseHookConfig(options.hooks) }
71
- : loadedConfig;
72
+ const config = loadedConfig;
72
73
  const store = new SqliteWorkspaceStore(stateDir);
73
74
  const workspaces = new WorkspaceRegistry(config, store);
74
75
  const auditStore = new ActivityAuditStore(stateDir);
package/dist/server.js CHANGED
@@ -9,6 +9,7 @@ import { CapabilityError, createCapabilityRegistry } from "./mcp/server/core/cap
9
9
  import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform } from "./mcp/artifacts/artifact-tools.js";
10
10
  import { ArtifactError } from "./mcp/artifacts/artifact-error.js";
11
11
  import { loadConfig } from "./runtime/config/config.js";
12
+ import { liveWorkspaceConfigPanelState } from "./runtime/config/inspection/live.js";
12
13
  import { CodeIntelligenceError } from "./lsp/code-intelligence.js";
13
14
  import { installManagedLanguageServers, installedManagedLanguageServers, supportedManagedLanguageServers, } from "./lsp/runtime/managed-language-servers.js";
14
15
  import { HookRunner } from "./mcp/hooks/hooks.js";
@@ -130,7 +131,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
130
131
  };
131
132
  };
132
133
  const toolDescriptions = buildToolDescriptions(config);
133
- const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)), config.commandShellRuntime);
134
+ const hooks = new HookRunner({}, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)), config.commandShellRuntime, config.configDir, config.configRuntime.sources);
134
135
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
135
136
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
136
137
  const reviewChangesAvailable = config.widgets === "changes";
@@ -138,7 +139,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
138
139
  let batchExecutor;
139
140
  const batchExecuteAvailable = config.toolMode !== "codex";
140
141
  const capabilityRegistry = createCapabilityRegistry({
141
- inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
142
+ inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, {}, config.configDir, config.configRuntime.sources),
142
143
  ...subagentMcp.registryDependencies,
143
144
  externalMcp: externalMcpCapability,
144
145
  workspaceRecovery: {
@@ -215,7 +216,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
215
216
  }
216
217
  if (input.operation === "managed.install") {
217
218
  if (!config.allowAgentLanguageServerInstall) {
218
- throw new CapabilityError("code.managed_install_disabled", "Agent-managed Language Server installation is disabled. Enable it explicitly with forgerelay init --force.");
219
+ throw new CapabilityError("code.managed_install_disabled", "Agent-managed Language Server installation is disabled. Enable it explicitly with forgerelay init --advanced.");
219
220
  }
220
221
  const install = options.managedLanguageServerInstaller ?? installManagedLanguageServers;
221
222
  const installed = await install(input.servers, config.configDir);
@@ -439,14 +440,17 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
439
440
  },
440
441
  });
441
442
  };
442
- const workspacePanelState = (workspaceId) => {
443
+ const workspacePanelState = async (workspaceId) => {
443
444
  const remembered = workspacePanelStates.get(workspaceId);
444
445
  if (remoteWorkspaces.has(workspaceId) || compositeWorkspaces.has(workspaceId)) {
445
446
  return remembered;
446
447
  }
447
448
  try {
448
- const live = liveWorkspacePanelState(workspaces.getWorkspace(workspaceId));
449
- return remembered ? { ...live, ...remembered } : live;
449
+ const workspace = workspaces.getWorkspace(workspaceId);
450
+ const live = liveWorkspacePanelState(workspace);
451
+ const presentation = remembered ? { ...live, ...remembered } : live;
452
+ const configuration = await liveWorkspaceConfigPanelState(config, workspace.root);
453
+ return configuration ? { ...presentation, configuration } : presentation;
450
454
  }
451
455
  catch {
452
456
  return undefined;