@akira-tl/forgerelay 1.2.5 → 1.3.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -2
  3. package/dist/cli/config/domains/context-cli.js +86 -0
  4. package/dist/cli/config/domains/domain-cli.js +468 -0
  5. package/dist/cli/config/general.js +174 -0
  6. package/dist/cli/config/inspect.js +29 -26
  7. package/dist/cli/config/migrate.js +6 -22
  8. package/dist/cli/config/scope.js +35 -0
  9. package/dist/cli/connect/relay.js +284 -0
  10. package/dist/cli/core/command-tree.js +68 -0
  11. package/dist/cli/core/serve-options.js +71 -0
  12. package/dist/cli/init/setup-config.js +26 -0
  13. package/dist/cli/init.js +37 -8
  14. package/dist/cli/maintenance-prune.js +1 -1
  15. package/dist/cli/maintenance.js +6 -6
  16. package/dist/cli/mcp/external-mcp.js +29 -17
  17. package/dist/cli/mcp/status.js +2 -2
  18. package/dist/cli/system/status.js +35 -0
  19. package/dist/cli.js +132 -272
  20. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +2 -2
  21. package/dist/mcp/server/core/schemas.js +2 -10
  22. package/dist/mcp/server/operations/runtime/operation-runtime.js +11 -7
  23. package/dist/runtime/config/config.js +30 -29
  24. package/dist/runtime/config/definition/general-config.js +18 -3
  25. package/dist/runtime/config/resolution/resolver.js +7 -4
  26. package/dist/runtime/config/user-config.js +6 -6
  27. package/dist/runtime/config/validation/paths.js +12 -0
  28. package/dist/subagents/profiles.js +37 -0
  29. package/dist/workspaces/bootstrap.js +31 -14
  30. package/dist/workspaces/context.js +159 -7
  31. package/dist/workspaces/relay/auth/cli-test-support.js +22 -0
  32. package/dist/workspaces/resources/context-sources.js +29 -0
  33. package/dist/workspaces/resources/resource-monitor.js +29 -6
  34. package/dist/workspaces/resources/skills.js +15 -10
  35. package/dist/workspaces/sessions.js +20 -2
  36. package/dist/workspaces/state/project-context.js +34 -8
  37. package/dist/workspaces.js +5 -2
  38. package/docs/chatgpt-coding-workflow.md +23 -20
  39. package/docs/configuration.md +40 -27
  40. package/docs/gotchas.md +8 -6
  41. package/docs/roadmap.md +1 -1
  42. package/package.json +2 -2
  43. package/schemas/v1/config.project-local.schema.json +74 -0
  44. package/schemas/v1/config.project.schema.json +74 -0
  45. package/schemas/v1/config.user.schema.json +57 -2
  46. package/scripts/ci/config-v2-product-acceptance.mjs +17 -12
  47. package/scripts/debug/runtime.mjs +19 -3
  48. package/scripts/debug/runtime.test.mjs +3 -0
  49. package/scripts/debug/serve.mjs +2 -2
@@ -24,6 +24,7 @@ export class WorkspaceResourceMonitor {
24
24
  tracked: new Map(),
25
25
  subscriptions: new Map(),
26
26
  changes: [],
27
+ announcements: [],
27
28
  deliveredRevisionByScope: new Map(),
28
29
  };
29
30
  state.root = input.root;
@@ -88,6 +89,20 @@ export class WorkspaceResourceMonitor {
88
89
  this.recordChange(workspaceId, key, oldContent, newContent);
89
90
  }));
90
91
  }
