@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
package/lib/decide.ts CHANGED
@@ -2,6 +2,9 @@ import { VERSION } from './version.ts';
2
2
  import { parseTaskScope } from './task/command-options.ts';
3
3
  import { applyHumanDecision } from './task/decision-intents.ts';
4
4
  import { canonicalTimestamp } from './task/write.ts';
5
+ import { consumeHumanOverride, failureId } from './task/human-override.ts';
6
+ import { resolveTaskRef } from './task/resolve-ref.ts';
7
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task/task-execution-lock.ts';
5
8
 
6
9
  type DecideOptions = {
7
10
  repoRoot?: string;
@@ -9,23 +12,33 @@ type DecideOptions = {
9
12
  version?: string;
10
13
  };
11
14
 
12
- function parseDecisionParts(parts: string[]): { decision: string; needsImplementation: boolean | undefined } {
15
+ function parseDecisionParts(parts: string[]): { decision: string; needsImplementation: boolean | undefined; overrideTicket?: string; overrideTarget?: string; overrideScope?: string } {
13
16
  const decision: string[] = [];
14
17
  let needsImplementation: boolean | undefined;
18
+ let overrideTicket: string | undefined;
19
+ let overrideTarget: string | undefined;
20
+ let overrideScope: string | undefined;
15
21
  for (let index = 0; index < parts.length; index += 1) {
16
- if (parts[index] !== '--needs-implementation') {
22
+ const part = parts[index];
23
+ if (part === '--needs-implementation') {
24
+ if (needsImplementation !== undefined) throw new Error("duplicate option '--needs-implementation'");
25
+ const value = parts[++index];
26
+ if (value !== 'true' && value !== 'false') {
27
+ throw new Error("--needs-implementation must be 'true' or 'false'");
28
+ }
29
+ needsImplementation = value === 'true';
30
+ } else if (part === '--override-ticket' || part === '--override-target' || part === '--override-scope') {
31
+ const value = parts[++index];
32
+ if (!value) throw new Error(`${part} requires a value`);
33
+ if (part === '--override-ticket') overrideTicket = value;
34
+ else if (part === '--override-target') overrideTarget = value;
35
+ else overrideScope = value;
36
+ } else {
17
37
  decision.push(parts[index]!);
18
- continue;
19
38
  }
20
- if (needsImplementation !== undefined) throw new Error("duplicate option '--needs-implementation'");
21
- const value = parts[++index];
22
- if (value !== 'true' && value !== 'false') {
23
- throw new Error("--needs-implementation must be 'true' or 'false'");
24
- }
25
- needsImplementation = value === 'true';
26
39
  }
27
40
  if (decision.length === 0) throw new Error('decision content is required');
28
- return { decision: decision.join(' '), needsImplementation };
41
+ return { decision: decision.join(' '), needsImplementation, overrideTicket, overrideTarget, overrideScope };
29
42
  }
30
43
 
31
44
  export async function decide(args: string[], options: DecideOptions = {}): Promise<number> {
@@ -64,17 +77,49 @@ export async function decide(args: string[], options: DecideOptions = {}): Promi
64
77
  try {
65
78
  const parsedDecision = parseDecisionParts(decisionParts);
66
79
  const now = (options.now ?? canonicalTimestamp)();
67
- const result = applyHumanDecision({
80
+ const request = {
68
81
  taskRef, selector, decision: parsedDecision.decision,
69
82
  needsImplementation: parsedDecision.needsImplementation
70
- }, {
83
+ };
84
+ const writeOptions = {
71
85
  repoRoot: options.repoRoot,
72
86
  metadataProvider: () => ({ timestamp: now, agentInfraVersion: options.version ?? VERSION })
73
- });
87
+ };
88
+ const resolved = taskRef ? resolveTaskRef(taskRef, { repoRoot: options.repoRoot }) : null;
89
+ let result;
90
+ const execute = () => {
91
+ let current = applyHumanDecision(request, writeOptions);
92
+ if (current.status !== 'failed' || !parsedDecision.overrideTicket) return current;
93
+ if (!parsedDecision.overrideTarget || !parsedDecision.overrideScope) throw new Error('override ticket requires --override-target and --override-scope');
94
+ const consumed = consumeHumanOverride({
95
+ taskRef: resolved?.ok ? resolved.taskId : String(taskRef ?? ''),
96
+ ticketId: parsedDecision.overrideTicket,
97
+ failureId: failureId('decision-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
98
+ target: parsedDecision.overrideTarget,
99
+ scope: parsedDecision.overrideScope
100
+ }, {
101
+ ...writeOptions,
102
+ effectExecutor: (capability) => {
103
+ const retried = applyHumanDecision(request, { ...writeOptions, manualOverride: capability });
104
+ current = retried;
105
+ return retried.status === 'failed' || retried.status === 'planned'
106
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no decision effect was committed' : `${retried.error?.code ?? 'DECISION_FAILED'}: ${retried.error?.message ?? 'manual decision effect failed'}` }
107
+ : null;
108
+ }
109
+ });
110
+ if (consumed.status === 'failed') throw new Error(consumed.error.message);
111
+ return current;
112
+ };
113
+ result = resolved?.ok
114
+ ? withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-decision', execute)
115
+ : execute();
74
116
  if (result.error) throw new Error(result.error.message);
75
117
  return 0;
76
118
  } catch (error) {
77
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
119
+ const message = error instanceof TaskExecutionLockError
120
+ ? `${error.code}: ${error.message}`
121
+ : error instanceof Error ? error.message : String(error);
122
+ process.stderr.write(`Error: ${message}\n`);
78
123
  return 1;
79
124
  }
80
125
  }
@@ -105,7 +105,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
105
105
  return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
106
106
  }
107
107
 
108
- function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[] }, runner: GitRunner = defaultRunner) {
108
+ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[]; expectedSha?: string }, runner: GitRunner = defaultRunner) {
109
109
  if (!input.remote || input.refs.length === 0) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [], error: { code: 'GIT_PUSH_INPUT_INVALID', message: 'Remote and refs are required' } };
