@akira-tl/forgerelay 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.1] - 2026-08-09
8
+
9
+ ### Changed
10
+
11
+ - GitHub CI is now release-only: ordinary branch pushes, pull-request updates, and manual dispatches do not start cloud CI; the reusable CI workflow is invoked only by the stable `vX.Y.Z` tag release workflow.
12
+ - Release automation now triggers only for stable `vX.Y.Z` version tags, waits for cloud CI to pass, then publishes; ordinary branch pushes never enter the CI or publication workflows.
13
+ - Global system instructions now come from exactly one configured file, defaulting to `~/.agents/AGENTS.md`; `FORGERELAY_AGENT_DIR` remains a skill-compatibility path rather than an instruction source.
14
+
15
+ ### Fixed
16
+
17
+ - Symbolic-link system instruction entries now follow their configured target even when the canonical source lives outside the runtime instruction directory.
18
+ - Project instruction aliases that resolve to the same file are loaded once, preventing duplicate `AGENTS.md` context on case-insensitive filesystems such as default macOS and Windows checkouts.
19
+
7
20
  ## [0.1.0] - 2026-08-09
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Give MCP coding agents a real local workspace.**
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/%40akira-tl%2Fforgerelay?style=flat-square)](https://www.npmjs.com/package/@akira-tl/forgerelay)
6
- [![CI](https://img.shields.io/github/actions/workflow/status/Akira-TL/forgerelay/ci.yml?style=flat-square&branch=main)](https://github.com/Akira-TL/forgerelay/actions/workflows/ci.yml)
6
+ [![Release](https://img.shields.io/github/actions/workflow/status/Akira-TL/forgerelay/release.yml?style=flat-square&label=release)](https://github.com/Akira-TL/forgerelay/actions/workflows/release.yml)
7
7
  [![License](https://img.shields.io/npm/l/%40akira-tl%2Fforgerelay?style=flat-square)](LICENSE)
8
8
 
9
9
  ForgeRelay is a self-hosted MCP server that lets ChatGPT and other MCP-capable
@@ -227,9 +227,11 @@ npm run release:major
227
227
  npm run release:verify
228
228
  ```
229
229
 
230
- Pushing a matching `vX.Y.Z` tag to `Akira-TL/forgerelay` is the publish action.
231
- GitHub Actions verifies the tagged commit, publishes `@akira-tl/forgerelay` to
232
- npm, and creates the matching GitHub Release.
230
+ Daily branch pushes do not run cloud CI. When preparing a release, run the full
231
+ local release verification first. Pushing a matching `vX.Y.Z` tag to
232
+ `Akira-TL/forgerelay` is the only cloud CI and publish trigger: GitHub Actions
233
+ runs the reusable multi-platform CI, then publishes `@akira-tl/forgerelay` and
234
+ creates the matching GitHub Release only after CI succeeds.
233
235
 
234
236
  See [Versioning and Release Management](docs/versioning.md) for the bootstrap and
235
237
  Trusted Publishing setup.
package/dist/config.js CHANGED
@@ -164,6 +164,17 @@ function defaultWorktreeRoot() {
164
164
  function defaultAgentDir() {
165
165
  return join(homedir(), ".codex");
166
166
  }
167
+ function defaultSystemInstructionsPath() {
168
+ return join(homedir(), ".agents", "AGENTS.md");
169
+ }
170
+ function parseSystemInstructionsPath(value) {
171
+ if (value === undefined)
172
+ return resolve(defaultSystemInstructionsPath());
173
+ if (typeof value !== "string" || value.trim().length === 0) {
174
+ throw new Error("FORGERELAY_SYSTEM_INSTRUCTIONS_PATH must be one non-empty path");
175
+ }
176
+ return resolve(expandHomePath(value.trim()));
177
+ }
167
178
  export function loadConfig(env = process.env) {
168
179
  const files = loadForgeRelayFiles(env);
169
180
  const host = env.HOST ?? files.config.host ?? "127.0.0.1";
@@ -202,6 +213,7 @@ export function loadConfig(env = process.env) {
202
213
  ? files.config.subagents === true
203
214
  : parseBoolean(productEnv(env, "SUBAGENTS")),
204
215
  agentDir: resolve(expandHomePath(productEnv(env, "AGENT_DIR") ?? files.config.agentDir ?? defaultAgentDir())),
216
+ systemInstructionsPath: parseSystemInstructionsPath(productEnv(env, "SYSTEM_INSTRUCTIONS_PATH") ?? files.config.systemInstructionsPath),
205
217
  logging: parseLoggingConfig(env),
206
218
  };
207
219
  }
@@ -1,7 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, opendir, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
- import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
5
4
  import { closeManagedWorktree, createManagedWorktree, resolveManagedWorktreeBase, } from "./git-worktrees.js";
6
5
  import { AccessDeniedError, assertAllowedPath, isPathInsideRoot, resolveAllowedPath, } from "./roots.js";
7
6
  import { loadWorkspaceSkills, markSkillActivated, resolveSkillReadPath, } from "./skills.js";
@@ -381,21 +380,34 @@ export class WorkspaceRegistry {
381
380
  return assertAllowedPath(root, this.config.allowedRoots);
382
381
  }
383
382
  async loadInitialAgentsFiles(root) {
384
- const agentDir = resolve(this.config.agentDir);
385
383
  const resolvedRoot = (await tryRealpath(root)) ?? root;
386
- const resolvedAgentDir = (await tryRealpath(agentDir)) ?? agentDir;
384
+ const systemInstructionsPath = resolve(this.config.systemInstructionsPath);
387
385
  const loadedFiles = [];
388
- for (const file of loadProjectContextFiles({ cwd: root, agentDir })) {
389
- const path = resolve(file.path);
390
- if (!isInitialAgentsFilePath(path, root, agentDir))
391
- continue;
392
- const content = await readResolvedContextFile(path, file.content, resolvedRoot, resolvedAgentDir);
386
+ const loadedRealPaths = new Set();
387
+ const systemInstructions = await readSystemInstructions(systemInstructionsPath);
388
+ const systemInstructionsRealPath = await tryRealpath(systemInstructionsPath);
389
+ if (systemInstructions !== undefined) {
390
+ loadedFiles.push({
391
+ path: systemInstructionsPath,
392
+ content: systemInstructions,
393
+ });
394
+ if (systemInstructionsRealPath)
395
+ loadedRealPaths.add(systemInstructionsRealPath);
396
+ }
397
+ for (const fileName of CONTEXT_FILE_NAMES) {
398
+ const path = join(root, fileName);
399
+ const content = await readResolvedProjectContextFile(path, resolvedRoot);
393
400
  if (content === undefined)
394
401
  continue;
402
+ const realPath = await tryRealpath(path);
403
+ if (realPath && loadedRealPaths.has(realPath))
404
+ continue;
395
405
  loadedFiles.push({
396
406
  path,
397
407
  content,
398
408
  });
409
+ if (realPath)
410
+ loadedRealPaths.add(realPath);
399
411
  }
400
412
  return loadedFiles;
401
413
  }
@@ -408,7 +420,10 @@ export class WorkspaceRegistry {
408
420
  loadedRealPaths.add(realPath);
409
421
  }
410
422
  const discovered = [];
423
+ const agentDir = resolve(this.config.agentDir);
411
424
  await walkWorkspace(root, async (path, entry) => {
425
+ if (isPathInsideRoot(path, agentDir))
426
+ return;
412
427
  if (!entry.isFile())
413
428
  return;
414
429
  if (!CONTEXT_FILE_NAMES.has(entry.name))
@@ -480,20 +495,32 @@ export function formatAgentsPath(path, workspaceRoot) {
480
495
  }
481
496
  return relationship.split(sep).join("/");
482
497
  }
483
- function isInitialAgentsFilePath(path, root, agentDir) {
484
- if (isPathInsideRoot(path, agentDir))
485
- return true;
498
+ function isProjectRootInstructionPath(path, root) {
486
499
  return isPathInsideRoot(path, root) && dirname(path) === root;
487
500
  }
488
- async function readResolvedContextFile(path, fallbackContent, root, agentDir) {
501
+ async function readSystemInstructions(path) {
502
+ try {
503
+ return await readFile(path, "utf8");
504
+ }
505
+ catch (error) {
506
+ if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
507
+ return undefined;
508
+ }
509
+ throw error;
510
+ }
511
+ }
512
+ async function readResolvedProjectContextFile(path, root) {
489
513
  try {
490
514
  const resolvedPath = await realpath(path);
491
- if (!isInitialAgentsFilePath(resolvedPath, root, agentDir))
515
+ if (!isProjectRootInstructionPath(resolvedPath, root))
492
516
  return undefined;
493
517
  return await readFile(resolvedPath, "utf8");
494
518
  }
495
- catch {
496
- return fallbackContent;
519
+ catch (error) {
520
+ if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
521
+ return undefined;
522
+ }
523
+ throw error;
497
524
  }
498
525
  }
499
526
  async function tryRealpath(path) {
@@ -87,10 +87,14 @@ placed into a merge-conflict state.
87
87
  Legacy `devspace/*` managed branches remain closable when they are already stored
88
88
  in workspace metadata; only new managed branches use `forgerelay/*`.
89
89
 
90
- ## Project instructions
90
+ ## Instructions
91
91
 
92
- When a workspace opens, ForgeRelay loads root-level instruction files when they
93
- exist:
92
+ When a workspace opens, ForgeRelay first loads exactly one global system-instructions
93
+ file. The default is `~/.agents/AGENTS.md`; configure a different single path with
94
+ `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH`. Symbolic links are followed so this entry can
95
+ point at a canonical source elsewhere on disk.
96
+
97
+ ForgeRelay then loads root-level project instruction files when they exist:
94
98
 
95
99
  ```text
96
100
  AGENTS.md
@@ -99,8 +103,10 @@ CLAUDE.md
99
103
  CLAUDE.MD
100
104
  ```
101
105
 
102
- Nested instruction files are returned as available paths rather than all being
103
- injected eagerly. Read the relevant nested file before working under that path.
106
+ Nested project instruction files are returned as available paths rather than all
107
+ being injected eagerly. Read the relevant nested file before working under that path.
108
+ `FORGERELAY_AGENT_DIR` is not an instruction source; it remains only a compatibility
109
+ skill-discovery path.
104
110
 
105
111
  ## Agent Skills
106
112
 
@@ -136,6 +136,18 @@ programs when the optional `node-pty` dependency is available.
136
136
  | `changes` | Attach UI to `open_workspace` and aggregate `show_changes`. |
137
137
  | `off` | Disable widget UI. |
138
138
 
139
+ ## System instructions
140
+
141
+ ForgeRelay loads exactly one global system-instructions file. The default is
142
+ `~/.agents/AGENTS.md`. Set `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH` or the
143
+ `systemInstructionsPath` config key to point at a different single file.
144
+ Arrays or empty values are not accepted. Symbolic links are followed, so the
145
+ runtime entry may point at a canonical source elsewhere on disk.
146
+
147
+ Project-root `AGENTS.md` / `CLAUDE.md` files remain project context and are
148
+ loaded separately. `FORGERELAY_AGENT_DIR` does not select a global instruction
149
+ file; it remains a compatibility path for Agent Skills.
150
+
139
151
  ## Skills and subagents
140
152
 
141
153
  | Variable | Purpose |
@@ -94,24 +94,30 @@ npm run release:tag-check -- v0.1.0
94
94
  A release tag must exactly equal `v` followed by the package version, and
95
95
  `Unreleased` must be empty.
96
96
 
97
- ## Tag-triggered publishing
97
+ ## CI and tag-triggered publishing
98
98
 
99
- `.github/workflows/release.yml` runs for tags matching `v*.*.*` in the
100
- `Akira-TL/forgerelay` repository. The release helper performs the strict SemVer
101
- validation; tags that only match the broad GitHub glob but not the package
102
- version are rejected.
99
+ `.github/workflows/ci.yml` is a reusable workflow with only a `workflow_call`
100
+ entry point. It does not run on ordinary branch pushes, pull-request updates, or
101
+ manual dispatches. Day-to-day development therefore does not consume cloud CI.
102
+
103
+ `.github/workflows/release.yml` is the single GitHub Actions entry point. It runs
104
+ only for stable SemVer tags matching `v[0-9]+.[0-9]+.[0-9]+` in the
105
+ `Akira-TL/forgerelay` repository. The release helper then requires the tag to
106
+ exactly equal `v` plus the package version. Ordinary branch pushes never enter
107
+ cloud CI or publication.
103
108
 
104
109
  For a valid tag, the workflow:
105
110
 
106
- 1. checks out the tagged commit with full Git history;
107
- 2. verifies the tagged commit is contained in `origin/main`;
108
- 3. installs dependencies;
109
- 4. checks the tag against `package.json` and `CHANGELOG.md`;
110
- 5. runs `npm run release:verify`;
111
- 6. runs `npm pack --dry-run`;
112
- 7. checks whether the exact npm version is already published;
113
- 8. publishes `@akira-tl/forgerelay` when necessary;
114
- 9. creates the matching GitHub Release after npm publication succeeds.
111
+ 1. invokes `.github/workflows/ci.yml` for the cloud multi-platform verification;
112
+ 2. waits for every CI matrix job to pass;
113
+ 3. checks out the tagged commit with full Git history;
114
+ 4. verifies the tagged commit is contained in `origin/main`;
115
+ 5. installs dependencies and checks the tag against `package.json` and `CHANGELOG.md`;
116
+ 6. rebuilds the publish artifact on the publication runner;
117
+ 7. runs `npm pack --dry-run`;
118
+ 8. checks whether the exact npm version is already published;
119
+ 9. publishes `@akira-tl/forgerelay` when necessary;
120
+ 10. creates the matching GitHub Release after npm publication succeeds.
115
121
 
116
122
  The npm existence check makes the workflow safely restartable when npm
117
123
  publication succeeds but GitHub Release creation fails afterward.
@@ -152,7 +158,8 @@ npm publishing token.
152
158
  3. Run the appropriate `release:patch`, `release:minor`, or `release:major`
153
159
  command.
154
160
  4. Review the generated version and changelog diff.
155
- 5. Run `npm run release:verify`.
161
+ 5. Run `npm run release:verify` locally. This full local gate is a release-time
162
+ operation; ordinary development pushes do not need to run the full release gate.
156
163
  6. Commit the release-ready code and metadata and push `main`.
157
164
  7. Create the exact version tag, for example:
158
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",