@fitlab-ai/agent-infra 0.11.9 → 0.11.10

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 (124) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/git/workflow.js +1 -1
  3. package/dist/lib/internal/cli-route-inventory.js +0 -3
  4. package/dist/lib/internal/sandbox-control.js +30 -5
  5. package/dist/lib/internal/task-finalization.js +14 -23
  6. package/dist/lib/internal/task-operation-registry.js +2 -3
  7. package/dist/lib/internal/task-platform-recovery.js +9 -4
  8. package/dist/lib/platform/completion-backfill.js +2 -1
  9. package/dist/lib/platform/context.js +4 -2
  10. package/dist/lib/platform/github-client.js +20 -17
  11. package/dist/lib/platform/github-provider.js +42 -22
  12. package/dist/lib/platform/github-release-notes.js +4 -2
  13. package/dist/lib/platform/in-label-sync.js +2 -1
  14. package/dist/lib/platform/issue-comments.js +13 -9
  15. package/dist/lib/platform/operation-coordinator.js +1 -1
  16. package/dist/lib/platform/pr-review.js +2 -1
  17. package/dist/lib/platform/provider-bridge.js +4 -5
  18. package/dist/lib/platform/provider-contract.d.ts +3 -6
  19. package/dist/lib/platform/provider-contract.js +3 -15
  20. package/dist/lib/platform/provider-validation.js +9 -4
  21. package/dist/lib/platform/pull-requests.js +4 -2
  22. package/dist/lib/sandbox/control/client.js +54 -6
  23. package/dist/lib/sandbox/control/executor.js +8 -3
  24. package/dist/lib/sandbox/removal.js +0 -1
  25. package/dist/lib/task/artifact-command.js +11 -1
  26. package/dist/lib/task/artifact-lifecycle.js +119 -62
  27. package/dist/lib/task/artifact-selection.js +1 -1
  28. package/dist/lib/task/capabilities.js +55 -21
  29. package/dist/lib/task/commands/status.js +0 -27
  30. package/dist/lib/task/commit-identity.js +22 -7
  31. package/dist/lib/task/commit-intent.js +67 -26
  32. package/dist/lib/task/commit-operation.js +19 -8
  33. package/dist/lib/task/completion-facts.js +32 -10
  34. package/dist/lib/task/control-authority.js +2 -4
  35. package/dist/lib/task/current-contract.js +1 -5
  36. package/dist/lib/task/events.js +8 -274
  37. package/dist/lib/task/execution-inputs.js +14 -0
  38. package/dist/lib/task/finalization-envelope.js +4 -0
  39. package/dist/lib/task/finalization.js +10 -36
  40. package/dist/lib/task/frontmatter.js +1 -1
  41. package/dist/lib/task/ledger-intents.js +4 -7
  42. package/dist/lib/task/orchestration.js +11 -10
  43. package/dist/lib/task/platform-operation-journal.js +17 -10
  44. package/dist/lib/task/platform-operation-recovery.js +95 -28
  45. package/dist/lib/task/review-fingerprint.js +4 -26
  46. package/dist/lib/task/verification-engine.js +95 -6
  47. package/dist/lib/task/write.js +0 -11
  48. package/dist/lifecycle-build-manifest.json +3 -3
  49. package/lib/git/workflow.ts +1 -1
  50. package/lib/internal/cli-route-inventory.ts +0 -2
  51. package/lib/internal/sandbox-control.ts +39 -6
  52. package/lib/internal/task-finalization.ts +16 -24
  53. package/lib/internal/task-operation-registry.ts +2 -3
  54. package/lib/internal/task-platform-recovery.ts +9 -4
  55. package/lib/platform/completion-backfill.ts +2 -1
  56. package/lib/platform/context.ts +8 -4
  57. package/lib/platform/github-client.ts +24 -20
  58. package/lib/platform/github-provider.ts +47 -26
  59. package/lib/platform/github-release-notes.ts +5 -3
  60. package/lib/platform/in-label-sync.ts +2 -1
  61. package/lib/platform/issue-comments.ts +14 -9
  62. package/lib/platform/operation-coordinator.ts +1 -1
  63. package/lib/platform/pr-review.ts +2 -1
  64. package/lib/platform/provider-bridge.ts +10 -9
  65. package/lib/platform/provider-contract.ts +6 -21
  66. package/lib/platform/provider-loader.ts +7 -6
  67. package/lib/platform/provider-validation.ts +30 -8
  68. package/lib/platform/pull-requests.ts +4 -2
  69. package/lib/platform/repository-metadata.ts +3 -2
  70. package/lib/platform/security-alerts.ts +1 -1
  71. package/lib/platform/types.ts +2 -0
  72. package/lib/sandbox/control/client.ts +57 -6
  73. package/lib/sandbox/control/executor.ts +8 -3
  74. package/lib/sandbox/removal.ts +2 -10
  75. package/lib/task/artifact-command.ts +10 -1
  76. package/lib/task/artifact-lifecycle.ts +100 -52
  77. package/lib/task/artifact-selection.ts +1 -1
  78. package/lib/task/capabilities.ts +47 -24
  79. package/lib/task/commands/status.ts +0 -31
  80. package/lib/task/commit-identity.ts +24 -7
  81. package/lib/task/commit-intent.ts +58 -24
  82. package/lib/task/commit-operation.ts +17 -8
  83. package/lib/task/completion-facts.ts +31 -9
  84. package/lib/task/control-authority.ts +1 -4
  85. package/lib/task/current-contract.ts +1 -6
  86. package/lib/task/events.ts +8 -247
  87. package/lib/task/execution-inputs.ts +25 -0
  88. package/lib/task/finalization-envelope.ts +21 -0
  89. package/lib/task/finalization.ts +10 -35
  90. package/lib/task/frontmatter.ts +1 -1
  91. package/lib/task/ledger-intents.ts +4 -7
  92. package/lib/task/orchestration.ts +11 -17
  93. package/lib/task/platform-operation-journal.ts +18 -11
  94. package/lib/task/platform-operation-recovery.ts +90 -31
  95. package/lib/task/review-fingerprint.ts +4 -26
  96. package/lib/task/verification-engine.ts +103 -9
  97. package/lib/task/write.ts +0 -19
  98. package/package.json +1 -1
  99. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  100. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  101. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  102. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  104. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
  105. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  106. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  107. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  108. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  109. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  110. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  113. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/templates/task.zh-CN.md +0 -12
  116. package/templates/.github/workflows/pr-label.yml +1 -2
  117. package/dist/lib/internal/task-invalidation.js +0 -63
  118. package/dist/lib/task/intermediate-cleanup.js +0 -327
  119. package/dist/lib/task/invalidation-command.js +0 -88
  120. package/dist/lib/task/invalidation.js +0 -242
  121. package/lib/internal/task-invalidation.ts +0 -41
  122. package/lib/task/intermediate-cleanup.ts +0 -393
  123. package/lib/task/invalidation-command.ts +0 -88
  124. package/lib/task/invalidation.ts +0 -313