110
110
  const operations: GitOperation[] = [];
111
111
  for (const ref of input.refs) {
@@ -120,7 +120,16 @@ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string
120
120
  }
121
121
  const canonical = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
122
122
  const remoteFact = run(runner, input.cwd, ['ls-remote', '--exit-code', input.remote, canonical]);
123
- operations.push({ name: 'push', ref, status: remoteFact.status === 0 ? 'applied' : 'failed', message: remoteFact.status === 0 ? undefined : 'Remote ref verification failed' });
123
+ const remoteSha = remoteFact.status === 0 ? remoteFact.stdout.trim().split(/\s+/)[0] ?? null : null;
124
+ const verified = remoteFact.status === 0 && (!input.expectedSha || remoteSha === input.expectedSha);
125
+ operations.push({
126
+ name: 'push',
127
+ ref,
128
+ status: verified ? 'applied' : 'failed',
129
+ message: verified ? undefined : input.expectedSha
130
+ ? `Remote ref verification failed: expected ${input.expectedSha}, received ${remoteSha ?? 'unavailable'}`
131
+ : 'Remote ref verification failed'
132
+ });
124
133
  }
125
134
  const failures = operations.filter((item) => item.status === 'failed').length;
126
135
  return { status: failures === 0 ? 'applied' as const : failures === operations.length ? 'failed' as const : 'degraded' as const, changed: failures < operations.length, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations, error: failures ? { code: 'GIT_PUSH_PARTIAL', message: `${failures} ref push operation(s) failed` } : null };
package/lib/init.ts CHANGED
@@ -249,7 +249,7 @@ async function cmdInit(): Promise<void> {
249
249
  agentClients: serializeAgentClients(defaultState)
250
250
  };
251
251
 
252
- if (sandboxEngine) config.sandbox.engine = sandboxEngine;
252
+ if (sandboxEngine) config.sandbox.engine = { [currentPlatform]: sandboxEngine };
253
253
  if (templateSources.length > 0) config.templates = { sources: templateSources };
254
254
  if (skillSources.length > 0) config.skills = { sources: skillSources };
255
255
 
@@ -14,6 +14,7 @@ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-l
14
14
  import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
15
15
  import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
16
16
  import { resolveTaskRef } from '../task/resolve-ref.ts';
