@fitlab-ai/agent-infra 0.11.10 → 0.11.11

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 (132) hide show
  1. package/dist/lib/agent-clients/adapter.js +20 -0
  2. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  3. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  4. package/dist/lib/internal/manual-validation.js +2 -2
  5. package/dist/lib/internal/platform-pr.js +4 -5
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-create.js +15 -1
  8. package/dist/lib/internal/task-finalization.js +1 -3
  9. package/dist/lib/internal/task-operation-registry.js +4 -138
  10. package/dist/lib/platform/pr-summary.js +45 -8
  11. package/dist/lib/platform/pull-requests.js +79 -14
  12. package/dist/lib/platform/verification-sync.js +17 -14
  13. package/dist/lib/sandbox/commands/create.js +18 -2
  14. package/dist/lib/sandbox/commands/enter.js +47 -33
  15. package/dist/lib/sandbox/control/client.js +46 -12
  16. package/dist/lib/sandbox/control/executor.js +4 -11
  17. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  18. package/dist/lib/sandbox/control/protocol.js +2 -3
  19. package/dist/lib/sandbox/control/server.js +42 -2
  20. package/dist/lib/sandbox/environment.js +150 -0
  21. package/dist/lib/sandbox/recovery.js +8 -0
  22. package/dist/lib/sandbox/workspace-identity.js +0 -14
  23. package/dist/lib/sandbox/workspace-view.js +33 -6
  24. package/dist/lib/task/artifact-command.js +7 -2
  25. package/dist/lib/task/artifact-lifecycle.js +2 -1
  26. package/dist/lib/task/capabilities.js +4 -1
  27. package/dist/lib/task/control-authority.js +0 -2
  28. package/dist/lib/task/events.js +1 -1
  29. package/dist/lib/task/finalization.js +37 -54
  30. package/dist/lib/task/frontmatter.js +24 -1
  31. package/dist/lib/task/ledger-intents.js +2 -1
  32. package/dist/lib/task/platform-operation-journal.js +12 -0
  33. package/dist/lib/task/platform-operation-recovery.js +14 -7
  34. package/dist/lib/task/resolve-ref.js +8 -3
  35. package/dist/lifecycle-build-manifest.json +3 -3
  36. package/lib/agent-clients/adapter.ts +22 -0
  37. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  38. package/lib/agent-clients/adapters/codex.ts +4 -0
  39. package/lib/internal/manual-validation.ts +2 -2
  40. package/lib/internal/platform-pr.ts +3 -4
  41. package/lib/internal/task-artifact.ts +1 -1
  42. package/lib/internal/task-create.ts +15 -2
  43. package/lib/internal/task-finalization.ts +1 -3
  44. package/lib/internal/task-operation-registry.ts +4 -150
  45. package/lib/platform/pr-summary.ts +43 -7
  46. package/lib/platform/pull-requests.ts +74 -13
  47. package/lib/platform/verification-sync.ts +18 -14
  48. package/lib/sandbox/commands/create.ts +24 -2
  49. package/lib/sandbox/commands/enter.ts +42 -39
  50. package/lib/sandbox/control/client.ts +52 -15
  51. package/lib/sandbox/control/executor.ts +5 -12
  52. package/lib/sandbox/control/host-recovery.ts +195 -0
  53. package/lib/sandbox/control/protocol.ts +4 -5
  54. package/lib/sandbox/control/server.ts +42 -1
  55. package/lib/sandbox/environment.ts +162 -0
  56. package/lib/sandbox/recovery.ts +10 -0
  57. package/lib/sandbox/tool-types.ts +1 -0
  58. package/lib/sandbox/workspace-identity.ts +0 -26
  59. package/lib/sandbox/workspace-view.ts +33 -8
  60. package/lib/task/artifact-command.ts +9 -2
  61. package/lib/task/artifact-lifecycle.ts +2 -1
  62. package/lib/task/capabilities.ts +5 -2
  63. package/lib/task/control-authority.ts +0 -4
  64. package/lib/task/events.ts +1 -1
  65. package/lib/task/finalization.ts +34 -51
  66. package/lib/task/frontmatter.ts +25 -1
  67. package/lib/task/ledger-intents.ts +2 -1
  68. package/lib/task/platform-operation-journal.ts +29 -2
  69. package/lib/task/platform-operation-recovery.ts +12 -6
  70. package/lib/task/resolve-ref.ts +8 -3
  71. package/package.json +1 -1
  72. package/templates/.agents/rules/README.en.md +1 -1
  73. package/templates/.agents/rules/README.zh-CN.md +1 -1
  74. package/templates/.agents/rules/issue-sync.en.md +1 -1
  75. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  76. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  77. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  78. package/templates/.agents/rules/review-handshake.en.md +3 -0
  79. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  80. package/templates/.agents/rules/task-management.en.md +1 -1
  81. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  82. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  83. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  84. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  85. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  86. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  87. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  88. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  89. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  90. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  91. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  92. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  93. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  94. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  95. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  96. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  97. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
  98. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
  99. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  100. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  101. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  102. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  103. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  104. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  105. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  106. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  107. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  108. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  109. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  110. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  111. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  112. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  116. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  117. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  118. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  119. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  120. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  121. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  122. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  123. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
  124. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
  125. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  126. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  127. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  128. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  129. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  130. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  131. package/dist/lib/task/finalization-handoff.js +0 -93
  132. package/lib/task/finalization-handoff.ts +0 -105
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { AGENT_CLIENT_CAPABILITY_IDS, AGENT_CLIENT_SUPPORT_LEVELS, isAgentClientId } from './types.js';
2
3
  import { SANDBOX_HOOK_PHASES } from '../sandbox/tool-types.js';
