@fitlab-ai/agent-infra 0.11.7 → 0.11.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 (95) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/internal/cli-route-inventory.js +3 -0
  6. package/dist/lib/internal/platform-pr-review.js +20 -3
  7. package/dist/lib/internal/task-operation-registry.js +3 -1
  8. package/dist/lib/internal/task-platform-recovery.js +56 -0
  9. package/dist/lib/merge.js +25 -5
  10. package/dist/lib/platform/completion-backfill.js +2 -1
  11. package/dist/lib/platform/in-label-sync.js +12 -1
  12. package/dist/lib/platform/issue-comments.js +58 -4
  13. package/dist/lib/platform/issues.js +105 -5
  14. package/dist/lib/platform/operation-coordinator.js +74 -0
  15. package/dist/lib/platform/pr-review.js +107 -1
  16. package/dist/lib/platform/pr-summary.js +79 -57
  17. package/dist/lib/platform/pull-requests.js +96 -4
  18. package/dist/lib/sandbox/commands/create.js +6 -3
  19. package/dist/lib/sandbox/commands/ls.js +4 -3
  20. package/dist/lib/sandbox/commands/rm.js +1 -1
  21. package/dist/lib/sandbox/config.js +22 -4
  22. package/dist/lib/sandbox/git-metadata.js +50 -0
  23. package/dist/lib/sandbox/index.js +1 -1
  24. package/dist/lib/sandbox/mounts.js +12 -0
  25. package/dist/lib/sandbox/recovery.js +11 -0
  26. package/dist/lib/sandbox/removal.js +58 -20
  27. package/dist/lib/sandbox/tools.js +29 -7
  28. package/dist/lib/task/activity-intent.js +4 -3
  29. package/dist/lib/task/create-service.js +5 -3
  30. package/dist/lib/task/create.js +22 -5
  31. package/dist/lib/task/finalization-artifacts.js +4 -1
  32. package/dist/lib/task/finalization.js +157 -24
  33. package/dist/lib/task/lifecycle.js +38 -2
  34. package/dist/lib/task/platform-operation-journal.js +152 -0
  35. package/dist/lib/task/platform-operation-recovery.js +459 -0
  36. package/dist/lib/task/workflow-warning-intents.js +7 -1
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex.ts +1 -1
  39. package/lib/agent-clients/config.ts +9 -3
  40. package/lib/defaults.json +6 -3
  41. package/lib/internal/cli-route-inventory.ts +2 -0
  42. package/lib/internal/platform-pr-review.ts +14 -3
  43. package/lib/internal/task-operation-registry.ts +3 -1
  44. package/lib/internal/task-platform-recovery.ts +53 -0
  45. package/lib/merge.ts +26 -6
  46. package/lib/platform/completion-backfill.ts +2 -1
  47. package/lib/platform/in-label-sync.ts +12 -0
  48. package/lib/platform/issue-comments.ts +59 -3
  49. package/lib/platform/issues.ts +103 -5
  50. package/lib/platform/operation-coordinator.ts +96 -0
  51. package/lib/platform/pr-review.ts +113 -1
  52. package/lib/platform/pr-summary.ts +22 -1
  53. package/lib/platform/pull-requests.ts +105 -5
  54. package/lib/sandbox/commands/create.ts +7 -8
  55. package/lib/sandbox/commands/ls.ts +4 -3
  56. package/lib/sandbox/commands/rm.ts +1 -1
  57. package/lib/sandbox/config.ts +23 -5
  58. package/lib/sandbox/git-metadata.ts +66 -0
  59. package/lib/sandbox/index.ts +1 -1
  60. package/lib/sandbox/mounts.ts +14 -0
  61. package/lib/sandbox/recovery.ts +13 -0
  62. package/lib/sandbox/removal.ts +57 -19
  63. package/lib/sandbox/tool-types.ts +1 -1
  64. package/lib/sandbox/tools.ts +36 -7
  65. package/lib/task/activity-intent.ts +4 -3
  66. package/lib/task/create-service.ts +5 -3
  67. package/lib/task/create.ts +17 -5
  68. package/lib/task/finalization-artifacts.ts +4 -1
  69. package/lib/task/finalization.ts +154 -21
  70. package/lib/task/lifecycle.ts +35 -3
  71. package/lib/task/platform-operation-journal.ts +230 -0
  72. package/lib/task/platform-operation-recovery.ts +435 -0
  73. package/lib/task/workflow-warning-intents.ts +9 -1
  74. package/package.json +1 -1
  75. package/templates/.agents/README.en.md +19 -17
  76. package/templates/.agents/README.zh-CN.md +18 -16
  77. package/templates/.agents/rules/README.en.md +1 -1
  78. package/templates/.agents/rules/README.zh-CN.md +1 -1
  79. package/templates/.agents/rules/task-management.en.md +12 -0
  80. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  81. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  82. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  83. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  87. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  88. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  89. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  90. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  91. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  92. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  93. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  94. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  95. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
