@fitlab-ai/agent-infra 0.9.8 → 0.9.10

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 (182) 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/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -9,6 +9,8 @@ import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontma
9
9
  import { loadShortIdByTaskId } from '../short-id.ts';
10
10
  import { getOpenWorkflowWarnings, formatWorkflowWarningSummary, type WorkflowWarning } from '../workflow-warnings.ts';
11
11
  import { parseActivityLog, pairEntries } from '../activity-log.ts';
12
+ import { readRun, type OrchestrationRun } from '../orchestration.ts';
13
+ import type { DelegationReceipt } from '../delegation-receipts.ts';
12
14
  import { statusCard, type DisplayMessage } from '../../server/display.ts';
13
15
 
14
16
  const USAGE = `Usage: ai task status [<N | TASK-id> | --task <ref> | -t <ref>]
@@ -219,6 +221,19 @@ type RuntimeInfo = {
219
221
  log: string;
220
222
  };
221
223
 
224
+ type PendingDelegationInfo = Readonly<Pick<
225
+ DelegationReceipt,
226
+ 'id' | 'role' | 'stage' | 'round' | 'artifact' | 'client' | 'status'
227
+ >>;
228
+
229
+ type OrchestrationInfo = Readonly<{
230
+ status: 'absent' | OrchestrationRun['status'];
231
+ runId: string;
232
+ nextStage: string;
233
+ pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
234
+ pendingDelegation: PendingDelegationInfo | null;
235
+ }>;
236
+
222
237
  type ManagedRunRecord = {
223
238
  run_id: string;
224
239
  engine: string;
@@ -323,6 +338,39 @@ function collectRuntime(taskDir: string, workflow: WorkflowInfo, run: Runner): R
323
338
  };
324
339
  }
325
340
 
341
+ function pendingDelegationInfo(receipt: DelegationReceipt | null): PendingDelegationInfo | null {
342
+ if (!receipt) return null;
343
+ return {
344
+ id: receipt.id,
345
+ role: receipt.role,
346
+ stage: receipt.stage,
347
+ round: receipt.round,
348
+ artifact: receipt.artifact,
349
+ client: receipt.client,
350
+ status: receipt.status
351
+ };
352
+ }
353
+
354
+ function collectOrchestration(taskDir: string): OrchestrationInfo {
355
+ const run = readRun(taskDir);
356
+ if (!run) {
357
+ return {
358
+ status: 'absent',
359
+ runId: DASH,
360
+ nextStage: DASH,
361
+ pause: null,
362
+ pendingDelegation: null
363
+ };
364
+ }
365
+ return {
366
+ status: run.status,
367
+ runId: run.runId,
368
+ nextStage: run.nextStage ?? DASH,
369
+ pause: run.pause,
370
+ pendingDelegation: pendingDelegationInfo(run.pendingDelegation)
371
+ };
372
+ }
373
+
326
374
  type StatusModel = {
327
375
  taskId: string;
328
376
  shortId: string;
@@ -332,6 +380,7 @@ type StatusModel = {
332
380
  artifacts: { count: number; groups: { stage: string; files: string[] }[] };
333
381
  workflow: WorkflowInfo;
334
382
  runtime: RuntimeInfo;
383
+ orchestration: OrchestrationInfo;
335
384
  git: GitInfo;
336
385
  };
337
386
 
@@ -380,6 +429,26 @@ function renderStatus(model: StatusModel): string[] {
380
429
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
381
430
  }
382
431
 
432
+ lines.push(
433
+ '',
434
+ 'Orchestration',
435
+ ...renderPairs([
436
+ ['status', model.orchestration.status],
437
+ ['run_id', model.orchestration.runId],
438
+ ['next_stage', model.orchestration.nextStage],
439
+ ['pause_code', model.orchestration.pause?.code ?? DASH],
440
+ ['pause_message', model.orchestration.pause?.message ?? DASH],
441
+ ['recoverable', model.orchestration.pause ? String(model.orchestration.pause.recoverable) : DASH],
442
+ ['pending_id', model.orchestration.pendingDelegation?.id ?? DASH],
443
+ ['pending_role', model.orchestration.pendingDelegation?.role ?? DASH],
444
+ ['pending_stage', model.orchestration.pendingDelegation?.stage ?? DASH],
445
+ ['pending_round', model.orchestration.pendingDelegation ? String(model.orchestration.pendingDelegation.round) : DASH],
446
+ ['pending_artifact', model.orchestration.pendingDelegation?.artifact ?? DASH],
447
+ ['pending_client', model.orchestration.pendingDelegation?.client ?? DASH],
448
+ ['pending_status', model.orchestration.pendingDelegation?.status ?? DASH]
449
+ ])
450
+ );
451
+
383
452
  lines.push(
384
453
  '',
385
454
  'Workflow',
@@ -425,6 +494,9 @@ function renderStatus(model: StatusModel): string[] {
425
494
  }
426
495
 
427
496
  function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
497
+ if (model.orchestration.status === 'paused') return 'warning';
498
+ if (model.orchestration.status === 'running') return 'running';
499
+ if (model.orchestration.status === 'completed') return 'success';
428
500
  if (model.workflow.state === 'in-progress') return model.workflow.stale === 'yes' ? 'warning' : 'running';
429
501
  if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed')) return 'success';
430
502
  if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
@@ -449,6 +521,7 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
449
521
  artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
450
522
  workflow,
451
523
  runtime: collectRuntime(input.taskDir, workflow, run),
524
+ orchestration: collectOrchestration(input.taskDir),
452
525
  git: collectGit(fm.branch ?? '', run)
453
526
  };
454
527
  }
@@ -483,6 +556,9 @@ function statusModelToDisplay(model: StatusModel): DisplayMessage {
483
556
  `Task ${model.taskId} (${model.shortId})`,
484
557
  modelTone(model),
485
558
  [
559
+ ['orchestration', model.orchestration.status],
560
+ ['run', model.orchestration.runId],
561
+ ['next', model.orchestration.nextStage],
486
562
  ['workflow', model.workflow.state],
487
563
  ['step', model.workflow.step],
488
564
  ['runtime', model.runtime.status],
@@ -530,8 +606,9 @@ export {
530
606
  collectGit,
531
607
  collectWorkflow,
532
608
  collectRuntime,
609
+ collectOrchestration,
533
610
  renderStatus,
534
611
  statusModelToDisplay,
535
612
  METADATA_KEYS
536
613
  };
537
- export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel, BuildStatusModelInput };
614
+ export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, OrchestrationInfo, PendingDelegationInfo, StatusModel, BuildStatusModelInput };
@@ -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 };