@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -0,0 +1,430 @@
1
+ import fs from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+
4
+ import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.ts';
5
+ import { parseTaskFrontmatter } from './frontmatter.ts';
6
+ import { resolveTaskContext, resolveTaskRef } from './resolve-ref.ts';
7
+ import { commitPushDecision } from './commit-policy.ts';
8
+ import { mergeOperationWarnings, type OperationWarning } from './operation-outcome.ts';
9
+ import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
10
+ import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
11
+ import { captureTaskWriteMetadata, writeTask } from './write.ts';
12
+ import type { TaskMutation } from './write.ts';
13
+ import {
14
+ commitOrchestrationStageCompletion,
15
+ planOrchestrationStageCompletion
16
+ } from './orchestration.ts';
17
+ import type { OrchestrationStageCompletion } from './orchestration.ts';
18
+
19
+ type CommitExecutionMode = 'direct' | 'orchestrated';
20
+
21
+ type CommitOperationInput = Readonly<{
22
+ cwd: string;
23
+ paths: readonly string[];
24
+ message: string;
25
+ expectedHead: string;
26
+ expectedTree: string;
27
+ taskRef?: string;
28
+ agent?: string;
29
+ mode?: CommitExecutionMode;
30
+ push?: Readonly<{
31
+ remote: string;
32
+ refs: readonly string[];
33
+ automatic: boolean;
34
+ }>;
35
+ }>;
36
+
37
+ type CommitOperationResult = Readonly<{
38
+ status: 'applied' | 'no-op' | 'failed' | 'blocked';
39
+ changed: boolean;
40
+ result: 'committed' | 'no_op' | 'committed_with_warnings' | 'failed' | 'blocked';
41
+ taskId: string | null;
42
+ mode: CommitExecutionMode;
43
+ warnings: readonly OperationWarning[];
44
+ snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'];
45
+ operations: readonly unknown[];
46
+ error: Readonly<{ code: string; message: string }> | null;
47
+ }>;
48
+
49
+ type BoundTask = Readonly<{
50
+ repoRoot: string;
51
+ taskId: string;
52
+ taskDir: string;
53
+ taskMdPath: string;
54
+ state: 'active' | 'blocked' | 'completed' | 'archive';
55
+ branch: string;
56
+ }>;
57
+
58
+ const HEAD_REF = /^refs\/heads\/(.+)$/;
59
+
60
+ function isCommitExecutionMode(value: unknown): value is CommitExecutionMode {
61
+ return value === 'direct' || value === 'orchestrated';
62
+ }
63
+
64
+ function gitText(repoRoot: string, args: readonly string[]): string {
65
+ return execFileSync('git', [...args], {
66
+ cwd: repoRoot,
67
+ encoding: 'utf8',
68
+ stdio: ['ignore', 'pipe', 'pipe']
69
+ }).trim();
70
+ }
71
+
72
+ function canonicalRepositoryRoot(cwd: string): string {
73
+ return gitText(cwd, ['rev-parse', '--show-toplevel']);
74
+ }
75
+
76
+ function failure(
77
+ input: CommitOperationInput,
78
+ mode: CommitExecutionMode,
79
+ taskId: string | null,
80
+ code: string,
81
+ message: string,
82
+ status: 'failed' | 'blocked' = 'failed',
83
+ snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'] = null
84
+ ): CommitOperationResult {
85
+ return {
86
+ status,
87
+ changed: false,
88
+ result: status,
89
+ taskId,
90
+ mode,
91
+ warnings: [],
92
+ snapshot: snapshot ?? inspectGitWorkflow(input.cwd).snapshot,
93
+ operations: [],
94
+ error: { code, message }
95
+ };
96
+ }
97
+
98
+ function warning(
99
+ code: string,
100
+ message: string,
101
+ retryable: boolean,
102
+ step: string,
103
+ target: string,
104
+ severity: OperationWarning['severity']
105
+ ): OperationWarning {
106
+ return { code, message, retryable, step, target, severity };
107
+ }
108
+
109
+ function resolveBoundTask(input: CommitOperationInput):
110
+ | { kind: 'none' }
111
+ | { kind: 'bound'; task: BoundTask }
112
+ | { kind: 'error'; result: CommitOperationResult } {
113
+ const mode = input.mode ?? 'direct';
114
+ if (input.taskRef !== undefined) {
115
+ const resolved = resolveTaskRef(input.taskRef, { repoRoot: input.cwd });
116
+ if (!resolved.ok) return {
117
+ kind: 'error',
118
+ result: failure(input, mode, resolved.taskId, resolved.code, resolved.message)
119
+ };
120
+ return boundTask(input, resolved.taskId, resolved.taskDir, resolved.taskMdPath, resolved.repoRoot, resolved.state);
121
+ }
122
+ const context = resolveTaskContext(undefined, { repoRoot: input.cwd });
123
+ if (context.ok) return boundTask(input, context.taskId, context.taskDir, context.taskMdPath, context.repoRoot, context.state);
124
+ if (context.code === 'TASK_CONTEXT_NOT_FOUND') return { kind: 'none' };
125
+ return {
126
+ kind: 'error',
127
+ result: failure(input, mode, context.taskId, context.code, context.message)
128
+ };
129
+ }
130
+
131
+ function boundTask(
132
+ input: CommitOperationInput,
133
+ taskId: string,
134
+ taskDir: string,
135
+ taskMdPath: string,
136
+ repoRoot: string,
137
+ state: BoundTask['state']
138
+ ): { kind: 'bound'; task: BoundTask } | { kind: 'error'; result: CommitOperationResult } {
139
+ let branch: string;
140
+ try {
141
+ const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMdPath, 'utf8'));
142
+ branch = frontmatter.branch ?? '';
143
+ } catch (error) {
144
+ return {
145
+ kind: 'error',
146
+ result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', error instanceof Error ? error.message : String(error))
147
+ };
148
+ }
149
+ if (!branch) return {
150
+ kind: 'error',
151
+ result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', 'task branch is missing')
152
+ };
153
+ return { kind: 'bound', task: { repoRoot, taskId, taskDir, taskMdPath, state, branch } };
154
+ }
155
+
156
+ function validateCommon(input: CommitOperationInput, branch: string | null): Readonly<{ code: string; message: string }> | null {
157
+ if (!input.message.trim()) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit message is required' };
158
+ if (input.paths.length === 0 && !input.push) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'At least one explicit path is required unless this is a push-only retry' };
159
+ if (!/^[a-f0-9]{40,64}$/.test(input.expectedHead)) return { code: 'GIT_HEAD_EXPECTATION_REQUIRED', message: 'A valid expected HEAD is required' };
160
+ if (!/^[a-f0-9]{40,64}$/.test(input.expectedTree)) return { code: 'GIT_TREE_EXPECTATION_REQUIRED', message: 'A valid expected tree is required' };
161
+ if (input.paths.some((candidate) => !candidate || candidate.startsWith('/') || candidate.split(/[\\/]/).includes('..'))) {
162
+ return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit paths must stay inside the repository' };
163
+ }
164
+ if (input.paths.some((candidate) => /(^|\/)(?:\.env|credentials\.json|id_rsa)(?:$|\/)/i.test(candidate))) {
165
+ return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Sensitive paths cannot be committed' };
166
+ }
167
+ if (branch !== null && branch === '') return { code: 'GIT_BRANCH_INVALID', message: 'A named branch is required' };
168
+ if (input.push) {
169
+ if (!input.push.remote || input.push.refs.length !== 1) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push requires one remote and one ref' };
170
+ const ref = input.push.refs[0] ?? '';
171
+ if (!HEAD_REF.test(ref) || HEAD_REF.exec(ref)?.[1] !== branch) {
172
+ return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+
178
+ function validateOrchestrated(
179
+ input: CommitOperationInput,
180
+ task: BoundTask
181
+ ): { plan: OrchestrationStageCompletion } | { error: Readonly<{ code: string; message: string }> } {
182
+ if (!input.agent) return { error: { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'orchestrated commit requires an agent' } };
183
+ const planned = planOrchestrationStageCompletion(task.taskId, {
184
+ stage: 'commit',
185
+ round: 1,
186
+ artifact: 'commit',
187
+ role: 'executor',
188
+ agent: input.agent
189
+ }, { repoRoot: task.repoRoot });
190
+ const run = planned.result.run;
191
+ if (
192
+ !planned.plan
193
+ || !run
194
+ || run.status !== 'running'
195
+ || !run.commitAuthorization.issuedAt
196
+ || run.commitAuthorization.consumedAt
197
+ ) return {
198
+ error: {
199
+ code: planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
200
+ message: planned.result.error?.message ?? 'orchestrated commit requires one matching activated commit delegation'
201
+ }
202
+ };
203
+ return { plan: planned.plan };
204
+ }
205
+
206
+ function syncTaskCommit(
207
+ input: CommitOperationInput,
208
+ task: BoundTask,
209
+ committedHead: string
210
+ ): OperationWarning | null {
211
+ let content: string;
212
+ try {
213
+ content = fs.readFileSync(task.taskMdPath, 'utf8');
214
+ const activity = locateActivityLog(content);
215
+ if (!activity) return warning(
216
+ 'TASK_STATUS_SYNC_FAILED',
217
+ 'task has no unique Activity Log section',
218
+ true,
219
+ 'task',
220
+ task.taskId,
221
+ 'ACTION_REQUIRED'
222
+ );
223
+ const metadata = captureTaskWriteMetadata();
224
+ const commitNote = gitText(task.repoRoot, ['show', '-s', '--format=%h %s', committedHead]);
225
+ const alreadyLogged = activity.entries.some((entry) => entry.step === 'Commit' && entry.note === commitNote);
226
+ const mutations: TaskMutation[] = [];
227
+ if (!alreadyLogged) mutations.push({
228
+ kind: 'section',
229
+ aliases: ['活动日志', 'Activity Log'],
230
+ heading: activity.heading,
231
+ body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Commit', agent: input.agent!, note: commitNote })
232
+ });
233
+ const frontmatter: Record<string, string> = { assigned_to: input.agent! };
234
+ mutations.push({ kind: 'frontmatter', set: frontmatter });
235
+ if (mutations.length === 0) return null;
236
+ const written = writeTask({
237
+ taskRef: task.taskId,
238
+ expectedState: 'active',
239
+ mutations
240
+ }, { repoRoot: task.repoRoot, metadataProvider: () => metadata });
241
+ if (written.status === 'failed') return warning(
242
+ 'TASK_STATUS_SYNC_FAILED',
243
+ written.error.message,
244
+ true,
245
+ 'task',
246
+ task.taskId,
247
+ 'ACTION_REQUIRED'
248
+ );
249
+ return null;
250
+ } catch (error) {
251
+ return warning(
252
+ 'TASK_STATUS_SYNC_FAILED',
253
+ error instanceof Error ? error.message : String(error),
254
+ true,
255
+ 'task',
256
+ task.taskId,
257
+ 'ACTION_REQUIRED'
258
+ );
259
+ }
260
+ }
261
+
262
+ function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
263
+ const inspected = inspectGitWorkflow(input.cwd);
264
+ if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
265
+ const branch = inspected.snapshot.branch;
266
+ const commonError = validateCommon(input, branch);
267
+ if (commonError) return failure(input, mode, task?.taskId ?? null, commonError.code, commonError.message, 'blocked', inspected.snapshot);
268
+ if (task && task.state !== 'active') return failure(input, mode, task.taskId, 'TASK_STATE_INVALID', 'task-bound commit requires an active task', 'blocked', inspected.snapshot);
269
+ if (task && task.branch !== branch) return failure(input, mode, task.taskId, 'GIT_BRANCH_MISMATCH', `Task branch '${task.branch}' does not match current branch '${branch}'`, 'blocked', inspected.snapshot);
270
+ if (task && !input.agent) return failure(input, mode, task.taskId, 'COMMIT_AGENT_REQUIRED', 'task-bound commit requires an agent', 'blocked', inspected.snapshot);
271
+ const orchestrationValidation = mode === 'orchestrated'
272
+ ? (task
273
+ ? validateOrchestrated(input, task)
274
+ : { error: { code: 'ORCHESTRATION_TASK_REQUIRED', message: 'orchestrated commit requires a task context' } })
275
+ : null;
276
+ if (orchestrationValidation && 'error' in orchestrationValidation) {
277
+ const error = orchestrationValidation.error;
278
+ return failure(input, mode, task?.taskId ?? null, error.code, error.message, 'blocked', inspected.snapshot);
279
+ }
280
+ const orchestrationCompletion = orchestrationValidation && 'plan' in orchestrationValidation
281
+ ? orchestrationValidation.plan
282
+ : null;
283
+
284
+ const committed = input.paths.length === 0
285
+ ? (() => {
286
+ if (inspected.snapshot.worktree.length > 0 || inspected.snapshot.staged.length > 0) return {
287
+ status: 'failed' as const,
288
+ changed: false,
289
+ snapshot: inspected.snapshot,
290
+ operations: [],
291
+ error: { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Push-only retry requires a clean working tree' }
292
+ };
293
+ if (inspected.snapshot.head !== input.expectedHead) return {
294
+ status: 'failed' as const,
295
+ changed: false,
296
+ snapshot: inspected.snapshot,
297
+ operations: [],
298
+ error: { code: 'GIT_HEAD_MISMATCH', message: `Expected HEAD ${input.expectedHead}, received ${inspected.snapshot.head}` }
299
+ };
300
+ const currentTree = gitText(input.cwd, ['rev-parse', 'HEAD^{tree}']);
301
+ if (currentTree !== input.expectedTree) return {
302
+ status: 'failed' as const,
303
+ changed: false,
304
+ snapshot: inspected.snapshot,
305
+ operations: [],
306
+ error: { code: 'GIT_TREE_MISMATCH', message: `Expected current tree ${input.expectedTree}, received ${currentTree}` }
307
+ };
308
+ return {
309
+ status: 'no-op' as const,
310
+ changed: false,
311
+ snapshot: inspected.snapshot,
312
+ operations: [{ name: 'commit', status: 'no-op' as const }],
313
+ error: null
314
+ };
315
+ })()
316
+ : commitExplicitPaths({
317
+ cwd: input.cwd,
318
+ paths: input.paths,
319
+ message: input.message,
320
+ expectedHead: input.expectedHead,
321
+ expectedTree: input.expectedTree
322
+ });
323
+ if (committed.status === 'failed') return {
324
+ ...committed,
325
+ result: 'failed',
326
+ taskId: task?.taskId ?? null,
327
+ mode,
328
+ warnings: [],
329
+ snapshot: committed.snapshot
330
+ };
331
+
332
+ const warnings: OperationWarning[] = [];
333
+ if (orchestrationCompletion) {
334
+ try {
335
+ commitOrchestrationStageCompletion(orchestrationCompletion);
336
+ } catch (error) {
337
+ warnings.push(warning(
338
+ 'ORCHESTRATION_COMPLETION_FAILED',
339
+ error instanceof Error ? error.message : String(error),
340
+ true,
341
+ 'orchestration',
342
+ task?.taskId ?? 'orchestration',
343
+ 'ACTION_REQUIRED'
344
+ ));
345
+ }
346
+ }
347
+ let status: CommitOperationResult['status'] = committed.status;
348
+ let result: CommitOperationResult['result'] = committed.status === 'applied' ? 'committed' : 'no_op';
349
+ if (input.push) {
350
+ const target = `${input.push.remote}:${input.push.refs[0]}`;
351
+ const decision = commitPushDecision({ branch, automatic: input.push.automatic }, target);
352
+ if (!decision.shouldPush) {
353
+ warnings.push(decision.warning!);
354
+ } else {
355
+ const pushed = pushGitRefs({
356
+ cwd: input.cwd,
357
+ remote: input.push.remote,
358
+ refs: input.push.refs,
359
+ expectedSha: committed.snapshot?.head ?? gitText(input.cwd, ['rev-parse', 'HEAD'])
360
+ });
361
+ if (pushed.status !== 'applied') warnings.push(warning(
362
+ 'COMMIT_PUSH_FAILED',
363
+ pushed.error?.message ?? 'Git push failed',
364
+ true,
365
+ 'push',
366
+ target,
367
+ 'ACTION_REQUIRED'
368
+ ));
369
+ }
370
+ }
371
+ if (warnings.length > 0) {
372
+ status = 'applied';
373
+ result = 'committed_with_warnings';
374
+ }
375
+ if (task && (committed.status === 'applied' || committed.status === 'no-op')) {
376
+ const taskWarning = syncTaskCommit(input, task, committed.snapshot?.head ?? gitText(task.repoRoot, ['rev-parse', 'HEAD']));
377
+ if (taskWarning) {
378
+ warnings.push(taskWarning);
379
+ status = 'applied';
380
+ result = 'committed_with_warnings';
381
+ }
382
+ }
383
+ return {
384
+ status,
385
+ changed: committed.changed,
386
+ result,
387
+ taskId: task?.taskId ?? null,
388
+ mode,
389
+ warnings: mergeOperationWarnings(warnings),
390
+ snapshot: committed.snapshot,
391
+ operations: committed.operations,
392
+ error: null
393
+ };
394
+ }
395
+
396
+ function executeCommitOperation(input: CommitOperationInput): CommitOperationResult {
397
+ if (input.mode !== undefined && !isCommitExecutionMode(input.mode)) return {
398
+ status: 'blocked',
399
+ changed: false,
400
+ result: 'blocked',
401
+ taskId: null,
402
+ mode: 'direct',
403
+ warnings: [],
404
+ snapshot: null,
405
+ operations: [],
406
+ error: { code: 'COMMIT_MODE_INVALID', message: 'commit mode must be direct or orchestrated' }
407
+ };
408
+ const mode: CommitExecutionMode = input.mode ?? 'direct';
409
+ let normalizedInput: CommitOperationInput;
410
+ try {
411
+ normalizedInput = { ...input, cwd: canonicalRepositoryRoot(input.cwd) };
412
+ } catch (error) {
413
+ return failure(input, mode, null, 'GIT_INSPECT_FAILED', error instanceof Error ? error.message : 'Unable to resolve Git repository root', 'blocked');
414
+ }
415
+ const resolved = resolveBoundTask(normalizedInput);
416
+ if (resolved.kind === 'error') return resolved.result;
417
+ const task = resolved.kind === 'bound' ? resolved.task : null;
418
+ if (mode === 'orchestrated' && !task) return failure(normalizedInput, mode, null, 'ORCHESTRATION_TASK_REQUIRED', 'orchestrated commit requires a task context', 'blocked');
419
+ try {
420
+ return task
421
+ ? withRepositoryMutationLock(normalizedInput.cwd, () => withTaskExecutionLock(normalizedInput.cwd, task.taskId, 'commit-operation', () => executeUnlocked(normalizedInput, task, mode)))
422
+ : withRepositoryMutationLock(normalizedInput.cwd, () => executeUnlocked(normalizedInput, null, mode));
423
+ } catch (error) {
424
+ const code = error instanceof TaskExecutionLockError ? error.code : 'COMMIT_OPERATION_FAILED';
425
+ return failure(normalizedInput, mode, task?.taskId ?? null, code, error instanceof Error ? error.message : String(error), 'blocked');
426
+ }
427
+ }
428
+
429
+ export { executeCommitOperation };
430
+ export type { CommitExecutionMode, CommitOperationInput, CommitOperationResult };
@@ -0,0 +1,30 @@
1
+ import type { OperationWarning } from './operation-outcome.ts';
2
+
3
+ type CommitPushPolicy = Readonly<{
4
+ branch: string;
5
+ automatic: boolean;
6
+ }>;
7
+
8
+ type CommitPushDecision = Readonly<{
9
+ shouldPush: boolean;
10
+ warning: OperationWarning | null;
11
+ }>;
12
+
13
+ function commitPushDecision(policy: CommitPushPolicy, target: string): CommitPushDecision {
14
+ const branch = policy.branch.trim();
15
+ if (policy.automatic && (branch === 'main' || branch === 'master')) return {
16
+ shouldPush: false,
17
+ warning: {
18
+ code: 'COMMIT_AUTOPUSH_PROTECTED_BRANCH',
19
+ message: `Automatic commit push is disabled for protected branch '${branch}'`,
20
+ retryable: false,
21
+ step: 'push',
22
+ target,
23
+ severity: 'IMPORTANT'
24
+ }
25
+ };
26
+ return { shouldPush: true, warning: null };
27
+ }
28
+
29
+ export { commitPushDecision };
30
+ export type { CommitPushDecision, CommitPushPolicy };
@@ -16,6 +16,7 @@ import { resolveTaskContext } from './resolve-ref.ts';
16
16
  import { extractSection, extractSubSection, findSectionHeading } from './sections.ts';
17
17
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
18
18
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
19
+ import { allowsManualOverride } from './guard-override.ts';
19
20
 
20
21
  type HumanDecisionRequest = {
21
22
  taskRef?: string;
@@ -85,7 +86,8 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
85
86
  if (!request.selector || !request.decision.trim()) return failed('DECISION_PAYLOAD_INVALID', 'selector and decision are required');
86
87
  const resolved = resolveTaskContext(request.taskRef, { repoRoot: options.repoRoot });
87
88
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
88
- if (resolved.state !== 'active') return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
+ const stateOverride = allowsManualOverride(options.manualOverride, 'decision-intent', 'TASK_STATE_MISMATCH');
90
+ if (resolved.state !== 'active' && !stateOverride) return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
91
  let content: string;
90
92
  let rows: LedgerRow[];
91
93
  try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); rows = parseLedger(content); }
@@ -170,7 +172,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
170
172
  kind: 'section', aliases: ACTIVITY_ALIASES, heading: activity.heading,
171
173
  body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Human Decision', agent: 'human', note: `${row.id} decided → ${recordId}` })
172
174
  });
173
- const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: 'active', mutations, dryRun: request.dryRun }, {
175
+ const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
174
176
  ...options,
175
177
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
176
178
  metadataProvider: () => metadata