@fitlab-ai/agent-infra 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  7. package/dist/lib/decide.js +68 -14
  8. package/dist/lib/init.js +1 -1
  9. package/dist/lib/internal/release-workflow.js +1 -3
  10. package/dist/lib/internal/task-activity.js +57 -7
  11. package/dist/lib/internal/task-event.js +57 -4
  12. package/dist/lib/internal/task-ledger.js +59 -5
  13. package/dist/lib/internal/task-lifecycle.js +59 -8
  14. package/dist/lib/internal/task-orchestration.js +11 -3
  15. package/dist/lib/internal/task-override.js +131 -0
  16. package/dist/lib/internal/task-review.js +64 -6
  17. package/dist/lib/internal/task-verify.js +65 -3
  18. package/dist/lib/internal/task-warning.js +57 -4
  19. package/dist/lib/platform/pr-summary.js +5 -3
  20. package/dist/lib/runtime/agent-runtime.js +56 -0
  21. package/dist/lib/sandbox/commands/create.js +389 -311
  22. package/dist/lib/sandbox/control/container-identity.js +1 -1
  23. package/dist/lib/sandbox/control/executor.js +10 -2
  24. package/dist/lib/sandbox/control/lifecycle.js +392 -72
  25. package/dist/lib/sandbox/control/state.js +27 -11
  26. package/dist/lib/sandbox/engine.js +14 -7
  27. package/dist/lib/sandbox/mounts.js +20 -5
  28. package/dist/lib/sandbox/recovery.js +78 -10
  29. package/dist/lib/sandbox/workspace-view.js +62 -40
  30. package/dist/lib/server/process-state.js +51 -1
  31. package/dist/lib/task/activity-intent.js +4 -1
  32. package/dist/lib/task/decision-intents.js +4 -2
  33. package/dist/lib/task/delegation-receipts.js +37 -16
  34. package/dist/lib/task/events.js +9 -5
  35. package/dist/lib/task/guard-override.js +31 -0
  36. package/dist/lib/task/human-override.js +1240 -0
  37. package/dist/lib/task/ledger-intents.js +6 -3
  38. package/dist/lib/task/lifecycle.js +69 -14
  39. package/dist/lib/task/orchestration.js +60 -6
  40. package/dist/lib/task/review-finalization.js +3 -2
  41. package/dist/lib/task/verification-engine.js +40 -14
  42. package/dist/lib/task/verification.js +13 -1
  43. package/dist/lib/task/workflow-warning-intents.js +4 -2
  44. package/dist/lib/task/write.js +2 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  47. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
  49. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  51. package/lib/decide.ts +59 -14
  52. package/lib/init.ts +1 -1
  53. package/lib/internal/release-workflow.ts +1 -3
  54. package/lib/internal/task-activity.ts +46 -6
  55. package/lib/internal/task-event.ts +53 -4
  56. package/lib/internal/task-ledger.ts +48 -4
  57. package/lib/internal/task-lifecycle.ts +56 -12
  58. package/lib/internal/task-orchestration.ts +12 -4
  59. package/lib/internal/task-override.ts +138 -0
  60. package/lib/internal/task-review.ts +51 -6
  61. package/lib/internal/task-verify.ts +52 -3
  62. package/lib/internal/task-warning.ts +44 -3
  63. package/lib/platform/pr-summary.ts +10 -3
  64. package/lib/runtime/agent-runtime.ts +76 -0
  65. package/lib/sandbox/commands/create.ts +88 -4
  66. package/lib/sandbox/config.ts +2 -1
  67. package/lib/sandbox/control/container-identity.ts +3 -3
  68. package/lib/sandbox/control/executor.ts +10 -2
  69. package/lib/sandbox/control/lifecycle.ts +463 -76
  70. package/lib/sandbox/control/protocol.ts +5 -2
  71. package/lib/sandbox/control/state.ts +35 -18
  72. package/lib/sandbox/engine.ts +20 -9
  73. package/lib/sandbox/mounts.ts +23 -5
  74. package/lib/sandbox/recovery.ts +90 -9
  75. package/lib/sandbox/workspace-view.ts +70 -40
  76. package/lib/server/process-state.ts +48 -1
  77. package/lib/task/activity-intent.ts +4 -2
  78. package/lib/task/decision-intents.ts +4 -2
  79. package/lib/task/delegation-receipts.ts +46 -18
  80. package/lib/task/events.ts +10 -5
  81. package/lib/task/guard-override.ts +50 -0
  82. package/lib/task/human-override.ts +1503 -0
  83. package/lib/task/ledger-intents.ts +6 -3
  84. package/lib/task/lifecycle.ts +71 -14
  85. package/lib/task/orchestration.ts +89 -7
  86. package/lib/task/review-finalization.ts +6 -2
  87. package/lib/task/verification-engine.ts +42 -16
  88. package/lib/task/verification.ts +15 -1
  89. package/lib/task/workflow-warning-intents.ts +4 -2
  90. package/lib/task/write.ts +4 -1
  91. package/package.json +1 -1
  92. package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
  95. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  96. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
