@fitlab-ai/agent-infra 0.9.7 → 0.9.8

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 (96) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  7. package/dist/lib/decide.js +68 -14
  8. package/dist/lib/init.js +1 -1
  9. package/dist/lib/internal/release-workflow.js +1 -3
  10. package/dist/lib/internal/task-activity.js +57 -7
  11. package/dist/lib/internal/task-event.js +57 -4
  12. package/dist/lib/internal/task-ledger.js +59 -5
  13. package/dist/lib/internal/task-lifecycle.js +59 -8
  14. package/dist/lib/internal/task-orchestration.js +11 -3
  15. package/dist/lib/internal/task-override.js +131 -0
  16. package/dist/lib/internal/task-review.js +64 -6
  17. package/dist/lib/internal/task-verify.js +65 -3
  18. package/dist/lib/internal/task-warning.js +57 -4
  19. package/dist/lib/platform/pr-summary.js +5 -3
  20. package/dist/lib/runtime/agent-runtime.js +56 -0
  21. package/dist/lib/sandbox/commands/create.js +389 -311
  22. package/dist/lib/sandbox/control/container-identity.js +1 -1
  23. package/dist/lib/sandbox/control/executor.js +10 -2
  24. package/dist/lib/sandbox/control/lifecycle.js +392 -72
  25. package/dist/lib/sandbox/control/state.js +27 -11
  26. package/dist/lib/sandbox/engine.js +14 -7
  27. package/dist/lib/sandbox/mounts.js +20 -5
  28. package/dist/lib/sandbox/recovery.js +78 -10
  29. package/dist/lib/sandbox/workspace-view.js +62 -40
  30. package/dist/lib/server/process-state.js +51 -1
  31. package/dist/lib/task/activity-intent.js +4 -1
  32. package/dist/lib/task/decision-intents.js +4 -2
  33. package/dist/lib/task/delegation-receipts.js +37 -16
  34. package/dist/lib/task/events.js +9 -5
  35. package/dist/lib/task/guard-override.js +31 -0
  36. package/dist/lib/task/human-override.js +1240 -0
  37. package/dist/lib/task/ledger-intents.js +6 -3
  38. package/dist/lib/task/lifecycle.js +69 -14
  39. package/dist/lib/task/orchestration.js +60 -6
  40. package/dist/lib/task/review-finalization.js +3 -2
  41. package/dist/lib/task/verification-engine.js +40 -14
  42. package/dist/lib/task/verification.js +13 -1
  43. package/dist/lib/task/workflow-warning-intents.js +4 -2
  44. package/dist/lib/task/write.js +2 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  47. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
  49. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  51. package/lib/decide.ts +59 -14
  52. package/lib/init.ts +1 -1
  53. package/lib/internal/release-workflow.ts +1 -3
  54. package/lib/internal/task-activity.ts +46 -6
  55. package/lib/internal/task-event.ts +53 -4
  56. package/lib/internal/task-ledger.ts +48 -4
  57. package/lib/internal/task-lifecycle.ts +56 -12
  58. package/lib/internal/task-orchestration.ts +12 -4
  59. package/lib/internal/task-override.ts +138 -0
  60. package/lib/internal/task-review.ts +51 -6
  61. package/lib/internal/task-verify.ts +52 -3
  62. package/lib/internal/task-warning.ts +44 -3
  63. package/lib/platform/pr-summary.ts +10 -3
  64. package/lib/runtime/agent-runtime.ts +76 -0
  65. package/lib/sandbox/commands/create.ts +88 -4
  66. package/lib/sandbox/config.ts +2 -1
  67. package/lib/sandbox/control/container-identity.ts +3 -3
  68. package/lib/sandbox/control/executor.ts +10 -2
  69. package/lib/sandbox/control/lifecycle.ts +463 -76
  70. package/lib/sandbox/control/protocol.ts +5 -2
  71. package/lib/sandbox/control/state.ts +35 -18
  72. package/lib/sandbox/engine.ts +20 -9
  73. package/lib/sandbox/mounts.ts +23 -5
  74. package/lib/sandbox/recovery.ts +90 -9
  75. package/lib/sandbox/workspace-view.ts +70 -40
  76. package/lib/server/process-state.ts +48 -1
  77. package/lib/task/activity-intent.ts +4 -2
  78. package/lib/task/decision-intents.ts +4 -2
  79. package/lib/task/delegation-receipts.ts +46 -18
  80. package/lib/task/events.ts +10 -5
  81. package/lib/task/guard-override.ts +50 -0
  82. package/lib/task/human-override.ts +1503 -0
  83. package/lib/task/ledger-intents.ts +6 -3
  84. package/lib/task/lifecycle.ts +71 -14
  85. package/lib/task/orchestration.ts +89 -7
  86. package/lib/task/review-finalization.ts +6 -2
  87. package/lib/task/verification-engine.ts +42 -16
  88. package/lib/task/verification.ts +15 -1
  89. package/lib/task/workflow-warning-intents.ts +4 -2
  90. package/lib/task/write.ts +4 -1
  91. package/package.json +1 -1
  92. package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
  95. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  96. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
