@fitlab-ai/agent-infra 0.11.12 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
  2. package/dist/lib/internal/sandbox-control.js +1 -81
  3. package/dist/lib/internal/task-finalization.js +26 -19
  4. package/dist/lib/internal/task-lifecycle.js +1 -1
  5. package/dist/lib/platform/github-client.js +33 -3
  6. package/dist/lib/sandbox/commands/create.js +6 -2
  7. package/dist/lib/sandbox/commands/ls.js +4 -5
  8. package/dist/lib/sandbox/commands/rm.js +1 -1
  9. package/dist/lib/sandbox/control/client.js +4 -64
  10. package/dist/lib/sandbox/control/executor.js +4 -50
  11. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  12. package/dist/lib/sandbox/control/protocol.js +5 -16
  13. package/dist/lib/sandbox/control/server.js +203 -234
  14. package/dist/lib/sandbox/control/task-view.js +0 -13
  15. package/dist/lib/sandbox/mounts.js +7 -0
  16. package/dist/lib/sandbox/recovery.js +28 -9
  17. package/dist/lib/sandbox/removal.js +19 -0
  18. package/dist/lib/sandbox/workspace-identity.js +18 -6
  19. package/dist/lib/sandbox/workspace-view.js +48 -2
  20. package/dist/lib/task/commands/ls.js +3 -3
  21. package/dist/lib/task/control-authority.js +2 -0
  22. package/dist/lib/task/control-recovery.js +1 -2
  23. package/dist/lib/task/finalization.js +14 -0
  24. package/dist/lib/task/lifecycle.js +203 -31
  25. package/dist/lib/task/orchestration.js +26 -72
  26. package/dist/lib/task/short-id.js +37 -27
  27. package/dist/lib/task/verification-engine.js +4 -11
  28. package/dist/lifecycle-build-manifest.json +3 -3
  29. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +20 -23
  30. package/lib/internal/sandbox-control.ts +0 -97
  31. package/lib/internal/task-finalization.ts +30 -18
  32. package/lib/internal/task-lifecycle.ts +1 -1
  33. package/lib/platform/github-client.ts +34 -3
  34. package/lib/sandbox/commands/create.ts +6 -1
  35. package/lib/sandbox/commands/ls.ts +4 -5
  36. package/lib/sandbox/commands/rm.ts +1 -1
  37. package/lib/sandbox/control/client.ts +5 -78
  38. package/lib/sandbox/control/executor.ts +2 -53
  39. package/lib/sandbox/control/lifecycle.ts +67 -4
  40. package/lib/sandbox/control/protocol.ts +6 -20
  41. package/lib/sandbox/control/server.ts +9 -44
  42. package/lib/sandbox/control/task-view.ts +0 -19
  43. package/lib/sandbox/mounts.ts +8 -0
  44. package/lib/sandbox/recovery.ts +35 -16
  45. package/lib/sandbox/removal.ts +21 -2
  46. package/lib/sandbox/workspace-identity.ts +16 -6
  47. package/lib/sandbox/workspace-view.ts +41 -2
  48. package/lib/task/commands/ls.ts +3 -3
  49. package/lib/task/control-authority.ts +1 -0
  50. package/lib/task/control-recovery.ts +2 -3
  51. package/lib/task/finalization.ts +14 -0
  52. package/lib/task/lifecycle.ts +180 -27
  53. package/lib/task/orchestration.ts +30 -68
  54. package/lib/task/short-id.ts +33 -40
  55. package/lib/task/verification-engine.ts +3 -9
  56. package/package.json +1 -1
  57. package/templates/.agents/rules/README.en.md +1 -0
  58. package/templates/.agents/rules/README.zh-CN.md +1 -0
  59. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  60. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  61. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  62. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  63. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
  64. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
  65. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  66. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  67. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  68. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
@@ -9,7 +9,7 @@ import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
9
9
  import { toEnginePath } from './engines/wsl2-paths.js';
10
10
  import { sandboxCoreBindMounts } from './mounts.js';
11
11
  import { resolveSandboxGitMetadata } from './git-metadata.js';
