@fitlab-ai/agent-infra 0.9.9 → 0.9.10

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 (86) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +17 -10
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +11 -31
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +15 -33
  5. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  6. package/dist/lib/agent-clients/adapters/index.js +3 -1
  7. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  8. package/dist/lib/agent-clients/tokens.js +2 -2
  9. package/dist/lib/agent-clients/types.js +2 -1
  10. package/dist/lib/defaults.json +5 -0
  11. package/dist/lib/internal/codex-lifecycle.js +2 -2
  12. package/dist/lib/internal/codex-sandbox-controller.js +4 -3
  13. package/dist/lib/internal/sandbox-control.js +2 -2
  14. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  15. package/dist/lib/run/tui.js +5 -2
  16. package/dist/lib/sandbox/control/controller-registration.js +4 -2
  17. package/dist/lib/sandbox/control/executor.js +4 -1
  18. package/dist/lib/task/codex-orchestration.js +31 -34
  19. package/dist/lib/task/delegation-receipts.js +8 -7
  20. package/dist/lib/task/finalization.js +72 -46
  21. package/dist/lib/task/index.js +0 -9
  22. package/dist/lib/task/orchestration.js +1 -1
  23. package/dist/lib/task/verification-engine.js +0 -3
  24. package/dist/lifecycle-build-manifest.json +3 -3
  25. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +25 -10
  26. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +16 -33
  27. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +16 -35
  28. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  29. package/lib/agent-clients/adapters/index.ts +3 -1
  30. package/lib/agent-clients/adapters/traecli.ts +67 -0
  31. package/lib/agent-clients/tokens.ts +2 -2
  32. package/lib/agent-clients/types.ts +2 -1
  33. package/lib/defaults.json +5 -0
  34. package/lib/internal/codex-lifecycle.ts +2 -2
  35. package/lib/internal/codex-sandbox-controller.ts +4 -3
  36. package/lib/internal/sandbox-control.ts +2 -2
  37. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  38. package/lib/run/tui.ts +5 -3
  39. package/lib/sandbox/control/controller-registration.ts +8 -3
  40. package/lib/sandbox/control/executor.ts +8 -2
  41. package/lib/task/codex-orchestration.ts +38 -44
  42. package/lib/task/delegation-receipts.ts +8 -6
  43. package/lib/task/finalization.ts +84 -42
  44. package/lib/task/index.ts +0 -8
  45. package/lib/task/orchestration.ts +2 -1
  46. package/lib/task/verification-engine.ts +0 -3
  47. package/package.json +1 -1
  48. package/templates/.agents/QUICKSTART.en.md +2 -3
  49. package/templates/.agents/QUICKSTART.zh-CN.md +2 -3
  50. package/templates/.agents/rules/README.en.md +1 -0
  51. package/templates/.agents/rules/README.zh-CN.md +1 -0
  52. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  53. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  54. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  55. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  56. package/templates/.agents/rules/pr-sync.en.md +1 -1
  57. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  58. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  59. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  60. package/templates/.agents/rules/review-method.en.md +1 -0
  61. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  62. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  63. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  64. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  65. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  66. package/templates/.agents/skills/create-pr/SKILL.en.md +12 -6
  67. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +12 -6
  68. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  69. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  70. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  71. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  72. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  73. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  74. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  75. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  76. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  77. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  78. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  79. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  80. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  81. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  82. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  83. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  84. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  85. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  86. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
