@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.
- package/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +96 -4
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/create-service.js +5 -3
- package/dist/lib/task/create.js +22 -5
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +105 -5
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/create-service.ts +5 -3
- package/lib/task/create.ts +17 -5
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +11 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -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,
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -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
|
|
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
|
|
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: ${
|
|
1184
|
+
p.outro(`Dry run: ${targets.length} sandbox(es) found, nothing deleted`);
|
|
1134
1185
|
return;
|
|
1135
1186
|
}
|
|
1136
|
-
for (const row of
|
|
1137
|
-
|
|
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 ${
|
|
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<{
|
package/lib/sandbox/tools.ts
CHANGED
|
@@ -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
|
|
258
|
+
if (value === undefined) {
|
|
247
259
|
return [];
|
|
248
260
|
}
|
|
249
|
-
if (!
|
|
250
|
-
throw new Error('sandbox
|
|
261
|
+
if (!isPlainObject(value)) {
|
|
262
|
+
throw new Error('sandbox.tools.definitions must be an object');
|
|
251
263
|
}
|
|
252
|
-
return value.map((
|
|
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: ') &&
|
|
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' && !
|
|
342
|
-
return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must
|
|
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
|
|
204
|
-
|
|
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:
|
|
255
|
+
status: 'degraded',
|
|
254
256
|
changed: local.changed,
|
|
255
257
|
task: local.task,
|
|
256
258
|
issue,
|
package/lib/task/create.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
453
|
-
|
|
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 = {
|