3
4
  const PROJECT_ASSET_CATEGORIES = ['managed', 'merged', 'ejected'];
@@ -65,6 +66,24 @@ function freezeTool(tool) {
65
66
  if (!tool.containerMount.startsWith('/')) {
66
67
  throw new Error(`Sandbox tool '${tool.id}' containerMount must be absolute`);
67
68
  }
69
+ if (tool.hostStateMounts !== undefined && !Array.isArray(tool.hostStateMounts)) {
70
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must be an array`);
71
+ }
72
+ const hostStateMounts = tool.hostStateMounts?.map((entry) => {
73
+ if (!entry || typeof entry.hostSubdir !== 'string' || typeof entry.containerSubpath !== 'string') {
74
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must contain relative path pairs`);
75
+ }
76
+ for (const value of [entry.hostSubdir, entry.containerSubpath]) {
77
+ const normalized = path.posix.normalize(value);
78
+ if (value === ''
79
+ || path.posix.isAbsolute(value)
80
+ || normalized === '..'
81
+ || normalized.startsWith('../')) {
82
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must stay within the tool directories`);
83
+ }
84
+ }
85
+ return Object.freeze({ ...entry });
86
+ });
68
87
  if (!tool.versionCmd) {
69
88
  throw new Error(`Sandbox tool '${tool.id}' requires versionCmd`);
70
89
  }
@@ -82,6 +101,7 @@ function freezeTool(tool) {
82
101
  ...(tool.hostLiveMounts ? {
83
102
  hostLiveMounts: Object.freeze(tool.hostLiveMounts.map((entry) => Object.freeze({ ...entry })))
84
103
  } : {}),
104
+ ...(hostStateMounts ? { hostStateMounts: Object.freeze(hostStateMounts) } : {}),
85
105
  ...(tool.postSetupCmds ? { postSetupCmds: Object.freeze([...tool.postSetupCmds]) } : {}),
86
106
  ...(tool.tmpfs ? {
87
107
  tmpfs: Object.freeze({
@@ -241,6 +241,19 @@ const codexRecoveryChecks = [
241
241
  path: '/home/devuser/.codex'
242
242
  }
243
243
  },
244
+ {
245
+ id: 'app-server-package-writable',
246
+ probe: {
247
+ script: 'probe="$1/.agent-infra-app-server-package-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
248
+ args: ['/home/devuser/.codex/packages/app-server-daemon'],
249
+ user: 'devuser'
250
+ },
251
+ finding: {
252
+ repairKind: 'hard-failure',
253
+ message: 'Codex app-server package directory is not writable by devuser.',
254
+ path: '/home/devuser/.codex/packages/app-server-daemon'
255
+ }
256
+ },
244
257
  {
245
258
  id: 'prompts-link',
246
259
  when: {
@@ -51,6 +51,10 @@ const codexAdapter = defineAgentClientAdapter({
51
51
  versionCmd: 'codex --version',
52
52
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
53
53
  tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
54
+ hostStateMounts: [{
55
+ hostSubdir: 'packages/app-server-daemon',
56
+ containerSubpath: 'packages/app-server-daemon'
57
+ }],
54
58
  hostLiveMounts: [
55
59
  {
56
60
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -196,7 +196,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
196
196
  if (transaction.phase === 'prepared') {
197
197
  const staged = await syncPullRequestSummary(taskRef, {
198
198
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
199
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
199
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
200
200
  });
201
201
  if (!['applied', 'no-op'].includes(staged.status))
202
202
  transactionFailure(staged.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_FAILED', message: 'pending summary staging failed' });
@@ -261,7 +261,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
261
261
  try {
262
262
  await syncPullRequestSummary(taskRef, {
263
263
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult: primaryResult, strict: true,
264
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
264
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
265
265
  });
266
266
  }
267
267
  catch {
@@ -10,7 +10,7 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
10
10
  agent-infra-internal platform-pr bind <task-ref> --pr <token> --agent <agent> [--dry-run] [--cwd <path>]
11
11
  agent-infra-internal platform-pr skip <task-ref> --agent <agent> [--dry-run] [--cwd <path>]
12
12
  agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
13
- agent-infra-internal platform-pr sync-in-labels --pr <N> [--dry-run] [--cwd <path>]
13
+ agent-infra-internal platform-pr sync-in-labels --pr <token> [--dry-run] [--cwd <path>]
14
14
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
15
15
  agent-infra-internal platform-pr change-report <task-ref> --agent <agent> --mechanical-file <path> --precheck-file <path> [--dry-run] [--cwd <path>]
16
16
  agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> --change-report-file <path> --result <pr_created|pr_reused|no_op> [--manual-phase pending|final ...] [--strict] [--dry-run] [--cwd <path>]
@@ -91,13 +91,12 @@ async function platformPr(args = []) {
91
91
  fail(`sync-in-labels does not accept --${unexpected}`);
92
92
  return;
93
93
  }
94
- const pr = Number(values.pr);
95
- if (!Number.isInteger(pr) || pr <= 0) {
96
- fail('sync-in-labels requires a positive --pr');
94
+ if (typeof values.pr !== 'string' || !values.pr || values.pr.trim() !== values.pr) {
95
+ fail('sync-in-labels requires a non-empty --pr token');
97
96
  return;
98
97
  }
99
98
  const cwd = path.resolve(typeof values.cwd === 'string' ? values.cwd : process.cwd());
100
- finish(await syncPlatformPullRequestInLabels(pr, { cwd, dryRun: values.dryRun === true }));
99
+ finish(await syncPlatformPullRequestInLabels(values.pr, { cwd, dryRun: values.dryRun === true }));
101
100
  return;
102
101
  }
103
102
  const taskRef = args[1];
@@ -1,6 +1,6 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
3
- const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
3
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family> [--reason-code <reason>] [--source-finding <id> --source-artifact <name> --source-sha256 <digest>]\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize workflow artifact facts.\n`;
4
4
  function taskArtifact(args = []) {
5
5
  if (!ensureInternalHandlerRoute('task-artifact', args))
6
6
  return;
@@ -54,7 +54,21 @@ async function taskCreate(args) {
54
54
  throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
55
55
  if (stat.size > SANDBOX_CONTROL_MAX_BYTES)
56
56
  throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
57
- const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
57
+ const inputContent = fs.readFileSync(inputPath, 'utf8');
58
+ let candidate;
59
+ try {
60
+ candidate = validateTaskCreateCandidate(JSON.parse(inputContent));
61
+ }
62
+ catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
65
+ output(failed(code, message, false, {
66
+ requestId: null,
67
+ accepted: false,
68
+ recovery: 'new-request-id'
69
+ }));
70
+ return;
71
+ }
58
72
  const environment = resolveSandboxControlTransport();
59
73
  if (environment.kind === 'fail-closed') {
60
74
  output(failed(environment.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID', 'sandbox client control configuration is invalid'));
@@ -65,9 +65,7 @@ async function taskFinalization(args = []) {
65
65
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
66
66
  if (transport.kind === 'sandbox-local') {
67
67
  try {
68
- const response = requestSandboxTaskFinalization({
69
- agent: boundOperation.request.agent
70
- });
68
+ const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
71
69
  process.stdout.write(response.stdout);
72
70
  process.stderr.write(response.stderr);
73
71
  process.exitCode = response.exitCode ?? 1;
@@ -3,7 +3,8 @@ import path from 'node:path';
3
3
  import { accessSandboxTaskView, parseSandboxTaskView, taskViewFromStatus } from '../sandbox/control/task-view.js';
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { PUBLIC_CLI_COMMAND_ALIASES, PUBLIC_CLI_SELECTOR_ALIASES, internalRouteSelector } from './cli-route-inventory.js';
6
- import { readSandboxControlIdentitySentinel } from '../sandbox/control/identity-sentinel.js';
6
+ import { hasSandboxControlMarker, resolveSandboxControlTransport, sandboxTaskMarkerState } from '../sandbox/environment.js';
7
+ export { resolveSandboxControlTransport } from '../sandbox/environment.js';
7
8
  const descriptor = (dispatcher, command, selector, scope, effect, taskRefSource = scope === 'non-task' ? 'none' : 'argv') => ({ dispatcher, command, selector, scope, effect, taskRefSource, guardBeforeImport: true });
8
9
  function internalTaskRoutes() {
9
10
  return [
@@ -232,141 +233,6 @@ export function resolveDelegatedTaskOperation(args) {
232
233
  return null;
233
234
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
234
235
  }
235
- const TASK_MARKER_KEYS = [
236
- 'AGENT_INFRA_TASK_ID',
237
- 'AGENT_INFRA_CONTROL_TOKEN',
238
- 'AGENT_INFRA_CONTROL_GENERATION',
239
- 'AGENT_INFRA_CONTROL_DIR',
240
- 'AGENT_INFRA_CONTROL_STATUS_DIR',
241
- 'AGENT_INFRA_RUNTIME_DIR'
242
- ];
243
- const TASK_CONTROL_MARKER_KEYS = [
244
- 'AGENT_INFRA_CONTROL_TOKEN',
245
- 'AGENT_INFRA_CONTROL_GENERATION',
246
- 'AGENT_INFRA_CONTROL_DIR',
247
- 'AGENT_INFRA_CONTROL_STATUS_DIR'
248
- ];
249
- const TASK_CONTROL_CONFIG_KEYS = [
250
- 'AGENT_INFRA_CONTROL_TOKEN',
251
- 'AGENT_INFRA_CONTROL_GENERATION',
252
- 'AGENT_INFRA_CONTROL_ROOT_ID',
253
- 'AGENT_INFRA_CONTROL_DIR',
254
- 'AGENT_INFRA_CONTROL_STATUS_DIR'
255
- ];
256
- export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
257
- function nativeDirectoryProbe(candidate) {
258
- try {
259
- // The fixed mount is a trust anchor. Do not use a mutable node:fs export:
260
- // a preload must not turn a mounted sandbox into the direct-host path by
261
- // replacing the ordinary filesystem probe.
262
- const binding = process.binding('fs');
263
- if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
264
- return 'unknown';
265
- const result = binding.internalModuleStat(candidate);
266
- if (result === 1)
267
- return 'present';
268
- if (result === -2)
269
- return 'absent';
270
- return 'unknown';
271
- }
272
- catch {
273
- return 'unknown';
274
- }
275
- }
276
- function taskMarkerState(env) {
277
- const present = (key) => Boolean(env[key]);
278
- const taskId = present('AGENT_INFRA_TASK_ID');
279
- const controls = TASK_CONTROL_MARKER_KEYS.every(present);
280
- const runtime = present('AGENT_INFRA_RUNTIME_DIR');
281
- const any = TASK_MARKER_KEYS.some(present);
282
- if (!any)
283
- return 'none';
284
- if (taskId && controls && runtime)
285
- return 'task-bound';
286
- if (!taskId && controls && !runtime)
287
- return 'branch-only';
288
- return 'incomplete';
289
- }
290
- export function resolveSandboxControlTransport(env = process.env, options = {}) {
291
- const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
292
- || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
293
- || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
294
- const fixedStatusDir = options.statusMountPath
295
- ?? SANDBOX_CONTROL_STATUS_MOUNT;
296
- const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
297
- ? nativeDirectoryProbe(fixedStatusDir) : 'absent';
298
- if (fixedStatusProbe === 'unknown') {
299
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
300
- }
301
- const fixedStatusMounted = fixedStatusProbe === 'present';
302
- // A fixed status mount is installed only in a task-bound sandbox. Its
303
- // presence is not mutable by the sandbox process, so marker removal must
304
- // never make that process look like a direct host invocation.
305
- if (!hasAnyMarker) {
306
- return fixedStatusMounted
307
- ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
308
- : { kind: 'direct-host', reasonCode: null };
309
- }
310
- const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
311
- if (!hasCompleteConfig) {
312
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
313
- }
314
- const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
315
- const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
316
- ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
317
- if (configuredStatusProbe === 'unknown') {
318
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
319
- }
320
- const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
321
- ? configuredStatusProbe === 'present' ? configuredStatusDir : null
322
- : fixedStatusMounted ? fixedStatusDir : null;
323
- const statusMounted = statusDir !== null;
324
- if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
325
- return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
326
- }
327
- const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
328
- const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
329
- if (statusMounted) {
330
- let sentinel;
331
- try {
332
- sentinel = readSandboxControlIdentitySentinel(statusDir);
333
- }
334
- catch (error) {
335
- const message = error instanceof Error ? error.message : '';
336
- return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
337
- ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
338
- : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
339
- }
340
- if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
341
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
342
- }
343
- if (env.AGENT_INFRA_CONTROL_ROOT_ID && sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
344
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
345
- }
346
- if (sentinel.mode === 'task-bound'
347
- && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
348
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
349
- }
350
- if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
351
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
352
- }
353
- if (!hasCompleteConfig) {
354
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
355
- }
356
- return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
357
- }
358
- if (!hasAnyMarker)
359
- return { kind: 'direct-host', reasonCode: null };
360
- if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
361
- return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
362
- }
363
- return options.localWorkflow
364
- ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
365
- : { kind: 'broker-client', reasonCode: null };
366
- }
367
- export function hasTaskBoundMarker(env = process.env) {
368
- return taskMarkerState(env) === 'task-bound';
369
- }
370
236
  function explicitTaskRefForOperation(dispatcher, command, args, selected) {
371
237
  if (selected.taskRefSource === 'none' || selected.taskRefSource === 'environment')
372
238
  return null;
@@ -446,7 +312,7 @@ function isSandboxExecutorRoute(dispatcher, command, args, env) {
446
312
  && first(args) === 'execute'
447
313
  && Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST)
448
314
  && Boolean(env.AGENT_INFRA_RUNTIME_DIR)
449
- && !TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
315
+ && !hasSandboxControlMarker(env);
450
316
  }
451
317
  function viewFromEnvironment(env) {
452
318
  const statusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
@@ -492,7 +358,7 @@ export function guardTaskOperation(dispatcher, command, args = [], options = {})
492
358
  taskView: null
493
359
  };
494
360
  }
495
- const markerState = taskMarkerState(env);
361
+ const markerState = sandboxTaskMarkerState(env);
496
362
  if (markerState === 'incomplete' && !isSandboxExecutorRoute(dispatcher, command, args, env)) {
497
363
  throw new TaskViewOperationError('SANDBOX_TASK_VIEW_MARKER_INVALID', 'sandbox task markers are incomplete; refusing to infer host-direct authority');
498
364
  }
@@ -16,6 +16,7 @@ import { resourceIdentityEquals } from './resource-identity.js';
16
16
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
17
17
  import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
18
18
  import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
19
+ import { readManualValidationTransaction } from '../task/manual-validation-transaction.js';
19
20
  function summaryMarker(taskId) {
20
21
  return `<!-- sync-pr:${taskId}:summary -->`;
21
22
  }
@@ -119,6 +120,20 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
119
120
  return { ok: false, status: inspectionError(error), error };
120
121
  }
121
122
  const remote = inspected.value;
123
+ if (!remote.identity) {
124
+ return {
125
+ ok: false,
126
+ status: 'failed',
127
+ error: { code: 'PR_IDENTITY_MISSING', message: 'Provider pull-request inspection did not return a canonical identity', retryable: false }
128
+ };
129
+ }
130
+ if (!resourceIdentityEquals(remote.identity, prIdentity)) {
131
+ return {
132
+ ok: false,
133
+ status: 'failed',
134
+ error: { code: 'PR_IDENTITY_MISMATCH', message: 'Provider returned a different pull request identity than requested', retryable: false }
135
+ };
136
+ }
122
137
  const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
123
138
  const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
124
139
  const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
@@ -127,7 +142,7 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
127
142
  value: {
128
143
  repository: context.platform.repository,
129
144
  number,
130
- identity: remote.identity ?? prIdentity,
145
+ identity: remote.identity,
131
146
  nodeId: remote.id,
132
147
  url: remote.displayUrl || '',
133
148
  state: remote.state === 'closed' ? 'closed' : 'open',
@@ -261,14 +276,18 @@ async function summaryContext(taskRef, options = {}) {
261
276
  artifacts: []
262
277
  };
263
278
  const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
264
- const prNumber = resourceIdentityNumber(prIdentity);
279
+ let prNumber = resourceIdentityNumber(prIdentity);
265
280
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
266
281
  const context = loaded.ok ? loaded.value.context : loaded.context;
267
282
  let pullRequest = null;
268
283
  if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
269
284
  const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
270
- if (inspected.ok)
285
+ if (inspected.ok) {
271
286
  pullRequest = inspected.value;
287
+ if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
288
+ prNumber = inspected.value.number;
289
+ }
290
+ }
272
291
  }
273
292
  const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
274
293
  return {
@@ -493,7 +512,7 @@ async function syncPullRequestSummary(taskRef, options) {
493
512
  const prIdentity = boundFact.identity.resource;
494
513
  const boundPrNumber = resourceIdentityNumber(prIdentity);
495
514
  knownPrNumber = boundPrNumber;
496
- const prNumber = boundPrNumber;
515
+ let prNumber = boundPrNumber;
497
516
  const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
498
517
  if (!initial.ok)
499
518
  return fail(initial.status, context, initial.error);
@@ -505,9 +524,23 @@ async function syncPullRequestSummary(taskRef, options) {
505
524
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
506
525
  return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
507
526
  if (manual) {
508
- if (prNumber === null)
509
- return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
510
- if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
527
+ if (!Number.isSafeInteger(initial.value.number) || initial.value.number <= 0)
528
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot', retryable: false }, prNumber);
529
+ prNumber = initial.value.number;
530
+ knownPrNumber = prNumber;
531
+ if (!manual.transactionId)
532
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'manual-validation summary requires a transaction ID', retryable: false }, prNumber);
533
+ if (manual.phase === 'pending') {
534
+ const transaction = readManualValidationTransaction(resolved.taskDir, {
535
+ transactionId: manual.transactionId,
536
+ taskId: resolved.taskId,
537
+ prNumber,
538
+ prHeadSha: initial.value.head.sha
539
+ });
540
+ if (!transaction.ok)
541
+ return fail('failed', context, platformError(transaction.error), prNumber);
542
+ }
543
+ if (manual.phase === 'final' && (!manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
511
544
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
512
545
  if (manual.phase === 'final') {
513
546
  if (manual.authority !== 'coordinator')
@@ -688,7 +721,11 @@ async function syncPullRequestSummary(taskRef, options) {
688
721
  target: JSON.stringify(prIdentity),
689
722
  expectedDigest: createHash('sha256').update(desired).digest('hex'),
690
723
  dependency: 'deferred',
691
- pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile }
724
+ pullRequestSummary: {
725
+ body: options.body,
726
+ changeReportFile: options.changeReportFile,
727
+ ...(manual?.transactionId ? { manualValidation: { ...manual, transactionId: manual.transactionId } } : {})
728
+ }
692
729
  };
693
730
  if (options.skipQueue)
694
731
  return publish();
@@ -317,6 +317,21 @@ function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallba
317
317
  Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
318
318
  return snapshot;
319
319
  }
320
+ function inspectedPullRequestIdentityError(remote, requested) {
321
+ if (!remote.identity)
322
+ return {
323
+ code: 'PR_IDENTITY_MISSING',
324
+ message: 'Provider pull-request inspection did not return a canonical identity',
325
+ retryable: false
326
+ };
327
+ if (!resourceIdentityEquals(remote.identity, requested))
328
+ return {
329
+ code: 'PR_IDENTITY_MISMATCH',
330
+ message: 'Provider returned a different pull request identity than requested',
331
+ retryable: false
332
+ };
333
+ return null;
334
+ }
320
335
  function pullRequestIdentity(pullRequest) {
321
336
  return pullRequest.identity || { kind: 'number', value: pullRequest.number };
322
337
  }
@@ -339,6 +354,9 @@ async function inspectExternalPullRequest(base, prIdentity) {
339
354
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
340
355
  if (!inspected.ok)
341
356
  return providerPullRequestError(base, inspected.error, prNumber);
357
+ const identityError = inspectedPullRequestIdentityError(inspected.value, prIdentity);
358
+ if (identityError)
359
+ return providerPullRequestError(base, identityError, prNumber);
342
360
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
343
361
  return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
344
362
  platform: base.context.platform,
@@ -392,6 +410,12 @@ async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
392
410
  platform: context.platform, capabilities: context.capabilities,
393
411
  resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
394
412
  });
413
+ const identityError = inspectedPullRequestIdentityError(inspected.value, identity);
414
+ if (identityError)
415
+ return result('failed', null, null, number, {
416
+ platform: context.platform, capabilities: context.capabilities,
417
+ resource: { kind: 'pull-request', number, identity }, error: identityError
418
+ });
395
419
  return result('no-op', null, null, number, {
396
420
  platform: context.platform, capabilities: context.capabilities,
397
421
  resource: { kind: 'pull-request', number, identity },
@@ -448,27 +472,38 @@ function resourceIdentityLabel(identity) {
448
472
  const number = resourceIdentityNumber(identity);
449
473
  return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
450
474
  }
451
- function inLabelResource(kind, identity, before, expected, after, effect) {
452
- return { kind, number: resourceIdentityNumber(identity), identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
475
+ function inLabelResource(kind, identity, before, expected, after, effect, displayNumber = resourceIdentityNumber(identity)) {
476
+ return { kind, number: displayNumber, identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
453
477
  }
454
- async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
478
+ async function syncPlatformPullRequestInLabels(prToken, options = {}) {
455
479
  const cwd = path.resolve(options.cwd || process.cwd());
480
+ const token = String(prToken);
456
481
  const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
457
482
  const context = loaded.ok ? loaded.value.context : loaded.context;
458
483
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
459
484
  if (!loaded.ok || !usable)
460
- return result(context.status, null, null, prNumber, {
485
+ return result(context.status, null, null, null, {
461
486
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
462
487
  });
463
- if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
464
- return result('failed', null, null, prNumber, {
488
+ if (!token || token.trim() !== token)
489
+ return result('failed', null, null, null, {
465
490
  platform: context.platform, capabilities: context.capabilities,
466
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
491
+ error: { code: 'PR_IDENTITY_INVALID', message: 'PR identity token must be non-empty and trimmed', retryable: false }
467
492
  });
468
493
  const provider = loaded.value.provider;
469
494
  const operationContext = providerOperationContext(loaded.value);
470
495
  const repository = context.platform.repository;
471
- const pullRequestIdentity = providerResourceToken(provider, 'pull-request', String(prNumber));
496
+ let pullRequestIdentity;
497
+ try {
498
+ pullRequestIdentity = providerResourceToken(provider, 'pull-request', token);
499
+ }
500
+ catch (error) {
501
+ return result('failed', null, null, null, {
502
+ platform: context.platform, capabilities: context.capabilities,
503
+ error: { code: 'PR_IDENTITY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
504
+ });
505
+ }
506
+ let prNumber = resourceIdentityNumber(pullRequestIdentity);
472
507
  const providerFailure = (error) => result(error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error), null, null, prNumber, {
473
508
  platform: context.platform,
474
509
  capabilities: context.capabilities,
@@ -480,7 +515,13 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
480
515
  : unsupportedProviderOperation(provider, 'changeRequests.inspect');
481
516
  if (!inspected.ok)
482
517
  return providerFailure(inspected.error);
483
- const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber);
518
+ const identityError = inspectedPullRequestIdentityError(inspected.value, pullRequestIdentity);
519
+ if (identityError)
520
+ return providerFailure(identityError);
521
+ if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
522
+ prNumber = inspected.value.number;
523
+ }
524
+ const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber ?? 0);
484
525
  const files = provider.changeRequests?.listFiles
485
526
  ? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
486
527
  : unsupportedProviderOperation(provider, 'changeRequests.listFiles');
@@ -549,7 +590,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
549
590
  }))
550
591
  ];
551
592
  const willWrite = context.capabilities.triage && !options.dryRun;
552
- const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
593
+ const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op', item.number));
553
594
  if (options.dryRun || !context.capabilities.triage)
554
595
  return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
555
596
  platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
@@ -611,6 +652,16 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
611
652
  evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
612
653
  error: { ...providerError(reread.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
613
654
  });
655
+ if (item.kind === 'pull-request') {
656
+ const identityError = inspectedPullRequestIdentityError(reread.value, item.identity);
657
+ if (identityError)
658
+ return result('blocked', null, issueNumber, prNumber, {
659
+ changed: true, platform: context.platform, capabilities: context.capabilities,
660
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
661
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
662
+ error: { ...identityError, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${identityError.message}` }
663
+ });
664
+ }
614
665
  const after = reread.value.labels || [];
615
666
  const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
616
667
  state.after = after;
@@ -623,7 +674,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
623
674
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
624
675
  });
625
676
  if (item.kind === 'pull-request')
626
- currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber, currentPullRequest);
677
+ currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber ?? currentPullRequest.number, currentPullRequest);
627
678
  }
628
679
  return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
629
680
  changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
@@ -856,9 +907,13 @@ async function resolveExternalPullRequest(taskRef, options) {
856
907
  ? await base.provider.changeRequests.inspect({
857
908
  context: providerOperationContext(base.loadedContext),
858
909
  target: pullRequestIdentity(selected.selected)
859
- }).then((response) => response.ok
860
- ? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
861
- : response)
910
+ }).then((response) => {
911
+ if (!response.ok)
912
+ return response;
913
+ const identityError = inspectedPullRequestIdentityError(response.value, pullRequestIdentity(selected.selected));
914
+ return identityError ? { ok: false, error: identityError }
915
+ : { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) };
916
+ })
862
917
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
863
918
  if (!rechecked.ok || !rechecked.value)
864
919
  return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
@@ -1152,6 +1207,9 @@ async function createExternalPullRequest(taskRef, options, base) {
1152
1207
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1153
1208
  if (!inspected.ok)
1154
1209
  return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
1210
+ const identityError = inspectedPullRequestIdentityError(inspected.value, createdIdentity);
1211
+ if (identityError)
1212
+ return withCreation(providerPullRequestError(base, identityError, number), { kind: 'created', createdByCurrentOperation: true });
1155
1213
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
1156
1214
  if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
1157
1215
  return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -1354,6 +1412,13 @@ async function syncPlatformPullRequestImpl(taskRef, options) {
1354
1412
  code: 'IN_LABEL_SYNC_PARTIAL',
1355
1413
  message: `In-label synchronization is partial or unknown: ${reread.error.message}`
1356
1414
  }, base.prNumber));
1415
+ const identityError = inspectedPullRequestIdentityError(reread.value, base.prIdentity);
1416
+ if (identityError)
1417
+ return softenFailure(providerPullRequestError(base, {
1418
+ ...identityError,
1419
+ code: 'IN_LABEL_SYNC_PARTIAL',
1420
+ message: `Pull-request metadata synchronization is partial or unknown: ${identityError.message}`
1421
+ }, base.prNumber));
1357
1422
  finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
1358
1423
  const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
1359
1424
  .filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();