@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
@@ -2,6 +2,8 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getProcessStartTime } from '../../server/process-state.ts';
5
+ import { createTask } from '../../task/create-service.ts';
6
+ import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
5
7
  import {
6
8
  bindSandboxControlTask,
7
9
  validateSandboxControlRequest,
@@ -13,9 +15,12 @@ function readManifest(manifestPath: string): SandboxControlManifest {
13
15
  const stat = fs.lstatSync(manifestPath);
14
16
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
15
17
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as SandboxControlManifest;
18
+ if (manifest.version !== 2) {
19
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
20
+ }
16
21
  if (
17
- manifest.version !== 1
18
- || typeof manifest.repoRoot !== 'string'
22
+ typeof manifest.repoRoot !== 'string'
23
+ || typeof manifest.worktreeRoot !== 'string'
19
24
  || typeof manifest.channelDir !== 'string'
20
25
  || typeof manifest.token !== 'string'
21
26
  ) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -39,8 +44,28 @@ function writeResponse(directory: string, response: SandboxControlResponse): voi
39
44
  fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
40
45
  }
41
46
 
47
+ export function sandboxControlSafeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
48
+ return Object.fromEntries(
49
+ Object.entries(env).filter(
50
+ ([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')
51
+ )
52
+ );
53
+ }
54
+
55
+ function consumeRequest(directory: string, id: string): void {
56
+ try {
57
+ fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
58
+ } catch (error) {
59
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
60
+ throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+
42
66
  export function serveSandboxControl(manifestPath: string, signal: AbortSignal = new AbortController().signal): void {
43
67
  const manifest = readManifest(manifestPath);
68
+ assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
44
69
  const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
45
70
  const startTime = getProcessStartTime(process.pid);
46
71
  if (!startTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
@@ -52,15 +77,18 @@ export function serveSandboxControl(manifestPath: string, signal: AbortSignal =
52
77
  })}\n`, { mode: 0o600, flag: 'wx' });
53
78
  const requestsDir = path.join(manifest.channelDir, 'requests');
54
79
  const responsesDir = path.join(manifest.channelDir, 'responses');
80
+ const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
55
81
  assertRealDirectory(manifest.channelDir);
56
82
  fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
57
83
  fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
84
+ fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
58
85
  assertRealDirectory(requestsDir, manifest.channelDir);
59
86
  assertRealDirectory(responsesDir, manifest.channelDir);
60
- const consumed = new Set<string>();
87
+ assertRealDirectory(consumedDir, path.dirname(manifestPath));
61
88
  while (!signal.aborted) {
62
89
  const current = readManifest(manifestPath);
63
90
  if (current.token !== manifest.token) return;
91
+ assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
64
92
  assertRealDirectory(requestsDir, manifest.channelDir);
65
93
  assertRealDirectory(responsesDir, manifest.channelDir);
66
94
  for (const name of fs.readdirSync(requestsDir).sort()) {
@@ -76,26 +104,36 @@ export function serveSandboxControl(manifestPath: string, signal: AbortSignal =
76
104
  manifest
77
105
  );
78
106
  id = request.id;
79
- if (consumed.has(id)) throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
80
- consumed.add(id);
81
- const result = spawnSync(
82
- process.execPath,
83
- ['--experimental-strip-types', '--no-warnings', process.argv[1]!, request.family, ...bindSandboxControlTask(request, manifest.taskId!)],
84
- {
85
- cwd: manifest.repoRoot,
86
- encoding: 'utf8',
87
- env: Object.fromEntries(
88
- Object.entries(process.env).filter(([key]) => !key.startsWith('AGENT_INFRA_CONTROL_'))
89
- )
107
+ consumeRequest(consumedDir, id);
108
+ if (request.family === 'task-create') {
109
+ const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
110
+ response = {
111
+ version: 1, id,
112
+ exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
113
+ stdout: `${JSON.stringify(result)}\n`, stderr: ''
114
+ };
115
+ } else {
116
+ const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
117
+ if (request.family === 'task-orchestration') {
118
+ boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
90
119
  }
91
- );
92
- response = {
93
- version: 1,
94
- id,
95
- exitCode: result.status ?? 1,
96
- stdout: result.stdout ?? '',
97
- stderr: result.stderr ?? ''
98
- };
120
+ const result = spawnSync(
121
+ process.execPath,
122
+ ['--experimental-strip-types', '--no-warnings', process.argv[1]!, request.family, ...boundArgs],
123
+ {
124
+ cwd: manifest.repoRoot,
125
+ encoding: 'utf8',
126
+ env: sandboxControlSafeEnv()
127
+ }
128
+ );
129
+ response = {
130
+ version: 1,
131
+ id,
132
+ exitCode: result.status ?? 1,
133
+ stdout: result.stdout ?? '',
134
+ stderr: result.stderr ?? ''
135
+ };
136
+ }
99
137
  } catch (error) {
100
138
  response = {
101
139
  version: 1,
@@ -113,6 +113,7 @@ export function materializeSandboxWorkspaceView(params: Readonly<{
113
113
  export function materializeSandboxControl(params: Readonly<{
114
114
  base: string;
115
115
  repoRoot: string;
116
+ worktreeRoot: string;
116
117
  project: string;
117
118
  container: string;
118
119
  branch: string;
@@ -120,6 +121,10 @@ export function materializeSandboxControl(params: Readonly<{
120
121
  }>): SandboxControlSetup {
121
122
  const { root, channelDir, manifestPath } = sandboxControlPaths(params);
122
123
  assertSafeDirectory(root, path.resolve(params.base));
124
+ const consumedDir = path.join(root, 'consumed');
125
+ assertSafeDirectory(consumedDir, root);
126
+ fs.rmSync(consumedDir, { recursive: true, force: true });
127
+ fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
123
128
  for (const queue of ['requests', 'responses']) {
124
129
  const directory = path.join(channelDir, queue);
125
130
  assertSafeDirectory(directory, root);
@@ -127,9 +132,11 @@ export function materializeSandboxControl(params: Readonly<{
127
132
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
128
133
  }
129
134
  const token = randomBytes(32).toString('hex');
135
+ const repoRoot = fs.realpathSync.native(params.repoRoot);
130
136
  const manifest: SandboxControlManifest = {
131
- version: 1,
132
- repoRoot: fs.realpathSync.native(params.repoRoot),
137
+ version: 2,
138
+ repoRoot,
139
+ worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
133
140
  project: params.project,
134
141
  container: params.container,
135
142
  branch: params.branch,
@@ -2,10 +2,14 @@ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
2
2
  const NEXT_H2_RE = /^##\s/;
3
3
  const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
4
4
  const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
5
+ const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
6
+ const ATTEMPT_RE = /(?:^|;\s*)attempt=([A-Za-z0-9-]{8,64})(?:;|$)/;
7
+ const COMMIT_STARTED_RE = /^started; attempt=([A-Za-z0-9-]{8,64}); baseline=([a-f0-9]{40,64}); agent=([a-z-]+)$/;
5
8
 
6
9
  type LogEntry = { time: string; step: string; agent: string; note: string };
7
- type StepRow = { step: string; agent: string; started: string; done: string; note: string };
10
+ type StepRow = { step: string; agent: string; started: string; done: string; note: string; attempt?: string };
8
11
  type ActivityLogSection = { heading: string; body: string; entries: LogEntry[] };
12
+ type CommitAttempt = Readonly<{ attempt: string; baseline: string; agent: string }>;
9
13
 
10
14
  function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
11
15
  const section = locateActivityLog(content);
@@ -37,26 +41,61 @@ function appendActivityEntry(section: ActivityLogSection, entry: LogEntry): stri
37
41
  return section.body ? `${section.body}\n${line}` : line;
38
42
  }
39
43
 
44
+ function activityAttempt(note: string): string | null {
45
+ return ATTEMPT_RE.exec(note)?.[1] ?? null;
46
+ }
47
+
48
+ function parseCommitAttemptStarted(note: string): CommitAttempt | null {
49
+ const match = COMMIT_STARTED_RE.exec(note);
50
+ return match ? { attempt: match[1]!, baseline: match[2]!, agent: match[3]! } : null;
51
+ }
52
+
53
+ function commitAttemptStartedNote(attempt: CommitAttempt): string {
54
+ return `started; attempt=${attempt.attempt}; baseline=${attempt.baseline}; agent=${attempt.agent}`;
55
+ }
56
+
40
57
  function pairEntries(entries: LogEntry[]): StepRow[] {
41
58
  const rows: StepRow[] = [];
42
59
  const open = new Map<string, StepRow[]>();
43
60
  for (const entry of entries) {
44
61
  const started = STARTED_SUFFIX_RE.test(entry.step);
45
- const base = entry.step.replace(STARTED_SUFFIX_RE, '');
62
+ const aborted = ABORTED_SUFFIX_RE.test(entry.step);
63
+ const base = entry.step.replace(STARTED_SUFFIX_RE, '').replace(ABORTED_SUFFIX_RE, '');
46
64
  if (started) {
47
- const row = { step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note };
65
+ const attempt = activityAttempt(entry.note);
66
+ const row: StepRow = {
67
+ step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note,
68
+ ...(attempt ? { attempt } : {})
69
+ };
48
70
  rows.push(row);
49
71
  const queue = open.get(base) ?? [];
50
72
  queue.push(row);
51
73
  open.set(base, queue);
52
74
  } else {
53
- const pending = open.get(base)?.shift();
75
+ const queue = open.get(base) ?? [];
76
+ const terminalAttempt = aborted ? activityAttempt(entry.note) : null;
77
+ const pendingIndex = terminalAttempt === null
78
+ ? 0
79
+ : queue.findIndex((row) => row.attempt === terminalAttempt);
80
+ const pending = pendingIndex >= 0 ? queue.splice(pendingIndex, 1)[0] : undefined;
54
81
  if (pending) Object.assign(pending, { done: entry.time, agent: entry.agent, note: entry.note });
55
- else rows.push({ step: base, agent: entry.agent, started: '', done: entry.time, note: entry.note });
82
+ else rows.push({
83
+ step: aborted ? entry.step : base,
84
+ agent: entry.agent, started: '', done: entry.time, note: entry.note,
85
+ ...(terminalAttempt ? { attempt: terminalAttempt } : {})
86
+ });
56
87
  }
57
88
  }
58
89
  return rows;
59
90
  }
60
91
 
61
- export { parseActivityLog, locateActivityLog, appendActivityEntry, pairEntries };
62
- export type { LogEntry, StepRow, ActivityLogSection };
92
+ export {
93
+ activityAttempt,
94
+ appendActivityEntry,
95
+ commitAttemptStartedNote,
96
+ locateActivityLog,
97
+ pairEntries,
98
+ parseActivityLog,
99
+ parseCommitAttemptStarted
100
+ };
101
+ export type { ActivityLogSection, CommitAttempt, LogEntry, StepRow };
@@ -2,7 +2,13 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.ts';
5
+ import {
6
+ locateActivityLog,
7
+ appendActivityEntry,
8
+ pairEntries,
9
+ parseCommitAttemptStarted
10
+ } from './activity-log.ts';
11
+ import type { CommitAttempt } from './activity-log.ts';
6
12
  import { commitIntentPath, digest, readCommitIntent } from './commit-intent.ts';
7
13
  import type { CommitIntent } from './commit-intent.ts';
8
14
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
@@ -10,7 +16,8 @@ import { parseReviewSummary } from './review-artifacts.ts';
10
16
  import {
11
17
  extractReviewBaseline,
12
18
  extractReviewedSnapshotTree,
13
- findAuthoritativeReviewCodeArtifact
19
+ findAuthoritativeReviewCodeArtifact,
20
+ parseReviewedGitTree
14
21
  } from './review-fingerprint.ts';
15
22
  import type { TaskMutation } from './write.ts';
16
23
 
@@ -20,6 +27,7 @@ type CommitFinalizationDisposition =
20
27
  | 'recoverable'
21
28
  | 'invalid'
22
29
  | 'conflict'
30
+ | 'retryable-start'
23
31
  | 'orphaned-start';
24
32
 
25
33
  type CommitFinalizationCode =
@@ -39,6 +47,7 @@ type CommitFinalizationInspection = Readonly<{
39
47
  needsLog: boolean;
40
48
  intent: CommitIntent | null;
41
49
  intentDigest: string | null;
50
+ attempt: CommitAttempt | null;
42
51
  }>;
43
52
 
44
53
  type CreatePrCommitGate = Readonly<{
@@ -93,6 +102,7 @@ function outcome(
93
102
  needsLog: false,
94
103
  intent: null,
95
104
  intentDigest: null,
105
+ attempt: null,
96
106
  ...overrides
97
107
  };
98
108
  }
@@ -128,6 +138,16 @@ function inspectCommitFinalization(
128
138
  const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
129
139
  const file = commitIntentPath(taskDir);
130
140
  if (!fs.existsSync(file)) {
141
+ if (openRows.length === 1) {
142
+ const attempt = parseCommitAttemptStarted(openRows[0]!.note);
143
+ if (attempt && attempt.baseline === currentHead) {
144
+ return outcome('retryable-start', {
145
+ message: 'a structured Commit attempt can be safely reused',
146
+ currentHead,
147
+ attempt
148
+ });
149
+ }
150
+ }
131
151
  if (openRows.length > 0) {
132
152
  return outcome('orphaned-start', {
133
153
  code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
@@ -204,7 +224,7 @@ function inspectCommitFinalization(
204
224
  const summary = parseReviewSummary(reviewContent);
205
225
  const reviewBaseline = extractReviewBaseline(reviewContent);
206
226
  const reviewTree = extractReviewedSnapshotTree(reviewContent);
207
- if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !SHA_RE.test(reviewTree)) {
227
+ if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !parseReviewedGitTree(reviewTree)) {
208
228
  return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
209
229
  }
210
230
  const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { syncPlatformComment } from '../platform/issue-comments.ts';
5
+ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
6
+ import type { PlatformResult } from '../platform/types.ts';
7
+ import { VERSION } from '../version.ts';
8
+ import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
9
+ import { createLocalTask, type TaskCreateCandidateV1 } from './create.ts';
10
+ import { verifyTaskEvent } from './verification.ts';
11
+
12
+ type TaskCreateStatus = 'applied' | 'no-op' | 'degraded' | 'failed' | 'blocked';
13
+ type TaskCreateOperation = { name: string; status: string; reasonCode: string | null };
14
+ type TaskCreateWarning = { code: string; severity: string; action: string };
15
+ type TaskCreateResult = Readonly<{
16
+ status: TaskCreateStatus;
17
+ changed: boolean;
18
+ task: { id: string | null; shortId: string | null };
19
+ issue: { number: number; url: string } | null;
20
+ operations: readonly TaskCreateOperation[];
21
+ warnings: readonly TaskCreateWarning[];
22
+ error: { code: string; message: string; retryable: boolean } | null;
23
+ }>;
24
+
25
+ type CreateTaskOptions = Readonly<{
26
+ repoRoot: string;
27
+ agentInfraVersion?: string;
28
+ dependencies?: Partial<TaskCreateDependencies>;
29
+ }>;
30
+
31
+ type TaskCreateDependencies = Readonly<{
32
+ createIssue: typeof createPlatformIssue;
33
+ syncIssue: typeof syncPlatformIssue;
34
+ syncComment: typeof syncPlatformComment;
35
+ addWarning: typeof applyWorkflowWarningIntent;
36
+ }>;
37
+
38
+ const DEFAULT_DEPENDENCIES: TaskCreateDependencies = {
39
+ createIssue: createPlatformIssue,
40
+ syncIssue: syncPlatformIssue,
41
+ syncComment: syncPlatformComment,
42
+ addWarning: applyWorkflowWarningIntent
43
+ };
44
+
45
+ function verifyCreatedTask(repoRoot: string, taskId: string): { operation: TaskCreateOperation; error: TaskCreateResult['error'] } {
46
+ const verification = verifyTaskEvent(
47
+ { taskRef: taskId, event: 'create-task.completed' },
48
+ { repoRoot }
49
+ );
50
+ if (verification.status === 'pass') {
51
+ return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
52
+ }
53
+ const blocked = verification.status === 'blocked';
54
+ return {
55
+ operation: {
56
+ name: 'task:verify', status: verification.status,
57
+ reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
58
+ },
59
+ error: {
60
+ code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
61
+ message: `Created task did not pass the ${verification.event} gate`,
62
+ retryable: blocked
63
+ }
64
+ };
65
+ }
66
+
67
+ function platformOperations(prefix: string, operations: readonly { name: string; status: string; reasonCode?: string | null }[]): TaskCreateOperation[] {
68
+ return operations.map((operation) => ({
69
+ name: `${prefix}:${operation.name}`,
70
+ status: operation.status,
71
+ reasonCode: operation.reasonCode ?? null
72
+ }));
73
+ }
74
+
75
+ function issueFromTask(repoRoot: string, taskId: string): { number: number; url: string } | null {
76
+ const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
77
+ const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
78
+ return match ? { number: Number(match[1]), url: '' } : null;
79
+ }
80
+
81
+ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResult {
82
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
83
+ let candidate: TaskCreateCandidateV1;
84
+ let local;
85
+ try {
86
+ candidate = value as TaskCreateCandidateV1;
87
+ local = createLocalTask(candidate, {
88
+ repoRoot: options.repoRoot,
89
+ agentInfraVersion: options.agentInfraVersion ?? VERSION
90
+ });
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
94
+ return {
95
+ status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
96
+ operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
97
+ };
98
+ }
99
+
100
+ const operations: TaskCreateOperation[] = [{
101
+ name: 'task:local', status: local.status, reasonCode: null
102
+ }];
103
+ let issue = issueFromTask(options.repoRoot, local.task.id);
104
+ const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
105
+ operations.push(...platformOperations('platform-create', created.operations));
106
+ if (created.issue) issue = { number: created.issue.number, url: created.issue.url };
107
+
108
+ let platformFailure: PlatformResult | null = created.status === 'failed' || created.status === 'blocked' ? created : null;
109
+ if (!platformFailure && (created.task.issueNumber || issue)) {
110
+ const synced = dependencies.syncIssue(local.task.id, {
111
+ cwd: options.repoRoot,
112
+ agent: candidate.agent,
113
+ status: 'waiting-for-triage',
114
+ assignees: 'current',
115
+ milestone: 'initial',
116
+ issueType: true,
117
+ fields: true
118
+ });
119
+ operations.push(...platformOperations('platform-sync', synced.operations));
120
+ if (synced.issue) issue = { number: synced.issue.number, url: synced.issue.url };
121
+ if (synced.status === 'failed' || synced.status === 'blocked') platformFailure = synced;
122
+ if (!platformFailure) {
123
+ const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
124
+ operations.push(...platformOperations('platform-comment', commented.operations));
125
+ if (commented.status === 'failed' || commented.status === 'blocked') platformFailure = commented;
126
+ }
127
+ }
128
+
129
+ if (platformFailure) {
130
+ const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
131
+ dependencies.addWarning({
132
+ kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
133
+ code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
134
+ action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
135
+ }, { repoRoot: options.repoRoot });
136
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
137
+ operations.push(verified.operation);
138
+ if (verified.error) return {
139
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
140
+ task: local.task, issue, operations, warnings: [], error: verified.error
141
+ };
142
+ return {
143
+ status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
144
+ changed: local.changed,
145
+ task: local.task,
146
+ issue,
147
+ operations,
148
+ warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
149
+ error
150
+ };
151
+ }
152
+
153
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
154
+ operations.push(verified.operation);
155
+ if (verified.error) return {
156
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
157
+ task: local.task, issue, operations, warnings: [], error: verified.error
158
+ };
159
+ return {
160
+ status: local.status,
161
+ changed: local.changed,
162
+ task: local.task,
163
+ issue,
164
+ operations,
165
+ warnings: [],
166
+ error: null
167
+ };
168
+ }
169
+
170
+ export { createTask };
171
+ export type { CreateTaskOptions, TaskCreateDependencies, TaskCreateOperation, TaskCreateResult, TaskCreateStatus, TaskCreateWarning };