@@ -7,6 +7,7 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
7
7
  import { coordinatePlatformWrite } from './operation-coordinator.ts';
8
8
  import { resolvePlatformProviderContext } from './context.ts';
9
9
  import type { PlatformClient } from './context.ts';
10
+ import { providerErrorRetryable } from './provider-validation.ts';
10
11
  import { platformResult } from './types.ts';
11
12
  import type { PlatformOperation, PlatformResult } from './types.ts';
12
13
  import {
@@ -34,6 +35,10 @@ import {
34
35
  } from './comment-safety.ts';
35
36
  import type { FenceRange } from './comment-safety.ts';
36
37
 
38
+ function isRetryable(error: { code: string; retryable?: boolean }): boolean {
39
+ return error.retryable ?? providerErrorRetryable(error.code);
40
+ }
41
+
37
42
  type RemoteComment = {
38
43
  id: number | string;
39
44
  body: string;
@@ -578,7 +583,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
578
583
  const issue = resourceIdentityNumber(issueIdentityFromTask);
579
584
  const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
580
585
  if (!listed.ok) {
581
- return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
586
+ return platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', {
582
587
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
583
588
  });
584
589
  }
@@ -648,10 +653,10 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
648
653
  })
649
654
  : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
650
655
  if (!written.ok) {
651
- if (!current && written.error.retryable) {
656
+ if (!current && isRetryable(written.error)) {
652
657
  // A provider owns reconciliation because only it knows how to address the resource.
653
658
  }
654
- return platformResult(written.error.retryable ? 'blocked' : 'failed', {
659
+ return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
655
660
  ...contextFields(context),
656
661
  resource: { kind: 'issue', number: issue },
657
662
  operations,
@@ -674,7 +679,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
674
679
  })
675
680
  : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
676
681
  if (!deleted.ok) {
677
- return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
682
+ return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
678
683
  ...contextFields(context),
679
684
  resource: { kind: 'issue', number: issue },
680
685
  operations,
@@ -686,7 +691,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
686
691
  if (options.kind === 'summary') {
687
692
  const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
688
693
  if (!refreshed.ok) {
689
- return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
694
+ return platformResult(isRetryable(refreshed.error) ? 'blocked' : 'failed', {
690
695
  ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
691
696
  });
692
697
  }
@@ -714,7 +719,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
714
719
  })
715
720
  : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
716
721
  if (!deleted.ok) {
717
- return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
722
+ return platformResult(isRetryable(deleted.error) ? 'blocked' : 'failed', {
718
723
  ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
719
724
  });
720
725
  }
@@ -725,7 +730,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
725
730
  })
