@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-orchestration.js +68 -29
- package/dist/lib/sandbox/commands/create.js +1 -0
- package/dist/lib/sandbox/control/client.js +29 -15
- package/dist/lib/sandbox/control/protocol.js +23 -4
- package/dist/lib/sandbox/control/server.js +53 -18
- package/dist/lib/sandbox/workspace-view.js +8 -2
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +14 -3
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/orchestration.js +151 -15
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-orchestration.ts +54 -28
- package/lib/sandbox/commands/create.ts +1 -0
- package/lib/sandbox/control/client.ts +42 -18
- package/lib/sandbox/control/protocol.ts +43 -8
- package/lib/sandbox/control/server.ts +60 -22
- package/lib/sandbox/workspace-view.ts +9 -2
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +23 -3
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/orchestration.ts +182 -18
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- 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.
|
|
18
|
-
|| typeof manifest.
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
132
|
-
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,
|
package/lib/task/activity-log.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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({
|
|
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 {
|
|
62
|
-
|
|
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 {
|
|
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) || !
|
|
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 };
|