@moretea-labs/forge 1.7.0 → 1.7.2

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 +26 -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 +125 -101
  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 +69 -29
  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 +150 -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 +815 -1015
  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 +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  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,10 @@ 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
+ import { ensureRepositoryMutationWorkHandle } from '../../runtime/control-plane/execution/work-handle-authority';
8
9
  import { isTerminalWorkContractStatus } from '../../runtime/control-plane/facade/types';
9
10
  import { executeRepositoryCommand, previewRepositoryCommandExecution } from '../repositories/command-executor';
10
11
  import { withControllerLock } from '../repositories/locks';
@@ -53,11 +54,12 @@ import {
53
54
  executeRepositoryCommandViaProcessRuntime,
54
55
  } from '../../runtime/execution/process-runtime/command-facade';
55
56
  import { assessWorkMode, parseExplicitTaskMode } from '../controller/work-mode';
57
+ import { classifyRepositoryCommand } from '../repositories/command-classifier';
56
58
  import { normalizeRepositoryCommand } from '../repositories/command-normalization';
57
59
  import { readRepositoryRange } from '../repository/inspector';
58
60
  import { getMcpPolicy } from './policy';
59
61
  import { redactMcpText } from './redaction';
60
- import type { CallToolResult, McpToolDefinition } from './tools';
62
+ import type { CallToolResult, McpToolDefinition } from './tool-contract';
61
63
  import {
62
64
  boundUtf8,
63
65
  compactCommandOutput,
@@ -287,87 +289,7 @@ function withResponseMeta(payload: Record<string, unknown>, startedAt: number):
287
289
  return response;
288
290
  }
289
291
 
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
- }
292
+ export type RepositoryToolCallerContext = RepositoryWorkAttributionCaller;
371
293
 
372
294
  export function claimedSessionEditBinding(
373
295
  controllerHome: string,
@@ -375,7 +297,7 @@ export function claimedSessionEditBinding(
375
297
  caller?: RepositoryToolCallerContext,
376
298
  explicitWorkId?: unknown,
377
299
  ): EditSessionBinding | undefined {
378
- const workId = claimedSessionWorkId(controllerHome, repository, caller, explicitWorkId);
300
+ const workId = resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId);
379
301
  if (!workId || !caller?.principalId?.trim()) return undefined;
380
302
  const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
381
303
  if (!work) return undefined;
@@ -423,7 +345,7 @@ function resolveRepositorySelectionForClaimedWork(
423
345
  allowSoleRepository: true,
424
346
  });
425
347
  if (!explicitWorkId || checkoutId) return repository;
426
- const work = claimedExplicitWork(controllerHome, repository.repoId, caller, explicitWorkId);
348
+ const work = resolveExplicitClaimedRepositoryWork(controllerHome, repository, caller, explicitWorkId);
427
349
  if (!work?.checkoutId || work.checkoutId === repository.activeCheckoutId) return repository;
428
350
  return resolveRepositorySelection({
429
351
  repoId: repository.repoId,
@@ -445,6 +367,32 @@ function rawDefaultBranchMergeCommand(repository: ReturnType<typeof resolveRepos
445
367
  return /(?:^|[;&|]\s*)git\s+merge\b/.test(normalized.shellCommand ?? '');
446
368
  }
447
369
 
370
+ interface HistoricalReadOnlyWorkContext {
371
+ workId: string;
372
+ status: string;
373
+ mode: 'historical_read_only_observation';
374
+ attribution: 'not_active_work';
375
+ }
376
+
377
+ function historicalReadOnlyWorkContext(
378
+ controllerHome: string,
379
+ repository: ReturnType<typeof resolveRepositorySelection>,
380
+ args: Record<string, unknown>,
381
+ ): HistoricalReadOnlyWorkContext | undefined {
382
+ const requestedWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
383
+ if (!requestedWorkId) return undefined;
384
+ const work = getWorkContract({ controllerHome, repoId: repository.repoId }, requestedWorkId);
385
+ if (!work || !isTerminalWorkContractStatus(work.status)) return undefined;
386
+ const classification = classifyRepositoryCommand(args.command as string | string[], repository.defaultBranch);
387
+ if (classification.risk !== 'readonly') return undefined;
388
+ return {
389
+ workId: work.workId,
390
+ status: work.status,
391
+ mode: 'historical_read_only_observation',
392
+ attribution: 'not_active_work',
393
+ };
394
+ }
395
+
448
396
  function resolveRepositoryCommandTarget(
449
397
  controllerHome: string,
450
398
  args: Record<string, unknown>,
@@ -454,6 +402,7 @@ function resolveRepositoryCommandTarget(
454
402
  repository: ReturnType<typeof resolveRepositorySelection>;
455
403
  executionIdentity: ResolvedExecutionIdentity;
456
404
  workspace?: { workspaceId: string; root: string; registered: false };
405
+ historicalWorkContext?: HistoricalReadOnlyWorkContext;
457
406
  } {
458
407
  const workspaceRoot = typeof args.workspace_root === 'string' ? args.workspace_root.trim() : '';
459
408
  const checkoutId = typeof args.checkout_id === 'string' ? args.checkout_id.trim() : '';
@@ -474,10 +423,37 @@ function resolveRepositoryCommandTarget(
474
423
  workspace: { workspaceId: target.workspaceId, root: target.canonicalRoot, registered: false },
475
424
  };
476
425
  }
426
+ // A terminal Work no longer owns mutation or checkout authority. An exact
427
+ // terminal work_id may still accompany a read-only observation immediately
428
+ // after finalize, so preserve it as explicit historical context while
429
+ // executing against the caller-selected/current repository checkout. Any
430
+ // non-read-only command continues through claimed Work resolution and fails
431
+ // closed for terminal attribution.
432
+ const selectedRepository = resolveRepositorySelection({
433
+ repoId: repoIdValue || undefined,
434
+ checkoutId: checkoutId || undefined,
435
+ controllerHome,
436
+ allowSoleRepository: true,
437
+ });
438
+ const historicalWorkContext = historicalReadOnlyWorkContext(controllerHome, selectedRepository, args);
439
+ if (historicalWorkContext) {
440
+ return {
441
+ repository: selectedRepository,
442
+ executionIdentity: {
443
+ schemaVersion: 1,
444
+ authority: 'repository',
445
+ repositoryId: selectedRepository.repoId,
446
+ checkoutId: selectedRepository.activeCheckoutId,
447
+ canonicalRoot: selectedRepository.canonicalRoot,
448
+ },
449
+ historicalWorkContext,
450
+ };
451
+ }
452
+
477
453
  const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
478
454
  const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
479
455
  const workId = explicitWorkId
480
- ? claimedSessionWorkId(controllerHome, repository, caller, explicitWorkId)
456
+ ? resolveClaimedRepositoryWorkId(controllerHome, repository, caller, explicitWorkId)
481
457
  : undefined;
482
458
  return {
483
459
  repository,
@@ -615,6 +591,7 @@ function compactProcessCommandPayload(input: {
615
591
  repoId: string;
616
592
  checkoutId?: string;
617
593
  workspace?: { workspaceId: string; root: string; registered: false };
594
+ historicalWorkContext?: HistoricalReadOnlyWorkContext;
618
595
  processId?: string;
619
596
  process?: unknown;
620
597
  completed?: boolean;
@@ -653,6 +630,7 @@ function compactProcessCommandPayload(input: {
653
630
  repoId: input.repoId,
654
631
  ...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
655
632
  ...(input.workspace ? { workspace: input.workspace } : {}),
633
+ ...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
656
634
  ...(input.processId ? { processId: input.processId } : {}),
657
635
  ...(completed ? { exitCode: input.exitCode ?? (ok ? 0 : 1) } : {}),
658
636
  // A running default response otherwise loses its only dependency-bound
@@ -693,6 +671,7 @@ function compactProcessCommandPayload(input: {
693
671
  repoId: input.repoId,
694
672
  ...(input.checkoutId ? { checkoutId: input.checkoutId } : {}),
695
673
  ...(input.workspace ? { workspace: input.workspace } : {}),
674
+ ...(input.historicalWorkContext ? { historicalWorkContext: input.historicalWorkContext } : {}),
696
675
  ...(input.processId ? { processId: input.processId } : {}),
697
676
  ok: input.ok,
698
677
  exitCode: input.exitCode,
@@ -998,22 +977,42 @@ export async function callRepositoryTool(
998
977
  case 'repository_safe_patch_apply': {
999
978
  const repository = resolveRepositorySelectionForClaimedWork(controllerHome, args, repoIdValue, caller);
1000
979
  const binding = safePatchEditBinding(controllerHome, repository, caller, args);
980
+ if (binding?.workId) {
981
+ const work = getWorkContract({ controllerHome, repoId: repository.repoId }, binding.workId);
982
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${binding.workId}`);
983
+ const requestedPaths = buildSafePatchPlan(repository, { operations: args.operations, chunkSize: args.chunk_size })
984
+ .chunks.flatMap((chunk) => chunk.paths);
985
+ assertWorkPathsWithinScope(work, requestedPaths, {
986
+ forbidden: 'WORK_MUTATION_FORBIDDEN_PATH',
987
+ outOfScope: 'WORK_MUTATION_PATH_OUT_OF_SCOPE',
988
+ });
989
+ }
1001
990
  const applied = withControllerLock(
1002
991
  controllerHome,
1003
992
  { scope: 'repository', repoId: repository.repoId },
1004
993
  'mcp:repository_safe_patch_apply',
1005
- () => applySafePatch(repository, {
1006
- sessionId: args.session_id,
1007
- purpose: args.purpose,
1008
- operations: args.operations,
1009
- chunkSize: args.chunk_size,
1010
- expectedRevision: args.expected_revision,
1011
- allowedPaths: args.allowed_paths,
1012
- continueOnError: args.continue_on_error,
1013
- refreshFingerprints: args.refresh_fingerprints,
1014
- recoverStaleSession: args.recover_stale_session,
1015
- binding,
1016
- }),
994
+ () => {
995
+ if (binding?.workId) {
996
+ ensureRepositoryMutationWorkHandle({
997
+ controllerHome,
998
+ repository,
999
+ workId: binding.workId,
1000
+ principalId: binding.principalId ?? caller?.principalId ?? '',
1001
+ });
1002
+ }
1003
+ return applySafePatch(repository, {
1004
+ sessionId: args.session_id,
1005
+ purpose: args.purpose,
1006
+ operations: args.operations,
1007
+ chunkSize: args.chunk_size,
1008
+ expectedRevision: args.expected_revision,
1009
+ allowedPaths: args.allowed_paths,
1010
+ continueOnError: args.continue_on_error,
1011
+ refreshFingerprints: args.refresh_fingerprints,
1012
+ recoverStaleSession: args.recover_stale_session,
1013
+ binding,
1014
+ });
1015
+ },
1017
1016
  60_000,
1018
1017
  );
1019
1018
  const changedFiles = [
@@ -1098,7 +1097,7 @@ export async function callRepositoryTool(
1098
1097
  case 'repository_command_execute': {
1099
1098
  const explorationGuidance = fragmentedRepositoryExplorationGuidance(args.command);
1100
1099
  const target = resolveRepositoryCommandTarget(controllerHome, args, repoIdValue, caller);
1101
- const { repository, executionIdentity } = target;
1100
+ const { repository, executionIdentity, historicalWorkContext } = target;
1102
1101
  const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1103
1102
  if (!explicitWorkId) assertNoBoundExecutionSessionMutation(controllerHome, repository, caller);
1104
1103
  const deliveryWorkId = rawDefaultBranchMergeCommand(repository, args.command)
@@ -1107,6 +1106,23 @@ export async function callRepositoryTool(
1107
1106
  if (deliveryWorkId) {
1108
1107
  throw new Error(`WORK_DELIVERY_REQUIRES_FINALIZE: ${deliveryWorkId} must pass Work verification and use rh_work finalize before default-branch integration`);
1109
1108
  }
1109
+ if (executionIdentity.workId) {
1110
+ const mutationClassification = classifyRepositoryCommand(args.command as string | string[], repository.defaultBranch);
1111
+ if (mutationClassification.risk === 'workspace_write' || mutationClassification.risk === 'destructive') {
1112
+ withControllerLock(
1113
+ controllerHome,
1114
+ { scope: 'repository', repoId: repository.repoId },
1115
+ 'mcp:repository_command_execute:mutation-authority',
1116
+ () => ensureRepositoryMutationWorkHandle({
1117
+ controllerHome,
1118
+ repository,
1119
+ workId: executionIdentity.workId!,
1120
+ principalId: caller?.principalId ?? '',
1121
+ }),
1122
+ 60_000,
1123
+ );
1124
+ }
1125
+ }
1110
1126
  const timeoutMs = typeof args.timeout_ms === 'number'
1111
1127
  ? args.timeout_ms
1112
1128
  : typeof args.timeout_ms === 'string'
@@ -1206,6 +1222,13 @@ export async function callRepositoryTool(
1206
1222
  });
1207
1223
  }
1208
1224
  if (routeClass.route === 'process_direct' || routeClass.route === 'process_managed' || routeClass.route === 'durable') {
1225
+ const processRequestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
1226
+ if (executionIdentity.workId && !processRequestId) {
1227
+ throw new Error(
1228
+ `WORK_PROCESS_REQUEST_ID_REQUIRED: ${executionIdentity.workId}; `
1229
+ + 'Work-attributed Process Runtime commands require a stable request_id so reconnect/retry can resume the original Process without duplicate execution.',
1230
+ );
1231
+ }
1209
1232
  const processResult = await executeRepositoryCommandViaProcessRuntime({
1210
1233
  controllerHome,
1211
1234
  repository,
@@ -1217,7 +1240,7 @@ export async function callRepositoryTool(
1217
1240
  : typeof args.interactive_wait_ms === 'number' ? args.interactive_wait_ms : undefined,
1218
1241
  maxOutputBytes,
1219
1242
  returnHandleImmediately,
1220
- requestId: typeof args.request_id === 'string' ? args.request_id : undefined,
1243
+ requestId: processRequestId || undefined,
1221
1244
  workId: executionIdentity.workId,
1222
1245
  executionIdentity,
1223
1246
  allowNonGitWorkspace: target.workspace !== undefined,
@@ -1272,6 +1295,7 @@ export async function callRepositoryTool(
1272
1295
  repoId: repository.repoId,
1273
1296
  checkoutId: repository.activeCheckoutId,
1274
1297
  workspace: target.workspace,
1298
+ historicalWorkContext,
1275
1299
  processId: handle?.processId,
1276
1300
  process: detailLevel === 'detail' ? handle : undefined,
1277
1301
  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 };