@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,66 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ export function resolveAcceptancePrefix(repoRoot) {
6
+ const configuredPrefix = process.env.FORGERELAY_ACCEPTANCE_PREFIX?.trim();
7
+ return configuredPrefix ? resolve(repoRoot, configuredPrefix) : undefined;
8
+ }
9
+
10
+ export function resolveAcceptanceTarball({ repoRoot, artifactDir, npmCli }) {
11
+ const configuredTarball = process.env.FORGERELAY_ACCEPTANCE_TARBALL?.trim();
12
+ const configuredDir = process.env.FORGERELAY_ACCEPTANCE_ARTIFACT_DIR?.trim();
13
+ if (configuredTarball && configuredDir) {
14
+ throw new Error("Set only one of FORGERELAY_ACCEPTANCE_TARBALL or FORGERELAY_ACCEPTANCE_ARTIFACT_DIR.");
15
+ }
16
+
17
+ if (configuredTarball) {
18
+ const tarball = resolve(repoRoot, configuredTarball);
19
+ assertTarball(tarball, "configured acceptance tarball");
20
+ console.log(`Using prebuilt npm artifact: ${tarball}`);
21
+ return tarball;
22
+ }
23
+
24
+ if (configuredDir) {
25
+ const directory = resolve(repoRoot, configuredDir);
26
+ const packages = existsSync(directory)
27
+ ? readdirSync(directory).filter((name) => name.endsWith(".tgz"))
28
+ : [];
29
+ if (packages.length !== 1) {
30
+ throw new Error(
31
+ `Acceptance artifact directory must contain exactly one .tgz package; found ${packages.length}: ${directory}`,
32
+ );
33
+ }
34
+ const tarball = resolve(directory, packages[0]);
35
+ assertTarball(tarball, "downloaded acceptance tarball");
36
+ console.log(`Using prebuilt npm artifact: ${tarball}`);
37
+ return tarball;
38
+ }
39
+
40
+ const result = spawnSync(
41
+ process.execPath,
42
+ [npmCli, "pack", "--json", "--pack-destination", artifactDir],
43
+ {
44
+ cwd: repoRoot,
45
+ env: process.env,
46
+ encoding: "utf8",
47
+ windowsHide: true,
48
+ shell: false,
49
+ },
50
+ );
51
+ if (result.error || result.status !== 0) {
52
+ throw new Error(`npm pack failed: ${result.error?.message ?? result.stderr ?? result.stdout ?? result.status}`);
53
+ }
54
+ const report = JSON.parse(result.stdout);
55
+ const filename = report?.[0]?.filename;
56
+ if (!filename) throw new Error(`npm pack did not report a package filename: ${result.stdout}`);
57
+ const tarball = resolve(artifactDir, filename);
58
+ assertTarball(tarball, "locally packed acceptance tarball");
59
+ return tarball;
60
+ }
61
+
62
+ function assertTarball(tarball, label) {
63
+ if (!tarball.endsWith(".tgz") || !existsSync(tarball)) {
64
+ throw new Error(`${label} is missing or is not an npm .tgz artifact: ${tarball}`);
65
+ }
66
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ const npmCli = process.env.npm_execpath;
6
+ if (!npmCli) throw new Error("ci:contract must run through npm so npm_execpath is available");
7
+
8
+ console.log(`Core CI contract: ${process.platform}/${process.arch} ${process.version}`);
9
+ runNpm(["--version"], "npm version");
10
+ runNpm(["run", "config:schema:check"], "Config schemas");
11
+ runNpm(["run", "architecture:check"], "Architecture");
12
+ runNpm(["run", "release:check"], "Release metadata");
13
+ runNpm(["run", "typecheck"], "Typecheck");
14
+ console.log("Core CI contract passed.");
15
+
16
+ function runNpm(args, label) {
17
+ console.log(`\n== ${label} ==`);
18
+ const result = spawnSync(process.execPath, [npmCli, ...args], {
19
+ cwd: process.cwd(),
20
+ env: process.env,
21
+ stdio: "inherit",
22
+ windowsHide: true,
23
+ shell: false,
24
+ });
25
+ if (result.error) throw result.error;
26
+ if (result.status !== 0) throw new Error(`${label} failed with exit ${result.status ?? "unknown"}`);
27
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ const npmCli = process.env.npm_execpath;
6
+ if (!npmCli) throw new Error("ci:platform-tests must run through npm so npm_execpath is available");
7
+
8
+ const tests = [
9
+ "src/cli/mcp/diagnostics.test.ts",
10
+ "src/runtime/shell/command-shell-runtime.test.ts",
11
+ "src/runtime/state/lock/file-lock.test.ts",
12
+ "src/runtime/state/runtime-lease.test.ts",
13
+ "src/runtime/state/db/migrations.test.ts",
14
+ "src/mcp/process/process-platform.test.ts",
15
+ "src/mcp/process/process-sessions.test.ts",
16
+ "src/mcp/filesystem/roots.test.ts",
17
+ "src/mcp/filesystem/file-mutations.test.ts",
18
+ "src/workspaces/state/workspace-store.test.ts",
19
+ "src/workspaces.test.ts",
20
+ "src/workspaces/conversation-checkout.test.ts",
21
+ "src/workspaces/conversation-worktree.test.ts",
22
+ "src/workspaces/git/worktree-recovery.test.ts",
23
+ ];
24
+
25
+ console.log(`Platform-sensitive CI tests: ${process.platform}/${process.arch} (${tests.length} files).`);
26
+ for (const path of tests) {
27
+ console.log(`\n== ${path} ==`);
28
+ const result = spawnSync(process.execPath, [npmCli, "exec", "--", "tsx", path], {
29
+ cwd: process.cwd(),
30
+ env: process.env,
31
+ stdio: "inherit",
32
+ windowsHide: true,
33
+ shell: false,
34
+ });
35
+ if (result.error) throw result.error;
36
+ if (result.status !== 0) throw new Error(`Platform-sensitive test failed with exit ${result.status ?? "unknown"}: ${path}`);
37
+ }
38
+ console.log("Platform-sensitive CI tests passed.");
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, rmSync } from "node:fs";
6
+ import { mkdir } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { resolveAcceptancePrefix, resolveAcceptanceTarball } from "./acceptance-artifact.mjs";
9
+
10
+ if (process.platform !== "win32") {
11
+ console.log("Prepared Windows product install skipped outside Windows.");
12
+ process.exit(0);
13
+ }
14
+
15
+ const npmCli = process.env.npm_execpath;
16
+ if (!npmCli) throw new Error("Windows product preparation must run through npm so npm_execpath is available");
17
+
18
+ const repoRoot = process.cwd();
19
+ const prefix = resolveAcceptancePrefix(repoRoot);
20
+ if (!prefix) {
21
+ throw new Error("FORGERELAY_ACCEPTANCE_PREFIX is required for the shared Windows product install.");
22
+ }
23
+
24
+ rmSync(prefix, { recursive: true, force: true });
25
+ await mkdir(prefix, { recursive: true });
26
+ const artifactDir = join(prefix, ".artifact-fallback");
27
+ await mkdir(artifactDir, { recursive: true });
28
+ const tarball = resolveAcceptanceTarball({ repoRoot, artifactDir, npmCli });
29
+
30
+ const install = spawnSync(
31
+ process.execPath,
32
+ [npmCli, "install", "--global", "--prefix", prefix, "--no-audit", "--no-fund", tarball],
33
+ {
34
+ cwd: repoRoot,
35
+ env: process.env,
36
+ encoding: "utf8",
37
+ windowsHide: true,
38
+ shell: false,
39
+ timeout: 240_000,
40
+ },
41
+ );
42
+ if (install.error || install.status !== 0) {
43
+ throw new Error(
44
+ `Shared Windows product install failed: ${install.error?.message ?? install.stderr ?? install.stdout ?? install.status}`,
45
+ );
46
+ }
47
+
48
+ const installedRoot = join(prefix, "node_modules", "@akira-tl", "forgerelay");
49
+ for (const expected of [
50
+ join(installedRoot, "dist", "cli.js"),
51
+ join(prefix, "forgerelay.cmd"),
52
+ join(prefix, "forgerelay.ps1"),
53
+ ]) {
54
+ assert.ok(existsSync(expected), `Prepared Windows product install is missing: ${expected}`);
55
+ }
56
+
57
+ console.log(`Prepared shared Windows product install from ${tarball} at ${prefix}.`);
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { resolve } from "node:path";
6
+
7
+ const npmCli = process.env.npm_execpath;
8
+ if (!npmCli) throw new Error("ci:test-shard must run through npm so npm_execpath is available");
9
+
10
+ const shard = process.argv[2];
11
+ const allowed = new Set(["runtime-config", "workspace-mcp", "lsp", "subagent-ui-cli"]);
12
+ if (!allowed.has(shard)) {
13
+ throw new Error(`Unknown CI test shard '${shard ?? ""}'. Expected one of: ${[...allowed].join(", ")}`);
14
+ }
15
+
16
+ const repoRoot = process.cwd();
17
+ const pkg = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
18
+ const commands = String(pkg.scripts?.test ?? "").split(" && ").filter(Boolean);
19
+ if (commands.length === 0) throw new Error("package.json test script did not contain any commands");
20
+
21
+ const selected = commands.filter((command) => classify(command) === shard);
22
+ if (selected.length === 0) throw new Error(`CI test shard '${shard}' is empty`);
23
+
24
+ console.log(`CI test shard ${shard}: ${selected.length}/${commands.length} test commands.`);
25
+ for (const command of selected) runCommand(command);
26
+ console.log(`CI test shard ${shard} passed.`);
27
+
28
+ function classify(command) {
29
+ if (command === "npm run build:app") return "workspace-mcp";
30
+ if (command.includes("src/lsp/")) return "lsp";
31
+ if (
32
+ command.includes("src/workspaces/")
33
+ || command.includes("src/workspaces.test")
34
+ || command.includes("src/mcp/")
35
+ || command.includes("src/activity/")
36
+ ) return "workspace-mcp";
37
+ if (command.includes("src/runtime/") || command.startsWith("node --test scripts/")) {
38
+ return "runtime-config";
39
+ }
40
+ return "subagent-ui-cli";
41
+ }
42
+
43
+ function runCommand(command) {
44
+ console.log(`\n== ${command} ==`);
45
+ let executable;
46
+ let args;
47
+ if (command.startsWith("node --test ")) {
48
+ executable = process.execPath;
49
+ args = ["--test", ...command.slice("node --test ".length).split(" ").filter(Boolean)];
50
+ } else if (command.startsWith("tsx ")) {
51
+ executable = process.execPath;
52
+ args = [npmCli, "exec", "--", "tsx", ...command.slice(4).split(" ").filter(Boolean)];
53
+ } else if (command.startsWith("npm run ")) {
54
+ executable = process.execPath;
55
+ args = [npmCli, "run", command.slice("npm run ".length).trim()];
56
+ } else {
57
+ throw new Error(`Unsupported command in package.json test script: ${command}`);
58
+ }
59
+
60
+ const result = spawnSync(executable, args, {
61
+ cwd: repoRoot,
62
+ env: process.env,
63
+ stdio: "inherit",
64
+ windowsHide: true,
65
+ shell: false,
66
+ });
67
+ if (result.error) throw result.error;
68
+ if (result.status !== 0) {
69
+ throw new Error(`CI test shard '${shard}' command failed with exit ${result.status ?? "unknown"}: ${command}`);
70
+ }
71
+ }
@@ -6,6 +6,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
+ import { resolveAcceptancePrefix, resolveAcceptanceTarball } from "./gates/acceptance-artifact.mjs";
9
10
 
10
11
  if (process.platform !== "win32") {
11
12
  console.log("Windows PowerShell 5.1 packaged acceptance skipped outside Windows.");
@@ -362,7 +363,8 @@ async function exerciseHookRuntime(runtime) {
362
363
 
363
364
  async function exercisePackagedPowerShellShim(powershell, expectedVersion) {
364
365
  const artifactDir = join(root, "artifact");
365
- const prefix = join(root, "prefix");
366
+ const sharedPrefix = resolveAcceptancePrefix(process.cwd());
367
+ const prefix = sharedPrefix ?? join(root, "prefix");
366
368
  const configDir = join(root, "config");
367
369
  const stateDir = join(root, "state");
368
370
  await Promise.all([
@@ -372,14 +374,12 @@ async function exercisePackagedPowerShellShim(powershell, expectedVersion) {
372
374
  mkdir(stateDir, { recursive: true }),
373
375
  ]);
374
376
 
375
- const packed = runNpm(["pack", "--json", "--pack-destination", artifactDir]);
376
- const packResult = JSON.parse(packed.stdout);
377
- const filename = packResult?.[0]?.filename;
378
- if (!filename) throw new Error(`npm pack did not report a package filename: ${packed.stdout}`);
379
- const tarball = join(artifactDir, filename);
380
- assert.ok(existsSync(tarball), `packed artifact is missing: ${tarball}`);
381
-
382
- runNpm(["install", "--global", "--prefix", prefix, tarball]);
377
+ if (!sharedPrefix) {
378
+ const tarball = resolveAcceptanceTarball({ repoRoot: process.cwd(), artifactDir, npmCli });
379
+ runNpm(["install", "--global", "--prefix", prefix, tarball]);
380
+ } else {
381
+ console.log(`Using prepared acceptance install: ${prefix}`);
382
+ }
383
383
  const shim = join(prefix, "forgerelay.ps1");
384
384
  assert.ok(existsSync(shim), `npm did not create the PowerShell launcher shim: ${shim}`);
385
385
 
@@ -6,6 +6,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
+ import { resolveAcceptancePrefix, resolveAcceptanceTarball } from "./gates/acceptance-artifact.mjs";
9
10
 
10
11
  if (process.platform !== "win32") {
11
12
  console.log("PowerShell 7 packaged acceptance skipped outside Windows.");
@@ -338,7 +339,8 @@ async function exerciseHookRuntime(runtime) {
338
339
 
339
340
  async function exercisePackagedPowerShellShim(pwsh, expectedVersion) {
340
341
  const artifactDir = join(root, "artifact");
341
- const prefix = join(root, "prefix");
342
+ const sharedPrefix = resolveAcceptancePrefix(process.cwd());
343
+ const prefix = sharedPrefix ?? join(root, "prefix");
342
344
  const configDir = join(root, "config");
343
345
  const stateDir = join(root, "state");
344
346
  await Promise.all([
@@ -348,14 +350,12 @@ async function exercisePackagedPowerShellShim(pwsh, expectedVersion) {
348
350
  mkdir(stateDir, { recursive: true }),
349
351
  ]);
350
352
 
351
- const packed = runNpm(["pack", "--json", "--pack-destination", artifactDir]);
352
- const packResult = JSON.parse(packed.stdout);
353
- const filename = packResult?.[0]?.filename;
354
- if (!filename) throw new Error(`npm pack did not report a package filename: ${packed.stdout}`);
355
- const tarball = join(artifactDir, filename);
356
- assert.ok(existsSync(tarball), `packed artifact is missing: ${tarball}`);
357
-
358
- runNpm(["install", "--global", "--prefix", prefix, tarball]);
353
+ if (!sharedPrefix) {
354
+ const tarball = resolveAcceptanceTarball({ repoRoot: process.cwd(), artifactDir, npmCli });
355
+ runNpm(["install", "--global", "--prefix", prefix, tarball]);
356
+ } else {
357
+ console.log(`Using prepared acceptance install: ${prefix}`);
358
+ }
359
359
  const shim = join(prefix, "forgerelay.ps1");
360
360
  assert.ok(existsSync(shim), `npm did not create the PowerShell launcher shim: ${shim}`);
361
361
 
@@ -9,11 +9,13 @@ if (!npmCli) {
9
9
 
10
10
  console.log(`CI environment: ${process.platform}/${process.arch} ${process.version}`);
11
11
  runNpm(["--version"], "npm version");
12
+ runNpm(["run", "config:schema:check"], "Config schemas");
12
13
  runNpm(["run", "architecture:check"], "Architecture");
13
14
  runNpm(["run", "release:check"], "Release metadata");
14
15
  runNpm(["run", "typecheck"], "Typecheck");
15
16
  runNpm(["test"], "Full test suite");
16
17
  runNpm(["run", "build"], "Build");
18
+ runNpm(["run", "config:product-accept"], "Config v2 packaged product acceptance");
17
19
  if (process.platform === "win32") runNpm(["run", "pwsh:accept"], "PowerShell 7 packaged acceptance");
18
20
  // The traffic audit is an end-to-end wire-budget check, not a platform feature
19
21
  // matrix. Run it once on Linux so regressions are gated without tripling the
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { spawnSync } from "node:child_process";
10
+ import { resolveAcceptancePrefix, resolveAcceptanceTarball } from "./gates/acceptance-artifact.mjs";
10
11
 
11
12
  if (process.platform !== "win32") {
12
13
  console.log("Windows packaged product acceptance skipped outside Windows.");
@@ -22,7 +23,8 @@ const root = await import("node:fs/promises").then(({ mkdtemp }) =>
22
23
 
23
24
  try {
24
25
  const artifactDir = join(root, "artifact");
25
- const prefix = join(root, "prefix");
26
+ const sharedPrefix = resolveAcceptancePrefix(process.cwd());
27
+ const prefix = sharedPrefix ?? join(root, "prefix");
26
28
  const projectRoot = join(root, "project");
27
29
  await Promise.all([
28
30
  mkdir(artifactDir, { recursive: true }),
@@ -31,14 +33,12 @@ try {
31
33
  ]);
32
34
  await writeFile(join(projectRoot, "README.md"), "Windows product acceptance workspace\n", "utf8");
33
35
 
34
- const packed = runNpm(["pack", "--json", "--pack-destination", artifactDir]);
35
- const packResult = JSON.parse(packed.stdout);
36
- const filename = packResult?.[0]?.filename;
37
- if (!filename) throw new Error(`npm pack did not report a package filename: ${packed.stdout}`);
38
- const tarball = join(artifactDir, filename);
39
- assert.ok(existsSync(tarball), `packed artifact is missing: ${tarball}`);
40
-
41
- runNpm(["install", "--global", "--prefix", prefix, tarball]);
36
+ if (!sharedPrefix) {
37
+ const tarball = resolveAcceptanceTarball({ repoRoot: process.cwd(), artifactDir, npmCli });
38
+ runNpm(["install", "--global", "--prefix", prefix, tarball]);
39
+ } else {
40
+ console.log(`Using prepared acceptance install: ${prefix}`);
41
+ }
42
42
  const shim = join(prefix, "forgerelay.cmd");
43
43
  assert.ok(existsSync(shim), `npm did not create the Windows launcher shim: ${shim}`);
44
44
 
@@ -105,7 +105,6 @@ async function exercisePackagedRuntime({ family, executable, doctorIdentity, roo
105
105
  executable,
106
106
  },
107
107
  shellInstructions: true,
108
- skillsEnabled: false,
109
108
  }, null, 2),
110
109
  "utf8",
111
110
  );
@@ -0,0 +1,4 @@
1
+ export function generatedSchemaTextMatches(actual, expected) {
2
+ if (typeof actual !== "string") return false;
3
+ return actual.replaceAll("\r\n", "\n") === expected;
4
+ }
@@ -0,0 +1,18 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { generatedSchemaTextMatches } from "./schema-text.mjs";
4
+
5
+ test("generated schema freshness accepts CRLF checkout line endings", () => {
6
+ const expected = "{\n \"title\": \"Config\"\n}\n";
7
+ const windowsCheckout = expected.replaceAll("\n", "\r\n");
8
+
9
+ assert.equal(generatedSchemaTextMatches(windowsCheckout, expected), true);
10
+ });
11
+
12
+ test("generated schema freshness still rejects semantic content changes", () => {
13
+ const expected = "{\n \"title\": \"Config\"\n}\n";
14
+ const changed = "{\r\n \"title\": \"Changed\"\r\n}\r\n";
15
+
16
+ assert.equal(generatedSchemaTextMatches(changed, expected), false);
17
+ assert.equal(generatedSchemaTextMatches(undefined, expected), false);
18
+ });
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CONFIG_DEFINITION_CATALOG } from "../../src/runtime/config/definition/catalog.ts";
7
+ import { generateConfigSchemaFiles } from "../../src/runtime/config/definition/schema.ts";
8
+ import { generatedSchemaTextMatches } from "./schema-text.mjs";
9
+
10
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
11
+ const command = process.argv[2] ?? "check";
12
+ const generated = generateConfigSchemaFiles(CONFIG_DEFINITION_CATALOG)
13
+ .map((entry) => ({
14
+ ...entry,
15
+ content: `${JSON.stringify(entry.schema, null, 2)}\n`,
16
+ }))
17
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
18
+
19
+ if (command === "write") {
20
+ for (const entry of generated) {
21
+ const path = resolve(repoRoot, entry.relativePath);
22
+ await mkdir(dirname(path), { recursive: true });
23
+ await writeFile(path, entry.content, "utf8");
24
+ }
25
+ await removeStaleCurrentMajorSchemas();
26
+ console.log(`Generated ${generated.length} Config System v1 schema files.`);
27
+ } else if (command === "check") {
28
+ const mismatches = [];
29
+ for (const entry of generated) {
30
+ const path = resolve(repoRoot, entry.relativePath);
31
+ let actual;
32
+ try {
33
+ actual = await readFile(path, "utf8");
34
+ } catch {
35
+ actual = undefined;
36
+ }
37
+ if (!generatedSchemaTextMatches(actual, entry.content)) mismatches.push(entry.relativePath);
38
+ }
39
+ const expected = new Set(generated.map((entry) => entry.relativePath));
40
+ for (const path of await currentMajorSchemaPaths()) {
41
+ if (!expected.has(path)) mismatches.push(path);
42
+ }
43
+ if (mismatches.length > 0) {
44
+ console.error("Generated Config schemas are out of date:");
45
+ for (const path of [...new Set(mismatches)].sort()) console.error(`- ${path}`);
46
+ console.error("Run: npm run config:schema:generate");
47
+ process.exit(1);
48
+ }
49
+ console.log(`Config schema check passed: ${generated.length} generated v1 schema files are current.`);
50
+ } else {
51
+ throw new Error(`Unknown schema command ${JSON.stringify(command)}; expected check or write.`);
52
+ }
53
+
54
+ async function removeStaleCurrentMajorSchemas() {
55
+ const expected = new Set(generated.map((entry) => entry.relativePath));
56
+ const { rm } = await import("node:fs/promises");
57
+ for (const relativePath of await currentMajorSchemaPaths()) {
58
+ if (!expected.has(relativePath)) await rm(resolve(repoRoot, relativePath));
59
+ }
60
+ }
61
+
62
+ async function currentMajorSchemaPaths() {
63
+ const directory = resolve(repoRoot, "schemas/v1");
64
+ let names;
65
+ try {
66
+ names = await readdir(directory);
67
+ } catch {
68
+ return [];
69
+ }
70
+ return names
71
+ .filter((name) => name.endsWith(".schema.json"))
72
+ .map((name) => `schemas/v1/${name}`)
73
+ .sort();
74
+ }
@@ -3,6 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { CodeIntelligenceManager } from "../../dist/lsp/runtime/manager.js";
6
+ import { ConfigRuntime } from "../../dist/runtime/config/runtime/config-runtime.js";
6
7
  import { findExecutable, probeExecutable } from "./interop-support.mjs";
7
8
 
8
9
  const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
@@ -121,8 +122,10 @@ for (const fixture of fixtures) {
121
122
 
122
123
  const root = join(interopRoot, fixture.id);
123
124
  fixture.setup(root);
125
+ const configDir = join(root, ".forgerelay-user");
126
+ mkdirSync(configDir, { recursive: true });
124
127
  const manager = new CodeIntelligenceManager(
125
- { languageServers: {} },
128
+ { configDir, configRuntime: new ConfigRuntime() },
126
129
  {
127
130
  startTimeoutMs: 30_000,
128
131
  requestTimeoutMs: 30_000,
@@ -9,23 +9,27 @@ if (!npmCli) {
9
9
  throw new Error("release:pack must be launched through npm so npm_execpath is available");
10
10
  }
11
11
 
12
- const outputDir = resolve(process.cwd(), process.env.RELEASE_ARTIFACT_DIR ?? ".release-artifacts");
12
+ const repoRoot = process.cwd();
13
+ const outputDir = resolve(repoRoot, process.env.RELEASE_ARTIFACT_DIR ?? ".release-artifacts");
13
14
  rmSync(outputDir, { recursive: true, force: true });
14
15
  mkdirSync(outputDir, { recursive: true });
15
16
 
16
17
  const result = spawnSync(
17
18
  process.execPath,
18
- [npmCli, "pack", "--pack-destination", outputDir],
19
+ [npmCli, "pack", "--json", "--pack-destination", outputDir],
19
20
  {
20
- cwd: process.cwd(),
21
+ cwd: repoRoot,
21
22
  env: process.env,
22
- stdio: "inherit",
23
+ encoding: "utf8",
24
+ stdio: ["ignore", "pipe", "pipe"],
23
25
  windowsHide: true,
24
26
  shell: false,
25
27
  },
26
28
  );
29
+ if (result.stderr) process.stderr.write(result.stderr);
27
30
  if (result.error) throw result.error;
28
31
  if (result.status !== 0) {
32
+ if (result.stdout) process.stdout.write(result.stdout);
29
33
  throw new Error(`npm pack failed with exit ${result.status ?? "unknown"}`);
30
34
  }
31
35
 
@@ -33,4 +37,60 @@ const packages = readdirSync(outputDir).filter((name) => name.endsWith(".tgz"));
33
37
  if (packages.length !== 1) {
34
38
  throw new Error(`release:pack expected exactly one .tgz artifact, found ${packages.length}`);
35
39
  }
40
+
41
+ const report = parsePackReport(result.stdout);
42
+ const packagedPaths = new Set(report.files.map((file) => file.path));
43
+ const expectedSchemas = readdirSync(resolve(repoRoot, "schemas", "v1"))
44
+ .filter((name) => name.endsWith(".schema.json"))
45
+ .map((name) => `schemas/v1/${name}`)
46
+ .sort();
47
+ if (expectedSchemas.length === 0) {
48
+ throw new Error("release:pack expected at least one generated Config System v1 schema");
49
+ }
50
+ const missingSchemas = expectedSchemas.filter((path) => !packagedPaths.has(path));
51
+ if (missingSchemas.length > 0) {
52
+ throw new Error(`release:pack omitted generated Config schemas: ${missingSchemas.join(", ")}`);
53
+ }
54
+
55
+ const expectedRuntimeResources = [
56
+ "dist/cli.js",
57
+ ...collectPackageFiles(resolve(repoRoot, "templates"), "templates"),
58
+ ...collectPackageFiles(resolve(repoRoot, "capabilities"), "capabilities"),
59
+ ].sort();
60
+ const missingRuntimeResources = expectedRuntimeResources.filter((path) => !packagedPaths.has(path));
61
+ if (missingRuntimeResources.length > 0) {
62
+ throw new Error(
63
+ `release:pack omitted ForgeRelay-owned runtime resources: ${missingRuntimeResources.join(", ")}`,
64
+ );
65
+ }
66
+
36
67
  console.log(`Verified npm artifact: ${packages[0]}`);
68
+ console.log(`Verified packaged Config schemas: ${expectedSchemas.join(", ")}`);
69
+ console.log(`Verified packaged ForgeRelay-owned runtime resources: ${expectedRuntimeResources.join(", ")}`);
70
+
71
+ function collectPackageFiles(directory, prefix) {
72
+ const files = [];
73
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
74
+ const relative = `${prefix}/${entry.name}`;
75
+ if (entry.isDirectory()) {
76
+ files.push(...collectPackageFiles(resolve(directory, entry.name), relative));
77
+ } else if (entry.isFile()) {
78
+ files.push(relative);
79
+ }
80
+ }
81
+ return files;
82
+ }
83
+
84
+ function parsePackReport(stdout) {
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(stdout);
88
+ } catch (error) {
89
+ const reason = error instanceof Error ? error.message : String(error);
90
+ throw new Error(`npm pack did not return valid JSON metadata: ${reason}`);
91
+ }
92
+ if (!Array.isArray(parsed) || parsed.length !== 1 || !Array.isArray(parsed[0]?.files)) {
93
+ throw new Error("npm pack returned an unexpected JSON report");
94
+ }
95
+ return parsed[0];
96
+ }