92
+ announce(workspaceId, text, coveredComponents) {
93
+ const state = this.states.get(workspaceId);
94
+ if (!state)
95
+ return;
96
+ state.revision += 1;
97
+ state.announcements.push({
98
+ revision: state.revision,
99
+ text,
100
+ coveredComponents,
101
+ });
102
+ if (state.announcements.length > MAX_HISTORY) {
103
+ state.announcements.splice(0, state.announcements.length - MAX_HISTORY);
104
+ }
105
+ }
91
106
  markSkillActivated(workspaceId, skillPath) {
92
107
  const state = this.states.get(workspaceId);
93
108
  if (!state)
@@ -118,25 +133,31 @@ export class WorkspaceResourceMonitor {
118
133
  synchronizeSharedWatch(key);
119
134
  const deliveredRevision = state.deliveredRevisionByScope.get(conversationScopeId) ?? state.revision;
120
135
  const changes = state.changes.filter((change) => change.revision > deliveredRevision);
136
+ const announcements = state.announcements.filter((announcement) => announcement.revision > deliveredRevision);
121
137
  state.deliveredRevisionByScope.set(conversationScopeId, state.revision);
122
- if (changes.length === 0)
138
+ if (changes.length === 0 && announcements.length === 0)
123
139
  return undefined;
124
- const grouped = coalesceChanges(changes);
125
140
  const sections = [];
126
141
  const coveredComponents = new Set();
127
- for (const change of grouped) {
142
+ for (const change of coalesceChanges(changes)) {
128
143
  const formatted = formatResourceChange(change);
129
144
  if (!formatted)
130
145
  continue;
131
- sections.push(formatted.text);
146
+ sections.push({ revision: change.revision, text: formatted.text });
132
147
  for (const component of formatted.coveredComponents)
133
148
  coveredComponents.add(component);
134
149
  }
150
+ for (const announcement of announcements) {
151
+ sections.push({ revision: announcement.revision, text: announcement.text });
152
+ for (const component of announcement.coveredComponents)
153
+ coveredComponents.add(component);
154
+ }
155
+ sections.sort((left, right) => left.revision - right.revision);
135
156
  this.pruneDeliveredHistory(state);
136
157
  if (sections.length === 0)
137
158
  return undefined;
138
159
  const header = "Workspace context changed after this Workspace was opened. Apply only these deltas; unchanged instructions and Skill metadata remain active:";
139
- const joined = [header, ...sections].join("\n\n");
160
+ const joined = [header, ...sections.map((section) => section.text)].join("\n\n");
140
161
  const text = joined.length <= MAX_DELIVERY_CHARACTERS
141
162
  ? joined
142
163
  : `${joined.slice(0, MAX_DELIVERY_CHARACTERS)}\n\n[Additional Workspace context deltas were truncated; reopen with context=\"auto\" to refresh metadata.]`;
@@ -199,11 +220,13 @@ export class WorkspaceResourceMonitor {
199
220
  }
200
221
  }
201
222
  pruneDeliveredHistory(state) {
202
- if (state.changes.length === 0 || state.deliveredRevisionByScope.size === 0)
223
+ if ((state.changes.length === 0 && state.announcements.length === 0) || state.deliveredRevisionByScope.size === 0)
203
224
  return;
204
225
  const floor = Math.min(...state.deliveredRevisionByScope.values());
205
226
  while (state.changes[0] && state.changes[0].revision <= floor)
206
227
  state.changes.shift();
228
+ while (state.announcements[0] && state.announcements[0].revision <= floor)
229
+ state.announcements.shift();
207
230
  }
208
231
  }
209
232
  function subscribeSharedWatch(inputPath, subscriberId, subscriber) {
@@ -4,16 +4,21 @@ import { basename, dirname, extname, join, resolve, sep } from "node:path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  import { markAdvertisedFileSourceActivated, resolveAdvertisedFileReadPath, } from "../../mcp/filesystem/advertised-files.js";
6
6
  import { expandHomePath, isPathInsideRoot } from "../../mcp/filesystem/roots.js";
7
+ export function redactSkillDiagnosticMessage(diagnostic) {
8
+ let message = diagnostic.message;
9
+ const hiddenPaths = [
10
+ diagnostic.path,
11
+ diagnostic.collision?.winnerPath,
12
+ diagnostic.collision?.loserPath,
13
+ ].filter((path) => Boolean(path));
14
+ for (const path of hiddenPaths)
15
+ message = message.split(path).join("<skill-path>");
16
+ return message;
17
+ }
7
18
  const FRONTMATTER_DELIMITER = "---";
8
- export function effectiveSkillPaths(config, cwd) {
9
- const defaultPathCandidates = [
10
- resolve(cwd, ".agents", "skills"),
11
- resolve(cwd, ".forgerelay", "skills"),
12
- config.configSkillsDir,
13
- ];
14
- const defaultPaths = defaultPathCandidates.filter((path) => path !== undefined && existsSync(path));
19
+ export function effectiveSkillPaths(config, cwd, skillPaths = config.skillPaths) {
15
20
  const seen = new Set();
16
- return [...defaultPaths, ...config.skillPaths]
21
+ return skillPaths
17
22
  .map((path) => resolveSkillPath(path, cwd))
18
23
  .filter((path) => {
19
24
  if (seen.has(path))
@@ -25,13 +30,13 @@ export function effectiveSkillPaths(config, cwd) {
25
30
  function resolveSkillPath(path, cwd) {
26
31
  return resolve(cwd, expandHomePath(path));
27
32
  }
28
- export function loadWorkspaceSkills(config, cwd) {
33
+ export function loadWorkspaceSkills(config, cwd, skillPaths = config.skillPaths) {
29
34
  if (!config.skillsEnabled)
30
35
  return { skills: [], diagnostics: [] };
31
36
  const skills = [];
32
37
  const diagnostics = [];
33
38
  const winners = new Map();
34
- for (const sourcePath of effectiveSkillPaths(config, cwd)) {
39
+ for (const sourcePath of effectiveSkillPaths(config, cwd, skillPaths)) {
35
40
  const candidates = discoverSkills(sourcePath, diagnostics);
36
41
  for (const skill of candidates) {
37
42
  const existing = winners.get(skill.name);
@@ -21,6 +21,7 @@ export class WorkspaceSessionService {
21
21
  workspaces;
22
22
  context;
23
23
  pendingOpens = new Map();
24
+ pendingContextRebuilds = new Map();
24
25
  lastWorkspaceGcAt = 0;
25
26
  constructor(config, store, workspaces, context) {
26
27
  this.config = config;
@@ -294,8 +295,23 @@ export class WorkspaceSessionService {
294
295
  }
295
296
  }
296
297
  async reusedWorkspaceContext(workspace) {
298
+ const existing = this.pendingContextRebuilds.get(workspace.id);
299
+ if (existing)
300
+ return existing;
301
+ const rebuild = this.rebuildReusedWorkspaceContext(workspace);
302
+ this.pendingContextRebuilds.set(workspace.id, rebuild);
303
+ try {
304
+ return await rebuild;
305
+ }
306
+ finally {
307
+ if (this.pendingContextRebuilds.get(workspace.id) === rebuild) {
308
+ this.pendingContextRebuilds.delete(workspace.id);
309
+ }
310
+ }
311
+ }
312
+ async rebuildReusedWorkspaceContext(workspace) {
297
313
  workspace.project = await resolveProjectContext(this.config.configDir, workspace.root);
298
- Object.assign(workspace, this.context.loadSkillsForWorkspace(workspace.root));
314
+ Object.assign(workspace, await this.context.loadContextSourcesForWorkspace(workspace.project, workspace.root));
299
315
  workspace.capabilityGuides = loadCapabilityGuides(this.config);
300
316
  workspace.agentProfiles = await loadSubagentProfiles(this.config, workspace.root);
301
317
  workspace.scannedInstructionDirs.clear();
@@ -538,11 +554,13 @@ export class WorkspaceSessionService {
538
554
  return existing;
539
555
  }
540
556
  const root = this.context.assertWorkspaceRootAllowed(session.root, session.mode, session.sourceRoot);
557
+ const contextSources = this.context.defaultContextSources(root);
541
558
  const restoredWorkspace = {
542
559
  id: session.id,
543
560
  root,
544
561
  mode: session.mode,
545
562
  sourceRoot: session.sourceRoot,
563
+ contextSources,
546
564
  worktree: session.mode === "worktree"
547
565
  ? {
548
566
  path: root,
@@ -555,7 +573,7 @@ export class WorkspaceSessionService {
555
573
  managed: session.managed,
556
574
  }
557
575
  : undefined,
558
- ...this.context.loadSkillsForWorkspace(root),
576
+ ...this.context.loadSkillsForWorkspace(root, contextSources.skillPaths),
559
577
  capabilityGuides: loadCapabilityGuides(this.config),
560
578
  agentProfiles: [],
561
579
  activatedSkillDirs: new Set(),
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
- import { lstat, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { isAbsolute, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { withFileLock } from "../../runtime/state/lock/file-lock.js";
@@ -193,20 +193,46 @@ function newProjectId() {
193
193
  async function hasGitMetadataAncestor(workspaceRoot) {
194
194
  let current = workspaceRoot;
195
195
  for (;;) {
196
- try {
197
- await lstat(join(current, ".git"));
196
+ if (await isGitMetadataMarker(join(current, ".git")))
198
197
  return true;
199
- }
200
- catch (error) {
201
- if (!isErrno(error, "ENOENT"))
202
- throw error;
203
- }
204
198
  const parent = resolve(current, "..");
205
199
  if (parent === current)
206
200
  return false;
207
201
  current = parent;
208
202
  }
209
203
  }
204
+ async function isGitMetadataMarker(path) {
205
+ let metadata;
206
+ try {
207
+ metadata = await stat(path);
208
+ }
209
+ catch (error) {
210
+ if (isErrno(error, "ENOENT"))
211
+ return false;
212
+ throw error;
213
+ }
214
+ if (metadata.isDirectory()) {
215
+ try {
216
+ return (await readFile(join(path, "HEAD"), "utf8")).trim().length > 0;
217
+ }
218
+ catch (error) {
219
+ if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR"))
220
+ return false;
221
+ throw error;
222
+ }
223
+ }
224
+ if (metadata.isFile()) {
225
+ try {
226
+ return /^gitdir:\s*\S+/i.test((await readFile(path, "utf8")).trim());
227
+ }
228
+ catch (error) {
229
+ if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR"))
230
+ return false;
231
+ throw error;
232
+ }
233
+ }
234
+ return false;
235
+ }
210
236
  function isGitRepositoryMiss(error) {
211
237
  if (!isRecord(error))
212
238
  return false;
@@ -360,14 +360,16 @@ export class WorkspaceRegistry {
360
360
  });
361
361
  }
362
362
  async createWorkspaceContext(input) {
363
+ const project = await resolveProjectContext(this.config.configDir, input.root);
364
+ const contextResources = await this.context.loadContextSourcesForWorkspace(project, input.root);
363
365
  const workspace = {
364
366
  id: `ws_${randomBytes(5).toString("hex")}`,
365
367
  root: input.root,
366
368
  mode: input.mode,
367
369
  sourceRoot: input.sourceRoot,
368
- project: await resolveProjectContext(this.config.configDir, input.root),
370
+ project,
371
+ ...contextResources,
369
372
  worktree: input.worktree,
370
- ...this.context.loadSkillsForWorkspace(input.root),
371
373
  capabilityGuides: loadCapabilityGuides(this.config),
372
374
  agentProfiles: await loadSubagentProfiles(this.config, input.root),
373
375
  activatedSkillDirs: new Set(),
@@ -421,6 +423,7 @@ export class WorkspaceRegistry {
421
423
  discoverPathInstructions(workspace, inputPath) {
422
424
  return this.context.discoverPathInstructions(workspace, inputPath);
423
425
  }
426
+ refreshContextSources(workspaceId) { return this.context.refreshContextSourcesForWorkspace(this.getWorkspace(workspaceId)); }
424
427
  claimResourceUpdates(workspaceId, conversationScopeId) {
425
428
  return this.context.claimResourceUpdates(workspaceId, conversationScopeId);
426
429
  }
@@ -174,23 +174,20 @@ state and does not recreate the physical backing.
174
174
 
175
175
  ## Instructions
176
176
 
177
- When a workspace opens, ForgeRelay first loads exactly one global system-instructions
178
- file. The default is `~/.agents/AGENTS.md`; configure a different single path with
179
- `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH`. Symbolic links are followed so this entry can
180
- point at a canonical source elsewhere on disk.
177
+ Agent context sources are resolved through General Config v2 using
178
+ `runtime > project-local > project > user > built-in` precedence and replace semantics.
179
+ ForgeRelay loads exactly one selected system-instructions file; the built-in default is
180
+ `~/.agents/AGENTS.md`, and `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH` is the runtime replacement.
181
+ A missing selected system file is reported as unavailable but does not block Workspace open.
181
182
 
182
- ForgeRelay then loads root-level project instruction files when they exist:
183
-
184
- ```text
185
- AGENTS.md
186
- AGENTS.MD
187
- CLAUDE.md
188
- CLAUDE.MD
189
- ```
183
+ Project/path-scoped instruction discovery is controlled by `instructionNames`. The built-in
184
+ list is exactly `["AGENTS.md"]`; `CLAUDE.md`, `GEMINI.md`, or other basenames are discovered
185
+ only when explicitly configured. `FORGERELAY_INSTRUCTION_NAMES` provides a comma-separated
186
+ runtime replacement list.
190
187
 
191
188
  To keep broad workspaces such as `~` fast, initial nested-instruction discovery is
192
189
  bounded to direct child directories instead of recursively walking the whole tree.
193
- Deeper `AGENTS.md` / `CLAUDE.md` files are discovered lazily along a path the first
190
+ Deeper configured instruction filenames are discovered lazily along a path the first
194
191
  time the Agent accesses it, and already-scanned directories are cached for the life
195
192
  of that workspace handle. A `read` result carries any newly discovered local
196
193
  instructions before the requested file content. Side-effecting file tools and shell
@@ -241,16 +238,22 @@ force the Host to discard a cached tool schema.
241
238
 
242
239
  ## Agent Skills
243
240
 
244
- ForgeRelay discovers Skills in precedence order from:
241
+ `skillPaths` is an ordered General Config v2 list with replace semantics. Its built-in
242
+ effective value is:
245
243
 
246
- - project `.agents/skills`
247
- - project `.forgerelay/skills`
248
- - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
249
- - paths explicitly added through `FORGERELAY_SKILL_PATHS`
244
+ ```text
245
+ ~/.agents/skills
246
+ ./.agents/skills
247
+ ```
250
248
 
251
- These paths are discovery sources, not one shared ownership domain. Project `.agents/skills` is the open Agent Skills ecosystem and may contain files or symlinks managed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. ForgeRelay does not automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`.
249
+ The first path is user-home scoped; `./...` paths resolve from the current Workspace root.
250
+ Project, Project Local, User, or runtime config can replace the complete list with any
251
+ Agent ecosystem directories. `FORGERELAY_SKILL_PATHS` is the comma-separated runtime
252
+ replacement. ForgeRelay does not implicitly scan `.forgerelay/skills`, provider-private
253
+ Skill directories, or `FORGERELAY_AGENT_DIR/skills`.
252
254
 
253
- Same-named collisions use the first source: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
255
+ Same-named collisions use source-list order: the first discovered Skill name wins and the
256
+ losing source remains visible through diagnostics.
254
257
 
255
258
  When a task matches an advertised skill, read its `SKILL.md` before using other
256
259
  files in the skill directory.
@@ -888,21 +888,43 @@ Payload 用于策略和自动化,不包含文件正文、native-file credentia
888
888
 
889
889
  Hook 命令与 ForgeRelay 使用同一个本地用户权限。项目 `.forgerelay/hooks/*.json` 是可执行项目约定;允许某个 root 后,应把该 root 中的项目 Hook 视为本地开发环境的一部分。详见 [Security Model](security.md)。
890
890
 
891
- ## System instructions
892
-
893
- ForgeRelay loads exactly one global system-instructions file. The default is
894
- `~/.agents/AGENTS.md`. Set `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH` or the
895
- `systemInstructionsPath` config key to point at a different single file.
896
- Arrays or empty values are not accepted. Symbolic links are followed, so the
897
- runtime entry may point at a canonical source elsewhere on disk.
898
-
899
- Project-root `AGENTS.md` / `CLAUDE.md` files remain project context and are
900
- loaded separately. Initial nested-instruction discovery checks only direct child
901
- directories; deeper instruction files are discovered lazily when a workspace path
902
- is first accessed. Reads surface newly discovered instructions inline, while
903
- side-effecting file/shell operations stop before execution and require a retry if
904
- that access discovers new local instructions. `FORGERELAY_AGENT_DIR` does not
905
- select a global instruction file; its `skills` child is an additional Agent Skills source.
891
+ ## Agent context sources
892
+
893
+ ForgeRelay resolves Agent instruction and Skill sources through General Config v2.
894
+ The three source fields use normal precedence (`runtime > project-local > project > user > built-in`),
895
+ and each higher-precedence value **replaces** the lower-precedence/default value rather than appending to it.
896
+ `config get`, `config sources`, and `config explain` expose the same effective values and provenance.
897
+
898
+ `systemInstructionsPath` selects exactly one system-instructions file. Its built-in default is
899
+ `~/.agents/AGENTS.md`; `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH` is the runtime override. A missing
900
+ selected file does not prevent Workspace open, but ForgeRelay reports that instruction source as
901
+ unavailable. `~` resolves through the current user's home directory; Workspace-relative paths are
902
+ resolved from the current Workspace root.
903
+
904
+ `instructionNames` controls hierarchical project-instruction discovery. Its built-in effective value is
905
+ only `["AGENTS.md"]`; files such as `CLAUDE.md` or `GEMINI.md` are discovered only when explicitly
906
+ listed. `FORGERELAY_INSTRUCTION_NAMES` accepts a comma-separated runtime replacement list. Initial
907
+ nested discovery checks direct child directories; deeper matching instruction files are discovered lazily
908
+ when a Workspace path is first accessed. Reads surface newly discovered instructions inline, while
909
+ side-effecting file/shell operations stop before execution and require a retry if that access discovers new
910
+ local instructions.
911
+
912
+ `skillPaths` is an ordered source list. Its built-in effective value is:
913
+
914
+ ```json
915
+ ["~/.agents/skills", "./.agents/skills"]
916
+ ```
917
+
918
+ `FORGERELAY_SKILL_PATHS` accepts a comma-separated runtime **replacement** list. Project and Project
919
+ Local config may select another complete list as well. Missing configured directories are skipped quietly;
920
+ existing unreadable/invalid Skills and same-name collisions remain diagnosable. If the same Skill name
921
+ appears in multiple configured sources, the first source in the effective list wins. ForgeRelay does not
922
+ implicitly scan `~/.forgerelay/skills`, `<project>/.forgerelay/skills`, provider-private Skill directories, or
923
+ `FORGERELAY_AGENT_DIR/skills`.
924
+
925
+ `forgerelay init` exposes these three fields with direct input prompts. Pressing Enter accepts the displayed
926
+ current/default value; accepting the built-in default does not serialize a redundant override into
927
+ `config.json`.
906
928
 
907
929
  ## Skills and subagents
908
930
 
@@ -911,18 +933,9 @@ select a global instruction file; its `skills` child is an additional Agent Skil
911
933
  | `FORGERELAY_SKILLS` | Set to `0` to hide skills. Enabled by default. |
912
934
  | `FORGERELAY_SUBAGENTS` | Set to `1` to expose configured subagent profiles. |
913
935
  | `FORGERELAY_AGENT_DIR` | Defaults to `~/.codex`; used by supported Agent integrations, not as an automatic Skill source. |
914
- | `FORGERELAY_SKILL_PATHS` | Optional comma-separated additional skill directories. |
915
-
916
- Skills are discovered in precedence order from:
917
-
918
- - project `.agents/skills`
919
- - project `.forgerelay/skills`
920
- - the active ForgeRelay config directory's `skills` folder (`~/.forgerelay/skills` by default)
921
- - paths explicitly added through `FORGERELAY_SKILL_PATHS`
922
-
923
- Project `.agents/skills` belongs to the open Agent Skills ecosystem and may contain files or symlinks installed by other Agent tooling. ForgeRelay-owned project/system Skills stay under `.forgerelay/skills` and the active ForgeRelay config directory. Global Agent runtime directories such as `~/.agents/skills` and `FORGERELAY_AGENT_DIR/skills` are not scanned automatically.
924
-
925
- When the same Skill name appears in more than one source, the first source wins: project Agent Skills override project ForgeRelay Skills, which override system ForgeRelay Skills and explicit additional paths.
936
+ | `FORGERELAY_SYSTEM_INSTRUCTIONS_PATH` | Runtime replacement for the selected system instruction file. |
937
+ | `FORGERELAY_INSTRUCTION_NAMES` | Runtime comma-separated replacement list for project instruction basenames. |
938
+ | `FORGERELAY_SKILL_PATHS` | Runtime comma-separated replacement list for Skill source directories. |
926
939
 
927
940
  When subagents are enabled, canonical v1.2 profiles are discovered from:
928
941
 
package/docs/gotchas.md CHANGED
@@ -192,14 +192,16 @@ Skills are enabled by default. Check:
192
192
  FORGERELAY_SKILLS=1 forgerelay serve
193
193
  ```
194
194
 
195
- Standard automatic discovery paths include:
195
+ The built-in `skillPaths` list is:
196
196
 
197
- - project `.agents/skills`
198
- - project `.forgerelay/skills`
199
- - active ForgeRelay config `skills` directory (`~/.forgerelay/skills` by default)
200
- - additional paths explicitly configured through `FORGERELAY_SKILL_PATHS`
197
+ ```text
198
+ ~/.agents/skills
199
+ ./.agents/skills
200
+ ```
201
+
202
+ The first entry is user-home scoped; the second is relative to the current Workspace root. Any User, Project, Project Local, or runtime `skillPaths` value replaces that complete list. `FORGERELAY_SKILL_PATHS` is the comma-separated runtime replacement.
201
203
 
202
- ForgeRelay does **not** automatically scan global Agent runtime Skill directories such as `~/.agents/skills` or `FORGERELAY_AGENT_DIR/skills`. Project `.agents/skills` remains the open Agent Skills source, while ForgeRelay-owned project/system Skills live under `.forgerelay/skills` and the active ForgeRelay config `skills` directory.
204
+ If Skills are still missing, inspect the effective source with `forgerelay config get`, `forgerelay config sources`, or `forgerelay config explain config.skillPaths`. ForgeRelay does **not** implicitly scan `~/.forgerelay/skills`, project `.forgerelay/skills`, provider-private Skill directories, or `FORGERELAY_AGENT_DIR/skills`.
203
205
 
204
206
  ## Subagent profiles do not appear
205
207
 
package/docs/roadmap.md CHANGED
@@ -189,7 +189,7 @@ capability
189
189
  - completed background process 的 Agent 可消费状态最多保留 5 分钟;底层 ChildProcess/PTY handle 在退出时立即释放,completed notice 数量与 active process 数量都有硬上限,并缩小单 process 输出驻留预算;
190
190
  - 高输出 head/tail buffer 保留 Unicode code-point 语义,但不再通过 `Array.from(整段输出)` 构造巨型临时数组,降低 V8 heap 扩容与 GC 压力;
191
191
  - MCP transport registry 与 review checkpoint state 加入容量边界;正常 transport close/workspace close 仍立即释放,异常遗弃对象不能再无限累积;OAuth 过期 authorization code 也会主动淘汰;
192
- - `open_workspace` 的 instruction discovery 首轮只检查 root 与直接子目录,不再递归整棵 workspace。更深层 `AGENTS.md` / `CLAUDE.md` Agent 首次访问对应路径时沿祖先目录惰性发现,并缓存已扫描目录;read 可直接携带新发现指令,write/edit/rename/delete/bash 等副作用调用则在执行前返回指令并要求重试;
192
+ - `open_workspace` 的 instruction discovery 首轮只检查 root 与直接子目录,不再递归整棵 workspace。更深层只匹配 effective `instructionNames`(当前 built-in default 只有 `AGENTS.md`,其他 basename 需显式配置),在 Agent 首次访问对应路径时沿祖先目录惰性发现,并缓存已扫描目录;read 可直接携带新发现指令,write/edit/rename/delete/bash 等副作用调用则在执行前返回指令并要求重试;
193
193
  - Workspace SQLite 继续作为本地持久化真源,不引入 Redis/PostgreSQL/Docker。高频 session/conversation `lastUsedAt` touch 进入内存 write-behind cache,最多每 5 分钟事务批量 flush,normal shutdown 再显式 flush;create/close/status 等语义性状态仍同步持久化;
194
194
  - debug runtime telemetry 定期报告 RSS/heap、transport、process、workspace cache 与 review state 数量,为后续真实实例资源趋势提供可观测性。
195
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.2.5",
3
+ "version": "1.3.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",
@@ -68,7 +68,7 @@
68
68
  "release:push-ready": "node scripts/release/push-ready.mjs",
69
69
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
70
70
  "start": "node dist/cli.js serve",
71
- "test": "node --test scripts/debug/runtime.test.mjs scripts/config/schema-text.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/workspaces/relay/tests/composite.test.ts && tsx src/runtime/config/definition/schema.test.ts && tsx src/runtime/config/resolution/resolver.test.ts && tsx src/runtime/security/project-execution-trust.test.ts && tsx src/runtime/config/resolution/hooks.test.ts && tsx src/workspaces/state/project-context.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/config/external-mcp-registry.test.ts && tsx src/runtime/config/external-mcp-auth-store.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-oauth.test.ts && tsx src/cli/mcp/status.test.ts && tsx src/cli/mcp/diagnostics.test.ts && tsx src/cli/mcp/external-mcp.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/operations/project-trust.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/hooks/hooks-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-transform.test.ts && tsx src/mcp/hooks/external-mcp-transform-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-trust.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-context.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/mcp/server/operations/hooks.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/project-trust.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
71
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/config/schema-text.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/workspaces/relay/tests/composite.test.ts && tsx src/runtime/config/definition/schema.test.ts && tsx src/runtime/config/resolution/resolver.test.ts && tsx src/runtime/security/project-execution-trust.test.ts && tsx src/runtime/config/resolution/hooks.test.ts && tsx src/workspaces/state/project-context.test.ts && tsx src/runtime/config/config.test.ts && tsx src/cli/config/general.test.ts && tsx src/cli/config/domains/domain-cli.test.ts && node --test --import tsx src/cli/config/domains/context-cli.test.ts && node --test --import tsx src/cli/config/inspect.test.ts src/cli/config/migrate.test.ts && tsx src/runtime/config/external-mcp-registry.test.ts && tsx src/runtime/config/external-mcp-auth-store.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-oauth.test.ts && tsx src/cli/mcp/status.test.ts && tsx src/cli/mcp/diagnostics.test.ts && tsx src/cli/mcp/external-mcp.test.ts && tsx src/runtime/shell/command-shell-runtime.test.ts && tsx src/runtime/instructions/shell-instructions.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/cli/shell/setup.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/operations/project-trust.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/cli/system/status.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/hooks/hooks-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-transform.test.ts && tsx src/mcp/hooks/external-mcp-transform-trust.test.ts && tsx src/mcp/operations/external-mcp/external-mcp-trust.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-context.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/mcp/server/operations/hooks.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/project-trust.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
72
72
  "typecheck": "tsc -p tsconfig.json --noEmit",
73
73
  "release:check": "node scripts/release-version.mjs check",
74
74
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -5,6 +5,80 @@
5
5
  "$schema": {
6
6
  "description": "Editor-only JSON Schema URL. Ignored by ForgeRelay resolution.",
7
7
  "type": "string"
8
+ },
9
+ "systemInstructionsPath": {
10
+ "description": "Selected system-level Agent instruction file consumed by ForgeRelay.",
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "default": "~/.agents/AGENTS.md",
14
+ "x-forgerelay-scopes": [
15
+ "runtime",
16
+ "project-local",
17
+ "project",
18
+ "user",
19
+ "built-in"
20
+ ],
21
+ "x-forgerelay-merge": "replace",
22
+ "x-forgerelay-reload": "hot",
23
+ "x-forgerelay-sensitivity": "public",
24
+ "x-forgerelay-interpolation": "none",
25
+ "x-forgerelay-execution-effect": "none",
26
+ "x-forgerelay-runtime-override": {
27
+ "env": "FORGERELAY_SYSTEM_INSTRUCTIONS_PATH"
28
+ }
29
+ },
30
+ "instructionNames": {
31
+ "description": "Project instruction basenames discovered hierarchically within a Workspace.",
32
+ "type": "array",
33
+ "items": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "default": [
38
+ "AGENTS.md"
39
+ ],
40
+ "x-forgerelay-scopes": [
41
+ "runtime",
42
+ "project-local",
43
+ "project",
44
+ "user",
45
+ "built-in"
46
+ ],
47
+ "x-forgerelay-merge": "replace",
48
+ "x-forgerelay-reload": "hot",
49
+ "x-forgerelay-sensitivity": "public",
50
+ "x-forgerelay-interpolation": "none",
51
+ "x-forgerelay-execution-effect": "none",
52
+ "x-forgerelay-runtime-override": {
53
+ "env": "FORGERELAY_INSTRUCTION_NAMES"
54
+ }
55
+ },
56
+ "skillPaths": {
57
+ "description": "Ordered Agent Skill source directories; explicit higher-precedence lists replace lower-precedence lists.",
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string",
61
+ "minLength": 1
62
+ },
63
+ "default": [
64
+ "~/.agents/skills",
65
+ "./.agents/skills"
66
+ ],
67
+ "x-forgerelay-scopes": [
68
+ "runtime",
69
+ "project-local",
70
+ "project",
71
+ "user",
72
+ "built-in"
73
+ ],
74
+ "x-forgerelay-merge": "replace",
75
+ "x-forgerelay-reload": "hot",
76
+ "x-forgerelay-sensitivity": "public",
77
+ "x-forgerelay-interpolation": "none",
78
+ "x-forgerelay-execution-effect": "none",
79
+ "x-forgerelay-runtime-override": {
80
+ "env": "FORGERELAY_SKILL_PATHS"
81
+ }
8
82
  }
9
83
  },
10
84
  "additionalProperties": false,