@@ -0,0 +1,138 @@
1
+ import {
2
+ consumeHumanOverride,
3
+ diagnoseHumanOverrideForTask,
4
+ issueHumanOverride
5
+ } from '../task/human-override.ts';
6
+ import type { ConsumeHumanOverrideRequest, HumanOverrideRequest } from '../task/human-override.ts';
7
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
8
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
9
+
10
+ const USAGE = `Usage: agent-infra-internal task-override <task-ref> <diagnose|issue|consume> [options]
11
+
12
+ Options: --failure-id <id> --target <target> --operator <operator> --reason <reason> --scope <scope>
13
+ --intent <lifecycle-intent> --alert-number <N> --issue-number <N> --staging-dir <path>
14
+ --expires-at <timestamp> --ticket <ticket-id> --pr-number <N>
15
+ `;
16
+
17
+ const FLAGS: Record<string, string> = {
18
+ '--failure-id': 'failureId',
19
+ '--target': 'target',
20
+ '--operator': 'operator',
21
+ '--reason': 'reason',
22
+ '--scope': 'scope',
23
+ '--intent': 'intent',
24
+ '--failure-digest': 'failureDigest',
25
+ '--expires-at': 'expiresAt',
26
+ '--ticket': 'ticketId',
27
+ '--facts': 'observedFacts',
28
+ '--alert-number': 'alertNumber',
29
+ '--issue-number': 'issueNumber',
30
+ '--pr-number': 'pullRequestNumber',
31
+ '--staging-dir': 'stagingDir'
32
+ };
33
+
34
+ function usageFailure(message: string): void {
35
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'OVERRIDE_PAYLOAD_INVALID', message } })}\n`);
36
+ process.stderr.write(USAGE);
37
+ process.exitCode = 1;
38
+ }
39
+
40
+ function taskOverride(args: string[] = []): void {
41
+ if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
42
+ const [taskRef, operation] = args;
43
+ if (!taskRef || !operation || !['diagnose', 'issue', 'consume'].includes(operation)) {
44
+ usageFailure('task ref and a valid operation are required');
45
+ return;
46
+ }
47
+ const values: Record<string, unknown> = { taskRef };
48
+ const seen = new Set<string>();
49
+ for (let index = 2; index < args.length; index += 1) {
50
+ const flag = args[index]!;
51
+ const key = FLAGS[flag];
52
+ if (!key) { usageFailure(`unknown option '${flag}'`); return; }
53
+ if (seen.has(flag)) { usageFailure(`duplicate option '${flag}'`); return; }
54
+ const value = args[++index];
55
+ if (value === undefined || value.startsWith('--')) { usageFailure(`option '${flag}' requires a value`); return; }
56
+ seen.add(flag);
57
+ values[key] = key === 'observedFacts' ? value.split(',').map((fact) => fact.trim()).filter(Boolean)
58
+ : key === 'alertNumber' || key === 'issueNumber' || key === 'pullRequestNumber' ? Number(value) : value;
59
+ }
60
+
61
+ const required: Record<string, string[]> = {
62
+ diagnose: [],
63
+ issue: ['failureId', 'target', 'operator', 'reason', 'scope', 'expiresAt'],
64
+ consume: ['failureId', 'target', 'scope', 'ticketId']
65
+ };
66
+ const missing = required[operation]!.find((key) => values[key] === undefined);
67
+ if (missing) { usageFailure(`${operation} requires '--${missing.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}'`); return; }
68
+
69
+ const resolved = resolveTaskRef(taskRef);
70
+ if (!resolved.ok) {
71
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+
76
+ let result;
77
+ try {
78
+ if (operation === 'diagnose') {
79
+ result = diagnoseHumanOverrideForTask(
80
+ taskRef,
81
+ values.failureId ? String(values.failureId) : undefined,
82
+ values.target ? String(values.target) : undefined
83
+ );
84
+ } else if (operation === 'issue') {
85
+ result = withTaskExecutionLock(
86
+ resolved.repoRoot,
87
+ resolved.taskId,
88
+ 'task-override.issue',
89
+ () => issueHumanOverride({
90
+ taskRef,
91
+ failureId: String(values.failureId),
92
+ target: String(values.target),
93
+ operator: String(values.operator),
94
+ reason: String(values.reason),
95
+ scope: String(values.scope),
96
+ expiresAt: String(values.expiresAt),
97
+ ...(values.intent ? { intent: String(values.intent) as HumanOverrideRequest['intent'] } : {}),
98
+ ...(values.alertNumber !== undefined ? { alertNumber: Number(values.alertNumber) } : {}),
99
+ ...(values.issueNumber !== undefined ? { issueNumber: Number(values.issueNumber) } : {}),
100
+ ...(values.pullRequestNumber !== undefined ? { pullRequestNumber: Number(values.pullRequestNumber) } : {}),
101
+ ...(values.stagingDir ? { stagingDir: String(values.stagingDir) } : {}),
102
+ ...(values.failureDigest ? { failureDigest: String(values.failureDigest) } : {})
103
+ } satisfies HumanOverrideRequest)
104
+ );
105
+ } else {
106
+ result = withTaskExecutionLock(
107
+ resolved.repoRoot,
108
+ resolved.taskId,
109
+ 'task-override.consume',
110
+ () => consumeHumanOverride({
111
+ taskRef,
112
+ ticketId: String(values.ticketId),
113
+ failureId: String(values.failureId),
114
+ target: String(values.target),
115
+ scope: String(values.scope),
116
+ ...(values.intent ? { intent: String(values.intent) as ConsumeHumanOverrideRequest['intent'] } : {}),
117
+ ...(values.alertNumber !== undefined ? { alertNumber: Number(values.alertNumber) } : {}),
118
+ ...(values.issueNumber !== undefined ? { issueNumber: Number(values.issueNumber) } : {}),
119
+ ...(values.pullRequestNumber !== undefined ? { pullRequestNumber: Number(values.pullRequestNumber) } : {}),
120
+ ...(values.stagingDir ? { stagingDir: String(values.stagingDir) } : {}),
121
+ ...(values.failureDigest ? { failureDigest: String(values.failureDigest) } : {}),
122
+ ...(values.observedFacts ? { observedFacts: values.observedFacts as string[] } : {})
123
+ } satisfies ConsumeHumanOverrideRequest)
124
+ );
125
+ }
126
+ } catch (cause) {
127
+ if (cause instanceof TaskExecutionLockError) {
128
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: cause.code, message: cause.message } })}\n`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ throw cause;
133
+ }
134
+ process.stdout.write(`${JSON.stringify(result)}\n`);
135
+ if (result.status === 'failed') process.exitCode = 1;
136
+ }
137
+
138
+ export { taskOverride };
@@ -1,6 +1,9 @@
1
1
  import { finalizeReviewSummary } from '../task/review-finalization.ts';
