@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
@@ -132,6 +132,11 @@ else
132
132
  taskLifecycle(process.argv.slice(3));
133
133
  break;
134
134
  }
135
+ case 'task-override': {
136
+ const { taskOverride } = await import('../lib/internal/task-override.js');
137
+ taskOverride(process.argv.slice(3));
138
+ break;
139
+ }
135
140
  case 'task-short-id': {
136
141
  const { taskShortId } = await import('../lib/internal/task-short-id.js');
137
142
  taskShortId(process.argv.slice(3));
@@ -11,7 +11,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
11
11
  commands: { level: 'integrated' },
12
12
  hooks: { level: 'integrated' },
13
13
  subagents: { level: 'experimental' },
14
- orchestration: { level: 'unsupported' },
14
+ orchestration: { level: 'experimental' },
15
15
  sandbox: { level: 'integrated' },
16
16
  verification: { level: 'compatible' }
17
17
  },
@@ -21,8 +21,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
21
21
  guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
22
22
  },
23
23
  delegationEvidence: {
24
- actualModel: 'unavailable',
25
- actualReasoningEffort: 'unavailable'
24
+ actualModel: 'host-event',
25
+ actualReasoningEffort: 'spawn-ack'
26
26
  },
27
27
  project: {
28
28
  ownedPathPrefixes: ['.claude/'],
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
7
8
  const LIFECYCLE_HOOKS = Object.freeze([
8
9
  Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
9
10
  Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
@@ -482,7 +483,7 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
482
483
  fs.rmSync(schemaDir, { recursive: true, force: true });
483
484
  }
484
485
  const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
485
- const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
486
+ const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
486
487
  const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
487
488
  const transport = new CodexAppServerTransport();
488
489
  transport.start();
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { verifyLifecycleBuildIdentity } from './build-identity.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
5
6
  const DEFAULT_TTL_MS = 30_000;
6
7
  const DEFAULT_TOMBSTONE_MS = 86_400_000;
7
8
  const DEFAULT_MAX_RECORDS = 128;
@@ -22,7 +23,7 @@ function sameController(left, right) {
22
23
  }
23
24
  function createCodexCapabilityStore(options = {}) {
24
25
  const root = options.root
25
- ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
26
+ ?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
26
27
  const now = options.now ?? Date.now;
27
28
  const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
28
29
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
@@ -38,12 +38,39 @@ function controlFromEnvironment() {
38
38
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
39
39
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
40
40
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
41
- if (!token || !generation || !channelDir || !statusDir) {
41
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
42
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
42
43
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
43
44
  }
44
- return { token, generation, channelDir, statusDir };
45
+ return { token, generation, channelDir, statusDir, runtimeDir };
46
+ }
47
+ function verifyRuntime(runtimeDir) {
48
+ let stat;
49
+ try {
50
+ stat = fs.lstatSync(runtimeDir);
51
+ }
52
+ catch {
53
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
54
+ }
55
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
56
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
57
+ }
58
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
59
+ try {
60
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
61
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
62
+ throw new Error('probe mismatch');
63
+ }
64
+ }
65
+ catch {
66
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
67
+ }
68
+ finally {
69
+ fs.rmSync(probe, { force: true });
70
+ }
45
71
  }
46
72
  function verifyControl(taskId, control) {
73
+ verifyRuntime(control.runtimeDir);
47
74
  let status = null;
48
75
  let lastError = null;
49
76
  for (let attempt = 0; attempt < 5; attempt += 1) {
@@ -153,6 +180,7 @@ function isolatedEnvironment(home, shimDir, contextPath, taskId, control, provid
153
180
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
154
181
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
155
182
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
183
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
156
184
  AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
157
185
  };
158
186
  }
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
5
6
  const LOCK_RETRY_MS = 10;
6
7
  const LOCK_TIMEOUT_MS = 1_000;
7
8
  const LOCK_STALE_MS = 30_000;
@@ -58,7 +59,7 @@ function writeRecord(file, record, expectedRevision) {
58
59
  }
59
60
  }
60
61
  function createCodexLifecycleStore(options) {
61
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
62
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
62
63
  const now = options.now ?? (() => new Date().toISOString());
63
64
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
64
65
  fs.chmodSync(root, 0o700);
@@ -2,25 +2,44 @@ import { VERSION } from './version.js';
2
2
  import { parseTaskScope } from './task/command-options.js';
3
3
  import { applyHumanDecision } from './task/decision-intents.js';
4
4
  import { canonicalTimestamp } from './task/write.js';
5
+ import { consumeHumanOverride, failureId } from './task/human-override.js';
6
+ import { resolveTaskRef } from './task/resolve-ref.js';
7
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task/task-execution-lock.js';
5
8
  function parseDecisionParts(parts) {
6
9
  const decision = [];
7
10
  let needsImplementation;
11
+ let overrideTicket;
12
+ let overrideTarget;
13
+ let overrideScope;
8
14
  for (let index = 0; index < parts.length; index += 1) {
9
- if (parts[index] !== '--needs-implementation') {
10
- decision.push(parts[index]);
11
- continue;
15
+ const part = parts[index];
16
+ if (part === '--needs-implementation') {
17
+ if (needsImplementation !== undefined)
18
+ throw new Error("duplicate option '--needs-implementation'");
19
+ const value = parts[++index];
20
+ if (value !== 'true' && value !== 'false') {
21
+ throw new Error("--needs-implementation must be 'true' or 'false'");
22
+ }
23
+ needsImplementation = value === 'true';
12
24
  }
13
- if (needsImplementation !== undefined)
14
- throw new Error("duplicate option '--needs-implementation'");
15
- const value = parts[++index];
16
- if (value !== 'true' && value !== 'false') {
17
- throw new Error("--needs-implementation must be 'true' or 'false'");
25
+ else if (part === '--override-ticket' || part === '--override-target' || part === '--override-scope') {
26
+ const value = parts[++index];
27
+ if (!value)
28
+ throw new Error(`${part} requires a value`);
29
+ if (part === '--override-ticket')
30
+ overrideTicket = value;
31
+ else if (part === '--override-target')
32
+ overrideTarget = value;
33
+ else
34
+ overrideScope = value;
35
+ }
36
+ else {
37
+ decision.push(parts[index]);
18
38
  }
19
- needsImplementation = value === 'true';
20
39
  }
21
40
  if (decision.length === 0)
22
41
  throw new Error('decision content is required');
23
- return { decision: decision.join(' '), needsImplementation };
42
+ return { decision: decision.join(' '), needsImplementation, overrideTicket, overrideTarget, overrideScope };
24
43
  }
25
44
  export async function decide(args, options = {}) {
26
45
  let scope;
@@ -79,19 +98,54 @@ export async function decide(args, options = {}) {
79
98
  try {
80
99
  const parsedDecision = parseDecisionParts(decisionParts);
81
100
  const now = (options.now ?? canonicalTimestamp)();
82
- const result = applyHumanDecision({
101
+ const request = {
83
102
  taskRef, selector, decision: parsedDecision.decision,
84
103
  needsImplementation: parsedDecision.needsImplementation
85
- }, {
104
+ };
105
+ const writeOptions = {
86
106
  repoRoot: options.repoRoot,
87
107
  metadataProvider: () => ({ timestamp: now, agentInfraVersion: options.version ?? VERSION })
88
- });
108
+ };
109
+ const resolved = taskRef ? resolveTaskRef(taskRef, { repoRoot: options.repoRoot }) : null;
110
+ let result;
111
+ const execute = () => {
112
+ let current = applyHumanDecision(request, writeOptions);
113
+ if (current.status !== 'failed' || !parsedDecision.overrideTicket)
114
+ return current;
115
+ if (!parsedDecision.overrideTarget || !parsedDecision.overrideScope)
116
+ throw new Error('override ticket requires --override-target and --override-scope');
117
+ const consumed = consumeHumanOverride({
118
+ taskRef: resolved?.ok ? resolved.taskId : String(taskRef ?? ''),
119
+ ticketId: parsedDecision.overrideTicket,
120
+ failureId: failureId('decision-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
121
+ target: parsedDecision.overrideTarget,
122
+ scope: parsedDecision.overrideScope
123
+ }, {
124
+ ...writeOptions,
125
+ effectExecutor: (capability) => {
126
+ const retried = applyHumanDecision(request, { ...writeOptions, manualOverride: capability });
127
+ current = retried;
128
+ return retried.status === 'failed' || retried.status === 'planned'
129
+ ? { 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'}` }
130
+ : null;
131
+ }
132
+ });
133
+ if (consumed.status === 'failed')
134
+ throw new Error(consumed.error.message);
135
+ return current;
136
+ };
137
+ result = resolved?.ok
138
+ ? withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-decision', execute)
139
+ : execute();
89
140
  if (result.error)
90
141
  throw new Error(result.error.message);
91
142
  return 0;
92
143
  }
93
144
  catch (error) {
94
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
145
+ const message = error instanceof TaskExecutionLockError
146
+ ? `${error.code}: ${error.message}`
147
+ : error instanceof Error ? error.message : String(error);
148
+ process.stderr.write(`Error: ${message}\n`);
95
149
  return 1;
96
150
  }
97
151
  }
package/dist/lib/init.js CHANGED
@@ -180,7 +180,7 @@ async function cmdInit() {
180
180
  agentClients: serializeAgentClients(defaultState)
181
181
  };
182
182
  if (sandboxEngine)
183
- config.sandbox.engine = sandboxEngine;
183
+ config.sandbox.engine = { [currentPlatform]: sandboxEngine };
184
184
  if (templateSources.length > 0)
185
185
  config.templates = { sources: templateSources };
186
186
  if (skillSources.length > 0)
@@ -21,9 +21,7 @@ const DEMO_INPUT_PATHS = [
21
21
  'lib/paths.ts',
22
22
  'lib/render.ts',
23
23
  'lib/builtin-tuis.ts',
24
- 'lib/sandbox/engines/',
25
- 'src/sync-templates.js',
26
- 'templates/'
24
+ 'lib/sandbox/engines/'
27
25
  ];
28
26
  const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
29
27
  const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';
@@ -1,4 +1,7 @@
1
1
  import { applyPrReviewActivityIntent, inspectPrReviewActivity } from '../task/activity-intent.js';
2
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
3
+ import { resolveTaskRef } from '../task/resolve-ref.js';
4
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
2
5
  const USAGE = `Usage: agent-infra-internal task-activity <task-ref> pr-review-inspect
3
6
  agent-infra-internal task-activity <task-ref> pr-review-start --agent <agent> --artifact <canonical.md> --head <40hex> [--dry-run]
4
7
  agent-infra-internal task-activity <task-ref> pr-review-complete --agent <agent> --artifact <canonical.md> --head <40hex> --verdict <approved|changes-requested|commented> --blockers <N> --major <N> --minor <N> [--dry-run]
@@ -14,14 +17,15 @@ const FLAGS = {
14
17
  '--major': 'major',
15
18
  '--minor': 'minor',
16
19
  '--outcome': 'outcome',
17
- '--reason': 'reason'
20
+ '--reason': 'reason',
21
+ '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
18
22
  };
19
23
  const COMMON = ['agent', 'artifact', 'head'];
20
24
  const ALLOWED = {
21
25
  'pr-review-inspect': new Set(['kind', 'taskRef']),
22
- 'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun']),
23
- 'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun']),
24
- 'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun'])
26
+ 'pr-review-start': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
27
+ 'pr-review-complete': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'verdict', 'blockers', 'major', 'minor', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']),
28
+ 'pr-review-terminate': new Set(['kind', 'taskRef', 'agent', 'artifact', 'head', 'outcome', 'reason', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope'])
25
29
  };
26
30
  function usageFailure(message) {
27
31
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'ACTIVITY_PAYLOAD_INVALID', message } })}\n`);
@@ -96,9 +100,55 @@ function taskActivity(args = []) {
96
100
  return;
97
101
  }
98
102
  }
99
- const result = applyPrReviewActivityIntent(values);
100
- process.stdout.write(`${JSON.stringify(result)}\n`);
101
- if (result.status === 'failed')
103
+ const dryRunConflict = overrideDryRunConflict(values);
104
+ if (dryRunConflict) {
105
+ usageFailure(dryRunConflict.message);
106
+ return;
107
+ }
108
+ const resolved = resolveTaskRef(taskRef);
109
+ if (!resolved.ok) {
110
+ usageFailure(resolved.message);
111
+ return;
112
+ }
113
+ let result;
114
+ let humanOverride = null;
115
+ try {
116
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-activity.${kind}`, () => {
117
+ let current = applyPrReviewActivityIntent(values, { lockAlreadyHeld: true });
118
+ if (current.status !== 'failed' || !values.overrideTicket)
119
+ return current;
120
+ if (!values.overrideTarget || !values.overrideScope) {
121
+ usageFailure('override ticket requires target and scope');
122
+ return current;
123
+ }
124
+ const consumed = consumeHumanOverride({
125
+ taskRef, ticketId: String(values.overrideTicket),
126
+ failureId: failureId('activity-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
127
+ target: String(values.overrideTarget), scope: String(values.overrideScope)
128
+ }, {
129
+ effectExecutor: (capability) => {
130
+ const retried = applyPrReviewActivityIntent(values, { lockAlreadyHeld: true, manualOverride: capability });
131
+ current = retried;
132
+ return retried.status === 'failed' || retried.status === 'planned'
133
+ ? { 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'}` }
134
+ : null;
135
+ }
136
+ });
137
+ humanOverride = consumed;
138
+ return current;
139
+ });
140
+ }
141
+ catch (error) {
142
+ if (!(error instanceof TaskExecutionLockError))
143
+ throw error;
144
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
149
+ const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
150
+ humanOverride.status === 'failed');
151
+ if (result.status === 'failed' || overrideFailed)
102
152
  process.exitCode = 1;
103
153
  }
104
154
  export { taskActivity };
@@ -1,5 +1,8 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
2
  import { applyTaskEvent, eventCatalog } from '../task/events.js';
3
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
4
+ import { resolveTaskRef } from '../task/resolve-ref.js';
5
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
3
6
  const USAGE = `Usage: agent-infra-internal task-event <N | TASK-id> <event> --agent <agent> [event options] [--orchestrated] [--dry-run]
4
7
 
5
8
  Apply one closed-set task lifecycle event and print a structured JSON result.
@@ -10,7 +13,8 @@ const FLAGS = {
10
13
  '--fix-for': 'fixFor', '--implementation-input': 'implementationInput',
11
14
  '--verdict': 'verdict', '--blockers': 'blockers', '--major': 'major',
12
15
  '--minor': 'minor', '--manual-validation': 'manualValidation', '--files-modified': 'filesModified',
13
- '--tests-passed': 'testsPassed', '--summary-result': 'summaryResult'
16
+ '--tests-passed': 'testsPassed', '--summary-result': 'summaryResult',
17
+ '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
14
18
  };
15
19
  const NUMERIC = new Set(['round', 'question', 'blockers', 'major', 'minor', 'manualValidation', 'filesModified', 'testsPassed']);
16
20
  function usageFailure(message) {
@@ -77,9 +81,58 @@ function taskEvent(args = []) {
77
81
  return;
78
82
  }
79
83
  request.agent = agent;
80
- const result = applyTaskEvent(request);
81
- process.stdout.write(`${JSON.stringify(result)}\n`);
82
- if (result.status === 'failed')
84
+ const dryRunConflict = overrideDryRunConflict(request);
85
+ if (dryRunConflict) {
86
+ usageFailure(dryRunConflict.message);
87
+ return;
88
+ }
89
+ const resolved = resolveTaskRef(request.taskRef);
90
+ if (!resolved.ok) {
91
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
92
+ process.exitCode = 1;
93
+ return;
94
+ }
95
+ let result;
96
+ let humanOverride = null;
97
+ try {
98
+ result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-event.${request.event}`, () => {
99
+ let current = applyTaskEvent(request, { lockAlreadyHeld: true });
100
+ const values = request;
101
+ if (current.status !== 'failed' || !values.overrideTicket)
102
+ return current;
103
+ if (!values.overrideTarget || !values.overrideScope) {
104
+ return { ...current, humanOverride: { status: 'failed', error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires target and scope' } } };
105
+ }
106
+ const consumed = consumeHumanOverride({
107
+ taskRef: request.taskRef,
108
+ ticketId: String(values.overrideTicket),
109
+ failureId: failureId('task-event', current.error?.code ?? 'EVENT_TRANSITION_INVALID'),
110
+ target: String(values.overrideTarget),
111
+ scope: String(values.overrideScope)
112
+ }, {
113
+ effectExecutor: (capability) => {
114
+ const retried = applyTaskEvent(request, { lockAlreadyHeld: true, manualOverride: capability });
115
+ current = retried;
116
+ return retried.status === 'failed' || retried.status === 'planned'
117
+ ? { 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'}` }
118
+ : null;
119
+ }
120
+ });
121
+ humanOverride = consumed;
122
+ return current;
123
+ });
124
+ }
125
+ catch (error) {
126
+ if (!(error instanceof TaskExecutionLockError))
127
+ throw error;
128
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
133
+ const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
134
+ humanOverride.status === 'failed');
135
+ if (result.status === 'failed' || overrideFailed)
83
136
  process.exitCode = 1;
84
137
  }