@@ -17,6 +17,7 @@ type ControllerControl = Readonly<{
17
17
  generation: string;
18
18
  channelDir: string;
19
19
  statusDir: string;
20
+ runtimeDir: string;
20
21
  }>;
21
22
 
22
23
  type CodexSandboxControllerContext = Readonly<{
@@ -95,13 +96,38 @@ function controlFromEnvironment(): ControllerControl {
95
96
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
96
97
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
97
98
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
98
- if (!token || !generation || !channelDir || !statusDir) {
99
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
100
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
99
101
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
100
102
  }
101
- return { token, generation, channelDir, statusDir };
103
+ return { token, generation, channelDir, statusDir, runtimeDir };
104
+ }
105
+
106
+ function verifyRuntime(runtimeDir: string): void {
107
+ let stat: fs.Stats;
108
+ try {
109
+ stat = fs.lstatSync(runtimeDir);
110
+ } catch {
111
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
112
+ }
113
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
114
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
115
+ }
116
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
117
+ try {
118
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
119
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
120
+ throw new Error('probe mismatch');
121
+ }
122
+ } catch {
123
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
124
+ } finally {
125
+ fs.rmSync(probe, { force: true });
126
+ }
102
127
  }
103
128
 
104
129
  function verifyControl(taskId: string, control: ControllerControl): void {
130
+ verifyRuntime(control.runtimeDir);
105
131
  let status: ReturnType<typeof readSandboxControlStatus> | null = null;
106
132
  let lastError: unknown = null;
107
133
  for (let attempt = 0; attempt < 5; attempt += 1) {
@@ -214,6 +240,7 @@ function isolatedEnvironment(
214
240
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
215
241
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
216
242
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
243
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
217
244
  AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
218
245
  };
219
246
  }
@@ -11,6 +11,7 @@ import type {
11
11
  CodexLifecycleEvent,
12
12
  CodexLifecycleState
13
13
  } from './evidence.ts';
14
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
14
15
 
15
16
  type StoredCodexLifecycle = Readonly<{
16
17
  schemaVersion: 1;
@@ -105,7 +106,7 @@ function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevisio
105
106
  }
106
107
 
107
108
  function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
108
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
109
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
109
110
  const now = options.now ?? (() => new Date().toISOString());
110
111
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
111
112
  fs.chmodSync(root, 0o700);
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
  }
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
 
@@ -36,9 +36,7 @@ const DEMO_INPUT_PATHS = [
36
36
  'lib/paths.ts',
37
37
  'lib/render.ts',
38
38
  'lib/builtin-tuis.ts',
39
- 'lib/sandbox/engines/',
40
- 'src/sync-templates.js',
41
- 'templates/'
39
+ 'lib/sandbox/engines/'
42
40
  ] as const;
43
41
  const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
44
42
  const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
@@ -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 };
@@ -2,14 +2,17 @@ import fs from 'node:fs';
2
2
 
3
3
  import { applyLedgerIntent } from '../task/ledger-intents.ts';
4
4
  import type { LedgerIntent } from '../task/ledger-intents.ts';
5
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
5
6
  import { isReviewStage, parseLedger, summarizeLedgerStage, validateLedgerRows } from '../task/ledger.ts';
6
7
  import { resolveTaskRef } from '../task/resolve-ref.ts';
8
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
7
9
 