2
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
3
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
4
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
2
5
 
3
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run]\n';
6
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
4
7
 
5
8
  function failUsage(message: string): void {
6
9
  process.stdout.write(`${JSON.stringify({
@@ -30,10 +33,13 @@ function taskReview(args: string[] = []): void {
30
33
  let artifact = '';
31
34
  let dryRun = false;
32
35
  let orchestrated = false;
36
+ let overrideTicket = '';
37
+ let overrideTarget = '';
38
+ let overrideScope = '';
33
39
  const seen = new Set<string>();
34
40
  for (let index = 2; index < args.length; index += 1) {
35
41
  const flag = args[index]!;
36
- if (!['--stage', '--artifact', '--orchestrated', '--dry-run'].includes(flag)) {
42
+ if (!['--stage', '--artifact', '--orchestrated', '--dry-run', '--override-ticket', '--override-target', '--override-scope'].includes(flag)) {
37
43
  failUsage(`unknown option '${flag}'`);
38
44
  return;
39
45
  }
@@ -56,15 +62,54 @@ function taskReview(args: string[] = []): void {
56
62
  return;
57
63
  }
58
64
  if (flag === '--stage') stage = value;
59
- else artifact = value;
65
+ else if (flag === '--artifact') artifact = value;
66
+ else if (flag === '--override-ticket') overrideTicket = value;
67
+ else if (flag === '--override-target') overrideTarget = value;
68
+ else if (flag === '--override-scope') overrideScope = value;
60
69
  }
61
70
  if (!stage || !artifact) {
62
71
  failUsage("options '--stage' and '--artifact' are required");
63
72
  return;
64
73
  }
65
- const result = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun });
66
- process.stdout.write(`${JSON.stringify(result)}\n`);
67
- if (result.status === 'failed') process.exitCode = 1;
74
+ const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
75
+ if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
76
+ const resolved = resolveTaskRef(args[0]!);
77
+ if (!resolved.ok) { failUsage(resolved.message); return; }
78
+ let result;
79
+ let humanOverride: unknown = null;
80
+ try {
81
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => {
82
+ let current = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
83
+ if (current.status !== 'failed' || !overrideTicket) return current;
84
+ if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
85
+ const consumed = consumeHumanOverride({
86
+ taskRef: args[0]!, ticketId: overrideTicket,
87
+ failureId: failureId('review-finalization', current.error?.code ?? 'TASK_STATE_MISMATCH'),
88
+ target: overrideTarget, scope: overrideScope
89
+ }, {
90
+ effectExecutor: (capability) => {
91
+ const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
92
+ current = retried;
93
+ return retried.status === 'failed' || retried.status === 'planned'
94
+ ? { 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'}` }
95
+ : null;
96
+ }
97
+ });
98
+ humanOverride = consumed;
99
+ return current;
100
+ });
101
+ } catch (error) {
102
+ if (!(error instanceof TaskExecutionLockError)) throw error;
103
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
108
+ const overrideFailed = Boolean(
109
+ humanOverride && typeof humanOverride === 'object' &&
110
+ (humanOverride as { status?: unknown }).status === 'failed'
111
+ );
112
+ if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
68
113
  }
