@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.
- package/CHANGELOG.md +22 -0
- package/README.md +8 -10
- package/dist/activity/audit-store.js +44 -6
- package/dist/activity/mcp-query-tools.js +53 -36
- package/dist/activity/query-service.js +41 -11
- package/dist/cli.js +16 -17
- package/dist/composite-activity.js +124 -33
- package/dist/config.js +8 -13
- package/dist/db/migrations.js +9 -0
- package/dist/db/schema.js +1 -0
- package/dist/lsp/test-support/server-fixture.js +7 -7
- package/dist/mcp/server-instructions.js +2 -2
- package/dist/process-sessions.js +2 -2
- package/dist/remote-auth.js +11 -1
- package/dist/remote-mcp-connection-pool.js +90 -0
- package/dist/remote-transport.js +28 -14
- package/dist/remote-workspace-relay.js +53 -23
- package/dist/server.js +105 -70
- package/dist/skills.js +1 -1
- package/dist/subagents/profiles.js +1 -2
- package/dist/subagents/providers/adapters/pi.js +2 -2
- package/dist/subagents/providers/availability.js +2 -2
- package/dist/subagents/providers/path.js +4 -4
- package/dist/ui/.vite/manifest.json +32 -32
- package/dist/ui/activity-panel-app.html +3 -3
- package/dist/ui/assets/{activity-panel-app-E1ju2dqI.js → activity-panel-app-CUAN6zyW.js} +1 -1
- package/dist/ui/assets/{heavy-payload-CeW-n9w5.js → heavy-payload-CgzrutLm.js} +1 -1
- package/dist/ui/assets/{review-payload-B9CO298v.js → review-payload-BrLbezbq.js} +1 -1
- package/dist/ui/assets/{scrollbar-C2twAENW.js → scrollbar-CbhpdW05.js} +1 -1
- package/dist/ui/assets/workspace-app-Bhj96tsR.js +1 -0
- package/dist/ui/assets/{workspace-app-CwbJnb_w.js → workspace-app-CxwJuZyS.js} +1 -1
- package/dist/ui/assets/{workspace-app-BztEvZIC.js → workspace-app-D6UR0AFl.js} +3 -3
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +1 -0
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +1 -0
- package/dist/ui/workspace-app.html +4 -4
- package/dist/ui/workspace-lifecycle-app.html +4 -4
- package/dist/user-config.js +3 -19
- package/dist/workspace-presentation.js +69 -0
- package/dist/workspace-store.js +29 -1
- package/dist/workspaces.js +59 -19
- package/docs/agent-profile-schema.md +4 -9
- package/docs/artifact-exchange.md +2 -1
- package/docs/chatgpt-coding-workflow.md +21 -19
- package/docs/configuration.md +34 -39
- package/docs/gotchas.md +10 -7
- package/docs/roadmap.md +11 -4
- package/docs/security.md +3 -2
- package/docs/setup.md +8 -5
- package/docs/versioning.md +1 -1
- package/package.json +3 -2
- package/scripts/debug/accept.mjs +7 -1
- package/scripts/debug/relay-accept.mjs +0 -1
- package/scripts/debug/runtime.mjs +0 -4
- package/scripts/debug/runtime.test.mjs +0 -2
- package/scripts/debug/traffic/run.sh +5 -0
- package/scripts/debug/traffic/traffic-audit.mjs +907 -0
- package/scripts/release/push-ready.mjs +124 -0
- package/scripts/release/push-ready.test.mjs +108 -0
- package/scripts/release/release-gate.test.mjs +1 -0
- package/scripts/release-proof.mjs +16 -1
- package/dist/ui/assets/workspace-app-YnUST8IP.css +0 -1
- package/dist/ui/assets/workspace-app-rKuhdae8.js +0 -1
- 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-
|
|
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-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="./assets/workspace-app-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./assets/workspace-app-
|
|
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-
|
|
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-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="./assets/workspace-app-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./assets/workspace-app-
|
|
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">
|
package/dist/user-config.js
CHANGED
|
@@ -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
|
|
12
|
+
const explicit = env.FORGERELAY_CONFIG_DIR;
|
|
13
13
|
if (explicit)
|
|
14
14
|
return resolve(expandHomePath(explicit));
|
|
15
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/workspace-store.js
CHANGED
|
@@ -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({
|
|
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
|
+
}
|
package/dist/workspaces.js
CHANGED
|
@@ -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
|
-
|
|
196
|
+
bootstrapContextComponents,
|
|
197
|
+
includeBootstrapContext: bootstrapContextComponents.length > 0,
|
|
196
198
|
};
|
|
197
199
|
}
|
|
198
200
|
const targetKeys = await this.workspaceTargetKeys(workspace);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
494
|
+
bootstrapContextComponents,
|
|
495
|
+
includeBootstrapContext: bootstrapContextComponents.length > 0,
|
|
489
496
|
};
|
|
490
497
|
}
|
|
491
498
|
const delivery = this.store.getContextDelivery(conversationScopeId, targetKey);
|
|
492
|
-
const
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
|
1160
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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.
|
|
353
|
-
`DEVSPACE_*`
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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.
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
36
|
+
## Environment variables
|
|
37
37
|
|
|
38
|
-
The public prefix is `FORGERELAY_*`.
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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`.
|
|
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
|
-
-
|
|
613
|
-
- project `.forgerelay/agents/*.md
|
|
614
|
-
|
|
615
|
-
-
|
|
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
|
|
31
|
+
## Existing DevSpace config stopped being used
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
`~/.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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`.
|