8
10
  const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, stage-status\n`;
9
11
  const FLAGS: Record<string, string> = {
10
12
  '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
11
13
  '--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
12
14
  '--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
15
+ , '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
13
16
  };
14
17
  const NUMERIC = new Set(['ordinal', 'round']);
15
18
  const BOOLEAN = new Set(['needsImplementation']);
@@ -58,11 +61,16 @@ function taskLedger(args: string[] = []): void {
58
61
  'finding-review': ['needsImplementation'],
59
62
  'decision-upsert': ['needsImplementation']
60
63
  };
61
- const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? [])]);
64
+ const overrideFields = kind === 'stage-status' || kind === 'decision-next-id'
65
+ ? []
66
+ : ['overrideTicket', 'overrideTarget', 'overrideScope'];
67
+ const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? []), ...overrideFields]);
62
68
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
63
69
  const missing = required[kind]!.find((key) => values[key] === undefined);
64
70
  if (unexpected) { usageFailure(`${kind} does not accept '${unexpected}'`); return; }
65
71
  if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
72
+ const dryRunConflict = overrideDryRunConflict(values);
73
+ if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
66
74
  if (kind === 'stage-status') {
67
75
  if (!isReviewStage(String(values.stage))) { usageFailure(`stage-status requires a valid review stage`); return; }
68
76
  const resolved = resolveTaskRef(taskRef);
@@ -87,9 +95,45 @@ function taskLedger(args: string[] = []): void {
87
95
  }
88
96
  return;
89
97
  }
90
- const result = applyLedgerIntent(values as LedgerIntent);
91
- process.stdout.write(`${JSON.stringify(result)}\n`);
92
- if (result.status === 'failed') process.exitCode = 1;
98
+ const lockResolved = resolveTaskRef(taskRef);
99
+ if (!lockResolved.ok) { usageFailure(lockResolved.message); return; }
100
+ let result;
101
+ let humanOverride: unknown = null;
102
+ try {
103
+ result = withTaskExecutionLock(lockResolved.repoRoot, lockResolved.taskId, `task-ledger.${kind}`, () => {
104
+ let current = applyLedgerIntent(values as LedgerIntent);
105
+ if (current.status !== 'failed' || !values.overrideTicket) return current;
106
+ if (!values.overrideTarget || !values.overrideScope) { usageFailure('override ticket requires target and scope'); return current; }
107
+ const consumed = consumeHumanOverride({
108
+ taskRef,
109
+ ticketId: String(values.overrideTicket),
110
+ failureId: failureId('ledger-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
111
+ target: String(values.overrideTarget),
112
+ scope: String(values.overrideScope)
113
+ }, {
114
+ effectExecutor: (capability) => {
115
+ const retried = applyLedgerIntent(values as LedgerIntent, { manualOverride: capability });
116
+ current = retried;
117
+ return retried.status === 'failed'
118
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'LEDGER_FAILED'}: ${retried.error?.message ?? 'manual ledger effect failed'}` }
119
+ : null;
120
+ }
121
+ });
122
+ humanOverride = consumed;
123
+ return current;
124
+ });
125
+ } catch (error) {
126
+ if (!(error instanceof TaskExecutionLockError)) throw error;
127
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
132
+ const overrideFailed = Boolean(
133
+ humanOverride && typeof humanOverride === 'object' &&
134
+ (humanOverride as { status?: unknown }).status === 'failed'
135
+ );
136
+ if (result.status === 'failed' || overrideFailed) process.exitCode = 1;
93
137
  }
94
138
 
95
139
  export { taskLedger };
@@ -1,14 +1,16 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.ts';
2
3
  import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.ts';
3
4
  import type { TaskLifecycleRequest } from '../task/lifecycle.ts';
4
- import { resolveTaskRef } from '../task/resolve-ref.ts';
5
+ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
5
6
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
6
7
 
7
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
8
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
8
9
  const FLAGS: Record<string, string> = {
9
10
  '--agent': 'agent', '--reason': 'reason', '--unblock-condition': 'unblockCondition',
10
11
  '--note': 'note', '--alert-number': 'alertNumber', '--staging-dir': 'stagingDir',
11
- '--issue-number': 'issueNumber'
12
+ '--issue-number': 'issueNumber', '--override-ticket': 'overrideTicket',
13
+ '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
12
14
  };
13
15
 