69
114
 
70
115
  export { taskReview };
@@ -1,6 +1,9 @@
1
1
  import { renderTaskVerification, verifyTaskEvent } from '../task/verification.ts';
2
+ import { consumeHumanOverride, failureId } from '../task/human-override.ts';
3
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
4
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
2
5
 
3
- const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text]\n';
6
+ 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';
4
7
 
5
8
  function fail(message: string): void {
6
9
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'VERIFY_PAYLOAD_INVALID', message } })}\n`);
@@ -11,21 +14,67 @@ function fail(message: string): void {
11
14
  function taskVerify(args: string[] = []): void {
12
15
  if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
13
16
  if (!args[0] || !args[1] || args[0].startsWith('--') || args[1].startsWith('--')) { fail('task ref and verification event are required'); return; }
17
+ const taskRef = args[0];
18
+ const event = args[1];
14
19
  let artifact: string | undefined;
15
20
  let format: 'json' | 'text' = 'json';
21
+ let overrideTicket: string | undefined;
22
+ let overrideTarget: string | undefined;
23
+ let overrideScope: string | undefined;
16
24
  const seen = new Set<string>();
17
25
  for (let index = 2; index < args.length; index += 1) {
18
26
  const flag = args[index]!;
19
- if (flag !== '--artifact' && flag !== '--format') { fail(`unknown option '${flag}'`); return; }
27
+ if (!['--artifact', '--format', '--override-ticket', '--override-target', '--override-scope'].includes(flag)) { fail(`unknown option '${flag}'`); return; }
20
28
  if (seen.has(flag)) { fail(`duplicate option '${flag}'`); return; }
21
29
  const value = args[++index];
22
30
  if (!value || value.startsWith('--')) { fail(`option '${flag}' requires a value`); return; }
23
31
  seen.add(flag);
24
32
  if (flag === '--artifact') artifact = value;
33
+ else if (flag === '--override-ticket') overrideTicket = value;
34
+ else if (flag === '--override-target') overrideTarget = value;
35
+ else if (flag === '--override-scope') overrideScope = value;
25
36
  else if (value === 'json' || value === 'text') format = value;
26
37
  else { fail("option '--format' requires 'json' or 'text'"); return; }
27
38
  }
28
- const result = verifyTaskEvent({ taskRef: args[0], event: args[1], ...(artifact ? { artifact } : {}) });
39
+ const resolved = resolveTaskRef(taskRef);
40
+ if (!resolved.ok) { fail(resolved.message); return; }
41
+ let result;
42
+ let humanOverride: unknown = null;
43
+ try {
44
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, () => {
45
+ let current = verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) });
46
+ if (current.status === 'pass' || !overrideTicket) return current;
47
+ if (!overrideTarget || !overrideScope) { fail('override ticket requires target and scope'); return current; }
48
+ const failureProducer = current.status === 'blocked' || current.status === 'fail'
49
+ ? 'verification-engine'
50
+ : 'task-verify';
51
+ const failureCode = current.status === 'blocked'
52
+ ? 'CHECK_BLOCKED'
53
+ : current.status === 'fail'
54
+ ? 'CHECK_FAILED'
55
+ : current.error?.code ?? 'VERIFY_TASK_STATE_MISMATCH';
56
+ const consumed = consumeHumanOverride({
57
+ taskRef, ticketId: overrideTicket,
58
+ failureId: failureId(failureProducer, failureCode),
59
+ target: overrideTarget, scope: overrideScope
60
+ }, {
61
+ effectExecutor: (capability) => {
62
+ const retried = verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }, { manualOverride: capability });
63
+ current = retried;
64
+ return retried.status !== 'pass'
65
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'VERIFY_FAILED'}: ${retried.error?.message ?? 'manual verification effect failed'}` }
66
+ : null;
67
+ }
68
+ });
69
+ humanOverride = consumed;
70
+ return current;
71
+ });
72
+ } catch (error) {
73
+ if (!(error instanceof TaskExecutionLockError)) throw error;
74
+ fail(`${error.code}: ${error.message}`);
75
+ return;
76
+ }
77
+ if (humanOverride) result = { ...result, humanOverride } as typeof result;
29
78
  process.stdout.write(format === 'text' ? renderTaskVerification(result) : `${JSON.stringify(result)}\n`);