17
+ import { hasSealableOrchestrationDelegation } from '../task/orchestration.ts';
17
18
  import {
18
19
  activateCodexOrchestrationDelegation,
19
20
  activateCodexSpawnDelegation,
@@ -100,7 +101,7 @@ function hookDefinitionHash(): string {
100
101
  function controllerBinding(taskId: string) {
101
102
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
102
103
  if (!contextPath) return undefined;
103
- const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
104
+ const context = verifyCodexSandboxControllerContext(contextPath, { repoRoot: process.cwd() });
104
105
  return {
105
106
  instanceDigest: context.controllerInstanceDigest,
106
107
  controlGeneration: context.controlGeneration
@@ -139,7 +140,8 @@ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent
139
140
  sessionId: text('sessionId'),
140
141
  turnId: text('turnId'),
141
142
  childThreadId: text('childThreadId'),
142
- nativeAgent: text('nativeAgent')
143
+ nativeAgent: text('nativeAgent'),
144
+ source: 'hook'
143
145
  };
144
146
  if (phase === 'subagent-stop') return {
145
147
  type: 'hook-stop',
@@ -326,8 +328,17 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
326
328
  }
327
329
  const result = store.apply(event);
328
330
  if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
329
- const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
330
- outputBridgeResult(bridged);
331
+ if (!hasSealableOrchestrationDelegation('codex', event.childThreadId, { repoRoot: process.cwd() })) {
332
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
333
+ return;
334
+ }
335
+ output({
336
+ status: result.state.status,
337
+ changed: true,
338
+ evidence: result.state,
339
+ diagnostics: [],
340
+ error: result.state.error
341
+ });
331
342
  return;
332
343
  }
333
344
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
@@ -350,7 +361,9 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
350
361
  return;
351
362
  }
352
363
  if (parsed.operation === 'resolve-stop') {
353
- const terminal = await resolveCodexTerminal(childThreadId);
364
+ const stopTurnId = store.read(childThreadId).state.stop?.turnId;
365
+ if (!stopTurnId) throw new Error('Codex lifecycle stop hook is not available');
366
+ const terminal = await resolveCodexTerminal(childThreadId, stopTurnId);
354
367
  const latest = store.apply(terminal);
355
368
  if (latest.state.status !== 'stop-ready') {
356
369
  throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
@@ -26,8 +26,6 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
26
26
  return null;
27
27
  }
28
28
  const allowed = new Set([
29
- '--task-id',
30
- '--task-ref',
31
29
  '--context',
32
30
  '--executor-model',
33
31
  '--executor-reasoning-effort',
@@ -37,6 +35,11 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
37
35
  const values: Record<string, string> = {};
38
36
  for (let index = 1; index < args.length; index += 1) {
39
37
  const flag = args[index]!;
38
+ if (flag === '--task-id' || flag.startsWith('--task-id=')
39
+ || flag === '--task-ref' || flag.startsWith('--task-ref=')) {
40
+ fail('CODEX_SANDBOX_CONTROLLER_IDENTITY_OPTION_UNSUPPORTED', 'controller task identity options are not supported', 2);
41
+ return null;
42
+ }
40
43
  const value = args[++index];
41
44
  if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
42
45
  fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
@@ -50,17 +53,12 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
50
53
  async function codexSandboxController(args: string[] = []): Promise<void> {
51
54
  const parsed = parse(args);
52
55
  if (!parsed || process.exitCode) return;
53
- const taskId = parsed.values['--task-id'];
54
- if (!taskId) {
55
- fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
56
- return;
57
- }
58
56
  try {
59
57
  if (parsed.operation === 'verify-context') {
60
58
  const contextPath = parsed.values['--context']
61
59
  ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
62
60
  if (!contextPath) throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
63
- const context = verifyCodexSandboxControllerContext(contextPath, taskId);
61
+ const context = verifyCodexSandboxControllerContext(contextPath);
64
62
  process.stdout.write(`${JSON.stringify({
65
63
  status: 'ready',
66
64
  changed: false,
@@ -77,8 +75,6 @@ async function codexSandboxController(args: string[] = []): Promise<void> {
77
75
  })}\n`);
78
76
  return;
79
77
  }
80
- const taskRef = parsed.values['--task-ref'];
81
- if (!taskRef) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
82
78
  const policy = [
83
79
  '--executor-model',
84
80
  '--executor-reasoning-effort',
@@ -90,8 +86,6 @@ async function codexSandboxController(args: string[] = []): Promise<void> {
90
86
  throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
91
87
  }
92
88
  const exitCode = await runCodexSandboxController({
93
- taskId,
94
- taskRef,
95
89
  executorModel: parsed.values['--executor-model'],
96
90
  executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
97
91
  reviewerModel: parsed.values['--reviewer-model'],
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
- import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs, pushRebasedBranch } from '../git/workflow.ts';
2
+ import { inspectGitWorkflow, pushRebasedBranch } from '../git/workflow.ts';
3
3
  import { compareReviewTrees, snapshotReview } from '../git/review-snapshot.ts';
4
4
  import { resolvePostReviewGlobs } from '../task/review-fingerprint.ts';
5
+ import { executeCommitOperation } from '../task/commit-operation.ts';
5
6
 
6
7
  function output(value: unknown): void { process.stdout.write(`${JSON.stringify(value)}\n`); }
7
8
 
@@ -18,11 +19,9 @@ function gitWorkflow(args: string[] = []): void {
18
19
  else if (action === 'commit') {
19
20
  const inputIndex = rest.indexOf('--input');
20
21
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
21
- result = input ? commitExplicitPaths({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
22
- } else if (action === 'push') {
23
- const inputIndex = rest.indexOf('--input');
24
- const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
25
- result = input ? pushGitRefs({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
22
+ result = input
23
+ ? executeCommitOperation({ cwd: cwd ?? process.cwd(), ...input })
24
+ : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
26
25
  } else if (action === 'push-rebased') {
27
26
  const inputIndex = rest.indexOf('--input');
28
27
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
@@ -17,13 +17,13 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
17
17
  agent-infra-internal platform-pr resolve-external <task-ref> --agent <agent> [--pr <N>] [--dry-run] [--cwd <path>]
18
18
  agent-infra-internal platform-pr create <task-ref> --agent <agent> --base <branch> --head <branch> --title-file <path|-> --body-file <path|-> [--draft] [--dry-run] [--cwd <path>]
19
19
  agent-infra-internal platform-pr bind <task-ref> --pr <N> --agent <agent> [--dry-run] [--cwd <path>]
20
- agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] [--dry-run] [--cwd <path>]
20
+ agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
21
21
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
22
- agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> [--dry-run] [--cwd <path>]
22
+ agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
23
23
  `;
24
24
 
25
25
  const BOOLEAN_FLAGS = new Set(['--draft', '--dry-run', '--metadata', '--closing-issue']);
26
- const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr']);
26
+ const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr', '--result']);
27
27
 
28
28
  function key(flag: string): string {
29
29
  return flag.slice(2).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
@@ -76,9 +76,9 @@ function platformPr(args: string[] = []): void {
76
76
  'resolve-external': ['cwd', 'agent', 'pr', 'dryRun'],
77
77
  create: ['cwd', 'agent', 'base', 'head', 'titleFile', 'bodyFile', 'draft', 'dryRun'],
78
78
  bind: ['cwd', 'agent', 'pr', 'dryRun'],
79
- sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'dryRun'],
79
+ sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'result', 'dryRun'],
80
80
  'summary-context': ['cwd'],
81
- 'summary-sync': ['cwd', 'agent', 'bodyFile', 'dryRun']
81
+ 'summary-sync': ['cwd', 'agent', 'bodyFile', 'result', 'dryRun']
82
82
  };
83
83
  const unexpected = Object.keys(values).find((name) => !allowed[operation]!.includes(name));
84
84
  if (unexpected) { fail(`${operation} does not accept --${unexpected}`); return; }
@@ -88,6 +88,11 @@ function platformPr(args: string[] = []): void {
88
88
  const agent = normalizeAgentToken(values.agent);
89
89
  if (!agent) { fail(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
90
90
  values.agent = agent;
91
+ const primaryResult = values.result === undefined ? undefined : values.result;
92
+ if (primaryResult !== undefined && !['pr_created', 'pr_reused', 'no_op'].includes(primaryResult as string)) {
93
+ fail('--result must be pr_created, pr_reused, or no_op');
94
+ return;
95
+ }
91
96
  if (operation === 'resolve-external') {
92
97
  const pr = values.pr === undefined ? undefined : Number(values.pr);
93
98
  if (pr !== undefined && (!Number.isInteger(pr) || pr <= 0)) { fail('resolve-external requires a positive --pr'); return; }
@@ -102,13 +107,15 @@ function platformPr(args: string[] = []): void {
102
107
  }
103
108
  if (operation === 'sync') {
104
109
  if (values.metadata !== true && values.closingIssue !== true) { fail('sync requires --metadata or --closing-issue'); return; }
105
- finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, dryRun: values.dryRun === true }));
110
+ if (!primaryResult) { fail('sync requires --result pr_created, pr_reused, or no_op'); return; }
111
+ finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', dryRun: values.dryRun === true }));
106
112
  return;
107
113
  }
108
114
  if (operation === 'summary-sync') {
109
115
  if (typeof values.bodyFile !== 'string') { fail('summary-sync requires --body-file'); return; }
116
+ if (!primaryResult) { fail('summary-sync requires --result pr_created, pr_reused, or no_op'); return; }
110
117
  try {
111
- finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), dryRun: values.dryRun === true }));
118
+ finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', dryRun: values.dryRun === true }));
112
119
  } catch (error) {
113
120
  fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
114
121
  }
@@ -25,20 +25,19 @@ type DemoResult = {
25
25
  outputPath: string | null;
26
26
  };
27
27
 
28
+ // The tape is the canonical init interaction contract. Do not hash all of
29
+ // lib/init.ts: non-visual config serialization changes must not re-record it.
28
30
  const DEMO_INPUT_PATHS = [
29
31
  'assets/demo-init.tape',
30
32
  'scripts/demo-regen.sh',
31
33
  'scripts/normalize-gif-duration.py',
32
34
  'bin/cli.ts',
33
- 'lib/init.ts',
34
35
  'lib/log.ts',
35
36
  'lib/prompt.ts',
36
37
  'lib/paths.ts',
37
38
  'lib/render.ts',
38
39
  'lib/builtin-tuis.ts',
39
- 'lib/sandbox/engines/',
40
- 'src/sync-templates.js',
41
- 'templates/'
40
+ 'lib/sandbox/engines/'
42
41
  ] as const;
43
42
  const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
44
43
  const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
@@ -1,6 +1,84 @@
1
- import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.ts';
1
+ import {
2
+ requestSandboxControl,
3
+ requestSandboxTaskFinalization,
4
+ requestSandboxTaskControl,
5
+ SandboxControlClientError
6
+ } from '../sandbox/control/client.ts';
7
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
8
  import { serveSandboxControl } from '../sandbox/control/server.ts';
3
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
+ import {
11
+ controllerProofFromContext,
12
+ verifyCodexSandboxControllerContext
13
+ } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
14
+
15
+ function isCanonicalCodexPrepare(args: readonly string[]): boolean {
16
+ if (args[1] !== 'prepare') return false;
17
+ const clients: string[] = [];
18
+ for (let index = 2; index < args.length; index += 1) {
19
+ if (args[index] === '--client') clients.push(args[index + 1] ?? '');
20
+ else if (args[index]?.startsWith('--client=')) clients.push(args[index]!.slice('--client='.length));
21
+ }
22
+ return clients.length === 1 && clients[0] === 'codex';
23
+ }
24
+
25
+ type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
26
+
27
+ function writeFinalizationEnvelope(
28
+ status: FinalizationStatus,
29
+ accepted: boolean,
30
+ error: { code: string; message: string; retryable: boolean },
31
+ changed = false,
32
+ result: unknown = null
33
+ ): void {
34
+ process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
35
+ }
36
+
37
+ function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
38
+ if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return 'unknown';
39
+ return error.retryable ? 'blocked' : 'failed';
40
+ }
41
+
42
+ function sandboxFinalizationClient(args: string[]): void {
43
+ if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
44
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
45
+ writeFinalizationEnvelope('failed', false, error);
46
+ process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ const agent = normalizeAgentToken(args[3]);
51
+ if (!agent) {
52
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
53
+ writeFinalizationEnvelope('failed', false, error);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ let response;
58
+ try {
59
+ response = requestSandboxTaskFinalization({ agent });
60
+ } catch (error) {
61
+ if (!(error instanceof SandboxControlClientError)) throw error;
62
+ writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
63
+ process.stderr.write(`${error.detail.message}\n`);
64
+ process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
65
+ return;
66
+ }
67
+ if (response.phase === 'rejected') {
68
+ const error = response.error ?? {
69
+ code: 'SANDBOX_CONTROL_REJECTED',
70
+ message: 'sandbox control rejected the finalization request',
71
+ retryable: false
72
+ };
73
+ writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
74
+ process.stderr.write(`${error.message}\n`);
75
+ process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
76
+ return;
77
+ }
78
+ process.stdout.write(response.stdout);
79
+ process.stderr.write(response.stderr);
80
+ process.exitCode = response.exitCode ?? 1;
81
+ }
4
82
 
5
83
  async function sandboxControl(args: string[]): Promise<void> {
6
84
  const [operation, ...rest] = args;
@@ -31,9 +109,21 @@ async function sandboxControl(args: string[]): Promise<void> {
31
109
  }
32
110
  if (operation === 'client') {
33
111
  const [family = '', ...commandArgs] = rest;
112
+ if (family === 'task-finalization') {
113
+ sandboxFinalizationClient(commandArgs);
114
+ return;
115
+ }
34
116
  let response;
35
117
  try {
36
- response = requestSandboxControl({ family, args: commandArgs });
118
+ if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
119
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
120
+ const proof = contextPath
121
+ ? controllerProofFromContext(verifyCodexSandboxControllerContext(contextPath))
122
+ : null;
123
+ response = requestSandboxTaskControl({ family, args: commandArgs, controllerProof: proof });
124
+ } else {
125
+ response = requestSandboxControl({ family, args: commandArgs });
126
+ }
37
127
  } catch (error) {
38
128
  if (!(error instanceof SandboxControlClientError)) throw error;
39
129
  process.stderr.write(`${error.detail.message}\n`);
@@ -6,6 +6,9 @@ import type {
6
6
  PrReviewActivityIntent,
7
7
  PrReviewInspectIntent
8
8
  } from '../task/activity-intent.ts';
9
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
10
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
11
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
9
12
 
10
13
  const USAGE = `Usage: agent-infra-internal task-activity <task-ref> pr-review-inspect
11
14
  agent-infra-internal task-activity <task-ref> pr-review-start --agent <agent> --artifact <canonical.md> --head <40hex> [--dry-run]
@@ -24,13 +27,14 @@ const FLAGS: Record<string, string> = {
24
27
  '--minor': 'minor',
25
28
  '--outcome': 'outcome',
26
29
  '--reason': 'reason'
30
+ , '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
27
31
  };
28
32
  const COMMON = ['agent', 'artifact', 'head'] as const;
29
33
  const ALLOWED: Record<string, ReadonlySet<string>> = {
30
34
  'pr-review-inspect': new Set(['kind', 'taskRef']),
31
- 'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun']),
32
- 'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun']),
33
- 'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun'])
35
+ 'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
36
+ 'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
37
+ 'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope'])
34
38
  };
35
39
 
36
40
  function usageFailure(message: string): void {
@@ -83,9 +87,45 @@ function taskActivity(args: string[] = []): void {
83
87
  for (const key of operationRequired) {
84
88
  if (values[key] === undefined) { usageFailure(`${kind} requires '${key}'`); return; }
85
89
  }
86
- const result = applyPrReviewActivityIntent(values as PrReviewActivityIntent);
87
- process.stdout.write(`${JSON.stringify(result)}\n`);
88
- if (result.status === 'failed') process.exitCode = 1;
90
+ const dryRunConflict = overrideDryRunConflict(values);
91
+ if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
92
+ const resolved = resolveTaskRef(taskRef);
93
+ if (!resolved.ok) { usageFailure(resolved.message); return; }
94
+ let result;
95
+ let humanOverride: unknown = null;
96
+ try {
97
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity.${kind}`, () => {
98
+ let current = applyPrReviewActivityIntent(values as PrReviewActivityIntent, { lockAlreadyHeld: true });
99
+ if (current.status !== 'failed' || !values.overrideTicket) return current;
100
+ if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
101
+ const consumed = consumeHumanOverride({
102
+ taskRef, ticketId: String(values.overrideTicket),
103
+ failureId: failureId('activity-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
104
+ target: String(values.overrideTarget), scope: String(values.overrideScope)
105
+ }, {
106
+ effectExecutor: (capability) => {
107
+ const retried = applyPrReviewActivityIntent(values as PrReviewActivityIntent, { lockAlreadyHeld: true, manualOverride: capability });
108
+ current = retried;
109
+ return retried.status === 'failed' || retried.status === 'planned'
110
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no activity effect was committed' : `${retried.error?.code ?? 'ACTIVITY_FAILED'}: ${retried.error?.message ?? 'manual activity effect failed'}` }
111
+ : null;
112
+ }
113
+ });
114
+ humanOverride = consumed;
115
+ return current;
116
+ });
117
+ } catch (error) {
118
+ if (!(error instanceof TaskExecutionLockError)) throw error;
119
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
124
+ const overrideFailed = Boolean(
125
+ humanOverride && typeof humanOverride === 'object' &&
126
+ (humanOverride as { status?: unknown }).status === 'failed'
127
+ );
128
+ if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
89
129
  }
90
130
 
91
131
  export { taskActivity };
@@ -1,6 +1,9 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
2
  import { applyTaskEvent, eventCatalog } from '../task/events.ts';
3
3
  import type { TaskEventRequest, Verdict } from '../task/events.ts';
4
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
5
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
6
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
4
7
 
5
8
  const USAGE = `Usage: agent-infra-internal task-event <N | TASK-id> <event> --agent <agent> [event options] [--orchestrated] [--dry-run]
6
9
 
@@ -13,7 +16,8 @@ const FLAGS: Record<string, keyof TaskEventRequest> = {
13
16
  '--fix-for': 'fixFor', '--implementation-input': 'implementationInput',
14
17
  '--verdict': 'verdict', '--blockers': 'blockers', '--major': 'major',
15
18
  '--minor': 'minor', '--manual-validation': 'manualValidation', '--files-modified': 'filesModified',
16
- '--tests-passed': 'testsPassed', '--summary-result': 'summaryResult'
19
+ '--tests-passed': 'testsPassed', '--summary-result': 'summaryResult',
20
+ '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
17
21
  };
18
22
  const NUMERIC = new Set(['round', 'question', 'blockers', 'major', 'minor', 'manualValidation', 'filesModified', 'testsPassed']);
19
23
 
@@ -51,9 +55,54 @@ function taskEvent(args: string[] = []): void {
51
55
  const agent = normalizeAgentToken(String(request.agent ?? ''));
52
56
  if (!agent) { usageFailure(`invalid --agent '${request.agent}': ${AGENT_USAGE_HINT}`); return; }
53
57
  request.agent = agent;
54
- const result = applyTaskEvent(request);
55
- process.stdout.write(`${JSON.stringify(result)}\n`);
56
- if (result.status === 'failed') process.exitCode = 1;
58
+ const dryRunConflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
59
+ if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
60
+ const resolved = resolveTaskRef(request.taskRef);
61
+ if (!resolved.ok) {
62
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ let result;
67
+ let humanOverride: unknown = null;
68
+ try {
69
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => {
70
+ let current = applyTaskEvent(request, { lockAlreadyHeld: true });
71
+ const values = request as Record<string, unknown>;
72
+ if (current.status !== 'failed' || !values.overrideTicket) return current;
73
+ if (!values.overrideTarget || !values.overrideScope) {
74
+ return { ...current, humanOverride: { status: 'failed', error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires target and scope' } } } as typeof current & { humanOverride: unknown };
75
+ }
76
+ const consumed = consumeHumanOverride({
77
+ taskRef: request.taskRef,
78
+ ticketId: String(values.overrideTicket),
79
+ failureId: failureId('task-event', current.error?.code ?? 'EVENT_TRANSITION_INVALID'),
80
+ target: String(values.overrideTarget),
81
+ scope: String(values.overrideScope)
82
+ }, {
83
+ effectExecutor: (capability) => {
84
+ const retried = applyTaskEvent(request, { lockAlreadyHeld: true, manualOverride: capability });
85
+ current = retried;
86
+ return retried.status === 'failed' || retried.status === 'planned'
87
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no task event was committed' : `${retried.error?.code ?? 'EVENT_FAILED'}: ${retried.error?.message ?? 'manual task event effect failed'}` }
88
+ : null;
89
+ }
90
+ });
91
+ humanOverride = consumed;
92
+ return current;
93
+ });
94
+ } catch (error) {
95
+ if (!(error instanceof TaskExecutionLockError)) throw error;
96
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
101
+ const overrideFailed = Boolean(
102
+ humanOverride && typeof humanOverride === 'object' &&
103
+ (humanOverride as { status?: unknown }).status === 'failed'
104
+ );
105
+ if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
57
106
  }
58
107
 
59
108
  export { taskEvent };