@akira-tl/forgerelay 0.8.5 → 0.8.7

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 (63) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +8 -10
  3. package/dist/activity/audit-store.js +44 -6
  4. package/dist/activity/mcp-query-tools.js +53 -36
  5. package/dist/activity/query-service.js +41 -11
  6. package/dist/cli.js +16 -17
  7. package/dist/composite-activity.js +124 -33
  8. package/dist/config.js +8 -13
  9. package/dist/db/migrations.js +9 -0
  10. package/dist/db/schema.js +1 -0
  11. package/dist/lsp/test-support/server-fixture.js +7 -7
  12. package/dist/mcp/server-instructions.js +2 -2
  13. package/dist/process-sessions.js +2 -2
  14. package/dist/remote-auth.js +11 -1
  15. package/dist/remote-mcp-connection-pool.js +90 -0
  16. package/dist/remote-transport.js +28 -14
  17. package/dist/remote-workspace-relay.js +53 -23
  18. package/dist/server.js +105 -70
  19. package/dist/skills.js +1 -1
  20. package/dist/subagents/profiles.js +1 -2
  21. package/dist/subagents/providers/adapters/pi.js +2 -2
  22. package/dist/subagents/providers/availability.js +2 -2
  23. package/dist/subagents/providers/path.js +4 -4
  24. package/dist/ui/.vite/manifest.json +32 -32
  25. package/dist/ui/activity-panel-app.html +3 -3
  26. package/dist/ui/assets/{activity-panel-app-E1ju2dqI.js → activity-panel-app-CUAN6zyW.js} +1 -1
  27. package/dist/ui/assets/{heavy-payload-CeW-n9w5.js → heavy-payload-CgzrutLm.js} +1 -1
  28. package/dist/ui/assets/{review-payload-B9CO298v.js → review-payload-BrLbezbq.js} +1 -1
  29. package/dist/ui/assets/{scrollbar-C2twAENW.js → scrollbar-CbhpdW05.js} +1 -1
  30. package/dist/ui/assets/workspace-app-Bhj96tsR.js +1 -0
  31. package/dist/ui/assets/{workspace-app-CwbJnb_w.js → workspace-app-CxwJuZyS.js} +1 -1
  32. package/dist/ui/assets/{workspace-app-BztEvZIC.js → workspace-app-D6UR0AFl.js} +3 -3
  33. package/dist/ui/assets/workspace-app-ldjBmCJR.css +1 -0
  34. package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +1 -0
  35. package/dist/ui/workspace-app.html +4 -4
  36. package/dist/ui/workspace-lifecycle-app.html +4 -4
  37. package/dist/user-config.js +3 -19
  38. package/dist/workspace-presentation.js +69 -0
  39. package/dist/workspace-store.js +29 -1
  40. package/dist/workspaces.js +59 -19
  41. package/docs/agent-profile-schema.md +4 -9
  42. package/docs/artifact-exchange.md +2 -1
  43. package/docs/chatgpt-coding-workflow.md +21 -19
  44. package/docs/configuration.md +34 -39
  45. package/docs/gotchas.md +10 -7
  46. package/docs/roadmap.md +11 -4
  47. package/docs/security.md +3 -2
  48. package/docs/setup.md +8 -5
  49. package/docs/versioning.md +1 -1
  50. package/package.json +3 -2
  51. package/scripts/debug/accept.mjs +7 -1
  52. package/scripts/debug/relay-accept.mjs +0 -1
  53. package/scripts/debug/runtime.mjs +0 -4
  54. package/scripts/debug/runtime.test.mjs +0 -2
  55. package/scripts/debug/traffic/run.sh +5 -0
  56. package/scripts/debug/traffic/traffic-audit.mjs +907 -0
  57. package/scripts/release/push-ready.mjs +124 -0
  58. package/scripts/release/push-ready.test.mjs +108 -0
  59. package/scripts/release/release-gate.test.mjs +1 -0
  60. package/scripts/release-proof.mjs +16 -1
  61. package/dist/ui/assets/workspace-app-YnUST8IP.css +0 -1
  62. package/dist/ui/assets/workspace-app-rKuhdae8.js +0 -1
  63. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +0 -1
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ForgeRelay Diff</title>
7
- <script type="module" crossorigin src="./assets/workspace-app-rKuhdae8.js"></script>
7
+ <script type="module" crossorigin src="./assets/workspace-app-Bhj96tsR.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/chunk-EyZ2wyi3.js">
9
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-CwbJnb_w.js">
10
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-BztEvZIC.js">
11
- <link rel="stylesheet" crossorigin href="./assets/workspace-app-YnUST8IP.css">
9
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-CxwJuZyS.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-D6UR0AFl.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/workspace-app-ldjBmCJR.css">
12
12
  </head>