@@ -24,6 +24,7 @@ import type {
24
24
  import type { SandboxConfig } from './config.ts';
25
25
  import { toEnginePath } from './engines/wsl2-paths.ts';
26
26
  import { sandboxCoreBindMounts } from './mounts.ts';
27
+ import { resolveSandboxGitMetadata } from './git-metadata.ts';
27
28
  import {
28
29
  assertSandboxTaskSource,
29
30
  sandboxControlPaths,
@@ -764,7 +765,19 @@ function expectedMounts(params: {
764
765
  ? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
765
766
  : null;
766
767
  const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
768
+ const worktrees = worktreeDirCandidates(config, branch).filter((candidate) =>
769
+ fs.existsSync(path.join(candidate, '.git'))
770
+ );
771
+ const gitMetadata = worktrees.length === 1
772
+ ? resolveSandboxGitMetadata(worktrees[0]!, control.channelDir)
773
+ : null;
767
774
  const core = sandboxCoreBindMounts(config, branch, {
775
+ ...(gitMetadata === null
776
+ ? {}
777
+ : {
778
+ gitCommonDir: gitMetadata.commonDir,
779
+ worktreeGitFile: gitMetadata.worktreeGitFile
780
+ }),
768
781
  workspaceViewRoot: view.root,
769
782
  controlDir: control.channelDir,
770
783
  controlStatusDir: control.statusDir,
@@ -22,6 +22,7 @@ import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './sh
22
22
  import {
23
23
  parseSandboxWorkspaceIdentity,
24
24
  resolveSandboxCleanupTarget,
25
+ resolveSandboxTarget,
25
26
  sameSandboxWorkspaceIdentity,
26
27
  type SandboxCleanupTarget,
27
28
  type SandboxWorkspaceKey
@@ -1112,7 +1113,10 @@ async function rmUnbound(
1112
1113
  tools: SandboxTool[],
1113
1114
  options: { dryRun: boolean; assumeYes: boolean }
1114
1115
  ): Promise<void> {
1115
- return rmUnboundCore(config, tools, options);
1116
+ return withRepositoryMutationLock(
1117
+ config.repoRoot,
1118
+ () => rmUnboundCore(config, tools, options)
1119
+ );
1116
1120
  }
1117
1121
 
1118
1122
  async function rmUnboundCore(
@@ -1127,32 +1131,66 @@ async function rmUnboundCore(
1127
1131
  sandboxBranchLabel(config),
1128
1132
  { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) }
1129
1133
  );
1130
- const rows = [...listed.running, ...listed.nonRunning];
1134
+ const discovered = [...listed.running, ...listed.nonRunning];
1135
+ const branchMismatches: Array<{ container: string; labelBranch: string; taskBranch: string }> = [];
1136
+ const classified = discovered.map((row) => {
1137
+ if (!row.branch) return { row, protected: true, cleanupTarget: null };
1138
+ if (row.workspaceMode === 'task-bound' && row.taskId) {
1139
+ try {
1140
+ resolveSandboxTarget(row.taskId, config.repoRoot);
1141
+ return { row, protected: true, cleanupTarget: null };
1142
+ } catch {
1143
+ try {
1144
+ const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
1145
+ if (cleanupTarget.taskState === 'blocked'
1146
+ || cleanupTarget.taskState === 'completed'
1147
+ || cleanupTarget.taskState === 'archive') {
1148
+ if (row.branch !== cleanupTarget.branch) {
1149
+ branchMismatches.push({ container: row.name, labelBranch: row.branch, taskBranch: cleanupTarget.branch });
1150
+ return { row, protected: true, cleanupTarget: null };
1151
+ }
1152
+ const activeBranchTarget = resolveSandboxTarget(cleanupTarget.branch, config.repoRoot);
1153
+ if (activeBranchTarget.workspace.mode === 'branch-only') {
1154
+ return { row, protected: false, cleanupTarget };
1155
+ }
1156
+ }
1157
+ } catch {
1158
+ // An unresolved task identity has no active short-id evidence, so keep it out of bulk cleanup.
1159
+ }
1160
+ return { row, protected: true, cleanupTarget: null };
1161
+ }
1162
+ }
1163
+ if (row.workspaceMode !== 'branch-only') return { row, protected: true, cleanupTarget: null };
1164
+ const cleanupTarget = resolveSandboxCleanupTarget(row.branch, config.repoRoot);
1165
+ return cleanupTarget.workspace.mode === 'branch-only'
1166
+ ? { row, protected: false, cleanupTarget }
1167
+ : { row, protected: true, cleanupTarget: null };
1168
+ });
1169
+ const protectedBranches = new Set(classified
1170
+ .filter((entry) => entry.protected)
1171
+ .map(({ row }) => row.branch));
1172
+ const targets = classified
1173
+ .filter((entry) => !entry.protected && !protectedBranches.has(entry.row.branch) && entry.cleanupTarget)
1174
+ .map(({ row, cleanupTarget }) => ({
1175
+ row,
1176
+ cleanupTarget: cleanupTarget!,
1177
+ target: resolveRmTarget(config, tools, cleanupTarget!, { discoveredContainers: [row.name] })
1178
+ }));
1131
1179
  p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
1180
+ for (const mismatch of branchMismatches) {
1181
+ p.log.warn(`Skipping task-bound sandbox '${mismatch.container}': container branch '${mismatch.labelBranch}' differs from task record branch '${mismatch.taskBranch}'. Inspect the task record and container labels before retrying cleanup.`);
1182
+ }
1132
1183
  if (options.dryRun) {
1133
- p.outro(`Dry run: ${rows.length} sandbox(es) found, nothing deleted`);
1184
+ p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
1134
1185
  return;
1135
1186
  }
1136
- for (const row of rows) {
1137
- if (!row.branch) {
1138
- runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
1139
- continue;
1140
- }
1141
- const cleanupTarget: SandboxCleanupTarget = {
1142
- requestedRef: row.branch,
1143
- branch: row.branch,
1144
- workspace: row.workspaceMode === 'task-bound' && row.taskId
1145
- ? { mode: 'task-bound', taskId: row.taskId }
1146
- : { mode: 'branch-only' },
1147
- taskState: 'branch-only'
1148
- };
1149
- const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
1150
- await rmOne(config, tools, cleanupTarget.branch, {
1187
+ for (const { row, cleanupTarget, target } of targets) {
1188
+ await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
1151
1189
  assumeYes: options.assumeYes,
1152
1190
  target,
1153
1191
  cleanupTarget
1154
1192
  });
1155
1193
  }
1156
- p.outro(pc.green(`Removed ${rows.length} sandbox(es)`));
1194
+ p.outro(pc.green(`Removed ${targets.length} sandbox(es)`));
1157
1195
  }
1158
1196
  export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
@@ -16,7 +16,7 @@ type SandboxTool = {
16
16
  pathRewriteFiles?: string[];
17
17
  hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
18
18
  postSetupCmds?: string[];
19
- tmpfs?: { size?: string; seed?: string[] };
19
+ tmpfs?: { size?: string; seed?: string[]; exec?: boolean };
20
20
  };
21
21
 
22
22
  type SandboxAlias = Readonly<{
@@ -85,6 +85,16 @@ function asOptionalNonEmptyString(value: unknown, field: string, context: string
85
85
  return value;
86
86
  }
87
87
 
88
+ function asOptionalBoolean(value: unknown, field: string, context: string): boolean | undefined {
89
+ if (value === undefined) {
90
+ return undefined;
91
+ }
92
+ if (typeof value !== 'boolean') {
93
+ throw new Error(`${context}: field "${field}" must be a boolean when provided`);
94
+ }
95
+ return value;
96
+ }
97
+
88
98
  function asStringRecord(value: unknown, field: string, context: string): Record<string, string> | undefined {
89
99
  if (value === undefined) {
90
100
  return undefined;
@@ -193,18 +203,20 @@ function parseTmpfs(value: unknown, context: string): SandboxTool['tmpfs'] {
193
203
  if (!isPlainObject(value)) {
194
204
  throw new Error(`${context}: field "tmpfs" must be an object when provided`);
195
205
  }
206
+ const exec = asOptionalBoolean(value.exec, 'tmpfs.exec', context);
196
207
  return {
197
208
  size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
198
- seed: asStringArray(value.seed, 'tmpfs.seed', context)
209
+ seed: asStringArray(value.seed, 'tmpfs.seed', context),
210
+ ...(exec === undefined ? {} : { exec })
199
211
  };
200
212
  }
201
213
 
202
214
  export function parseCustomTool(
203
215
  entry: unknown,
204
216
  index: number,
205
- options: { home: string }
217
+ options: { home: string },
218
+ context = `customTools[${index}]`
206
219
  ): SandboxTool {
207
- const context = `customTools[${index}]`;
208
220
  if (!isPlainObject(entry)) {
209
221
  throw new Error(`${context} must be an object`);
210
222
  }
@@ -243,13 +255,30 @@ export function parseCustomTool(
243
255
  }
244
256
 
245
257
  export function parseCustomTools(value: unknown, options: { home: string }): SandboxTool[] {
246
- if (value === undefined || value === null) {
258
+ if (value === undefined) {
247
259
  return [];
248
260
  }
249
- if (!Array.isArray(value)) {
250
- throw new Error('sandbox: "customTools" must be an array');
261
+ if (!isPlainObject(value)) {
262
+ throw new Error('sandbox.tools.definitions must be an object');
251
263
  }
252
- return value.map((entry, index) => parseCustomTool(entry, index, options));
264
+ return Object.entries(value).map(([id, definition], index) => {
265
+ const context = `sandbox.tools.definitions.${id}`;
266
+ if (!TOOL_ID_PATTERN.test(id)) {
267
+ throw new Error(`${context}: tool id must match ${TOOL_ID_PATTERN.source}`);
268
+ }
269
+ if (!isPlainObject(definition)) {
270
+ throw new Error(`${context} must be an object`);
271
+ }
272
+ if (Object.hasOwn(definition, 'id')) {
273
+ throw new Error(`${context}.id is not allowed; the definition key is the tool id`);
274
+ }
275
+ return parseCustomTool(
276
+ { ...definition, id },
277
+ index,
278
+ options,
279
+ context
280
+ );
281
+ });
253
282
  }
254
283
 
255
284
  export function resolveTools(config: ToolsConfig): SandboxTool[] {
@@ -108,6 +108,7 @@ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
108
108
  const VERDICTS: ReadonlySet<string> = new Set(['approved', 'changes-requested', 'commented']);
109
109
  const OUTCOMES: ReadonlySet<string> = new Set(['aborted', 'superseded']);
110
110
  const SUCCESS_STATUSES: ReadonlySet<PrReviewArtifactStatus> = new Set(['applied', 'no-op']);
111
+ const LOCAL_COMPLETION_STATUSES: ReadonlySet<PrReviewArtifactStatus> = new Set(['applied', 'no-op', 'blocked', 'failed']);
111
112
  const ARTIFACT_STATUSES: ReadonlySet<string> = new Set([
112
113
  'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
113
114
  ]);
@@ -209,7 +210,7 @@ function readSnapshot(taskRef: string, repoRoot?: string):
209
210
  if (terminal.length === 1) {
210
211
  const note = terminal[0]!.note;
211
212
  const terminalMatchesStatus =
212
- (note.startsWith('Verdict: ') && SUCCESS_STATUSES.has(parsed.status))
213
+ (note.startsWith('Verdict: ') && LOCAL_COMPLETION_STATUSES.has(parsed.status))
213
214
  || (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
214
215
  || (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
215
216
  || (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
@@ -338,8 +339,8 @@ function applyLocked(intent: PrReviewActivityIntent, normalizedAgent: string, op
338
339
  if (roundState.startedAgent === null) {
339
340
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
340
341
  }
341
- if (intent.kind === 'pr-review-complete' && !SUCCESS_STATUSES.has(roundState.status)) {
342
- return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must be applied or no-op before completion`, state.taskId);
342
+ if (intent.kind === 'pr-review-complete' && !LOCAL_COMPLETION_STATUSES.has(roundState.status)) {
343
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must record a publication outcome before completion`, state.taskId);
343
344
  }
344
345
  if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
345
346
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
@@ -4,6 +4,7 @@ import type { PlatformResult } from '../platform/types.ts';
4
4
  import { VERSION } from '../version.ts';
5
5
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
6
6
  import { createLocalTask, type TaskCreateCandidateV1 } from './create.ts';
7
+ import { TaskExecutionLockError } from './task-execution-lock.ts';
7
8
  import { verifyTaskEvent } from './verification.ts';
8
9
 
9
10
  type TaskCreateStatus = 'applied' | 'no-op' | 'degraded' | 'failed' | 'blocked';
@@ -200,8 +201,9 @@ async function createTask(value: unknown, options: CreateTaskOptions): Promise<T
200
201
  });
201
202
  } catch (error) {
202
203
  const message = error instanceof Error ? error.message : String(error);
203
- const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
204
- return taskCreateFailure({ code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' });
204
+ const lockBusy = error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY';
205
+ const code = lockBusy ? error.code : /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
206
+ return taskCreateFailure({ code, message, retryable: lockBusy || code === 'TASK_CREATE_LOCK_TIMEOUT' });
205
207
  }
206
208
 
207
209
  const operations: TaskCreateOperation[] = [{
@@ -250,7 +252,7 @@ async function createTask(value: unknown, options: CreateTaskOptions): Promise<T
250
252
  task: local.task, issue, operations, warnings: [], error: verified.error
251
253
  };
252
254
  return {
253
- status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
255
+ status: 'degraded',
254
256
  changed: local.changed,
255
257
  task: local.task,
256
258
  issue,
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
 
6
6
  import { isValidAgentInfraVersion, VERSION } from '../version.ts';
7
7
  import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.ts';
8
- import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
8
+ import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
9
9
  import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.ts';
10
10
  import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.ts';
11
11
  import { CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, parseTaskQualification } from './qualification-audit.ts';
@@ -419,7 +419,7 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
419
419
  const canonical = canonicalTaskCreateCandidate(candidate);
420
420
  const candidateDigest = sha256(canonical);
421
421
  const keyDigest = sha256(candidate.idempotencyKey);
422
- return withCreateLock(repoRoot, projectConfig.project, () => {
422
+ function createUnderRepositoryLock(): LocalTaskCreateResult {
423
423
  const recovered = recoverPublishedTask(repoRoot, keyDigest, candidateDigest);
424
424
  if (recovered) {
425
425
  return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId, state: recovered.state } };
@@ -446,12 +446,24 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
446
446
  fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
447
447
  const destination = path.join(activeRoot, taskId);
448
448
  fs.renameSync(temporary, destination);
449
- let shortId: string;
450
449
  const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
451
450
  if (!allocated.shortId) throw new Error('TASK_CREATE_SHORT_ID_FAILED');
452
- shortId = allocated.shortId;
453
- return { status: 'applied', changed: true, task: { id: taskId, shortId, state: 'active' } };
451
+ return { status: 'applied', changed: true, task: { id: taskId, shortId: allocated.shortId, state: 'active' } };
452
+ }
453
+
454
+ return withCreateLock(repoRoot, projectConfig.project, () => {
455
+ let callbackError: unknown;
456
+ try {
457
+ return withRepositoryMutationLock(repoRoot, () => {
458
+ try { return createUnderRepositoryLock(); }
459
+ catch (error) { callbackError = error; throw error; }
460
+ });
461
+ } catch (error) {
462
+ if (callbackError !== undefined) throw callbackError;
463
+ throw error;
464
+ }
454
465
  });
466
+
455
467
  }
456
468
 
457
469
  export {
@@ -15,7 +15,10 @@ const COMPLETION_BACKFILL_FAMILIES = [
15
15
  'plan',
16
16
  'review-plan',
17
17
  'code',
18
- 'review-code'
18
+ 'review-code',
19
+ 'manual-validation',
20
+ 'validation-run',
21
+ 'pr-review'
19
22
  ] as const satisfies readonly ArtifactFamily[];
20
23
 
21
24
  type CompletionArtifactResult = {