@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.
Files changed (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -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
@@ -23,6 +23,7 @@ function prepareDelegation(input, options = {}) {
23
23
  spawnMode: null,
24
24
  agent: null,
25
25
  status: 'prepared',
26
+ hostEvidence: null,
26
27
  afterFingerprint: null,
27
28
  changedPaths: Object.freeze([]),
28
29
  createdAt: (options.now ?? (() => new Date().toISOString()))(),
@@ -65,6 +66,12 @@ function activateDelegation(receipt, event, options = {}) {
65
66
  if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
66
67
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
67
68
  }
69
+ if (event.hostEvidence
70
+ && (receipt.client !== 'codex'
71
+ || !event.hostEvidence.hookDefinitionHash.trim()
72
+ || !Number.isSafeInteger(event.hostEvidence.startRevision)
73
+ || event.hostEvidence.startRevision < 1))
74
+ return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
68
75
  return { ok: true, receipt: Object.freeze({
69
76
  ...receipt,
70
77
  status: 'activated',
@@ -75,6 +82,12 @@ function activateDelegation(receipt, event, options = {}) {
75
82
  actualReasoningEffort,
76
83
  modelFallbackReason: event.modelFallbackReason ?? null,
77
84
  reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
85
+ hostEvidence: event.hostEvidence ? Object.freeze({
86
+ ...event.hostEvidence,
87
+ stopRevision: null,
88
+ consumer: null,
89
+ consumedAt: null
90
+ }) : receipt.hostEvidence ?? null,
78
91
  activatedAt: (options.now ?? (() => new Date().toISOString()))()
79
92
  }) };
80
93
  }
@@ -105,11 +118,23 @@ function sealDelegation(receipt, event, options = {}) {
105
118
  if (disallowed)
106
119
  return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
107
120
  }
121
+ if (event.hostEvidence) {
122
+ if (!receipt.hostEvidence
123
+ || !Number.isSafeInteger(event.hostEvidence.stopRevision)
124
+ || event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
125
+ || event.hostEvidence.consumer !== receipt.id
126
+ || !event.hostEvidence.consumedAt.trim())
127
+ return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
128
+ }
108
129
  return { ok: true, receipt: Object.freeze({
109
130
  ...receipt,
110
131
  status: 'sealed',
111
132
  afterFingerprint: event.afterFingerprint,
112
133
  changedPaths: Object.freeze([...event.changedPaths]),
134
+ hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
135
+ ...receipt.hostEvidence,
136
+ ...event.hostEvidence
137
+ }) : receipt.hostEvidence ?? null,
113
138
  sealedAt: (options.now ?? (() => new Date().toISOString()))()
114
139
  }) };
115
140
  }
@@ -18,7 +18,8 @@ const eventCatalog = [
18
18
  'review-plan.started', 'review-plan.completed',
19
19
  'code.started', 'code.completed',
20
20
  'review-code.started', 'review-code.completed',
21
- 'manual-validation.started', 'manual-validation.completed'
21
+ 'manual-validation.started', 'manual-validation.completed',
22
+ 'validation-run.started', 'validation-run.completed'
22
23
  ];
23
24
  const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun']);
24
25
  const SCHEMAS = {
@@ -36,7 +37,9 @@ const SCHEMAS = {
36
37
  'review-code.started': { optional: ['round'] },
37
38
  'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
38
39
  'manual-validation.started': { optional: ['round'] },
39
- 'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] }
40
+ 'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] },
41
+ 'validation-run.started': { optional: ['round'] },
42
+ 'validation-run.completed': { required: ['artifact'], optional: ['round'] }
40
43
  };
41
44
  function validateTaskEventRequest(request) {
42
45
  if (!eventCatalog.includes(request.event))
@@ -84,7 +87,8 @@ const FAMILY = {
84
87
  'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
85
88
  code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
86
89
  'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
87
- 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
90
+ 'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
91
+ 'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
88
92
  };
89
93
  const REVIEW_LEDGER_STAGES = {
90
94
  'review-analysis': 'analysis',
@@ -180,6 +184,8 @@ function identity(request) {
180
184
  note = `Fixed ${request.blockers} blockers, ${request.major} major, ${request.minor} minor issues${request.manualValidation ? `, skipped ${request.manualValidation} manual-validation` : ''} → ${request.artifact}`;
181
185
  else if (family === 'code')
182
186
  note = `Code implemented, ${request.filesModified} files modified, ${request.testsPassed} tests passed → ${request.artifact}`;
187
+ else if (family === 'validation-run')
188
+ note = `Validation evidence recorded → ${request.artifact}`;
183
189
  else {
184
190
  const verdict = request.verdict === 'approved' ? 'Approved' : request.verdict === 'changes-requested' ? 'Changes Requested' : 'Rejected';
185
191
  note = `Verdict: ${verdict}, blockers: ${request.blockers}, major: ${request.major}, minor: ${request.minor}, Manual-validation: ${request.manualValidation} → ${request.artifact}`;
@@ -329,7 +335,7 @@ function applyTaskEventUnlocked(request, options = {}) {
329
335
  if (findingCountError)
330
336
  return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
331
337
  let orchestrationCompletion = null;
332
- if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation') {
338
+ if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
333
339
  const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
334
340
  const execution = validateLifecycleExecution(normalized.taskRef, {
335
341
  mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
@@ -6,7 +6,8 @@ const COMPLETION_BACKFILL_FAMILIES = [
6
6
  'review-plan',
7
7
  'code',
8
8
  'review-code',
9
- 'manual-validation'
9
+ 'manual-validation',
10
+ 'validation-run'
10
11
  ];
11
12
  function inspectCompletionArtifacts(taskRef, options = {}) {
12
13
  const artifacts = [];
@@ -114,14 +114,8 @@ function finalizeImplementationInput(rows, id, decisionEvidence, decidedAt) {
114
114
  next.forEach(validateInput);
115
115
  return next;
116
116
  }
117
- function selectPendingImplementationInput(rows, reviewCompletedAt) {
118
- const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
119
- if (!Number.isFinite(reviewEpoch))
120
- throw new Error('latest review completion time is invalid');
117
+ function selectPendingImplementationInput(rows) {
121
118
  const pending = rows.filter((row) => row.needsImplementation && row.status === 'pending');
122
- const stale = pending.find((row) => Date.parse(row.decidedAt.replace(' ', 'T')) <= reviewEpoch);
123
- if (stale)
124
- throw new Error(`implementation input ${stale.id} is not later than the latest approved review`);
125
119
  return [...pending].sort((left, right) => {
126
120
  const time = Date.parse(left.decidedAt.replace(' ', 'T')) - Date.parse(right.decidedAt.replace(' ', 'T'));
127
121
  return time || Number.parseInt(left.id.slice(3), 10) - Number.parseInt(right.id.slice(3), 10);
@@ -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
  export async function runTask(args) {
@@ -89,6 +91,13 @@ export async function runTask(args) {
89
91
  status(rest);
90
92
  break;
91
93
  }
94
+ case 'validate': {
95
+ const { validate } = await import('./commands/validate.js');
96
+ const exitCode = await validate(rest);
97
+ if (exitCode !== 0)
98
+ process.exitCode = exitCode;
99
+ break;
100
+ }
92
101
  default:
93
102
  process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
94
103
  process.stdout.write(`${USAGE}\n`);