@fitlab-ai/agent-infra 0.9.4 → 0.9.5

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 (56) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +10 -0
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  9. package/dist/lib/git/worktree-identity.js +64 -0
  10. package/dist/lib/internal/codex-lifecycle.js +202 -0
  11. package/dist/lib/internal/task-create.js +48 -0
  12. package/dist/lib/internal/task-orchestration.js +68 -29
  13. package/dist/lib/sandbox/commands/create.js +1 -0
  14. package/dist/lib/sandbox/control/client.js +29 -15
  15. package/dist/lib/sandbox/control/protocol.js +23 -4
  16. package/dist/lib/sandbox/control/server.js +53 -18
  17. package/dist/lib/sandbox/workspace-view.js +8 -2
  18. package/dist/lib/task/activity-log.js +32 -5
  19. package/dist/lib/task/commit-finalization.js +14 -3
  20. package/dist/lib/task/create-service.js +137 -0
  21. package/dist/lib/task/create.js +333 -0
  22. package/dist/lib/task/orchestration.js +151 -15
  23. package/dist/lib/task/review-fingerprint.js +4 -0
  24. package/dist/lib/task/workspace-snapshot.js +78 -3
  25. package/lib/agent-clients/adapter.ts +4 -4
  26. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  27. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  28. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  29. package/lib/agent-clients/adapters/codex.ts +2 -2
  30. package/lib/git/worktree-identity.ts +86 -0
  31. package/lib/internal/codex-lifecycle.ts +204 -0
  32. package/lib/internal/task-create.ts +49 -0
  33. package/lib/internal/task-orchestration.ts +54 -28
  34. package/lib/sandbox/commands/create.ts +1 -0
  35. package/lib/sandbox/control/client.ts +42 -18
  36. package/lib/sandbox/control/protocol.ts +43 -8
  37. package/lib/sandbox/control/server.ts +60 -22
  38. package/lib/sandbox/workspace-view.ts +9 -2
  39. package/lib/task/activity-log.ts +46 -7
  40. package/lib/task/commit-finalization.ts +23 -3
  41. package/lib/task/create-service.ts +171 -0
  42. package/lib/task/create.ts +372 -0
  43. package/lib/task/orchestration.ts +182 -18
  44. package/lib/task/review-fingerprint.ts +9 -0
  45. package/lib/task/workspace-snapshot.ts +98 -4
  46. package/package.json +7 -7
  47. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  48. package/templates/.agents/rules/task-management.en.md +1 -0
  49. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  50. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  51. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  52. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  53. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  54. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  55. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  56. package/templates/.codex/hooks.json +50 -0