726
731
  : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
727
732
  if (!written.ok) {
728
- return platformResult(written.error.retryable ? 'blocked' : 'failed', {
733
+ return platformResult(isRetryable(written.error) ? 'blocked' : 'failed', {
729
734
  ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
730
735
  });
731
736
  }
@@ -736,7 +741,7 @@ async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): P
736
741
  const error = reconciled.ok
737
742
  ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
738
743
  : reconciled.error;
739
- return platformResult(error.retryable ? 'blocked' : 'failed', {
744
+ return platformResult(isRetryable(error) ? 'blocked' : 'failed', {
740
745
  ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
741
746
  });
742
747
  }
@@ -837,7 +842,7 @@ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: strin
837
842
  ? { ok: true as const, value: response.value.map((comment) => ({ id: comment.id, body: comment.body, user: comment.author?.name ? { login: comment.author.name } : undefined })) }
838
843
  : response)
839
844
  : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
840
- if (!listed.ok) return platformResult(listed.error.retryable ? 'blocked' : 'failed', { ...contextFields(context), error: listed.error });
845
+ if (!listed.ok) return platformResult(isRetryable(listed.error) ? 'blocked' : 'failed', { ...contextFields(context), error: listed.error });
841
846
  const matches = findMarkerComments(listed.value, MARKERS.task(resolved.taskId));
842
847
  if (matches.length > 1) return platformResult('failed', { ...contextFields(context), error: { code: 'COMMENT_MARKER_CONFLICT', message: 'Multiple task comments use the registered marker', retryable: false } });
843
848
  const owner = matches[0]?.user?.login;
@@ -48,7 +48,7 @@ async function coordinatePlatformWrite<T extends OperationResult>(input: Coordin
48
48
  excludeId: operationId
49
49
  });
