@fitlab-ai/agent-infra 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +8 -2
  4. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  5. package/dist/lib/agent-clients/config.js +11 -3
  6. package/dist/lib/agent-clients/custom-tool-invocations.js +92 -0
  7. package/dist/lib/agent-clients/next-steps.js +1 -1
  8. package/dist/lib/agent-clients/reconcile.js +3 -3
  9. package/dist/lib/agent-clients/schema.js +18 -10
  10. package/dist/lib/defaults.json +6 -3
  11. package/dist/lib/init.js +2 -2
  12. package/dist/lib/internal/agent-client.js +3 -3
  13. package/dist/lib/internal/cli-route-inventory.js +3 -0
  14. package/dist/lib/internal/platform-pr-review.js +20 -3
  15. package/dist/lib/internal/task-operation-registry.js +3 -1
  16. package/dist/lib/internal/task-platform-recovery.js +56 -0
  17. package/dist/lib/merge.js +25 -5
  18. package/dist/lib/platform/completion-backfill.js +2 -1
  19. package/dist/lib/platform/in-label-sync.js +12 -1
  20. package/dist/lib/platform/issue-comments.js +58 -4
  21. package/dist/lib/platform/issues.js +105 -5
  22. package/dist/lib/platform/operation-coordinator.js +74 -0
  23. package/dist/lib/platform/pr-review.js +107 -1
  24. package/dist/lib/platform/pr-summary.js +79 -57
  25. package/dist/lib/platform/pull-requests.js +96 -4
  26. package/dist/lib/sandbox/commands/create.js +6 -3
  27. package/dist/lib/sandbox/commands/ls.js +4 -3
  28. package/dist/lib/sandbox/commands/rm.js +1 -1
  29. package/dist/lib/sandbox/config.js +22 -4
  30. package/dist/lib/sandbox/git-metadata.js +50 -0
  31. package/dist/lib/sandbox/index.js +1 -1
  32. package/dist/lib/sandbox/mounts.js +12 -0
  33. package/dist/lib/sandbox/recovery.js +12 -22
  34. package/dist/lib/sandbox/removal.js +58 -20
  35. package/dist/lib/sandbox/tools.js +31 -7
  36. package/dist/lib/task/activity-intent.js +4 -3
  37. package/dist/lib/task/create-service.js +5 -3
  38. package/dist/lib/task/create.js +22 -5
  39. package/dist/lib/task/finalization-artifacts.js +4 -1
  40. package/dist/lib/task/finalization.js +243 -113
  41. package/dist/lib/task/lifecycle.js +38 -2
  42. package/dist/lib/task/platform-operation-journal.js +152 -0
  43. package/dist/lib/task/platform-operation-recovery.js +459 -0
  44. package/dist/lib/task/verification.js +0 -1
  45. package/dist/lib/task/workflow-warning-intents.js +7 -1
  46. package/dist/lib/update.js +2 -2
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapters/codex.ts +1 -1
  49. package/lib/agent-clients/config.ts +9 -3
  50. package/lib/agent-clients/custom-tool-invocations.ts +129 -0
  51. package/lib/agent-clients/next-steps.ts +3 -3
  52. package/lib/agent-clients/reconcile.ts +3 -3
  53. package/lib/agent-clients/schema.ts +18 -10
  54. package/lib/defaults.json +6 -3
  55. package/lib/init.ts +2 -2
  56. package/lib/internal/agent-client.ts +3 -3
  57. package/lib/internal/cli-route-inventory.ts +2 -0
  58. package/lib/internal/platform-pr-review.ts +14 -3
  59. package/lib/internal/task-operation-registry.ts +3 -1
  60. package/lib/internal/task-platform-recovery.ts +53 -0
  61. package/lib/merge.ts +26 -6
  62. package/lib/platform/completion-backfill.ts +2 -1
  63. package/lib/platform/in-label-sync.ts +12 -0
  64. package/lib/platform/issue-comments.ts +59 -3
  65. package/lib/platform/issues.ts +103 -5
  66. package/lib/platform/operation-coordinator.ts +96 -0
  67. package/lib/platform/pr-review.ts +113 -1
  68. package/lib/platform/pr-summary.ts +22 -1
  69. package/lib/platform/pull-requests.ts +105 -5
  70. package/lib/sandbox/commands/create.ts +7 -8
  71. package/lib/sandbox/commands/ls.ts +4 -3
  72. package/lib/sandbox/commands/rm.ts +1 -1
  73. package/lib/sandbox/config.ts +23 -5
  74. package/lib/sandbox/git-metadata.ts +66 -0
  75. package/lib/sandbox/index.ts +1 -1
  76. package/lib/sandbox/mounts.ts +14 -0
  77. package/lib/sandbox/recovery.ts +13 -25
  78. package/lib/sandbox/removal.ts +57 -19
  79. package/lib/sandbox/tool-types.ts +2 -1
  80. package/lib/sandbox/tools.ts +38 -7
  81. package/lib/task/activity-intent.ts +4 -3
  82. package/lib/task/create-service.ts +5 -3
  83. package/lib/task/create.ts +17 -5
  84. package/lib/task/finalization-artifacts.ts +4 -1
  85. package/lib/task/finalization.ts +242 -106
  86. package/lib/task/lifecycle.ts +35 -3
  87. package/lib/task/platform-operation-journal.ts +230 -0
  88. package/lib/task/platform-operation-recovery.ts +435 -0
  89. package/lib/task/verification.ts +1 -2
  90. package/lib/task/workflow-warning-intents.ts +9 -1
  91. package/lib/update.ts +2 -3
  92. package/package.json +1 -1
  93. package/templates/.agents/README.en.md +20 -66
  94. package/templates/.agents/README.zh-CN.md +19 -65
  95. package/templates/.agents/rules/README.en.md +1 -1
  96. package/templates/.agents/rules/README.zh-CN.md +1 -1
  97. package/templates/.agents/rules/next-step-output.en.md +1 -1
  98. package/templates/.agents/rules/next-step-output.zh-CN.md +1 -1
  99. package/templates/.agents/rules/task-management.en.md +12 -0
  100. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  101. package/templates/.agents/scripts/lib/agent-infra-package.js +0 -7
  102. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
  103. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  106. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  108. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
  109. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
  110. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  111. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  112. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  113. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  114. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  115. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  116. package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
  117. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
  118. package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
  119. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
  120. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  121. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  122. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  123. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  124. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -1
  125. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
  126. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
  127. package/dist/lib/agent-clients/custom-tuis.js +0 -99
  128. package/lib/agent-clients/custom-tuis.ts +0 -149
