@akira-tl/forgerelay 0.8.6 → 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 +7 -0
- package/dist/db/migrations.js +9 -0
- package/dist/db/schema.js +1 -0
- package/dist/mcp/server-instructions.js +2 -2
- package/dist/server.js +35 -31
- package/dist/workspace-store.js +29 -1
- package/dist/workspaces.js +59 -19
- package/docs/chatgpt-coding-workflow.md +12 -10
- package/docs/configuration.md +14 -10
- package/docs/roadmap.md +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.7] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `open_workspace(context="auto")` now tracks bootstrap delivery per component and returns only changed or removed AGENTS/nested-instruction, Skill/diagnostic, Capability-guide, and Subagent-profile domains after the first full delivery; `full`, `none`, Composite member, Relay, and legacy whole-fingerprint delivery behavior remain compatible.
|
|
12
|
+
- Bash guidance now requires long `yieldTimeMs` waits for long-running or wait-only process operations, explicitly noting that completion returns immediately before the feedback window expires so Agents do not short-poll every few seconds.
|
|
13
|
+
|
|
7
14
|
## [0.8.6] - 2026-09-02
|
|
8
15
|
|
|
9
16
|
### Added
|
package/dist/db/migrations.js
CHANGED
|
@@ -79,6 +79,11 @@ const migrations = [
|
|
|
79
79
|
name: "workspace-session-aliases",
|
|
80
80
|
up: migrateWorkspaceSessionAliases,
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
version: 17,
|
|
84
|
+
name: "workspace-context-components",
|
|
85
|
+
up: migrateWorkspaceContextComponents,
|
|
86
|
+
},
|
|
82
87
|
];
|
|
83
88
|
export function migrateDatabase(sqlite) {
|
|
84
89
|
const migrate = sqlite.transaction(() => {
|
|
@@ -387,6 +392,10 @@ function migrateWorkspaceSessionAliases(sqlite) {
|
|
|
387
392
|
on workspace_session_aliases(workspace_session_id);
|
|
388
393
|
`);
|
|
389
394
|
}
|
|
395
|
+
function migrateWorkspaceContextComponents(sqlite) {
|
|
396
|
+
migrateWorkspaceContextDeliveries(sqlite);
|
|
397
|
+
addColumnIfMissing(sqlite, "workspace_context_deliveries", "component_fingerprints_json", "text");
|
|
398
|
+
}
|
|
390
399
|
function migrateActivityHostTurnWorkspace(sqlite) {
|
|
391
400
|
migrateActivityHostTurns(sqlite);
|
|
392
401
|
addColumnIfMissing(sqlite, "activity_host_turns", "workspace_id", "text");
|
package/dist/db/schema.js
CHANGED
|
@@ -53,6 +53,7 @@ export const workspaceContextDeliveries = sqliteTable("workspace_context_deliver
|
|
|
53
53
|
conversationScopeId: text("conversation_scope_id").notNull(),
|
|
54
54
|
targetKey: text("target_key").notNull(),
|
|
55
55
|
contextFingerprint: text("context_fingerprint").notNull(),
|
|
56
|
+
componentFingerprintsJson: text("component_fingerprints_json"),
|
|
56
57
|
deliveredAt: text("delivered_at").notNull(),
|
|
57
58
|
}, (table) => [
|
|
58
59
|
primaryKey({ columns: [table.conversationScopeId, table.targetKey] }),
|
|
@@ -30,7 +30,7 @@ export function buildToolDescriptions(config) {
|
|
|
30
30
|
rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
31
31
|
delete: `Delete one path or multiple paths inside an open workspace or the OS temp directory. Use path for one target or paths for multiple targets; a bulk Delete preflights all targets before deleting anything. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
32
32
|
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
33
|
-
shell: `Run or manage a shell process inside an open workspace.${shellSurface} Commands
|
|
33
|
+
shell: `Run or manage a shell process inside an open workspace.${shellSurface} Commands run with the local user's authority; workspace containment does not make shell execution a sandbox. For action=run, yieldTimeMs is the feedback wait (default 10000ms; 0 returns processId) and timeoutMs is the independent execution limit. action=process waits/interacts with processId. For long-running commands or wait-only process calls, set a long yieldTimeMs near the Host deadline (60000ms when supported), not short polling. This wait is a maximum: if the process finishes sooner, the call returns immediately. If still running, reuse processId with another long wait; use short waits only for interaction. Background completions can arrive later. Call ${toolNames.openWorkspace} first with workspaceId. Expose only behind strong authentication.`,
|
|
34
34
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
35
35
|
};
|
|
36
36
|
}
|
|
@@ -61,7 +61,7 @@ function defaultWorkflowInstructions(config) {
|
|
|
61
61
|
return `Use ${toolNames.read} for direct file reads, ${toolNames.rename} and ${toolNames.delete} for direct path moves or removals, apply_patch for content modifications, exec_command for inspection, tests, builds, and other commands, and ${toolNames.writeStdin} to poll or interact with running processes.`;
|
|
62
62
|
}
|
|
63
63
|
const inspection = `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
64
|
-
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, Git/package scripts, generators, formatters, and shell-suited commands.
|
|
64
|
+
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, Git/package scripts, generators, formatters, and shell-suited commands. For long ${toolNames.shell} commands or wait-only calls, set yieldTimeMs near Host deadline (60000ms when supported); do not poll every few seconds. Completion returns immediately if sooner; if still running, reuse processId with a long wait. Short waits are only for interaction.`);
|
|
65
65
|
}
|
|
66
66
|
function joinInstructions(...parts) {
|
|
67
67
|
return parts
|
package/dist/server.js
CHANGED
|
@@ -56,7 +56,7 @@ import { createWorkspaceStore } from "./workspace-store.js";
|
|
|
56
56
|
import { WorkspaceTaskReminderTracker } from "./workspace-task-reminders.js";
|
|
57
57
|
import { WorkspaceTaskStore } from "./workspace-tasks.js";
|
|
58
58
|
import { compactWorkspacePresentation } from "./workspace-presentation.js";
|
|
59
|
-
import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
|
|
59
|
+
import { formatAgentsPath, WorkspaceRegistry, } from "./workspaces.js";
|
|
60
60
|
import { formatAvailableSubagentProfile, summarizeSubagentProfile } from "./subagents/profiles.js";
|
|
61
61
|
import { formatSubagentProviderAvailabilitySummary, formatUnavailableSubagentProvider, getSubagentProviderAvailabilitySnapshot, } from "./subagents/providers/availability.js";
|
|
62
62
|
import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from "./subagents/sessions/mcp/audit.js";
|
|
@@ -1566,6 +1566,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1566
1566
|
providerUnavailableReason: availability?.reason,
|
|
1567
1567
|
};
|
|
1568
1568
|
});
|
|
1569
|
+
const bootstrapComponents = new Set(opened.bootstrapContextComponents);
|
|
1569
1570
|
return {
|
|
1570
1571
|
member: memberName,
|
|
1571
1572
|
workspaceId: compositeWorkspaceId,
|
|
@@ -1575,16 +1576,13 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1575
1576
|
capabilityFingerprint,
|
|
1576
1577
|
capabilityCatalog,
|
|
1577
1578
|
includeBootstrapContext: opened.includeBootstrapContext,
|
|
1578
|
-
...(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
agents,
|
|
1586
|
-
skillDiagnostics: redactSkillDiagnosticPaths(workspace.skillDiagnostics),
|
|
1587
|
-
}
|
|
1579
|
+
...(bootstrapComponents.has("capabilityGuides") ? { capabilityGuides } : {}),
|
|
1580
|
+
...(bootstrapComponents.has("agentsFiles") ? { agentsFiles } : {}),
|
|
1581
|
+
...(bootstrapComponents.has("availableAgentsFiles") ? { availableAgentsFiles } : {}),
|
|
1582
|
+
...(bootstrapComponents.has("skills") ? { skills } : {}),
|
|
1583
|
+
...(bootstrapComponents.has("agentProfiles") ? { agentProviders, agents } : {}),
|
|
1584
|
+
...(bootstrapComponents.has("skillDiagnostics")
|
|
1585
|
+
? { skillDiagnostics: redactSkillDiagnosticPaths(workspace.skillDiagnostics) }
|
|
1588
1586
|
: {}),
|
|
1589
1587
|
instruction: opened.includeBootstrapContext
|
|
1590
1588
|
? `Bootstrap context for Composite member ${memberName}. Keep using Composite workspaceId ${compositeWorkspaceId} and pass member=${memberName} for work operations.`
|
|
@@ -2888,7 +2886,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2888
2886
|
rememberWorkspacePanelState(opened.workspaceId, response);
|
|
2889
2887
|
return response;
|
|
2890
2888
|
}
|
|
2891
|
-
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
|
|
2889
|
+
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, bootstrapContextComponents, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
|
|
2892
2890
|
conversationScopeId,
|
|
2893
2891
|
protectedWorkspaceIds,
|
|
2894
2892
|
});
|
|
@@ -2934,15 +2932,18 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2934
2932
|
const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
|
|
2935
2933
|
path: formatAgentsPath(file.path, workspace.root),
|
|
2936
2934
|
}));
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
2935
|
+
const bootstrapComponents = new Set(bootstrapContextComponents);
|
|
2936
|
+
const visibleSkills = bootstrapComponents.has("skills") ? cardSkills : [];
|
|
2937
|
+
const visibleSkillDiagnostics = bootstrapComponents.has("skillDiagnostics")
|
|
2939
2938
|
? redactSkillDiagnosticPaths(workspace.skillDiagnostics)
|
|
2940
2939
|
: [];
|
|
2941
|
-
const visibleCapabilityGuides =
|
|
2942
|
-
const visibleAgentProviders =
|
|
2943
|
-
const visibleAgents =
|
|
2944
|
-
const loadedAgentsFiles =
|
|
2945
|
-
const availableAgentsFileOutputs =
|
|
2940
|
+
const visibleCapabilityGuides = bootstrapComponents.has("capabilityGuides") ? capabilityGuides : [];
|
|
2941
|
+
const visibleAgentProviders = bootstrapComponents.has("agentProfiles") ? cardAgentProviders : [];
|
|
2942
|
+
const visibleAgents = bootstrapComponents.has("agentProfiles") ? cardAgents : [];
|
|
2943
|
+
const loadedAgentsFiles = bootstrapComponents.has("agentsFiles") ? cardAgentsFiles : [];
|
|
2944
|
+
const availableAgentsFileOutputs = bootstrapComponents.has("availableAgentsFiles")
|
|
2945
|
+
? cardAvailableAgentsFiles
|
|
2946
|
+
: [];
|
|
2946
2947
|
const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
|
|
2947
2948
|
const workspaceManagementInstruction = "Use open_workspace(action=\"list\") for lightweight Workspace inventory. Use action=\"inspect\" with one known workspaceId for bounded read-only metadata without opening/resuming it. Explicitly open a Workspace before executing or mutating against it, and ask the user before close_workspace cleanup.";
|
|
2948
2949
|
const cardInstruction = config.skillsEnabled
|
|
@@ -2953,7 +2954,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2953
2954
|
? [
|
|
2954
2955
|
`Workspace already exists as ${workspace.id} for this directory.`,
|
|
2955
2956
|
"Reuse this workspaceId for subsequent tool calls.",
|
|
2956
|
-
|
|
2957
|
+
`Project bootstrap context components included in this response: ${bootstrapContextComponents.join(", ")}. Components not listed are unchanged and are not repeated.`,
|
|
2957
2958
|
workspaceContextInstruction,
|
|
2958
2959
|
workspaceManagementInstruction,
|
|
2959
2960
|
].join("\n\n")
|
|
@@ -3072,16 +3073,19 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
3072
3073
|
capabilityFingerprint,
|
|
3073
3074
|
contextFingerprint,
|
|
3074
3075
|
capabilityCatalog,
|
|
3075
|
-
...(
|
|
3076
|
-
? {
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
}
|
|
3076
|
+
...(bootstrapComponents.has("capabilityGuides")
|
|
3077
|
+
? { capabilityGuides: visibleCapabilityGuides }
|
|
3078
|
+
: {}),
|
|
3079
|
+
...(bootstrapComponents.has("agentsFiles") ? { agentsFiles: loadedAgentsFiles } : {}),
|
|
3080
|
+
...(bootstrapComponents.has("availableAgentsFiles")
|
|
3081
|
+
? { availableAgentsFiles: availableAgentsFileOutputs }
|
|
3082
|
+
: {}),
|
|
3083
|
+
...(bootstrapComponents.has("skills") ? { skills: visibleSkills } : {}),
|
|
3084
|
+
...(bootstrapComponents.has("agentProfiles")
|
|
3085
|
+
? { agentProviders: visibleAgentProviders, agents: visibleAgents }
|
|
3086
|
+
: {}),
|
|
3087
|
+
...(bootstrapComponents.has("skillDiagnostics")
|
|
3088
|
+
? { skillDiagnostics: visibleSkillDiagnostics }
|
|
3085
3089
|
: {}),
|
|
3086
3090
|
instruction,
|
|
3087
3091
|
},
|
|
@@ -4097,7 +4101,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
4097
4101
|
.min(0)
|
|
4098
4102
|
.max(300_000)
|
|
4099
4103
|
.optional()
|
|
4100
|
-
.describe("
|
|
4104
|
+
.describe("Maximum feedback wait, not a minimum delay: if the process finishes sooner, the call returns immediately. For long-running commands or wait-only action=process calls, set a long window near the Host request deadline (60000ms when supported) instead of repeated short polling. For action=run, use 0 for immediate background handoff; otherwise defaults to 10000ms. For action=process, wait-only calls default to 5000ms and interaction to 250ms."),
|
|
4101
4105
|
timeoutMs: z
|
|
4102
4106
|
.number()
|
|
4103
4107
|
.int()
|
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 = [];
|
|
@@ -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
|
package/docs/configuration.md
CHANGED
|
@@ -287,16 +287,20 @@ deprecated compatibility input and no longer allocates another identity for the
|
|
|
287
287
|
same physical target; use `newWorktree: true` for genuinely separate Git isolation.
|
|
288
288
|
|
|
289
289
|
Bootstrap delivery is tracked separately from Workspace identity.
|
|
290
|
-
`open_workspace` defaults to `context="auto"`: ForgeRelay
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
300
304
|
|
|
301
305
|
Composite Workspaces use the same `open_workspace` entry point with
|
|
302
306
|
`kind="composite"` and a human-readable `name`. They have no filesystem root of
|
package/docs/roadmap.md
CHANGED
|
@@ -305,7 +305,11 @@ must be published successfully before work begins on the next stage.
|
|
|
305
305
|
allowlist-based read-only inspection of other Workspaces and their safe Task
|
|
306
306
|
projections;
|
|
307
307
|
- **0.8.5** — verify the complete contract across Workspace Relay and publish the
|
|
308
|
-
accepted 0.8 lifecycle/Task model
|
|
308
|
+
accepted 0.8 lifecycle/Task model;
|
|
309
|
+
- **0.8.7** — make `open_workspace(context="auto")` bootstrap delivery component-level,
|
|
310
|
+
returning only changed/removed AGENTS, nested-instruction, Skill/diagnostic,
|
|
311
|
+
Capability-guide, or Subagent-profile domains while preserving `full`/`none`,
|
|
312
|
+
Composite member, Relay, and legacy delivery-record semantics.
|
|
309
313
|
|
|
310
314
|
The release boundary is part of the dependency graph, not just a documentation
|
|
311
315
|
milestone: the next stage remains blocked until the previous version's tag-triggered
|