14
16
  function usageFailure(message: string): void {
@@ -39,19 +41,61 @@ function taskLifecycle(args: string[] = []): void {
39
41
  const agent = normalizeAgentToken(String(values.agent ?? ''));
40
42
  if (!agent) { usageFailure(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
41
43
  values.agent = agent;
42
- const resolved = resolveTaskRef(String(values.taskRef));
43
- if (!resolved.ok) {
44
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
45
- process.exitCode = 1;
46
- return;
44
+ const dryRunConflict = overrideDryRunConflict(values);
45
+ if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
46
+ let repoRoot: string;
47
+ let taskId: string;
48
+ if (values.intent === 'restore') {
49
+ // restore's precondition is that the task does NOT exist locally yet (it only lives
50
+ // in a staging dir), so resolveTaskRef's existence check is inapplicable here; identity
51
+ // is guaranteed by applyTaskLifecycle's own full-TASK-id check plus staging validation.
52
+ try {
53
+ repoRoot = detectRepoRoot();
54
+ } catch (error) {
55
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'REPO_ROOT_NOT_FOUND', message: error instanceof Error ? error.message : String(error) } })}\n`);
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ taskId = String(values.taskRef);
60
+ } else {
61
+ const resolved = resolveTaskRef(String(values.taskRef));
62
+ if (!resolved.ok) {
63
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ repoRoot = resolved.repoRoot;
68
+ taskId = resolved.taskId;
47
69
  }
48
- let result: ReturnType<typeof applyTaskLifecycle>;
70
+ let result: ReturnType<typeof applyTaskLifecycle> & { humanOverride?: unknown };
49
71
  try {
50
72
  result = withTaskExecutionLock(
51
- resolved.repoRoot,
52
- resolved.taskId,
73
+ repoRoot,
74
+ taskId,
53
75
  `task-lifecycle.${String(values.intent)}`,
54
- () => applyTaskLifecycle(values as TaskLifecycleRequest)
76
+ () => {
77
+ const lifecycleResult = applyTaskLifecycle(values as TaskLifecycleRequest);
78
+ if (lifecycleResult.status !== 'failed' || !values.overrideTicket) return lifecycleResult;
79
+ if (!values.overrideTarget || !values.overrideScope) {
80
+ return {
81
+ ...lifecycleResult,
82
+ error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
83
+ };
84
+ }
85
+ const override = consumeHumanOverride({
86
+ taskRef: String(values.taskRef),
87
+ ticketId: String(values.overrideTicket),
88
+ failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
89
+ target: String(values.overrideTarget),
90
+ scope: String(values.overrideScope),
91
+ intent: String(values.intent) as TaskLifecycleRequest['intent'],
92
+ ...(values.alertNumber ? { alertNumber: Number(values.alertNumber) } : {}),
93
+ ...(values.issueNumber ? { issueNumber: Number(values.issueNumber) } : {}),
94
+ ...(values.stagingDir ? { stagingDir: String(values.stagingDir) } : {})
95
+ });
96
+ if (override.status === 'failed') return { ...lifecycleResult, humanOverride: override };
97
+ return { ...lifecycleResult, ...override, humanOverride: override, error: null };
98
+ }
55
99
  );
56
100
  } catch (error) {
57
101
  if (!(error instanceof TaskExecutionLockError)) throw error;
@@ -247,12 +247,12 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
247
247
  } else if (intent === 'hook-start') {
248
248
  const missing = requireValues([
249
249
  ...(taskRef === 'auto' ? ['--client'] : []),
250
- '--native-agent', '--child-id', '--parent-id', '--spawn-mode'
250
+ '--native-agent', '--child-id', '--parent-id'
251
251
  ]);
252
252
  if (missing) { usageFailure(`intent 'hook-start' requires '${missing}'`); return; }
253
253
  const event = {
254
254
  nativeAgent: values['--native-agent']!, childId: values['--child-id']!,
255
- parentId: values['--parent-id']!, spawnMode: values['--spawn-mode']!,
255
+ parentId: values['--parent-id']!, spawnMode: values['--spawn-mode'],
256
256
  actualModel: values['--actual-model'],
257
257
  actualReasoningEffort: values['--actual-reasoning-effort'],
258
258
  modelFallbackReason: values['--model-fallback-reason'],
@@ -266,7 +266,11 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
266
266
  const missing = requireValues(['--client', '--native-agent', '--child-id']);
267
267
  if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
268
268
  result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
269
- nativeAgent: values['--native-agent']!, childId: values['--child-id']!
269
+ nativeAgent: values['--native-agent']!, childId: values['--child-id']!,
270
+ actualModel: values['--actual-model'],
271
+ actualReasoningEffort: values['--actual-reasoning-effort'],
272
+ modelFallbackReason: values['--model-fallback-reason'],
273
+ reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
270
274
  }, coreOptions);
271
275
  } else {
272
276
  const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
@@ -276,7 +280,11 @@ async function taskOrchestration(args: string[] = []): Promise<void> {
276
280
  result = sealOrchestrationDelegation(taskRef!, {
277
281
  childId: values['--child-id']!, exitCode,
278
282
  afterFingerprint: values['--after-fingerprint']!,
279
- changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
283
+ changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : [],
284
+ actualModel: values['--actual-model'],
285
+ actualReasoningEffort: values['--actual-reasoning-effort'],
286
+ modelFallbackReason: values['--model-fallback-reason'],
287
+ reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
280
288
  }, coreOptions);
281
289
  }
282
290
  } else if (intent === 'advance') {