85
138
  export { taskEvent };
@@ -1,12 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import { applyLedgerIntent } from '../task/ledger-intents.js';
3
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
3
4
  import { isReviewStage, parseLedger, summarizeLedgerStage, validateLedgerRows } from '../task/ledger.js';
4
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
6
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
5
7
  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`;
6
8
  const FLAGS = {
7
9
  '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
8
10
  '--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
9
- '--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
11
+ '--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation',
12
+ '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
10
13
  };
11
14
  const NUMERIC = new Set(['ordinal', 'round']);
12
15
  const BOOLEAN = new Set(['needsImplementation']);
@@ -75,7 +78,10 @@ function taskLedger(args = []) {
75
78
  'finding-review': ['needsImplementation'],
76
79
  'decision-upsert': ['needsImplementation']
77
80
  };
78
- const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...(optional[kind] ?? [])]);
81
+ const overrideFields = kind === 'stage-status' || kind === 'decision-next-id'
82
+ ? []
83
+ : ['overrideTicket', 'overrideTarget', 'overrideScope'];
84
+ const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...(optional[kind] ?? []), ...overrideFields]);
79
85
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
80
86
  const missing = required[kind].find((key) => values[key] === undefined);
81
87
  if (unexpected) {
@@ -86,6 +92,11 @@ function taskLedger(args = []) {
86
92
  usageFailure(`${kind} requires '${missing}'`);
87
93
  return;
88
94
  }
95
+ const dryRunConflict = overrideDryRunConflict(values);
96
+ if (dryRunConflict) {
97
+ usageFailure(dryRunConflict.message);
98
+ return;
99
+ }
89
100
  if (kind === 'stage-status') {
90
101
  if (!isReviewStage(String(values.stage))) {
91
102
  usageFailure(`stage-status requires a valid review stage`);
@@ -114,9 +125,52 @@ function taskLedger(args = []) {
114
125
  }
115
126
  return;
116
127
  }
117
- const result = applyLedgerIntent(values);
118
- process.stdout.write(`${JSON.stringify(result)}\n`);
119
- if (result.status === 'failed')
128
+ const lockResolved = resolveTaskRef(taskRef);
129
+ if (!lockResolved.ok) {
130
+ usageFailure(lockResolved.message);
131
+ return;
132
+ }
133
+ let result;
134
+ let humanOverride = null;
135
+ try {
136
+ result = withTaskExecutionLock(lockResolved.repoRoot, lockResolved.taskId, `task-ledger.${kind}`, () => {
137
+ let current = applyLedgerIntent(values);
138
+ if (current.status !== 'failed' || !values.overrideTicket)
139
+ return current;
140
+ if (!values.overrideTarget || !values.overrideScope) {
141
+ usageFailure('override ticket requires target and scope');
142
+ return current;
143
+ }
144
+ const consumed = consumeHumanOverride({
145
+ taskRef,
146
+ ticketId: String(values.overrideTicket),
147
+ failureId: failureId('ledger-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
148
+ target: String(values.overrideTarget),
149
+ scope: String(values.overrideScope)
150
+ }, {
151
+ effectExecutor: (capability) => {
152
+ const retried = applyLedgerIntent(values, { manualOverride: capability });
153
+ current = retried;
154
+ return retried.status === 'failed'
155
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'LEDGER_FAILED'}: ${retried.error?.message ?? 'manual ledger effect failed'}` }
156
+ : null;
157
+ }
158
+ });
159
+ humanOverride = consumed;
160
+ return current;
161
+ });
162
+ }
163
+ catch (error) {
164
+ if (!(error instanceof TaskExecutionLockError))
165
+ throw error;
166
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
167
+ process.exitCode = 1;
168
+ return;
169
+ }
170
+ process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
171
+ const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
172
+ humanOverride.status === 'failed');
173
+ if (result.status === 'failed' || overrideFailed)
120
174
  process.exitCode = 1;