13
13
  <body>
14
14
  <main id="app" class="shell">
@@ -4,11 +4,11 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ForgeRelay Workspace Lifecycle</title>
7
- <script type="module" crossorigin src="./assets/workspace-lifecycle-app-CEfMdudP.js"></script>
7
+ <script type="module" crossorigin src="./assets/workspace-lifecycle-app-Cqfhx9pV.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/chunk-EyZ2wyi3.js">
9
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-CwbJnb_w.js">
10
- <link rel="modulepreload" crossorigin href="./assets/workspace-app-BztEvZIC.js">
11
- <link rel="stylesheet" crossorigin href="./assets/workspace-app-YnUST8IP.css">
9
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-CxwJuZyS.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/workspace-app-D6UR0AFl.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/workspace-app-ldjBmCJR.css">
12
12
  </head>
13
13
  <body>
14
14
  <main id="app" class="shell">
@@ -9,14 +9,10 @@ const AUTH_LOCK_TIMEOUT_MS = 5_000;
9
9
  const AUTH_LOCK_STALE_MS = 30_000;
10
10
  const AUTH_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
11
11
  export function forgerelayConfigDir(env = process.env) {
12
- const explicit = env.FORGERELAY_CONFIG_DIR ?? env.DEVSPACE_CONFIG_DIR;
12
+ const explicit = env.FORGERELAY_CONFIG_DIR;
13
13
  if (explicit)
14
14
  return resolve(expandHomePath(explicit));
15
- const current = join(homedir(), ".forgerelay");
16
- const legacy = join(homedir(), ".devspace");
17
- if (existsSync(current) || !existsSync(legacy))
18
- return resolve(current);
19
- return resolve(legacy);
15
+ return resolve(join(homedir(), ".forgerelay"));
20
16
  }
21
17
  export function forgerelayConfigPath(env = process.env) {
22
18
  return join(forgerelayConfigDir(env), "config.json");
@@ -56,7 +52,6 @@ export function loadForgeRelayFiles(env = process.env) {
56
52
  auth: authExists ? readJsonFile(authPath) : {},
57
53
  hooks: hooksExists ? readJsonFile(hooksPath) : {},
58
54
  hookFiles: readHookFiles(join(dir, "hooks")),
59
- usingLegacyDir: dir === resolve(join(homedir(), ".devspace")),
60
55
  };
61
56
  }
62
57
  export function writeForgeRelayConfig(config, env = process.env) {
@@ -153,22 +148,11 @@ export function ensureForgeRelayDefaultSkills(env = process.env) {
153
148
  return [targetPath];
154
149
  }
155
150
  export function resolveSubagentsFlag(config, env = process.env) {
156
- const value = env.FORGERELAY_SUBAGENTS ?? env.DEVSPACE_SUBAGENTS;
151
+ const value = env.FORGERELAY_SUBAGENTS;
157
152
  if (value === undefined)
158
153
  return config.subagents;
159
154
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
160
155
  }
161
- // Legacy exported names remain temporarily so existing integrations and tests do not
162
- // break while the public product surface migrates to ForgeRelay.
163
- export const devspaceConfigDir = forgerelayConfigDir;
164
- export const devspaceConfigPath = forgerelayConfigPath;
165
- export const devspaceAuthPath = forgerelayAuthPath;
166
- export const devspaceSkillsDir = forgerelaySkillsDir;
167
- export const devspaceAgentsDir = forgerelayAgentsDir;
168
- export const loadDevspaceFiles = loadForgeRelayFiles;
169
- export const writeDevspaceConfig = writeForgeRelayConfig;
170
- export const writeDevspaceAuth = writeForgeRelayAuth;
171
- export const ensureDevspaceDefaultSkills = ensureForgeRelayDefaultSkills;
172
156
  function readHookFiles(directory) {
173
157
  if (!existsSync(directory))
174
158
  return {};
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ const PRESENTATION_FIELDS = [
3
+ "workspaceId",
4
+ "kind",
5
+ "name",
6
+ "members",
7
+ "root",
8
+ "path",
9
+ "mode",
10
+ "sourceRoot",
11
+ "workspaceReused",
12
+ "includeBootstrapContext",
13
+ "worktree",
14
+ "summary",
15
+ ];
16
+ export function compactWorkspacePresentation(card) {
17
+ const presentation = {};
18
+ for (const field of PRESENTATION_FIELDS) {
19
+ const value = card[field];
20
+ if (value !== undefined)
21
+ presentation[field] = value;
22
+ }
23
+ assignProjectedArray(presentation, "agentsFiles", card.agentsFiles, (entry) => pickFields(entry, ["path"]));
24
+ assignProjectedArray(presentation, "availableAgentsFiles", card.availableAgentsFiles, (entry) => pickFields(entry, ["path"]));
25
+ assignProjectedArray(presentation, "skills", card.skills, (entry) => pickFields(entry, ["name"]));
26
+ assignProjectedArray(presentation, "agentProviders", card.agentProviders, (entry) => pickFields(entry, ["name", "available", "reason"]));
27
+ assignProjectedArray(presentation, "agents", card.agents, (entry) => pickFields(entry, [
28
+ "name",
29
+ "provider",
30
+ "model",
31
+ "providerAvailable",
32
+ ]));
33
+ presentation.presentationRevision = presentationRevision(card, presentation);
34
+ return presentation;
35
+ }
36
+ function assignProjectedArray(target, field, value, project) {
37
+ if (!Array.isArray(value))
38
+ return;
39
+ const projected = value.flatMap((entry) => {
40
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
41
+ return [];
42
+ const item = project(entry);
43
+ return Object.keys(item).length > 0 ? [item] : [];
44
+ });
45
+ target[field] = projected;
46
+ }
47
+ function pickFields(value, fields) {
48
+ const projected = {};
49
+ for (const field of fields) {
50
+ const entry = value[field];
51
+ if (typeof entry === "string" ||
52
+ typeof entry === "boolean" ||
53
+ typeof entry === "number") {
54
+ projected[field] = entry;
55
+ }
56
+ }
57
+ return projected;
58
+ }
59
+ function presentationRevision(card, presentation) {
60
+ if (typeof card.presentationRevision === "string" && card.presentationRevision.length > 0) {
61
+ return card.presentationRevision;
62
+ }
63
+ if (typeof card.contextFingerprint === "string" && card.contextFingerprint.length > 0) {
64
+ return card.contextFingerprint;
65
+ }
66
+ return createHash("sha256")
67
+ .update(JSON.stringify(presentation))
68
+ .digest("hex");
69
+ }
@@ -266,9 +266,18 @@ export class SqliteWorkspaceStore {
266
266
  }
267
267
  setContextDelivery(input) {
268
268
  const deliveredAt = new Date().toISOString();
269
+ const componentFingerprintsJson = input.componentFingerprints
270
+ ? JSON.stringify(input.componentFingerprints)
271
+ : null;
269
272
  const row = this.database.db
270
273
  .insert(workspaceContextDeliveries)
271
- .values({ ...input, deliveredAt })
274
+ .values({
275
+ conversationScopeId: input.conversationScopeId,
276
+ targetKey: input.targetKey,
277
+ contextFingerprint: input.contextFingerprint,
278
+ componentFingerprintsJson,
279
+ deliveredAt,
280
+ })
272
281
  .onConflictDoUpdate({
273
282
  target: [
274
283
  workspaceContextDeliveries.conversationScopeId,
@@ -276,6 +285,7 @@ export class SqliteWorkspaceStore {
276
285
  ],
277
286
  set: {
278
287
  contextFingerprint: input.contextFingerprint,
288
+ componentFingerprintsJson,
279
289
  deliveredAt,
280
290
  },
281
291
  })
@@ -388,10 +398,28 @@ function rowToWorkspaceConversationBinding(row) {
388
398
  };
389
399
  }
390
400
  function rowToWorkspaceContextDelivery(row) {
401
+ const componentFingerprints = parseContextComponentFingerprints(row.componentFingerprintsJson);
391
402
  return {
392
403
  conversationScopeId: row.conversationScopeId,
393
404
  targetKey: row.targetKey,
394
405
  contextFingerprint: row.contextFingerprint,
406
+ ...(componentFingerprints ? { componentFingerprints } : {}),
395
407
  deliveredAt: row.deliveredAt,
396
408
  };
397
409
  }
410
+ function parseContextComponentFingerprints(value) {
411
+ if (!value)
412
+ return undefined;
413
+ try {
414
+ const parsed = JSON.parse(value);
415
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
416
+ return undefined;
417
+ const entries = Object.entries(parsed);
418
+ if (entries.some(([, fingerprint]) => typeof fingerprint !== "string"))
419
+ return undefined;
420
+ return Object.fromEntries(entries);
421
+ }
422
+ catch {
423
+ return undefined;
424
+ }
425
+ }
@@ -190,30 +190,35 @@ export class WorkspaceRegistry {
190
190
  : await this.reusedWorkspaceContext(await this.workspaceForOpen(workspaceId));
191
191
  const workspace = context.workspace;
192
192
  if (!conversationScopeId || !this.store) {
193
+ const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, []);
193
194
  return {
194
195
  ...context,
195
- includeBootstrapContext: bootstrapContext !== "none",
196
+ bootstrapContextComponents,
197
+ includeBootstrapContext: bootstrapContextComponents.length > 0,
196
198
  };
197
199
  }
198
200
  const targetKeys = await this.workspaceTargetKeys(workspace);
199
- const contextAlreadyDelivered = targetKeys.some((targetKey) => this.store?.getContextDelivery(conversationScopeId, targetKey)?.contextFingerprint ===
200
- context.contextFingerprint);
201
- const includeBootstrapContext = resolveBootstrapContextVisibility(bootstrapContext, contextAlreadyDelivered);
201
+ const deliveries = targetKeys
202
+ .map((targetKey) => this.store?.getContextDelivery(conversationScopeId, targetKey))
203
+ .filter((delivery) => delivery !== undefined);
204
+ const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, deliveries, context.contextFingerprint);
205
+ const includeBootstrapContext = bootstrapContextComponents.length > 0;
202
206
  for (const targetKey of targetKeys) {
203
207
  this.store.setConversationBinding({
204
208
  conversationScopeId,
205
209
  targetKey,
206
210
  workspaceSessionId: workspace.id,
207
211
  });
208
- if (includeBootstrapContext) {
212
+ if (bootstrapContext !== "none" && (includeBootstrapContext || deliveries.some((delivery) => delivery.contextFingerprint === context.contextFingerprint && !delivery.componentFingerprints))) {
209
213
  this.store.setContextDelivery({
210
214
  conversationScopeId,
211
215
  targetKey,
212
216
  contextFingerprint: context.contextFingerprint,
217
+ componentFingerprints: context.bootstrapComponentFingerprints,
213
218
  });
214
219
  }
215
220
  }
216
- return { ...context, includeBootstrapContext };
221
+ return { ...context, bootstrapContextComponents, includeBootstrapContext };
217
222
  }
218
223
  async listStaleWorkspaces(workspace) {
219
224
  if (!this.store)
@@ -483,26 +488,32 @@ export class WorkspaceRegistry {
483
488
  }
484
489
  withConversationContext(context, conversationScopeId, targetKey, bootstrapContext) {
485
490
  if (!conversationScopeId || !this.store) {
491
+ const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, []);
486
492
  return {
487
493
  ...context,
488
- includeBootstrapContext: bootstrapContext !== "none",
494
+ bootstrapContextComponents,
495
+ includeBootstrapContext: bootstrapContextComponents.length > 0,
489
496
  };
490
497
  }
491
498
  const delivery = this.store.getContextDelivery(conversationScopeId, targetKey);
492
- const includeBootstrapContext = resolveBootstrapContextVisibility(bootstrapContext, delivery?.contextFingerprint === context.contextFingerprint);
499
+ const bootstrapContextComponents = resolveBootstrapContextComponents(bootstrapContext, context.bootstrapComponentFingerprints, delivery ? [delivery] : [], context.contextFingerprint);
500
+ const includeBootstrapContext = bootstrapContextComponents.length > 0;
493
501
  this.store.setConversationBinding({
494
502
  conversationScopeId,
495
503
  targetKey,
496
504
  workspaceSessionId: context.workspace.id,
497
505
  });
498
- if (includeBootstrapContext) {
506
+ if (bootstrapContext !== "none" &&
507
+ (includeBootstrapContext ||
508
+ (delivery?.contextFingerprint === context.contextFingerprint && !delivery.componentFingerprints))) {
499
509
  this.store.setContextDelivery({
500
510
  conversationScopeId,
501
511
  targetKey,
502
512
  contextFingerprint: context.contextFingerprint,
513
+ componentFingerprints: context.bootstrapComponentFingerprints,
503
514
  });
504
515
  }
505
- return { ...context, includeBootstrapContext };
516
+ return { ...context, bootstrapContextComponents, includeBootstrapContext };
506
517
  }
507
518
  pruneIdleWorkspaceSessions(protectedWorkspaceIds, force = false) {
508
519
  if (!this.store)
@@ -691,12 +702,14 @@ export class WorkspaceRegistry {
691
702
  workspace.loadedInstructionRealPaths.clear();
692
703
  const agentsFiles = await this.loadInitialAgentsFiles(workspace);
693
704
  const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
694
- const contextFingerprint = bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles);
705
+ const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
695
706
  return {
696
707
  workspace,
697
708
  agentsFiles,
698
709
  availableAgentsFiles,
699
710
  contextFingerprint,
711
+ bootstrapComponentFingerprints,
712
+ bootstrapContextComponents: [...BOOTSTRAP_CONTEXT_COMPONENTS],
700
713
  hookReports: [],
701
714
  workspaceReused: true,
702
715
  includeBootstrapContext: true,
@@ -994,12 +1007,14 @@ export class WorkspaceRegistry {
994
1007
  });
995
1008
  const agentsFiles = await this.loadInitialAgentsFiles(workspace);
996
1009
  const availableAgentsFiles = await this.findAvailableAgentsFiles(workspace, agentsFiles);
997
- const contextFingerprint = bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles);
1010
+ const { contextFingerprint, componentFingerprints: bootstrapComponentFingerprints, } = bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles);
998
1011
  return {
999
1012
  workspace,
1000
1013
  agentsFiles,
1001
1014
  availableAgentsFiles,
1002
1015
  contextFingerprint,
1016
+ bootstrapComponentFingerprints,
1017
+ bootstrapContextComponents: [...BOOTSTRAP_CONTEXT_COMPONENTS],
1003
1018
  hookReports,
1004
1019
  workspaceReused: false,
1005
1020
  includeBootstrapContext: true,
@@ -1152,14 +1167,28 @@ export class WorkspaceRegistry {
1152
1167
  return loaded;
1153
1168
  }
1154
1169
  }
1155
- function resolveBootstrapContextVisibility(mode, contextAlreadyDelivered) {
1156
- if (mode === "full")
1157
- return true;
1170
+ const BOOTSTRAP_CONTEXT_COMPONENTS = [
1171
+ "agentsFiles",
1172
+ "availableAgentsFiles",
1173
+ "skills",
1174
+ "skillDiagnostics",
1175
+ "capabilityGuides",
1176
+ "agentProfiles",
1177
+ ];
1178
+ function resolveBootstrapContextComponents(mode, currentFingerprints, deliveries, contextFingerprint) {
1158
1179
  if (mode === "none")
1159
- return false;
1160
- return !contextAlreadyDelivered;
1180
+ return [];
1181
+ if (mode === "full")
1182
+ return [...BOOTSTRAP_CONTEXT_COMPONENTS];
1183
+ if (deliveries.length === 0)
1184
+ return [...BOOTSTRAP_CONTEXT_COMPONENTS];
1185
+ if (contextFingerprint &&
1186
+ deliveries.some((delivery) => !delivery.componentFingerprints && delivery.contextFingerprint === contextFingerprint)) {
1187
+ return [];
1188
+ }
1189
+ return BOOTSTRAP_CONTEXT_COMPONENTS.filter((component) => !deliveries.some((delivery) => delivery.componentFingerprints?.[component] === currentFingerprints[component]));
1161
1190
  }
1162
- function bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFiles) {
1191
+ function bootstrapContextFingerprints(workspace, agentsFiles, availableAgentsFiles) {
1163
1192
  const payload = {
1164
1193
  agentsFiles: agentsFiles
1165
1194
  .map((file) => ({ path: resolve(file.path), content: file.content }))
@@ -1195,7 +1224,18 @@ function bootstrapContextFingerprint(workspace, agentsFiles, availableAgentsFile
1195
1224
  }))
1196
1225
  .sort((left, right) => left.name.localeCompare(right.name)),
1197
1226
  };
1198
- return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
1227
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1228
+ return {
1229
+ contextFingerprint: hash(payload),
1230
+ componentFingerprints: {
1231
+ agentsFiles: hash(payload.agentsFiles),
1232
+ availableAgentsFiles: hash(payload.availableAgentsFiles),
1233
+ skills: hash(payload.skills),
1234
+ skillDiagnostics: hash(payload.skillDiagnostics),
1235
+ capabilityGuides: hash(payload.capabilityGuides),
1236
+ agentProfiles: hash(payload.agentProfiles),
1237
+ },
1238
+ };
1199
1239
  }
1200
1240
  function canonicalPersistedWorkspacePath(path) {
1201
1241
  const missingSegments = [];
@@ -13,15 +13,10 @@ New locations:
13
13
  .forgerelay/agents/*.md
14
14
  ```
15
15
 
16
- Migration compatibility:
17
-
18
- ```text
19
- ~/.devspace/agents/*.md
20
- .devspace/agents/*.md
21
- ```
22
-
23
- When an existing legacy config directory is reused, its global `agents` folder
24
- remains active automatically.
16
+ Rename-era DevSpace profile discovery has ended. `~/.devspace/agents/*.md` and
17
+ `.devspace/agents/*.md` are not scanned automatically. Move older profiles into
18
+ a canonical ForgeRelay location, or temporarily point `FORGERELAY_CONFIG_DIR`
19
+ at the old global config directory while migrating.
25
20
 
26
21
  ## Example
27
22
 
@@ -9,7 +9,8 @@ Enable the capability with:
9
9
  FORGERELAY_ARTIFACTS=1 forgerelay serve
10
10
  ```
11
11
 
12
- The legacy `DEVSPACE_ARTIFACTS` variable remains a fallback during migration.
12
+ Use the canonical `FORGERELAY_ARTIFACTS` variable; the old
13
+ `DEVSPACE_ARTIFACTS` name is no longer read.
13
14
 
14
15
  ## Workflow
15
16
 
@@ -30,12 +30,14 @@ open_workspace(path="~/project")
30
30
  ```
31
31
 
32
32
  The default `context="auto"` keeps the first useful bootstrap while avoiding
33
- replay. ForgeRelay tracks delivered context by conversation plus canonical
34
- Workspace target and a content fingerprint, independently from persistent
35
- Workspace identity. Different conversations may reuse the same `workspaceId` while
36
- each receives the current bootstrap once. If loaded instruction contents or
37
- relevant Skill, Capability guide, profile, diagnostic, or nested-instruction
38
- metadata changes, the next automatic open returns the refreshed bootstrap.
33
+ replay. ForgeRelay tracks delivery by conversation plus canonical Workspace target,
34
+ with both an overall `contextFingerprint` and component fingerprints for loaded
35
+ instructions, nested-instruction discovery, Skills, Skill diagnostics, Capability
36
+ guides, and Subagent profiles. Different conversations may reuse the same
37
+ `workspaceId` while each receives the current bootstrap independently. After the
38
+ first full delivery, an automatic open returns only components that changed or were
39
+ removed; an emptied component is returned as an empty array so the Host can clear
40
+ stale state without replaying unrelated bootstrap context.
39
41
 
40
42
  Two explicit controls are available for exceptional cases:
41
43
 
@@ -44,10 +46,10 @@ open_workspace(workspaceId="ws_...", context="full")
44
46
  open_workspace(workspaceId="ws_...", context="none")
45
47
  ```
46
48
 
47
- `full` forces a bootstrap refresh. `none` opens/resumes the Workspace without
48
- returning the full project context and does not record the current fingerprint as
49
- already delivered. Context-delivery state remains conversation-scoped and does not
50
- change the persistent Workspace identity.
49
+ `full` forces every bootstrap component to refresh. `none` opens/resumes the
50
+ Workspace without returning bootstrap components and does not acknowledge changed
51
+ component fingerprints as delivered. Context-delivery state remains
52
+ conversation-scoped and does not change the persistent Workspace identity.
51
53
 
52
54
  Do not enumerate Workspace state on every normal open. Use the same Core tool in
53
55
  inventory mode only when the user wants to discover known Workspaces, continue earlier
@@ -244,14 +246,15 @@ files in the skill directory.
244
246
 
245
247
  ## Local subagent profiles
246
248
 
247
- With `FORGERELAY_SUBAGENTS=1`, profiles are discovered from the active global
248
- config directory plus:
249
+ With `FORGERELAY_SUBAGENTS=1`, profiles are discovered from the active ForgeRelay
250
+ global config directory plus:
249
251
 
250
252
  ```text
251
253
  .forgerelay/agents/*.md
252
- .devspace/agents/*.md # migration compatibility
253
254
  ```
254
255
 
256
+ Rename-era `.devspace/agents` discovery has ended.
257
+
255
258
  The workspace result exposes only compact profile metadata so the host can
256
259
  choose a provider/profile without loading full provider launch details. Read the
257
260
  ForgeRelay-owned `subagents` capability guide when delegation is actually needed;
@@ -349,9 +352,8 @@ and keeps widget usage focused on workspace/change review.
349
352
 
350
353
  ## Legacy configuration
351
354
 
352
- `FORGERELAY_*` is the canonical environment-variable prefix. Equivalent
353
- `DEVSPACE_*` variables remain accepted as fallbacks during migration.
354
-
355
- Likewise, an existing `~/.devspace` configuration/state setup is reused when the
356
- new ForgeRelay location does not yet exist. See
357
- [Configuration Reference](configuration.md) for the compatibility rules.
355
+ `FORGERELAY_*` is the canonical environment-variable prefix. Rename-era
356
+ `DEVSPACE_*` fallbacks and automatic `~/.devspace` reuse have ended. Migrate
357
+ older installations explicitly; persisted internal identifiers that would
358
+ otherwise orphan state remain compatible. See
359
+ [Configuration Reference](configuration.md) for the current rules.
@@ -18,10 +18,10 @@ Override the directory with:
18
18
  FORGERELAY_CONFIG_DIR=/path/to/config npx @akira-tl/forgerelay serve
19
19
  ```
20
20
 
21
- For migration compatibility, `DEVSPACE_CONFIG_DIR` is accepted when
22
- `FORGERELAY_CONFIG_DIR` is unset. Without either variable, ForgeRelay uses
23
- `~/.forgerelay` unless that directory is absent and an existing `~/.devspace`
24
- directory is present; in that case the legacy directory is reused.
21
+ The rename-era automatic fallback has ended. `DEVSPACE_CONFIG_DIR` is ignored,
22
+ and the default directory is always `~/.forgerelay`. If an older installation
23
+ still has data in a DevSpace-named directory, migrate it explicitly or point
24
+ `FORGERELAY_CONFIG_DIR` at that directory during the migration.
25
25
 
26
26
  ## Commands
27
27
 
@@ -33,20 +33,11 @@ npx @akira-tl/forgerelay config get
33
33
  npx @akira-tl/forgerelay config set publicBaseUrl https://forge.example.com/forgerelay/main,https://forge-alt.example.com/relay
34
34
  ```
35
35
 
36
- ## Environment variable compatibility
36
+ ## Environment variables
37
37
 
38
- The public prefix is `FORGERELAY_*`.
39
-
40
- During the rename transition, the equivalent `DEVSPACE_*` variable remains a
41
- fallback when the ForgeRelay variable is unset. For example:
42
-
43
- ```text
44
- FORGERELAY_ALLOWED_ROOTS
45
- ↓ if unset
46
- DEVSPACE_ALLOWED_ROOTS
47
- ```
48
-
49
- When both are present, `FORGERELAY_*` wins.
38
+ The public prefix is `FORGERELAY_*`. Rename-era `DEVSPACE_*` environment
39
+ variables are no longer read. Migrate automation, service files, shell profiles,
40
+ and CI configuration to the canonical ForgeRelay names.
50
41
 
51
42
  ## Core variables
52
43
 
@@ -88,10 +79,10 @@ A single persisted string remains fully supported, so existing configs require n
88
79
  migration. For environment configuration, use a comma-separated list in
89
80
  `FORGERELAY_PUBLIC_BASE_URL`.
90
81
 
91
- If an existing legacy state/worktree directory is present and the new default is
92
- not, ForgeRelay reuses the legacy location rather than orphaning stored state.
93
- Persisted `stateDir` and `worktreeRoot` values in `config.json` also continue to
94
- win over defaults.
82
+ ForgeRelay no longer auto-detects DevSpace-named state or worktree directories.
83
+ Persisted `stateDir` and `worktreeRoot` values in `config.json` continue to win
84
+ over defaults, so an explicit configuration may temporarily point at an older
85
+ location while data is migrated.
95
86
 
96
87
  ## Native artifact download
97
88
 
@@ -145,8 +136,7 @@ MCP clients discover metadata from:
145
136
  | `codex` | Experimental Codex-shaped compatibility adapter using `open_workspace`, `close_workspace`, `read`, `rename`, `delete`, `apply_patch`, `exec_command`, `write_stdin`, and `capability`. It does not define the ForgeRelay canonical interface. |
146
137
 
147
138
  `FORGERELAY_MINIMAL_TOOLS` remains a compatibility-style boolean alias when the
148
- explicit tool mode is unset. The corresponding legacy `DEVSPACE_*` names are
149
- also accepted.
139
+ explicit tool mode is unset. DevSpace-prefixed equivalents are no longer read.
150
140
 
151
141
  `minimal` and `full` now resolve to the same regular 9-tool `tools/list`; `full`
152
142
  is retained only as a configuration-compatibility value. `codex` selects a
@@ -297,16 +287,20 @@ deprecated compatibility input and no longer allocates another identity for the
297
287
  same physical target; use `newWorktree: true` for genuinely separate Git isolation.
298
288
 
299
289
  Bootstrap delivery is tracked separately from Workspace identity.
300
- `open_workspace` defaults to `context="auto"`: ForgeRelay fingerprints the current
301
- project context and returns the full AGENTS/Skills/Capability-guide/profile bootstrap
302
- only when that conversation has not already received the current fingerprint for
303
- the canonical workspace target. `context="full"` forces a refresh;
304
- `context="none"` opens or resumes the Workspace without returning the full bootstrap
305
- and does not mark the current fingerprint as delivered. Conversation-scoped
306
- bootstrap delivery therefore remains independent from the persistent Workspace
307
- identity: another conversation may reuse the same Workspace while independently
308
- receiving the current bootstrap once, and changed context produces a new fingerprint
309
- for the next `auto` open.
290
+ `open_workspace` defaults to `context="auto"`: ForgeRelay keeps the overall
291
+ `contextFingerprint` for change detection while also tracking fingerprints for the
292
+ individual bootstrap components (`agentsFiles`, nested-instruction discovery,
293
+ Skills, Skill diagnostics, Capability guides, and Subagent profiles). The first
294
+ useful open returns the complete bootstrap; later `auto` opens return only components
295
+ whose current fingerprint has not already been delivered to that conversation for
296
+ the canonical Workspace target. A changed component is returned as its complete
297
+ current value, including an empty array when previously delivered content was removed,
298
+ so Hosts can clear stale bootstrap state without receiving unrelated context again.
299
+ `context="full"` forces every component to be returned. `context="none"` opens or
300
+ resumes the Workspace without returning bootstrap components and does not acknowledge
301
+ new component fingerprints. Conversation-scoped bootstrap delivery therefore remains
302
+ independent from the persistent Workspace identity, and another conversation may reuse
303
+ the same Workspace while receiving its own current bootstrap state.
310
304
 
311
305
  Composite Workspaces use the same `open_workspace` entry point with
312
306
  `kind="composite"` and a human-readable `name`. They have no filesystem root of
@@ -347,8 +341,7 @@ remains durable.
347
341
  | --- | --- | --- |
348
342
  | `FORGERELAY_TASK_REMINDER_INTERVAL` | `30` | Successful semantic work calls between Task update reminders; `0` disables reminders. |
349
343
 
350
- The same value may be persisted as `taskReminderInterval` in `config.json`. The
351
- legacy-compatible `DEVSPACE_TASK_REMINDER_INTERVAL` environment name is also accepted.
344
+ The same value may be persisted as `taskReminderInterval` in `config.json`.
352
345
 
353
346
  Use `open_workspace(action="list")` only when the Agent needs lightweight inventory
354
347
  to discover known Workspaces, continue earlier work, or organize Workspace state. The
@@ -609,10 +602,12 @@ Standard Agent Skills are discovered from:
609
602
 
610
603
  When subagents are enabled, profiles are discovered from:
611
604
 
612
- - `~/.forgerelay/agents/*.md` for new installations;
613
- - project `.forgerelay/agents/*.md`;
614
- - active legacy config directory `~/.devspace/agents/*.md` when reused;
615
- - project `.devspace/agents/*.md` for migration compatibility.
605
+ - the active ForgeRelay config directory's `agents/*.md`;
606
+ - project `.forgerelay/agents/*.md`.
607
+
608
+ DevSpace-named profile directories are no longer discovered automatically. Move
609
+ those profiles or explicitly select their parent with `FORGERELAY_CONFIG_DIR`
610
+ during migration.
616
611
 
617
612
  The ForgeRelay-owned `subagents` capability guide teaches the current CLI
618
613
  workflow on demand:
package/docs/gotchas.md CHANGED
@@ -28,11 +28,10 @@ npm rebuild better-sqlite3
28
28
  npx @akira-tl/forgerelay doctor
29
29
  ```
30
30
 
31
- ## Existing DevSpace config is still being used
31
+ ## Existing DevSpace config stopped being used
32
32
 
33
- This is intentional migration behavior. If `~/.forgerelay` does not exist but
34
- `~/.devspace` does, ForgeRelay reuses the legacy config directory so existing
35
- OAuth/state configuration is not silently abandoned.
33
+ The automatic rename-era fallback has ended. ForgeRelay now defaults to
34
+ `~/.forgerelay` and ignores `DEVSPACE_*` environment variables.
36
35
 
37
36
  Check the resolved directory:
38
37
 
@@ -40,7 +39,8 @@ Check the resolved directory:
40
39
  forgerelay doctor
41
40
  ```
42
41
 
43
- To force a new location, set:
42
+ If an older installation still needs data from a DevSpace-named directory,
43
+ point the canonical setting at it explicitly while migrating:
44
44
 
45
45
  ```bash
46
46
  FORGERELAY_CONFIG_DIR="$HOME/.forgerelay" forgerelay init
@@ -135,7 +135,9 @@ New installs normally use:
135
135
  ~/.forgerelay/auth.json
136
136
  ```
137
137
 
138
- A migrated install may still use `~/.devspace/auth.json`.
138
+ ForgeRelay no longer discovers `~/.devspace/auth.json` automatically. Move the
139
+ credential file to the active ForgeRelay config directory or explicitly set
140
+ `FORGERELAY_CONFIG_DIR` while migrating.
139
141
 
140
142
  Regenerate setup intentionally with:
141
143
 
@@ -227,7 +229,8 @@ New profile locations include:
227
229
  .forgerelay/agents/*.md
228
230
  ```
229
231
 
230
- Legacy `.devspace/agents` paths remain supported.
232
+ Legacy `.devspace/agents` paths are no longer scanned automatically. Move
233
+ profiles into one of the canonical locations before upgrading.
231
234
 
232
235
  `forgerelay agents ls` lists sessions, not profile definitions. The compact
233
236
  profile catalog is returned through `open_workspace`.