@fitlab-ai/agent-infra 0.11.4 → 0.11.5

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 (111) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/platform/pr-summary.js +4 -23
  15. package/dist/lib/platform/verification-sync.js +12 -49
  16. package/dist/lib/sandbox/control/client.js +4 -1
  17. package/dist/lib/sandbox/control/executor.js +12 -4
  18. package/dist/lib/sandbox/control/protocol.js +2 -1
  19. package/dist/lib/sandbox/removal.js +15 -8
  20. package/dist/lib/task/activity-intent.js +1 -2
  21. package/dist/lib/task/artifact-operations.js +36 -2
  22. package/dist/lib/task/artifact-schema.js +7 -7
  23. package/dist/lib/task/control-authority.js +9 -166
  24. package/dist/lib/task/decision-intents.js +2 -4
  25. package/dist/lib/task/events.js +7 -28
  26. package/dist/lib/task/finalization-handoff.js +92 -0
  27. package/dist/lib/task/finalization.js +123 -12
  28. package/dist/lib/task/intermediate-cleanup.js +9 -148
  29. package/dist/lib/task/ledger-intents.js +3 -6
  30. package/dist/lib/task/lifecycle.js +50 -11
  31. package/dist/lib/task/local-artifact-finalization.js +13 -28
  32. package/dist/lib/task/review-command.js +1 -2
  33. package/dist/lib/task/review-finalization.js +1 -2
  34. package/dist/lib/task/snapshot.js +6 -6
  35. package/dist/lib/task/verification-engine.js +16 -11
  36. package/dist/lib/task/verification.js +1 -13
  37. package/dist/lib/task/workflow-warning-intents.js +2 -4
  38. package/dist/lib/task/write.js +1 -2
  39. package/dist/lifecycle-build-manifest.json +3 -3
  40. package/lib/decide.ts +5 -35
  41. package/lib/git/branch-worktree.ts +41 -0
  42. package/lib/internal/cli-route-inventory.ts +0 -2
  43. package/lib/internal/task-activity.ts +6 -34
  44. package/lib/internal/task-event.ts +4 -37
  45. package/lib/internal/task-finalization.ts +9 -2
  46. package/lib/internal/task-ledger.ts +5 -38
  47. package/lib/internal/task-lifecycle.ts +2 -2
  48. package/lib/internal/task-operation-registry.ts +1 -4
  49. package/lib/internal/task-review.ts +5 -31
  50. package/lib/internal/task-verify.ts +3 -39
  51. package/lib/internal/task-warning.ts +3 -32
  52. package/lib/platform/pr-summary.ts +4 -25
  53. package/lib/platform/verification-sync.ts +13 -53
  54. package/lib/sandbox/control/client.ts +5 -1
  55. package/lib/sandbox/control/executor.ts +13 -4
  56. package/lib/sandbox/control/protocol.ts +3 -2
  57. package/lib/sandbox/removal.ts +23 -7
  58. package/lib/task/activity-intent.ts +1 -2
  59. package/lib/task/artifact-operations.ts +58 -2
  60. package/lib/task/artifact-schema.ts +9 -7
  61. package/lib/task/control-authority.ts +14 -188
  62. package/lib/task/decision-intents.ts +2 -4
  63. package/lib/task/events.ts +8 -27
  64. package/lib/task/finalization-handoff.ts +104 -0
  65. package/lib/task/finalization.ts +120 -12
  66. package/lib/task/intermediate-cleanup.ts +9 -125
  67. package/lib/task/ledger-intents.ts +3 -6
  68. package/lib/task/lifecycle.ts +45 -13
  69. package/lib/task/local-artifact-finalization.ts +16 -35
  70. package/lib/task/review-command.ts +2 -5
  71. package/lib/task/review-finalization.ts +1 -4
  72. package/lib/task/snapshot.ts +6 -6
  73. package/lib/task/verification-engine.ts +20 -10
  74. package/lib/task/verification.ts +1 -15
  75. package/lib/task/workflow-warning-intents.ts +2 -4
  76. package/lib/task/write.ts +1 -4
  77. package/package.json +1 -1
  78. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  79. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  82. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  83. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  84. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  85. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  86. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  87. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  88. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  89. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  90. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  91. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  92. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  93. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  94. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  95. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  96. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  97. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  99. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  100. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  101. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  102. package/dist/lib/internal/task-override.js +0 -138
  103. package/dist/lib/task/artifact-recovery.js +0 -476
  104. package/dist/lib/task/artifact-repair-intent.js +0 -181
  105. package/dist/lib/task/guard-override.js +0 -31
  106. package/dist/lib/task/human-override.js +0 -1259
  107. package/lib/internal/task-override.ts +0 -144
  108. package/lib/task/artifact-recovery.ts +0 -572
  109. package/lib/task/artifact-repair-intent.ts +0 -227
  110. package/lib/task/guard-override.ts +0 -50
  111. package/lib/task/human-override.ts +0 -1522