@@ -3,28 +3,28 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { randomUUID } from 'node:crypto';
6
- import { loadConfig } from '../../sandbox/config.ts';
7
- import { detectEngine } from '../../sandbox/engine.ts';
8
- import { runEngine } from '../../sandbox/shell.ts';
9
- import { sandboxBranchLabel, sandboxLabel } from '../../sandbox/constants.ts';
10
- import { fetchSandboxRows, selectSandboxContainer } from '../../sandbox/commands/list-running.ts';
11
- import { containerNameCandidates } from '../../sandbox/constants.ts';
12
- import { resolveSandboxTarget } from '../../sandbox/workspace-identity.ts';
13
- import { sandboxControlPaths } from '../../sandbox/workspace-view.ts';
6
+ import { loadConfig } from '../sandbox/config.ts';
7
+ import { detectEngine } from '../sandbox/engine.ts';
8
+ import { runEngine } from '../sandbox/shell.ts';
9
+ import { sandboxBranchLabel, sandboxLabel } from '../sandbox/constants.ts';
10
+ import { fetchSandboxRows, selectSandboxContainer } from '../sandbox/commands/list-running.ts';
11
+ import { containerNameCandidates } from '../sandbox/constants.ts';
12
+ import { resolveSandboxTarget } from '../sandbox/workspace-identity.ts';
13
+ import { sandboxControlPaths } from '../sandbox/workspace-view.ts';
14
14
  import {
15
15
  appendSandboxControlAudit,
16
16
  readActiveLease,
17
17
  readSandboxControlStatus
18
- } from '../../sandbox/control/state.ts';
19
- import type { SandboxControlLease, SandboxControlManifest } from '../../sandbox/control/protocol.ts';
18
+ } from '../sandbox/control/state.ts';
19
+ import type { SandboxControlLease, SandboxControlManifest } from '../sandbox/control/protocol.ts';
20
20
  import {
21
21
  SANDBOX_CONTROL_FUTURE_SKEW_MS,
22
22
  SANDBOX_CONTROL_STATUS_STALE_MS
23
- } from '../../sandbox/control/protocol.ts';
24
- import { getProcessStartTime } from '../../server/process-state.ts';
25
- import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
23
+ } from '../sandbox/control/protocol.ts';
24
+ import { getProcessStartTime } from '../server/process-state.ts';
25
+ import { assertGitWorktreeBinding } from '../git/worktree-identity.ts';
26
26
 
27
- const USAGE = `Usage: ai task validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
27
+ const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] -- <command> [args...]`;
28
28
  const MAX_TIMEOUT_MS = 60 * 60 * 1000;
29
29
  const RECOVERY_GRACE_MS = 30_000;
30
30
 
@@ -250,33 +250,68 @@ function inplaceValidation(config: ReturnType<typeof loadConfig>, target: Return
250
250
  }
251
251
  }
252
252
 