@@ -8,7 +8,7 @@ import { ENGINES, detectEngine, engineDisplayName, isManagedEngine, stopManagedV
8
8
  import { pruneSandboxDanglingImages } from './image-prune.js';
9
9
  import { assertManagedPath, removeManagedDir, removeWorktreeDir } from './managed-fs.js';
10
10
  import { run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from './shell.js';
11
- import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
11
+ import { parseSandboxWorkspaceIdentity, resolveSandboxCleanupTarget, resolveSandboxTarget, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
12
12
  import { sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
13
13
  import { advanceSandboxRemovalJournalToPhase, advanceSandboxRemovalJournalPhase, claimSandboxRemovalJournal, clearSandboxRemovalJournalRecord, isDefaultSandboxRemovalJournal, listSandboxRemovalJournals, removeSandboxControlRoot, readSandboxControlManifest, sandboxRemovalPhaseIndex } from './control/lifecycle.js';
14
14
  import { inspectSandboxControlContainer } from './control/container-identity.js';
@@ -792,38 +792,76 @@ async function rmPurgeCore(config, tools, prompt = {}) {
792
792
  p.outro(pc.green('All project sandboxes removed'));
793
793
  }
794
794
  async function rmUnbound(config, tools, options) {
795
- return rmUnboundCore(config, tools, options);
795
+ return withRepositoryMutationLock(config.repoRoot, () => rmUnboundCore(config, tools, options));
796
796
  }
797
797
  async function rmUnboundCore(config, tools, options) {
798
798
  const engine = detectEngine(config);
799
799
  const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
800
- const rows = [...listed.running, ...listed.nonRunning];
800
+ const discovered = [...listed.running, ...listed.nonRunning];
801
+ const branchMismatches = [];
802
+ const classified = discovered.map((row) => {
803
+ if (!row.branch)
804
+ return { row, protected: true, cleanupTarget: null };
805
+ if (row.workspaceMode === 'task-bound' && row.taskId) {
806
+ try {
807
+ resolveSandboxTarget(row.taskId, config.repoRoot);
808
+ return { row, protected: true, cleanupTarget: null };
809
+ }
810
+ catch {
811
+ try {
812
+ const cleanupTarget = resolveSandboxCleanupTarget(row.taskId, config.repoRoot, { allowProtected: true });
813
+ if (cleanupTarget.taskState === 'blocked'
814
+ || cleanupTarget.taskState === 'completed'
815
+ || cleanupTarget.taskState === 'archive') {
816
+ if (row.branch !== cleanupTarget.branch) {
817
+ branchMismatches.push({ container: row.name, labelBranch: row.branch, taskBranch: cleanupTarget.branch });
818
+ return { row, protected: true, cleanupTarget: null };
819
+ }
820
+ const activeBranchTarget = resolveSandboxTarget(cleanupTarget.branch, config.repoRoot);
821
+ if (activeBranchTarget.workspace.mode === 'branch-only') {
822
+ return { row, protected: false, cleanupTarget };
823
+ }
824
+ }
825
+ }
826
+ catch {
827
+ // An unresolved task identity has no active short-id evidence, so keep it out of bulk cleanup.
828
+ }
829
+ return { row, protected: true, cleanupTarget: null };
830
+ }
831
+ }
832
+ if (row.workspaceMode !== 'branch-only')
833
+ return { row, protected: true, cleanupTarget: null };
834
+ const cleanupTarget = resolveSandboxCleanupTarget(row.branch, config.repoRoot);
835
+ return cleanupTarget.workspace.mode === 'branch-only'
836
+ ? { row, protected: false, cleanupTarget }
837
+ : { row, protected: true, cleanupTarget: null };
838
+ });
839
+ const protectedBranches = new Set(classified
840
+ .filter((entry) => entry.protected)
841
+ .map(({ row }) => row.branch));
842
+ const targets = classified
843
+ .filter((entry) => !entry.protected && !protectedBranches.has(entry.row.branch) && entry.cleanupTarget)
844
+ .map(({ row, cleanupTarget }) => ({
845
+ row,
846
+ cleanupTarget: cleanupTarget,
847
+ target: resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] })
848
+ }));
801
849
  p.intro(pc.cyan(`Removing sandboxes for ${config.project}`));
850
+ for (const mismatch of branchMismatches) {
851
+ 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.`);
852
+ }
802
853
  if (options.dryRun) {
803
- p.outro(`Dry run: ${rows.length} sandbox(es) found, nothing deleted`);
854
+ p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
804
855
  return;
805
856
  }
806
- for (const row of rows) {
807
- if (!row.branch) {
808
- runSafeEngine(engine, 'docker', ['rm', '-f', row.name]);
809
- continue;
810
- }
811
- const cleanupTarget = {
812
- requestedRef: row.branch,
813
- branch: row.branch,
814
- workspace: row.workspaceMode === 'task-bound' && row.taskId
815
- ? { mode: 'task-bound', taskId: row.taskId }
816
- : { mode: 'branch-only' },
817
- taskState: 'branch-only'
818
- };
819
- const target = resolveRmTarget(config, tools, cleanupTarget, { discoveredContainers: [row.name] });
820
- await rmOne(config, tools, cleanupTarget.branch, {
857
+ for (const { row, cleanupTarget, target } of targets) {
858
+ await runRmOneUnderRepositoryLock(config, tools, cleanupTarget.branch, {
821
859
  assumeYes: options.assumeYes,
822
860
  target,
823
861
  cleanupTarget
824
862
  });
825
863
  }
826
- p.outro(pc.green(`Removed ${rows.length} sandbox(es)`));
864
+ p.outro(pc.green(`Removed ${targets.length} sandbox(es)`));
827
865
  }
828
866
  export { authorizeWorktrees, rmOne, rmPurge, rmUnbound };
829
867
  //# sourceMappingURL=removal.js.map
@@ -56,6 +56,15 @@ function asOptionalNonEmptyString(value, field, context) {
56
56
  }
57
57
  return value;
58
58
  }
59
+ function asOptionalBoolean(value, field, context) {
60
+ if (value === undefined) {
61
+ return undefined;
62
+ }
63
+ if (typeof value !== 'boolean') {
64
+ throw new Error(`${context}: field "${field}" must be a boolean when provided`);
65
+ }
66
+ return value;
67
+ }
59
68
  function asStringRecord(value, field, context) {
60
69
  if (value === undefined) {
61
70
  return undefined;
@@ -158,13 +167,14 @@ function parseTmpfs(value, context) {
158
167
  if (!isPlainObject(value)) {
159
168
  throw new Error(`${context}: field "tmpfs" must be an object when provided`);
160
169
  }
170
+ const exec = asOptionalBoolean(value.exec, 'tmpfs.exec', context);
161
171
  return {
162
172
  size: asOptionalNonEmptyString(value.size, 'tmpfs.size', context),
163
- seed: asStringArray(value.seed, 'tmpfs.seed', context)
173
+ seed: asStringArray(value.seed, 'tmpfs.seed', context),
174
+ ...(exec === undefined ? {} : { exec })
164
175
  };
165
176
  }
166
- export function parseCustomTool(entry, index, options) {
167
- const context = `customTools[${index}]`;
177
+ export function parseCustomTool(entry, index, options, context = `customTools[${index}]`) {
168
178
  if (!isPlainObject(entry)) {
169
179
  throw new Error(`${context} must be an object`);
170
180
  }
@@ -177,6 +187,7 @@ export function parseCustomTool(entry, index, options) {
177
187
  if (!containerMount.startsWith('/')) {
178
188
  throw new Error(`${context}: field "containerMount" must be an absolute path`);
179
189
  }
190
+ const invoke = asOptionalNonEmptyString(entry.invoke, 'invoke', context);
180
191
  const tool = {
181
192
  id,
182
193
  name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
@@ -186,6 +197,7 @@ export function parseCustomTool(entry, index, options) {
186
197
  versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
187
198
  setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
188
199
  ?? `Run \`${id}\` inside the container to set up.`,
200
+ ...(invoke === undefined ? {} : { invoke }),
189
201
  envVars: asStringRecord(entry.envVars, 'envVars', context),
190
202
  hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
191
203
  hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
@@ -198,13 +210,25 @@ export function parseCustomTool(entry, index, options) {
198
210
  return tool;
199
211
  }
200
212
  export function parseCustomTools(value, options) {
201
- if (value === undefined || value === null) {
213
+ if (value === undefined) {
202
214
  return [];
203
215
  }
204
- if (!Array.isArray(value)) {
205
- throw new Error('sandbox: "customTools" must be an array');
216
+ if (!isPlainObject(value)) {
217
+ throw new Error('sandbox.tools.definitions must be an object');
206
218
  }
207
- return value.map((entry, index) => parseCustomTool(entry, index, options));
219
+ return Object.entries(value).map(([id, definition], index) => {
220
+ const context = `sandbox.tools.definitions.${id}`;
221
+ if (!TOOL_ID_PATTERN.test(id)) {
222
+ throw new Error(`${context}: tool id must match ${TOOL_ID_PATTERN.source}`);
223
+ }
224
+ if (!isPlainObject(definition)) {
225
+ throw new Error(`${context} must be an object`);
226
+ }
227
+ if (Object.hasOwn(definition, 'id')) {
228
+ throw new Error(`${context}.id is not allowed; the definition key is the tool id`);
229
+ }
230
+ return parseCustomTool({ ...definition, id }, index, options, context);
231
+ });
208
232
  }
209
233
  export function resolveTools(config) {
210
234
  return [...createSandboxCapabilityPlan(config).tools];
@@ -11,6 +11,7 @@ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
11
11
  const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
12
12
  const OUTCOMES = new Set(['aborted', 'superseded']);
13
13
  const SUCCESS_STATUSES = new Set(['applied', 'no-op']);
14
+ const LOCAL_COMPLETION_STATUSES = new Set(['applied', 'no-op', 'blocked', 'failed']);
14
15
  const ARTIFACT_STATUSES = new Set([
15
16
  'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
16
17
  ]);
@@ -102,7 +103,7 @@ function readSnapshot(taskRef, repoRoot) {
102
103
  }
103
104
  if (terminal.length === 1) {
104
105
  const note = terminal[0].note;
105
- const terminalMatchesStatus = (note.startsWith('Verdict: ') && SUCCESS_STATUSES.has(parsed.status))
106
+ const terminalMatchesStatus = (note.startsWith('Verdict: ') && LOCAL_COMPLETION_STATUSES.has(parsed.status))
106
107
  || (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
107
108
  || (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
108
109
  || (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
@@ -233,8 +234,8 @@ function applyLocked(intent, normalizedAgent, options) {
233
234
  if (roundState.startedAgent === null) {
234
235
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
235
236
  }
236
- if (intent.kind === 'pr-review-complete' && !SUCCESS_STATUSES.has(roundState.status)) {
237
- return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must be applied or no-op before completion`, state.taskId);
237
+ if (intent.kind === 'pr-review-complete' && !LOCAL_COMPLETION_STATUSES.has(roundState.status)) {
238
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must record a publication outcome before completion`, state.taskId);
238
239
  }
239
240
  if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
240
241
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
@@ -3,6 +3,7 @@ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
3
3
  import { VERSION } from '../version.js';
4
4
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
5
5
  import { createLocalTask } from './create.js';
6
+ import { TaskExecutionLockError } from './task-execution-lock.js';
6
7
  import { verifyTaskEvent } from './verification.js';
7
8
  const DEFAULT_DEPENDENCIES = {
8
9
  createIssue: createPlatformIssue,
@@ -151,8 +152,9 @@ async function createTask(value, options) {
151
152
  }
152
153
  catch (error) {
153
154
  const message = error instanceof Error ? error.message : String(error);
154
- const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
155
- return taskCreateFailure({ code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' });
155
+ const lockBusy = error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY';
156
+ const code = lockBusy ? error.code : /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
157
+ return taskCreateFailure({ code, message, retryable: lockBusy || code === 'TASK_CREATE_LOCK_TIMEOUT' });
156
158
  }
157
159
  const operations = [{
158
160
  name: 'task:local', status: local.status, reasonCode: null
@@ -203,7 +205,7 @@ async function createTask(value, options) {
203
205
  task: local.task, issue, operations, warnings: [], error: verified.error
204
206
  };
205
207
  return {
206
- status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
208
+ status: 'degraded',
207
209
  changed: local.changed,
208
210
  task: local.task,
209
211
  issue,
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { isValidAgentInfraVersion, VERSION } from '../version.js';
6
6
  import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
7
- import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
7
+ import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.js';
8
8
  import { readDeliveryDefaults, validateBaseRef, validateRemote } from './delivery-target.js';
9
9
  import { buildUnboundFact, encodePrDeliveryFact } from './pr-delivery-fact.js';
10
10
  import { CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, parseTaskQualification } from './qualification-audit.js';
@@ -375,7 +375,7 @@ function createLocalTask(value, options) {
375
375
  const canonical = canonicalTaskCreateCandidate(candidate);
376
376
  const candidateDigest = sha256(canonical);
377
377
  const keyDigest = sha256(candidate.idempotencyKey);
378
- return withCreateLock(repoRoot, projectConfig.project, () => {
378
+ function createUnderRepositoryLock() {
379
379
  const recovered = recoverPublishedTask(repoRoot, keyDigest, candidateDigest);
380
380
  if (recovered) {
381
381
  return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId, state: recovered.state } };
@@ -402,12 +402,29 @@ function createLocalTask(value, options) {
402
402
  fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
403
403
  const destination = path.join(activeRoot, taskId);
404
404
  fs.renameSync(temporary, destination);
405
- let shortId;
406
405
  const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
407
406
  if (!allocated.shortId)
408
407
  throw new Error('TASK_CREATE_SHORT_ID_FAILED');
409
- shortId = allocated.shortId;
410
- return { status: 'applied', changed: true, task: { id: taskId, shortId, state: 'active' } };
408
+ return { status: 'applied', changed: true, task: { id: taskId, shortId: allocated.shortId, state: 'active' } };
409
+ }
410
+ return withCreateLock(repoRoot, projectConfig.project, () => {
411
+ let callbackError;
412
+ try {
413
+ return withRepositoryMutationLock(repoRoot, () => {
414
+ try {
415
+ return createUnderRepositoryLock();
416
+ }
417
+ catch (error) {
418
+ callbackError = error;
419
+ throw error;
420
+ }
421
+ });
422
+ }
423
+ catch (error) {
424
+ if (callbackError !== undefined)
425
+ throw callbackError;
426
+ throw error;
427
+ }
411
428
  });
412
429
  }
413
430
  export { canonicalTaskCreateCandidate, createLocalTask, validateTaskCreateCandidate };
@@ -5,7 +5,10 @@ const COMPLETION_BACKFILL_FAMILIES = [
5
5
  'plan',
6
6
  'review-plan',
7
7
  'code',
8
- 'review-code'
8
+ 'review-code',
9
+ 'manual-validation',
10
+ 'validation-run',
11
+ 'pr-review'
9
12
  ];
10
13
  function inspectCompletionArtifacts(taskRef, options = {}) {
11
14
  const artifacts = [];