@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
@@ -274,7 +274,6 @@ export function createSandboxExecutorExecutionContext(params) {
274
274
  generation: params.generation,
275
275
  manifestPath: absolute('manifestPath', params.manifestPath),
276
276
  requestId: params.requestId,
277
- handoffDirectory: absolute('handoffDirectory', params.handoffDirectory ?? path.join(params.worktreeRoot, '.agent-infra-handoff')),
278
277
  ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
279
278
  ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
280
279
  lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
@@ -302,7 +301,6 @@ export function assertTaskControlExecutionContext(context) {
302
301
  absolute('worktreeRoot', context.worktreeRoot);
303
302
  absolute('runtimeDir', context.runtimeDir);
304
303
  absolute('manifestPath', context.manifestPath);
305
- absolute('handoffDirectory', context.handoffDirectory);
306
304
  if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
307
305
  invalidContext('runtimeDir is not bound to the manifest control root');
308
306
  }
@@ -113,7 +113,7 @@ function validateTaskEventRequest(request) {
113
113
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'initiator is invalid' };
114
114
  if (request.requestId !== undefined && (!request.requestId.trim() || /[\r\n]/.test(request.requestId)))
115
115
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'requestId must be a non-empty single line' };
116
- if (request.reasonCode && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'].includes(request.reasonCode))
116
+ if (request.reasonCode && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'].includes(request.reasonCode))
117
117
  return { code: 'EVENT_PAYLOAD_INVALID', message: 'reasonCode is invalid' };
118
118
  for (const [name, value] of [['sourceFinding', request.sourceFinding], ['sourceArtifact', request.sourceArtifact], ['sourceSha256', request.sourceSha256]]) {
119
119
  if (value !== undefined && (!value.trim() || /[\r\n]/.test(value)))
@@ -10,22 +10,45 @@ import { resourceIdentityEquals } from '../platform/resource-identity.js';
10
10
  import { parseTaskFrontmatter } from './frontmatter.js';
11
11
  import { recoverPlatformOperations } from './platform-operation-recovery.js';
12
12
  import { applyTaskLifecycle, inspectTaskLifecycleProgress, previewTaskCompletion } from './lifecycle.js';
13
- import { resolveTaskRef } from './resolve-ref.js';
13
+ import { enumerateAllTaskDirs, resolveTaskRef } from './resolve-ref.js';
14
14
  import { inspectShortIdRegistry } from './short-id.js';
15
15
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
16
16
  import { verifyTaskEvent } from './verification.js';
17
17
  import { projectFinalizationWarning } from './workflow-warning-intents.js';
18
18
  import { mergeOperationWarnings } from './operation-outcome.js';
19
- import { readTaskFinalizationHandoff } from './finalization-handoff.js';
20
19
  import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
21
20
  import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
22
21
  const RECEIPT_VERSION = 4;
23
22
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
24
- function finalizationRoot(repoRoot) {
25
- return path.join(repoRoot, '.agents', 'workspace', '.task-finalization');
23
+ export function resolveTaskFinalizationDirectory(repoRoot, taskId) {
24
+ const tasks = enumerateAllTaskDirs(repoRoot).filter((task) => task.taskId === taskId);
25
+ if (tasks.length === 0)
26
+ return null;
27
+ if (tasks.length !== 1)
28
+ throw new Error(`FINALIZATION_TASK_IDENTITY_AMBIGUOUS: ${taskId}`);
29
+ const task = tasks[0];
30
+ let canonicalDir;
31
+ try {
32
+ canonicalDir = fs.realpathSync.native(task.taskDir);
33
+ }
34
+ catch (error) {
35
+ throw new Error(`FINALIZATION_TASK_IDENTITY_UNAVAILABLE: ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ let frontmatter;
38
+ try {
39
+ frontmatter = parseTaskFrontmatter(fs.readFileSync(path.join(canonicalDir, 'task.md'), 'utf8'));
40
+ }
41
+ catch (error) {
42
+ throw new Error(`FINALIZATION_TASK_IDENTITY_INVALID: ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
43
+ }
44
+ if (task.taskId !== taskId || frontmatter.id !== taskId) {
45
+ throw new Error(`FINALIZATION_TASK_IDENTITY_INVALID: task directory and task.md must identify ${taskId}`);
46
+ }
47
+ return canonicalDir;
26
48
  }
27
49
  function receiptPath(repoRoot, taskId) {
28
- return path.join(finalizationRoot(repoRoot), `${taskId}.json`);
50
+ const taskDir = resolveTaskFinalizationDirectory(repoRoot, taskId);
51
+ return taskDir ? path.join(taskDir, '.task-finalization.json') : null;
29
52
  }
30
53
  function now() {
31
54
  return new Date().toISOString();
@@ -89,7 +112,7 @@ function validWarning(value) {
89
112
  && (warning.status === 'open' || warning.status === 'resolved')
90
113
  && (warning.resolvedAt === null || typeof warning.resolvedAt === 'string');
91
114
  }
92
- function validateReceipt(value, taskId) {
115
+ export function validateTaskFinalizationReceipt(value, taskId) {
93
116
  if (!value || typeof value !== 'object' || Array.isArray(value))
94
117
  throw new Error('receipt must be an object');
95
118
  const receipt = value;
@@ -128,18 +151,18 @@ function validateReceipt(value, taskId) {
128
151
  }
129
152
  function readReceipt(repoRoot, taskId) {
130
153
  const file = receiptPath(repoRoot, taskId);
131
- if (!fs.existsSync(file))
154
+ if (!file || !fs.existsSync(file))
132
155
  return null;
133
156
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
134
- return validateReceipt(value, taskId);
157
+ return validateTaskFinalizationReceipt(value, taskId);
135
158
  }
136
159
  function readTaskFinalizationReceipt(repoRoot, taskId) {
137
160
  return readReceipt(path.resolve(repoRoot), taskId);
138
161
  }
139
162
  function writeReceipt(repoRoot, receipt) {
140
- const directory = finalizationRoot(repoRoot);
141
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
142
163
  const target = receiptPath(repoRoot, receipt.taskId);
164
+ if (!target)
165
+ throw new Error(`TASK_FINALIZATION_TASK_NOT_FOUND: ${receipt.taskId}`);
143
166
  const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
144
167
  try {
145
168
  fs.writeFileSync(temporary, `${JSON.stringify(receipt)}\n`, { mode: 0o600, flag: 'wx' });
@@ -670,7 +693,7 @@ async function prepareUnderLock(request, taskId, options) {
670
693
  let receipt;
671
694
  try {
672
695
  const file = receiptPath(repoRoot, taskId);
673
- const existed = fs.existsSync(file);
696
+ const existed = file !== null && fs.existsSync(file);
674
697
  receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId, options.controlBinding);
675
698
  if (!existed)
676
699
  writeReceipt(repoRoot, receipt);
@@ -969,34 +992,13 @@ async function commitPreparedTaskFinalization(request, options) {
969
992
  }
970
993
  }
971
994
  let receipt = readReceipt(repoRoot, resolved.taskId);
972
- let importedHandoff = false;
973
- if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
974
- try {
975
- const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
976
- receipt = validateReceipt(handoff.receipt, resolved.taskId);
977
- if (receipt.lifecycle !== 'pending')
978
- throw new Error('handoff lifecycle must be pending');
979
- if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
980
- throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
981
- }
982
- writeReceipt(repoRoot, receipt);
983
- importedHandoff = true;
984
- try {
985
- handoff.cleanup();
986
- }
987
- catch { /* keep the persisted canonical receipt usable */ }
988
- }
989
- catch (error) {
990
- return failed(resolved.taskId, {
991
- code: 'TASK_FINALIZATION_HANDOFF_INVALID',
992
- message: error instanceof Error ? error.message : String(error), retryable: false
993
- });
994
- }
995
- }
996
995
  if (!receipt)
997
996
  return failed(resolved.taskId, {
998
997
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
999
998
  });
999
+ if (options.controlBinding && !receipt.controlBinding) {
1000
+ receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
1001
+ }
1000
1002
  if (receipt.lifecycle === 'done' && receipt.verification !== 'pending' && resolved.state === 'completed') {
1001
1003
  return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
1002
1004
  }
@@ -1008,25 +1010,6 @@ async function commitPreparedTaskFinalization(request, options) {
1008
1010
  message: 'task lifecycle progress cannot be verified', retryable: false
1009
1011
  });
1010
1012
  }
1011
- if (!importedHandoff && options.handoffDirectory && request.handoffSha256) {
1012
- try {
1013
- const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
1014
- const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
1015
- if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
1016
- || nextReceipt.lifecycle !== receipt.lifecycle)
1017
- throw new Error('handoff is not continuous with canonical receipt');
1018
- try {
1019
- handoff.cleanup();
1020
- }
1021
- catch { /* keep the persisted canonical receipt usable */ }
1022
- }
1023
- catch (error) {
1024
- return failed(resolved.taskId, {
1025
- code: 'TASK_FINALIZATION_HANDOFF_INVALID',
1026
- message: error instanceof Error ? error.message : String(error), retryable: false
1027
- });
1028
- }
1029
- }
1030
1013
  }
1031
1014
  if (receipt.taskComment === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1032
1015
  return failed(resolved.taskId, {
@@ -53,6 +53,29 @@ function serializeScalar(value) {
53
53
  }
54
54
  return serialized;
55
55
  }
56
+ function stripInlineComment(value) {
57
+ const first = value.search(/\S/u);
58
+ let activeQuote = first >= 0 && (value[first] === '"' || value[first] === "'") ? value[first] : null;
59
+ for (let index = activeQuote ? first + 1 : Math.max(first, 0); index < value.length; index += 1) {
60
+ const character = value[index];
61
+ if (activeQuote === '"' && character === '\\') {
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (activeQuote && character === activeQuote) {
66
+ if (activeQuote === "'" && value[index + 1] === "'") {
67
+ index += 1;
68
+ continue;
69
+ }
70
+ activeQuote = null;
71
+ continue;
72
+ }
73
+ if (!activeQuote && character === '#' && (index === 0 || /\s/u.test(value[index - 1]))) {
74
+ return value.slice(0, index).trimEnd();
75
+ }
76
+ }
77
+ return value;
78
+ }
56
79
  function parseTaskFrontmatter(content) {
57
80
  const result = {};
58
81
  if (!content.startsWith('---'))
@@ -69,7 +92,7 @@ function parseTaskFrontmatter(content) {
69
92
  if (colon === -1)
70
93
  continue;
71
94
  const key = line.slice(0, colon).trim();
72
- const value = line.slice(colon + 1).trim();
95
+ const value = stripInlineComment(line.slice(colon + 1).trim());
73
96
  if (key)
74
97
  result[key] = value;
75
98
  }
@@ -336,7 +336,8 @@ function applyLedgerIntent(intent, options = {}) {
336
336
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
337
337
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
338
338
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
339
- if (!sameRoundMinorClose && !allowed.has(intent.status)) {
339
+ const refreshOpenEvidence = before.status === 'open' && intent.status === 'open';
340
+ if (!sameRoundMinorClose && !refreshOpenEvidence && !allowed.has(intent.status)) {
340
341
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
341
342
  }
342
343
  after = { ...before, status: intent.status, evidence };
@@ -69,6 +69,18 @@ function parseJournal(file, taskId) {
69
69
  ? !item.pullRequestSummary || typeof item.pullRequestSummary.body !== 'string'
70
70
  || !item.pullRequestSummary.body.trim() || typeof item.pullRequestSummary.changeReportFile !== 'string'
71
71
  || !item.pullRequestSummary.changeReportFile.trim()
72
+ || (item.pullRequestSummary.manualValidation !== undefined && (!item.pullRequestSummary.manualValidation
73
+ || typeof item.pullRequestSummary.manualValidation !== 'object'
74
+ || Array.isArray(item.pullRequestSummary.manualValidation)
75
+ || !['pending', 'final'].includes(item.pullRequestSummary.manualValidation.phase)
76
+ || typeof item.pullRequestSummary.manualValidation.transactionId !== 'string'
77
+ || !item.pullRequestSummary.manualValidation.transactionId.trim()
78
+ || (item.pullRequestSummary.manualValidation.receiptDigest !== undefined && !/^[a-f0-9]{64}$/u.test(item.pullRequestSummary.manualValidation.receiptDigest))
79
+ || (item.pullRequestSummary.manualValidation.prHeadSha !== undefined && !/^[a-f0-9]{40}$/u.test(item.pullRequestSummary.manualValidation.prHeadSha))
80
+ || (item.pullRequestSummary.manualValidation.authority !== undefined && item.pullRequestSummary.manualValidation.authority !== 'coordinator')
81
+ || (item.pullRequestSummary.manualValidation.phase === 'final' && (!/^[a-f0-9]{64}$/u.test(item.pullRequestSummary.manualValidation.receiptDigest ?? '')
82
+ || !/^[a-f0-9]{40}$/u.test(item.pullRequestSummary.manualValidation.prHeadSha ?? '')
83
+ || item.pullRequestSummary.manualValidation.authority !== 'coordinator'))))
72
84
  : item.pullRequestSummary !== undefined)
73
85
  || (item.kind === 'pull-request-review'
74
86
  ? !item.pullRequestReview || typeof item.pullRequestReview.prNumber !== 'string'
@@ -193,6 +193,16 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
193
193
  catch {
194
194
  return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
195
195
  }
196
+ const summary = operation.pullRequestSummary;
197
+ if (!summary)
198
+ return platformResult('blocked', { error: {
199
+ code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
200
+ } });
201
+ const manualValidationHeading = /^###\s+(?:⏳\s+(?:Manual Validation Pending|人工验证待收尾)|✅\s+(?:Manual Validation Passed|人工验证已通过))\s*$/mu.test(summary.body);
202
+ if (manualValidationHeading && !summary.manualValidation)
203
+ return platformResult('blocked', { error: {
204
+ code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'Queued manual-validation summary has no persisted transaction context', retryable: false
205
+ } });
196
206
  const resolved = await resolvePlatformProviderContext({ cwd, client });
197
207
  if (!resolved.ok)
198
208
  return resolved.context;
@@ -207,13 +217,9 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
207
217
  return platformResult('blocked', { error: {
208
218
  code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple pull-request comments contain the queued summary marker', retryable: false
209
219
  } });
210
- if (summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
220
+ if (!summary.manualValidation && summaries.length === 1 && createHash('sha256').update(normalizeCommentContent(summaries[0].body)).digest('hex') === operation.expectedDigest) {
211
221
  return platformResult('no-op');
212
222
  }
213
- if (!operation.pullRequestSummary)
214
- return platformResult('blocked', { error: {
215
- code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Pull-request summary recovery content is missing', retryable: false
216
- } });
217
223
  const bound = await inspectPlatformPullRequest(taskId, { cwd, client });
218
224
  if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, identity))
219
225
  return platformResult('blocked', { error: bound.error ?? {
@@ -223,8 +229,9 @@ async function replayPullRequestSummary(taskId, operation, agent, cwd, client) {
223
229
  agent,
224
230
  cwd,
225
231
  client,
226
- body: operation.pullRequestSummary.body,
227
- changeReportFile: operation.pullRequestSummary.changeReportFile,
232
+ body: summary.body,
233
+ changeReportFile: summary.changeReportFile,
234
+ ...(summary.manualValidation ? { manualValidation: summary.manualValidation } : {}),
228
235
  primaryResult: 'no_op',
229
236
  strict: true,
230
237
  skipQueue: true,
@@ -98,16 +98,21 @@ function enumerateTaskDirs(repoRoot) {
98
98
  */
99
99
  function enumerateAllTaskDirs(repoRoot) {
100
100
  const out = [];
101
- const seen = new Set();
101
+ const seen = new Map();
102
102
  const add = (taskId, taskDir, state) => {
103
103
  if (!TASK_ID_RE.test(taskId))
104
104
  return;
105
105
  const stat = fs.lstatSync(taskDir);
106
106
  if (!stat.isDirectory() || stat.isSymbolicLink() || !fs.existsSync(path.join(taskDir, 'task.md')))
107
107
  return;
108
- if (seen.has(taskId))
108
+ const canonicalDir = fs.realpathSync.native(taskDir);
109
+ const previousDir = seen.get(taskId);
110
+ if (previousDir !== undefined) {
111
+ if (previousDir === canonicalDir)
112
+ return;
109
113
  throw new Error(`duplicate task identity: ${taskId}`);
110
- seen.add(taskId);
114
+ }
115
+ seen.set(taskId, canonicalDir);
111
116
  out.push({ taskId, taskDir, state });
112
117
  };
113
118
  for (const state of FLAT_WORKSPACE_DIRS) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocolVersion": 3,
3
- "packageVersion": "0.11.10",
4
- "sourceInputHash": "3500e8eb7fac8bbad3d95124002f8042af3d160185a5b635eb9b4d8f88a991cb",
5
- "internalExecutableBuildHash": "51a4c472b6a54b3ca3c28ab049683175d3ea4730fe560868f853cd603b5a98f1"
3
+ "packageVersion": "0.11.11",
4
+ "sourceInputHash": "576098e3f88ceca11a0a0f94573a3ceca2b14c97b29a6b934ae07849e214c889",
5
+ "internalExecutableBuildHash": "065375c31d3564a65236fc43bad9947395ea0873ae2531d9b05f42956ef30f1e"
6
6
  }
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import {
2
3
  AGENT_CLIENT_CAPABILITY_IDS,
3
4
  AGENT_CLIENT_SUPPORT_LEVELS,
@@ -216,6 +217,26 @@ function freezeTool(tool: SandboxTool): SandboxTool {
216
217
  if (!tool.containerMount.startsWith('/')) {
217
218
  throw new Error(`Sandbox tool '${tool.id}' containerMount must be absolute`);
218
219
  }
220
+ if (tool.hostStateMounts !== undefined && !Array.isArray(tool.hostStateMounts)) {
221
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must be an array`);
222
+ }
223
+ const hostStateMounts = tool.hostStateMounts?.map((entry) => {
224
+ if (!entry || typeof entry.hostSubdir !== 'string' || typeof entry.containerSubpath !== 'string') {
225
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must contain relative path pairs`);
226
+ }
227
+ for (const value of [entry.hostSubdir, entry.containerSubpath]) {
228
+ const normalized = path.posix.normalize(value);
229
+ if (
230
+ value === ''
231
+ || path.posix.isAbsolute(value)
232
+ || normalized === '..'
233
+ || normalized.startsWith('../')
234
+ ) {
235
+ throw new Error(`Agent Client '${tool.id}' hostStateMounts must stay within the tool directories`);
236
+ }
237
+ }
238
+ return Object.freeze({ ...entry });
239
+ });
219
240
  if (!tool.versionCmd) {
220
241
  throw new Error(`Sandbox tool '${tool.id}' requires versionCmd`);
221
242
  }
@@ -233,6 +254,7 @@ function freezeTool(tool: SandboxTool): SandboxTool {
233
254
  ...(tool.hostLiveMounts ? {
234
255
  hostLiveMounts: Object.freeze(tool.hostLiveMounts.map((entry) => Object.freeze({ ...entry })))
235
256
  } : {}),
257
+ ...(hostStateMounts ? { hostStateMounts: Object.freeze(hostStateMounts) } : {}),
236
258
  ...(tool.postSetupCmds ? { postSetupCmds: Object.freeze([...tool.postSetupCmds]) } : {}),
237
259
  ...(tool.tmpfs ? {
238
260
  tmpfs: Object.freeze({
@@ -285,6 +285,19 @@ const codexRecoveryChecks: readonly AgentClientSandboxRecoveryCheck[] = [
285
285
  path: '/home/devuser/.codex'
286
286
  }
287
287
  },
288
+ {
289
+ id: 'app-server-package-writable',
290
+ probe: {
291
+ script: 'probe="$1/.agent-infra-app-server-package-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"',
292
+ args: ['/home/devuser/.codex/packages/app-server-daemon'],
293
+ user: 'devuser'
294
+ },
295
+ finding: {
296
+ repairKind: 'hard-failure',
297
+ message: 'Codex app-server package directory is not writable by devuser.',
298
+ path: '/home/devuser/.codex/packages/app-server-daemon'
299
+ }
300
+ },
288
301
  {
289
302
  id: 'prompts-link',
290
303
  when: {
@@ -55,6 +55,10 @@ const codexAdapter = defineAgentClientAdapter({
55
55
  versionCmd: 'codex --version',
56
56
  setupHint: 'Run codex once inside the container and choose Device Code login if needed.',
57
57
  tmpfs: { size: '512m', seed: ['config.toml', 'model-catalogs'], exec: true },
58
+ hostStateMounts: [{
59
+ hostSubdir: 'packages/app-server-daemon',
60
+ containerSubpath: 'packages/app-server-daemon'
61
+ }],
58
62
  hostLiveMounts: [
59
63
  {
60
64
  hostPath: hostJoin(home, '.codex', 'auth.json'),
@@ -206,7 +206,7 @@ async function executeManualValidationTransactionLocked(
206
206
  if (transaction.phase === 'prepared') {
207
207
  const staged = await syncPullRequestSummary(taskRef, {
208
208
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile!), primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', strict: true,
209
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
209
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
210
210
  });
211
211
  if (!['applied', 'no-op'].includes(staged.status)) transactionFailure(staged.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_FAILED', message: 'pending summary staging failed' });
212
212
  transitionAndPersist('summary-staged');
@@ -264,7 +264,7 @@ async function executeManualValidationTransactionLocked(
264
264
  try {
265
265
  await syncPullRequestSummary(taskRef, {
266
266
  cwd, client: options.client, agent, body: pendingBody, changeReportFile: path.resolve(cwd, values.changeReportFile!), primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', strict: true,
267
- manualValidation: { phase: 'pending' }, lockAlreadyHeld: true
267
+ manualValidation: { phase: 'pending', transactionId: transaction.transactionId }, lockAlreadyHeld: true
268
268
  });
269
269
  } catch {
270
270
  // Preserve recovery-required when pending compensation cannot be confirmed.
@@ -22,7 +22,7 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
22
22
  agent-infra-internal platform-pr bind <task-ref> --pr <token> --agent <agent> [--dry-run] [--cwd <path>]
23
23
  agent-infra-internal platform-pr skip <task-ref> --agent <agent> [--dry-run] [--cwd <path>]
24
24
  agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
25
- agent-infra-internal platform-pr sync-in-labels --pr <N> [--dry-run] [--cwd <path>]
25
+ agent-infra-internal platform-pr sync-in-labels --pr <token> [--dry-run] [--cwd <path>]
26
26
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
27
27
  agent-infra-internal platform-pr change-report <task-ref> --agent <agent> --mechanical-file <path> --precheck-file <path> [--dry-run] [--cwd <path>]
28
28
  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>]
@@ -89,10 +89,9 @@ async function platformPr(args: string[] = []): Promise<void> {
89
89
  const values = parsed.values;
90
90
  const unexpected = Object.keys(values).find((name) => !['cwd', 'pr', 'dryRun'].includes(name));
91
91
  if (unexpected) { fail(`sync-in-labels does not accept --${unexpected}`); return; }
92
- const pr = Number(values.pr);
93
- if (!Number.isInteger(pr) || pr <= 0) { fail('sync-in-labels requires a positive --pr'); return; }
92
+ if (typeof values.pr !== 'string' || !values.pr || values.pr.trim() !== values.pr) { fail('sync-in-labels requires a non-empty --pr token'); return; }
94
93
  const cwd = path.resolve(typeof values.cwd === 'string' ? values.cwd : process.cwd());
95
- finish(await syncPlatformPullRequestInLabels(pr, { cwd, dryRun: values.dryRun === true }));
94
+ finish(await syncPlatformPullRequestInLabels(values.pr, { cwd, dryRun: values.dryRun === true }));
96
95
  return;
97
96
  }
98
97
  const taskRef = args[1];
@@ -1,7 +1,7 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.ts';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
3
3
 
4
- 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`;
4
+ 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`;
5
5
 
6
6
 
7
7
  function taskArtifact(args: string[] = []): void {
@@ -7,7 +7,7 @@ import {
7
7
  SandboxControlClientError
8
8
  } from '../sandbox/control/client.ts';
9
9
  import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.ts';
10
- import { validateTaskCreateCandidate } from '../task/create.ts';
10
+ import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../task/create.ts';
11
11
  import {
12
12
  createTask,
13
13
  parseTaskCreateResult,
@@ -82,7 +82,20 @@ async function taskCreate(args: string[]): Promise<void> {
82
82
  const stat = fs.lstatSync(inputPath);
83
83
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
84
84
  if (stat.size > SANDBOX_CONTROL_MAX_BYTES) throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
85
- const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
85
+ const inputContent = fs.readFileSync(inputPath, 'utf8');
86
+ let candidate: TaskCreateCandidateV1;
87
+ try {
88
+ candidate = validateTaskCreateCandidate(JSON.parse(inputContent));
89
+ } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
92
+ output(failed(code, message, false, {
93
+ requestId: null,
94
+ accepted: false,
95
+ recovery: 'new-request-id'
96
+ }));
97
+ return;
98
+ }
86
99
  const environment = resolveSandboxControlTransport();
87
100
  if (environment.kind === 'fail-closed') {
88
101
  output(failed(environment.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID', 'sandbox client control configuration is invalid'));
@@ -80,9 +80,7 @@ async function taskFinalization(args: string[] = []): Promise<void> {
80
80
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
81
81
  if (transport.kind === 'sandbox-local') {
82
82
  try {
83
- const response = requestSandboxTaskFinalization({
84
- agent: boundOperation.request.agent
85
- });
83
+ const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
86
84
  process.stdout.write(response.stdout);
87
85
  process.stderr.write(response.stderr);
88
86
  process.exitCode = response.exitCode ?? 1;