121
175
  }
122
176
  export { taskLedger };
@@ -1,12 +1,14 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
+ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
2
3
  import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.js';
3
- import { resolveTaskRef } from '../task/resolve-ref.js';
4
+ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
5
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
5
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
6
+ 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`;
6
7
  const FLAGS = {
7
8
  '--agent': 'agent', '--reason': 'reason', '--unblock-condition': 'unblockCondition',
8
9
  '--note': 'note', '--alert-number': 'alertNumber', '--staging-dir': 'stagingDir',
9
- '--issue-number': 'issueNumber'
10
+ '--issue-number': 'issueNumber', '--override-ticket': 'overrideTicket',
11
+ '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
10
12
  };
11
13
  function usageFailure(message) {
12
14
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
@@ -58,15 +60,64 @@ function taskLifecycle(args = []) {
58
60
  return;
59
61
  }
60
62
  values.agent = agent;
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;
63
+ const dryRunConflict = overrideDryRunConflict(values);
64
+ if (dryRunConflict) {
65
+ usageFailure(dryRunConflict.message);
65
66
  return;
66
67
  }
68
+ let repoRoot;
69
+ let taskId;
70
+ if (values.intent === 'restore') {
71
+ // restore's precondition is that the task does NOT exist locally yet (it only lives
72
+ // in a staging dir), so resolveTaskRef's existence check is inapplicable here; identity
73
+ // is guaranteed by applyTaskLifecycle's own full-TASK-id check plus staging validation.
74
+ try {
75
+ repoRoot = detectRepoRoot();
76
+ }
77
+ catch (error) {
78
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'REPO_ROOT_NOT_FOUND', message: error instanceof Error ? error.message : String(error) } })}\n`);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ taskId = String(values.taskRef);
83
+ }
84
+ else {
85
+ const resolved = resolveTaskRef(String(values.taskRef));
86
+ if (!resolved.ok) {
87
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+ repoRoot = resolved.repoRoot;
92
+ taskId = resolved.taskId;
93
+ }
67
94
  let result;
68
95
  try {
69
- result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-lifecycle.${String(values.intent)}`, () => applyTaskLifecycle(values));
96
+ result = withTaskExecutionLock(repoRoot, taskId, `task-lifecycle.${String(values.intent)}`, () => {
97
+ const lifecycleResult = applyTaskLifecycle(values);
98
+ if (lifecycleResult.status !== 'failed' || !values.overrideTicket)
99
+ return lifecycleResult;
100
+ if (!values.overrideTarget || !values.overrideScope) {
101
+ return {
102
+ ...lifecycleResult,
103
+ error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
104
+ };
105
+ }
106
+ const override = consumeHumanOverride({
107
+ taskRef: String(values.taskRef),
108
+ ticketId: String(values.overrideTicket),
109
+ failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
110
+ target: String(values.overrideTarget),
111
+ scope: String(values.overrideScope),
112
+ intent: String(values.intent),
113
+ ...(values.alertNumber ? { alertNumber: Number(values.alertNumber) } : {}),
114
+ ...(values.issueNumber ? { issueNumber: Number(values.issueNumber) } : {}),
115
+ ...(values.stagingDir ? { stagingDir: String(values.stagingDir) } : {})
116
+ });
117
+ if (override.status === 'failed')
118
+ return { ...lifecycleResult, humanOverride: override };
119
+ return { ...lifecycleResult, ...override, humanOverride: override, error: null };
120
+ });
70
121
  }
71
122
  catch (error) {
72
123
  if (!(error instanceof TaskExecutionLockError))