@@ -1,11 +1,10 @@
1
1
  import { parseReviewCommand } from '../task/review-command.ts';
2
2
  import { finalizeReviewSummary, preflightReviewSummary } from '../task/review-finalization.ts';
3
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
4
3
  import { resolveTaskRef } from '../task/resolve-ref.ts';
5
4
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
6
5
  import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
7
6
 
8
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
7
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run]\n';
9
8
 
10
9
  function failUsage(message: string): void {
11
10
  process.stdout.write(`${JSON.stringify({
@@ -27,37 +26,16 @@ async function taskReview(args: string[] = []): Promise<void> {
27
26
  let request;
28
27
  try { request = parseReviewCommand(args); }
29
28
  catch (error) { failUsage(error instanceof Error ? error.message : String(error)); return; }
30
- const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
29
+ const { stage, artifact, dryRun, orchestrated } = request;
31
30
  const operation = args[1]!;
32
- const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
33
- if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
34
31
  const resolved = resolveTaskRef(args[0]!);
35
32
  if (!resolved.ok) { failUsage(resolved.message); return; }
36
33
  let result;
37
- let humanOverride: unknown = null;
38
34
  try {
39
35
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
40
- let current = operation === 'preflight'
36
+ return operation === 'preflight'
41
37
  ? preflightReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
42
38
  : finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
43
- if (operation === 'preflight') return current;
44
- if (current.status !== 'failed' || !overrideTicket) return current;
45
- if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
46
- const consumed = await consumeHumanOverride({
47
- taskRef: args[0]!, ticketId: overrideTicket,
48
- failureId: failureId('review-finalization', current.error?.code ?? 'TASK_STATE_MISMATCH'),
49
- target: overrideTarget, scope: overrideScope
50
- }, {
51
- effectExecutor: (capability) => {
52
- const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
53
- current = retried;
54
- return retried.status === 'failed' || retried.status === 'planned'
55
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
56
- : null;
57
- }
58
- });
59
- humanOverride = consumed;
60
- return current;
61
39
  });
62
40
  } catch (error) {
63
41
  if (!(error instanceof TaskExecutionLockError)) throw error;
@@ -65,12 +43,8 @@ async function taskReview(args: string[] = []): Promise<void> {
65
43
  process.exitCode = 1;
66
44
  return;
67
45
  }
68
- process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
69
- const overrideFailed = Boolean(
70
- humanOverride && typeof humanOverride === 'object' &&
71
- (humanOverride as { status?: unknown }).status === 'failed'
72
- );
73
- if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
46
+ process.stdout.write(`${JSON.stringify(result)}\n`);
47
+ if (result.status === 'failed') process.exitCode = 1;
74
48
  }
75
49
 
76
50
  export { taskReview };
@@ -1,10 +1,9 @@
1
1
  import { renderTaskVerification, verifyTaskEvent } from '../task/verification.ts';
2
- import { consumeHumanOverride, failureId } from '../task/human-override.ts';
3
2
  import { resolveTaskRef } from '../task/resolve-ref.ts';
4
3
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
5
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
6
5
 
7
- const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
6
+ const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text]\n';
8
7
 
9
8
  function fail(message: string): void {
10
9
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'VERIFY_PAYLOAD_INVALID', message } })}\n`);
@@ -21,63 +20,28 @@ async function taskVerify(args: string[] = []): Promise<void> {
21
20
  const event = args[1];
22
21
  let artifact: string | undefined;
23
22
  let format: 'json' | 'text' = 'json';
24
- let overrideTicket: string | undefined;
25
- let overrideTarget: string | undefined;
26
- let overrideScope: string | undefined;
27
23
  const seen = new Set<string>();
28
24
  for (let index = 2; index < args.length; index += 1) {
29
25
  const flag = args[index]!;
30
- if (!['--artifact', '--format', '--override-ticket', '--override-target', '--override-scope'].includes(flag)) { fail(`unknown option '${flag}'`); return; }
26
+ if (!['--artifact', '--format'].includes(flag)) { fail(`unknown option '${flag}'`); return; }
31
27
  if (seen.has(flag)) { fail(`duplicate option '${flag}'`); return; }
32
28
  const value = args[++index];
33
29
  if (!value || value.startsWith('--')) { fail(`option '${flag}' requires a value`); return; }
34
30
  seen.add(flag);
35
31
  if (flag === '--artifact') artifact = value;
36
- else if (flag === '--override-ticket') overrideTicket = value;
37
- else if (flag === '--override-target') overrideTarget = value;
38
- else if (flag === '--override-scope') overrideScope = value;
39
32
  else if (value === 'json' || value === 'text') format = value;
40
33
  else { fail("option '--format' requires 'json' or 'text'"); return; }
41
34
  }
42
35
  const resolved = resolveTaskRef(taskRef);
43
36
  if (!resolved.ok) { fail(resolved.message); return; }
44
37
  let result;
45
- let humanOverride: unknown = null;
46
38
  try {
47
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => {
48
- let current = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) });
49
- if (current.status === 'pass' || !overrideTicket) return current;
50
- if (!overrideTarget || !overrideScope) { fail('override ticket requires target and scope'); return current; }
51
- const failureProducer = current.status === 'blocked' || current.status === 'fail'
52
- ? 'verification-engine'
53
- : 'task-verify';
54
- const failureCode = current.status === 'blocked'
55
- ? 'CHECK_BLOCKED'
56
- : current.status === 'fail'
57
- ? 'CHECK_FAILED'
58
- : current.error?.code ?? 'VERIFY_TASK_STATE_MISMATCH';
59
- const consumed = await consumeHumanOverride({
60
- taskRef, ticketId: overrideTicket,
61
- failureId: failureId(failureProducer, failureCode),
62
- target: overrideTarget, scope: overrideScope
63
- }, {
64
- effectExecutor: async (capability) => {
65
- const retried = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }, { manualOverride: capability });
66
- current = retried;
67
- return retried.status !== 'pass'
68
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'VERIFY_FAILED'}: ${retried.error?.message ?? 'manual verification effect failed'}` }
69
- : null;
70
- }
71
- });
72
- humanOverride = consumed;
73
- return current;
74
- });
39
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }));
75
40
  } catch (error) {
76
41
  if (!(error instanceof TaskExecutionLockError)) throw error;
77
42
  fail(`${error.code}: ${error.message}`);
78
43
  return;
79
44
  }
80
- if (humanOverride) result = { ...result, humanOverride } as typeof result;
81
45
  process.stdout.write(format === 'text' ? renderTaskVerification(result) : `${JSON.stringify(result)}\n`);
82
46
  process.exitCode = result.status === 'pass' ? 0 : result.status === 'blocked' ? 2 : 1;
83
47
  }
@@ -1,6 +1,5 @@
1
1
  import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.ts';
2
2
  import type { WorkflowWarningIntent } from '../task/workflow-warning-intents.ts';
3
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
4
3
  import { resolveTaskRef } from '../task/resolve-ref.ts';
5
4
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
6
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
@@ -9,7 +8,6 @@ const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-stat
9
8
  const FLAGS: Record<string, string> = {
10
9
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
11
10
  '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
12
- , '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
13
11
  };
14
12
 
15
13
  function usageFailure(message: string): void {
@@ -48,50 +46,23 @@ async function taskWarning(args: string[] = []): Promise<void> {
48
46
  };
49
47
  const optional = kind === 'list' ? ['status'] : [];
50
48
  const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...optional]);
51
- allowed.add('overrideTicket'); allowed.add('overrideTarget'); allowed.add('overrideScope');
52
49
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
53
50
  const missing = required[kind]!.find((key) => values[key] === undefined);
54
51
  if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
55
52
  if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
56
- const dryRunConflict = overrideDryRunConflict(values);
57
- if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
58
53
  const resolved = resolveTaskRef(taskRef);
59
54
  if (!resolved.ok) { usageFailure(resolved.message); return; }
60
55
  let result;
61
- let humanOverride: unknown = null;
62
56
  try {
63
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => {
64
- let current = applyWorkflowWarningIntent(values as WorkflowWarningIntent);
65
- if (current.status !== 'failed' || !values.overrideTicket) return current;
66
- if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
67
- const consumed = await consumeHumanOverride({
68
- taskRef, ticketId: String(values.overrideTicket),
69
- failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
70
- target: String(values.overrideTarget), scope: String(values.overrideScope)
71
- }, {
72
- effectExecutor: (capability) => {
73
- const retried = applyWorkflowWarningIntent(values as WorkflowWarningIntent, { manualOverride: capability });
74
- current = retried;
75
- return retried.status === 'failed' || retried.status === 'planned'
76
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no workflow warning effect was committed' : `${retried.error?.code ?? 'WARNING_FAILED'}: ${retried.error?.message ?? 'manual warning effect failed'}` }
77
- : null;
78
- }
79
- });
80
- humanOverride = consumed;
81
- return current;
82
- });
57
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => applyWorkflowWarningIntent(values as WorkflowWarningIntent));
83
58
  } catch (error) {
84
59
  if (!(error instanceof TaskExecutionLockError)) throw error;
85
60
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
86
61
  process.exitCode = 1;
87
62
  return;
88
63
  }
89
- process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
90
- const overrideFailed = Boolean(
91
- humanOverride && typeof humanOverride === 'object' &&
92
- (humanOverride as { status?: unknown }).status === 'failed'
93
- );
94
- if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
64
+ process.stdout.write(`${JSON.stringify(result)}\n`);
65
+ if (result.status === 'failed') process.exitCode = 1;
95
66
  }
96
67
 
97
68
  export { taskWarning };
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
 
4
4
  import { enumerateArtifacts } from '../task/artifacts.ts';
5
5
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.ts';
6
- import { renderHumanOverrideAudit } from '../task/human-override.ts';
7
6
  import { resolveTaskRef } from '../task/resolve-ref.ts';
8
7
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
9
8
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.ts';
@@ -11,7 +10,7 @@ import type { PrDeliveryFact } from '../task/pr-delivery-fact.ts';
11
10
  import { resolvePlatformProviderContext } from './context.ts';
12
11
  import type { LoadedContext, PlatformClient } from './context.ts';
13
12
  import { normalizeCommentContent } from './issue-comments.ts';
14
- import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
13
+ import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.ts';
15
14
  import { platformResult } from './types.ts';
16
15
  import type { PlatformResult } from './types.ts';
17
16
  import type { OperationWarning } from '../task/operation-outcome.ts';
@@ -74,25 +73,6 @@ function summaryMarker(taskId: string): string {
74
73
  return `<!-- sync-pr:${taskId}:summary -->`;
75
74
  }
76
75
 
77
- function escapeSummaryAuditText(value: string): string {
78
- return escapeHtmlText(value).replace(/\r\n?/g, '\n');
79
- }
80
-
81
- function renderSummaryAuditLine(value: string): string {
82
- const safe = escapeSummaryAuditText(value);
83
- const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
84
- const delimiter = '`'.repeat(longestRun + 1);
85
- return `${delimiter} ${safe} ${delimiter}`;
86
- }
87
-
88
- function renderSafeHumanOverrideAudit(value: string): string {
89
- const normalized = value.trim().replace(/\r\n?/g, '\n');
90
- if (!normalized) return '';
91
- const [heading, ...rows] = normalized.split('\n');
92
- const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
93
- return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
94
- }
95
-
96
76
  function isSafeSummaryEnvelope(value: string, taskId: string): boolean {
97
77
  return value.includes(summaryMarker(taskId))
98
78
  && (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
@@ -136,9 +116,8 @@ function platformError(error: { code: string; message: string; retryable?: boole
136
116
  return { code: error.code, message: error.message, retryable: error.retryable ?? false };
137
117
  }
138
118
 
139
- function buildPullRequestSummary(taskId: string, body: string, headSha: string, humanOverrideAudit = ''): string {
140
- const sections = [body.replace(/\s+$/, ''), renderSafeHumanOverrideAudit(humanOverrideAudit)].filter(Boolean).join('\n\n');
141
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
119
+ function buildPullRequestSummary(taskId: string, body: string, headSha: string): string {
120
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
142
121
  }
143
122
 
144
123
  function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
@@ -643,7 +622,7 @@ async function syncPullRequestSummary(
643
622
  `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
644
623
  )
645
624
  : replaced.value;
646
- const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
625
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
647
626
  if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
648
627
  code: 'PR_SUMMARY_RENDER_INVALID',
649
628
  message: 'Summary contains an invalid or duplicated reserved control marker',
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { execFileSync } from "node:child_process";
7
7
 
8
+ import { resolveBranchWorktree } from "../git/branch-worktree.ts";
9
+
8
10
  import { resolvePlatformProviderContext } from "./context.ts";
9
11
  import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.ts";
10
12
  import { requirementSectionAnchors } from "./issues.ts";
@@ -1054,7 +1056,10 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1054
1056
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
1055
1057
  }
1056
1058
  const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
1057
- const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
1059
+ const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
1060
+ if (!gitCwd) {
1061
+ return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
1062
+ }
1058
1063
  const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
1059
1064
  if (!changedFilesResult.ok) {
1060
1065
  return { ...changedFilesResult, type: "network_error" };
@@ -1082,25 +1087,6 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1082
1087
  return { ok: true, labels: planned.target, mode: "mapped" };
1083
1088
  }
1084
1089
 
1085
- function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
1086
- try {
1087
- const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
1088
- cwd: repositoryRoot,
1089
- encoding: "utf8",
1090
- stdio: ["pipe", "pipe", "pipe"]
1091
- }).trim().split(/\r?\n\r?\n/);
1092
- const expectedRef = `refs/heads/${branch}`;
1093
- for (const record of records) {
1094
- const worktree = /^worktree (.+)$/m.exec(record)?.[1];
1095
- const branchRef = /^branch (.+)$/m.exec(record)?.[1];
1096
- if (worktree && branchRef === expectedRef) return worktree;
1097
- }
1098
- } catch {
1099
- // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
1100
- }
1101
- return null;
1102
- }
1103
-
1104
1090
  function loadInLabelMapping(shared: VerificationShared): any {
1105
1091
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
1106
1092
  if (!fs.existsSync(configPath)) {
@@ -1141,42 +1127,16 @@ function resolvePrHeadSha(context: any): any {
1141
1127
  return fallback();
1142
1128
  }
1143
1129
 
1144
- const worktreeList = gitText(["worktree", "list", "--porcelain"], context.taskDir);
1145
- if (!worktreeList.ok) {
1146
- return fallback();
1147
- }
1148
-
1149
- const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
1130
+ const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
1150
1131
  if (!matchedWorktree) {
1151
- return fallback();
1152
- }
1153
-
1154
- const headInWorktree = gitText(["rev-parse", "HEAD"], matchedWorktree);
1155
- if (!headInWorktree.ok) {
1156
- return fallback();
1157
- }
1158
-
1159
- return headInWorktree;
1160
- }
1161
-
1162
- function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
1163
- let currentWorktree = "";
1164
- for (const rawLine of String(porcelainOutput || "").split("\n")) {
1165
- const line = rawLine.trimEnd();
1166
- if (line.startsWith("worktree ")) {
1167
- currentWorktree = line.slice("worktree ".length).trim();
1168
- continue;
1169
- }
1170
-
1171
- if (line.startsWith("branch refs/heads/")) {
1172
- const usedBranch = line.slice("branch refs/heads/".length).trim();
1173
- if (usedBranch === branch && currentWorktree) {
1174
- return currentWorktree;
1175
- }
1176
- }
1132
+ return {
1133
+ ok: false,
1134
+ type: "check_failed",
1135
+ message: `Task branch '${branch}' is not checked out in a usable worktree`
1136
+ };
1177
1137
  }
1178
1138
 
1179
- return null;
1139
+ return gitText(["rev-parse", "HEAD"], matchedWorktree);
1180
1140
  }
1181
1141
 
1182
1142
  function interpolate(template: any, taskDir: any, artifactFile: any): any {
@@ -393,6 +393,7 @@ export function requestSandboxControl(params: Readonly<{
393
393
 
394
394
  export function requestSandboxTaskFinalization(params: Readonly<{
395
395
  agent: string;
396
+ prepareHandoff?: (binding: Readonly<{ generation: string; requestId: string }>) => string;
396
397
  channelDir?: string;
397
398
  statusDir?: string;
398
399
  token?: string;
@@ -403,15 +404,18 @@ export function requestSandboxTaskFinalization(params: Readonly<{
403
404
  if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
404
405
  const auth = authority(params);
405
406
  const issuedAt = Date.now();
407
+ const id = randomUUID();
408
+ const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
406
409
  const request: SandboxTaskFinalizationRequest = {
407
410
  version: 3,
408
- id: randomUUID(),
411
+ id,
409
412
  ...auth,
410
413
  issuedAt,
411
414
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
412
415
  family: 'task-finalization',
413
416
  operation: 'complete',
414
417
  agent,
418
+ handoffSha256,
415
419
  args: [],
416
420
  controllerProcess: null,
417
421
  controllerProof: null
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
+ import { homedir } from 'node:os';
5
6
  import { getProcessStartTime } from '../../server/process-state.ts';
6
7
  import {
7
8
  createTask,
@@ -41,6 +42,11 @@ import {
41
42
  openCodexControllerRegistration,
42
43
  resolveCodexControllerBinding
43
44
  } from './controller-registration.ts';
45
+ import { shareBranchDir } from '../constants.ts';
46
+
47
+ function handoffDirectory(manifest: SandboxControlManifest): string {
48
+ return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
49
+ }
44
50
 
45
51
  export type SandboxControlExecutionResult = {
46
52
  exitCode: number;
@@ -373,10 +379,11 @@ async function executeRequestInner(
373
379
  }
374
380
  }
375
381
  if (request.family === 'task-finalization') {
376
- const operation = parseTaskControlOperation(
382
+ const parsed = parseTaskControlOperation(
377
383
  'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
378
384
  );
379
- if (operation.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
385
+ if (parsed.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
386
+ const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
380
387
  const result = dispatchTaskControlOperation(
381
388
  createSandboxExecutorExecutionContext({
382
389
  repoRoot: manifest.repoRoot,
@@ -385,7 +392,8 @@ async function executeRequestInner(
385
392
  taskId: manifest.taskId!,
386
393
  generation: manifest.generation,
387
394
  manifestPath,
388
- requestId: request.id
395
+ requestId: request.id,
396
+ handoffDirectory: handoffDirectory(manifest)
389
397
  }),
390
398
  operation
391
399
  );
@@ -407,7 +415,8 @@ async function executeRequestInner(
407
415
  taskId: manifest.taskId!,
408
416
  generation: manifest.generation,
409
417
  manifestPath,
410
- requestId: request.id
418
+ requestId: request.id,
419
+ handoffDirectory: handoffDirectory(manifest)
411
420
  });
412
421
  const format = (value: unknown): SandboxControlExecutionResult => {
413
422
  const result = value as { status?: string };
@@ -65,7 +65,7 @@ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
65
65
  family: 'task-lifecycle'; args: string[];
66
66
  }>;
67
67
  export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
68
- family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
68
+ family: 'task-finalization'; operation: 'complete'; agent: string; handoffSha256: string; args: [];
69
69
  }>;
70
70
  export type SandboxTaskCreateRequest = RequestBase & Readonly<{
71
71
  family: 'task-create'; candidate: TaskCreateCandidateV1;
@@ -279,11 +279,12 @@ export function validateSandboxControlRequest(
279
279
  return request as SandboxCodexControllerRequest;
280
280
  }
281
281
  if (request.family === 'task-finalization') {
282
- const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
282
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
283
283
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
284
284
  || request.operation !== 'complete'
285
285
  || !Array.isArray(request.args) || request.args.length !== 0
286
286
  || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
287
+ || typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
287
288
  || request.controllerProcess !== null || request.controllerProof !== null) {
288
289
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
289
290
  }
@@ -47,6 +47,7 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
47
47
  import type { SandboxTool } from './tools.ts';
48
48
  import { getProcessStartTime } from '../server/process-state.ts';
49
49
  import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
50
+ import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
50
51
  import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
51
52
  import {
52
53
  formatIntermediateCleanupReport,
@@ -959,7 +960,10 @@ async function rmOne(
959
960
  branch: string,
960
961
  options: RmOneOptions = {}
961
962
  ): Promise<void> {
962
- await runRmOneUnderRepositoryLock(config, tools, branch, options);
963
+ await withRepositoryMutationLock(
964
+ config.repoRoot,
965
+ () => runRmOneUnderRepositoryLock(config, tools, branch, options)
966
+ );
963
967
  }
964
968
 
965
969
  async function removeUncheckedSandbox(
@@ -998,7 +1002,16 @@ async function removeUncheckedSandbox(
998
1002
  return null;
999
1003
  }
1000
1004
 
1005
+ const worktreePermits = options.permits ?? await authorizeWorktrees(
1006
+ existingWorktrees,
1007
+ { allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) },
1008
+ { ...options.prompt, interactive: options.interactive }
1009
+ );
1010
+
1001
1011
  for (const container of matchedContainers) runSafeEngine(engine, 'docker', ['rm', '-f', container]);
1012
+ if (!sandboxContainersRemoved(engine, matchedContainers)) {
1013
+ throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
1014
+ }
1002
1015
  for (const root of controlRoots) {
1003
1016
  fs.rmSync(root, { recursive: true, force: true });
1004
1017
  removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
@@ -1011,12 +1024,15 @@ async function removeUncheckedSandbox(
1011
1024
  for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
1012
1025
  if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
1013
1026
  if (shouldRemoveWorktree) {
1014
- for (const worktree of existingWorktrees) fs.rmSync(worktree, { recursive: true, force: true });
1015
- runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
1027
+ for (const worktree of existingWorktrees) {
1028
+ const permit = worktreePermits.get(path.resolve(worktree));
1029
+ if (!permit) throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
1030
+ removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
1031
+ }
1016
1032
  }
1017
- if (shouldDeleteBranch) runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1018
- if (!sandboxContainersRemoved(engine, matchedContainers)) {
1019
- throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
1033
+ if (shouldDeleteBranch) {
1034
+ assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
1035
+ runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1020
1036
  }
1021
1037
  if (target.workspace.mode === 'task-bound') {
1022
1038
  releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
@@ -1045,7 +1061,7 @@ async function rmPurge(
1045
1061
  tools: SandboxTool[],
1046
1062
  prompt: PromptDependencies = {}
1047
1063
  ): Promise<void> {
1048
- return rmPurgeCore(config, tools, prompt);
1064
+ return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
1049
1065
  }
1050
1066
 
1051
1067
  async function rmPurgeCore(
@@ -14,7 +14,6 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
14
14
  import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
15
15
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
16
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
- import { allowsManualOverride } from './guard-override.ts';
18
17
 
19
18
  type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
20
19
  type PrReviewOutcome = 'aborted' | 'superseded';
@@ -398,7 +397,7 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
398
397
  if ('code' in validated) return failed(intent, validated.code, validated.message);
399
398
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
400
399
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
401
- if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
400
+ if (resolved.state !== 'active') {
402
401
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
403
402
  }
404
403
  if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });
@@ -27,6 +27,7 @@ type ArtifactStructuralDiagnosticCode =
27
27
  | 'ARTIFACT_EMPTY_SECTION'
28
28
  | 'ARTIFACT_SECTION_ORDER_INVALID'
29
29
  | 'ARTIFACT_HEADING_TRAILING_PUNCTUATION'
30
+ | 'ARTIFACT_STATE_CHECK_PATTERN_MISSING'
30
31
  | 'ARTIFACT_REQUIRED_PATTERN_MISSING';
31
32
 
32
33
  type ArtifactStructuralDiagnostic = Readonly<{
@@ -254,13 +255,68 @@ function inspectArtifactPatterns(
254
255
  };
255
256
  }
256
257
 
258
+ function inspectArtifactStateCheck(
259
+ content: string,
260
+ schema: ArtifactSchema,
261
+ scopeToStateCheckSection: boolean
262
+ ): ArtifactStructureResult {
263
+ const stateCheck = schema.sections.find((section) => section.id === 'state-check');
264
+ if (!stateCheck || schema.stateCheckPatterns.length === 0) {
265
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
266
+ }
267
+
268
+ if (!scopeToStateCheckSection) {
269
+ const diagnostics = schema.stateCheckPatterns
270
+ .filter((pattern) => !new RegExp(pattern, 'm').test(content))
271
+ .map((pattern) => diagnostic(
272
+ 'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
273
+ `artifact is missing required state-check command '${pattern}'`,
274
+ stateCheck.id,
275
+ null
276
+ ));
277
+ return {
278
+ ok: diagnostics.length === 0,
279
+ family: schema.family,
280
+ semanticDigest: canonicalSemanticDigest(content),
281
+ diagnostics
282
+ };
283
+ }
284
+
285
+ const scanned = scanVisibleMarkdown(content);
286
+ const headings = scanned.headings.filter((heading) => (
287
+ heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)
288
+ ));
289
+ if (headings.length !== 1) {
290
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
291
+ }
292
+
293
+ const bodyBounds = sectionBodyBounds(content, headings[0]!, scanned);
294
+ const body = content.slice(bodyBounds.start, bodyBounds.end);
295
+ const diagnostics = schema.stateCheckPatterns
296
+ .filter((pattern) => !new RegExp(pattern, 'm').test(body))
297
+ .map((pattern) => diagnostic(
298
+ 'ARTIFACT_STATE_CHECK_PATTERN_MISSING',
299
+ `state-check section is missing required command '${pattern}'`,
300
+ stateCheck.id,
301
+ lineNumber(content, headings[0]!.start)
302
+ ));
303
+ return {
304
+ ok: diagnostics.length === 0,
305
+ family: schema.family,
306
+ semanticDigest: canonicalSemanticDigest(content),
307
+ diagnostics
308
+ };
309
+ }
310
+
257
311
  function inspectArtifactContract(
258
312
  content: string,
259
- schema: ArtifactSchema
313
+ schema: ArtifactSchema,
314
+ options: { scopeStateCheckSection?: boolean } = {}
260
315
  ): ArtifactStructureResult {
261
316
  const structure = inspectArtifactStructure(content, schema);
317
+ const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
262
318
  const patterns = inspectArtifactPatterns(content, schema);
263
- const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
319
+ const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
264
320
  return {
265
321
  ok: diagnostics.length === 0,
266
322
  family: schema.family,