12
- import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
12
+ import { assertSandboxTaskSource, sandboxCompletedTaskMountSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
13
13
  import { resolveTaskWorkspace } from './task-resolver.js';
14
14
  import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
15
15
  import { inspectWorktree } from './worktree-safety.js';
@@ -460,25 +460,38 @@ export function startCompletedSandboxContainer(params) {
460
460
  const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
461
461
  const historicalSource = path.join(activeRoot, params.taskId);
462
462
  fs.mkdirSync(activeRoot, { recursive: true });
463
+ let replacedEmptyRoot = false;
464
+ let emptyRootMode = 0o700;
463
465
  try {
464
- fs.lstatSync(historicalSource);
465
- throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
466
+ const existing = fs.lstatSync(historicalSource);
467
+ if (existing.isSymbolicLink() || !existing.isDirectory() || fs.readdirSync(historicalSource).length > 0) {
468
+ throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
469
+ }
470
+ emptyRootMode = existing.mode & 0o777;
471
+ fs.rmdirSync(historicalSource);
472
+ replacedEmptyRoot = true;
466
473
  }
467
474
  catch (error) {
468
475
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
469
476
  throw error;
470
477
  }
471
- fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
478
+ let temporaryLink = false;
472
479
  try {
480
+ fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
481
+ temporaryLink = true;
473
482
  params.start(params.engine, params.container);
474
483
  }
475
484
  finally {
476
- const temporarySource = fs.lstatSync(historicalSource);
477
- const resolvedSource = fs.realpathSync.native(historicalSource);
478
- if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
479
- throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
485
+ if (temporaryLink) {
486
+ const temporarySource = fs.lstatSync(historicalSource);
487
+ const resolvedSource = fs.realpathSync.native(historicalSource);
488
+ if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
489
+ throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
490
+ }
491
+ fs.unlinkSync(historicalSource);
480
492
  }
481
- fs.unlinkSync(historicalSource);
493
+ if (replacedEmptyRoot)
494
+ fs.mkdirSync(historicalSource, { mode: emptyRootMode });
482
495
  }
483
496
  }
484
497
  function sameHostSource(left, right) {
@@ -519,6 +532,7 @@ function expectedMounts(params) {
519
532
  ? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
520
533
  : null;
521
534
  const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
535
+ const completedTaskSource = taskId ? sandboxCompletedTaskMountSource(config.repoRoot, taskId) : null;
522
536
  const worktrees = worktreeDirCandidates(config, branch).filter((candidate) => fs.existsSync(path.join(candidate, '.git')));
523
537
  const gitMetadata = worktrees.length === 1
524
538
  ? resolveSandboxGitMetadata(worktrees[0], control.channelDir)
@@ -538,6 +552,7 @@ function expectedMounts(params) {
538
552
  : {
539
553
  runtimeDir: control.runtimeDir,
540
554
  taskSources: taskSources.accessiblePaths,
555
+ completedTaskSource: completedTaskSource,
541
556
  taskId: taskId
542
557
  })
543
558
  }).map((mount) => ({
@@ -548,6 +563,10 @@ function expectedMounts(params) {
548
563
  && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
549
564
  ? { sourceAccessiblePaths: taskSources.accessiblePaths }
550
565
  : {}),
566
+ ...(completedTaskSource !== null && taskId !== null
567
+ && mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
568
+ ? { sourceAccessiblePaths: [completedTaskSource] }
569
+ : {}),
551
570
  expectedRW: !mount.readOnly
552
571
  }));
