@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 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
@@ -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 execute with the local user's authority; workspace containment does not make shell execution a sandbox. For action=run, yieldTimeMs is only the feedback wait (default 10000ms; 0 returns a processId immediately) and optional timeoutMs is the independent total execution limit. action=process polls/waits for incremental output, writes input, resizes a PTY, or interrupts by processId. Keep explicit waits below the Host request deadline; use 60000ms only when supported. Completed background results may be attached to a later result for the same workspaceId. Call ${toolNames.openWorkspace} first and pass workspaceId. Expose this capability only behind strong authentication.`,
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. If ${toolNames.shell} returns a running processId, use action=\"process\" to poll/wait/interact/resize/interrupt it. When only waiting, set yieldTimeMs instead of short polling; reuse processId if still running. Otherwise keep working and consume its later completion notice.`);
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
- ...(opened.includeBootstrapContext
1579
- ? {
1580
- capabilityGuides,
1581
- agentsFiles,
1582
- availableAgentsFiles,
1583
- skills,
1584
- agentProviders,
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 visibleSkills = includeBootstrapContext ? cardSkills : [];
2938
- const visibleSkillDiagnostics = includeBootstrapContext
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 = includeBootstrapContext ? capabilityGuides : [];
2942
- const visibleAgentProviders = includeBootstrapContext ? cardAgentProviders : [];
2943
- const visibleAgents = includeBootstrapContext ? cardAgents : [];
2944
- const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
2945
- const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
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
- "The complete project context is included because it has not yet been provided in this conversation or host context.",
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
- ...(includeBootstrapContext
3076
- ? {
3077
- capabilityGuides: visibleCapabilityGuides,
3078
- agentsFiles: loadedAgentsFiles,
3079
- availableAgentsFiles: availableAgentsFileOutputs,
3080
- skills: visibleSkills,
3081
- agentProviders: visibleAgentProviders,
3082
- agents: visibleAgents,
3083
- skillDiagnostics: visibleSkillDiagnostics,
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("Feedback window before returning. For action=run, use 0 for immediate background handoff; otherwise defaults to 10000ms. For action=process, polling defaults to 5000ms and interaction to 250ms."),
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()
@@ -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 = [];
@@ -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
@@ -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 fingerprints the current
291
- project context and returns the full AGENTS/Skills/Capability-guide/profile bootstrap
292
- only when that conversation has not already received the current fingerprint for
293
- the canonical workspace target. `context="full"` forces a refresh;
294
- `context="none"` opens or resumes the Workspace without returning the full bootstrap
295
- and does not mark the current fingerprint as delivered. Conversation-scoped
296
- bootstrap delivery therefore remains independent from the persistent Workspace
297
- identity: another conversation may reuse the same Workspace while independently
298
- receiving the current bootstrap once, and changed context produces a new fingerprint
299
- 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.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",