@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { syncPlatformComment } from '../platform/issue-comments.js';
|
|
4
|
+
import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
5
|
+
import { VERSION } from '../version.js';
|
|
6
|
+
import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
|
|
7
|
+
import { createLocalTask } from './create.js';
|
|
8
|
+
import { verifyTaskEvent } from './verification.js';
|
|
9
|
+
const DEFAULT_DEPENDENCIES = {
|
|
10
|
+
createIssue: createPlatformIssue,
|
|
11
|
+
syncIssue: syncPlatformIssue,
|
|
12
|
+
syncComment: syncPlatformComment,
|
|
13
|
+
addWarning: applyWorkflowWarningIntent
|
|
14
|
+
};
|
|
15
|
+
function verifyCreatedTask(repoRoot, taskId) {
|
|
16
|
+
const verification = verifyTaskEvent({ taskRef: taskId, event: 'create-task.completed' }, { repoRoot });
|
|
17
|
+
if (verification.status === 'pass') {
|
|
18
|
+
return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
|
|
19
|
+
}
|
|
20
|
+
const blocked = verification.status === 'blocked';
|
|
21
|
+
return {
|
|
22
|
+
operation: {
|
|
23
|
+
name: 'task:verify', status: verification.status,
|
|
24
|
+
reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
|
|
25
|
+
},
|
|
26
|
+
error: {
|
|
27
|
+
code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
|
|
28
|
+
message: `Created task did not pass the ${verification.event} gate`,
|
|
29
|
+
retryable: blocked
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function platformOperations(prefix, operations) {
|
|
34
|
+
return operations.map((operation) => ({
|
|
35
|
+
name: `${prefix}:${operation.name}`,
|
|
36
|
+
status: operation.status,
|
|
37
|
+
reasonCode: operation.reasonCode ?? null
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function issueFromTask(repoRoot, taskId) {
|
|
41
|
+
const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
|
|
42
|
+
const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
|
|
43
|
+
return match ? { number: Number(match[1]), url: '' } : null;
|
|
44
|
+
}
|
|
45
|
+
function createTask(value, options) {
|
|
46
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
|
|
47
|
+
let candidate;
|
|
48
|
+
let local;
|
|
49
|
+
try {
|
|
50
|
+
candidate = value;
|
|
51
|
+
local = createLocalTask(candidate, {
|
|
52
|
+
repoRoot: options.repoRoot,
|
|
53
|
+
agentInfraVersion: options.agentInfraVersion ?? VERSION
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
|
|
59
|
+
return {
|
|
60
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
61
|
+
operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const operations = [{
|
|
65
|
+
name: 'task:local', status: local.status, reasonCode: null
|
|
66
|
+
}];
|
|
67
|
+
let issue = issueFromTask(options.repoRoot, local.task.id);
|
|
68
|
+
const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
69
|
+
operations.push(...platformOperations('platform-create', created.operations));
|
|
70
|
+
if (created.issue)
|
|
71
|
+
issue = { number: created.issue.number, url: created.issue.url };
|
|
72
|
+
let platformFailure = created.status === 'failed' || created.status === 'blocked' ? created : null;
|
|
73
|
+
if (!platformFailure && (created.task.issueNumber || issue)) {
|
|
74
|
+
const synced = dependencies.syncIssue(local.task.id, {
|
|
75
|
+
cwd: options.repoRoot,
|
|
76
|
+
agent: candidate.agent,
|
|
77
|
+
status: 'waiting-for-triage',
|
|
78
|
+
assignees: 'current',
|
|
79
|
+
milestone: 'initial',
|
|
80
|
+
issueType: true,
|
|
81
|
+
fields: true
|
|
82
|
+
});
|
|
83
|
+
operations.push(...platformOperations('platform-sync', synced.operations));
|
|
84
|
+
if (synced.issue)
|
|
85
|
+
issue = { number: synced.issue.number, url: synced.issue.url };
|
|
86
|
+
if (synced.status === 'failed' || synced.status === 'blocked')
|
|
87
|
+
platformFailure = synced;
|
|
88
|
+
if (!platformFailure) {
|
|
89
|
+
const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
|
|
90
|
+
operations.push(...platformOperations('platform-comment', commented.operations));
|
|
91
|
+
if (commented.status === 'failed' || commented.status === 'blocked')
|
|
92
|
+
platformFailure = commented;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (platformFailure) {
|
|
96
|
+
const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
|
|
97
|
+
dependencies.addWarning({
|
|
98
|
+
kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
|
|
99
|
+
code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
|
|
100
|
+
action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
|
|
101
|
+
}, { repoRoot: options.repoRoot });
|
|
102
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
103
|
+
operations.push(verified.operation);
|
|
104
|
+
if (verified.error)
|
|
105
|
+
return {
|
|
106
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
107
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
|
|
111
|
+
changed: local.changed,
|
|
112
|
+
task: local.task,
|
|
113
|
+
issue,
|
|
114
|
+
operations,
|
|
115
|
+
warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
|
|
116
|
+
error
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
120
|
+
operations.push(verified.operation);
|
|
121
|
+
if (verified.error)
|
|
122
|
+
return {
|
|
123
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
124
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
status: local.status,
|
|
128
|
+
changed: local.changed,
|
|
129
|
+
task: local.task,
|
|
130
|
+
issue,
|
|
131
|
+
operations,
|
|
132
|
+
warnings: [],
|
|
133
|
+
error: null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export { createTask };
|
|
137
|
+
//# sourceMappingURL=create-service.js.map
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
|
|
5
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
6
|
+
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'];
|
|
7
|
+
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'];
|
|
8
|
+
const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'];
|
|
9
|
+
const EFFORTS = ['High', 'Medium', 'Low'];
|
|
10
|
+
const TASK_INPUT_KEYS = [
|
|
11
|
+
'sources', 'facts', 'constraints', 'decisions', 'alternatives',
|
|
12
|
+
'acceptanceCriteria', 'openQuestions'
|
|
13
|
+
];
|
|
14
|
+
function payloadError(message) {
|
|
15
|
+
throw new Error(`TASK_CREATE_PAYLOAD_INVALID: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function exactKeys(value, expected, label) {
|
|
18
|
+
const actual = Object.keys(value).sort();
|
|
19
|
+
const wanted = [...expected].sort();
|
|
20
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
21
|
+
payloadError(`${label} has unknown or missing fields`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function cleanString(value, label, maxBytes, oneLine = false) {
|
|
25
|
+
if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > maxBytes) {
|
|
26
|
+
payloadError(`${label} must be a non-empty string within ${maxBytes} bytes`);
|
|
27
|
+
}
|
|
28
|
+
if (/\0|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) || (oneLine && /[\r\n]/u.test(value))) {
|
|
29
|
+
payloadError(`${label} contains unsupported control characters`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function enumValue(value, allowed, label) {
|
|
34
|
+
if (typeof value !== 'string' || !allowed.includes(value))
|
|
35
|
+
payloadError(`${label} is invalid`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function validateTaskCreateCandidate(value) {
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
40
|
+
payloadError('candidate must be an object');
|
|
41
|
+
const candidate = value;
|
|
42
|
+
exactKeys(candidate, [
|
|
43
|
+
'version', 'idempotencyKey', 'agent', 'title', 'type', 'branchSlug',
|
|
44
|
+
'priority', 'effort', 'description', 'taskInput'
|
|
45
|
+
], 'candidate');
|
|
46
|
+
if (candidate.version !== 1)
|
|
47
|
+
payloadError('version must be 1');
|
|
48
|
+
const idempotencyKey = cleanString(candidate.idempotencyKey, 'idempotencyKey', 64, true);
|
|
49
|
+
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)) {
|
|
50
|
+
payloadError('idempotencyKey must be a UUID v4');
|
|
51
|
+
}
|
|
52
|
+
const title = cleanString(candidate.title, 'title', 400, true);
|
|
53
|
+
if ([...title].length > 100)
|
|
54
|
+
payloadError('title exceeds 100 Unicode code points');
|
|
55
|
+
const branchSlug = cleanString(candidate.branchSlug, 'branchSlug', 64, true);
|
|
56
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(branchSlug) || branchSlug.length < 3) {
|
|
57
|
+
payloadError('branchSlug must be lowercase ASCII kebab-case with length 3-64');
|
|
58
|
+
}
|
|
59
|
+
const description = cleanString(candidate.description, 'description', 8 * 1024);
|
|
60
|
+
if (!candidate.taskInput || typeof candidate.taskInput !== 'object' || Array.isArray(candidate.taskInput)) {
|
|
61
|
+
payloadError('taskInput must be an object');
|
|
62
|
+
}
|
|
63
|
+
const rawInput = candidate.taskInput;
|
|
64
|
+
exactKeys(rawInput, TASK_INPUT_KEYS, 'taskInput');
|
|
65
|
+
const taskInput = Object.fromEntries(TASK_INPUT_KEYS.map((key) => {
|
|
66
|
+
const list = rawInput[key];
|
|
67
|
+
if (!Array.isArray(list) || list.length > 50)
|
|
68
|
+
payloadError(`taskInput.${key} must contain at most 50 items`);
|
|
69
|
+
return [key, list.map((item, index) => cleanString(item, `taskInput.${key}[${index}]`, 2 * 1024, true))];
|
|
70
|
+
}));
|
|
71
|
+
return {
|
|
72
|
+
version: 1,
|
|
73
|
+
idempotencyKey,
|
|
74
|
+
agent: enumValue(candidate.agent, AGENTS, 'agent'),
|
|
75
|
+
title,
|
|
76
|
+
type: enumValue(candidate.type, TYPES, 'type'),
|
|
77
|
+
branchSlug,
|
|
78
|
+
priority: enumValue(candidate.priority, PRIORITIES, 'priority'),
|
|
79
|
+
effort: enumValue(candidate.effort, EFFORTS, 'effort'),
|
|
80
|
+
description,
|
|
81
|
+
taskInput
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function canonicalTaskCreateCandidate(candidate) {
|
|
85
|
+
const valid = validateTaskCreateCandidate(candidate);
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
version: valid.version,
|
|
88
|
+
idempotencyKey: valid.idempotencyKey,
|
|
89
|
+
agent: valid.agent,
|
|
90
|
+
title: valid.title,
|
|
91
|
+
type: valid.type,
|
|
92
|
+
branchSlug: valid.branchSlug,
|
|
93
|
+
priority: valid.priority,
|
|
94
|
+
effort: valid.effort,
|
|
95
|
+
description: valid.description,
|
|
96
|
+
taskInput: Object.fromEntries(TASK_INPUT_KEYS.map((key) => [key, valid.taskInput[key]]))
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function sha256(value) {
|
|
100
|
+
return createHash('sha256').update(value).digest('hex');
|
|
101
|
+
}
|
|
102
|
+
function formatTimestamp(date) {
|
|
103
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
104
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
105
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
106
|
+
const offset = Math.abs(offsetMinutes);
|
|
107
|
+
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)}`;
|
|
108
|
+
}
|
|
109
|
+
function taskIdFor(date) {
|
|
110
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
111
|
+
return `TASK-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
112
|
+
}
|
|
113
|
+
function yamlString(value) {
|
|
114
|
+
return JSON.stringify(value);
|
|
115
|
+
}
|
|
116
|
+
function markdownBlock(value) {
|
|
117
|
+
return value.split(/\r?\n/).map((line) => /^(?:#{1,6}\s|```)/.test(line) ? `\\${line}` : line).join('\n');
|
|
118
|
+
}
|
|
119
|
+
function renderList(items) {
|
|
120
|
+
return items.map((item) => `- ${item}`).join('\n');
|
|
121
|
+
}
|
|
122
|
+
function replaceEmptySubsection(content, heading, items) {
|
|
123
|
+
if (items.length === 0)
|
|
124
|
+
return content;
|
|
125
|
+
return content.replace(`${heading}\n`, `${heading}\n\n${renderList(items)}\n`);
|
|
126
|
+
}
|
|
127
|
+
function renderTask(params) {
|
|
128
|
+
const { candidate } = params;
|
|
129
|
+
const workflow = candidate.type === 'bugfix' ? 'bug-fix' : candidate.type === 'refactor' ? 'refactoring' : 'feature-development';
|
|
130
|
+
const branch = `${params.project}-${candidate.type}-${candidate.branchSlug}`;
|
|
131
|
+
const frontmatter = [
|
|
132
|
+
'---',
|
|
133
|
+
`id: ${params.taskId}`,
|
|
134
|
+
`type: ${candidate.type}`,
|
|
135
|
+
`branch: ${branch}`,
|
|
136
|
+
`workflow: ${workflow}`,
|
|
137
|
+
'status: active',
|
|
138
|
+
`created_at: ${params.timestamp}`,
|
|
139
|
+
`updated_at: ${params.timestamp}`,
|
|
140
|
+
`agent_infra_version: ${params.agentInfraVersion}`,
|
|
141
|
+
`priority: ${candidate.priority}`,
|
|
142
|
+
`effort: ${candidate.effort}`,
|
|
143
|
+
'start_date:',
|
|
144
|
+
'target_date:',
|
|
145
|
+
'current_step: requirement-analysis',
|
|
146
|
+
`assigned_to: ${candidate.agent}`,
|
|
147
|
+
'pr_status: pending',
|
|
148
|
+
`task_create_key_digest: ${params.keyDigest}`,
|
|
149
|
+
`task_create_candidate_digest: ${params.candidateDigest}`,
|
|
150
|
+
'---'
|
|
151
|
+
].join('\n');
|
|
152
|
+
let content = params.template.replace(/^---\n[\s\S]*?\n---/, frontmatter);
|
|
153
|
+
content = content.replace('# 任务:[标题]', `# 任务:${candidate.title}`);
|
|
154
|
+
content = content.replace('[清晰简洁地描述任务。]', markdownBlock(candidate.description));
|
|
155
|
+
const sections = [
|
|
156
|
+
['### 来源', 'sources'], ['### 已确认事实与证据', 'facts'], ['### 约束', 'constraints'],
|
|
157
|
+
['### 已确认决策', 'decisions'], ['### 候选与否决方案', 'alternatives'],
|
|
158
|
+
['### 验收标准', 'acceptanceCriteria'], ['### 未决事项', 'openQuestions']
|
|
159
|
+
];
|
|
160
|
+
for (const [heading, key] of sections)
|
|
161
|
+
content = replaceEmptySubsection(content, heading, candidate.taskInput[key]);
|
|
162
|
+
content = content.replace('- **关联 Issue**:#XXX', '- **关联 Issue**:N/A');
|
|
163
|
+
content = content.replace('- **关联 PR**:#XXX', '- **关联 PR**:N/A');
|
|
164
|
+
content = content.replace('- **分支**:`feature/xxx`', `- **分支**:\`${branch}\``);
|
|
165
|
+
const log = [
|
|
166
|
+
`- ${params.timestamp} — **Create Task [started]** by ${candidate.agent} — started`,
|
|
167
|
+
`- ${params.timestamp} — **Create Task** by ${candidate.agent} — Task created from structured candidate`
|
|
168
|
+
].join('\n');
|
|
169
|
+
content = content.replace('\n## 完成检查清单', `\n${log}\n\n## 完成检查清单`);
|
|
170
|
+
return `${content.trimEnd()}\n`;
|
|
171
|
+
}
|
|
172
|
+
function readProject(repoRoot) {
|
|
173
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
174
|
+
if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
|
|
175
|
+
throw new Error('TASK_CREATE_CONFIG_INVALID: project is missing or invalid');
|
|
176
|
+
}
|
|
177
|
+
return config.project;
|
|
178
|
+
}
|
|
179
|
+
function assertRealDirectory(directory, code) {
|
|
180
|
+
const stat = fs.lstatSync(directory);
|
|
181
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
182
|
+
throw new Error(code);
|
|
183
|
+
}
|
|
184
|
+
function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
185
|
+
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
186
|
+
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
187
|
+
assertRealDirectory(lockRoot, 'TASK_CREATE_LOCK_FAILED');
|
|
188
|
+
let callbackError;
|
|
189
|
+
try {
|
|
190
|
+
return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
|
|
191
|
+
try {
|
|
192
|
+
return operation();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
callbackError = error;
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}, { lockRoot });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (callbackError !== undefined)
|
|
202
|
+
throw callbackError;
|
|
203
|
+
if (error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY') {
|
|
204
|
+
throw new Error('TASK_CREATE_LOCK_TIMEOUT');
|
|
205
|
+
}
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function writeReceipt(receiptPath, value) {
|
|
211
|
+
const temporary = `${receiptPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
212
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
213
|
+
try {
|
|
214
|
+
fs.renameSync(temporary, receiptPath);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
try {
|
|
218
|
+
fs.unlinkSync(temporary);
|
|
219
|
+
}
|
|
220
|
+
catch { /* best effort */ }
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest) {
|
|
225
|
+
for (const name of fs.readdirSync(activeRoot)) {
|
|
226
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(name))
|
|
227
|
+
continue;
|
|
228
|
+
const taskDir = path.join(activeRoot, name);
|
|
229
|
+
const stat = fs.lstatSync(taskDir);
|
|
230
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
231
|
+
continue;
|
|
232
|
+
const taskMd = path.join(taskDir, 'task.md');
|
|
233
|
+
if (!fs.existsSync(taskMd))
|
|
234
|
+
continue;
|
|
235
|
+
const taskStat = fs.lstatSync(taskMd);
|
|
236
|
+
if (!taskStat.isFile() || taskStat.isSymbolicLink())
|
|
237
|
+
continue;
|
|
238
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
239
|
+
if (!content.includes(`task_create_key_digest: ${keyDigest}`))
|
|
240
|
+
continue;
|
|
241
|
+
if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
|
|
242
|
+
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
243
|
+
}
|
|
244
|
+
const shortId = loadShortIdByTaskId(repoRoot).get(name)
|
|
245
|
+
?? mutateShortIdRegistry(repoRoot, name, 'alloc').shortId;
|
|
246
|
+
if (!shortId)
|
|
247
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
248
|
+
return { taskId: name, shortId };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function createLocalTask(value, options) {
|
|
253
|
+
const candidate = validateTaskCreateCandidate(value);
|
|
254
|
+
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
255
|
+
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
256
|
+
const activeRoot = path.join(workspaceRoot, 'active');
|
|
257
|
+
assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
258
|
+
assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
259
|
+
const project = readProject(repoRoot);
|
|
260
|
+
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
261
|
+
const candidateDigest = sha256(canonical);
|
|
262
|
+
const keyDigest = sha256(candidate.idempotencyKey);
|
|
263
|
+
const receipts = path.join(workspaceRoot, '.task-create');
|
|
264
|
+
return withCreateLock(repoRoot, workspaceRoot, () => {
|
|
265
|
+
if (fs.existsSync(receipts))
|
|
266
|
+
assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
267
|
+
else
|
|
268
|
+
fs.mkdirSync(receipts, { mode: 0o700 });
|
|
269
|
+
const receiptPath = path.join(receipts, `${keyDigest}.json`);
|
|
270
|
+
if (fs.existsSync(receiptPath)) {
|
|
271
|
+
const stat = fs.lstatSync(receiptPath);
|
|
272
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
273
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
274
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
|
275
|
+
if (receipt.candidateDigest !== candidateDigest)
|
|
276
|
+
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
277
|
+
if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
|
|
278
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
279
|
+
}
|
|
280
|
+
return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
|
|
281
|
+
}
|
|
282
|
+
const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
|
|
283
|
+
if (recovered) {
|
|
284
|
+
writeReceipt(receiptPath, {
|
|
285
|
+
version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
|
|
286
|
+
});
|
|
287
|
+
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
|
|
288
|
+
}
|
|
289
|
+
const base = options.now?.() ?? new Date();
|
|
290
|
+
let taskDate = new Date(base.getTime());
|
|
291
|
+
let taskId = taskIdFor(taskDate);
|
|
292
|
+
while (fs.existsSync(path.join(activeRoot, taskId))) {
|
|
293
|
+
taskDate = new Date(taskDate.getTime() + 1_000);
|
|
294
|
+
taskId = taskIdFor(taskDate);
|
|
295
|
+
}
|
|
296
|
+
const timestamp = formatTimestamp(base);
|
|
297
|
+
const templatePath = path.join(repoRoot, '.agents', 'templates', 'task.md');
|
|
298
|
+
const templateStat = fs.lstatSync(templatePath);
|
|
299
|
+
if (!templateStat.isFile() || templateStat.isSymbolicLink())
|
|
300
|
+
throw new Error('TASK_CREATE_TEMPLATE_INVALID');
|
|
301
|
+
const rendered = renderTask({
|
|
302
|
+
template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
|
|
303
|
+
agentInfraVersion: options.agentInfraVersion ?? 'unknown', keyDigest, candidateDigest
|
|
304
|
+
});
|
|
305
|
+
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
306
|
+
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
307
|
+
fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
|
|
308
|
+
const destination = path.join(activeRoot, taskId);
|
|
309
|
+
fs.renameSync(temporary, destination);
|
|
310
|
+
let shortId;
|
|
311
|
+
try {
|
|
312
|
+
const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
|
|
313
|
+
if (!allocated.shortId)
|
|
314
|
+
throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
315
|
+
shortId = allocated.shortId;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
mutateShortIdRegistry(repoRoot, taskId, 'release');
|
|
326
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
return { status: 'applied', changed: true, task: { id: taskId, shortId } };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export { canonicalTaskCreateCandidate, createLocalTask, validateTaskCreateCandidate };
|
|
333
|
+
//# sourceMappingURL=create.js.map
|