@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 +13 -0
- package/README.md +6 -4
- package/dist/config.js +12 -0
- package/dist/workspaces.js +42 -15
- package/docs/chatgpt-coding-workflow.md +11 -5
- package/docs/configuration.md +12 -0
- package/docs/versioning.md +22 -15
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/@akira-tl/forgerelay)
|
|
6
|
-
[](https://github.com/Akira-TL/forgerelay/actions/workflows/release.yml)
|
|
7
7
|
[](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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
}
|
package/dist/workspaces.js
CHANGED
|
@@ -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
|
|
384
|
+
const systemInstructionsPath = resolve(this.config.systemInstructionsPath);
|
|
387
385
|
const loadedFiles = [];
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
515
|
+
if (!isProjectRootInstructionPath(resolvedPath, root))
|
|
492
516
|
return undefined;
|
|
493
517
|
return await readFile(resolvedPath, "utf8");
|
|
494
518
|
}
|
|
495
|
-
catch {
|
|
496
|
-
|
|
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
|
-
##
|
|
90
|
+
## Instructions
|
|
91
91
|
|
|
92
|
-
When a workspace opens, ForgeRelay loads
|
|
93
|
-
|
|
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
|
|
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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 |
|
package/docs/versioning.md
CHANGED
|
@@ -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
|
-
##
|
|
97
|
+
## CI and tag-triggered publishing
|
|
98
98
|
|
|
99
|
-
`.github/workflows/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
111
|
-
6.
|
|
112
|
-
7.
|
|
113
|
-
8.
|
|
114
|
-
9.
|
|
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
|
|