253
- export async function validate(args: string[]): Promise<number> {
254
- const options = parseValidateArgs(args);
253
+ function errorCode(message: string): string {
254
+ const match = message.match(/^([A-Z][A-Z0-9_]*)/);
255
+ return match && match[1]!.length >= 3 ? match[1]! : 'TASK_VALIDATE_FAILED';
256
+ }
257
+
258
+ function fail(format: 'text' | 'json', code: string, message: string): void {
259
+ if (format === 'json') {
260
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code, message } })}\n`);
261
+ } else {
262
+ process.stderr.write(`${message}\n`);
263
+ }
264
+ process.exitCode = 1;
265
+ }
266
+
267
+ function sniffFormat(args: string[]): 'text' | 'json' {
268
+ const index = args.indexOf('--format');
269
+ return index >= 0 && args[index + 1] === 'json' ? 'json' : 'text';
270
+ }
271
+
272
+ function taskValidate(args: string[]): void {
273
+ let options: ValidateOptions;
274
+ try {
275
+ options = parseValidateArgs(args);
276
+ } catch (error) {
277
+ fail(sniffFormat(args), 'TASK_VALIDATE_ARGS_INVALID', error instanceof Error ? error.message : String(error));
278
+ return;
279
+ }
255
280
  if (options.help) {
256
281
  process.stdout.write(`${USAGE}\n`);
257
- return 0;
282
+ return;
283
+ }
284
+ try {
285
+ const config = loadConfig();
286
+ const target = resolveSandboxTarget(options.target, config.repoRoot);
287
+ const commit = git(config.repoRoot, ['rev-parse', target.branch]);
288
+ const startedAt = new Date().toISOString();
289
+ const result = options.scope === 'snapshot'
290
+ ? snapshotValidation(config.repoRoot, commit, options)
291
+ : inplaceValidation(config, target, options);
292
+ const evidence = {
293
+ version: 1,
294
+ taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
295
+ branch: target.branch,
296
+ scope: options.scope,
297
+ commit,
298
+ command: path.basename(options.command[0]!),
299
+ startedAt,
300
+ completedAt: new Date().toISOString(),
301
+ exitCode: result.exitCode,
302
+ signal: result.signal,
303
+ cleanup: result.cleanup()
304
+ };
305
+ if (options.format === 'json') {
306
+ process.stdout.write(`${JSON.stringify({ status: 'applied', changed: false, evidence, error: null })}\n`);
307
+ } else {
308
+ process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
309
+ }
310
+ process.exitCode = result.exitCode;
311
+ } catch (error) {
312
+ const message = error instanceof Error ? error.message : String(error);
313
+ fail(options.format, errorCode(message), message);
258
314
  }
259
- const config = loadConfig();
260
- const target = resolveSandboxTarget(options.target, config.repoRoot);
261
- const commit = git(config.repoRoot, ['rev-parse', target.branch]);
262
- const startedAt = new Date().toISOString();
263
- const result = options.scope === 'snapshot'
264
- ? snapshotValidation(config.repoRoot, commit, options)
265
- : inplaceValidation(config, target, options);
266
- const evidence = {
267
- version: 1,
268
- taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
269
- branch: target.branch,
270
- scope: options.scope,
271
- commit,
272
- command: path.basename(options.command[0]!),
273
- startedAt,
274
- completedAt: new Date().toISOString(),
275
- exitCode: result.exitCode,
276
- signal: result.signal,
277
- cleanup: result.cleanup()
278
- };
279
- if (options.format === 'json') process.stdout.write(`${JSON.stringify(evidence)}\n`);
280
- else process.stdout.write(`Validation ${result.exitCode === 0 ? 'passed' : 'failed'} (${options.scope}, ${evidence.command}, cleanup=${evidence.cleanup}).\n`);
281
- return result.exitCode;
282
315
  }
316
+
317
+ export { taskValidate };
package/lib/run/tui.ts CHANGED
@@ -2,14 +2,15 @@ import { renderAgentClientInvocation } from '../agent-clients/invocation.ts';
2
2
  import { getAgentClientAdapter } from '../agent-clients/registry.ts';
3
3
  import type { AgentClientId } from '../agent-clients/types.ts';
4
4
 
5
- export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode';
5
+ export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode' | 'traecli';
6
6
 
7
- const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode']);
7
+ const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode', 'traecli']);
8
8
  const TUI_AGENT_CLIENT_IDS: Readonly<Record<TuiName, AgentClientId>> = Object.freeze({
9
9
  claude: 'claude-code',
10
10
  codex: 'codex',
11
11
  antigravity: 'antigravity-cli',
12
- opencode: 'opencode'
12
+ opencode: 'opencode',
13
+ traecli: 'traecli'
13
14
  });
14
15
 
15
16
  export type CommandConfig = {
@@ -46,5 +47,6 @@ export function buildTuiCommand(tui: TuiName, prompt: string): [string, string[]
46
47
  if (tui === 'claude') return ['claude', ['--dangerously-skip-permissions', '--print', prompt]];
47
48
  if (tui === 'antigravity') return ['agy', ['--dangerously-skip-permissions', '--print', prompt]];
48
49
  if (tui === 'opencode') return ['opencode', ['run', '--dangerously-skip-permissions', prompt]];
50
+ if (tui === 'traecli') return ['traecli', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
49
51
  return ['codex', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
50
52
  }
@@ -188,10 +188,12 @@ function assertRegistrationBinding(
188
188
  ): void {
189
189
  if (registration.taskId !== manifest.taskId
190
190
  || registration.controlGeneration !== manifest.generation
191
- || registration.containerId !== manifest.containerIdentity.id
192
- || JSON.stringify(registration.buildIdentity) !== JSON.stringify(buildIdentity)) {
191
+ || registration.containerId !== manifest.containerIdentity.id) {
193
192
  fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration binding is invalid');
194
193
  }
194
+ if (registration.buildIdentity.protocolVersion !== buildIdentity.protocolVersion) {
195
+ fail('CODEX_LIFECYCLE_PROTOCOL_MISMATCH', 'Codex lifecycle protocol version does not match');
196
+ }
195
197
  }
196
198
 
197
199
  function atomicWrite(file: string, value: CodexControllerRegistrationV1): void {
@@ -322,7 +324,10 @@ export function resolveCodexControllerBinding(params: Readonly<{
322
324
  buildIdentity: LifecycleBuildIdentity;
323
325
  now?: number;
324
326
  probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
325
- }>): Readonly<{ instanceDigest: string; controlGeneration: string }> {
327
+ }>): Readonly<{
328
+ instanceDigest: string;
329
+ controlGeneration: string;
330
+ }> {
326
331
  if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
327
332
  fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot resolve a Codex controller registration');
328
333
  }
@@ -146,12 +146,15 @@ function controllerFailure(error: unknown): SandboxControlExecutionResult {
146
146
  ? error.code
147
147
  : /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
148
148
  ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
149
+ const message = error instanceof CodexControllerRegistrationError
150
+ ? error.message
151
+ : `${code}: controller operation failed; inspect the sandbox controller and rebuild the sandbox if needed`;
149
152
  const payload = {
150
153
  version: 1,
151
154
  status: 'failed',
152
155
  changed: false,
153
156
  lease: null,
154
- error: { code, message: `${code}: controller request failed`, retryable: false }
157
+ error: { code, message, retryable: false }
155
158
  };
156
159
  return { exitCode: 1, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
157
160
  }
@@ -265,7 +268,10 @@ export function executeRequest(
265
268
  ));
266
269
  }
267
270
  const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
268
- let controllerBinding: Readonly<{ instanceDigest: string; controlGeneration: string }> | null = null;
271
+ let controllerBinding: Readonly<{
272
+ instanceDigest: string;
273
+ controlGeneration: string;
274
+ }> | null = null;
269
275
  if (request.family === 'task-orchestration') {
270
276
  if (isCodexPrepare(request.args)) {
271
277
  if (!request.controllerProof) {
@@ -10,10 +10,16 @@ import {
10
10
  isCodexCapabilityProvenanceDetail
11
11
  } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
12
12
  import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
13
- import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
14
- import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
15
- import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
16
- import { verifyCodexSandboxControllerContext as verifyControllerContextFile } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
13
+ import {
14
+ computeLifecycleBuildIdentity,
15
+ verifyLifecycleBuildIdentity,
16
+ type LifecycleBuildIdentity,
17
+ type LifecycleIdentityWarning
18
+ } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
19
+ import { verifyCodexSandboxControllerContextWithWarnings as verifySandboxControllerWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
20
+ import {
21
+ verifyCodexSandboxControllerContextWithWarnings as verifyControllerContextFileWithWarnings
22
+ } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
17
23
  import type { AgentClientId } from '../agent-clients/types.ts';
18
24
  import { resolveTaskRef } from './resolve-ref.ts';
19
25
  import {
@@ -92,35 +98,6 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
92
98
  return options.store;
93
99
  }
94
100
 
95
- function controllerContext(
96
- repoRoot: string,
97
- brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
98
- ) {
99
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
100
- if (!contextPath) return null;
101
- if (brokerController) {
102
- return verifyControllerContextFile(contextPath, {
103
- repoRoot,
104
- generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
105
- });
106
- }
107
- return verifyCodexSandboxControllerContext(contextPath, { repoRoot });
108
- }
109
-
110
- function brokerBindingConflictsWithContext(
111
- repoRoot: string,
112
- brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
113
- ): boolean {
114
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
115
- if (!contextPath || !brokerController) return false;
116
- const context = verifyControllerContextFile(contextPath, {
117
- repoRoot,
118
- generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
119
- });
120
- return context.controllerInstanceDigest !== brokerController.instanceDigest
121
- || context.controlGeneration !== brokerController.controlGeneration;
122
- }
123
-
124
101
  function brokerControllerBinding(): Readonly<{ instanceDigest: string; controlGeneration: string }> | null {
125
102
  const raw = process.env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING;
126
103
  if (!raw) return null;
@@ -166,13 +143,17 @@ async function prepareCodexOrchestrationDelegation(
166
143
  }
167
144
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
168
145
  const brokerController = brokerControllerBinding();
169
- if (brokerBindingConflictsWithContext(repoRoot, brokerController)) {
170
- return bridgeFailure(
171
- 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
172
- 'broker and local controller bindings do not match'
173
- );
174
- }
175
- const localController = controllerContext(repoRoot, brokerController);
146
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
147
+ const contextVerification = contextPath
148
+ ? (brokerController
149
+ ? verifyControllerContextFileWithWarnings(contextPath, {
150
+ repoRoot,
151
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
152
+ })
153
+ : verifySandboxControllerWithWarnings(contextPath, { repoRoot }))
154
+ : null;
155
+ const identityWarnings: LifecycleIdentityWarning[] = [...(contextVerification?.warnings ?? [])];
156
+ const localController = contextVerification?.context ?? null;
176
157
  if (localController && brokerController
177
158
  && (localController.controllerInstanceDigest !== brokerController.instanceDigest
178
159
  || localController.controlGeneration !== brokerController.controlGeneration)) {
@@ -187,6 +168,11 @@ async function prepareCodexOrchestrationDelegation(
187
168
  } : null);
188
169
  const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
189
170
  const attested = capabilityStore.inspect(input.capabilityToken);
171
+ const capabilityIdentity = verifyLifecycleBuildIdentity(attested.buildIdentity, buildIdentity);
172
+ if (!capabilityIdentity.ok) {
173
+ return bridgeFailure(capabilityIdentity.code!, capabilityIdentity.message!);
174
+ }
175
+ identityWarnings.push(...capabilityIdentity.warnings);
190
176
  const lifecycleProvenance = {
191
177
  ...attested.buildIdentity,
192
178
  hookDefinitionHash: preflight.hookDefinitionHash,
@@ -206,7 +192,7 @@ async function prepareCodexOrchestrationDelegation(
206
192
  controlGeneration: controller.controlGeneration
207
193
  } } : {})
208
194
  };
209
- return prepareOrchestrationDelegation(taskRef, {
195
+ const prepared = prepareOrchestrationDelegation(taskRef, {
210
196
  ...input,
211
197
  lifecycleProvenance
212
198
  }, {
@@ -244,6 +230,9 @@ async function prepareCodexOrchestrationDelegation(
244
230
  }
245
231
  }
246
232
  });
233
+ return identityWarnings.length > 0 && prepared.status !== 'failed'
234
+ ? { ...prepared, warnings: Object.freeze(identityWarnings) }
235
+ : prepared;
247
236
  } catch (error) {
248
237
  if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
249
238
  const detailValue = error instanceof Error
@@ -290,10 +279,11 @@ async function activateCodexOrchestrationDelegation(
290
279
  });
291
280
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
292
281
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
293
- const controller = contextPath
294
- ? verifyCodexSandboxControllerContext(contextPath, { repoRoot })
282
+ const contextVerification = contextPath
283
+ ? verifySandboxControllerWithWarnings(contextPath, { repoRoot })
295
284
  : null;
296
- return activateMatchingOrchestrationDelegation('codex', {
285
+ const controller = contextVerification?.context ?? null;
286
+ const activated = activateMatchingOrchestrationDelegation('codex', {
297
287
  nativeAgent: evidence.nativeAgent,
298
288
  childId: evidence.childThreadId,
299
289
  parentId: evidence.parentThreadId,
@@ -318,6 +308,10 @@ async function activateCodexOrchestrationDelegation(
318
308
  controlGeneration: controller?.controlGeneration ?? null
319
309
  }
320
310
  }, coreOptions(options));
311
+ const warnings = contextVerification?.warnings ?? [];
312
+ return warnings.length > 0 && activated.status !== 'failed'
313
+ ? { ...activated, warnings: Object.freeze(warnings) }
314
+ : activated;
321
315
  } catch (error) {
322
316
  if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
323
317
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
@@ -108,6 +108,13 @@ function exactText(value: unknown): value is string {
108
108
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
109
109
  }
110
110
 
111
+ function validCodexBuildMetadata(value: Readonly<Record<string, unknown>>): boolean {
112
+ return Number.isSafeInteger(value.protocolVersion) && (value.protocolVersion as number) > 0
113
+ && exactText(value.packageVersion)
114
+ && exactText(value.internalExecutableBuildHash)
115
+ && exactText(value.lifecycleContractHash);
116
+ }
117
+
111
118
  function nullableText(value: unknown): value is string | null {
112
119
  return value === null || exactText(value);
113
120
  }
@@ -185,9 +192,6 @@ function hasCurrentCodexEvidence(receipt: DelegationReceipt): boolean {
185
192
  if (
186
193
  host?.kind !== 'codex-lifecycle-v2'
187
194
  || host.protocolVersion !== provenance.protocolVersion
188
- || host.packageVersion !== provenance.packageVersion
189
- || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
190
- || host.lifecycleContractHash !== provenance.lifecycleContractHash
191
195
  || host.hookDefinitionHash !== provenance.hookDefinitionHash
192
196
  || host.hookSource !== provenance.hookSource
193
197
  || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
@@ -531,9 +535,6 @@ function activateDelegation(
531
535
  if (
532
536
  !expected
533
537
  || event.hostEvidence.protocolVersion !== expected.protocolVersion
534
- || event.hostEvidence.packageVersion !== expected.packageVersion
535
- || event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
536
- || event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
537
538
  || event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
538
539
  || event.hostEvidence.hookSource !== expected.hookSource
539
540
  || event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
@@ -548,6 +549,7 @@ function activateDelegation(
548
549
  || event.parentId !== expected.capabilitySessionId
549
550
  || (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
550
551
  || (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
552
+ || !validCodexBuildMetadata(event.hostEvidence)
551
553
  ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
552
554
  }
553
555
  return { ok: true, receipt: Object.freeze({
@@ -85,6 +85,13 @@ type TaskFinalizationStep = Readonly<{
85
85
  pendingSteps?: readonly string[];
86
86
  }>;
87
87
 
88
+ type TaskCommentSyncOutcome = Readonly<{
89
+ receipt: TaskFinalizationReceipt;
90
+ step: TaskFinalizationStep;
91
+ changed: boolean;
92
+ error: FinalizationError | null;
93
+ }>;
94
+
88
95
  type TaskFinalizationResult = Readonly<{
89
96
  status: 'completed' | 'failed' | 'blocked';
90
97
  changed: boolean;
@@ -390,11 +397,18 @@ function mutationPatch(current: TaskFinalizationReceipt, mutation: FinalizationM
390
397
  }
391
398
  if (mutation.scope === 'verification') {
392
399
  if (mutation.operation === 'succeeded') {
400
+ const resolvedTaskWarning = current.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
393
401
  const warnings = resolveStepWarnings(current, 'verification');
394
- return { verification: 'done', warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null };
402
+ return {
403
+ verification: 'done',
404
+ taskComment: resolvedTaskWarning ? 'pending' : current.taskComment,
405
+ warningProjection: warnings.length > 0 ? 'pending' : 'done',
406
+ warnings,
407
+ lastError: null
408
+ };
395
409
  }
396
410
  const warnings = replaceWarning(current, warningFromError('verification', mutation.error), 'open');
397
- return { verification: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
411
+ return { verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
398
412
  }
399
413
  return mutation.operation === 'succeeded'
400
414
  ? { warningProjection: 'done', lastError: null }
@@ -472,6 +486,56 @@ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: T
472
486
  catch { return receipt; }
473
487
  }
474
488
 
489
+ function syncPendingTaskComment(input: {
490
+ repoRoot: string;
491
+ taskId: string;
492
+ agent: string;
493
+ receipt: TaskFinalizationReceipt;
494
+ commentSync: typeof syncPlatformComment;
495
+ consumedCapabilities: Set<string>;
496
+ }): TaskCommentSyncOutcome {
497
+ const { repoRoot, taskId, agent, commentSync, consumedCapabilities } = input;
498
+ let receipt = input.receipt;
499
+ if (receipt.taskComment !== 'pending') {
500
+ return {
501
+ receipt,
502
+ step: { status: receipt.taskComment === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null },
503
+ changed: false,
504
+ error: null
505
+ };
506
+ }
507
+ try {
508
+ const result = commentSync(taskId, { kind: 'task', agent, cwd: repoRoot });
509
+ const step = commentStep(result);
510
+ if (result.status === 'applied' || result.status === 'no-op') {
511
+ const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
512
+ const capability = issueCapability(receipt, 'task-comment');
513
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
514
+ scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
515
+ }, consumedCapabilities);
516
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
517
+ return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
518
+ }
519
+ const detail = step.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
520
+ const capability = issueCapability(receipt, 'task-comment');
521
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
522
+ scope: 'task-comment', operation: 'failed', error: detail
523
+ }, consumedCapabilities);
524
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
525
+ return { receipt, step, changed: result.changed, error: detail };
526
+ } catch (error) {
527
+ const detail = errorOf(error, 'COMMENT_SYNC_FAILED', true);
528
+ try {
529
+ const capability = issueCapability(receipt, 'task-comment');
530
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
531
+ scope: 'task-comment', operation: 'failed', error: detail
532
+ }, consumedCapabilities);
533
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
534
+ } catch { /* preserve the primary error */ }
535
+ return { receipt, step: { status: 'blocked', changed: false, error: detail }, changed: false, error: detail };
536
+ }
537
+ }
538
+
475
539
  function terminalResult(
476
540
  taskId: string,
477
541
  receipt: TaskFinalizationReceipt,
@@ -603,46 +667,15 @@ function applyUnderLock(
603
667
  }
604
668
 
605
669
  let taskComment: TaskFinalizationStep | null = null;
606
- try {
607
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
608
- if (receipt.taskComment !== 'pending') {
609
- taskComment = {
610
- status: receipt.taskComment === 'skipped' ? 'skipped' : 'no-op',
611
- changed: false,
612
- error: null
613
- };
614
- } else {
615
- const result = commentSync(taskId, { kind: 'task', agent: request.agent, cwd: repoRoot });
616
- taskComment = commentStep(result);
617
- if (result.status === 'applied' || result.status === 'no-op') {
618
- const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
619
- const capability = issueCapability(receipt, 'task-comment');
620
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
621
- scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
622
- }, consumedCapabilities);
623
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
624
- taskComment = skipped ? { ...taskComment, status: 'skipped' } : taskComment;
625
- changed = changed || result.changed;
626
- } else {
627
- const detail = taskComment.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
628
- const capability = issueCapability(receipt, 'task-comment');
629
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
630
- scope: 'task-comment', operation: 'failed', error: detail
631
- }, consumedCapabilities);
632
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
633
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, taskComment.error);
634
- }
635
- }
636
- } catch (error) {
637
- const detail = errorOf(error, 'COMMENT_SYNC_FAILED', true);
638
- try {
639
- const capability = issueCapability(receipt, 'task-comment');
640
- receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
641
- scope: 'task-comment', operation: 'failed', error: detail
642
- }, consumedCapabilities);
643
- receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
644
- } catch { /* preserve the primary error */ }
645
- return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: { status: 'blocked', changed: false, error: detail }, verification: null }, changed, detail);
670
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
671
+ const initialComment = syncPendingTaskComment({
672
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
673
+ });
674
+ receipt = initialComment.receipt;
675
+ taskComment = initialComment.step;
676
+ changed = changed || initialComment.changed;
677
+ if (initialComment.error) {
678
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
646
679
  }
647
680
 
648
681
  let verification: TaskFinalizationStep | null = null;
@@ -662,6 +695,15 @@ function applyUnderLock(
662
695
  scope: 'verification', operation: 'succeeded'
663
696
  }, consumedCapabilities);
664
697
  receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
698
+ const finalComment = syncPendingTaskComment({
699
+ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
700
+ });
701
+ receipt = finalComment.receipt;
702
+ if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
703
+ changed = changed || finalComment.changed;
704
+ if (finalComment.error) {
705
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
706
+ }
665
707
  } else {
666
708
  const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
667
709
  const capability = issueCapability(receipt, 'verification');
package/lib/task/index.ts CHANGED
@@ -10,7 +10,6 @@ Commands:
10
10
  ls [--all | --blocked | --completed] List tasks (default: active)
11
11
  show [--task <ref>] Print a task.md
12
12
  status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
13
- validate <ref> [options] -- <command> Run host validation in snapshot or inplace scope
14
13
 
15
14
  Examples:
16
15
  ai task cat 11 analysis
@@ -30,7 +29,6 @@ Examples:
30
29
  ai task show 11
31
30
  ai task show TASK-20260612-162737
32
31
  ai task status 11
33
- ai task validate 11 -- npm test
34
32
 
35
33
  Run 'ai task <command> --help' for details.`;
36
34
 
@@ -95,12 +93,6 @@ export async function runTask(args: string[]): Promise<void> {
95
93
  status(rest);
96
94
  break;
97
95
  }
98
- case 'validate': {
99
- const { validate } = await import('./commands/validate.ts');
100
- const exitCode = await validate(rest);
101
- if (exitCode !== 0) process.exitCode = exitCode;
102
- break;
103
- }
104
96
  default:
105
97
  process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
106
98
  process.stdout.write(`${USAGE}\n`);
@@ -119,6 +119,7 @@ type OrchestrationResult = Readonly<{
119
119
  taskId: string | null;
120
120
  run: OrchestrationRun | null;
121
121
  next: OrchestrationNext | null;
122
+ warnings?: readonly Readonly<{ code: string; message: string; action: string }>[];
122
123
  error: Readonly<{
123
124
  code: string;
124
125
  message: string;
@@ -181,7 +182,7 @@ function orchestrationPath(taskDir: string): string {
181
182
  return path.join(taskDir, 'orchestration.json');
182
183
  }
183
184
 
184
- const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; finish or clear active runs before upgrading';
185
+ const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; the file was left unchanged; rebuild the sandbox or manually repair the state before retrying';
185
186
 
186
187
  class OrchestrationStateError extends Error {
187
188
  readonly code = 'ORCHESTRATION_STATE_INVALID';
@@ -341,9 +341,6 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
341
341
  || (activatedCodex && (
342
342
  host?.kind !== 'codex-lifecycle-v2'
343
343
  || host.protocolVersion !== provenance.protocolVersion
344
- || host.packageVersion !== provenance.packageVersion
345
- || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
346
- || host.lifecycleContractHash !== provenance.lifecycleContractHash
347
344
  || host.hookDefinitionHash !== provenance.hookDefinitionHash
348
345
  || host.hookSource !== provenance.hookSource
349
346
  || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.9.9",
3
+ "version": "0.9.10",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,11 +27,10 @@ npm install
27
27
  npm run build
28
28
  ```
29
29
 
30
- Lifecycle hooks always invoke `agent-infra-internal` through `PATH`; they do not implicitly switch to checkout source or `dist`. In a Codex sandbox, the controller's `PATH`-first shim is authoritative and binds the controller's selected executable. Skills, rules, hooks, and task data continue to come from the current checkout. The commands above develop the CLI itself and do not change hook executable selection. If shell commands must also resolve to this checkout, link it and compare the active version with the package version:
30
+ Lifecycle hooks always invoke `agent-infra-internal` through `PATH`; they do not implicitly switch to checkout source or `dist`. In a Codex sandbox, the controller's `PATH`-first shim is authoritative and binds the controller's selected executable. Skills, rules, hooks, and task data continue to come from the current checkout. The commands above develop the CLI itself and do not change hook executable selection. To run the CLI from the current checkout, invoke the build output directly and compare its version with the package version. Do not run `npm link` on the host: it writes to npm's global prefix and can shadow a stable installation managed by Homebrew or npm.
31
31
 
32
32
  ```bash
33
- npm link
34
- ai version --raw
33
+ node ./dist/bin/cli.js version --raw
35
34
  node -p "'v' + require('./package.json').version"
36
35
  ```
37
36