@moretea-labs/forge 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -2,9 +2,9 @@ import { bindRepositoryEntities } from '../repositories/entity-migration';
2
2
  import { bootstrapLocalProject, diagnoseLatestLocalProjectSource } from '../repositories/local-project-onboarding';
3
3
  import { resolveEphemeralWorkspaceTarget } from '../repositories/ephemeral-workspace';
4
4
  import type { ResolvedExecutionIdentity } from '../../runtime/control-plane/execution/execution-identity';
5
- import { readExecutionSession } from '../../runtime/control-plane/execution/session-store';
5
+ import { assertNoBoundExecutionSessionMutation, resolveClaimedRepositoryWorkId, resolveExplicitClaimedRepositoryWork, type RepositoryWorkAttributionCaller } from '../../runtime/control-plane/execution/repository-work-attribution';
6
6
  import { getWorkContract } from '../../runtime/control-plane/facade/work-contract-store';
7
- import { getControllerSession, listControllerSessions } from '../../runtime/control-plane/facade/controller-session-store';
7
+ import { assertWorkPathsWithinScope } from '../../runtime/control-plane/execution/work-path-scope';
8
8
  import { isTerminalWorkContractStatus } from '../../runtime/control-plane/facade/types';
9
9
  import { executeRepositoryCommand, previewRepositoryCommandExecution } from '../repositories/command-executor';
10
10
  import { withControllerLock } from '../repositories/locks';
@@ -53,11 +53,12 @@ import {
53
53
  executeRepositoryCommandViaProcessRuntime,
54
54
  } from '../../runtime/execution/process-runtime/command-facade';
55
55
  import { assessWorkMode, parseExplicitTaskMode } from '../controller/work-mode';
56
+ import { classifyRepositoryCommand } from '../repositories/command-classifier';
56
57
  import { normalizeRepositoryCommand } from '../repositories/command-normalization';
57
58
  import { readRepositoryRange } from '../repository/inspector';
58
59
  import { getMcpPolicy } from './policy';
59
60
  import { redactMcpText } from './redaction';
60
- import type { CallToolResult, McpToolDefinition } from './tools';
61
+ import type { CallToolResult, McpToolDefinition } from './tool-contract';
61
62
  import {
62
63
  boundUtf8,
63
64
  compactCommandOutput,
@@ -287,87 +288,7 @@ function withResponseMeta(payload: Record<string, unknown>, startedAt: number):
287
288
  return response;
288
289
  }
289
290
 
