@fitlab-ai/agent-infra 0.11.8 → 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 (163) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +0 -6
  4. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  5. package/dist/lib/agent-clients/next-steps.js +1 -1
  6. package/dist/lib/agent-clients/reconcile.js +3 -3
  7. package/dist/lib/agent-clients/schema.js +18 -10
  8. package/dist/lib/git/workflow.js +1 -1
  9. package/dist/lib/init.js +2 -2
  10. package/dist/lib/internal/agent-client.js +3 -3
  11. package/dist/lib/internal/cli-route-inventory.js +0 -3
  12. package/dist/lib/internal/sandbox-control.js +30 -5
  13. package/dist/lib/internal/task-finalization.js +14 -23
  14. package/dist/lib/internal/task-operation-registry.js +2 -3
  15. package/dist/lib/internal/task-platform-recovery.js +9 -4
  16. package/dist/lib/platform/completion-backfill.js +2 -1
  17. package/dist/lib/platform/context.js +4 -2
  18. package/dist/lib/platform/github-client.js +20 -17
  19. package/dist/lib/platform/github-provider.js +42 -22
  20. package/dist/lib/platform/github-release-notes.js +4 -2
  21. package/dist/lib/platform/in-label-sync.js +2 -1
  22. package/dist/lib/platform/issue-comments.js +13 -9
  23. package/dist/lib/platform/operation-coordinator.js +1 -1
  24. package/dist/lib/platform/pr-review.js +2 -1
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +4 -2
  30. package/dist/lib/sandbox/control/client.js +54 -6
  31. package/dist/lib/sandbox/control/executor.js +8 -3
  32. package/dist/lib/sandbox/recovery.js +1 -22
  33. package/dist/lib/sandbox/removal.js +0 -1
  34. package/dist/lib/sandbox/tools.js +2 -0
  35. package/dist/lib/task/artifact-command.js +11 -1
  36. package/dist/lib/task/artifact-lifecycle.js +119 -62
  37. package/dist/lib/task/artifact-selection.js +1 -1
  38. package/dist/lib/task/capabilities.js +55 -21
  39. package/dist/lib/task/commands/status.js +0 -27
  40. package/dist/lib/task/commit-identity.js +22 -7
  41. package/dist/lib/task/commit-intent.js +67 -26
  42. package/dist/lib/task/commit-operation.js +19 -8
  43. package/dist/lib/task/completion-facts.js +32 -10
  44. package/dist/lib/task/control-authority.js +2 -4
  45. package/dist/lib/task/current-contract.js +1 -5
  46. package/dist/lib/task/events.js +8 -274
  47. package/dist/lib/task/execution-inputs.js +14 -0
  48. package/dist/lib/task/finalization-envelope.js +4 -0
  49. package/dist/lib/task/finalization.js +100 -129
  50. package/dist/lib/task/frontmatter.js +1 -1
  51. package/dist/lib/task/ledger-intents.js +4 -7
  52. package/dist/lib/task/orchestration.js +11 -10
  53. package/dist/lib/task/platform-operation-journal.js +17 -10
  54. package/dist/lib/task/platform-operation-recovery.js +95 -28
  55. package/dist/lib/task/review-fingerprint.js +4 -26
  56. package/dist/lib/task/verification-engine.js +95 -6
  57. package/dist/lib/task/verification.js +0 -1
  58. package/dist/lib/task/write.js +0 -11
  59. package/dist/lib/update.js +2 -2
  60. package/dist/lifecycle-build-manifest.json +3 -3
  61. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  62. package/lib/agent-clients/next-steps.ts +3 -3
  63. package/lib/agent-clients/reconcile.ts +3 -3
  64. package/lib/agent-clients/schema.ts +18 -10
  65. package/lib/git/workflow.ts +1 -1
  66. package/lib/init.ts +2 -2
  67. package/lib/internal/agent-client.ts +3 -3
  68. package/lib/internal/cli-route-inventory.ts +0 -2
  69. package/lib/internal/sandbox-control.ts +39 -6
  70. package/lib/internal/task-finalization.ts +16 -24
  71. package/lib/internal/task-operation-registry.ts +2 -3
  72. package/lib/internal/task-platform-recovery.ts +9 -4
  73. package/lib/platform/completion-backfill.ts +2 -1
  74. package/lib/platform/context.ts +8 -4
  75. package/lib/platform/github-client.ts +24 -20
  76. package/lib/platform/github-provider.ts +47 -26
  77. package/lib/platform/github-release-notes.ts +5 -3
  78. package/lib/platform/in-label-sync.ts +2 -1
  79. package/lib/platform/issue-comments.ts +14 -9
  80. package/lib/platform/operation-coordinator.ts +1 -1
  81. package/lib/platform/pr-review.ts +2 -1
  82. package/lib/platform/provider-bridge.ts +10 -9
  83. package/lib/platform/provider-contract.ts +6 -21
  84. package/lib/platform/provider-loader.ts +7 -6
  85. package/lib/platform/provider-validation.ts +30 -8
  86. package/lib/platform/pull-requests.ts +4 -2
  87. package/lib/platform/repository-metadata.ts +3 -2
  88. package/lib/platform/security-alerts.ts +1 -1
  89. package/lib/platform/types.ts +2 -0
  90. package/lib/sandbox/control/client.ts +57 -6
  91. package/lib/sandbox/control/executor.ts +8 -3
  92. package/lib/sandbox/recovery.ts +0 -25
  93. package/lib/sandbox/removal.ts +2 -10
  94. package/lib/sandbox/tool-types.ts +1 -0
  95. package/lib/sandbox/tools.ts +2 -0
  96. package/lib/task/artifact-command.ts +10 -1
  97. package/lib/task/artifact-lifecycle.ts +100 -52
  98. package/lib/task/artifact-selection.ts +1 -1
  99. package/lib/task/capabilities.ts +47 -24
  100. package/lib/task/commands/status.ts +0 -31
  101. package/lib/task/commit-identity.ts +24 -7
  102. package/lib/task/commit-intent.ts +58 -24
  103. package/lib/task/commit-operation.ts +17 -8
  104. package/lib/task/completion-facts.ts +31 -9
  105. package/lib/task/control-authority.ts +1 -4
  106. package/lib/task/current-contract.ts +1 -6
  107. package/lib/task/events.ts +8 -247
  108. package/lib/task/execution-inputs.ts +25 -0
  109. package/lib/task/finalization-envelope.ts +21 -0
  110. package/lib/task/finalization.ts +101 -123
  111. package/lib/task/frontmatter.ts +1 -1
  112. package/lib/task/ledger-intents.ts +4 -7
  113. package/lib/task/orchestration.ts +11 -17
  114. package/lib/task/platform-operation-journal.ts +18 -11
  115. package/lib/task/platform-operation-recovery.ts +90 -31
  116. package/lib/task/review-fingerprint.ts +4 -26
  117. package/lib/task/verification-engine.ts +103 -9
  118. package/lib/task/verification.ts +1 -2
  119. package/lib/task/write.ts +0 -19
  120. package/lib/update.ts +2 -3
  121. package/package.json +1 -1
  122. package/templates/.agents/README.en.md +2 -50
  123. package/templates/.agents/README.zh-CN.md +2 -50
  124. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  125. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  126. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  127. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  128. package/templates/.agents/rules/next-step-output.en.md +1 -1
  129. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  130. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  131. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  132. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +26 -9
  133. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  134. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  135. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  136. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  137. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  138. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  139. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  142. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  143. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  144. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  145. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  149. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  150. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  151. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +2 -268
  152. package/templates/.agents/templates/task.zh-CN.md +0 -12
  153. package/templates/.github/workflows/pr-label.yml +1 -2
  154. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  155. package/dist/lib/internal/task-invalidation.js +0 -63
  156. package/dist/lib/task/intermediate-cleanup.js +0 -327
  157. package/dist/lib/task/invalidation-command.js +0 -88
  158. package/dist/lib/task/invalidation.js +0 -242
  159. package/lib/agent-clients/custom-tuis.ts +0 -149
  160. package/lib/internal/task-invalidation.ts +0 -41
  161. package/lib/task/intermediate-cleanup.ts +0 -393
  162. package/lib/task/invalidation-command.ts +0 -88
  163. package/lib/task/invalidation.ts +0 -313
@@ -43,16 +43,24 @@ const AGENT_CLIENTS_SCHEMA = {
43
43
  }
44
44
  }))
45
45
  },
46
- customTUIs: {
47
- type: 'array',
48
- items: {
49
- type: 'object',
50
- additionalProperties: true,
51
- required: ['name', 'dir', 'invoke'],
52
- properties: {
53
- name: { type: 'string' },
54
- dir: { type: 'string' },
55
- invoke: { type: 'string' }
46
+ sandbox: {
47
+ type: 'object',
48
+ properties: {
49
+ tools: {
50
+ type: 'object',
51
+ properties: {
52
+ ids: { type: 'array', items: { type: 'string' } },
53
+ definitions: {
54
+ type: 'object',
55
+ additionalProperties: {
56
+ type: 'object',
57
+ properties: {
58
+ name: { type: 'string' },
59
+ invoke: { type: 'string' }
60
+ }
61
+ }
62
+ }
63
+ }
56
64
  }
57
65
  }
58
66
  }
@@ -100,7 +100,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
100
100
  const tree = value(runner, input.cwd, ['write-tree']);
101
101
  if (tree !== input.expectedTree) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'applied' as const }], error: { code: 'GIT_TREE_MISMATCH', message: `Expected staged tree ${input.expectedTree}, received ${tree ?? 'unavailable'}` } };
102
102
  }
103
- const commit = run(runner, input.cwd, ['commit', '-m', input.message]);
103
+ const commit = run(runner, input.cwd, ['commit', '--cleanup=verbatim', '-m', input.message]);
104
104
  const after = inspectGitWorkflow(input.cwd, runner);
105
105
  if (commit.status !== 0) return { status: 'failed' as const, changed: false, snapshot: after.snapshot, operations: [{ name: 'commit', status: 'failed' as const, message: commit.stderr.trim() }], error: { code: 'GIT_COMMIT_FAILED', message: commit.stderr.trim() } };
106
106
  return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
package/lib/init.ts CHANGED
@@ -314,8 +314,8 @@ async function cmdInit(): Promise<void> {
314
314
  console.log(' npm install -g @fitlab-ai/agent-infra');
315
315
  console.log('');
316
316
  if (workflowPlan.nextSteps.length === 0) {
317
- console.log(' No Agent Client project integration enabled.');
318
- console.log(` Configure "customTUIs" in ${configPath} before running update-agent-infra.`);
317
+ console.log(' No Agent Client or sandbox custom tool integration enabled.');
318
+ console.log(` Configure a custom tool in "sandbox.tools.definitions" and select it in "sandbox.tools.ids" in ${configPath}.`);
319
319
  console.log('');
320
320
  } else {
321
321
  console.log(' Next step: open this project in any AI TUI and run:');
@@ -5,7 +5,7 @@ import {
5
5
  AgentClientConfigError,
6
6
  normalizeAgentClients
7
7
  } from '../agent-clients/config.ts';
8
- import { normalizeCustomTUIs } from '../agent-clients/custom-tuis.ts';
8
+ import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.ts';
9
9
  import { renderNextStepCommands } from '../agent-clients/next-steps.ts';
10
10
  import { getAgentClientModelSelection } from '../agent-clients/registry.ts';
11
11
  import { isAgentClientId } from '../agent-clients/types.ts';
@@ -148,11 +148,11 @@ function agentClient(args: string[] = []): void {
148
148
 
149
149
  try {
150
150
  const clients = normalizeAgentClients(config);
151
- const custom = normalizeCustomTUIs(process.cwd(), config.customTUIs ?? []);
151
+ const custom = normalizeCustomToolInvocations(config);
152
152
  const commands = renderNextStepCommands({
153
153
  projectName: String(config.project ?? ''),
154
154
  state: clients.state,
155
- customTUIs: custom.items,
155
+ customToolInvocations: custom.items,
156
156
  skillName: parsed.skillName,
157
157
  ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
158
158
  ...(parsed.version ? { version: parsed.version } : {})
@@ -60,7 +60,6 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
60
60
  'task-orchestration': ['status', 'progress'],
61
61
  'task-review': ['preflight', 'finalize-summary'],
62
62
  'task-event': ['event'],
63
- 'task-invalidation': ['reconcile'],
64
63
  'task-lifecycle': ['intent'],
65
64
  'task-finalization': ['complete'],
66
65
  'task-platform-recovery': ['inspect', 'recover'],
@@ -98,7 +97,6 @@ export function internalRouteSelector(command: string, args: readonly string[]):
98
97
  if (command === 'task-short-id') return first(args) === 'list' && args.includes('--verify') ? 'list-verify' : first(args);
99
98
  if (command === 'task-orchestration') return args[1] === 'status' ? 'status' : args[1] ? 'progress' : '';
100
99
  if (command === 'task-review') return args[1] ?? '';
101
- if (command === 'task-invalidation') return args[1] ?? '';
102
100
  if (command === 'task-artifact') return args[1] ?? '';
103
101
  if (command === 'platform-pr-review' && first(args) === 'publish') {
104
102
  const scope = optionValue(args, '--scope') ?? '';
@@ -9,6 +9,7 @@ import { serveSandboxControl } from '../sandbox/control/server.ts';
9
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
11
11
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
12
+ import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
12
13
 
13
14
  type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
14
15
 
@@ -16,10 +17,11 @@ function writeFinalizationEnvelope(
16
17
  status: FinalizationStatus,
17
18
  accepted: boolean,
18
19
  error: { code: string; message: string; retryable: boolean },
19
- changed = false,
20
- result: unknown = null
20
+ changed: boolean | null = false,
21
+ result: unknown = null,
22
+ requestId: string | null = null
21
23
  ): void {
22
- process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
24
+ process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
23
25
  }
24
26
 
25
27
  function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
@@ -43,6 +45,28 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
43
45
  return response.exitCode ?? 1;
44
46
  }
45
47
 
48
+ function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
49
+ try {
50
+ const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
51
+ if (value.version === 2
52
+ && value.status
53
+ && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
54
+ && (typeof value.changed === 'boolean' || value.changed === null)) {
55
+ return serializeTaskFinalizationEnvelope({
56
+ status: value.status,
57
+ changed: value.changed,
58
+ accepted: true,
59
+ requestId: response.id,
60
+ result: value.result ?? null,
61
+ error: value.error ?? null
62
+ });
63
+ }
64
+ } catch {
65
+ // Preserve non-envelope output for diagnostics from the executor.
66
+ }
67
+ return response.stdout;
68
+ }
69
+
46
70
  function sandboxFinalizationClient(args: string[]): void {
47
71
  if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
48
72
  const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
@@ -63,7 +87,8 @@ function sandboxFinalizationClient(args: string[]): void {
63
87
  response = requestSandboxTaskFinalization({ agent });
64
88
  } catch (error) {
65
89
  if (!(error instanceof SandboxControlClientError)) throw error;
66
- writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
90
+ const status = finalizationErrorStatus(error.detail);
91
+ writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
67
92
  writeClientError(error);
68
93
  process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
69
94
  return;
@@ -74,12 +99,20 @@ function sandboxFinalizationClient(args: string[]): void {
74
99
  message: 'sandbox control rejected the finalization request',
75
100
  retryable: false
76
101
  };
77
- writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
102
+ const status = finalizationErrorStatus(error);
103
+ writeFinalizationEnvelope(
104
+ status,
105
+ error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
106
+ error,
107
+ status === 'unknown' ? null : false,
108
+ null,
109
+ response.id
110
+ );
78
111
  process.stderr.write(`${error.message}\n`);
79
112
  process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
80
113
  return;
81
114
  }
82
- process.stdout.write(response.stdout);
115
+ process.stdout.write(finalizationResponseOutput(response));
83
116
  process.stderr.write(response.stderr);
84
117
  process.exitCode = response.exitCode ?? 1;
85
118
  }
@@ -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 };