553
572
  const live = tools.flatMap((tool) => (tool.hostLiveMounts ?? []).flatMap(({ hostPath, containerSubpath }) => {
@@ -758,11 +758,20 @@ async function rmPurgeCore(config, tools, prompt = {}) {
758
758
  const confirm = prompt.confirm ?? p.confirm;
759
759
  const isCancel = prompt.isCancel ?? p.isCancel;
760
760
  p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
761
+ const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
762
+ const rows = [...listed.running, ...listed.nonRunning];
761
763
  const containers = runEngine(engine, 'docker', [
762
764
  'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
763
765
  ]).split('\n').filter(Boolean);
766
+ const completedTaskIds = [...new Set(rows
767
+ .filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
768
+ .map((row) => row.taskId))];
764
769
  for (const name of containers)
765
770
  runSafeEngine(engine, 'docker', ['rm', '-f', name]);
771
+ if (!sandboxContainersRemoved(engine, containers)) {
772
+ throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${containers.join(', ')}`);
773
+ }
774
+ let cleanupCompleted = true;
766
775
  const worktrees = fs.existsSync(config.worktreeBase)
767
776
  ? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
768
777
  : [];
@@ -773,6 +782,8 @@ async function rmPurgeCore(config, tools, prompt = {}) {
773
782
  fs.rmSync(worktree, { recursive: true, force: true });
774
783
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
775
784
  }
785
+ else
786
+ cleanupCompleted = false;
776
787
  }
777
788
  for (const dir of projectToolDirs(config, tools))
778
789
  fs.rmSync(dir, { recursive: true, force: true });
@@ -780,15 +791,23 @@ async function rmPurgeCore(config, tools, prompt = {}) {
780
791
  const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
781
792
  if (!isCancel(selected) && selected)
782
793
  fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
794
+ else
795
+ cleanupCompleted = false;
783
796
  }
784
797
  if (fs.existsSync(config.shareBase)) {
785
798
  const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
786
799
  if (!isCancel(selected) && selected)
787
800
  fs.rmSync(config.shareBase, { recursive: true, force: true });
801
+ else
802
+ cleanupCompleted = false;
788
803
  }
789
804
  for (const base of [config.workspaceViewBase, config.controlBase]) {
790
805
  fs.rmSync(path.join(base, config.project), { recursive: true, force: true });
791
806
  }
807
+ if (cleanupCompleted) {
808
+ for (const taskId of completedTaskIds)
809
+ releaseStaleShortIdRegistry(config.repoRoot, taskId);
810
+ }
792
811
  const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
793
812
  if (!isCancel(removeImage) && removeImage)
794
813
  runSafeEngine(engine, 'docker', ['rmi', config.imageName]);
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRemovedHashShortIdInput } from '../task/short-id.js';
4
+ import { resolveTaskRef } from '../task/resolve-ref.js';
4
5
  import { resolveTaskWorkspace } from './task-resolver.js';
5
6
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
7
  const SHORT_ID_RE = /^\d+$/;
@@ -38,6 +39,16 @@ function taskIdentity(repoRoot, taskId, registry) {
38
39
  }
39
40
  return { mode: 'task-bound', taskId, shortId: shortIds[0] };
40
41
  }
42
+ function cleanupTaskId(requestedRef, repoRoot) {
43
+ if (TASK_ID_RE.test(requestedRef))
44
+ return requestedRef;
45
+ if (!SHORT_ID_RE.test(requestedRef))
46
+ return null;
47
+ const task = resolveTaskRef(requestedRef, { repoRoot });
48
+ if (!task.ok)
49
+ throw new Error(task.message);
50
+ return task.taskId;
51
+ }
41
52
  export function resolveSandboxTarget(requestedRef, repoRoot) {
42
53
  if (isRemovedHashShortIdInput(requestedRef)) {
43
54
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
@@ -83,26 +94,27 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
83
94
  if (isRemovedHashShortIdInput(requestedRef)) {
84
95
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
85
96
  }
86
- if (TASK_ID_RE.test(requestedRef)) {
97
+ const taskId = cleanupTaskId(requestedRef, repoRoot);
98
+ if (taskId) {
87
99
  let task;
88
100
  try {
89
- task = resolveTaskWorkspace(requestedRef, repoRoot);
101
+ task = resolveTaskWorkspace(taskId, repoRoot);
90
102
  }
91
103
  catch (error) {
92
- if (!(error instanceof Error && error.message === `Task not found: ${requestedRef}`))
104
+ if (!(error instanceof Error && error.message === `Task not found: ${taskId}`))
93
105
  throw error;
94
- const branch = options.resolveMissingTask?.(requestedRef);
106
+ const branch = options.resolveMissingTask?.(taskId);
95
107
  if (!branch)
96
108
  throw error;
97
109
  return {
98
110
  requestedRef,
99
111
  branch,
100
- workspace: { mode: 'task-bound', taskId: requestedRef },
112
+ workspace: { mode: 'task-bound', taskId },
101
113
  taskState: 'unknown'
102
114
  };
103
115
  }
104
116
  if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
105
- throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
117
+ throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
106
118
  }
107
119
  return {
108
120
  requestedRef,
@@ -111,13 +111,16 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
111
111
  }
112
112
  return canonical;
113
113
  }
114
- export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
114
+ export function prepareSandboxWorkspaceMountTargets(worktreeRoot, taskId) {
115
115
  const resolvedWorktreeRoot = path.resolve(worktreeRoot);
116
116
  const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
117
117
  const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
118
118
  const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
119
119
  const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
120
- for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
120
+ const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
121
+ if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId))
122
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
123
+ for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
121
124
  assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
122
125
  }
123
126
  fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
@@ -125,9 +128,52 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
125
128
  fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
126
129
  fs.chmodSync(hostPath, 0o700);
127
130
  }
131
+ if (completedTaskTarget)
132
+ fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
128
133
  fs.chmodSync(workspaceRoot, 0o700);
129
134
  fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
130
135
  }
136
+ export function prepareSandboxCompletedTaskMountSource(repoRoot, taskId) {
137
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
138
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
139
+ const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
140
+ fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
141
+ const completedRoot = path.join(workspaceRoot, 'completed');
142
+ const completedStat = fs.lstatSync(completedRoot);
143
+ if (!completedStat.isDirectory() || completedStat.isSymbolicLink())
144
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
145
+ const target = path.join(completedRoot, taskId);
146
+ try {
147
+ const stat = fs.lstatSync(target);
148
+ if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(target).length > 0) {
149
+ throw new Error(`SANDBOX_COMPLETED_MOUNT_CONFLICT: ${target}`);
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (error.code !== 'ENOENT')
154
+ throw error;
155
+ fs.mkdirSync(target, { mode: 0o700 });
156
+ }
157
+ return sandboxCompletedTaskMountSource(repoRoot, taskId);
158
+ }
159
+ export function sandboxCompletedTaskMountSource(repoRoot, taskId) {
160
+ if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
161
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
162
+ const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
163
+ const stat = fs.lstatSync(source);
164
+ if (!stat.isDirectory() || stat.isSymbolicLink())
165
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
166
+ const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
167
+ const canonicalSource = fs.realpathSync.native(source);
168
+ const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
169
+ const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
170
+ const relative = path.relative(canonicalWorkspace, canonicalSource);
171
+ if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
172
+ || path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
173
+ || relative.startsWith('..') || path.isAbsolute(relative))
174
+ throw new Error('SANDBOX_TASK_SOURCE_INVALID');
175
+ return canonicalSource;
176
+ }
131
177
  export function materializeSandboxWorkspaceView(params) {
132
178
  const projectRoot = path.resolve(params.base, params.project);
133
179
  const { root } = sandboxWorkspaceViewPaths(params);
@@ -58,9 +58,9 @@ function collectTasks(repoRoot, state) {
58
58
  const dir = path.join(repoRoot, '.agents', 'workspace', state);
59
59
  if (!fs.existsSync(dir))
60
60
  return [];
61
- // Short ids live only in the registry and only for active tasks; archived
62
- // (blocked/completed) tasks have released their short id and render '-'.
63
- const shortIdByTaskId = state === 'active' ? loadShortIdByTaskId(repoRoot) : new Map();
61
+ const shortIdByTaskId = state === 'active' || state === 'completed'
62
+ ? loadShortIdByTaskId(repoRoot)
63
+ : new Map();
64
64
  const rows = [];
65
65
  for (const entry of fs.readdirSync(dir).sort()) {
66
66
  if (!TASK_ID_RE.test(entry))
@@ -320,6 +320,8 @@ export function parseTaskControlOperation(family, args) {
320
320
  const taskRef = args[0];
321
321
  const intent = args[1];
322
322
  if (family === 'task-lifecycle') {
323
+ if (intent === 'complete')
324
+ operationInvalid('complete must use task-finalization');
323
325
  const values = parseValues(args, 2, LIFECYCLE_FLAGS);
324
326
  const agent = normalizeAgentToken(value(values, '--agent') ?? '');
325
327
  if (!agent)
@@ -16,10 +16,9 @@ export function parseControlOutput(output) {
16
16
  export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
17
17
  'completed', 'evidence-written', 'publish-authorized'
18
18
  ]);
19
- const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
19
+ const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'];
20
20
  export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
21
21
  ...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
22
- { family: 'task-finalization', intent: 'complete', class: 'finalization' },
23
22
  { family: 'task-create', intent: 'create', class: 'task-create' },
24
23
  ...['controller.close', 'controller.verify'].map((intent) => ({ family: 'agent-client', intent, class: 'agent-client-operation' }))
25
24
  ]);
@@ -1133,6 +1133,20 @@ async function applyTaskFinalization(request, options) {
1133
1133
  const prepared = await prepareTaskFinalization(request, options);
1134
1134
  if (prepared.status !== 'prepared')
1135
1135
  return prepared;
1136
+ if (options.beforeLifecycle) {
1137
+ try {
1138
+ await options.beforeLifecycle(prepared.taskId);
1139
+ }
1140
+ catch (error) {
1141
+ const detail = errorOf(error, 'TASK_FINALIZATION_PRE_LIFECYCLE_FAILED', true);
1142
+ return failed(prepared.taskId, detail, {
1143
+ changed: prepared.changed,
1144
+ completedSteps: prepared.completedSteps,
1145
+ pendingSteps: prepared.pendingSteps,
1146
+ warnings: prepared.warnings
1147
+ });
1148
+ }
1149
+ }
1136
1150
  return commitPreparedTaskFinalization(request, options);
1137
1151
  }
1138
1152
  export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };