@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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 +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -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 +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- 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/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -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
|
+
};
|
|
@@ -6,6 +6,14 @@ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
|
6
6
|
type DelegationRole = 'executor' | 'reviewer';
|
|
7
7
|
type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
|
|
8
8
|
type DelegationStatus = 'prepared' | 'activated' | 'stage-completed' | 'sealed' | 'consumed' | 'aborted' | 'expired';
|
|
9
|
+
type DelegationHostEvidence = Readonly<{
|
|
10
|
+
kind: 'codex-lifecycle-v1';
|
|
11
|
+
hookDefinitionHash: string;
|
|
12
|
+
startRevision: number;
|
|
13
|
+
stopRevision: number | null;
|
|
14
|
+
consumer: string | null;
|
|
15
|
+
consumedAt: string | null;
|
|
16
|
+
}>;
|
|
9
17
|
|
|
10
18
|
type DelegationReceipt = Readonly<{
|
|
11
19
|
id: string;
|
|
@@ -28,6 +36,7 @@ type DelegationReceipt = Readonly<{
|
|
|
28
36
|
agent: string | null;
|
|
29
37
|
status: DelegationStatus;
|
|
30
38
|
workspaceSnapshotScope?: 'task';
|
|
39
|
+
hostEvidence?: DelegationHostEvidence | null;
|
|
31
40
|
beforeFingerprint: string;
|
|
32
41
|
afterFingerprint: string | null;
|
|
33
42
|
changedPaths: readonly string[];
|
|
@@ -55,7 +64,7 @@ function fail(code: string, message: string): ReceiptFailure {
|
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
function prepareDelegation(
|
|
58
|
-
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
67
|
+
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
59
68
|
options: { id?: () => string; now?: () => string } = {}
|
|
60
69
|
): DelegationReceipt {
|
|
61
70
|
return Object.freeze({
|
|
@@ -70,6 +79,7 @@ function prepareDelegation(
|
|
|
70
79
|
spawnMode: null,
|
|
71
80
|
agent: null,
|
|
72
81
|
status: 'prepared' as const,
|
|
82
|
+
hostEvidence: null,
|
|
73
83
|
afterFingerprint: null,
|
|
74
84
|
changedPaths: Object.freeze([]),
|
|
75
85
|
createdAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
@@ -90,6 +100,11 @@ function activateDelegation(
|
|
|
90
100
|
actualReasoningEffort?: string;
|
|
91
101
|
modelFallbackReason?: string;
|
|
92
102
|
reasoningEffortFallbackReason?: string;
|
|
103
|
+
hostEvidence?: Readonly<{
|
|
104
|
+
kind: 'codex-lifecycle-v1';
|
|
105
|
+
hookDefinitionHash: string;
|
|
106
|
+
startRevision: number;
|
|
107
|
+
}>;
|
|
93
108
|
}>,
|
|
94
109
|
options: { now?: () => string } = {}
|
|
95
110
|
): ReceiptResult {
|
|
@@ -124,6 +139,15 @@ function activateDelegation(
|
|
|
124
139
|
if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
|
|
125
140
|
return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
|
|
126
141
|
}
|
|
142
|
+
if (
|
|
143
|
+
event.hostEvidence
|
|
144
|
+
&& (
|
|
145
|
+
receipt.client !== 'codex'
|
|
146
|
+
|| !event.hostEvidence.hookDefinitionHash.trim()
|
|
147
|
+
|| !Number.isSafeInteger(event.hostEvidence.startRevision)
|
|
148
|
+
|| event.hostEvidence.startRevision < 1
|
|
149
|
+
)
|
|
150
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
|
|
127
151
|
return { ok: true, receipt: Object.freeze({
|
|
128
152
|
...receipt,
|
|
129
153
|
status: 'activated',
|
|
@@ -134,6 +158,12 @@ function activateDelegation(
|
|
|
134
158
|
actualReasoningEffort,
|
|
135
159
|
modelFallbackReason: event.modelFallbackReason ?? null,
|
|
136
160
|
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
|
|
161
|
+
hostEvidence: event.hostEvidence ? Object.freeze({
|
|
162
|
+
...event.hostEvidence,
|
|
163
|
+
stopRevision: null,
|
|
164
|
+
consumer: null,
|
|
165
|
+
consumedAt: null
|
|
166
|
+
}) : receipt.hostEvidence ?? null,
|
|
137
167
|
activatedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
138
168
|
}) };
|
|
139
169
|
}
|
|
@@ -154,7 +184,13 @@ function completeDelegationStage(
|
|
|
154
184
|
|
|
155
185
|
function sealDelegation(
|
|
156
186
|
receipt: DelegationReceipt,
|
|
157
|
-
event: Readonly<{
|
|
187
|
+
event: Readonly<{
|
|
188
|
+
childId: string;
|
|
189
|
+
exitCode: number;
|
|
190
|
+
afterFingerprint: string;
|
|
191
|
+
changedPaths: readonly string[];
|
|
192
|
+
hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
|
|
193
|
+
}>,
|
|
158
194
|
options: { now?: () => string } = {}
|
|
159
195
|
): ReceiptResult {
|
|
160
196
|
if (receipt.status !== 'stage-completed') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
|
|
@@ -169,11 +205,24 @@ function sealDelegation(
|
|
|
169
205
|
const disallowed = event.changedPaths.find((entry) => !allowed.has(entry));
|
|
170
206
|
if (disallowed) return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
|
|
171
207
|
}
|
|
208
|
+
if (event.hostEvidence) {
|
|
209
|
+
if (
|
|
210
|
+
!receipt.hostEvidence
|
|
211
|
+
|| !Number.isSafeInteger(event.hostEvidence.stopRevision)
|
|
212
|
+
|| event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
|
|
213
|
+
|| event.hostEvidence.consumer !== receipt.id
|
|
214
|
+
|| !event.hostEvidence.consumedAt.trim()
|
|
215
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
|
|
216
|
+
}
|
|
172
217
|
return { ok: true, receipt: Object.freeze({
|
|
173
218
|
...receipt,
|
|
174
219
|
status: 'sealed',
|
|
175
220
|
afterFingerprint: event.afterFingerprint,
|
|
176
221
|
changedPaths: Object.freeze([...event.changedPaths]),
|
|
222
|
+
hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
|
|
223
|
+
...receipt.hostEvidence,
|
|
224
|
+
...event.hostEvidence
|
|
225
|
+
}) : receipt.hostEvidence ?? null,
|
|
177
226
|
sealedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
178
227
|
}) };
|
|
179
228
|
}
|
|
@@ -196,4 +245,4 @@ export {
|
|
|
196
245
|
prepareDelegation,
|
|
197
246
|
sealDelegation
|
|
198
247
|
};
|
|
199
|
-
export type { DelegationReceipt, DelegationRole, DelegationStage, DelegationStatus, ReceiptResult };
|
|
248
|
+
export type { DelegationHostEvidence, DelegationReceipt, DelegationRole, DelegationStage, DelegationStatus, ReceiptResult };
|
package/lib/task/events.ts
CHANGED
|
@@ -35,7 +35,8 @@ const eventCatalog = [
|
|
|
35
35
|
'review-plan.started', 'review-plan.completed',
|
|
36
36
|
'code.started', 'code.completed',
|
|
37
37
|
'review-code.started', 'review-code.completed',
|
|
38
|
-
'manual-validation.started', 'manual-validation.completed'
|
|
38
|
+
'manual-validation.started', 'manual-validation.completed',
|
|
39
|
+
'validation-run.started', 'validation-run.completed'
|
|
39
40
|
] as const;
|
|
40
41
|
type TaskEventName = (typeof eventCatalog)[number];
|
|
41
42
|
type Verdict = 'approved' | 'changes-requested' | 'rejected';
|
|
@@ -83,7 +84,9 @@ const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[]
|
|
|
83
84
|
'review-code.started': { optional: ['round'] },
|
|
84
85
|
'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
85
86
|
'manual-validation.started': { optional: ['round'] },
|
|
86
|
-
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] }
|
|
87
|
+
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] },
|
|
88
|
+
'validation-run.started': { optional: ['round'] },
|
|
89
|
+
'validation-run.completed': { required: ['artifact'], optional: ['round'] }
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
function validateTaskEventRequest(request: TaskEventRequest): TaskEventError | null {
|
|
@@ -121,7 +124,8 @@ const FAMILY = {
|
|
|
121
124
|
'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
|
|
122
125
|
code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
|
|
123
126
|
'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
|
|
124
|
-
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
|
|
127
|
+
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
|
|
128
|
+
'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
|
|
125
129
|
} as const;
|
|
126
130
|
type EventFamily = keyof typeof FAMILY;
|
|
127
131
|
|
|
@@ -217,6 +221,7 @@ function identity(request: TaskEventRequest) {
|
|
|
217
221
|
else if (family === 'plan') note = `Plan completed, awaiting human review → ${request.artifact}`;
|
|
218
222
|
else if (family === 'code' && request.fixFor) note = `Fixed ${request.blockers} blockers, ${request.major} major, ${request.minor} minor issues${request.manualValidation ? `, skipped ${request.manualValidation} manual-validation` : ''} → ${request.artifact}`;
|
|
219
223
|
else if (family === 'code') note = `Code implemented, ${request.filesModified} files modified, ${request.testsPassed} tests passed → ${request.artifact}`;
|
|
224
|
+
else if (family === 'validation-run') note = `Validation evidence recorded → ${request.artifact}`;
|
|
220
225
|
else {
|
|
221
226
|
const verdict = request.verdict === 'approved' ? 'Approved' : request.verdict === 'changes-requested' ? 'Changes Requested' : 'Rejected';
|
|
222
227
|
note = `Verdict: ${verdict}, blockers: ${request.blockers}, major: ${request.major}, minor: ${request.minor}, Manual-validation: ${request.manualValidation} → ${request.artifact}`;
|
|
@@ -342,7 +347,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
|
|
|
342
347
|
);
|
|
343
348
|
if (findingCountError) return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
344
349
|
let orchestrationCompletion: OrchestrationStageCompletion | null = null;
|
|
345
|
-
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation') {
|
|
350
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
346
351
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
347
352
|
const execution = validateLifecycleExecution(normalized.taskRef, {
|
|
348
353
|
mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
|
|
@@ -131,13 +131,9 @@ function finalizeImplementationInput(
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function selectPendingImplementationInput(
|
|
134
|
-
rows: readonly ImplementationInput[]
|
|
134
|
+
rows: readonly ImplementationInput[]
|
|
135
135
|
): ImplementationInput | null {
|
|
136
|
-
const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
|
|
137
|
-
if (!Number.isFinite(reviewEpoch)) throw new Error('latest review completion time is invalid');
|
|
138
136
|
const pending = rows.filter((row) => row.needsImplementation && row.status === 'pending');
|
|
139
|
-
const stale = pending.find((row) => Date.parse(row.decidedAt.replace(' ', 'T')) <= reviewEpoch);
|
|
140
|
-
if (stale) throw new Error(`implementation input ${stale.id} is not later than the latest approved review`);
|
|
141
137
|
return [...pending].sort((left, right) => {
|
|
142
138
|
const time = Date.parse(left.decidedAt.replace(' ', 'T')) - Date.parse(right.decidedAt.replace(' ', 'T'));
|
|
143
139
|
return time || Number.parseInt(left.id.slice(3), 10) - Number.parseInt(right.id.slice(3), 10);
|
package/lib/task/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ Commands:
|
|
|
10
10
|
ls [--all | --blocked | --completed] List tasks (default: active)
|
|
11
11
|
show [--task <ref>] Print a task.md
|
|
12
12
|
status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
|
|
13
|
+
validate <ref> [options] -- <command> Run host validation in snapshot or inplace scope
|
|
13
14
|
|
|
14
15
|
Examples:
|
|
15
16
|
ai task cat 11 analysis
|
|
@@ -29,6 +30,7 @@ Examples:
|
|
|
29
30
|
ai task show 11
|
|
30
31
|
ai task show TASK-20260612-162737
|
|
31
32
|
ai task status 11
|
|
33
|
+
ai task validate 11 -- npm test
|
|
32
34
|
|
|
33
35
|
Run 'ai task <command> --help' for details.`;
|
|
34
36
|
|
|
@@ -93,6 +95,12 @@ export async function runTask(args: string[]): Promise<void> {
|
|
|
93
95
|
status(rest);
|
|
94
96
|
break;
|
|
95
97
|
}
|
|
98
|
+
case 'validate': {
|
|
99
|
+
const { validate } = await import('./commands/validate.ts');
|
|
100
|
+
const exitCode = await validate(rest);
|
|
101
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
96
104
|
default:
|
|
97
105
|
process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
|
|
98
106
|
process.stdout.write(`${USAGE}\n`);
|