@@ -0,0 +1,372 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.ts';
6
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
7
+
8
+ const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'] as const;
9
+ const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'] as const;
10
+ const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'] as const;
11
+ const EFFORTS = ['High', 'Medium', 'Low'] as const;
12
+ const TASK_INPUT_KEYS = [
13
+ 'sources', 'facts', 'constraints', 'decisions', 'alternatives',
14
+ 'acceptanceCriteria', 'openQuestions'
15
+ ] as const;
16
+
17
+ type TaskAgent = typeof AGENTS[number];
18
+ type TaskType = typeof TYPES[number];
19
+ type TaskPriority = typeof PRIORITIES[number];
20
+ type TaskEffort = typeof EFFORTS[number];
21
+
22
+ type TaskCreateCandidateV1 = Readonly<{
23
+ version: 1;
24
+ idempotencyKey: string;
25
+ agent: TaskAgent;
26
+ title: string;
27
+ type: TaskType;
28
+ branchSlug: string;
29
+ priority: TaskPriority;
30
+ effort: TaskEffort;
31
+ description: string;
32
+ taskInput: Readonly<Record<typeof TASK_INPUT_KEYS[number], readonly string[]>>;
33
+ }>;
34
+
35
+ type LocalTaskCreateResult = Readonly<{
36
+ status: 'applied' | 'no-op';
37
+ changed: boolean;
38
+ task: { id: string; shortId: string };
39
+ }>;
40
+
41
+ type LocalTaskCreateOptions = Readonly<{
42
+ repoRoot: string;
43
+ now?: () => Date;
44
+ agentInfraVersion?: string;
45
+ }>;
46
+
47
+ function payloadError(message: string): never {
48
+ throw new Error(`TASK_CREATE_PAYLOAD_INVALID: ${message}`);
49
+ }
50
+
51
+ function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {
52
+ const actual = Object.keys(value).sort();
53
+ const wanted = [...expected].sort();
54
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
55
+ payloadError(`${label} has unknown or missing fields`);
56
+ }
57
+ }
58
+
59
+ function cleanString(value: unknown, label: string, maxBytes: number, oneLine = false): string {
60
+ if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > maxBytes) {
61
+ payloadError(`${label} must be a non-empty string within ${maxBytes} bytes`);
62
+ }
63
+ if (/\0|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) || (oneLine && /[\r\n]/u.test(value))) {
64
+ payloadError(`${label} contains unsupported control characters`);
65
+ }
66
+ return value;
67
+ }
68
+
69
+ function enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
70
+ if (typeof value !== 'string' || !allowed.includes(value as T)) payloadError(`${label} is invalid`);
71
+ return value as T;
72
+ }
73
+
74
+ function validateTaskCreateCandidate(value: unknown): TaskCreateCandidateV1 {
75
+ if (!value || typeof value !== 'object' || Array.isArray(value)) payloadError('candidate must be an object');
76
+ const candidate = value as Record<string, unknown>;
77
+ exactKeys(candidate, [
78
+ 'version', 'idempotencyKey', 'agent', 'title', 'type', 'branchSlug',
79
+ 'priority', 'effort', 'description', 'taskInput'
80
+ ], 'candidate');
81
+ if (candidate.version !== 1) payloadError('version must be 1');
82
+ const idempotencyKey = cleanString(candidate.idempotencyKey, 'idempotencyKey', 64, true);
83
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
84
+ payloadError('idempotencyKey must be a UUID v4');
85
+ }
86
+ const title = cleanString(candidate.title, 'title', 400, true);
87
+ if ([...title].length > 100) payloadError('title exceeds 100 Unicode code points');
88
+ const branchSlug = cleanString(candidate.branchSlug, 'branchSlug', 64, true);
89
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(branchSlug) || branchSlug.length < 3) {
90
+ payloadError('branchSlug must be lowercase ASCII kebab-case with length 3-64');
91
+ }
92
+ const description = cleanString(candidate.description, 'description', 8 * 1024);
93
+ if (!candidate.taskInput || typeof candidate.taskInput !== 'object' || Array.isArray(candidate.taskInput)) {
94
+ payloadError('taskInput must be an object');
95
+ }
96
+ const rawInput = candidate.taskInput as Record<string, unknown>;
97
+ exactKeys(rawInput, TASK_INPUT_KEYS, 'taskInput');
98
+ const taskInput = Object.fromEntries(TASK_INPUT_KEYS.map((key) => {
99
+ const list = rawInput[key];
100
+ if (!Array.isArray(list) || list.length > 50) payloadError(`taskInput.${key} must contain at most 50 items`);
101
+ return [key, list.map((item, index) => cleanString(item, `taskInput.${key}[${index}]`, 2 * 1024, true))];
102
+ })) as unknown as TaskCreateCandidateV1['taskInput'];
103
+ return {
104
+ version: 1,
105
+ idempotencyKey,
106
+ agent: enumValue(candidate.agent, AGENTS, 'agent'),
107
+ title,
108
+ type: enumValue(candidate.type, TYPES, 'type'),
109
+ branchSlug,
110
+ priority: enumValue(candidate.priority, PRIORITIES, 'priority'),
111
+ effort: enumValue(candidate.effort, EFFORTS, 'effort'),
112
+ description,
113
+ taskInput
114
+ };
115
+ }
116
+
117
+ function canonicalTaskCreateCandidate(candidate: TaskCreateCandidateV1): string {
118
+ const valid = validateTaskCreateCandidate(candidate);
119
+ return JSON.stringify({
120
+ version: valid.version,
121
+ idempotencyKey: valid.idempotencyKey,
122
+ agent: valid.agent,
123
+ title: valid.title,
124
+ type: valid.type,
125
+ branchSlug: valid.branchSlug,
126
+ priority: valid.priority,
127
+ effort: valid.effort,
128
+ description: valid.description,
129
+ taskInput: Object.fromEntries(TASK_INPUT_KEYS.map((key) => [key, valid.taskInput[key]]))
130
+ });
131
+ }
132
+
133
+ function sha256(value: string): string {
134
+ return createHash('sha256').update(value).digest('hex');
135
+ }
136
+
137
+ function formatTimestamp(date: Date): string {
138
+ const pad = (value: number) => String(value).padStart(2, '0');
139
+ const offsetMinutes = -date.getTimezoneOffset();
140
+ const sign = offsetMinutes >= 0 ? '+' : '-';
141
+ const offset = Math.abs(offsetMinutes);
142
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)}`;
143
+ }
144
+
145
+ function taskIdFor(date: Date): string {
146
+ const pad = (value: number) => String(value).padStart(2, '0');
147
+ return `TASK-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
148
+ }
149
+
150
+ function yamlString(value: string): string {
151
+ return JSON.stringify(value);
152
+ }
153
+
154
+ function markdownBlock(value: string): string {
155
+ return value.split(/\r?\n/).map((line) => /^(?:#{1,6}\s|```)/.test(line) ? `\\${line}` : line).join('\n');
156
+ }
157
+
158
+ function renderList(items: readonly string[]): string {
159
+ return items.map((item) => `- ${item}`).join('\n');
160
+ }
161
+
162
+ function replaceEmptySubsection(content: string, heading: string, items: readonly string[]): string {
163
+ if (items.length === 0) return content;
164
+ return content.replace(`${heading}\n`, `${heading}\n\n${renderList(items)}\n`);
165
+ }
166
+
167
+ function renderTask(params: Readonly<{
168
+ template: string;
169
+ candidate: TaskCreateCandidateV1;
170
+ taskId: string;
171
+ project: string;
172
+ timestamp: string;
173
+ agentInfraVersion: string;
174
+ keyDigest: string;
175
+ candidateDigest: string;
176
+ }>): string {
177
+ const { candidate } = params;
178
+ const workflow = candidate.type === 'bugfix' ? 'bug-fix' : candidate.type === 'refactor' ? 'refactoring' : 'feature-development';
179
+ const branch = `${params.project}-${candidate.type}-${candidate.branchSlug}`;
180
+ const frontmatter = [
181
+ '---',
182
+ `id: ${params.taskId}`,
183
+ `type: ${candidate.type}`,
184
+ `branch: ${branch}`,
185
+ `workflow: ${workflow}`,
186
+ 'status: active',
187
+ `created_at: ${params.timestamp}`,
188
+ `updated_at: ${params.timestamp}`,
189
+ `agent_infra_version: ${params.agentInfraVersion}`,
190
+ `priority: ${candidate.priority}`,
191
+ `effort: ${candidate.effort}`,
192
+ 'start_date:',
193
+ 'target_date:',
194
+ 'current_step: requirement-analysis',
195
+ `assigned_to: ${candidate.agent}`,
196
+ 'pr_status: pending',
197
+ `task_create_key_digest: ${params.keyDigest}`,
198
+ `task_create_candidate_digest: ${params.candidateDigest}`,
199
+ '---'
200
+ ].join('\n');
201
+ let content = params.template.replace(/^---\n[\s\S]*?\n---/, frontmatter);
202
+ content = content.replace('# 任务:[标题]', `# 任务:${candidate.title}`);
203
+ content = content.replace('[清晰简洁地描述任务。]', markdownBlock(candidate.description));
204
+ const sections: Array<[string, keyof TaskCreateCandidateV1['taskInput']]> = [
205
+ ['### 来源', 'sources'], ['### 已确认事实与证据', 'facts'], ['### 约束', 'constraints'],
206
+ ['### 已确认决策', 'decisions'], ['### 候选与否决方案', 'alternatives'],
207
+ ['### 验收标准', 'acceptanceCriteria'], ['### 未决事项', 'openQuestions']
208
+ ];
209
+ for (const [heading, key] of sections) content = replaceEmptySubsection(content, heading, candidate.taskInput[key]);
210
+ content = content.replace('- **关联 Issue**:#XXX', '- **关联 Issue**:N/A');
211
+ content = content.replace('- **关联 PR**:#XXX', '- **关联 PR**:N/A');
212
+ content = content.replace('- **分支**:`feature/xxx`', `- **分支**:\`${branch}\``);
213
+ const log = [
214
+ `- ${params.timestamp} — **Create Task [started]** by ${candidate.agent} — started`,
215
+ `- ${params.timestamp} — **Create Task** by ${candidate.agent} — Task created from structured candidate`
216
+ ].join('\n');
217
+ content = content.replace('\n## 完成检查清单', `\n${log}\n\n## 完成检查清单`);
218
+ return `${content.trimEnd()}\n`;
219
+ }
220
+
221
+ function readProject(repoRoot: string): string {
222
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
223
+ if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
224
+ throw new Error('TASK_CREATE_CONFIG_INVALID: project is missing or invalid');
225
+ }
226
+ return config.project;
227
+ }
228
+
229
+ function assertRealDirectory(directory: string, code: string): void {
230
+ const stat = fs.lstatSync(directory);
231
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
232
+ }
233
+
234
+ function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
235
+ const lockRoot = path.join(workspaceRoot, '.task-create.lock');
236
+ fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
237
+ assertRealDirectory(lockRoot, 'TASK_CREATE_LOCK_FAILED');
238
+ let callbackError: unknown;
239
+ try {
240
+ return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
241
+ try { return operation(); }
242
+ catch (error) { callbackError = error; throw error; }
243
+ }, { lockRoot });
244
+ } catch (error) {
245
+ if (callbackError !== undefined) throw callbackError;
246
+ if (error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY') {
247
+ throw new Error('TASK_CREATE_LOCK_TIMEOUT');
248
+ }
249
+ const message = error instanceof Error ? error.message : String(error);
250
+ throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
251
+ }
252
+ }
253
+
254
+ function writeReceipt(receiptPath: string, value: unknown): void {
255
+ const temporary = `${receiptPath}.tmp.${process.pid}.${randomUUID()}`;
256
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
257
+ try { fs.renameSync(temporary, receiptPath); }
258
+ catch (error) {
259
+ try { fs.unlinkSync(temporary); } catch { /* best effort */ }
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ function recoverPublishedTask(activeRoot: string, repoRoot: string, keyDigest: string, candidateDigest: string): { taskId: string; shortId: string } | null {
265
+ for (const name of fs.readdirSync(activeRoot)) {
266
+ if (!/^TASK-\d{8}-\d{6}$/.test(name)) continue;
267
+ const taskDir = path.join(activeRoot, name);
268
+ const stat = fs.lstatSync(taskDir);
269
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
270
+ const taskMd = path.join(taskDir, 'task.md');
271
+ if (!fs.existsSync(taskMd)) continue;
272
+ const taskStat = fs.lstatSync(taskMd);
273
+ if (!taskStat.isFile() || taskStat.isSymbolicLink()) continue;
274
+ const content = fs.readFileSync(taskMd, 'utf8');
275
+ if (!content.includes(`task_create_key_digest: ${keyDigest}`)) continue;
276
+ if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
277
+ throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
278
+ }
279
+ const shortId = loadShortIdByTaskId(repoRoot).get(name)
280
+ ?? mutateShortIdRegistry(repoRoot, name, 'alloc').shortId;
281
+ if (!shortId) throw new Error('TASK_CREATE_RECEIPT_INVALID');
282
+ return { taskId: name, shortId };
283
+ }
284
+ return null;
285
+ }
286
+
287
+ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): LocalTaskCreateResult {
288
+ const candidate = validateTaskCreateCandidate(value);
289
+ const repoRoot = fs.realpathSync.native(options.repoRoot);
290
+ const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
291
+ const activeRoot = path.join(workspaceRoot, 'active');
292
+ assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
293
+ assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
294
+ const project = readProject(repoRoot);
295
+ const canonical = canonicalTaskCreateCandidate(candidate);
296
+ const candidateDigest = sha256(canonical);
297
+ const keyDigest = sha256(candidate.idempotencyKey);
298
+ const receipts = path.join(workspaceRoot, '.task-create');
299
+
300
+ return withCreateLock(repoRoot, workspaceRoot, () => {
301
+ if (fs.existsSync(receipts)) assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
302
+ else fs.mkdirSync(receipts, { mode: 0o700 });
303
+ const receiptPath = path.join(receipts, `${keyDigest}.json`);
304
+ if (fs.existsSync(receiptPath)) {
305
+ const stat = fs.lstatSync(receiptPath);
306
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_RECEIPT_INVALID');
307
+ const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')) as { candidateDigest?: string; taskId?: string; shortId?: string };
308
+ if (receipt.candidateDigest !== candidateDigest) throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
309
+ if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
310
+ throw new Error('TASK_CREATE_RECEIPT_INVALID');
311
+ }
312
+ return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
313
+ }
314
+
315
+ const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
316
+ if (recovered) {
317
+ writeReceipt(receiptPath, {
318
+ version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
319
+ });
320
+ return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
321
+ }
322
+
323
+ const base = options.now?.() ?? new Date();
324
+ let taskDate = new Date(base.getTime());
325
+ let taskId = taskIdFor(taskDate);
326
+ while (fs.existsSync(path.join(activeRoot, taskId))) {
327
+ taskDate = new Date(taskDate.getTime() + 1_000);
328
+ taskId = taskIdFor(taskDate);
329
+ }
330
+ const timestamp = formatTimestamp(base);
331
+ const templatePath = path.join(repoRoot, '.agents', 'templates', 'task.md');
332
+ const templateStat = fs.lstatSync(templatePath);
333
+ if (!templateStat.isFile() || templateStat.isSymbolicLink()) throw new Error('TASK_CREATE_TEMPLATE_INVALID');
334
+ const rendered = renderTask({
335
+ template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
336
+ agentInfraVersion: options.agentInfraVersion ?? 'unknown', keyDigest, candidateDigest
337
+ });
338
+ const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
339
+ fs.mkdirSync(temporary, { mode: 0o700 });
340
+ fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
341
+ const destination = path.join(activeRoot, taskId);
342
+ fs.renameSync(temporary, destination);
343
+ let shortId: string;
344
+ try {
345
+ const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
346
+ if (!allocated.shortId) throw new Error('TASK_CREATE_SHORT_ID_FAILED');
347
+ shortId = allocated.shortId;
348
+ } catch (error) {
349
+ fs.rmSync(destination, { recursive: true, force: true });
350
+ throw error;
351
+ }
352
+ try {
353
+ writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
354
+ } catch (error) {
355
+ mutateShortIdRegistry(repoRoot, taskId, 'release');
356
+ fs.rmSync(destination, { recursive: true, force: true });
357
+ throw error;
358
+ }
359
+ return { status: 'applied', changed: true, task: { id: taskId, shortId } };
360
+ });
361
+ }
362
+
363
+ export {
364
+ canonicalTaskCreateCandidate,
365
+ createLocalTask,
366
+ validateTaskCreateCandidate
367
+ };
368
+ export type {
369
+ LocalTaskCreateOptions,
370
+ LocalTaskCreateResult,
371
+ TaskCreateCandidateV1
372
+ };