@fitlab-ai/agent-infra 0.11.11 → 0.11.13

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 (154) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +7 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex.js +40 -1
  17. package/dist/lib/agent-clients/adapters/index.js +2 -10
  18. package/dist/lib/agent-clients/tokens.js +3 -2
  19. package/dist/lib/internal/agent-client.js +132 -72
  20. package/dist/lib/internal/cli-route-inventory.js +2 -2
  21. package/dist/lib/internal/sandbox-control.js +1 -81
  22. package/dist/lib/internal/task-finalization.js +26 -19
  23. package/dist/lib/internal/task-lifecycle.js +1 -1
  24. package/dist/lib/internal/task-operation-registry.js +1 -1
  25. package/dist/lib/internal/task-validate.js +1 -1
  26. package/dist/lib/platform/github-client.js +33 -3
  27. package/dist/lib/platform/issues.js +18 -6
  28. package/dist/lib/runtime/agent-runtime.js +6 -29
  29. package/dist/lib/sandbox/commands/control-status.js +1 -0
  30. package/dist/lib/sandbox/commands/create.js +7 -2
  31. package/dist/lib/sandbox/commands/ls.js +4 -5
  32. package/dist/lib/sandbox/commands/rm.js +1 -1
  33. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  34. package/dist/lib/sandbox/control/client.js +14 -201
  35. package/dist/lib/sandbox/control/executor.js +12 -125
  36. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  37. package/dist/lib/sandbox/control/protocol.js +15 -54
  38. package/dist/lib/sandbox/control/server.js +216 -281
  39. package/dist/lib/sandbox/control/task-view.js +0 -13
  40. package/dist/lib/sandbox/mounts.js +9 -1
  41. package/dist/lib/sandbox/recovery.js +31 -10
  42. package/dist/lib/sandbox/removal.js +28 -5
  43. package/dist/lib/sandbox/workspace-identity.js +18 -6
  44. package/dist/lib/sandbox/workspace-view.js +56 -19
  45. package/dist/lib/task/commands/ls.js +3 -3
  46. package/dist/lib/task/control-authority.js +56 -264
  47. package/dist/lib/task/control-recovery.js +2 -3
  48. package/dist/lib/task/create.js +2 -2
  49. package/dist/lib/task/delegation-receipts.js +74 -161
  50. package/dist/lib/task/events.js +88 -44
  51. package/dist/lib/task/finalization.js +35 -6
  52. package/dist/lib/task/lifecycle.js +274 -115
  53. package/dist/lib/task/orchestration.js +201 -194
  54. package/dist/lib/task/runtime-paths.js +10 -0
  55. package/dist/lib/task/short-id.js +37 -27
  56. package/dist/lib/task/verification-engine.js +32 -26
  57. package/dist/lib/task/workspace-snapshot.js +1 -1
  58. package/dist/lifecycle-build-manifest.json +3 -3
  59. package/lib/agent-clients/adapter.ts +39 -0
  60. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  62. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  63. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  64. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  65. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  66. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  67. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  68. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  69. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  70. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  71. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  72. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  73. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
  74. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  75. package/lib/agent-clients/adapters/codex.ts +44 -1
  76. package/lib/agent-clients/adapters/index.ts +7 -20
  77. package/lib/agent-clients/tokens.ts +4 -3
  78. package/lib/internal/agent-client.ts +142 -73
  79. package/lib/internal/cli-route-inventory.ts +2 -2
  80. package/lib/internal/sandbox-control.ts +0 -97
  81. package/lib/internal/task-finalization.ts +30 -18
  82. package/lib/internal/task-lifecycle.ts +1 -1
  83. package/lib/internal/task-operation-registry.ts +1 -1
  84. package/lib/internal/task-validate.ts +1 -1
  85. package/lib/platform/github-client.ts +34 -3
  86. package/lib/platform/issues.ts +18 -6
  87. package/lib/runtime/agent-runtime.ts +6 -41
  88. package/lib/sandbox/commands/control-status.ts +1 -0
  89. package/lib/sandbox/commands/create.ts +7 -1
  90. package/lib/sandbox/commands/ls.ts +4 -5
  91. package/lib/sandbox/commands/rm.ts +1 -1
  92. package/lib/sandbox/control/agent-operation.ts +49 -0
  93. package/lib/sandbox/control/client.ts +20 -254
  94. package/lib/sandbox/control/executor.ts +13 -136
  95. package/lib/sandbox/control/lifecycle.ts +67 -4
  96. package/lib/sandbox/control/protocol.ts +22 -66
  97. package/lib/sandbox/control/server.ts +21 -95
  98. package/lib/sandbox/control/task-view.ts +0 -19
  99. package/lib/sandbox/mounts.ts +10 -1
  100. package/lib/sandbox/recovery.ts +38 -17
  101. package/lib/sandbox/removal.ts +33 -7
  102. package/lib/sandbox/workspace-identity.ts +16 -6
  103. package/lib/sandbox/workspace-view.ts +50 -18
  104. package/lib/task/commands/ls.ts +3 -3
  105. package/lib/task/control-authority.ts +66 -378
  106. package/lib/task/control-recovery.ts +3 -4
  107. package/lib/task/create.ts +3 -3
  108. package/lib/task/delegation-receipts.ts +87 -231
  109. package/lib/task/events.ts +100 -42
  110. package/lib/task/finalization.ts +37 -7
  111. package/lib/task/lifecycle.ts +257 -109
  112. package/lib/task/orchestration.ts +260 -237
  113. package/lib/task/runtime-paths.ts +12 -0
  114. package/lib/task/short-id.ts +33 -40
  115. package/lib/task/verification-engine.ts +30 -25
  116. package/lib/task/workspace-snapshot.ts +1 -1
  117. package/package.json +1 -1
  118. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  119. package/templates/.agents/rules/README.en.md +2 -1
  120. package/templates/.agents/rules/README.zh-CN.md +2 -1
  121. package/templates/.agents/rules/next-step-output.en.md +6 -19
  122. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  123. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  124. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  127. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  132. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  133. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
  134. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
  135. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
  136. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
  137. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  138. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  139. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  142. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  143. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  144. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  145. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  149. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  150. package/dist/lib/internal/codex-lifecycle.js +0 -385
  151. package/dist/lib/task/codex-orchestration.js +0 -206
  152. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  153. package/lib/internal/codex-lifecycle.ts +0 -390
  154. package/lib/task/codex-orchestration.ts +0 -276
@@ -4,6 +4,45 @@ import {
4
4
  codexBeforeContainerCreateHook,
5
5
  codexRecoveryChecks
6
6
  } from './codex-sandbox.ts';
7
+ import { parseCodexLifecycleBinding } from './codex-lifecycle/binding.ts';
8
+ import { isCodexDelegationReceiptEvidence, isCodexLifecycleActivationEvidence } from './codex-lifecycle/evidence.ts';
9
+ import { codexControllerOperation } from './codex-lifecycle/controller-operation.ts';
10
+
11
+ function codexCarrierError(code: string): Error {
12
+ const error = new Error(code);
13
+ error.name = code;
14
+ return error;
15
+ }
16
+
17
+ function codexStageToken(stage: string): string {
18
+ const tokens: Readonly<Record<string, string>> = {
19
+ analysis: 'a',
20
+ plan: 'p',
21
+ code: 'c',
22
+ 'review-analysis': 'ra',
23
+ 'review-plan': 'rp',
24
+ 'review-code': 'rc',
25
+ commit: 'm'
26
+ };
27
+ const token = tokens[stage];
28
+ if (!token) throw codexCarrierError('CODEX_LIFECYCLE_SPAWN_IDENTITY_INVALID');
29
+ return token;
30
+ }
31
+
32
+ function codexLaunchCarrier(
33
+ adapterContext: string,
34
+ identity: Readonly<{ stage: string; round: number; role: string }>
35
+ ): Readonly<Record<string, string>> {
36
+ if (!Number.isSafeInteger(identity.round) || identity.round < 1
37
+ || !['executor', 'reviewer'].includes(identity.role)) {
38
+ throw codexCarrierError('CODEX_LIFECYCLE_SPAWN_IDENTITY_INVALID');
39
+ }
40
+ const label = `${codexStageToken(identity.stage)}_${identity.role === 'reviewer' ? 'r' : 'e'}_r${identity.round}`;
41
+ const taskName = `${label}${adapterContext}`;
42
+ const parsed = parseCodexLifecycleBinding(taskName);
43
+ if (!parsed) throw codexCarrierError('CODEX_LIFECYCLE_LAUNCH_CONTEXT_INVALID');
44
+ return Object.freeze({ task_name: parsed.taskName });
45
+ }
7
46
 
8
47
  const codexAdapter = defineAgentClientAdapter({
9
48
  id: 'codex',
@@ -29,8 +68,11 @@ const codexAdapter = defineAgentClientAdapter({
29
68
  actualReasoningEffort: 'app-server'
30
69
  },
31
70
  orchestrationAdapter: {
71
+ validateActivationEvidence: isCodexLifecycleActivationEvidence,
72
+ validateReceiptEvidence: isCodexDelegationReceiptEvidence,
73
+ createLaunchCarrier: codexLaunchCarrier,
32
74
  prepareDelegation: async (...args) => {
33
- const { prepareCodexOrchestrationDelegation } = await import('../../task/codex-orchestration.ts');
75
+ const { prepareCodexOrchestrationDelegation } = await import('./codex-orchestration.ts');
34
76
  return prepareCodexOrchestrationDelegation(...args);
35
77
  },
36
78
  recoverStarted: async (...args) => {
@@ -38,6 +80,7 @@ const codexAdapter = defineAgentClientAdapter({
38
80
  return recoverStartedLifecycleFromAdapter(...args);
39
81
  }
40
82
  },
83
+ sandboxControlOperation: codexControllerOperation,
41
84
  project: {
42
85
  ownedPathPrefixes: ['.codex/'],
43
86
  managed: ['.codex/hooks.json', '.codex/agents/'],
@@ -1,4 +1,3 @@
1
- import { AGENT_CLIENT_IDS } from '../types.ts';
2
1
  import type { AgentClientAdapter } from '../adapter.ts';
3
2
  import { claudeCodeAdapter } from './claude-code.ts';
4
3
  import { codexAdapter } from './codex.ts';
@@ -6,24 +5,12 @@ import { antigravityCliAdapter } from './antigravity-cli.ts';
6
5
  import { opencodeAdapter } from './opencode.ts';
7
6
  import { traecliAdapter } from './traecli.ts';
8
7
 
9
- const adaptersById = new Map(
10
- [
11
- claudeCodeAdapter,
12
- codexAdapter,
13
- antigravityCliAdapter,
14
- opencodeAdapter,
15
- traecliAdapter
16
- ].map((adapter) => [adapter.id, adapter])
17
- );
18
-
19
- const BUILTIN_AGENT_CLIENT_ADAPTERS: readonly AgentClientAdapter[] = Object.freeze(
20
- AGENT_CLIENT_IDS.map((id) => {
21
- const adapter = adaptersById.get(id);
22
- if (!adapter) {
23
- throw new Error(`Missing built-in Agent Client adapter '${id}'`);
24
- }
25
- return adapter;
26
- })
27
- );
8
+ const BUILTIN_AGENT_CLIENT_ADAPTERS: readonly AgentClientAdapter[] = Object.freeze([
9
+ claudeCodeAdapter,
10
+ codexAdapter,
11
+ antigravityCliAdapter,
12
+ opencodeAdapter,
13
+ traecliAdapter
14
+ ]);
28
15
 
29
16
  export { BUILTIN_AGENT_CLIENT_ADAPTERS };
@@ -1,8 +1,9 @@
1
1
  // Active AI collaborator short tokens — the write-side whitelist for the
2
2
  // activity log. Migrated from lib/task/commands/log.ts so that the write-side
3
3
  // internal commands and the rendering side share one definition.
4
- export const KNOWN_AI_AGENTS: ReadonlySet<string> =
5
- new Set(['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'traecli']);
4
+ export const AGENT_TOKEN_VALUES = ['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'traecli'] as const;
5
+ export type AgentToken = (typeof AGENT_TOKEN_VALUES)[number];
6
+ export const KNOWN_AI_AGENTS: ReadonlySet<string> = new Set(AGENT_TOKEN_VALUES);
6
7
 
7
8
  // Long name -> short name mapping: `claude-code` reuses the existing
8
9
  // orchestration precedent, and `antigravity-cli` is the active client.
@@ -12,7 +13,7 @@ export const AGENT_LONG_NAMES: Readonly<Record<string, string>> = {
12
13
  };
13
14
 
14
15
  export const AGENT_USAGE_HINT =
15
- "agent must be a short token (claude/codex/antigravity/opencode/cursor/traecli), " +
16
+ `agent must be a short token (${AGENT_TOKEN_VALUES.join('/')}), ` +
16
17
  "a long name (claude-code -> claude, antigravity-cli -> antigravity), or 'human'";
17
18
 
18
19
  // Strict write-side validation: the value must be exactly a short token, a
@@ -7,11 +7,11 @@ import {
7
7
  } from '../agent-clients/config.ts';
8
8
  import { normalizeCustomToolInvocations } from '../agent-clients/custom-tool-invocations.ts';
9
9
  import { renderNextStepCommands } from '../agent-clients/next-steps.ts';
10
- import { getAgentClientModelSelection } from '../agent-clients/registry.ts';
10
+ import { getAgentClientAdapter, getAgentClientModelSelection } from '../agent-clients/registry.ts';
11
11
  import { isAgentClientId } from '../agent-clients/types.ts';
12
12
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
13
13
 
14
- const USAGE = 'Usage: agent-infra-internal agent-client <next-steps|model-selection> [options]\n';
14
+ const USAGE = 'Usage: agent-infra-internal agent-client <next-steps|model-selection|launch-carrier> [options]\n';
15
15
 
16
16
  type ParsedArgs = Readonly<{
17
17
  skillName: string;
@@ -20,6 +20,14 @@ type ParsedArgs = Readonly<{
20
20
  format: 'text' | 'json';
21
21
  }>;
22
22
 
23
+ type LaunchCarrierArgs = Readonly<{
24
+ client: string;
25
+ adapterContext: string;
26
+ stage: string;
27
+ round: number;
28
+ role: string;
29
+ }>;
30
+
23
31
  function failure(code: string, message: string): void {
24
32
  process.stdout.write(`${JSON.stringify({
25
33
  status: 'failed',
@@ -84,68 +92,112 @@ function parseArgs(args: string[]): ParsedArgs | null {
84
92
  };
85
93
  }
86
94
 
87
- function agentClient(args: string[] = []): void {
88
- if (!ensureInternalHandlerRoute('agent-client', args)) return;
89
- if (internalHandlerRoute('agent-client', 'model-selection', args[0] ?? '')) {
90
- const values: Record<string, string> = {};
91
- const seen = new Set<string>();
92
- for (let index = 1; index < args.length; index += 1) {
93
- const flag = args[index]!;
94
- if (!['--client', '--format'].includes(flag) || seen.has(flag)) {
95
- failure('AGENT_CLIENT_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
96
- return;
97
- }
98
- const value = args[++index];
99
- if (!value || value.startsWith('--')) {
100
- failure('AGENT_CLIENT_PAYLOAD_INVALID', `option '${flag}' requires a value`);
101
- return;
102
- }
103
- seen.add(flag);
104
- values[flag] = value;
95
+ function parseLaunchCarrierArgs(args: string[]): LaunchCarrierArgs | null {
96
+ const allowed = new Set(['--client', '--adapter-context', '--stage', '--round', '--role', '--format']);
97
+ const values: Record<string, string> = {};
98
+ const seen = new Set<string>();
99
+ for (let index = 1; index < args.length; index += 1) {
100
+ const flag = args[index]!;
101
+ if (!allowed.has(flag) || seen.has(flag)) {
102
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
103
+ return null;
105
104
  }
106
- const client = values['--client'];
107
- const format = values['--format'] ?? 'text';
108
- if (!isAgentClientId(client) || !['text', 'json'].includes(format)) {
109
- failure('AGENT_CLIENT_PAYLOAD_INVALID', 'model-selection requires a known --client and text|json format');
110
- return;
105
+ const value = args[++index];
106
+ if (!value || value.startsWith('--')) {
107
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `option '${flag}' requires a value`);
108
+ return null;
111
109
  }
112
- const context = getAgentClientModelSelection(client);
113
- if (format === 'json') {
114
- process.stdout.write(`${JSON.stringify({
115
- status: 'resolved', changed: false, client, context, error: null
116
- })}\n`);
110
+ seen.add(flag);
111
+ values[flag] = value;
112
+ }
113
+ const round = Number(values['--round']);
114
+ if (!isAgentClientId(values['--client']) || !values['--adapter-context'] || !values['--stage'] || !values['--role']
115
+ || !Number.isSafeInteger(round) || round < 1 || (values['--format'] ?? 'json') !== 'json') {
116
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', 'launch-carrier requires client, adapter context, stage, positive round, role, and json format');
117
+ return null;
118
+ }
119
+ return {
120
+ client: values['--client']!,
121
+ adapterContext: values['--adapter-context']!,
122
+ stage: values['--stage']!,
123
+ round,
124
+ role: values['--role']!
125
+ };
126
+ }
127
+
128
+ function launchCarrier(args: string[]): void {
129
+ const parsed = parseLaunchCarrierArgs(args);
130
+ if (!parsed) return;
131
+ const createCarrier = getAgentClientAdapter(parsed.client).orchestrationAdapter?.createLaunchCarrier;
132
+ if (!createCarrier) {
133
+ process.stdout.write(`${JSON.stringify({
134
+ status: 'failed', changed: false, client: parsed.client, carrier: null,
135
+ error: { code: 'AGENT_CLIENT_LAUNCH_CARRIER_UNSUPPORTED', message: `Agent Client '${parsed.client}' does not support lifecycle launch carriers` }
136
+ })}\n`);
137
+ process.exitCode = 1;
138
+ return;
139
+ }
140
+ try {
141
+ const carrier = createCarrier(parsed.adapterContext, { stage: parsed.stage, round: parsed.round, role: parsed.role });
142
+ process.stdout.write(`${JSON.stringify({ status: 'ready', changed: false, client: parsed.client, carrier, error: null })}\n`);
143
+ } catch (error) {
144
+ process.stdout.write(`${JSON.stringify({
145
+ status: 'failed', changed: false, client: parsed.client, carrier: null,
146
+ error: { code: error instanceof Error && error.name ? error.name : 'AGENT_CLIENT_LAUNCH_CARRIER_INVALID', message: error instanceof Error ? error.message : String(error) }
147
+ })}\n`);
148
+ process.exitCode = 1;
149
+ }
150
+ }
151
+
152
+ function modelSelection(args: string[]): void {
153
+ const values: Record<string, string> = {};
154
+ const seen = new Set<string>();
155
+ for (let index = 1; index < args.length; index += 1) {
156
+ const flag = args[index]!;
157
+ if (!['--client', '--format'].includes(flag) || seen.has(flag)) {
158
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
117
159
  return;
118
160
  }
119
- if (context.kind === 'interactive-only') {
120
- process.stdout.write(`Model selection: interactive-only\nCommand: ${context.command}\n${context.guidance}\n`);
121
- } else {
122
- process.stdout.write(`Model selection: ${context.completeness} catalog\nSource: ${context.source}\n`);
123
- for (const model of context.models) {
124
- const efforts = model.reasoningEfforts?.length
125
- ? ` (${model.reasoningEfforts.join(', ')})`
126
- : '';
127
- process.stdout.write(`- ${model.id}${efforts}\n`);
128
- }
129
- if (context.guidance) process.stdout.write(`${context.guidance}\n`);
161
+ const value = args[++index];
162
+ if (!value || value.startsWith('--')) {
163
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', `option '${flag}' requires a value`);
164
+ return;
130
165
  }
166
+ seen.add(flag);
167
+ values[flag] = value;
168
+ }
169
+ const client = values['--client'];
170
+ const format = values['--format'] ?? 'text';
171
+ if (!isAgentClientId(client) || !['text', 'json'].includes(format)) {
172
+ failure('AGENT_CLIENT_PAYLOAD_INVALID', 'model-selection requires a known --client and text|json format');
131
173
  return;
132
174
  }
133
- const parsed = parseArgs(args);
134
- if (!parsed || process.exitCode) return;
175
+ const context = getAgentClientModelSelection(client);
176
+ if (format === 'json') {
177
+ process.stdout.write(`${JSON.stringify({ status: 'resolved', changed: false, client, context, error: null })}\n`);
178
+ return;
179
+ }
180
+ renderModelSelection(context);
181
+ }
135
182
 
136
- let config: Record<string, unknown>;
137
- try {
138
- const configPath = path.join(process.cwd(), '.agents', '.airc.json');
139
- const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as unknown;
140
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
141
- throw new Error('configuration root must be an object');
142
- }
143
- config = raw as Record<string, unknown>;
144
- } catch (error) {
145
- failure('AGENT_CLIENT_CONFIG_INVALID', String(error));
183
+ function renderModelSelection(context: ReturnType<typeof getAgentClientModelSelection>): void {
184
+ if (context.kind === 'interactive-only') {
185
+ process.stdout.write(`Model selection: interactive-only\nCommand: ${context.command}\n${context.guidance}\n`);
146
186
  return;
147
187
  }
188
+ process.stdout.write(`Model selection: ${context.completeness} catalog\nSource: ${context.source}\n`);
189
+ for (const model of context.models) {
190
+ const efforts = model.reasoningEfforts?.length ? ` (${model.reasoningEfforts.join(', ')})` : '';
191
+ process.stdout.write(`- ${model.id}${efforts}\n`);
192
+ }
193
+ if (context.guidance) process.stdout.write(`${context.guidance}\n`);
194
+ }
148
195
 
196
+ function renderNextSteps(args: string[]): void {
197
+ const parsed = parseArgs(args);
198
+ if (!parsed || process.exitCode) return;
199
+ const config = readAgentClientConfig();
200
+ if (!config) return;
149
201
  try {
150
202
  const clients = normalizeAgentClients(config);
151
203
  const custom = normalizeCustomToolInvocations(config);
@@ -157,32 +209,49 @@ function agentClient(args: string[] = []): void {
157
209
  ...(parsed.taskRef ? { taskRef: parsed.taskRef } : {}),
158
210
  ...(parsed.version ? { version: parsed.version } : {})
159
211
  });
160
- if (parsed.format === 'json') {
161
- process.stdout.write(`${JSON.stringify({
162
- status: 'rendered',
163
- changed: false,
164
- commands,
165
- diagnostics: custom.diagnostics,
166
- error: null
167
- })}\n`);
168
- return;
169
- }
170
- for (const command of commands) {
171
- process.stdout.write(` - ${command.displayName}: ${command.command}\n`);
172
- }
173
- for (const diagnostic of custom.diagnostics) {
174
- process.stderr.write(`${diagnostic.code} at ${diagnostic.path}\n`);
175
- }
212
+ writeNextStepOutput(parsed.format, commands, custom.diagnostics);
176
213
  } catch (error) {
177
214
  if (error instanceof AgentClientConfigError) {
178
215
  failure(error.code, error.message);
179
216
  return;
180
217
  }
181
- failure(
182
- 'AGENT_CLIENT_RENDER_INVALID',
183
- error instanceof Error ? error.message : String(error)
184
- );
218
+ failure('AGENT_CLIENT_RENDER_INVALID', error instanceof Error ? error.message : String(error));
219
+ }
220
+ }
221
+
222
+ function readAgentClientConfig(): Record<string, unknown> | null {
223
+ try {
224
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.agents', '.airc.json'), 'utf8')) as unknown;
225
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) throw new Error('configuration root must be an object');
226
+ return raw as Record<string, unknown>;
227
+ } catch (error) {
228
+ failure('AGENT_CLIENT_CONFIG_INVALID', String(error));
229
+ return null;
230
+ }
231
+ }
232
+
233
+ function writeNextStepOutput(
234
+ format: ParsedArgs['format'],
235
+ commands: ReturnType<typeof renderNextStepCommands>,
236
+ diagnostics: ReturnType<typeof normalizeCustomToolInvocations>['diagnostics']
237
+ ): void {
238
+ if (format === 'json') {
239
+ process.stdout.write(`${JSON.stringify({ status: 'rendered', changed: false, commands, diagnostics, error: null })}\n`);
240
+ return;
241
+ }
242
+ for (const command of commands) process.stdout.write(` - ${command.displayName}: ${command.command}\n`);
243
+ for (const diagnostic of diagnostics) process.stderr.write(`${diagnostic.code} at ${diagnostic.path}\n`);
244
+ }
245
+
246
+ function agentClient(args: string[] = []): void {
247
+ if (!ensureInternalHandlerRoute('agent-client', args)) return;
248
+ if (internalHandlerRoute('agent-client', 'launch-carrier', args[0] ?? '')) {
249
+ return launchCarrier(args);
250
+ }
251
+ if (internalHandlerRoute('agent-client', 'model-selection', args[0] ?? '')) {
252
+ return modelSelection(args);
185
253
  }
254
+ renderNextSteps(args);
186
255
  }
187
256
 
188
257
  export { agentClient };
@@ -36,8 +36,8 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
36
36
  'task-create': ['input'],
37
37
  'task-qualification': ['proposal', 'confirm', 'supersede', 'revoke'],
38
38
  'sandbox-control': ['serve', 'execute', 'recover', 'client'],
39
- 'agent-client': ['next-steps', 'model-selection'],
40
- 'codex-lifecycle': ['preflight', 'capability-arm', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
39
+ 'agent-client': ['next-steps', 'model-selection', 'launch-carrier'],
40
+ 'codex-lifecycle': ['preflight', 'hook-event', 'resolve-start', 'resolve-stop', 'consume'],
41
41
  'codex-sandbox-controller': ['verify-context', 'run'],
42
42
  'git-workflow': ['inspect', 'preview-tree', 'snapshot', 'compare-trees', 'commit', 'push-rebased'],
43
43
  'task-delivery': ['deliver'],
@@ -1,33 +1,12 @@
1
1
  import {
2
2
  recoverSandboxControl,
3
3
  requestSandboxControl,
4
- requestSandboxTaskFinalization,
5
4
  SandboxControlClientError
6
5
  } from '../sandbox/control/client.ts';
7
- import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
8
6
  import { serveSandboxControl } from '../sandbox/control/server.ts';
9
7
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
8
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
11
9
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
12
- import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
13
-
14
- type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
15
-
16
- function writeFinalizationEnvelope(
17
- status: FinalizationStatus,
18
- accepted: boolean,
19
- error: { code: string; message: string; retryable: boolean },
20
- changed: boolean | null = false,
21
- result: unknown = null,
22
- requestId: string | null = null
23
- ): void {
24
- process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
25
- }
26
-
27
- function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
28
- if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return 'unknown';
29
- return error.retryable ? 'blocked' : 'failed';
30
- }
31
10
 
32
11
  function writeClientError(error: SandboxControlClientError): void {
33
12
  process.stderr.write(`${error.detail.message}\n`);
@@ -45,78 +24,6 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
45
24
  return response.exitCode ?? 1;
46
25
  }
47
26
 
48
- function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
49
- try {
50
- const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
51
- if (value.version === 2
52
- && value.status
53
- && ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
54
- && (typeof value.changed === 'boolean' || value.changed === null)) {
55
- return serializeTaskFinalizationEnvelope({
56
- status: value.status,
57
- changed: value.changed,
58
- accepted: true,
59
- requestId: response.id,
60
- result: value.result ?? null,
61
- error: value.error ?? null
62
- });
63
- }
64
- } catch {
65
- // Preserve non-envelope output for diagnostics from the executor.
66
- }
67
- return response.stdout;
68
- }
69
-
70
- function sandboxFinalizationClient(args: string[]): void {
71
- if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
72
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
73
- writeFinalizationEnvelope('failed', false, error);
74
- process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
75
- process.exitCode = 1;
76
- return;
77
- }
78
- const agent = normalizeAgentToken(args[3]);
79
- if (!agent) {
80
- const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
81
- writeFinalizationEnvelope('failed', false, error);
82
- process.exitCode = 1;
83
- return;
84
- }
85
- let response;
86
- try {
87
- response = requestSandboxTaskFinalization({ agent });
88
- } catch (error) {
89
- if (!(error instanceof SandboxControlClientError)) throw error;
90
- const status = finalizationErrorStatus(error.detail);
91
- writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
92
- writeClientError(error);
93
- process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
94
- return;
95
- }
96
- if (response.phase === 'rejected') {
97
- const error = response.error ?? {
98
- code: 'SANDBOX_CONTROL_REJECTED',
99
- message: 'sandbox control rejected the finalization request',
100
- retryable: false
101
- };
102
- const status = finalizationErrorStatus(error);
103
- writeFinalizationEnvelope(
104
- status,
105
- error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
106
- error,
107
- status === 'unknown' ? null : false,
108
- null,
109
- response.id
110
- );
111
- process.stderr.write(`${error.message}\n`);
112
- process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
113
- return;
114
- }
115
- process.stdout.write(finalizationResponseOutput(response));
116
- process.stderr.write(response.stderr);
117
- process.exitCode = response.exitCode ?? 1;
118
- }
119
-
120
27
  async function sandboxControl(args: string[]): Promise<void> {
121
28
  if (!ensureInternalHandlerRoute('sandbox-control', args)) return;
122
29
  const [operation, ...rest] = args;
@@ -165,10 +72,6 @@ async function sandboxControl(args: string[]): Promise<void> {
165
72
  }
166
73
  if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
167
74
  const [family = '', ...commandArgs] = rest;
168
- if (family === 'task-finalization') {
169
- sandboxFinalizationClient(commandArgs);
170
- return;
171
- }
172
75
  let response;
173
76
  try {
174
77
  response = requestSandboxControl({ family, args: commandArgs });
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import {
2
3
  createDirectHostExecutionContext,
3
4
  dispatchTaskControlOperation,
@@ -6,8 +7,10 @@ import {
6
7
  import { applyTaskFinalization } from '../task/finalization.ts';
7
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
8
9
  import { resolveSandboxControlTransport } from './task-operation-registry.ts';
9
- import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.ts';
10
+ import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.ts';
11
+ import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
10
12
  import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
13
+ import { verifyTaskEvent } from '../task/verification.ts';
11
14
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
15
 
13
16
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
@@ -79,25 +82,34 @@ async function taskFinalization(args: string[] = []): Promise<void> {
79
82
  };
80
83
  const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
81
84
  if (transport.kind === 'sandbox-local') {
82
- try {
83
- const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
84
- process.stdout.write(response.stdout);
85
- process.stderr.write(response.stderr);
86
- process.exitCode = response.exitCode ?? 1;
87
- return;
88
- } catch (error) {
89
- const clientError = error instanceof SandboxControlClientError ? error : null;
90
- const detail = clientError
91
- ? clientError.detail
92
- : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
93
- const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
94
- ? 'unknown'
95
- : detail.retryable ? 'blocked' : 'failed';
96
- const changed = status === 'unknown' ? null : false;
97
- process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
98
- process.exitCode = exitCode(status);
85
+ const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
86
+ const taskId = process.env.AGENT_INFRA_TASK_ID;
87
+ const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
88
+ const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
89
+ if (!statusDir || !taskId || !generation || !controlRootId || taskId !== resolved.taskId) {
90
+ const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
91
+ process.stdout.write(envelope('failed', false, true, null, detail));
92
+ process.exitCode = 1;
99
93
  return;
100
94
  }
95
+ const result = await applyTaskFinalization(boundOperation.request, {
96
+ repoRoot,
97
+ beforeLifecycle: async (currentTaskId) => {
98
+ if (currentTaskId !== taskId) throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
99
+ await quiesceSandboxControlForTaskCompletion(
100
+ path.join(resolveTaskRuntimeRoot(taskId, { repoRoot }), 'sandbox-control'),
101
+ { taskId, generation, controlRootId, statusDir }
102
+ );
103
+ },
104
+ preflight: (request, options) => verifyTaskEvent(
105
+ { ...request, event: 'complete-task.hard-preflight' }, options
106
+ )
107
+ });
108
+ const status = result.status === 'prepared' ? 'failed' : result.status;
109
+ process.stdout.write(envelope(status, result.changed, true, result, result.error
110
+ ?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
111
+ process.exitCode = exitCode(status);
112
+ return;
101
113
  }
102
114
  const result = await dispatchTaskControlOperation(
103
115
  createDirectHostExecutionContext({ repoRoot }),
@@ -10,7 +10,7 @@ import type { TaskLifecycleResult } from '../task/lifecycle.ts';
10
10
  import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
11
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
12
 
13
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
13
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
14
14
 
15
15
  function usageFailure(message: string): void {
16
16
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
@@ -54,8 +54,8 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
54
54
  descriptor('internal', 'sandbox-control', 'client', 'conditional', 'progress', 'delegated'),
55
55
  descriptor('internal', 'agent-client', 'next-steps', 'non-task', 'diagnostic', 'none'),
56
56
  descriptor('internal', 'agent-client', 'model-selection', 'non-task', 'diagnostic', 'none'),
57
+ descriptor('internal', 'agent-client', 'launch-carrier', 'non-task', 'diagnostic', 'none'),
57
58
  descriptor('internal', 'codex-lifecycle', 'preflight', 'non-task', 'diagnostic', 'none'),
58
- descriptor('internal', 'codex-lifecycle', 'capability-arm', 'conditional', 'progress'),
59
59
  descriptor('internal', 'codex-lifecycle', 'hook-event', 'conditional', 'progress'),
60
60
  descriptor('internal', 'codex-lifecycle', 'resolve-start', 'conditional', 'progress'),
61
61
  descriptor('internal', 'codex-lifecycle', 'resolve-stop', 'conditional', 'progress'),
@@ -155,7 +155,7 @@ function inplaceValidation(config: ReturnType<typeof loadConfig>, target: Return
155
155
  const row = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, target.branch));
156
156
  if (!row) throw new Error('SANDBOX_VALIDATION_CONTAINER_NOT_FOUND');
157
157
  const control = sandboxControlPaths({
158
- base: config.controlBase, project: config.project,
158
+ base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
159
159
  container: row.name, identity: target.workspace
160
160
  });
161
161
  const manifest = readSandboxControlManifest(control.manifestPath);