30
79
  process.exitCode = result.status === 'pass' ? 0 : result.status === 'blocked' ? 2 : 1;
31
80
  }
@@ -1,10 +1,14 @@
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
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
5
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
3
6
 
4
7
  const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-status|list> [intent flags] [--dry-run]\n`;
5
8
  const FLAGS: Record<string, string> = {
6
9
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
7
10
  '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
11
+ , '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
8
12
  };
9
13
 
10
14
  function usageFailure(message: string): void {
@@ -38,13 +42,50 @@ function taskWarning(args: string[] = []): void {
38
42
  };
39
43
  const optional = kind === 'list' ? ['status'] : [];
40
44
  const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...optional]);
45
+ allowed.add('overrideTicket'); allowed.add('overrideTarget'); allowed.add('overrideScope');
41
46
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
42
47
  const missing = required[kind]!.find((key) => values[key] === undefined);
43
48
  if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
44
49
  if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
45
- const result = applyWorkflowWarningIntent(values as WorkflowWarningIntent);
46
- process.stdout.write(`${JSON.stringify(result)}\n`);
47
- if (result.status === 'failed') process.exitCode = 1;
50
+ const dryRunConflict = overrideDryRunConflict(values);
51
+ if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
52
+ const resolved = resolveTaskRef(taskRef);
53
+ if (!resolved.ok) { usageFailure(resolved.message); return; }
54
+ let result;
55
+ let humanOverride: unknown = null;
56
+ try {
57
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, () => {
58
+ let current = applyWorkflowWarningIntent(values as WorkflowWarningIntent);
59
+ if (current.status !== 'failed' || !values.overrideTicket) return current;
60
+ if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
61
+ const consumed = consumeHumanOverride({
62
+ taskRef, ticketId: String(values.overrideTicket),
63
+ failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
64
+ target: String(values.overrideTarget), scope: String(values.overrideScope)
65
+ }, {
66
+ effectExecutor: (capability) => {
67
+ const retried = applyWorkflowWarningIntent(values as WorkflowWarningIntent, { manualOverride: capability });
68
+ current = retried;
69
+ return retried.status === 'failed' || retried.status === 'planned'
70
+ ? { 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'}` }
71
+ : null;
72
+ }
73
+ });
74
+ humanOverride = consumed;
75
+ return current;
76
+ });
77
+ } catch (error) {
78
+ if (!(error instanceof TaskExecutionLockError)) throw error;
79
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
84
+ const overrideFailed = Boolean(
85
+ humanOverride && typeof humanOverride === 'object' &&
86
+ (humanOverride as { status?: unknown }).status === 'failed'
87
+ );
88
+ if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
48
89
  }
49
90
 
50
91
  export { taskWarning };
@@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process';
3
3
 
4
4
  import { enumerateArtifacts } from '../task/artifacts.ts';
5
5
  import { parseTaskFrontmatter } from '../task/frontmatter.ts';
6
+ import { renderHumanOverrideAudit } from '../task/human-override.ts';
6
7
  import { resolveTaskRef } from '../task/resolve-ref.ts';
7
8
  import { resolvePlatformContext } from './context.ts';