50
50
  if (drained.status !== 'applied' && drained.status !== 'no-op') {
51
- return input.block(drained.error ?? {
51
+ return input.block(drained.error ? { ...drained.error, retryable: true } : {
52
52
  code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
53
53
  message: 'A previous platform operation is still unresolved; the current write remains queued',
54
54
  retryable: true
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { coordinatePlatformWrite } from './operation-coordinator.ts';
6
6
  import { resolvePlatformProviderContext } from './context.ts';
7
7
  import type { PlatformClient } from './context.ts';
8
+ import { providerErrorRetryable } from './provider-validation.ts';
8
9
  import { platformResult } from './types.ts';
9
10
  import type { PlatformOperation, PlatformResult } from './types.ts';
10
11
  import {
@@ -283,7 +284,7 @@ export async function publishPrReview(options: {
283
284
  })
284
285
  : unsupportedProviderOperation(loaded.value.provider, 'reviews.publish');
285
286
  if (!published.ok) {
286
- if (published.error.retryable) {
287
+ if (published.error.retryable || providerErrorRetryable(published.error.code)) {
287
288
  const reconciled = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
288
289
  const found = reconciled.reviews.find((review) => firstLine(review.body) === marker);
289
290
  if (found) return platformResult('applied', {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import type {
4
- PlatformError,
4
+ ProviderError,
5
5
  ProviderOperationContext,
6
6
  PlatformProvider,
7
7
  ProviderResult,
@@ -15,6 +15,9 @@ import {
15
15
  } from './resource-identity.ts';
16
16
  import type { LoadedContext } from './context.ts';
17
17
  import type { PlatformResult } from './types.ts';
18
+ import type { PlatformError } from './types.ts';
19
+ import { providerErrorRetryable } from './provider-validation.ts';
20
+ import type { NormalizedProviderResult } from './provider-validation.ts';
18
21
 
19
22
  function providerOperationContext(
20
23
  loaded: LoadedContext,
@@ -29,30 +32,28 @@ function providerOperationContext(
29
32
  };
30
33
  }
31
34
 
32
- function providerError(error: PlatformError, fallbackCode: string): PlatformError {
35
+ function providerError(error: ProviderError | PlatformError, fallbackCode: string): PlatformError {
33
36
  return {
34
37
  code: error.code || fallbackCode,
35
38
  message: error.message || 'Platform provider operation failed',
36
- retryable: error.retryable
39
+ retryable: 'retryable' in error ? error.retryable : providerErrorRetryable(error.code)
37
40
  };
38
41
  }
39
42
 
40
- function providerStatus(error: PlatformError): PlatformResult['status'] {
41
- return error.retryable ? 'blocked' : 'failed';
43
+ function providerStatus(error: ProviderError | PlatformError): PlatformResult['status'] {
44
+ return ('retryable' in error ? error.retryable : providerErrorRetryable(error.code)) ? 'blocked' : 'failed';
42
45
  }
43
46
 
44
47
  function unsupportedProviderOperation(
45
48
  provider: PlatformProvider,
46
49
  operation: string
47
- ): ProviderResult<never> {
50
+ ): NormalizedProviderResult<never> {
48
51
  return {
49
52
  ok: false,
50
53
  error: {
51
54
  code: 'PLATFORM_CAPABILITY_UNSUPPORTED',
52
55
  message: `Platform '${provider.type}' does not provide ${operation}`,
53
- retryable: false,
54
- providerType: provider.type,
55
- phase: 'operation'
56
+ retryable: false
56
57
  }
57
58
  };
58
59
  }
@@ -12,17 +12,14 @@ type JsonValue =
12
12
  | JsonValue[]
13
13
  | { [key: string]: JsonValue };
14
14
 
15
- type PlatformError = {
15
+ type ProviderError = {
16
16
  code: string;
17
17
  message: string;
18
- retryable: boolean;
19
- providerType?: string;
20
- phase?: string;
21
18
  };
22
19
 
23
20
  type ProviderResult<T> =
24
21
  | { ok: true; value: T }
25
- | { ok: false; error: PlatformError; value?: T };
22
+ | { ok: false; error: ProviderError; value?: T };
26
23
 
27
24
  type ProviderOperationContext = {
28
25
  repositoryRoot: string;
@@ -534,10 +531,7 @@ function validatePlatformProvider(
534
531
  ok: false,
535
532
  error: {
536
533
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
537
- message: 'Provider must expose matching type, contractVersion, and context.resolve',
538
- retryable: false,
539
- providerType,
540
- phase: 'provider-validation'
534
+ message: 'Provider must expose matching type, contractVersion, and context.resolve'
541
535
  }
542
536
  };
543
537
  }
@@ -550,10 +544,7 @@ function validatePlatformProvider(
550
544
  ok: false,
551
545
  error: {
552
546
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
553
- message: 'Provider identity declaration is invalid',
554
- retryable: false,
555
- providerType,
556
- phase: 'provider-validation'
547
+ message: 'Provider identity declaration is invalid'
557
548
  }
558
549
  };
559
550
  }
@@ -567,10 +558,7 @@ function validatePlatformProvider(
567
558
  ok: false,
568
559
  error: {
569
560
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
570
- message: `Provider operation group ${groupName} is incomplete`,
571
- retryable: false,
572
- providerType,
573
- phase: 'provider-validation'
561
+ message: `Provider operation group ${groupName} is incomplete`
574
562
  }
575
563
  };
576
564
  }
@@ -583,9 +571,6 @@ function validatePlatformProvider(
583
571
  error: {
584
572
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
585
573
  message: `Provider operation group ${groupName} requires an identity declaration for ${resourceKind}`,
586
- retryable: false,
587
- providerType,
588
- phase: 'provider-validation'
589
574
  }
590
575
  };
591
576
  }
@@ -617,7 +602,7 @@ export type {
617
602
  MutationReceipt,
618
603
  PlatformCapabilities,
619
604
  PlatformContextSnapshot,
620
- PlatformError,
605
+ ProviderError,
621
606
  PlatformProvider,
622
607
  PlatformProviderFactory,
623
608
  PlatformProviderFactoryInput,
@@ -12,13 +12,14 @@ import {
12
12
  } from './provider-contract.ts';
13
13
  import type {
14
14
  JsonValue,
15
- PlatformError,
16
15
  PlatformProvider,
17
16
  PlatformProviderFactoryInput
18
17
  } from './provider-contract.ts';
19
18
  import { createGitHubProvider } from './github-provider.ts';
20
19
  import { createNoneProvider } from './none-provider.ts';
21
20
  import { wrapProviderOperations } from './provider-validation.ts';
21
+ import type { LoadedRuntimeProvider, RuntimePlatformProvider } from './provider-validation.ts';
22
+ import type { PlatformError } from './types.ts';
22
23
  import type { PlatformClient } from './context.ts';
23
24
 
24
25
  type ProviderLoaderOptions = {
@@ -28,7 +29,7 @@ type ProviderLoaderOptions = {
28
29
  };
29
30
 
30
31
  type LoadedPlatformProvider = {
31
- provider: PlatformProvider;
32
+ provider: LoadedRuntimeProvider;
32
33
  providerType: string;
33
34
  repositoryRoot: string;
34
35
  workingDirectory: string;
@@ -44,7 +45,7 @@ type ResolvedExternalSource = {
44
45
  importUrl: string;
45
46
  };
46
47
 
47
- const sessions = new Map<string, Promise<PlatformProvider>>();
48
+ const sessions = new Map<string, Promise<LoadedRuntimeProvider>>();
48
49
 
49
50
  function findRepositoryRoot(cwd: string): string {
50
51
  try {
@@ -151,15 +152,15 @@ async function instantiateProvider(
151
152
  importUrl: string | null,
152
153
  config: Readonly<Record<string, JsonValue>>,
153
154
  client?: PlatformClient
154
- ): Promise<PlatformProvider> {
155
+ ): Promise<LoadedRuntimeProvider> {
155
156
  const input: PlatformProviderFactoryInput = {
156
157
  providerType,
157
158
  contractVersion: PLATFORM_PROVIDER_CONTRACT_VERSION,
158
159
  repositoryRoot,
159
160
  config
160
161
  };
161
- if (providerType === 'github') return createGitHubProvider(input, client as never);
162
- if (providerType === 'none') return createNoneProvider(input);
162
+ if (providerType === 'github') return createGitHubProvider(input, client as never) as unknown as RuntimePlatformProvider;
163
+ if (providerType === 'none') return createNoneProvider(input) as unknown as RuntimePlatformProvider;
163
164
 
164
165
  let moduleValue: Record<string, unknown>;
165
166
  try {
@@ -13,7 +13,7 @@ import type {
13
13
  MilestoneReconciliation,
14
14
  MutationReceipt,
15
15
  PlatformContextSnapshot,
16
- PlatformError,
16
+ ProviderError,
17
17
  PlatformProvider,
18
18
  ProviderResult,
19
19
  ReleaseNoteAuthor,
@@ -29,8 +29,23 @@ import type {
29
29
  VerificationRemoteFacts
30
30
  } from './provider-contract.ts';
31
31
  import type { JsonValue } from './provider-contract.ts';
32
+ import type { PlatformError } from './types.ts';
32
33
 
33
34
  type ResultValidator<T> = (value: unknown) => T;
35
+ type NormalizedProviderResult<T> =
36
+ | { ok: true; value: T }
37
+ | { ok: false; error: PlatformError; value?: T };
38
+ type RuntimeResult<T> = T extends { ok: true; value: infer V }
39
+ ? { ok: true; value: V }
40
+ : T extends { ok: false; error: ProviderError; value?: infer V }
41
+ ? { ok: false; error: PlatformError; value?: V }
42
+ : T;
43
+ type RuntimeMethod<T> = T extends (...args: infer A) => Promise<infer R>
44
+ ? (...args: A) => Promise<RuntimeResult<R>>
45
+ : T;
46
+ type RuntimeProviderValue<T> = T extends object ? { [K in keyof T]: RuntimeMethod<T[K]> } : T;
47
+ type RuntimePlatformProvider = { [K in keyof PlatformProvider]: RuntimeProviderValue<PlatformProvider[K]> };
48
+ type LoadedRuntimeProvider = RuntimePlatformProvider;
34
49
 
35
50
  function validationError(providerType: string, phase: string, message: string, code = 'PLATFORM_PROVIDER_RESULT_INVALID'): PlatformError {
36
51
  return {
@@ -494,9 +509,11 @@ const PROVIDER_ERROR_CATALOG: Readonly<Record<string, { message: string; retryab
494
509
  REMOTE_INVALID: { message: 'The platform remote is invalid', retryable: false },
495
510
  UPSTREAM_UNRESOLVED: { message: 'The platform upstream repository could not be resolved', retryable: false },
496
511
  RESOURCE_NOT_FOUND: { message: 'The platform resource was not found', retryable: false },
512
+ COMMENT_LIST_FAILED: { message: 'Comments could not be listed', retryable: true },
497
513
  PR_NOT_FOUND: { message: 'The pull request was not found', retryable: false },
498
514
  ISSUE_NOT_FOUND: { message: 'The issue was not found', retryable: false },
499
515
  REMOTE_BUSY: { message: 'Platform provider is temporarily busy', retryable: true },
516
+ IN_LABEL_SYNC_PARTIAL: { message: 'In-label synchronization is partial or unknown', retryable: true },
500
517
  NETWORK_TRANSIENT: { message: 'The platform request failed temporarily', retryable: true },
501
518
  NETWORK_RETRY: { message: 'The platform request failed temporarily', retryable: true },
502
519
  NETWORK_RETRY_EXHAUSTED: { message: 'The platform request retries were exhausted', retryable: true },
@@ -538,12 +555,16 @@ function safeProviderError(providerType: string, operation: string, value: unkno
538
555
  return {
539
556
  code: definition ? item.code as string : 'PLATFORM_PROVIDER_OPERATION_FAILED',
540
557
  message: definition?.message || PROVIDER_ERROR_CATALOG.PLATFORM_PROVIDER_OPERATION_FAILED!.message,
541
- retryable: definition?.retryable || false,
558
+ retryable: definition?.retryable ?? false,
542
559
  providerType,
543
560
  phase: operation
544
561
  };
545
562
  }
546
563
 
564
+ function providerErrorRetryable(code: string): boolean {
565
+ return PROVIDER_ERROR_CATALOG[code]?.retryable ?? false;
566
+ }
567
+
547
568
  function validators(providerType: string, declaration?: ProviderIdentityDeclaration): Record<string, ResultValidator<unknown>> {
548
569
  return {
549
570
  'context.resolve': (value) => validateContext(value, providerType),
@@ -629,7 +650,7 @@ async function invokeProviderOperation<T>(
629
650
  operation: string,
630
651
  call: () => Promise<unknown>,
631
652
  validate: ResultValidator<T>
632
- ): Promise<ProviderResult<T>> {
653
+ ): Promise<NormalizedProviderResult<T>> {
633
654
  try {
634
655
  const raw = await call();
635
656
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, error: validationError(providerType, operation, 'Provider operation returned an invalid result envelope') };
@@ -646,8 +667,8 @@ async function invokeProviderOperation<T>(
646
667
  try {
647
668
  exactKeys(envelope, Object.hasOwn(envelope, 'value') ? ['ok', 'error', 'value'] : ['ok', 'error'], `${operation} result`);
648
669
  const error = envelope.error as Record<string, unknown>;
649
- exactKeys(error, ['code', 'message', 'retryable'], `${operation} error`);
650
- if (typeof error.code !== 'string' || !error.code || typeof error.message !== 'string' || !error.message || typeof error.retryable !== 'boolean') throw new Error('invalid provider error');
670
+ exactKeys(error, ['code', 'message'], `${operation} error`);
671
+ if (typeof error.code !== 'string' || !error.code || typeof error.message !== 'string' || !error.message) throw new Error('invalid provider error');
651
672
  return {
652
673
  ok: false,
653
674
  error: safeProviderError(providerType, operation, error),
@@ -663,7 +684,7 @@ async function invokeProviderOperation<T>(
663
684
  }
664
685
  }
665
686
 
666
- function wrapProviderOperations(provider: PlatformProvider): PlatformProvider {
687
+ function wrapProviderOperations(provider: PlatformProvider): RuntimePlatformProvider {
667
688
  const map = validators(provider.type, provider.identity);
668
689
  const wrapped: PlatformProvider = { ...provider, context: { ...provider.context } };
669
690
  wrapped.context.resolve = (input) => invokeProviderOperation(provider.type, 'context.resolve', () => provider.context.resolve(input), map['context.resolve']! as ResultValidator<PlatformContextSnapshot>);
@@ -680,7 +701,8 @@ function wrapProviderOperations(provider: PlatformProvider): PlatformProvider {
680
701
  }
681
702
  (wrapped as unknown as Record<string, unknown>)[groupName] = target;
682
703
  }
683
- return wrapped;
704
+ return wrapped as RuntimePlatformProvider;
684
705
  }
685
706
 
686
- export { invokeProviderOperation, validationError, wrapProviderOperations };
707
+ export { invokeProviderOperation, providerErrorRetryable, validationError, wrapProviderOperations };
708
+ export type { LoadedRuntimeProvider, NormalizedProviderResult, RuntimePlatformProvider };
@@ -12,6 +12,7 @@ import type { PlatformChangeRequestSnapshot } from './snapshots.ts';
12
12
  import { resolvePlatformProviderContext } from './context.ts';
13
13
  import type { PlatformClient } from './context.ts';
14
14
  import { inspectPlatformIssue } from './issues.ts';
15
+ import { providerErrorRetryable } from './provider-validation.ts';
15
16
  import { planPullRequestMetadata } from './pull-request-metadata.ts';
16
17
  import {
17
18
  planInLabelUpdate,
@@ -741,9 +742,10 @@ async function syncPlatformPullRequestInLabels(prNumber: number, options: Shared
741
742
  : unsupportedProviderOperation(provider, 'changeRequests.update');
742
743
  if (!synced.ok) {
743
744
  const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity))!;
744
- state.effect = synced.error.retryable ? 'unknown' : 'no-op';
745
+ const retryable = synced.error.retryable || providerErrorRetryable(synced.error.code);
746
+ state.effect = retryable ? 'unknown' : 'no-op';
745
747
  state.after = state.effect === 'unknown' ? null : item.snapshot.labels;
746
- const partial = successfulWrites > 0 || synced.error.retryable;
748
+ const partial = successfulWrites > 0 || retryable;
747
749
  const error = partial
748
750
  ? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
749
751
  : synced.error;
@@ -14,9 +14,10 @@ import type {
14
14
  LabelReconciliation,
15
15
  MilestoneDefinition,
16
16
  MilestoneInitialization,
17
- PlatformError,
17
+ ProviderError,
18
18
  ProviderResult
19
19
  } from './provider-contract.ts';
20
+ import type { PlatformError } from './types.ts';
20
21
  import { platformResult } from './types.ts';
21
22
  import type { PlatformResult } from './types.ts';
22
23
 
@@ -288,7 +289,7 @@ function operationStatus(changed: boolean): 'applied' | 'no-op' {
288
289
  return changed ? 'applied' : 'no-op';
289
290
  }
290
291
 
291
- function errorStatus(error: PlatformError): PlatformResult['status'] {
292
+ function errorStatus(error: PlatformError | ProviderError): PlatformResult['status'] {
292
293
  return error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error);
293
294
  }
294
295
 
@@ -9,10 +9,10 @@ import {
9
9
  providerStatus
10
10
  } from './provider-bridge.ts';
11
11
  import type {
12
- PlatformError,
13
12
  SecurityAlertKind,
14
13
  SecurityAlertSnapshot
15
14
  } from './provider-contract.ts';
15
+ import type { PlatformError } from './types.ts';
16
16
  import { platformResult } from './types.ts';
17
17
  import type { PlatformResult } from './types.ts';
18
18
 
@@ -4,6 +4,8 @@ type PlatformError = {
4
4
  code: string;
5
5
  message: string;
6
6
  retryable: boolean;
7
+ providerType?: string;
8
+ phase?: string;
7
9
  };
8
10
 
9
11
  type PlatformCapabilities = {
@@ -27,6 +27,7 @@ import { readSandboxControlIdentitySentinel } from './identity-sentinel.ts';
27
27
  import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.ts';
28
28
 
29
29
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
30
+ const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
30
31
 
31
32
  export class SandboxControlClientError extends Error {
32
33
  readonly detail: SandboxControlError;
@@ -114,7 +115,8 @@ function preflight(
114
115
  }
115
116
 
116
117
  function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
117
- if (request.family === 'task-lifecycle' || request.family === 'task-finalization') return 'progress';
118
+ if (request.family === 'task-lifecycle') return 'progress';
119
+ if (request.family === 'task-finalization') return 'recovery';
118
120
  return null;
119
121
  }
120
122
 
@@ -347,13 +349,20 @@ export function recoverSandboxControl(requestId: string, params: Readonly<{
347
349
  } else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
348
350
  clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, requestId);
349
351
  }
350
- sleep(25);
352
+ const remainingMs = deadline - Date.now();
353
+ if (remainingMs > 0) sleep(Math.min(25, Math.ceil(remainingMs)));
351
354
  continue;
352
355
  }
353
- if (response.phase === 'accepted') accepted = true;
354
- else return response;
356
+ if (response.phase === 'accepted') {
357
+ accepted = true;
358
+ } else if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
359
+ accepted = true;
360
+ } else {
361
+ return response;
362
+ }
355
363
  }
356
- sleep(25);
364
+ const remainingMs = deadline - Date.now();
365
+ if (remainingMs > 0) sleep(Math.min(25, Math.ceil(remainingMs)));
357
366
  }
358
367
  clientError(
359
368
  'SANDBOX_CONTROL_RESULT_UNKNOWN',
@@ -399,6 +408,7 @@ export function requestSandboxTaskFinalization(params: Readonly<{
399
408
  token?: string;
400
409
  generation?: string;
401
410
  timeoutMs?: number;
411
+ recoveryBudgetMs?: number;
402
412
  }>): SandboxControlResponse {
403
413
  const agent = normalizeAgentToken(params.agent);
404
414
  if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
@@ -420,7 +430,48 @@ export function requestSandboxTaskFinalization(params: Readonly<{
420
430
  controllerProcess: null,
421
431
  controllerProof: null
422
432
  };
423
- return exchangeSandboxControl(request, params);
433
+ let response: SandboxControlResponse;
434
+ try {
435
+ response = exchangeSandboxControl(request, params);
436
+ } catch (error) {
437
+ if (!(error instanceof SandboxControlClientError)
438
+ || error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
439
+ || !error.accepted || !error.requestId) throw error;
440
+ return recoverAcceptedTaskFinalization(error.requestId, params);
441
+ }
442
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
443
+ return recoverAcceptedTaskFinalization(response.id, params);
444
+ }
445
+ return response;
446
+ }
447
+
448
+ export function recoverAcceptedTaskFinalization(
449
+ requestId: string,
450
+ params: Readonly<{ channelDir?: string; recoveryBudgetMs?: number }>,
451
+ recover: typeof recoverSandboxControl = recoverSandboxControl
452
+ ): SandboxControlResponse {
453
+ const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
454
+ while (Date.now() < deadline) {
455
+ const timeoutMs = Math.min(30_000, deadline - Date.now());
456
+ if (timeoutMs <= 0) break;
457
+ try {
458
+ const response = recover(requestId, { channelDir: params.channelDir, timeoutMs });
459
+ if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') continue;
460
+ return response;
461
+ } catch (error) {
462
+ if (error instanceof SandboxControlClientError
463
+ && error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
464
+ && error.requestId === requestId) continue;
465
+ throw error;
466
+ }
467
+ }
468
+ clientError(
469
+ 'SANDBOX_CONTROL_RESULT_UNKNOWN',
470
+ 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id',
471
+ false,
472
+ true,
473
+ requestId
474
+ );
424
475
  }
425
476
 
426
477
  type CodexControllerClosed = Readonly<{
@@ -12,6 +12,7 @@ import {
12
12
  type TaskCreateResult
13
13
  } from '../../task/create-service.ts';
14
14
  import { applyTaskFinalization } from '../../task/finalization.ts';
15
+ import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.ts';
15
16
  import {
16
17
  bindSandboxControlTask,
17
18
  validateSandboxControlRequest,
@@ -535,10 +536,14 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
535
536
  result = request.family === 'task-finalization'
536
537
  ? {
537
538
  exitCode: 1,
538
- stdout: `${JSON.stringify({
539
- version: 1, status: 'failed', changed: false, accepted: true, result: null,
539
+ stdout: serializeTaskFinalizationEnvelope({
540
+ status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
541
+ changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
542
+ accepted: true,
543
+ requestId: request.id,
544
+ result: null,
540
545
  error: { code, message: detail, retryable: false }
541
- })}\n`,
546
+ }),
542
547
  stderr: ''
543
548
  }
544
549
  : request.family === 'task-create'
@@ -50,14 +50,6 @@ import { getProcessStartTime } from '../server/process-state.ts';
50
50
  import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
51
51
  import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
52
52
  import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
53
- import {
54
- formatIntermediateCleanupReport,
55
- cleanupIntermediateUnderRemovalCoordinator,
56
- protectIntermediateCleanupReport,
57
- mergeIntermediateCleanupReports,
58
- type IntermediateCleanupReport,
59
- scanIntermediateCleanup
60
- } from '../task/intermediate-cleanup.ts';
61
53
  import { createSandboxControlBindingEvidence } from './task-cleanup.ts';
62
54
  import {
63
55
  createCleanPermit,
@@ -946,7 +938,7 @@ async function runRmOneUnderRepositoryLock(
946
938
  tools: SandboxTool[],
947
939
  branch: string,
948
940
  options: RmOneOptions = {}
949
- ): Promise<IntermediateCleanupReport | null> {
941
+ ): Promise<null> {
950
942
  const target = options.target ?? resolveRmTarget(
951
943
  config,
952
944
  tools,
@@ -1048,7 +1040,7 @@ async function rmOneCore(
1048
1040
  tools: SandboxTool[],
1049
1041
  branch: string,
1050
1042
  options: RmOneOptions = {}
1051
- ): Promise<IntermediateCleanupReport | null> {
1043
+ ): Promise<null> {
1052
1044
  const target = options.target ?? resolveRmTarget(
1053
1045
  config,
1054
1046
  tools,
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
 
3
4
  import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecycle.ts';
4
5
  import { parseArtifactName } from './artifact-name.ts';
@@ -83,7 +84,15 @@ export function executeArtifactCommand(
83
84
  const input = { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskDir, family: schema.family, artifact };
84
85
  if (operation === 'init') {
85
86
  const context = resolveArtifactContext(taskRef, family, { repoRoot: resolved.repoRoot });
86
- if (context.status !== 'ready' || (context.next?.name !== artifact && !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round))) {
87
+ const existingCandidate = path.join(taskDir, artifact);
88
+ let matchingInitializedSkeleton = false;
89
+ try {
90
+ const existing = fs.readFileSync(existingCandidate, 'utf8');
91
+ matchingInitializedSkeleton = existing.includes(`<!-- artifact-context:${resolved.taskId}:${family}:${parsed.round} -->`);
92
+ } catch { /* an absent candidate is created only for the selected identity */ }
93
+ if ((context.status !== 'ready' && !matchingInitializedSkeleton) || (context.next?.name !== artifact
94
+ && !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round)
95
+ && !matchingInitializedSkeleton)) {
87
96
  return fail('ARTIFACT_INIT_CONTEXT_INVALID', context.error?.message ?? `artifact '${artifact}' is not the next ${family} artifact`);
88
97
  }
89
98
  return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };