@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
@@ -3,12 +3,11 @@ import {
3
3
  dispatchTaskControlOperation,
4
4
  parseTaskControlOperation
5
5
  } from '../task/control-authority.ts';
6
- import { applyTaskFinalization, bindTaskFinalizationReceipt, prepareTaskFinalization } from '../task/finalization.ts';
7
- import { publishTaskFinalizationHandoff } from '../task/finalization-handoff.ts';
6
+ import { applyTaskFinalization } from '../task/finalization.ts';
8
7
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
- import { verifyTaskEvent } from '../task/verification.ts';
10
8
  import { resolveSandboxControlTransport } from './task-operation-registry.ts';
11
9
  import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.ts';
10
+ import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
12
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
13
12
 
14
13
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
@@ -17,12 +16,13 @@ type FinalizationError = Readonly<{ code: string; message: string; retryable: bo
17
16
 
18
17
  function envelope(
19
18
  status: 'completed' | 'failed' | 'blocked' | 'unknown',
20
- changed: boolean,
19
+ changed: boolean | null,
21
20
  accepted: boolean,
22
21
  result: Awaited<ReturnType<typeof applyTaskFinalization>> | null,
23
- error: FinalizationError | null
22
+ error: FinalizationError | null,
23
+ requestId: string | null = null
24
24
  ): string {
25
- return `${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`;
25
+ return serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error });
26
26
  }
27
27
 
28
28
  function exitCode(status: 'completed' | 'failed' | 'blocked' | 'unknown'): number {
@@ -79,33 +79,25 @@ async function taskFinalization(args: string[] = []): Promise<void> {
79
79
  };
80
80
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
81
81
  if (transport.kind === 'sandbox-local') {
82
- const prepared = await prepareTaskFinalization(boundOperation.request, {
83
- repoRoot,
84
- preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
85
- });
86
- if (prepared.status !== 'prepared') {
87
- process.stdout.write(envelope(prepared.status, prepared.changed, true, prepared, prepared.error));
88
- process.exitCode = exitCode(prepared.status);
89
- return;
90
- }
91
82
  try {
92
83
  const response = requestSandboxTaskFinalization({
93
- agent: boundOperation.request.agent,
94
- prepareHandoff: (binding) => {
95
- const receipt = bindTaskFinalizationReceipt(repoRoot, boundOperation.request.taskRef, binding);
96
- return publishTaskFinalizationHandoff('/share/branch', receipt, binding);
97
- }
84
+ agent: boundOperation.request.agent
98
85
  });
99
86
  process.stdout.write(response.stdout);
100
87
  process.stderr.write(response.stderr);
101
88
  process.exitCode = response.exitCode ?? 1;
102
89
  return;
103
90
  } catch (error) {
104
- const detail = error instanceof SandboxControlClientError
105
- ? error.detail
91
+ const clientError = error instanceof SandboxControlClientError ? error : null;
92
+ const detail = clientError
93
+ ? clientError.detail
106
94
  : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
107
- process.stdout.write(envelope(detail.retryable ? 'blocked' : 'failed', false, false, null, detail));
108
- process.exitCode = detail.retryable ? 2 : 1;
95
+ const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
96
+ ? 'unknown'
97
+ : detail.retryable ? 'blocked' : 'failed';
98
+ const changed = status === 'unknown' ? null : false;
99
+ process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
100
+ process.exitCode = exitCode(status);
109
101
  return;
110
102
  }
111
103
  }
@@ -142,9 +142,8 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
142
142
  descriptor('internal', 'task-review', 'preflight', 'task-bound', 'artifact-write'),
143
143
  descriptor('internal', 'task-review', 'finalize-summary', 'task-bound', 'progress'),
144
144
  descriptor('internal', 'task-event', 'event', 'task-bound', 'progress'),
145
- descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
146
145
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
147
- descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
146
+ descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'recovery'),
148
147
  descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
149
148
  descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
150
149
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
@@ -222,7 +221,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
222
221
  'git-workflow', 'task-delivery', 'release-workflow', 'platform-release-notes', 'platform-security', 'platform-metadata', 'platform-context',
223
222
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
224
223
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
225
- 'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
224
+ 'task-orchestration', 'task-review', 'task-event', 'task-lifecycle',
226
225
  'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
227
226
  ]);
228
227
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
@@ -3,7 +3,7 @@ import { readPlatformOperationJournal } from '../task/platform-operation-journal
3
3
  import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
5
5
 
6
- const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all]\n';
6
+ const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all] [--attempts <positive-integer>]\n';
7
7
 
8
8
  function option(args: readonly string[], name: string): string | undefined {
9
9
  const index = args.indexOf(name);
@@ -38,14 +38,19 @@ async function taskPlatformRecovery(args: string[] = []): Promise<void> {
38
38
  }
39
39
  const agent = option(args, '--agent');
40
40
  const selection = option(args, '--selection') ?? 'all';
41
+ const attemptsInput = option(args, '--attempts');
42
+ const attempts = attemptsInput === undefined ? undefined : Number(attemptsInput);
43
+ const validAttempts = attemptsInput === undefined
44
+ || (attempts !== undefined && Number.isSafeInteger(attempts) && attempts > 0);
41
45
  if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(agent)
42
- || !['required', 'deferred', 'all'].includes(selection)) {
43
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent and --selection' } })}\n`);
46
+ || !['required', 'deferred', 'all'].includes(selection)
47
+ || !validAttempts) {
48
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent, --selection, and positive --attempts when supplied' } })}\n`);
44
49
  process.stderr.write(USAGE);
45
50
  process.exitCode = 1;
46
51
  return;
47
52
  }
48
- const result = await recoverPlatformOperations(task.taskId, selection as 'required' | 'deferred' | 'all', { agent, cwd: repoRoot });
53
+ const result = await recoverPlatformOperations(task.taskId, selection as 'required' | 'deferred' | 'all', { agent, cwd: repoRoot, ...(attempts === undefined ? {} : { attempts }) });
49
54
  process.stdout.write(`${JSON.stringify(result)}\n`);
50
55
  process.exitCode = result.status === 'applied' || result.status === 'no-op' ? 0 : result.status === 'blocked' ? 2 : 1;
51
56
  }
@@ -10,6 +10,7 @@ import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.ts'
10
10
  import { captureTaskWriteMetadata } from '../task/write.ts';
11
11
  import { syncPlatformComment } from './issue-comments.ts';
12
12
  import type { PlatformClient } from './context.ts';
13
+ import { providerErrorRetryable } from './provider-validation.ts';
13
14
  import { platformResult } from './types.ts';
14
15
  import type { PlatformOperation, PlatformResult } from './types.ts';
15
16
  import { taskIssueIdentity } from './task-identities.ts';
@@ -151,7 +152,7 @@ async function backfillCompletionComments(
151
152
  })));
152
153
  artifacts.push({ artifact: artifact.name, status: latest.status });
153
154
  if (latest.status === 'failed' || latest.status === 'blocked') return result({ ...latest, operations }, artifacts, []);
154
- if (latest.error) return result(platformResult(latest.error.retryable ? 'blocked' : 'failed', {
155
+ if (latest.error) return result(platformResult(latest.error.retryable || providerErrorRetryable(latest.error.code) ? 'blocked' : 'failed', {
155
156
  platform: latest.platform,
156
157
  resource: latest.resource,
157
158
  capabilities: latest.capabilities,
@@ -2,9 +2,12 @@ import path from 'node:path';
2
2
 
3
3
  import { loadPlatformProvider } from './provider-loader.ts';
4
4
  import { defaultGitRemote, parseGitHubRemote } from './github-provider.ts';
5
+ import { providerErrorRetryable } from './provider-validation.ts';
5
6
  import { platformResult } from './types.ts';
6
7
  import type { PlatformResult } from './types.ts';
7
- import type { PlatformProvider, PlatformContextSnapshot, PlatformError } from './provider-contract.ts';
8
+ import type { PlatformContextSnapshot } from './provider-contract.ts';
9
+ import type { PlatformError } from './types.ts';
10
+ import type { LoadedRuntimeProvider } from './provider-validation.ts';
8
11
 
9
12
  type PlatformClientResult<T> =
10
13
  | { ok: true; value: T }
@@ -23,7 +26,7 @@ type ContextOptions = {
23
26
  };
24
27
 
25
28
  type LoadedContext = {
26
- provider: PlatformProvider;
29
+ provider: LoadedRuntimeProvider;
27
30
  providerType: string;
28
31
  repositoryRoot: string;
29
32
  workingDirectory: string;
@@ -33,7 +36,7 @@ type LoadedContext = {
33
36
  };
34
37
 
35
38
  function errorStatus(error: PlatformError): PlatformResult['status'] {
36
- if (error.retryable || error.code === 'AUTH_REQUIRED' || error.code === 'PLATFORM_DEPENDENCY_MISSING') return 'blocked';
39
+ if (error.retryable || providerErrorRetryable(error.code) || error.code === 'AUTH_REQUIRED' || error.code === 'PLATFORM_DEPENDENCY_MISSING') return 'blocked';
37
40
  if (error.code === 'REMOTE_MISSING' || error.code === 'PLATFORM_UNSUPPORTED') return 'no-op';
38
41
  return 'failed';
39
42
  }
@@ -43,9 +46,10 @@ function contextError(
43
46
  error: PlatformError,
44
47
  repository: string | null = null
45
48
  ): PlatformResult {
49
+ const retryable = error.retryable ?? providerErrorRetryable(error.code);
46
50
  return platformResult(errorStatus(error), {
47
51
  platform: { type: providerType || null, repository, currentUser: null },
48
- error: { code: error.code, message: error.message, retryable: error.retryable }
52
+ error: { code: error.code, message: error.message, retryable }
49
53
  });
50
54
  }
51
55
 
@@ -3,13 +3,13 @@ import spawn from 'cross-spawn';
3
3
 
4
4
  import semver from 'semver';
5
5
 
6
- import type { PlatformError } from './types.ts';
7
-
8
6
  type RunResult = { status: number | null; stdout: string; stderr: string; error?: Error };
9
7
  type RunOptions = { cwd?: string; input?: string };
10
8
  type Runner = (args: string[], options: RunOptions) => RunResult;
11
9
  type RequestOptions = RunOptions & { method?: 'GET' | 'PATCH' | 'POST' | 'PUT' | 'DELETE' };
12
- type ClientResult<T> = { ok: true; value: T } | { ok: false; error: PlatformError };
10
+ type GitHubError = { code: string; message: string };
11
+ type ClientResult<T> = { ok: true; value: T } | { ok: false; error: GitHubError };
12
+ type GitHubRequestFailure = GitHubError & { requestRetryable: boolean };
13
13
  type ResponseMetadata = {
14
14
  status: number;
15
15
  requestUrl: string;
@@ -86,36 +86,40 @@ function defaultSleep(delayMs: number): void {
86
86
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
87
87
  }
88
88
 
89
- function classifyGitHubFailure(result: RunResult): PlatformError {
89
+ function classifyGitHubFailure(result: RunResult): GitHubRequestFailure {
90
90
  const errorCode = result.error && 'code' in result.error ? (result.error as NodeJS.ErrnoException).code : undefined;
91
91
  if (errorCode === 'ENOBUFS') {
92
92
  return {
93
93
  code: 'PLATFORM_OUTPUT_TOO_LARGE',
94
94
  message: 'GitHub CLI output exceeded the configured limit',
95
- retryable: false
95
+ requestRetryable: false
96
96
  };
97
97
  }
98
98
  const detail = boundedFailureDetail(result);
99
99
  const lower = detail.toLowerCase();
100
100
  if (/\b401\b|bad credentials|authentication required|not logged into/.test(lower)) {
101
- return { code: 'AUTH_REQUIRED', message: detail || 'GitHub authentication is required', retryable: false };
101
+ return { code: 'AUTH_REQUIRED', message: detail || 'GitHub authentication is required', requestRetryable: false };
102
102
  }
103
103
  if (/\b429\b|rate limit|secondary rate|\b5\d\d\b|timeout|timed out|econnreset|enotfound|dns|tls|socket|network/.test(lower)) {
104
- return { code: 'NETWORK_TRANSIENT', message: detail || 'GitHub request failed temporarily', retryable: true };
104
+ return { code: 'NETWORK_TRANSIENT', message: detail || 'GitHub request failed temporarily', requestRetryable: true };
105
105
  }
106
106
  if (/\b403\b|resource not accessible|permission denied/.test(lower)) {
107
- return { code: 'PERMISSION_DENIED', message: detail || 'GitHub permission denied', retryable: false };
107
+ return { code: 'PERMISSION_DENIED', message: detail || 'GitHub permission denied', requestRetryable: false };
108
108
  }
109
109
  if (/\b404\b/.test(lower)) {
110
- return { code: 'RESOURCE_NOT_FOUND', message: detail || 'GitHub resource not found', retryable: false };
110
+ return { code: 'RESOURCE_NOT_FOUND', message: detail || 'GitHub resource not found', requestRetryable: false };
111
111
  }
112
112
  if (/\b422\b|validation failed/.test(lower)) {
113
- return { code: 'PLATFORM_REQUEST_INVALID', message: detail || 'GitHub rejected the request', retryable: false };
113
+ return { code: 'PLATFORM_REQUEST_INVALID', message: detail || 'GitHub rejected the request', requestRetryable: false };
114
114
  }
115
115
  if (errorCode === 'ENOENT') {
116
- return { code: 'PLATFORM_DEPENDENCY_MISSING', message: detail || 'GitHub CLI is unavailable', retryable: false };
116
+ return { code: 'PLATFORM_DEPENDENCY_MISSING', message: detail || 'GitHub CLI is unavailable', requestRetryable: false };
117
117
  }
118
- return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', retryable: false };
118
+ return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', requestRetryable: false };
119
+ }
120
+
121
+ function clientError(error: GitHubError): GitHubError {
122
+ return { code: error.code, message: error.message };
119
123
  }
120
124
 
121
125
  function responseUrl(args: string[]): string {
@@ -164,13 +168,13 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
164
168
  const result = runner(args, request);
165
169
  if (result.status === 0) return { ok: true, value: result.stdout };
166
170
  const error = classifyGitHubFailure(result);
167
- if (!retryableMethod || !error.retryable) return { ok: false, error };
171
+ if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(error) };
168
172
  if (attempt >= delays.length) {
169
173
  return {
170
174
  ok: false,
171
175
  error: error.code === 'NETWORK_TRANSIENT'
172
- ? { ...error, code: 'NETWORK_RETRY_EXHAUSTED', message: `${error.message} (retry exhausted)` }
173
- : error
176
+ ? { ...clientError(error), code: 'NETWORK_RETRY_EXHAUSTED', message: `${error.message} (retry exhausted)` }
177
+ : clientError(error)
174
178
  };
175
179
  }
176
180
  sleep(delays[attempt]!);
@@ -181,12 +185,12 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
181
185
  return {
182
186
  version(request = {}) {
183
187
  const result = runner(['--version'], request);
184
- if (result.status !== 0) return { ok: false, error: classifyGitHubFailure(result) };
188
+ if (result.status !== 0) return { ok: false, error: clientError(classifyGitHubFailure(result)) };
185
189
  const version = result.stdout.match(/^gh version (\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\s|$)/m)?.[1];
186
190
  if (!version || !semver.valid(version)) {
187
191
  return {
188
192
  ok: false,
189
- error: { code: 'GH_CLI_VERSION_INVALID', message: 'GitHub CLI returned an invalid version', retryable: false }
193
+ error: { code: 'GH_CLI_VERSION_INVALID', message: 'GitHub CLI returned an invalid version' }
190
194
  };
191
195
  }
192
196
  return { ok: true, value: version };
@@ -203,7 +207,7 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
203
207
  } catch {
204
208
  return {
205
209
  ok: false,
206
- error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub returned invalid JSON', retryable: true }
210
+ error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub returned invalid JSON' }
207
211
  };
208
212
  }
209
213
  },
@@ -217,7 +221,7 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
217
221
  if (!parsed) {
218
222
  return {
219
223
  ok: false,
220
- error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub response metadata or JSON is invalid', retryable: true }
224
+ error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub response metadata or JSON is invalid' }
221
225
  };
222
226
  }
223
227
  return { ok: true, value: parsed };
@@ -225,5 +229,5 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
225
229
  };
226
230
  }
227
231
 
228
- export { classifyGitHubFailure, createGitHubClient, MINIMUM_GITHUB_CLI_VERSION, parseIncludedResponse };
232
+ export { createGitHubClient, MINIMUM_GITHUB_CLI_VERSION, parseIncludedResponse };
229
233
  export type { ClientResult, GitHubClient, JsonResponse, RequestOptions, ResponseMetadata, RunOptions, RunResult, Runner };
@@ -21,7 +21,7 @@ import type {
21
21
  MutationReceipt,
22
22
  MilestoneInitialization,
23
23
  PlatformContextSnapshot,
24
- PlatformError,
24
+ ProviderError,
25
25
  ProviderOperationContext,
26
26
  PlatformProvider,
27
27
  PlatformProviderFactoryInput,
@@ -38,6 +38,33 @@ import type {
38
38
  } from './provider-contract.ts';
39
39
  import { resourceIdentityNumber, resourceIdentityString } from './resource-identity.ts';
40
40
  import { extractPullRequestFileNames, syncLabelDelta } from './in-label-sync.ts';
41
+ import type { PlatformError } from './types.ts';
42
+
43
+ function providerError(error: PlatformError | ProviderError): ProviderError {
44
+ return { code: error.code, message: error.message };
45
+ }
46
+
47
+ function publicProviderErrors(provider: PlatformProvider): PlatformProvider {
48
+ const sanitize = async (call: () => Promise<unknown>): Promise<unknown> => {
49
+ const result = await call();
50
+ if (!result || typeof result !== 'object' || (result as { ok?: unknown }).ok !== false) return result;
51
+ const envelope = result as { error?: PlatformError | ProviderError };
52
+ return envelope.error ? { ...(result as Record<string, unknown>), error: providerError(envelope.error) } : result;
53
+ };
54
+ const wrapped: PlatformProvider = { ...provider, context: { ...provider.context } };
55
+ wrapped.context.resolve = (input) => sanitize(() => provider.context.resolve(input)) as ReturnType<PlatformProvider['context']['resolve']>;
56
+ for (const groupName of ['issues', 'comments', 'changeRequests', 'checks', 'reviews', 'releases', 'securityAlerts', 'repositoryMetadata', 'verification'] as const) {
57
+ const group = provider[groupName];
58
+ if (!group) continue;
59
+ const target: Record<string, unknown> = { ...group };
60
+ for (const method of Object.keys(group)) {
61
+ const original = group[method as keyof typeof group] as (...args: never[]) => Promise<unknown>;
62
+ target[method] = (input: unknown) => sanitize(() => original(input as never));
63
+ }
64
+ (wrapped as unknown as Record<string, unknown>)[groupName] = target;
65
+ }
66
+ return wrapped;
67
+ }
41
68
 
42
69
  const CURRENT_USER_QUERY = 'query { viewer { login } }';
43
70
  const ISSUE_TYPES_QUERY = `query($owner:String!){organization(login:$owner){issueTypes(first:20){nodes{id name pinnedFields{__typename ... on IssueFieldSingleSelect{id name options{id name}} ... on IssueFieldDate{id name} ... on IssueFieldText{id name} ... on IssueFieldNumber{id name}}}}}}`;
@@ -105,7 +132,6 @@ function resolveGitHubChangeRequestGitEvidence({
105
132
  error: {
106
133
  code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
107
134
  message: 'Pull request repository identity is inconsistent',
108
- retryable: false
109
135
  }
110
136
  };
111
137
  }
@@ -127,7 +153,6 @@ function resolveGitHubChangeRequestGitEvidence({
127
153
  error: {
128
154
  code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
129
155
  message: 'Pull request Git refs are invalid',
130
- retryable: false
131
156
  }
132
157
  };
133
158
  }
@@ -145,7 +170,6 @@ function resolveGitHubChangeRequestGitEvidence({
145
170
  error: {
146
171
  code: 'PR_MERGE_EVIDENCE_SOURCE_UNAVAILABLE',
147
172
  message: `No GitHub remote can provide evidence for ${repository}`,
148
- retryable: false
149
173
  }
150
174
  };
151
175
  }
@@ -155,8 +179,8 @@ function resolveGitHubChangeRequestGitEvidence({
155
179
  };
156
180
  }
157
181
 
158
- function failure(error: { code: string; message: string; retryable: boolean }): ProviderResult<PlatformContextSnapshot> {
159
- return { ok: false, error };
182
+ function failure(error: PlatformError | ProviderError): ProviderResult<PlatformContextSnapshot> {
183
+ return { ok: false, error: providerError(error) };
160
184
  }
161
185
 
162
186
  function resolveContext(
@@ -170,19 +194,17 @@ function resolveContext(
170
194
  return failure({
171
195
  code: 'GH_CLI_VERSION_UNSUPPORTED',
172
196
  message: 'GitHub CLI ' + version.value + ' is unsupported; install gh >= ' + MINIMUM_GITHUB_CLI_VERSION,
173
- retryable: false
174
197
  });
175
198
  }
176
199
  const remote = input.gitRemote || defaultGitRemote(cwd);
177
200
  if (!remote) {
178
- return failure({ code: 'REMOTE_MISSING', message: 'Git origin remote is not configured', retryable: false });
201
+ return failure({ code: 'REMOTE_MISSING', message: 'Git origin remote is not configured' });
179
202
  }
180
203
  const ownerRepo = parseGitHubRemote(remote);
181
204
  if (!ownerRepo) {
182
205
  return failure({
183
206
  code: /github\.com/i.test(remote) ? 'REMOTE_INVALID' : 'PLATFORM_UNSUPPORTED',
184
207
  message: 'Unable to parse GitHub owner/repo from configured remote',
185
- retryable: false
186
208
  });
187
209
  }
188
210
  const repository = client.json(['api', 'repos/' + ownerRepo], { cwd });
@@ -192,7 +214,6 @@ function resolveContext(
192
214
  if (!upstream) return failure({
193
215
  code: 'UPSTREAM_UNRESOLVED',
194
216
  message: 'Unable to resolve the upstream repository',
195
- retryable: false
196
217
  });
197
218
  const user = client.json(['api', 'graphql', '-f', 'query=' + CURRENT_USER_QUERY], { cwd });
198
219
  if (!user.ok) return failure(user.error);
@@ -241,18 +262,18 @@ function repositoryHead(cwd: string): string {
241
262
 
242
263
  function verifyRemoteBranch(cwd: string, repositoryName: string, head: string):
243
264
  | { ok: true; value: string }
244
- | { ok: false; error: { code: string; message: string; retryable: boolean } } {
265
+ | { ok: false; error: ProviderError } {
245
266
  const wanted = expectedHeadRepository(repositoryName, head);
246
267
  if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref)) return {
247
268
  ok: false,
248
- error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
269
+ error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid' }
249
270
  };
250
271
  const remotes = configuredGitRemotes(cwd);
251
272
  const remote = remotes.find((item) => item.repository.toLowerCase() === wanted.repository.toLowerCase())
252
273
  ?? remotes.find((item) => item.name === 'origin');
253
274
  if (!remote) return {
254
275
  ok: false,
255
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
276
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}` }
256
277
  };
257
278
  try {
258
279
  const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
@@ -262,19 +283,19 @@ function verifyRemoteBranch(cwd: string, repositoryName: string, head: string):
262
283
  const sha = match?.split(/\s+/)[0] || null;
263
284
  if (!sha || !/^[a-f0-9]{40}$/i.test(sha)) return {
264
285
  ok: false,
265
- error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
286
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist` }
266
287
  };
267
288
  return { ok: true, value: sha };
268
289
  } catch (error) {
269
290
  return {
270
291
  ok: false,
271
- error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
292
+ error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) }
272
293
  };
273
294
  }
274
295
  }
275
296
 
276
297
  function invalid(code: string, message: string): ProviderResult<never> {
277
- return { ok: false, error: { code, message, retryable: false } };
298
+ return { ok: false, error: { code, message } };
278
299
  }
279
300
 
280
301
  function changeRequestSnapshot(value: any): ChangeRequestSnapshot {
@@ -346,8 +367,8 @@ function createReceipt(remoteId: string): ProviderResult<MutationReceipt> {
346
367
  return { ok: true, value: { changed: true, remoteId } };
347
368
  }
348
369
 
349
- function partialFailure<T>(response: { ok: false; error: PlatformError }, value: T): ProviderResult<T> {
350
- return { ok: false, error: response.error, value };
370
+ function partialFailure<T>(response: { ok: false; error: PlatformError | ProviderError }, value: T): ProviderResult<T> {
371
+ return { ok: false, error: providerError(response.error), value };
351
372
  }
352
373
 
353
374
  function labelReconciliation(
@@ -390,8 +411,8 @@ function syncLabels(
390
411
  status: 'blocked' as const,
391
412
  changed: true,
392
413
  error: synced.error
393
- ? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
394
- : { code: 'IN_LABEL_SYNC_PARTIAL', message: 'In-label synchronization is partial or unknown', retryable: true }
414
+ ? { ...providerError(synced.error), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
415
+ : { code: 'IN_LABEL_SYNC_PARTIAL', message: 'In-label synchronization is partial or unknown' }
395
416
  };
396
417
  return synced;
397
418
  }
@@ -642,13 +663,13 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
642
663
  try {
643
664
  localHead = repositoryHead(context.workingDirectory);
644
665
  } catch (error) {
645
- return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
666
+ return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error) } };
646
667
  }
647
668
  const remote = verifyRemoteBranch(context.workingDirectory, repository(context), head);
648
669
  if (!remote.ok) return remote;
649
670
  if (remote.value !== localHead) return {
650
671
  ok: false,
651
- error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
672
+ error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}` }
652
673
  };
653
674
  return { ok: true, value: { sha: localHead } };
654
675
  },
@@ -909,7 +930,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
909
930
  try {
910
931
  const tag = resourceIdentityString(release) || '';
911
932
  const result = module.publishGitHubReleaseNotes({ repository: repository(context), tag, title, notesFile }, { cwd: context.workingDirectory, client });
912
- if (result.status === 'failed' || result.status === 'blocked') return { ok: false, error: result.error || { code: 'RELEASE_NOTES_PUBLISH_FAILED', message: 'Release notes publish failed', retryable: false } };
933
+ if (result.status === 'failed' || result.status === 'blocked') return { ok: false, error: result.error ? providerError(result.error) : { code: 'RELEASE_NOTES_PUBLISH_FAILED', message: 'Release notes publish failed' } };
913
934
  return { ok: true, value: { changed: result.changed, remoteId: tag } };
914
935
  } finally {
915
936
  fs.rmSync(temporaryRoot, { recursive: true, force: true });
@@ -921,7 +942,7 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
921
942
  repository: repository(context), commitOids, branch, historyLimit, fromTime, toTime
922
943
  }, { cwd: context.workingDirectory, client });
923
944
  if (collected.status === 'failed' || collected.status === 'blocked' || !('pullRequests' in collected)) {
924
- return { ok: false, error: collected.error || { code: 'RELEASE_NOTES_COLLECTION_FAILED', message: 'Release notes could not be collected', retryable: false } };
945
+ return { ok: false, error: collected.error ? providerError(collected.error) : { code: 'RELEASE_NOTES_COLLECTION_FAILED', message: 'Release notes could not be collected' } };
925
946
  }
926
947
  const mergedPullRequests = collected.pullRequests.map((item: any) => ({
927
948
  id: String(item.number), identity: { kind: 'number' as const, value: Number(item.number) }, number: Number(item.number),
@@ -1107,7 +1128,7 @@ function createGitHubProvider(
1107
1128
  input: PlatformProviderFactoryInput,
1108
1129
  client: GitHubClient = createGitHubClient()
1109
1130
  ): PlatformProvider {
1110
- return {
1131
+ return publicProviderErrors({
1111
1132
  type: input.providerType,
1112
1133
  contractVersion: 2,
1113
1134
  identity: { issue: 'number', 'pull-request': 'number', comment: 'number', release: 'key' },
@@ -1117,7 +1138,7 @@ function createGitHubProvider(
1117
1138
  return resolveContext(client, contextInput);
1118
1139
  }
1119
1140
  }
1120
- };
1141
+ });
1121
1142
  }
1122
1143
 
1123
1144
  export {
@@ -1,5 +1,6 @@
1
1
  import type { GitHubClient } from './github-client.ts';
2
2
  import { createGitHubClient } from './github-client.ts';
3
+ import { providerErrorRetryable } from './provider-validation.ts';
3
4
 
4
5
  type GitHubActorInput = {
5
6
  name?: string | null;
@@ -43,13 +44,14 @@ function normalizePlatformAuthor(author: { login?: string | null } | null | unde
43
44
  };
44
45
  }
45
46
 
46
- function failure(error: { code: string; message: string; retryable: boolean }) {
47
+ function failure(error: { code: string; message: string; retryable?: boolean }) {
48
+ const retryable = error.retryable ?? providerErrorRetryable(error.code);
47
49
  return {
48
- status: error.retryable ? 'blocked' as const : 'failed' as const,
50
+ status: retryable ? 'blocked' as const : 'failed' as const,
49
51
  changed: false,
50
52
  operation: null,
51
53
  url: null,
52
- error
54
+ error: { code: error.code, message: error.message }
53
55
  };
54
56
  }
55
57
 
@@ -1,5 +1,6 @@
1
1
  import { computeInLabels } from './metadata-labels.ts';
2
2
  import { createHash } from 'node:crypto';
3
+ import { providerErrorRetryable } from './provider-validation.ts';
3
4
  import type { PlatformError } from './types.ts';
4
5
 
5
6
  type InLabelPlan = {
@@ -150,7 +151,7 @@ function syncLabelDelta(
150
151
  const delta = labelDelta(current, target, prefix);
151
152
  let changed = false;
152
153
  const failure = (error: PlatformError): LabelDeltaResult => {
153
- if (changed || error.retryable) {
154
+ if (changed || error.retryable || providerErrorRetryable(error.code)) {
154
155
  return {
155
156
  status: 'blocked',
156
157
  changed,