8
9
  import { createGitHubClient } from './github-client.ts';
@@ -21,8 +22,9 @@ function summaryMarker(taskId: string): string {
21
22
  return `<!-- sync-pr:${taskId}:summary -->`;
22
23
  }
23
24
 
24
- function buildPullRequestSummary(taskId: string, body: string, headSha: string): string {
25
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
25
+ function buildPullRequestSummary(taskId: string, body: string, headSha: string, humanOverrideAudit = ''): string {
26
+ const sections = [body.replace(/\s+$/, ''), humanOverrideAudit.trim()].filter(Boolean).join('\n\n');
27
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
26
28
  }
27
29
 
28
30
  function reconcileSummaryComment(comments: SummaryComment[], taskId: string, desired: string):
@@ -74,7 +76,12 @@ function syncPullRequestSummary(taskRef: string, options: { agent: string; body:
74
76
  } catch (error) {
75
77
  return platformResult('failed', { platform: context.platform, capabilities: context.capabilities, error: { code: 'GIT_HEAD_UNRESOLVED', message: error instanceof Error ? error.message : String(error), retryable: false } });
76
78
  }
77
- const desired = buildPullRequestSummary(resolved.taskId, options.body, headSha);
79
+ const desired = buildPullRequestSummary(
80
+ resolved.taskId,
81
+ options.body,
82
+ headSha,
83
+ renderHumanOverrideAudit(fs.readFileSync(resolved.taskMdPath, 'utf8'))
84
+ );
78
85
  const listed = listRemoteComments(client, context.platform.repository, prNumber, resolved.repoRoot);
79
86
  if (!listed.ok) return platformResult(listed.error.retryable ? 'blocked' : 'failed', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: listed.error });
80
87
  const reconciliation = reconcileSummaryComment(listed.value, resolved.taskId, desired);
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+
3
+ export type AgentRuntimeStore = 'capabilities' | 'lifecycle';
4
+
5
+ type RuntimeResolutionOptions = Readonly<{
6
+ repoRoot?: string;
7
+ env?: NodeJS.ProcessEnv;
8
+ explicitRoot?: string;
9
+ client?: string;
10
+ store?: AgentRuntimeStore;
11
+ }>;
12
+
13
+ const CLIENT_NAMESPACE = /^[a-z][a-z0-9-]{0,31}$/u;
14
+
15
+ function runtimeError(code: string, message: string): Error {
16
+ const error = new Error(`${code}: ${message}`);
17
+ error.name = code;
18
+ return error;
19
+ }
20
+
21
+ function boundControlContext(env: NodeJS.ProcessEnv): boolean {
22
+ return [
23
+ 'AGENT_INFRA_CONTROL_TOKEN',
24
+ 'AGENT_INFRA_CONTROL_GENERATION',
25
+ 'AGENT_INFRA_EXECUTOR_MANIFEST',
26
+ 'AGENT_INFRA_CODEX_CONTROLLER_CONTEXT'
27
+ ].some((key) => typeof env[key] === 'string' && env[key]!.length > 0);
28
+ }
29
+
30
+ function clientNamespace(client: string): string {
31
+ if (!CLIENT_NAMESPACE.test(client)) {
32
+ throw runtimeError('AGENT_INFRA_RUNTIME_CLIENT_INVALID', `invalid runtime client namespace '${client}'`);
33
+ }
34
+ return client;
35
+ }
36
+
37
+ export function resolveAgentRuntimeRoot(options: Readonly<{
38
+ repoRoot?: string;
39
+ env?: NodeJS.ProcessEnv;
40
+ }> = {}): string {
41
+ const env = options.env ?? process.env;
42
+ const configured = env.AGENT_INFRA_RUNTIME_DIR;
43
+ if (configured !== undefined && configured.length > 0) {
44
+ if (!path.isAbsolute(configured)) {
45
+ throw runtimeError('AGENT_INFRA_RUNTIME_DIR_INVALID', 'runtime directory must be absolute');
46
+ }
47
+ return path.resolve(configured);
48
+ }
49
+ if (boundControlContext(env)) {
50
+ throw runtimeError(
51
+ 'AGENT_INFRA_RUNTIME_DIR_REQUIRED',
52
+ 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR'
53
+ );
54
+ }
55
+ return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
56
+ }
57
+
58
+ export function resolveAgentRuntimeStoreRoot(options: RuntimeResolutionOptions = {}): string {
59
+ const client = clientNamespace(options.client ?? 'codex');
60
+ if (options.explicitRoot) return path.resolve(options.explicitRoot);
61
+ const env = options.env ?? process.env;
62
+ const runtimeRoot = resolveAgentRuntimeRoot({ repoRoot: options.repoRoot, env });
63
+ if (env.AGENT_INFRA_RUNTIME_DIR) {
64
+ if (!options.store) {
65
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required for task-bound runtime');
66
+ }
67
+ return path.join(runtimeRoot, 'clients', client, options.store);
68
+ }
69
+ if (client === 'codex' && options.store) {
70
+ return path.join(runtimeRoot, `codex-${options.store}`);
71
+ }
72
+ if (!options.store) {
73
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required');
74
+ }
75
+ return path.join(runtimeRoot, 'clients', client, options.store);
76
+ }
@@ -69,6 +69,17 @@ import {
69
69
  prepareTmpfsMounts,
70
70
  startSandboxControlBroker
71
71
  } from '../recovery.ts';
72
+ import {
73
+ acquireSandboxControlReplacement,
74
+ assertSandboxControlCutoverSnapshot,
75
+ beginSandboxControlReplacement,
76
+ captureSandboxControlCutoverSnapshot,
77
+ commitSandboxControlReplacement,
78
+ quiesceSandboxControlRoot,
79
+ recoverSandboxControlReplacement,
80
+ readSandboxControlManifestForTransition
81
+ } from '../control/lifecycle.ts';
82
+ import { inspectSandboxControlContainer } from '../control/container-identity.ts';
72
83
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
73
84
  import { sandboxCoreBindMounts } from '../mounts.ts';
74
85
  import {
@@ -76,7 +87,8 @@ import {
76
87
  finalizeSandboxControlManifest,
77
88
  materializeSandboxControl,
78
89
  materializeSandboxWorkspaceView,
79
- prepareSandboxWorkspaceMountTarget,
90
+ prepareSandboxWorkspaceMountTargets,
91
+ sandboxControlPaths,
80
92
  } from '../workspace-view.ts';
81
93
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
82
94
  import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
@@ -1036,6 +1048,12 @@ export async function create(args: string[]): Promise<void> {
1036
1048
  }
1037
1049
  const effectiveResolvedTools = resolvedTools;
1038
1050
  const container = containerName(effectiveConfig, branch);
1051
+ const controlPaths = sandboxControlPaths({
1052
+ base: effectiveConfig.controlBase,
1053
+ project: effectiveConfig.project,
1054
+ container,
1055
+ identity: target.workspace
1056
+ });
1039
1057
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
1040
1058
  const shareCommon = shareCommonDir(effectiveConfig);
1041
1059
  const shareBranch = shareBranchDir(effectiveConfig, branch);
@@ -1224,6 +1242,22 @@ export async function create(args: string[]): Promise<void> {
1224
1242
  {
1225
1243
  title: `Starting container '${container}'`,
1226
1244
  task: async (message: (text: string) => void) => {
1245
+ const hadControlRootBeforeAcquire = fs.existsSync(controlPaths.root);
1246
+ const replacementLease = acquireSandboxControlReplacement(controlPaths.root);
1247
+ let replacementLeaseHeld = true;
1248
+ let replacementCutover: ReturnType<typeof beginSandboxControlReplacement> | null = null;
1249
+ try {
1250
+ const recoveryResult = await recoverSandboxControlReplacement(controlPaths.root, replacementLease);
1251
+ const hadExistingControlRoot = hadControlRootBeforeAcquire || recoveryResult === 'restored';
1252
+ const previousManifest = fs.existsSync(controlPaths.manifestPath)
1253
+ ? readSandboxControlManifestForTransition(controlPaths.manifestPath)
1254
+ : null;
1255
+ if (hadExistingControlRoot) {
1256
+ await quiesceSandboxControlRoot(controlPaths.root);
1257
+ }
1258
+ const previousCutoverSnapshot = hadExistingControlRoot
1259
+ ? captureSandboxControlCutoverSnapshot(controlPaths.root)
1260
+ : null;
1227
1261
  const existing = runSafeEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
1228
1262
  const matchedContainers = containerNameCandidates(effectiveConfig, branch)
1229
1263
  .filter((name) => existing.includes(name));
@@ -1266,6 +1300,14 @@ export async function create(args: string[]): Promise<void> {
1266
1300
  runSafeEngine(engine, 'docker', ['rm', name]);
1267
1301
  }
1268
1302
  }
1303
+ if (previousManifest) {
1304
+ const oldContainer = await inspectSandboxControlContainer(previousManifest);
1305
+ if (oldContainer.state !== 'absent') {
1306
+ throw new Error(oldContainer.state === 'unknown'
1307
+ ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${oldContainer.reason}`
1308
+ : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
1309
+ }
1310
+ }
1269
1311
 
1270
1312
  const aliasesFile = ensureSandboxAliasesFile(
1271
1313
  effectiveConfig.home,
@@ -1342,7 +1384,19 @@ export async function create(args: string[]): Promise<void> {
1342
1384
  container,
1343
1385
  identity: target.workspace
1344
1386
  });
1345
- prepareSandboxWorkspaceMountTarget(worktree);
1387
+ prepareSandboxWorkspaceMountTargets(worktree);
1388
+ if (previousCutoverSnapshot) {
1389
+ assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
1390
+ if (previousManifest) {
1391
+ const finalOldContainer = await inspectSandboxControlContainer(previousManifest);
1392
+ if (finalOldContainer.state !== 'absent') {
1393
+ throw new Error(finalOldContainer.state === 'unknown'
1394
+ ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${finalOldContainer.reason}`
1395
+ : 'SANDBOX_CONTROL_CONTAINER_REAPPEARED');
1396
+ }
1397
+ }
1398
+ replacementCutover = beginSandboxControlReplacement(controlPaths.root, replacementLease);
1399
+ }
1346
1400
  const control = materializeSandboxControl({
1347
1401
  base: effectiveConfig.controlBase,
1348
1402
  repoRoot: effectiveConfig.repoRoot,
@@ -1351,7 +1405,8 @@ export async function create(args: string[]): Promise<void> {
1351
1405
  container,
1352
1406
  branch,
1353
1407
  identity: target.workspace,
1354
- engine
1408
+ engine,
1409
+ replacementLease
1355
1410
  });
1356
1411
  hostShellConfig = prepareHostShellConfig({
1357
1412
  home: effectiveConfig.home,
@@ -1365,6 +1420,7 @@ export async function create(args: string[]): Promise<void> {
1365
1420
  workspaceViewRoot: workspaceView.root,
1366
1421
  controlDir: control.channelDir,
1367
1422
  controlStatusDir: control.statusDir,
1423
+ ...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
1368
1424
  ...(target.workspace.mode === 'task-bound'
1369
1425
  ? {
1370
1426
  taskSource: assertSandboxTaskSource(
@@ -1456,6 +1512,9 @@ export async function create(args: string[]): Promise<void> {
1456
1512
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1457
1513
  '-e',
1458
1514
  'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1515
+ ...(target.workspace.mode === 'task-bound'
1516
+ ? ['-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime']
1517
+ : []),
1459
1518
  '--label',
1460
1519
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1461
1520
  ...coreVolumes,
@@ -1512,14 +1571,27 @@ export async function create(args: string[]): Promise<void> {
1512
1571
  id: containerId,
1513
1572
  labels: expectedLabels
1514
1573
  });
1574
+ replacementLease.clearQuiescing();
1515
1575
  await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1576
+ if (replacementCutover) {
1577
+ commitSandboxControlReplacement(control.root, replacementLease);
1578
+ replacementCutover = null;
1579
+ }
1516
1580
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1517
1581
  } catch (error) {
1518
1582
  if (createdContainerRef) {
1519
1583
  runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
1520
1584
  runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
1521
1585
  }
1522
- removeDirRecursive(control.root);
1586
+ if (!hadExistingControlRoot) removeDirRecursive(control.root);
1587
+ if (replacementCutover) {
1588
+ try {
1589
+ await recoverSandboxControlReplacement(control.root, replacementLease);
1590
+ replacementCutover = null;
1591
+ } catch {
1592
+ // Preserve the cutover state for the next explicit recreate to recover.
1593
+ }
1594
+ }
1523
1595
  removeDirRecursive(workspaceView.root);
1524
1596
  throw error;
1525
1597
  }
@@ -1615,7 +1687,19 @@ export async function create(args: string[]): Promise<void> {
1615
1687
  );
1616
1688
  }
1617
1689
 
1690
+ replacementLease.release();
1691
+ replacementLeaseHeld = false;
1618
1692
  return 'Container started';
1693
+ } catch (error) {
1694
+ if (replacementLeaseHeld) {
1695
+ try {
1696
+ replacementLease.release();
1697
+ } catch {
1698
+ // Preserve the original cutover failure and its evidence.
1699
+ }
1700
+ }
1701
+ throw error;
1702
+ }
1619
1703
  }
1620
1704
  }
1621
1705
  ]);