290
- export interface RepositoryToolCallerContext {
291
- sessionId?: string;
292
- principalId?: string;
293
- controllerInstanceId?: string;
294
- }
295
-
296
- function claimedExplicitWork(
297
- controllerHome: string,
298
- repoId: string,
299
- caller: RepositoryToolCallerContext | undefined,
300
- requestedWorkId: string,
301
- ) {
302
- if (!caller?.principalId?.trim()) return undefined;
303
- const work = getWorkContract({ controllerHome, repoId }, requestedWorkId);
304
- if (!work || isTerminalWorkContractStatus(work.status)) throw new Error(`WORK_ATTRIBUTION_INVALID: ${requestedWorkId}`);
305
- const owner = getControllerSession({ controllerHome, repoId }, requestedWorkId);
306
- if (!owner) throw new Error(`WORK_CONTROLLER_CLAIM_REQUIRED: ${requestedWorkId}`);
307
- if ((owner.principalId?.trim() || owner.controllerId) !== caller.principalId.trim()) throw new Error(`WORK_CONTROLLER_OWNERSHIP_MISMATCH: ${requestedWorkId}`);
308
- return work;
309
- }
310
-
311
- function assertNoBoundExecutionSessionMutation(
312
- controllerHome: string,
313
- repository: ReturnType<typeof resolveRepositorySelection>,
314
- caller?: RepositoryToolCallerContext,
315
- ): void {
316
- if (!caller?.principalId?.trim() || !caller.sessionId?.trim()) return;
317
- const executionSession = readExecutionSession(controllerHome, {
318
- sessionId: caller.sessionId,
319
- principalId: caller.principalId,
320
- controllerInstanceId: caller.controllerInstanceId,
321
- });
322
- const workId = executionSession?.activeWorkId?.trim();
323
- if (!workId || executionSession?.activeRepositoryId !== repository.repoId) return;
324
- const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
325
- if (!work) throw new Error(`WORK_ATTRIBUTION_INVALID: ${workId}`);
326
- if (isTerminalWorkContractStatus(work.status)) {
327
- throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
328
- }
329
- throw new Error(`WORK_ATTRIBUTION_REQUIRED: ${work.workId}; active execution session mutations must pass work_id explicitly`);
330
- }
331
-
332
- function claimedSessionWorkId(
333
- controllerHome: string,
334
- repository: ReturnType<typeof resolveRepositorySelection>,
335
- caller?: RepositoryToolCallerContext,
336
- explicitWorkId?: unknown,
337
- ): string | undefined {
338
- const requestedWorkId = typeof explicitWorkId === 'string' ? explicitWorkId.trim() : '';
339
- if (requestedWorkId) {
340
- const work = claimedExplicitWork(controllerHome, repository.repoId, caller, requestedWorkId);
341
- if (!work) return undefined;
342
- if (work.checkoutId && work.checkoutId !== repository.activeCheckoutId) throw new Error(`WORK_CHECKOUT_MISMATCH: ${requestedWorkId}:${repository.activeCheckoutId}`);
343
- return requestedWorkId;
344
- }
345
- if (!caller?.principalId?.trim()) return undefined;
346
- if (caller.sessionId?.trim()) {
347
- const executionSession = readExecutionSession(controllerHome, {
348
- sessionId: caller.sessionId,
349
- principalId: caller.principalId,
350
- controllerInstanceId: caller.controllerInstanceId,
351
- });
352
- const workId = executionSession?.activeWorkId?.trim();
353
- if (workId && executionSession?.activeRepositoryId === repository.repoId && (!executionSession.activeCheckoutId || executionSession.activeCheckoutId === repository.activeCheckoutId)) {
354
- const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
355
- if (work && isTerminalWorkContractStatus(work.status)) {
356
- throw new Error(`WORK_ATTRIBUTION_TERMINAL: ${work.workId}:${work.status}`);
357
- }
358
- const owner = getControllerSession({ controllerHome, repoId: repository.repoId }, workId);
359
- if (work && owner?.sessionId === caller.sessionId && (owner.principalId?.trim() || owner.controllerId) === caller.principalId.trim()) return workId;
360
- }
361
- }
362
- const principal = caller.principalId.trim();
363
- const candidates = listControllerSessions({ controllerHome, repoId: repository.repoId })
364
- .filter((owner) => (owner.principalId?.trim() || owner.controllerId) === principal)
365
- .map((owner) => ({ owner, work: getWorkContract({ controllerHome, repoId: repository.repoId }, owner.workId) }))
366
- .filter((entry): entry is { owner: ReturnType<typeof listControllerSessions>[number]; work: NonNullable<ReturnType<typeof getWorkContract>> } => Boolean(entry.work && !isTerminalWorkContractStatus(entry.work.status) && (!entry.work.checkoutId || entry.work.checkoutId === repository.activeCheckoutId)));
367
- if (candidates.length === 1) return candidates[0].work.workId;
368
- if (candidates.length > 1) throw new Error(`WORK_ATTRIBUTION_AMBIGUOUS: principal ${principal} owns ${candidates.length} active Works on checkout ${repository.activeCheckoutId}`);
369
- return undefined;
370
- }
291
+ export type RepositoryToolCallerContext = RepositoryWorkAttributionCaller;
371
292
 
372
293
  export function claimedSessionEditBinding(
373
294
  controllerHome: string,
@@ -375,7 +296,7 @@ export function claimedSessionEditBinding(
375
296
  caller?: RepositoryToolCallerContext,
376
297
  explicitWorkId?: unknown,
377
298
  ): EditSessionBinding | undefined {
378
- const workId = claimedSessionWorkId(controllerHome, repository, caller, explicitWorkId);
299
+ const workId = resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId);
379
300
  if (!workId || !caller?.principalId?.trim()) return undefined;
380
301
  const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
381
302
  if (!work) return undefined;
@@ -423,7 +344,7 @@ function resolveRepositorySelectionForClaimedWork(
423
344
  allowSoleRepository: true,
424
345
  });
425
346
  if (!explicitWorkId || checkoutId) return repository;
426
- const work = claimedExplicitWork(controllerHome, repository.repoId, caller, explicitWorkId);
347
+ const work = resolveExplicitClaimedRepositoryWork(controllerHome, repository, caller, explicitWorkId);
427
348
  if (!work?.checkoutId || work.checkoutId === repository.activeCheckoutId) return repository;
428
349
  return resolveRepositorySelection({
429
350
  repoId: repository.repoId,
@@ -445,6 +366,32 @@ function rawDefaultBranchMergeCommand(repository: ReturnType<typeof resolveRepos
445
366
  return /(?:^|[;&|]\s*)git\s+merge\b/.test(normalized.shellCommand ?? '');
446
367
  }
447
368
 
369
+ interface HistoricalReadOnlyWorkContext {
370
+ workId: string;
371
+ status: string;
372
+ mode: 'historical_read_only_observation';
373
+ attribution: 'not_active_work';
374
+ }
375
+
376
+ function historicalReadOnlyWorkContext(
377
+ controllerHome: string,
378
+ repository: ReturnType<typeof resolveRepositorySelection>,
379
+ args: Record<string, unknown>,
380
+ ): HistoricalReadOnlyWorkContext | undefined {
381
+ const requestedWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
382
+ if (!requestedWorkId) return undefined;
383
+ const work = getWorkContract({ controllerHome, repoId: repository.repoId }, requestedWorkId);
384
+ if (!work || !isTerminalWorkContractStatus(work.status)) return undefined;
385
+ const classification = classifyRepositoryCommand(args.command as string | string[], repository.defaultBranch);
386
+ if (classification.risk !== 'readonly') return undefined;
387
+ return {
388
+ workId: work.workId,
389
+ status: work.status,
390
+ mode: 'historical_read_only_observation',
391
+ attribution: 'not_active_work',
392
+ };
393
+ }
394
+
448
395
  function resolveRepositoryCommandTarget(
449
396
  controllerHome: string,
450
397
  args: Record<string, unknown>,
@@ -454,6 +401,7 @@ function resolveRepositoryCommandTarget(
454
401
  repository: ReturnType<typeof resolveRepositorySelection>;
455
402
  executionIdentity: ResolvedExecutionIdentity;
456
403
  workspace?: { workspaceId: string; root: string; registered: false };
404
+ historicalWorkContext?: HistoricalReadOnlyWorkContext;
457
405
  } {
458
406
  const workspaceRoot = typeof args.workspace_root === 'string' ? args.workspace_root.trim() : '';
459
407
  const checkoutId = typeof args.checkout_id === 'string' ? args.checkout_id.trim() : '';
@@ -474,10 +422,37 @@ function resolveRepositoryCommandTarget(
474
422
  workspace: { workspaceId: target.workspaceId, root: target.canonicalRoot, registered: false },
475
423
  };
476
424
  }
425
+ // A terminal Work no longer owns mutation or checkout authority. An exact
426
+ // terminal work_id may still accompany a read-only observation immediately
427
+ // after finalize, so preserve it as explicit historical context while
428
+ // executing against the caller-selected/current repository checkout. Any
429
+ // non-read-only command continues through claimed Work resolution and fails
430
+ // closed for terminal attribution.
431
+ const selectedRepository = resolveRepositorySelection({
432
+ repoId: repoIdValue || undefined,
433
+ checkoutId: checkoutId || undefined,
434
+ controllerHome,
435
+ allowSoleRepository: true,
436
+ });
437
+ const historicalWorkContext = historicalReadOnlyWorkContext(controllerHome, selectedRepository, args);
438
+ if (historicalWorkContext) {
439
+ return {
440
+ repository: selectedRepository,
441
+ executionIdentity: {
442
+ schemaVersion: 1,
443
+ authority: 'repository',
444
+ repositoryId: selectedRepository.repoId,
445
+ checkoutId: selectedRepository.activeCheckoutId,
446
+ canonicalRoot: selectedRepository.canonicalRoot,
447
+ },
448
+ historicalWorkContext,
449
+ };
450
+ }
451
+
477
452
  const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
478
453
  const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
479
454
  const workId = explicitWorkId
480
- ? claimedSessionWorkId(controllerHome, repository, caller, explicitWorkId)
455
+ ? resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId)
481
456
  : undefined;
482
457
  return {
483
458
  repository,
@@ -615,6 +590,7 @@ function compactProcessCommandPayload(input: {
615
590
  repoId: string;
616
591
  checkoutId?: string;
617
592
  workspace?: { workspaceId: string; root: string; registered: false };
593
+ historicalWorkContext?: HistoricalReadOnlyWorkContext;
618
594
  processId?: string;
619
595
  process?: unknown;
620
596
  completed?: boolean;
@@ -653,6 +629,7 @@ function compactProcessCommandPayload(input: {
653
629
  repoId: input.repoId,
654
630
  ...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
655
631
  ...(input.workspace ? { workspace: input.workspace } : {}),
632
+ ...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
656
633
  ...(input.processId ? { processId: input.processId } : {}),
657
634
  ...(completed ? { exitCode: input.exitCode ?? (ok ? 0 : 1) } : {}),
658
635
  // A running default response otherwise loses its only dependency-bound
@@ -693,6 +670,7 @@ function compactProcessCommandPayload(input: {
693
670
  repoId: input.repoId,
694
671
  ...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
695
672
  ...(input.workspace ? { workspace: input.workspace } : {}),
673
+ ...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
696
674
  ...(input.processId ? { processId: input.processId } : {}),
697
675
  ok: input.ok,
698
676
  exitCode: input.exitCode,
@@ -998,6 +976,16 @@ export async function callRepositoryTool(
998
976
  case 'repository_safe_patch_apply': {
999
977
  const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
1000
978
  const binding = safePatchEditBinding(controllerHome, repository, caller, args);
979
+ if (binding?.workId) {
980
+ const work = getWorkContract({ controllerHome, repoId: repository.repoId }, binding.workId);
981
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${binding.workId}`);
982
+ const requestedPaths = buildSafePatchPlan(repository, { operations: args.operations, chunkSize: args.chunk_size })
983
+ .chunks.flatMap((chunk) => chunk.paths);
984
+ assertWorkPathsWithinScope(work, requestedPaths, {
985
+ forbidden: 'WORK_MUTATION_FORBIDDEN_PATH',
986
+ outOfScope: 'WORK_MUTATION_PATH_OUT_OF_SCOPE',
987
+ });
988
+ }
1001
989
  const applied = withControllerLock(
1002
990
  controllerHome,
1003
991
  { scope: 'repository', repoId: repository.repoId },
@@ -1098,7 +1086,7 @@ export async function callRepositoryTool(
1098
1086
  case 'repository_command_execute': {
1099
1087
  const explorationGuidance = fragmentedRepositoryExplorationGuidance(args.command);
1100
1088
  const target = resolveRepositoryCommandTarget(controllerHome, args, repoIdValue, caller);
1101
- const { repository, executionIdentity } = target;
1089
+ const { repository, executionIdentity, historicalWorkContext } = target;
1102
1090
  const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1103
1091
  if (!explicitWorkId) assertNoBoundExecutionSessionMutation(controllerHome, repository, caller);
1104
1092
  const deliveryWorkId = rawDefaultBranchMergeCommand(repository, args.command)
@@ -1206,6 +1194,13 @@ export async function callRepositoryTool(
1206
1194
  });
1207
1195
  }
1208
1196
  if (routeClass.route === 'process_direct' || routeClass.route === 'process_managed' || routeClass.route === 'durable') {
1197
+ const processRequestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
1198
+ if (executionIdentity.workId && !processRequestId) {
1199
+ throw new Error(
1200
+ `WORK_PROCESS_REQUEST_ID_REQUIRED: ${executionIdentity.workId}; `
1201
+ + 'Work-attributed Process Runtime commands require a stable request_id so reconnect/retry can resume the original Process without duplicate execution.',
1202
+ );
1203
+ }
1209
1204
  const processResult = await executeRepositoryCommandViaProcessRuntime({
1210
1205
  controllerHome,
1211
1206
  repository,
@@ -1217,7 +1212,7 @@ export async function callRepositoryTool(
1217
1212
  : typeof args.interactive_wait_ms === 'number' ? args.interactive_wait_ms : undefined,
1218
1213
  maxOutputBytes,
1219
1214
  returnHandleImmediately,
1220
- requestId: typeof args.request_id === 'string' ? args.request_id : undefined,
1215
+ requestId: processRequestId || undefined,
1221
1216
  workId: executionIdentity.workId,
1222
1217
  executionIdentity,
1223
1218
  allowNonGitWorkspace: target.workspace !== undefined,
@@ -1272,6 +1267,7 @@ export async function callRepositoryTool(
1272
1267
  repoId: repository.repoId,
1273
1268
  checkoutId: repository.activeCheckoutId,
1274
1269
  workspace: target.workspace,
1270
+ historicalWorkContext,
1275
1271
  processId: handle?.processId,
1276
1272
  process: detailLevel === 'detail' ? handle : undefined,
1277
1273
  completed,
@@ -13,7 +13,7 @@ import {
13
13
  type MultiRepositoryMcpToolContext,
14
14
  } from './multi-repository';
15
15
  import { callAccessTool } from './access-tools';
16
- import { callRepositoryTool } from './repository-tools';
16
+ import { callRepositoryToolWithPostFinalizeAttribution } from './post-finalize-work-attribution';
17
17
  import { callRuntimeTool } from '../../runtime/gateway/mcp/runtime-tools';
18
18
  import { callExecutionTool } from '../../runtime/gateway/mcp/execution-tools';
19
19
  import { callProcessTool } from '../../runtime/gateway/mcp/process-tools';
@@ -450,6 +450,36 @@ export function sameCanonicalRuntimeProxyIdentity(
450
450
  && left.runtimeInstanceId === right.runtimeInstanceId;
451
451
  }
452
452
 
453
+ export function canonicalRuntimeToolCallIsReplaySafe(name: string, args: Record<string, unknown>): boolean {
454
+ if (name !== 'repository_command_execute') return false;
455
+ const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
456
+ const requestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
457
+ return Boolean(workId && requestId);
458
+ }
459
+
460
+ export function canonicalRuntimeToolCallFailureIsTransient(error: unknown): boolean {
461
+ const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error ?? '');
462
+ return /Connection closed|socket[^\n]*closed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|UND_ERR_SOCKET|fetch failed|MCP_REQUEST_FAILED/i.test(message);
463
+ }
464
+
465
+ export async function callCanonicalRuntimeToolWithReplay<T>(input: {
466
+ name: string;
467
+ args: Record<string, unknown>;
468
+ call: () => Promise<T>;
469
+ reconnect: () => Promise<void>;
470
+ }): Promise<T> {
471
+ try {
472
+ return await input.call();
473
+ } catch (error) {
474
+ if (!canonicalRuntimeToolCallIsReplaySafe(input.name, input.args)
475
+ || !canonicalRuntimeToolCallFailureIsTransient(error)) {
476
+ throw error;
477
+ }
478
+ await input.reconnect();
479
+ return await input.call();
480
+ }
481
+ }
482
+
453
483
  export interface CanonicalRuntimeProxy {
454
484
  listTools(): Promise<{ tools: Tool[] }>;
455
485
  callTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>, timing?: MutableGatewayProxyTiming): Promise<CallToolResult>;
@@ -743,22 +773,36 @@ export function createCanonicalRuntimeProxy(ctx: MultiRepositoryMcpToolContext):
743
773
  }
744
774
  },
745
775
  async callTool(callerContext, name, args, timing = {}) {
746
- const { laneId, client } = await prepareLane(timing, name === 'process_wait' ? 'wait' : 'interactive');
776
+ const prepared = await prepareLane(timing, name === 'process_wait' ? 'wait' : 'interactive');
777
+ const laneId = prepared.laneId;
778
+ let activeClient = prepared.client;
747
779
  const gatewayCallStartedAtMs = Date.now();
748
780
  const callStarted = performance.now();
781
+ const forwardedRequest = {
782
+ name,
783
+ arguments: args,
784
+ _meta: canonicalRuntimeForwardingMeta(callerContext),
785
+ } as Parameters<Client['callTool']>[0];
749
786
  try {
750
787
  // The outer Connector session is already bound to the Canonical Runtime
751
788
  // schema and fenced by the Runtime-published fingerprint before dispatch.
752
789
  // Caller identity stays request-scoped while the inner MCP lane is reused.
753
- const response = await client.callTool(
754
- {
755
- name,
756
- arguments: args,
757
- _meta: canonicalRuntimeForwardingMeta(callerContext),
758
- } as Parameters<Client['callTool']>[0],
759
- undefined,
760
- { timeout: CANONICAL_RUNTIME_TOOL_CALL_TIMEOUT_MS },
761
- );
790
+ // Only keyed Work-attributed Process Runtime commands may replay after a
791
+ // transient inner disconnect: the stable request_id resolves to the
792
+ // original Process instead of spawning the side effect twice.
793
+ const response = await callCanonicalRuntimeToolWithReplay({
794
+ name,
795
+ args,
796
+ call: async () => await activeClient.callTool(
797
+ forwardedRequest,
798
+ undefined,
799
+ { timeout: CANONICAL_RUNTIME_TOOL_CALL_TIMEOUT_MS },
800
+ ),
801
+ reconnect: async () => {
802
+ await closeLane(laneId, activeClient);
803
+ activeClient = await clientForCurrentRuntime(laneId, timing);
804
+ },
805
+ });
762
806
  const result = response as unknown as CallToolResult;
763
807
  timing.gatewayProxyCallMs = roundedMs(performance.now() - callStarted);
764
808
  Object.assign(timing, deriveCanonicalForwardingTiming({
@@ -771,7 +815,7 @@ export function createCanonicalRuntimeProxy(ctx: MultiRepositoryMcpToolContext):
771
815
  timing.gatewayProxyCallMs = roundedMs(performance.now() - callStarted);
772
816
  // Poison only the failed lane. Other hot lanes remain available so one
773
817
  // Runtime/session failure cannot collapse the whole Gateway proxy pool.
774
- await closeLane(laneId, client);
818
+ await closeLane(laneId, activeClient);
775
819
  throw error;
776
820
  } finally {
777
821
  scheduler.release(laneId);
@@ -892,7 +936,7 @@ export function createForgeMcpServerFromContext(
892
936
  if (runtimeResult) return runtimeResult;
893
937
  const durableResult = await routeDurableMcpCall(ctx, name, args);
894
938
  if (durableResult) return durableResult;
895
- const repositoryResult = await callRepositoryTool(ctx.controllerHome, name, args, ctx);
939
+ const repositoryResult = await callRepositoryToolWithPostFinalizeAttribution(ctx.controllerHome, name, args, ctx);
896
940
  if (repositoryResult) return repositoryResult;
897
941
  return callMultiRepositoryTool(ctx, name, args);
898
942
  });
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ /** Transport-neutral MCP tool schema used by legacy, repository and Runtime surfaces. */
4
+ export interface McpToolDefinition {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: Record<string, unknown>;
8
+ annotations?: Record<string, unknown>;
9
+ }
10
+
11
+ /** Bounded text-first result contract shared by Forge MCP adapters. */
12
+ export type CallToolResult = Omit<SdkCallToolResult, 'content'> & {
13
+ content: Array<{ type: 'text'; text: string }>;
14
+ };
@@ -1,4 +1,4 @@
1
- import type { McpToolDefinition } from './tools';
1
+ import type { McpToolDefinition } from './tool-contract';
2
2
  import type { MultiRepositoryMcpToolContext } from './multi-repository';
3
3
  import { buildMultiRepositoryToolDefinitions } from './multi-repository';
4
4
  import { accessToolDefinitions } from './access-tools';
@@ -7,7 +7,7 @@ import {
7
7
  type ControllerAccessState,
8
8
  } from './access-mode';
9
9
  import { repositoryToolDefinitions } from './repository-tools';
10
- import { runtimeToolDefinitions } from '../../runtime/gateway/mcp/runtime-tools';
10
+ import { runtimeToolDefinitions } from '../../runtime/gateway/mcp/runtime-tool-definitions';
11
11
  import { executionToolDefinitions } from '../../runtime/gateway/mcp/execution-tools';
12
12
  import { processToolDefinitions } from '../../runtime/gateway/mcp/process-tools';
13
13
  import { injectDurableCommandFields } from '../../runtime/gateway/mcp/router';
@@ -1125,6 +1125,29 @@ export async function startMcpHttp(opts: McpHttpOptions): Promise<void> {
1125
1125
  });
1126
1126
  });
1127
1127
 
1128
+ // Recovery probes this endpoint every few seconds. Keep it strictly in-memory
1129
+ // and transport-scoped: whole-control-plane readiness below may traverse every
1130
+ // repository projection and probe the local bridge, which must never become a
1131
+ // periodic event-loop load generator for the public MCP Connector itself.
1132
+ app.get('/transport-ready', (_req, res) => {
1133
+ const sessionSnapshot = sessionRegistry.snapshot();
1134
+ const sessionCapacityReady = sessionSnapshot.acceptingNewSessions
1135
+ && runtimeStats.initializing < MAX_INITIALIZING_SESSIONS
1136
+ && runtimeStats.activePosts < MAX_ACTIVE_POSTS;
1137
+ res.status(sessionCapacityReady ? 200 : 503).json({
1138
+ ready: sessionCapacityReady,
1139
+ profile: toolContext.policy.profile,
1140
+ gateway: sessionCapacityReady ? 'ready' : 'saturated',
1141
+ sessionCapacity: sessionSnapshot,
1142
+ runtimeCapacity: {
1143
+ initializing: runtimeStats.initializing,
1144
+ maximumInitializing: MAX_INITIALIZING_SESSIONS,
1145
+ activePosts: runtimeStats.activePosts,
1146
+ maximumActivePosts: MAX_ACTIVE_POSTS,
1147
+ },
1148
+ });
1149
+ });
1150
+
1128
1151
  app.get('/ready', async (_req, res) => {
1129
1152
  const runtimeGeneration = currentRuntimeGeneration();
1130
1153
  const sessionSnapshot = sessionRegistry.snapshot();
@@ -42,7 +42,7 @@ const READ_ONLY_GIT_SUBCOMMANDS = new Set([
42
42
  'count-objects', 'verify-pack', 'check-ignore',
43
43
  ]);
44
44
 
45
- function shellSegments(command: string): string[] {
45
+ export function shellSegments(command: string): string[] {
46
46
  const segments: string[] = [];
47
47
  let current = '';
48
48
  let quote: "'" | '"' | undefined;
@@ -87,7 +87,7 @@ function shellSegments(command: string): string[] {
87
87
  return segments;
88
88
  }
89
89
 
90
- function shellWordsPreservingQuotes(segment: string): string[] {
90
+ export function shellWordsPreservingQuotes(segment: string): string[] {
91
91
  const words: string[] = [];
92
92
  let current = '';
93
93
  let quote: "'" | '"' | undefined;
@@ -72,6 +72,8 @@ export interface ExecuteRepositoryCommandInput {
72
72
  allowNonGitWorkspace?: boolean;
73
73
  /** Internal: command-facade already classified this as a bounded local script; retain policy/snapshot/audit without durable Process state. */
74
74
  allowOpaqueLocalScript?: boolean;
75
+ /** Internal/test seam: bound dirty-path fingerprint enrichment independently from child execution lifetime. */
76
+ snapshotFingerprintTimeoutMs?: number;
75
77
  }
76
78
 
77
79
 
@@ -98,6 +100,8 @@ export interface RepositoryCommandExecution {
98
100
  authorizationDecision?: AuthorizationDecision;
99
101
  approvalRequestId?: string;
100
102
  infrastructureError?: { code: string; message: string };
103
+ /** Child execution completed, but post-execution repository evidence could not be proven. Mutation state is unknown until a later authoritative inspection. */
104
+ evidenceError?: { code: string; message: string };
101
105
  externalPathUsages?: RepositoryCommandExternalPathUsage[];
102
106
  }
103
107
 
@@ -307,6 +311,7 @@ async function prepareRepositoryCommandExecutionAsync(
307
311
  ? emptyWorkspaceSnapshot()
308
312
  : await repositorySnapshotAsync(root, input.signal, {
309
313
  pathFingerprints: !(classification.risk === 'readonly' && controllerHome === undefined),
314
+ fingerprintTimeoutMs: input.snapshotFingerprintTimeoutMs,
310
315
  }));
311
316
  return finalizePreparedExecution(
312
317
  repository,
@@ -565,9 +570,19 @@ export async function executeRepositoryCommandAsync(
565
570
  ...hooks,
566
571
  signal,
567
572
  });
568
- const after = input.allowNonGitWorkspace
569
- ? before
570
- : await repositorySnapshotAsync(root, signal?.aborted ? undefined : signal);
573
+ let after: RepositoryCommandSnapshot | undefined = input.allowNonGitWorkspace ? before : undefined;
574
+ let evidenceError: RepositoryCommandExecution['evidenceError'];
575
+ if (!input.allowNonGitWorkspace) {
576
+ try {
577
+ after = await repositorySnapshotAsync(root, signal?.aborted ? undefined : signal, {
578
+ fingerprintTimeoutMs: input.snapshotFingerprintTimeoutMs,
579
+ });
580
+ } catch (error) {
581
+ const message = error instanceof Error ? error.message : String(error);
582
+ const code = message.includes(':') ? message.slice(0, message.indexOf(':')) : 'POST_EXECUTION_EVIDENCE_FAILED';
583
+ evidenceError = { code, message };
584
+ }
585
+ }
571
586
  const execution: RepositoryCommandExecution = {
572
587
  ...base,
573
588
  status: 'executed',
@@ -578,8 +593,9 @@ export async function executeRepositoryCommandAsync(
578
593
  stdout: result.stdout,
579
594
  stderr: result.stderr,
580
595
  after,
581
- repositoryChanged: snapshotChanged(before, after),
582
- changedPaths: changedSnapshotPaths(before, after),
596
+ repositoryChanged: after ? snapshotChanged(before, after) : undefined,
597
+ changedPaths: after ? changedSnapshotPaths(before, after) : undefined,
598
+ evidenceError,
583
599
  policyDecision: 'allowed',
584
600
  externalPathUsages: externalPathUsages.length > 0 ? externalPathUsages : undefined,
585
601
  infrastructureError: result.timedOut
@@ -594,7 +610,9 @@ export async function executeRepositoryCommandAsync(
594
610
  }
595
611
  : undefined,
596
612
  };
597
- if (execution.repositoryChanged) invalidateRepositoryReadCaches(root);
613
+ // Unknown post-execution mutation evidence is conservatively cache-invalidating;
614
+ // later reads must re-observe the repository rather than reuse a false unchanged state.
615
+ if (execution.repositoryChanged === true || execution.evidenceError) invalidateRepositoryReadCaches(root);
598
616
  auditCommand(controllerHome, repository, execution);
599
617
  return execution;
600
618
  }
@@ -84,7 +84,7 @@ const MAX_DIRTY_PATHS = 200;
84
84
  export async function repositorySnapshotAsync(
85
85
  root: string,
86
86
  signal?: AbortSignal,
87
- options: { pathFingerprints?: boolean } = {},
87
+ options: { pathFingerprints?: boolean; fingerprintTimeoutMs?: number } = {},
88
88
  ): Promise<RepositoryCommandSnapshot> {
89
89
  const [headResult, branchResult, statusResult, refsResult] = await Promise.all([
90
90
  gitAsync(root, ['rev-parse', '--verify', 'HEAD'], signal), gitAsync(root, ['branch', '--show-current'], signal),
@@ -107,7 +107,10 @@ export async function repositorySnapshotAsync(
107
107
  let pathFingerprints: Record<string, string> = {};
108
108
  if (includePathFingerprints) {
109
109
  const { computePathFingerprintsAsync } = await import('../../runtime/execution/thin-harness/fingerprint-worker');
110
- const fingerprint = await computePathFingerprintsAsync({ root, paths, statusByPath: Object.fromEntries(byPath), maxFileBytes: 256 * 1024, maxTotalBytes: 8 * 1024 * 1024, maxPaths: MAX_DIRTY_PATHS }, { signal, timeoutMs: 5_000 });
110
+ const fingerprintTimeoutMs = typeof options.fingerprintTimeoutMs === 'number' && Number.isFinite(options.fingerprintTimeoutMs)
111
+ ? Math.max(1, Math.trunc(options.fingerprintTimeoutMs))
112
+ : 5_000;
113
+ const fingerprint = await computePathFingerprintsAsync({ root, paths, statusByPath: Object.fromEntries(byPath), maxFileBytes: 256 * 1024, maxTotalBytes: 8 * 1024 * 1024, maxPaths: MAX_DIRTY_PATHS }, { signal, timeoutMs: fingerprintTimeoutMs });
111
114
  pathFingerprints = fingerprint.pathFingerprints;
112
115
  }
113
116
  return { head, branch: branchResult.ok ? branchResult.stdout || null : null, status: statusResult.stdout, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints };