@fitlab-ai/agent-infra 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { syncPlatformComment } from '../platform/issue-comments.ts';
5
+ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
6
+ import type { PlatformResult } from '../platform/types.ts';
7
+ import { VERSION } from '../version.ts';
8
+ import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
9
+ import { createLocalTask, type TaskCreateCandidateV1 } from './create.ts';
10
+ import { verifyTaskEvent } from './verification.ts';
11
+
12
+ type TaskCreateStatus = 'applied' | 'no-op' | 'degraded' | 'failed' | 'blocked';
13
+ type TaskCreateOperation = { name: string; status: string; reasonCode: string | null };
14
+ type TaskCreateWarning = { code: string; severity: string; action: string };
15
+ type TaskCreateResult = Readonly<{
16
+ status: TaskCreateStatus;
17
+ changed: boolean;
18
+ task: { id: string | null; shortId: string | null };
19
+ issue: { number: number; url: string } | null;
20
+ operations: readonly TaskCreateOperation[];
21
+ warnings: readonly TaskCreateWarning[];
22
+ error: { code: string; message: string; retryable: boolean } | null;
23
+ }>;
24
+
25
+ type CreateTaskOptions = Readonly<{
26
+ repoRoot: string;
27
+ agentInfraVersion?: string;
28
+ dependencies?: Partial<TaskCreateDependencies>;
29
+ }>;
30
+
31
+ type TaskCreateDependencies = Readonly<{
32
+ createIssue: typeof createPlatformIssue;
33
+ syncIssue: typeof syncPlatformIssue;
34
+ syncComment: typeof syncPlatformComment;
35
+ addWarning: typeof applyWorkflowWarningIntent;
36
+ }>;
37
+
38
+ const DEFAULT_DEPENDENCIES: TaskCreateDependencies = {
39
+ createIssue: createPlatformIssue,
40
+ syncIssue: syncPlatformIssue,
41
+ syncComment: syncPlatformComment,
42
+ addWarning: applyWorkflowWarningIntent
43
+ };
44
+
45
+ function verifyCreatedTask(repoRoot: string, taskId: string): { operation: TaskCreateOperation; error: TaskCreateResult['error'] } {
46
+ const verification = verifyTaskEvent(
47
+ { taskRef: taskId, event: 'create-task.completed' },
48
+ { repoRoot }
49
+ );
50
+ if (verification.status === 'pass') {
51
+ return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
52
+ }
53
+ const blocked = verification.status === 'blocked';
54
+ return {
55
+ operation: {
56
+ name: 'task:verify', status: verification.status,
57
+ reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
58
+ },
59
+ error: {
60
+ code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
61
+ message: `Created task did not pass the ${verification.event} gate`,
62
+ retryable: blocked
63
+ }
64
+ };
65
+ }
66
+
67
+ function platformOperations(prefix: string, operations: readonly { name: string; status: string; reasonCode?: string | null }[]): TaskCreateOperation[] {
68
+ return operations.map((operation) => ({
69
+ name: `${prefix}:${operation.name}`,
70
+ status: operation.status,
71
+ reasonCode: operation.reasonCode ?? null
72
+ }));
73
+ }
74
+
75
+ function issueFromTask(repoRoot: string, taskId: string): { number: number; url: string } | null {
76
+ const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
77
+ const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
78
+ return match ? { number: Number(match[1]), url: '' } : null;
79
+ }
80
+
81
+ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResult {
82
+ const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
83
+ let candidate: TaskCreateCandidateV1;
84
+ let local;
85
+ try {
86
+ candidate = value as TaskCreateCandidateV1;
87
+ local = createLocalTask(candidate, {
88
+ repoRoot: options.repoRoot,
89
+ agentInfraVersion: options.agentInfraVersion ?? VERSION
90
+ });
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
94
+ return {
95
+ status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
96
+ operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
97
+ };
98
+ }
99
+
100
+ const operations: TaskCreateOperation[] = [{
101
+ name: 'task:local', status: local.status, reasonCode: null
102
+ }];
103
+ let issue = issueFromTask(options.repoRoot, local.task.id);
104
+ const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
105
+ operations.push(...platformOperations('platform-create', created.operations));
106
+ if (created.issue) issue = { number: created.issue.number, url: created.issue.url };
107
+
108
+ let platformFailure: PlatformResult | null = created.status === 'failed' || created.status === 'blocked' ? created : null;
109
+ if (!platformFailure && (created.task.issueNumber || issue)) {
110
+ const synced = dependencies.syncIssue(local.task.id, {
111
+ cwd: options.repoRoot,
112
+ agent: candidate.agent,
113
+ status: 'waiting-for-triage',
114
+ assignees: 'current',
115
+ milestone: 'initial',
116
+ issueType: true,
117
+ fields: true
118
+ });
119
+ operations.push(...platformOperations('platform-sync', synced.operations));
120
+ if (synced.issue) issue = { number: synced.issue.number, url: synced.issue.url };
121
+ if (synced.status === 'failed' || synced.status === 'blocked') platformFailure = synced;
122
+ if (!platformFailure) {
123
+ const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
124
+ operations.push(...platformOperations('platform-comment', commented.operations));
125
+ if (commented.status === 'failed' || commented.status === 'blocked') platformFailure = commented;
126
+ }
127
+ }
128
+
129
+ if (platformFailure) {
130
+ const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
131
+ dependencies.addWarning({
132
+ kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
133
+ code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
134
+ action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
135
+ }, { repoRoot: options.repoRoot });
136
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
137
+ operations.push(verified.operation);
138
+ if (verified.error) return {
139
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
140
+ task: local.task, issue, operations, warnings: [], error: verified.error
141
+ };
142
+ return {
143
+ status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
144
+ changed: local.changed,
145
+ task: local.task,
146
+ issue,
147
+ operations,
148
+ warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
149
+ error
150
+ };
151
+ }
152
+
153
+ const verified = verifyCreatedTask(options.repoRoot, local.task.id);
154
+ operations.push(verified.operation);
155
+ if (verified.error) return {
156
+ status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
157
+ task: local.task, issue, operations, warnings: [], error: verified.error
158
+ };
159
+ return {
160
+ status: local.status,
161
+ changed: local.changed,
162
+ task: local.task,
163
+ issue,
164
+ operations,
165
+ warnings: [],
166
+ error: null
167
+ };
168
+ }
169
+
170
+ export { createTask };
171
+ export type { CreateTaskOptions, TaskCreateDependencies, TaskCreateOperation, TaskCreateResult, TaskCreateStatus, TaskCreateWarning };
@@ -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
+ };
@@ -4,9 +4,11 @@ import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
4
4
  import { isDecisionItem, listDecisionItems, selectDecisionItem } from './decision-items.ts';
5
5
  import {
6
6
  createImplementationInput,
7
+ finalizeImplementationInput,
7
8
  IMPLEMENTATION_INPUT_ALIASES,
8
9
  parseImplementationInputs,
9
- renderImplementationInputs
10
+ renderImplementationInputs,
11
+ selectDeclaredImplementationInput
10
12
  } from './implementation-inputs.ts';
11
13
  import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.ts';
12
14
  import type { LedgerRow } from './ledger.ts';
@@ -73,7 +75,7 @@ function existingDecision(
73
75
  ? { recordId, implementationInputId: null }
74
76
  : { conflict: '--needs-implementation is only valid for code-stage decisions' };
75
77
  const inputs = parseImplementationInputs(content).rows.filter((input) => input.ledgerId === row.id && input.decisionEvidence === row.evidence);
76
- if (inputs.length !== 1 || inputs[0]!.needsImplementation !== needsImplementation) {
78
+ if (inputs.length !== 1 || (needsImplementation !== undefined && inputs[0]!.needsImplementation !== needsImplementation)) {
77
79
  return { conflict: `implementation intent for '${row.id}' conflicts with the existing decision` };
78
80
  }
79
81
  return { recordId, implementationInputId: inputs[0]!.id };
@@ -106,9 +108,28 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
106
108
  return failed('DECISION_TARGET_INVALID', message, resolved.taskId);
107
109
  }
108
110
  const row = selected.row;
109
- if (row.stage === 'code' && request.needsImplementation === undefined) return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false', resolved.taskId, row.id);
110
111
  if (row.stage !== 'code' && request.needsImplementation !== undefined) return failed('DECISION_PAYLOAD_INVALID', '--needs-implementation is only valid for code-stage decisions', resolved.taskId, row.id);
111
112
 
113
+ let parsedInputs: ReturnType<typeof parseImplementationInputs> | null = null;
114
+ let declaredInput: ReturnType<typeof selectDeclaredImplementationInput> = null;
115
+ let effectiveNeedsImplementation = request.needsImplementation;
116
+ if (row.stage === 'code') {
117
+ try {
118
+ parsedInputs = parseImplementationInputs(content);
119
+ declaredInput = selectDeclaredImplementationInput(parsedInputs.rows, row.id, row.evidence);
120
+ } catch (error) {
121
+ return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
122
+ }
123
+ if (declaredInput) {
124
+ if (request.needsImplementation !== undefined && request.needsImplementation !== declaredInput.needsImplementation) {
125
+ return failed('DECISION_CONFLICT', `implementation intent for '${row.id}' conflicts with its declaration`, resolved.taskId, row.id);
126
+ }
127
+ effectiveNeedsImplementation = declaredInput.needsImplementation;
128
+ } else if (effectiveNeedsImplementation === undefined) {
129
+ return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false when no implementation intent was declared', resolved.taskId, row.id);
130
+ }
131
+ }
132
+
112
133
  let metadata;
113
134
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
114
135
  catch (error) { return failed('METADATA_CAPTURE_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId, row.id); }
@@ -132,18 +153,17 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
132
153
  ];
133
154
  let implementationInputId: string | null = null;
134
155
  if (row.stage === 'code') {
135
- let parsed;
136
- try { parsed = parseImplementationInputs(content); }
137
- catch (error) { return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id); }
138
- const input = createImplementationInput(parsed.rows, {
139
- ledgerId: row.id, decisionEvidence: evidence,
140
- needsImplementation: request.needsImplementation!, decidedAt: metadata.timestamp
141
- });
142
- implementationInputId = input.id;
156
+ const nextInputs = declaredInput
157
+ ? finalizeImplementationInput(parsedInputs!.rows, declaredInput.id, evidence, metadata.timestamp)
158
+ : [...parsedInputs!.rows, createImplementationInput(parsedInputs!.rows, {
159
+ ledgerId: row.id, decisionEvidence: evidence,
160
+ needsImplementation: effectiveNeedsImplementation!, decidedAt: metadata.timestamp
161
+ })];
162
+ implementationInputId = declaredInput?.id ?? nextInputs[nextInputs.length - 1]!.id;
143
163
  mutations.push({
144
164
  kind: 'section', aliases: IMPLEMENTATION_INPUT_ALIASES,
145
165
  heading: findSectionHeading(content, [...IMPLEMENTATION_INPUT_ALIASES]),
146
- body: renderImplementationInputs([...parsed.rows, input])
166
+ body: renderImplementationInputs(nextInputs)
147
167
  });
148
168
  }
149
169
  mutations.push({
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import type { AgentClientId } from '../agent-clients/types.ts';
4
+ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
4
5
 
5
6
  type DelegationRole = 'executor' | 'reviewer';
6
7
  type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
@@ -145,8 +146,7 @@ function completeDelegationStage(
145
146
  if (event.stage !== receipt.stage || event.round !== receipt.round || event.artifact !== receipt.artifact) {
146
147
  return fail('DELEGATION_STAGE_MISMATCH', 'stage completion identity does not match the active delegation');
147
148
  }
148
- const acceptedAgents = receipt.client === 'claude-code' ? ['claude', 'claude-code'] : [receipt.client];
149
- if (!acceptedAgents.includes(event.agent)) {
149
+ if (normalizeAgentToken(event.agent) !== normalizeAgentToken(receipt.client)) {
150
150
  return fail('DELEGATION_AGENT_MISMATCH', `stage agent '${event.agent}' does not match client '${receipt.client}'`);
151
151
  }
152
152
  return { ok: true, receipt: Object.freeze({ ...receipt, status: 'stage-completed', agent: event.agent }) };