@sireai/optimus 0.1.1

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 (204) hide show
  1. package/.env.example +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +104 -0
  4. package/dist/cli/optimus.d.ts +2 -0
  5. package/dist/cli/optimus.js +2951 -0
  6. package/dist/cli/optimus.js.map +1 -0
  7. package/dist/cli/self-update.d.ts +49 -0
  8. package/dist/cli/self-update.js +264 -0
  9. package/dist/cli/self-update.js.map +1 -0
  10. package/dist/config/load-config.d.ts +3 -0
  11. package/dist/config/load-config.js +321 -0
  12. package/dist/config/load-config.js.map +1 -0
  13. package/dist/config/optimus-paths.d.ts +13 -0
  14. package/dist/config/optimus-paths.js +44 -0
  15. package/dist/config/optimus-paths.js.map +1 -0
  16. package/dist/index.d.ts +25 -0
  17. package/dist/index.js +27 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/integrations/jira/jira-cli.d.ts +1 -0
  20. package/dist/integrations/jira/jira-cli.js +278 -0
  21. package/dist/integrations/jira/jira-cli.js.map +1 -0
  22. package/dist/integrations/jira/jira-client.d.ts +99 -0
  23. package/dist/integrations/jira/jira-client.js +521 -0
  24. package/dist/integrations/jira/jira-client.js.map +1 -0
  25. package/dist/integrations/jira/jira-submit.d.ts +71 -0
  26. package/dist/integrations/jira/jira-submit.js +351 -0
  27. package/dist/integrations/jira/jira-submit.js.map +1 -0
  28. package/dist/problem-solving-core/codex/codex-auth-resolver.d.ts +23 -0
  29. package/dist/problem-solving-core/codex/codex-auth-resolver.js +136 -0
  30. package/dist/problem-solving-core/codex/codex-auth-resolver.js.map +1 -0
  31. package/dist/problem-solving-core/codex/codex-connectivity-checks.d.ts +6 -0
  32. package/dist/problem-solving-core/codex/codex-connectivity-checks.js +81 -0
  33. package/dist/problem-solving-core/codex/codex-connectivity-checks.js.map +1 -0
  34. package/dist/problem-solving-core/codex/codex-failure-classifier.d.ts +2 -0
  35. package/dist/problem-solving-core/codex/codex-failure-classifier.js +49 -0
  36. package/dist/problem-solving-core/codex/codex-failure-classifier.js.map +1 -0
  37. package/dist/problem-solving-core/codex/codex-global-config.d.ts +17 -0
  38. package/dist/problem-solving-core/codex/codex-global-config.js +100 -0
  39. package/dist/problem-solving-core/codex/codex-global-config.js.map +1 -0
  40. package/dist/problem-solving-core/codex/codex-preflight.d.ts +13 -0
  41. package/dist/problem-solving-core/codex/codex-preflight.js +142 -0
  42. package/dist/problem-solving-core/codex/codex-preflight.js.map +1 -0
  43. package/dist/problem-solving-core/codex/codex-provider-profile.d.ts +14 -0
  44. package/dist/problem-solving-core/codex/codex-provider-profile.js +68 -0
  45. package/dist/problem-solving-core/codex/codex-provider-profile.js.map +1 -0
  46. package/dist/problem-solving-core/codex/codex-required-env.d.ts +3 -0
  47. package/dist/problem-solving-core/codex/codex-required-env.js +21 -0
  48. package/dist/problem-solving-core/codex/codex-required-env.js.map +1 -0
  49. package/dist/problem-solving-core/codex/codex-runner.d.ts +37 -0
  50. package/dist/problem-solving-core/codex/codex-runner.js +926 -0
  51. package/dist/problem-solving-core/codex/codex-runner.js.map +1 -0
  52. package/dist/problem-solving-core/codex/evolution-skill-guard.d.ts +36 -0
  53. package/dist/problem-solving-core/codex/evolution-skill-guard.js +143 -0
  54. package/dist/problem-solving-core/codex/evolution-skill-guard.js.map +1 -0
  55. package/dist/problem-solving-core/codex/repo-memory-service.d.ts +24 -0
  56. package/dist/problem-solving-core/codex/repo-memory-service.js +114 -0
  57. package/dist/problem-solving-core/codex/repo-memory-service.js.map +1 -0
  58. package/dist/problem-solving-core/codex/skill-sync-service.d.ts +35 -0
  59. package/dist/problem-solving-core/codex/skill-sync-service.js +280 -0
  60. package/dist/problem-solving-core/codex/skill-sync-service.js.map +1 -0
  61. package/dist/task-environment/cancellation/task-abort-registry.d.ts +17 -0
  62. package/dist/task-environment/cancellation/task-abort-registry.js +51 -0
  63. package/dist/task-environment/cancellation/task-abort-registry.js.map +1 -0
  64. package/dist/task-environment/cancellation/task-cancellation-service.d.ts +25 -0
  65. package/dist/task-environment/cancellation/task-cancellation-service.js +54 -0
  66. package/dist/task-environment/cancellation/task-cancellation-service.js.map +1 -0
  67. package/dist/task-environment/cancellation/task-cleanup-service.d.ts +22 -0
  68. package/dist/task-environment/cancellation/task-cleanup-service.js +67 -0
  69. package/dist/task-environment/cancellation/task-cleanup-service.js.map +1 -0
  70. package/dist/task-environment/delivery/commit-message/bugfix-commit-message-template.d.ts +13 -0
  71. package/dist/task-environment/delivery/commit-message/bugfix-commit-message-template.js +83 -0
  72. package/dist/task-environment/delivery/commit-message/bugfix-commit-message-template.js.map +1 -0
  73. package/dist/task-environment/delivery/commit-message/commit-message-builder.d.ts +6 -0
  74. package/dist/task-environment/delivery/commit-message/commit-message-builder.js +15 -0
  75. package/dist/task-environment/delivery/commit-message/commit-message-builder.js.map +1 -0
  76. package/dist/task-environment/delivery/commit-message/commit-message-template-types.d.ts +16 -0
  77. package/dist/task-environment/delivery/commit-message/commit-message-template-types.js +2 -0
  78. package/dist/task-environment/delivery/commit-message/commit-message-template-types.js.map +1 -0
  79. package/dist/task-environment/delivery/feishu-analysis-doc-service.d.ts +50 -0
  80. package/dist/task-environment/delivery/feishu-analysis-doc-service.js +454 -0
  81. package/dist/task-environment/delivery/feishu-analysis-doc-service.js.map +1 -0
  82. package/dist/task-environment/delivery/feishu-card-renderer.d.ts +38 -0
  83. package/dist/task-environment/delivery/feishu-card-renderer.js +449 -0
  84. package/dist/task-environment/delivery/feishu-card-renderer.js.map +1 -0
  85. package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.d.ts +34 -0
  86. package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js +201 -0
  87. package/dist/task-environment/delivery/feishu-content/feishu-content-renderer.js.map +1 -0
  88. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.d.ts +27 -0
  89. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js +74 -0
  90. package/dist/task-environment/delivery/feishu-content/feishu-copy-config.js.map +1 -0
  91. package/dist/task-environment/delivery/feishu-notifier.d.ts +45 -0
  92. package/dist/task-environment/delivery/feishu-notifier.js +250 -0
  93. package/dist/task-environment/delivery/feishu-notifier.js.map +1 -0
  94. package/dist/task-environment/delivery/feishu-templates/analysis-message-template.d.ts +6 -0
  95. package/dist/task-environment/delivery/feishu-templates/analysis-message-template.js +39 -0
  96. package/dist/task-environment/delivery/feishu-templates/analysis-message-template.js.map +1 -0
  97. package/dist/task-environment/delivery/feishu-templates/bugfix-message-template.d.ts +6 -0
  98. package/dist/task-environment/delivery/feishu-templates/bugfix-message-template.js +40 -0
  99. package/dist/task-environment/delivery/feishu-templates/bugfix-message-template.js.map +1 -0
  100. package/dist/task-environment/delivery/feishu-templates/default-message-template.d.ts +6 -0
  101. package/dist/task-environment/delivery/feishu-templates/default-message-template.js +33 -0
  102. package/dist/task-environment/delivery/feishu-templates/default-message-template.js.map +1 -0
  103. package/dist/task-environment/delivery/feishu-templates/patch-message-template.d.ts +6 -0
  104. package/dist/task-environment/delivery/feishu-templates/patch-message-template.js +40 -0
  105. package/dist/task-environment/delivery/feishu-templates/patch-message-template.js.map +1 -0
  106. package/dist/task-environment/delivery/feishu-templates/template-registry.d.ts +2 -0
  107. package/dist/task-environment/delivery/feishu-templates/template-registry.js +11 -0
  108. package/dist/task-environment/delivery/feishu-templates/template-registry.js.map +1 -0
  109. package/dist/task-environment/delivery/feishu-templates/template-types.d.ts +20 -0
  110. package/dist/task-environment/delivery/feishu-templates/template-types.js +2 -0
  111. package/dist/task-environment/delivery/feishu-templates/template-types.js.map +1 -0
  112. package/dist/task-environment/delivery/task-delivery-dispatcher.d.ts +14 -0
  113. package/dist/task-environment/delivery/task-delivery-dispatcher.js +109 -0
  114. package/dist/task-environment/delivery/task-delivery-dispatcher.js.map +1 -0
  115. package/dist/task-environment/delivery/task-delivery-service.d.ts +33 -0
  116. package/dist/task-environment/delivery/task-delivery-service.js +432 -0
  117. package/dist/task-environment/delivery/task-delivery-service.js.map +1 -0
  118. package/dist/task-environment/delivery/task-publication-service.d.ts +97 -0
  119. package/dist/task-environment/delivery/task-publication-service.js +1369 -0
  120. package/dist/task-environment/delivery/task-publication-service.js.map +1 -0
  121. package/dist/task-environment/execution-addresses.d.ts +40 -0
  122. package/dist/task-environment/execution-addresses.js +63 -0
  123. package/dist/task-environment/execution-addresses.js.map +1 -0
  124. package/dist/task-environment/intake/cli-file-intake.d.ts +12 -0
  125. package/dist/task-environment/intake/cli-file-intake.js +56 -0
  126. package/dist/task-environment/intake/cli-file-intake.js.map +1 -0
  127. package/dist/task-environment/intake/manual-problem-intake.d.ts +3 -0
  128. package/dist/task-environment/intake/manual-problem-intake.js +57 -0
  129. package/dist/task-environment/intake/manual-problem-intake.js.map +1 -0
  130. package/dist/task-environment/intake/polling-problem-intake.d.ts +14 -0
  131. package/dist/task-environment/intake/polling-problem-intake.js +232 -0
  132. package/dist/task-environment/intake/polling-problem-intake.js.map +1 -0
  133. package/dist/task-environment/observability/logger.d.ts +76 -0
  134. package/dist/task-environment/observability/logger.js +604 -0
  135. package/dist/task-environment/observability/logger.js.map +1 -0
  136. package/dist/task-environment/observability/runtime-panel.d.ts +82 -0
  137. package/dist/task-environment/observability/runtime-panel.js +1008 -0
  138. package/dist/task-environment/observability/runtime-panel.js.map +1 -0
  139. package/dist/task-environment/observability/sound-notifier.d.ts +18 -0
  140. package/dist/task-environment/observability/sound-notifier.js +71 -0
  141. package/dist/task-environment/observability/sound-notifier.js.map +1 -0
  142. package/dist/task-environment/orchestration/execution-context-assembler.d.ts +41 -0
  143. package/dist/task-environment/orchestration/execution-context-assembler.js +464 -0
  144. package/dist/task-environment/orchestration/execution-context-assembler.js.map +1 -0
  145. package/dist/task-environment/orchestration/git-change-classifier.d.ts +19 -0
  146. package/dist/task-environment/orchestration/git-change-classifier.js +106 -0
  147. package/dist/task-environment/orchestration/git-change-classifier.js.map +1 -0
  148. package/dist/task-environment/orchestration/harness-registry.d.ts +27 -0
  149. package/dist/task-environment/orchestration/harness-registry.js +116 -0
  150. package/dist/task-environment/orchestration/harness-registry.js.map +1 -0
  151. package/dist/task-environment/orchestration/harness-resolver.d.ts +8 -0
  152. package/dist/task-environment/orchestration/harness-resolver.js +39 -0
  153. package/dist/task-environment/orchestration/harness-resolver.js.map +1 -0
  154. package/dist/task-environment/orchestration/task-orchestrator.d.ts +45 -0
  155. package/dist/task-environment/orchestration/task-orchestrator.js +1122 -0
  156. package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -0
  157. package/dist/task-environment/orchestration/task-package-assembler.d.ts +4 -0
  158. package/dist/task-environment/orchestration/task-package-assembler.js +10 -0
  159. package/dist/task-environment/orchestration/task-package-assembler.js.map +1 -0
  160. package/dist/task-environment/orchestration/triage-agent.d.ts +54 -0
  161. package/dist/task-environment/orchestration/triage-agent.js +636 -0
  162. package/dist/task-environment/orchestration/triage-agent.js.map +1 -0
  163. package/dist/task-environment/orchestration/triage-runner.d.ts +65 -0
  164. package/dist/task-environment/orchestration/triage-runner.js +655 -0
  165. package/dist/task-environment/orchestration/triage-runner.js.map +1 -0
  166. package/dist/task-environment/publication-target.d.ts +12 -0
  167. package/dist/task-environment/publication-target.js +174 -0
  168. package/dist/task-environment/publication-target.js.map +1 -0
  169. package/dist/task-environment/runtime/blocking-event-queue.d.ts +7 -0
  170. package/dist/task-environment/runtime/blocking-event-queue.js +27 -0
  171. package/dist/task-environment/runtime/blocking-event-queue.js.map +1 -0
  172. package/dist/task-environment/runtime/optimus-runtime.d.ts +69 -0
  173. package/dist/task-environment/runtime/optimus-runtime.js +751 -0
  174. package/dist/task-environment/runtime/optimus-runtime.js.map +1 -0
  175. package/dist/task-environment/storage/sqlite-event-store.d.ts +52 -0
  176. package/dist/task-environment/storage/sqlite-event-store.js +288 -0
  177. package/dist/task-environment/storage/sqlite-event-store.js.map +1 -0
  178. package/dist/task-environment/storage/sqlite-task-store.d.ts +122 -0
  179. package/dist/task-environment/storage/sqlite-task-store.js +1182 -0
  180. package/dist/task-environment/storage/sqlite-task-store.js.map +1 -0
  181. package/dist/types.d.ts +629 -0
  182. package/dist/types.js +2 -0
  183. package/dist/types.js.map +1 -0
  184. package/embedded-skills/shared/repo-inspection/SKILL.md +9 -0
  185. package/embedded-skills/shared/repo-inspection/skill.json +5 -0
  186. package/embedded-skills/task/bugfix/android-debug-protocol/SKILL.md +10 -0
  187. package/embedded-skills/task/bugfix/android-debug-protocol/skill.json +6 -0
  188. package/harness/AGENTS.md +30 -0
  189. package/harness/CHECKLIST.md +44 -0
  190. package/harness/CONSTRAINTS.md +60 -0
  191. package/harness/FRAMEWORK.md +28 -0
  192. package/harness/GOAL.md +28 -0
  193. package/harness/HANDOFF.md +45 -0
  194. package/harness/TASK_PLAN.md +79 -0
  195. package/optimus.config.template.json +34 -0
  196. package/package.json +109 -0
  197. package/task-harnesses/bugfix/ACCEPT.md +47 -0
  198. package/task-harnesses/bugfix/CONSTRAINTS.md +46 -0
  199. package/task-harnesses/bugfix/CONTEXT.md +29 -0
  200. package/task-harnesses/bugfix/EVOLUTION.md +82 -0
  201. package/task-harnesses/bugfix/ROLE.md +29 -0
  202. package/task-harnesses/bugfix/STANDARD.md +250 -0
  203. package/task-harnesses/bugfix/manifest.json +13 -0
  204. package/task-harnesses/registry.json +8 -0
@@ -0,0 +1,1369 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { collectBusinessChangePaths, filterBusinessChangeEntries, parseGitStatusOutput } from "../orchestration/git-change-classifier.js";
7
+ import { CommitMessageBuilder } from "./commit-message/commit-message-builder.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const EXEC_MAX_BUFFER = 4 * 1024 * 1024;
10
+ export class TaskPublicationService {
11
+ logger;
12
+ commitMessageBuilder = new CommitMessageBuilder();
13
+ constructor(logger) {
14
+ this.logger = logger;
15
+ }
16
+ async publish(input) {
17
+ const createdAt = new Date().toISOString();
18
+ const publication = input.bundle.publication;
19
+ if (!publication?.applicable) {
20
+ return {
21
+ attempts: [this.buildAttempt(input.bundle, {
22
+ mode: input.dryRun ? "dry_run" : "review_submit",
23
+ status: "skipped",
24
+ summary: "Publication not applicable for this task.",
25
+ createdAt
26
+ })],
27
+ artifacts: []
28
+ };
29
+ }
30
+ if (!publication.hasPatch) {
31
+ return {
32
+ attempts: [this.buildAttempt(input.bundle, {
33
+ mode: input.dryRun ? "dry_run" : "review_submit",
34
+ status: "skipped",
35
+ summary: "Publication skipped because no patch artifact was produced.",
36
+ createdAt
37
+ })],
38
+ artifacts: []
39
+ };
40
+ }
41
+ if (publication.action !== "ready_for_publish") {
42
+ return {
43
+ attempts: [this.buildAttempt(input.bundle, {
44
+ mode: input.dryRun ? "dry_run" : "review_submit",
45
+ status: "skipped",
46
+ summary: `Publication not ready: ${publication.reason ?? publication.action}.`,
47
+ createdAt
48
+ })],
49
+ artifacts: []
50
+ };
51
+ }
52
+ const planContext = this.resolvePlanContext(input.bundle, input.reviewMode ?? "auto", input.dryRun);
53
+ if (!input.dryRun && planContext.reviewSystem.strategy === "manual") {
54
+ return {
55
+ attempts: [this.buildAttempt(input.bundle, {
56
+ mode: "review_submit",
57
+ status: "skipped",
58
+ summary: "Review submission skipped because only manual handoff is available.",
59
+ createdAt
60
+ })],
61
+ artifacts: []
62
+ };
63
+ }
64
+ const artifactDir = input.context?.addresses.artifactDir;
65
+ if (!artifactDir) {
66
+ return {
67
+ attempts: [this.buildAttempt(input.bundle, {
68
+ mode: input.dryRun ? "dry_run" : "review_submit",
69
+ status: "failed",
70
+ summary: `Publication ${input.dryRun ? "dry-run" : "review-submit"} failed because artifactDir is missing.`,
71
+ createdAt,
72
+ error: "artifact_dir_missing"
73
+ })],
74
+ artifacts: []
75
+ };
76
+ }
77
+ await mkdir(artifactDir, { recursive: true });
78
+ if (input.dryRun) {
79
+ const artifactPath = join(artifactDir, "publication-plan.md");
80
+ await writeFile(artifactPath, this.renderPlan(input.bundle, planContext), "utf8");
81
+ return {
82
+ attempts: [this.buildAttempt(input.bundle, {
83
+ mode: "dry_run",
84
+ status: "planned",
85
+ summary: `Publication dry-run plan generated for ${planContext.reviewSystem.strategy} -> ${this.renderTarget(input.bundle)}.`,
86
+ createdAt,
87
+ artifactPath
88
+ })],
89
+ artifacts: [{
90
+ kind: "publication_plan",
91
+ path: artifactPath,
92
+ createdAt
93
+ }]
94
+ };
95
+ }
96
+ const taskRootDir = input.context?.taskRootDir;
97
+ if (!taskRootDir) {
98
+ return {
99
+ attempts: [this.buildAttempt(input.bundle, {
100
+ mode: "review_submit",
101
+ status: "failed",
102
+ summary: "Review submission failed because taskRootDir is missing.",
103
+ createdAt,
104
+ error: "task_root_missing"
105
+ })],
106
+ artifacts: []
107
+ };
108
+ }
109
+ const packetPath = join(artifactDir, "review-packet.md");
110
+ try {
111
+ const execution = await this.executeReviewSubmit(input.bundle, planContext, input.context, taskRootDir, artifactDir);
112
+ await writeFile(packetPath, this.renderReviewPacket(input.bundle, planContext, execution.records), "utf8");
113
+ return {
114
+ attempts: [this.buildAttempt(input.bundle, {
115
+ mode: "review_submit",
116
+ status: execution.status,
117
+ summary: execution.summary,
118
+ createdAt,
119
+ artifactPath: packetPath,
120
+ ...(this.collectReviewMetadata(execution.records))
121
+ })],
122
+ artifacts: [{
123
+ kind: "review_packet",
124
+ path: packetPath,
125
+ createdAt
126
+ }]
127
+ };
128
+ }
129
+ catch (error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ await this.logger?.error("task.publication.review_submit.failed", {
132
+ taskId: input.bundle.taskId,
133
+ taskType: input.bundle.taskType,
134
+ publicationStrategy: planContext.reviewSystem.strategy,
135
+ reviewSystemType: planContext.reviewSystem.type,
136
+ reason: this.compact(message)
137
+ });
138
+ await writeFile(packetPath, this.renderReviewPacket(input.bundle, planContext, [{
139
+ label: "publication",
140
+ status: "failed",
141
+ strategy: planContext.reviewSystem.strategy,
142
+ detail: message
143
+ }]), "utf8");
144
+ return {
145
+ attempts: [this.buildAttempt(input.bundle, {
146
+ mode: "review_submit",
147
+ status: "failed",
148
+ summary: `Review submission failed: ${this.compact(message)}`,
149
+ createdAt,
150
+ artifactPath: packetPath,
151
+ error: this.compact(message)
152
+ })],
153
+ artifacts: [{
154
+ kind: "review_packet",
155
+ path: packetPath,
156
+ createdAt
157
+ }]
158
+ };
159
+ }
160
+ }
161
+ buildAttempt(bundle, attempt) {
162
+ return {
163
+ taskId: bundle.taskId,
164
+ ...(bundle.runId ? { runId: bundle.runId } : {}),
165
+ ...attempt
166
+ };
167
+ }
168
+ resolvePlanContext(bundle, reviewMode, dryRun) {
169
+ const publication = bundle.publication;
170
+ const projectTargets = publication?.git?.repoProjects ?? [];
171
+ return {
172
+ mode: dryRun ? "dry_run" : "review_submit",
173
+ projectTargets,
174
+ reviewSystem: this.resolveEffectiveReviewSystem(bundle, reviewMode, projectTargets)
175
+ };
176
+ }
177
+ resolveEffectiveReviewSystem(bundle, reviewMode, projectTargets) {
178
+ const detected = bundle.publication?.git?.reviewSystem;
179
+ if (reviewMode !== "auto") {
180
+ return this.buildOverrideReviewSystem(reviewMode, detected);
181
+ }
182
+ if (detected) {
183
+ return detected;
184
+ }
185
+ const projectStrategies = [...new Set(projectTargets
186
+ .map((project) => project.reviewSystem?.strategy)
187
+ .filter((value) => Boolean(value)))];
188
+ if (projectStrategies.length > 1) {
189
+ return {
190
+ type: "mixed",
191
+ strategy: "manual"
192
+ };
193
+ }
194
+ const firstProjectStrategy = projectStrategies[0];
195
+ if (firstProjectStrategy) {
196
+ return this.buildReviewSystemFromStrategy(firstProjectStrategy);
197
+ }
198
+ return {
199
+ type: "unknown",
200
+ strategy: "manual"
201
+ };
202
+ }
203
+ buildOverrideReviewSystem(reviewMode, detected) {
204
+ const strategyByMode = {
205
+ gerrit: "gerrit_change",
206
+ github: "github_pr",
207
+ manual: "manual"
208
+ };
209
+ const typeByMode = {
210
+ gerrit: "gerrit",
211
+ github: "github",
212
+ manual: "unknown"
213
+ };
214
+ return {
215
+ ...(detected ?? {}),
216
+ type: typeByMode[reviewMode],
217
+ strategy: strategyByMode[reviewMode]
218
+ };
219
+ }
220
+ buildReviewSystemFromStrategy(strategy) {
221
+ switch (strategy) {
222
+ case "gerrit_change":
223
+ return { type: "gerrit", strategy };
224
+ case "github_pr":
225
+ return { type: "github", strategy };
226
+ default:
227
+ return { type: "unknown", strategy: "manual" };
228
+ }
229
+ }
230
+ renderTarget(bundle) {
231
+ const publication = bundle.publication;
232
+ if (publication?.git?.upstreamRemote && publication.git.upstreamBranch) {
233
+ return `${publication.git.upstreamRemote}/${publication.git.upstreamBranch}`;
234
+ }
235
+ if (publication?.git?.repoProjects?.length) {
236
+ return `${publication.git.repoProjects.length} project target(s)`;
237
+ }
238
+ if (publication?.git?.targetBranch) {
239
+ return publication.git.targetSource === "manifest_revision"
240
+ ? `manifest revision ${publication.git.targetBranch}`
241
+ : publication.git.targetBranch;
242
+ }
243
+ return "upstream target";
244
+ }
245
+ renderPlan(bundle, plan) {
246
+ const publication = bundle.publication;
247
+ const lines = [
248
+ "# Publication Plan",
249
+ "",
250
+ "## Summary",
251
+ "",
252
+ `- Task: ${bundle.taskId}`,
253
+ `- Task Type: ${bundle.taskType}`,
254
+ `- Outcome: ${bundle.outcome}`,
255
+ `- Decision: ${bundle.summary.decision}`,
256
+ `- Mode: ${plan.mode}`,
257
+ `- Workspace Kind: ${publication?.git?.workspaceKind ?? "unknown"}`,
258
+ `- Publication Action: ${publication?.action ?? "none"}`,
259
+ `- Publication Reason: ${publication?.reason ?? "n/a"}`,
260
+ `- Branch: ${publication?.git?.branch ?? "n/a"}`,
261
+ `- Upstream: ${publication?.git?.upstreamRemote && publication.git.upstreamBranch ? `${publication.git.upstreamRemote}/${publication.git.upstreamBranch}` : "n/a"}`,
262
+ `- Manifest Revision: ${publication?.git?.manifestRevision ?? "n/a"}`,
263
+ `- Target Branch: ${publication?.git?.targetBranch ?? "n/a"}`,
264
+ `- Target Source: ${publication?.git?.targetSource ?? "n/a"}`,
265
+ `- Review System: ${this.renderReviewSystem(plan.reviewSystem)}`,
266
+ `- Patch: ${bundle.artifacts.patchDiff ?? "n/a"}`,
267
+ "",
268
+ "## Review Strategy",
269
+ "",
270
+ ...this.renderStrategySection(bundle, plan),
271
+ "",
272
+ "## Project Targets",
273
+ "",
274
+ ...(plan.projectTargets.length > 0
275
+ ? plan.projectTargets.map((project) => this.renderProjectTarget(project))
276
+ : ["- n/a"]),
277
+ ...(publication?.git?.unresolvedPaths?.length ? ["", `- Unresolved Paths: ${publication.git.unresolvedPaths.join(", ")}`] : []),
278
+ "",
279
+ "## Planned Next Step",
280
+ "",
281
+ ...this.renderPlannedNextSteps(bundle, plan.reviewSystem.strategy),
282
+ "",
283
+ "## Notes",
284
+ "",
285
+ "- Review submission stays decoupled from delivery notification.",
286
+ "- Patch publication remains review-first; no direct branch push is planned."
287
+ ];
288
+ return lines.join("\n");
289
+ }
290
+ renderReviewSystem(reviewSystem) {
291
+ const parts = [`${reviewSystem.type} (${reviewSystem.strategy})`];
292
+ if (reviewSystem.remoteName) {
293
+ parts.push(reviewSystem.remoteName);
294
+ }
295
+ if (reviewSystem.repository) {
296
+ parts.push(reviewSystem.repository);
297
+ }
298
+ return parts.join(" | ");
299
+ }
300
+ renderStrategySection(bundle, plan) {
301
+ const workspaceKind = bundle.publication?.git?.workspaceKind;
302
+ switch (plan.reviewSystem.strategy) {
303
+ case "gerrit_change":
304
+ if (workspaceKind === "repo_managed") {
305
+ return this.renderRepoManagedGerritStrategy(bundle, plan.projectTargets, plan.reviewSystem);
306
+ }
307
+ return this.renderGerritStrategy(bundle, plan.projectTargets, plan.reviewSystem);
308
+ case "github_pr":
309
+ return this.renderGithubStrategy(bundle, plan.projectTargets, plan.reviewSystem);
310
+ default:
311
+ return this.renderManualStrategy(bundle, plan.projectTargets, plan.reviewSystem);
312
+ }
313
+ }
314
+ renderRepoManagedGerritStrategy(bundle, projectTargets, reviewSystem) {
315
+ const targets = projectTargets.length > 0 ? projectTargets : [this.buildRootProjectTarget(bundle)];
316
+ const topic = targets.length > 1 ? this.buildTopic(bundle.taskId) : undefined;
317
+ return [
318
+ `- Strategy: split the patch by repo project, create commits in each project, then submit Gerrit changes per project${topic ? " with a shared topic" : ""}.`,
319
+ `- Submit: git push <remote> HEAD:refs/for/<branch>${topic ? `%topic=${topic}` : ""}`,
320
+ ...targets.map((target) => {
321
+ const effectiveReviewSystem = target.reviewSystem?.strategy ? target.reviewSystem : reviewSystem;
322
+ return `- ${this.renderTargetLabel(target)}: ${target.targetBranch ?? bundle.publication?.git?.targetBranch ?? "target branch unresolved"} | ${effectiveReviewSystem.remoteName ?? "origin"}`;
323
+ })
324
+ ];
325
+ }
326
+ renderGerritStrategy(bundle, projectTargets, reviewSystem) {
327
+ const targets = projectTargets.length > 0 ? projectTargets : [this.buildRootProjectTarget(bundle)];
328
+ const topic = targets.length > 1 ? this.buildTopic(bundle.taskId) : undefined;
329
+ const commands = targets.map((target) => {
330
+ const effectiveReviewSystem = target.reviewSystem?.strategy ? target.reviewSystem : reviewSystem;
331
+ const remote = target.upstreamRemote ?? effectiveReviewSystem.remoteName ?? "origin";
332
+ const branch = this.normalizeGerritTarget(target.targetBranch ?? bundle.publication?.git?.targetBranch);
333
+ if (!branch) {
334
+ return `- ${this.renderTargetLabel(target)}: missing target branch; cannot form refs/for push plan.`;
335
+ }
336
+ return `- ${this.renderTargetLabel(target)}: git push ${remote} ${this.buildGerritPushRef(branch, topic)}`;
337
+ });
338
+ return [
339
+ "- Strategy: create Gerrit changes via refs/for review refs.",
340
+ ...commands
341
+ ];
342
+ }
343
+ renderGithubStrategy(bundle, projectTargets, reviewSystem) {
344
+ const targets = projectTargets.length > 0 ? projectTargets : [this.buildRootProjectTarget(bundle)];
345
+ const commands = targets.flatMap((target) => {
346
+ const effectiveReviewSystem = target.reviewSystem?.strategy ? target.reviewSystem : reviewSystem;
347
+ const branchName = this.buildGithubBranchName(bundle.taskId, target.projectPath);
348
+ const remote = target.upstreamRemote ?? effectiveReviewSystem.remoteName ?? "origin";
349
+ const baseBranch = this.normalizeGitHubBaseBranch(target.targetBranch ?? bundle.publication?.git?.targetBranch);
350
+ const label = this.renderTargetLabel(target);
351
+ if (!baseBranch) {
352
+ return [`- ${label}: missing base branch; cannot form PR plan.`];
353
+ }
354
+ return [
355
+ `- ${label}: git push ${remote} HEAD:refs/heads/${branchName}`,
356
+ `- ${label}: gh pr create --base ${baseBranch} --head ${branchName}`
357
+ ];
358
+ });
359
+ return [
360
+ "- Strategy: push a review branch, then open a GitHub pull request.",
361
+ ...commands
362
+ ];
363
+ }
364
+ renderManualStrategy(bundle, projectTargets, reviewSystem) {
365
+ const targets = projectTargets.length > 0 ? projectTargets : [this.buildRootProjectTarget(bundle)];
366
+ return [
367
+ `- Strategy: manual review delivery because detected review path is ${reviewSystem.type}/${reviewSystem.strategy}.`,
368
+ ...targets.map((target) => `- ${this.renderTargetLabel(target)}: attach patch/result artifacts and hand off against ${target.targetBranch ?? bundle.publication?.git?.targetBranch ?? "target branch unresolved"}.`)
369
+ ];
370
+ }
371
+ buildRootProjectTarget(bundle) {
372
+ const git = bundle.publication?.git;
373
+ return {
374
+ projectPath: ".",
375
+ ...(git?.upstreamRemote ? { upstreamRemote: git.upstreamRemote } : {}),
376
+ ...(git?.upstreamBranch ? { upstreamBranch: git.upstreamBranch } : {}),
377
+ ...(git?.manifestRevision ? { manifestRevision: git.manifestRevision } : {}),
378
+ ...(git?.targetBranch ? { targetBranch: git.targetBranch } : {}),
379
+ ...(git?.targetSource ? { targetSource: git.targetSource } : {}),
380
+ ...(git?.reviewSystem ? { reviewSystem: git.reviewSystem } : {})
381
+ };
382
+ }
383
+ renderProjectTarget(project) {
384
+ const parts = [`- ${project.projectPath}: ${project.targetBranch ?? "n/a"} (${project.targetSource ?? "unknown"})`];
385
+ if (project.upstreamRemote && project.upstreamBranch) {
386
+ parts.push(`${project.upstreamRemote}/${project.upstreamBranch}`);
387
+ }
388
+ if (project.reviewSystem) {
389
+ parts.push(`${project.reviewSystem.type}/${project.reviewSystem.strategy}`);
390
+ }
391
+ return parts.join(" | ");
392
+ }
393
+ renderTargetLabel(target) {
394
+ return target.projectPath === "." ? "repo-root" : target.projectPath;
395
+ }
396
+ normalizeGerritTarget(branch) {
397
+ if (!branch) {
398
+ return undefined;
399
+ }
400
+ return branch.replace(/^refs\/heads\//, "");
401
+ }
402
+ normalizeGitHubBaseBranch(branch) {
403
+ if (!branch) {
404
+ return undefined;
405
+ }
406
+ return branch.replace(/^refs\/heads\//, "");
407
+ }
408
+ renderPlannedNextSteps(bundle, strategy) {
409
+ const workspaceKind = bundle.publication?.git?.workspaceKind;
410
+ switch (strategy) {
411
+ case "gerrit_change":
412
+ if (workspaceKind === "repo_managed") {
413
+ return [
414
+ "- Submit the patch as per-project Gerrit changes, with one shared topic only when multiple changes exist.",
415
+ "- Notify humans with result.md, patch.diff, and Gerrit topic/change metadata."
416
+ ];
417
+ }
418
+ return [
419
+ "- Submit the patch as Gerrit review changes, not as a direct branch update.",
420
+ "- Notify humans with result.md, patch.diff, and Gerrit-ready target refs."
421
+ ];
422
+ case "github_pr":
423
+ return [
424
+ "- Submit the patch through a pull request workflow, not as a direct branch update.",
425
+ "- Notify humans with result.md, patch.diff, and the planned PR base branch."
426
+ ];
427
+ default:
428
+ return [
429
+ "- Keep artifacts in manual review handoff because no safe automated review endpoint is resolved.",
430
+ "- Notify humans with result.md, patch.diff, and the unresolved publication context."
431
+ ];
432
+ }
433
+ }
434
+ async executeReviewSubmit(bundle, plan, context, taskRootDir, artifactDir) {
435
+ const session = {
436
+ bundle,
437
+ reviewSystem: plan.reviewSystem
438
+ };
439
+ await this.logger?.info("task.publication.review_submit.started", {
440
+ taskId: bundle.taskId,
441
+ taskType: bundle.taskType,
442
+ publicationStrategy: plan.reviewSystem.strategy,
443
+ reviewSystemType: plan.reviewSystem.type,
444
+ workspaceKind: bundle.publication?.git?.workspaceKind ?? "unknown"
445
+ });
446
+ const records = await this.submitBundle(session, plan, context, taskRootDir, artifactDir);
447
+ const status = this.resolveExecutionStatus(records);
448
+ const summary = this.buildExecutionSummary(records, plan.reviewSystem.strategy);
449
+ const reviewMetadata = this.collectReviewMetadata(records);
450
+ await this.logger?.debug("task.publication.review_submit.review_metadata", {
451
+ taskId: bundle.taskId,
452
+ taskType: bundle.taskType,
453
+ publicationStatus: status,
454
+ publicationStrategy: plan.reviewSystem.strategy,
455
+ topic: reviewMetadata.topic ?? null,
456
+ topicUrl: reviewMetadata.topicUrl ?? null,
457
+ reviewUrl: reviewMetadata.reviewUrl ?? null,
458
+ reviewUrls: reviewMetadata.reviewUrls ?? [],
459
+ changeIds: reviewMetadata.changeIds ?? (reviewMetadata.changeId ? [reviewMetadata.changeId] : [])
460
+ });
461
+ await this.logger?.info("task.publication.review_submit.completed", {
462
+ taskId: bundle.taskId,
463
+ taskType: bundle.taskType,
464
+ publicationStatus: status,
465
+ publicationStrategy: plan.reviewSystem.strategy,
466
+ submittedCount: records.filter((record) => record.status === "submitted").length,
467
+ failedCount: records.filter((record) => record.status === "failed").length,
468
+ skippedCount: records.filter((record) => record.status === "skipped").length,
469
+ topic: reviewMetadata.topic ?? null,
470
+ topicUrl: reviewMetadata.topicUrl ?? null,
471
+ reviewUrl: reviewMetadata.reviewUrl ?? null,
472
+ summary
473
+ });
474
+ return {
475
+ status,
476
+ summary,
477
+ records
478
+ };
479
+ }
480
+ async submitBundle(session, plan, context, taskRootDir, artifactDir) {
481
+ const publication = session.bundle.publication?.git;
482
+ if (!publication?.repoDir) {
483
+ throw new Error("Publication execution requires repoDir.");
484
+ }
485
+ const targets = await this.buildPatchTargets(session.bundle, plan, artifactDir);
486
+ await this.logger?.debug("task.publication.review_submit.targets_resolved", {
487
+ taskId: session.bundle.taskId,
488
+ taskType: session.bundle.taskType,
489
+ publicationStrategy: plan.reviewSystem.strategy,
490
+ reviewSystemType: plan.reviewSystem.type,
491
+ targetCount: targets.length,
492
+ targets: targets.map((target) => ({
493
+ label: target.label,
494
+ projectPath: target.projectPath,
495
+ repoDir: target.repoDir,
496
+ targetBranch: target.targetBranch ?? null,
497
+ remoteName: target.upstreamRemote ?? target.reviewSystem.remoteName ?? null,
498
+ hasPatchContent: Boolean(target.patchContent.trim())
499
+ }))
500
+ });
501
+ if (targets.length === 0) {
502
+ return [this.buildExecutionRecord({
503
+ label: "publication",
504
+ status: "skipped",
505
+ strategy: plan.reviewSystem.strategy,
506
+ targetBranch: undefined,
507
+ remoteName: plan.reviewSystem.remoteName,
508
+ submittedRef: undefined,
509
+ reviewUrl: undefined,
510
+ changeId: undefined,
511
+ detail: "No publishable patch target was resolved."
512
+ })];
513
+ }
514
+ const repoKind = this.resolveRepositoryKind(session.bundle, targets);
515
+ await this.logger?.debug("task.publication.review_submit.targets_resolved", {
516
+ taskId: session.bundle.taskId,
517
+ taskType: session.bundle.taskType,
518
+ publicationStrategy: plan.reviewSystem.strategy,
519
+ reviewSystemType: plan.reviewSystem.type,
520
+ repositoryKind: repoKind.kind,
521
+ repositories: repoKind.repos
522
+ });
523
+ if (repoKind.kind === "repo_managed") {
524
+ return this.submitRepoManagedGerrit(session, targets, context);
525
+ }
526
+ if (repoKind.kind === "plain_directory") {
527
+ return this.submitMultiGitRepositories(session, targets, context, taskRootDir);
528
+ }
529
+ return this.submitGitRoot(session, targets, context, taskRootDir);
530
+ }
531
+ async buildPatchTargets(bundle, plan, artifactDir) {
532
+ const publication = bundle.publication?.git;
533
+ const patchPath = bundle.artifacts.patchDiff;
534
+ if (!publication?.repoDir || !patchPath) {
535
+ throw new Error("Publication execution requires repoDir and patchDiff.");
536
+ }
537
+ await mkdir(artifactDir, { recursive: true });
538
+ const patchContent = await readFile(patchPath, "utf8");
539
+ const projectTargets = plan.projectTargets.length > 0 ? plan.projectTargets : [this.buildRootProjectTarget(bundle)];
540
+ const patchSections = projectTargets.some((project) => project.projectPath !== ".")
541
+ ? this.parsePatchSections(patchContent)
542
+ : [];
543
+ return projectTargets.map((project) => {
544
+ const targetPatch = project.projectPath === "."
545
+ ? patchContent
546
+ : this.buildProjectPatch(patchSections, project.projectPath) ?? "";
547
+ const repoRoot = publication.repoDir;
548
+ const targetRepoDir = project.projectPath === "."
549
+ ? repoRoot
550
+ : join(repoRoot, project.projectPath);
551
+ return this.buildPatchTarget({
552
+ label: this.renderTargetLabel(project),
553
+ projectPath: project.projectPath,
554
+ repoDir: targetRepoDir,
555
+ targetBranch: project.targetBranch ?? publication.targetBranch,
556
+ upstreamRemote: project.upstreamRemote ?? publication.upstreamRemote,
557
+ upstreamBranch: project.upstreamBranch,
558
+ reviewSystem: project.reviewSystem ?? plan.reviewSystem,
559
+ patchContent: targetPatch
560
+ });
561
+ });
562
+ }
563
+ resolveRepositoryKind(bundle, targets) {
564
+ const workspaceKind = bundle.publication?.git?.workspaceKind;
565
+ if (workspaceKind === "repo_managed") {
566
+ return {
567
+ kind: "repo_managed",
568
+ repos: targets.map((target) => ({ label: target.label, repoDir: target.repoDir }))
569
+ };
570
+ }
571
+ const uniqueRepos = [...new Map(targets.map((target) => [resolve(target.repoDir), { label: target.label, repoDir: resolve(target.repoDir) }])).values()];
572
+ if (workspaceKind === "plain_directory" || uniqueRepos.length > 1) {
573
+ return {
574
+ kind: "plain_directory",
575
+ repos: uniqueRepos
576
+ };
577
+ }
578
+ return {
579
+ kind: "git_root",
580
+ repos: uniqueRepos
581
+ };
582
+ }
583
+ async submitRepoManagedGerrit(session, targets, context) {
584
+ const commitMaterials = [];
585
+ const directWorkspace = this.shouldUseDirectWorkspaceCommits(context);
586
+ try {
587
+ for (const target of targets) {
588
+ try {
589
+ if (!target.patchContent.trim()) {
590
+ commitMaterials.push({
591
+ target,
592
+ commitHash: "",
593
+ changeId: undefined,
594
+ baseHead: undefined
595
+ });
596
+ continue;
597
+ }
598
+ const materialized = directWorkspace
599
+ ? await this.commitExistingWorkspace(session.bundle, target)
600
+ : await this.materializePatchCommit(session.bundle, target);
601
+ commitMaterials.push(materialized);
602
+ }
603
+ catch (error) {
604
+ commitMaterials.push({ target, commitHash: "", changeId: undefined, baseHead: undefined });
605
+ throw error;
606
+ }
607
+ }
608
+ const submittedMaterials = commitMaterials.filter((materialized) => materialized.target.patchContent.trim());
609
+ const topic = submittedMaterials.length > 1 ? this.buildTopic(session.bundle.taskId) : undefined;
610
+ await this.logger?.info("task.publication.repo_managed.submit_started", {
611
+ taskId: session.bundle.taskId,
612
+ taskType: session.bundle.taskType,
613
+ projectCount: submittedMaterials.length,
614
+ projects: submittedMaterials.map((materialized) => materialized.target.projectPath),
615
+ topic: topic ?? null
616
+ });
617
+ const records = [];
618
+ for (const materialized of commitMaterials) {
619
+ if (!materialized.target.patchContent.trim()) {
620
+ const topicUrl = this.buildGerritTopicUrl(materialized.target.reviewSystem, topic);
621
+ records.push(this.buildExecutionRecord({
622
+ label: materialized.target.label,
623
+ status: "skipped",
624
+ strategy: materialized.target.reviewSystem.strategy,
625
+ ...(topic ? { topic } : {}),
626
+ ...(topicUrl ? { topicUrl } : {}),
627
+ targetBranch: materialized.target.targetBranch,
628
+ remoteName: materialized.target.upstreamRemote ?? materialized.target.reviewSystem.remoteName,
629
+ submittedRef: undefined,
630
+ reviewUrl: undefined,
631
+ changeId: undefined,
632
+ detail: "No patch content mapped to this publication target."
633
+ }));
634
+ continue;
635
+ }
636
+ records.push(await this.uploadRepoManagedCommit(materialized, topic));
637
+ }
638
+ await this.logger?.info("task.publication.repo_managed.submit_completed", {
639
+ taskId: session.bundle.taskId,
640
+ taskType: session.bundle.taskType,
641
+ projectCount: submittedMaterials.length,
642
+ topic: topic ?? null,
643
+ submittedCount: records.filter((record) => record.status === "submitted").length,
644
+ skippedCount: records.filter((record) => record.status === "skipped").length,
645
+ failedCount: records.filter((record) => record.status === "failed").length
646
+ });
647
+ return records;
648
+ }
649
+ finally {
650
+ await this.cleanupMaterializedCommits(commitMaterials);
651
+ }
652
+ }
653
+ async uploadRepoManagedCommit(materialized, topic) {
654
+ return this.pushGerritCommit(materialized, topic);
655
+ }
656
+ async submitMultiGitRepositories(session, targets, context, taskRootDir) {
657
+ const materials = [];
658
+ const directWorkspace = this.shouldUseDirectWorkspaceCommits(context);
659
+ try {
660
+ for (const target of targets) {
661
+ try {
662
+ if (!target.patchContent.trim()) {
663
+ materials.push({ target, commitHash: "", changeId: undefined, baseHead: undefined });
664
+ continue;
665
+ }
666
+ const materialized = directWorkspace
667
+ ? await this.commitExistingWorkspace(session.bundle, target)
668
+ : await this.materializePatchCommit(session.bundle, target);
669
+ materials.push(materialized);
670
+ }
671
+ catch (error) {
672
+ materials.push({ target, commitHash: "", changeId: undefined, baseHead: undefined });
673
+ throw error;
674
+ }
675
+ }
676
+ const submittedMaterialCount = materials.filter((materialized) => materialized.target.patchContent.trim()).length;
677
+ const topic = submittedMaterialCount > 1 ? this.buildTopic(session.bundle.taskId) : undefined;
678
+ const records = [];
679
+ for (const materialized of materials) {
680
+ if (!materialized.target.patchContent.trim()) {
681
+ records.push(this.buildExecutionRecord({
682
+ label: materialized.target.label,
683
+ status: "skipped",
684
+ strategy: materialized.target.reviewSystem.strategy,
685
+ targetBranch: materialized.target.targetBranch,
686
+ remoteName: materialized.target.upstreamRemote ?? materialized.target.reviewSystem.remoteName,
687
+ submittedRef: undefined,
688
+ reviewUrl: undefined,
689
+ changeId: undefined,
690
+ detail: "No patch content mapped to this publication target."
691
+ }));
692
+ continue;
693
+ }
694
+ records.push(await this.pushGerritCommit(materialized, topic));
695
+ }
696
+ return records;
697
+ }
698
+ finally {
699
+ if (!directWorkspace) {
700
+ await this.cleanupMaterializedCommits(materials);
701
+ await rm(join(taskRootDir, ".optimus", "publication", "stage"), { recursive: true, force: true });
702
+ }
703
+ }
704
+ }
705
+ async submitGitRoot(session, targets, context, taskRootDir) {
706
+ const target = targets[0];
707
+ if (!target) {
708
+ throw new Error("No git-root publication target resolved.");
709
+ }
710
+ if (session.reviewSystem.strategy === "github_pr") {
711
+ return [await this.submitGithubTarget(session.bundle, target, context, taskRootDir)];
712
+ }
713
+ if (context?.addresses.mode === "copy") {
714
+ return [await this.submitGitRootCopyWorkspace(session.bundle, target, context)];
715
+ }
716
+ if (context?.addresses.mode === "inplace") {
717
+ return [await this.submitGitRootInplaceGerrit(session.bundle, target)];
718
+ }
719
+ return [await this.submitGitRootStagedGerrit(session.bundle, target, taskRootDir)];
720
+ }
721
+ async submitGitRootInplaceGerrit(bundle, target) {
722
+ const materialized = await this.commitExistingWorkspace(bundle, target);
723
+ try {
724
+ return await this.pushGerritCommit(materialized);
725
+ }
726
+ finally {
727
+ await this.cleanupMaterializedCommits([materialized]);
728
+ }
729
+ }
730
+ async submitGitRootCopyWorkspace(bundle, target, context) {
731
+ const workspaceDir = context.addresses.workspaceDir;
732
+ const statusBefore = await this.readCommand("git", ["-C", workspaceDir, "status", "--short"]);
733
+ if (!statusBefore.trim()) {
734
+ throw new Error(`Copy workspace ${workspaceDir} has no changes to publish.`);
735
+ }
736
+ const materialized = await this.commitExistingWorkspace(bundle, { ...target, repoDir: workspaceDir });
737
+ try {
738
+ return await this.pushGerritCommit(materialized);
739
+ }
740
+ finally {
741
+ await this.cleanupMaterializedCommits([materialized]);
742
+ }
743
+ }
744
+ async submitGitRootStagedGerrit(bundle, target, taskRootDir) {
745
+ const stageDir = join(taskRootDir, ".optimus", "publication", "stage", "repo-root");
746
+ const baseRef = await this.resolveBaseRef(target.repoDir, target.upstreamRemote ?? target.reviewSystem.remoteName, target.targetBranch);
747
+ await this.prepareStageWorktree(target.repoDir, stageDir, baseRef);
748
+ try {
749
+ const materialized = await this.materializePatchCommit(bundle, { ...target, repoDir: stageDir });
750
+ const output = await this.readCommand("git", [
751
+ "-C",
752
+ stageDir,
753
+ "push",
754
+ target.upstreamRemote ?? target.reviewSystem.remoteName ?? "origin",
755
+ this.buildGerritPushRef(this.requireGerritBranch(target.targetBranch))
756
+ ]);
757
+ return this.buildExecutionRecord({
758
+ label: target.label,
759
+ status: "submitted",
760
+ strategy: target.reviewSystem.strategy,
761
+ targetBranch: target.targetBranch,
762
+ remoteName: target.upstreamRemote ?? target.reviewSystem.remoteName,
763
+ reviewUrl: this.extractReviewUrl(output, target.reviewSystem),
764
+ changeId: materialized.changeId,
765
+ detail: this.compact(output)
766
+ });
767
+ }
768
+ finally {
769
+ await this.cleanupStageWorktree(target.repoDir, stageDir);
770
+ }
771
+ }
772
+ async submitGithubTarget(bundle, target, context, taskRootDir) {
773
+ const directWorkspace = context?.addresses.mode === "copy" || context?.addresses.mode === "inplace";
774
+ const stageDir = directWorkspace
775
+ ? (context?.addresses.mode === "copy" ? context.addresses.workspaceDir : target.repoDir)
776
+ : join(taskRootDir, ".optimus", "publication", "stage", this.slugify(target.label));
777
+ const branchTracking = context?.addresses.mode === "inplace"
778
+ ? await this.captureBranchTrackingSnapshot(stageDir)
779
+ : undefined;
780
+ let stagedWithWorktree = false;
781
+ if (!directWorkspace) {
782
+ const baseRef = await this.resolveBaseRef(target.repoDir, target.upstreamRemote ?? target.reviewSystem.remoteName, target.targetBranch);
783
+ await this.prepareStageWorktree(target.repoDir, stageDir, baseRef);
784
+ stagedWithWorktree = true;
785
+ }
786
+ let materialized;
787
+ try {
788
+ materialized = directWorkspace
789
+ ? await this.commitExistingWorkspace(bundle, { ...target, repoDir: stageDir })
790
+ : await this.materializePatchCommit(bundle, { ...target, repoDir: stageDir });
791
+ const branchName = this.buildGithubBranchName(bundle.taskId, target.projectPath);
792
+ await this.runCommand("git", ["-C", stageDir, "push", "-u", target.upstreamRemote ?? target.reviewSystem.remoteName ?? "origin", `HEAD:refs/heads/${branchName}`]);
793
+ const prOutput = await this.readCommand("gh", [
794
+ "pr", "create",
795
+ "--base", this.requireGithubBranch(target.targetBranch),
796
+ "--head", branchName,
797
+ "--title", bundle.summary.title,
798
+ "--body", this.buildGithubPrBody(bundle, target)
799
+ ], { cwd: stageDir });
800
+ const reviewUrl = this.extractReviewUrl(prOutput, target.reviewSystem);
801
+ return this.buildExecutionRecord({
802
+ label: target.label,
803
+ status: "submitted",
804
+ strategy: target.reviewSystem.strategy,
805
+ targetBranch: target.targetBranch,
806
+ remoteName: target.upstreamRemote ?? target.reviewSystem.remoteName,
807
+ submittedRef: branchName,
808
+ reviewUrl,
809
+ changeId: materialized.changeId,
810
+ detail: this.compact(prOutput)
811
+ });
812
+ }
813
+ finally {
814
+ if (directWorkspace && materialized) {
815
+ await this.cleanupMaterializedCommits([materialized]);
816
+ }
817
+ if (branchTracking) {
818
+ await this.restoreBranchTrackingSnapshot(stageDir, branchTracking);
819
+ }
820
+ if (stagedWithWorktree) {
821
+ await this.cleanupStageWorktree(target.repoDir, stageDir);
822
+ }
823
+ }
824
+ }
825
+ async captureBranchTrackingSnapshot(repoDir) {
826
+ const headCommit = await this.readCommand("git", ["-C", repoDir, "rev-parse", "HEAD"])
827
+ .then((value) => value.trim())
828
+ .catch(() => undefined);
829
+ if (!headCommit) {
830
+ return undefined;
831
+ }
832
+ const branchName = await this.readCommand("git", ["-C", repoDir, "rev-parse", "--abbrev-ref", "HEAD"])
833
+ .then((value) => value.trim())
834
+ .catch(() => undefined);
835
+ const normalizedBranchName = branchName && branchName !== "HEAD" ? branchName : undefined;
836
+ const upstreamRef = normalizedBranchName
837
+ ? await this.readCommand("git", ["-C", repoDir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
838
+ .then((value) => value.trim())
839
+ .catch(() => undefined)
840
+ : undefined;
841
+ return {
842
+ headCommit,
843
+ ...(normalizedBranchName ? { branchName: normalizedBranchName } : {}),
844
+ ...(upstreamRef ? { upstreamRef } : {})
845
+ };
846
+ }
847
+ async restoreBranchTrackingSnapshot(repoDir, snapshot) {
848
+ if (!snapshot.branchName) {
849
+ return;
850
+ }
851
+ try {
852
+ await this.runCommand("git", ["-C", repoDir, "branch", "--unset-upstream", snapshot.branchName]);
853
+ }
854
+ catch {
855
+ // Keep cleanup best-effort when the branch has no upstream.
856
+ }
857
+ if (snapshot.upstreamRef) {
858
+ await this.runCommand("git", ["-C", repoDir, "branch", "--set-upstream-to", snapshot.upstreamRef, snapshot.branchName]);
859
+ }
860
+ }
861
+ async materializePatchCommit(bundle, target) {
862
+ const baseHead = await this.readCommand("git", ["-C", target.repoDir, "rev-parse", "HEAD"])
863
+ .then((value) => value.trim())
864
+ .catch(() => undefined);
865
+ const patchFile = join(dirname(target.repoDir), `${this.slugify(target.label)}.publication.diff`);
866
+ await writeFile(patchFile, this.ensureTrailingNewline(target.patchContent), "utf8");
867
+ try {
868
+ await this.runCommand("git", ["-C", target.repoDir, "apply", patchFile]);
869
+ const statusEntries = await this.readGitStatusEntries(target.repoDir);
870
+ await this.stageBusinessChanges(target.repoDir, statusEntries);
871
+ if (!filterBusinessChangeEntries(statusEntries).length) {
872
+ return { target, commitHash: "", changeId: undefined, baseHead };
873
+ }
874
+ const changeId = target.reviewSystem.strategy === "gerrit_change"
875
+ ? this.buildGerritChangeId(bundle, target)
876
+ : undefined;
877
+ const changedPaths = collectBusinessChangePaths(statusEntries);
878
+ await this.runCommand("git", ["-C", target.repoDir, "commit", "--signoff", "-m", this.buildCommitMessage(bundle, target, changedPaths)]);
879
+ const commitHash = await this.readCommand("git", ["-C", target.repoDir, "rev-parse", "HEAD"]);
880
+ return {
881
+ target,
882
+ commitHash: commitHash.trim(),
883
+ changeId,
884
+ baseHead
885
+ };
886
+ }
887
+ finally {
888
+ await rm(patchFile, { force: true });
889
+ }
890
+ }
891
+ async commitExistingWorkspace(bundle, target) {
892
+ const baseHead = await this.readCommand("git", ["-C", target.repoDir, "rev-parse", "HEAD"])
893
+ .then((value) => value.trim())
894
+ .catch(() => undefined);
895
+ const statusEntries = await this.readGitStatusEntries(target.repoDir);
896
+ await this.stageBusinessChanges(target.repoDir, statusEntries);
897
+ if (!filterBusinessChangeEntries(statusEntries).length) {
898
+ throw new Error(`Workspace ${target.repoDir} has no changes to publish.`);
899
+ }
900
+ const changedPaths = collectBusinessChangePaths(statusEntries);
901
+ await this.runCommand("git", ["-C", target.repoDir, "commit", "--signoff", "-m", this.buildCommitMessage(bundle, target, changedPaths)]);
902
+ const commitHash = await this.readCommand("git", ["-C", target.repoDir, "rev-parse", "HEAD"]);
903
+ return {
904
+ target,
905
+ commitHash: commitHash.trim(),
906
+ changeId: target.reviewSystem.strategy === "gerrit_change" ? this.buildGerritChangeId(bundle, target) : undefined,
907
+ baseHead
908
+ };
909
+ }
910
+ async pushGerritCommit(materialized, topic) {
911
+ const branch = this.requireGerritBranch(materialized.target.targetBranch);
912
+ const output = await this.readCommand("git", [
913
+ "-C",
914
+ materialized.target.repoDir,
915
+ "push",
916
+ materialized.target.upstreamRemote ?? materialized.target.reviewSystem.remoteName ?? "origin",
917
+ this.buildGerritPushRef(branch, topic)
918
+ ]);
919
+ const topicUrl = this.buildGerritTopicUrl(materialized.target.reviewSystem, topic);
920
+ return this.buildExecutionRecord({
921
+ label: materialized.target.label,
922
+ status: "submitted",
923
+ strategy: materialized.target.reviewSystem.strategy,
924
+ ...(topic ? { topic } : {}),
925
+ ...(topicUrl ? { topicUrl } : {}),
926
+ targetBranch: materialized.target.targetBranch,
927
+ remoteName: materialized.target.upstreamRemote ?? materialized.target.reviewSystem.remoteName,
928
+ submittedRef: topic,
929
+ reviewUrl: this.extractReviewUrl(output, materialized.target.reviewSystem),
930
+ changeId: materialized.changeId,
931
+ detail: this.compact(output)
932
+ });
933
+ }
934
+ async refreshRepoProjectTarget(target, targetBranch) {
935
+ if (!targetBranch) {
936
+ return;
937
+ }
938
+ const branch = this.normalizeGerritTarget(targetBranch);
939
+ const remote = target.upstreamRemote ?? target.reviewSystem.remoteName ?? "origin";
940
+ if (!branch) {
941
+ return;
942
+ }
943
+ await this.runCommand("git", ["-C", target.repoDir, "fetch", remote, branch]);
944
+ await this.runCommand("git", ["-C", target.repoDir, "reset", "--hard", "FETCH_HEAD"]);
945
+ }
946
+ async refreshGitTarget(target, targetBranch, repoDirOverride) {
947
+ const branch = this.normalizeGitHubBaseBranch(targetBranch);
948
+ const remote = target.upstreamRemote ?? target.reviewSystem.remoteName ?? "origin";
949
+ const repoDir = repoDirOverride ?? target.repoDir;
950
+ if (!branch) {
951
+ return;
952
+ }
953
+ await this.runCommand("git", ["-C", repoDir, "fetch", remote, branch]);
954
+ }
955
+ shouldUseDirectWorkspaceCommits(context) {
956
+ return context?.addresses.mode === "inplace";
957
+ }
958
+ async cleanupMaterializedCommits(materials) {
959
+ for (const materialized of materials) {
960
+ if (!materialized.commitHash) {
961
+ await this.resetWorkspaceToBase(materialized.target.repoDir, materialized.baseHead);
962
+ continue;
963
+ }
964
+ try {
965
+ await this.resetWorkspaceToBase(materialized.target.repoDir, materialized.baseHead ?? `${materialized.commitHash}^`);
966
+ }
967
+ catch {
968
+ await this.resetWorkspaceToBase(materialized.target.repoDir, materialized.baseHead);
969
+ }
970
+ }
971
+ }
972
+ async resetWorkspaceToBase(repoDir, baseRef) {
973
+ await this.runCommand("git", ["-C", repoDir, "reset", "--hard", baseRef ?? "HEAD"]);
974
+ const statusEntries = await this.readGitStatusEntries(repoDir);
975
+ const untrackedBusinessPaths = collectBusinessChangePaths(statusEntries.filter((entry) => entry.isUntracked));
976
+ for (const relativePath of untrackedBusinessPaths) {
977
+ await rm(join(repoDir, relativePath), { recursive: true, force: true });
978
+ }
979
+ }
980
+ async resolveBaseRef(repoDir, remoteName, targetBranch) {
981
+ const normalizedBranch = this.normalizeGitHubBaseBranch(targetBranch);
982
+ if (remoteName && normalizedBranch) {
983
+ try {
984
+ await this.runCommand("git", ["-C", repoDir, "fetch", remoteName, normalizedBranch]);
985
+ return "FETCH_HEAD";
986
+ }
987
+ catch {
988
+ // Fall back to local HEAD when fetch is unavailable or blocked.
989
+ }
990
+ }
991
+ const head = await this.readCommand("git", ["-C", repoDir, "rev-parse", "HEAD"]);
992
+ return head.trim() || "HEAD";
993
+ }
994
+ async prepareStageWorktree(repoDir, stageDir, baseRef) {
995
+ await rm(stageDir, { recursive: true, force: true });
996
+ await mkdir(dirname(stageDir), { recursive: true });
997
+ await this.runCommand("git", ["-C", repoDir, "worktree", "add", "--detach", stageDir, baseRef]);
998
+ }
999
+ async cleanupStageWorktree(repoDir, stageDir) {
1000
+ try {
1001
+ await this.runCommand("git", ["-C", repoDir, "worktree", "remove", "--force", stageDir]);
1002
+ }
1003
+ catch {
1004
+ await rm(stageDir, { recursive: true, force: true });
1005
+ }
1006
+ }
1007
+ parsePatchSections(content) {
1008
+ const normalized = content.replace(/\r\n/g, "\n");
1009
+ const lines = normalized.split("\n");
1010
+ const sections = [];
1011
+ let current = [];
1012
+ for (const line of lines) {
1013
+ if (line.startsWith("diff --git ") && current.length > 0) {
1014
+ sections.push(current);
1015
+ current = [];
1016
+ }
1017
+ current.push(line);
1018
+ }
1019
+ if (current.length > 0) {
1020
+ sections.push(current);
1021
+ }
1022
+ return sections.map((sectionLines) => {
1023
+ const raw = this.ensureTrailingNewline(sectionLines.join("\n"));
1024
+ return this.buildPatchSection(raw, this.extractPatchPath(raw));
1025
+ });
1026
+ }
1027
+ extractPatchPath(raw) {
1028
+ const plusLine = raw.split("\n").find((line) => line.startsWith("+++ "));
1029
+ if (!plusLine) {
1030
+ return undefined;
1031
+ }
1032
+ const value = plusLine.slice(4).trim();
1033
+ if (value === "/dev/null") {
1034
+ const minusLine = raw.split("\n").find((line) => line.startsWith("--- a/"));
1035
+ return minusLine ? minusLine.slice("--- a/".length).trim() : undefined;
1036
+ }
1037
+ return value.startsWith("b/") ? value.slice(2).trim() : value;
1038
+ }
1039
+ buildProjectPatch(sections, projectPath) {
1040
+ const matched = sections
1041
+ .filter((section) => section.path && this.pathMatchesProject(section.path, projectPath))
1042
+ .map((section) => this.rewritePatchForProject(section.raw, projectPath));
1043
+ if (matched.length === 0) {
1044
+ return undefined;
1045
+ }
1046
+ return this.ensureTrailingNewline(matched.join("\n"));
1047
+ }
1048
+ rewritePatchForProject(raw, projectPath) {
1049
+ const prefix = `${projectPath.replace(/^\/+|\/+$/g, "")}/`;
1050
+ const lines = raw.replace(/\r\n/g, "\n").split("\n");
1051
+ const rewritten = lines.map((line) => {
1052
+ if (line.startsWith("diff --git ")) {
1053
+ return line
1054
+ .replace(` a/${prefix}`, " a/")
1055
+ .replace(` b/${prefix}`, " b/");
1056
+ }
1057
+ if (line.startsWith("--- a/")) {
1058
+ return line.replace(`--- a/${prefix}`, "--- a/");
1059
+ }
1060
+ if (line.startsWith("+++ b/")) {
1061
+ return line.replace(`+++ b/${prefix}`, "+++ b/");
1062
+ }
1063
+ if (line.startsWith("rename from ")) {
1064
+ return line.replace(`rename from ${prefix}`, "rename from ");
1065
+ }
1066
+ if (line.startsWith("rename to ")) {
1067
+ return line.replace(`rename to ${prefix}`, "rename to ");
1068
+ }
1069
+ if (line.startsWith("copy from ")) {
1070
+ return line.replace(`copy from ${prefix}`, "copy from ");
1071
+ }
1072
+ if (line.startsWith("copy to ")) {
1073
+ return line.replace(`copy to ${prefix}`, "copy to ");
1074
+ }
1075
+ return line;
1076
+ });
1077
+ return this.ensureTrailingNewline(rewritten.join("\n"));
1078
+ }
1079
+ pathMatchesProject(changedPath, projectPath) {
1080
+ return changedPath === projectPath || changedPath.startsWith(`${projectPath}/`);
1081
+ }
1082
+ buildCommitMessage(bundle, target, changedPaths) {
1083
+ return this.commitMessageBuilder.build({
1084
+ bundle,
1085
+ target,
1086
+ changedPaths
1087
+ });
1088
+ }
1089
+ buildGerritChangeId(bundle, target) {
1090
+ const hash = createHash("sha1")
1091
+ .update(`${bundle.taskId}:${target.projectPath}:${target.targetBranch ?? "none"}:${bundle.summary.title}`)
1092
+ .digest("hex");
1093
+ return `I${hash}`;
1094
+ }
1095
+ buildGithubPrBody(bundle, target) {
1096
+ const lines = [
1097
+ `Task: ${bundle.taskId}`,
1098
+ `Outcome: ${bundle.outcome}`,
1099
+ `Decision: ${bundle.summary.decision}`
1100
+ ];
1101
+ if (target.projectPath !== ".") {
1102
+ lines.push(`Project: ${target.projectPath}`);
1103
+ }
1104
+ if (bundle.summary.validation) {
1105
+ lines.push(`Validation: ${bundle.summary.validation}`);
1106
+ }
1107
+ return lines.join("\n");
1108
+ }
1109
+ buildGithubBranchName(taskId, projectPath) {
1110
+ const suffix = projectPath === "." ? "root" : this.slugify(projectPath);
1111
+ return `optimus/${taskId}-${suffix}`;
1112
+ }
1113
+ buildTopic(taskId) {
1114
+ const normalized = taskId.trim();
1115
+ const runtimeTaskSuffix = normalized.match(/^task-\d+-([a-f0-9]{8,})$/i)?.[1]?.toLowerCase();
1116
+ if (runtimeTaskSuffix) {
1117
+ return `optimus-${runtimeTaskSuffix}`;
1118
+ }
1119
+ const fallback = normalized.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase();
1120
+ return fallback ? `optimus-${fallback}` : "optimus";
1121
+ }
1122
+ buildGerritPushRef(branch, topic) {
1123
+ return `HEAD:refs/for/${branch}${topic ? `%topic=${topic}` : ""}`;
1124
+ }
1125
+ requireGerritBranch(branch) {
1126
+ const normalized = this.normalizeGerritTarget(branch);
1127
+ if (!normalized) {
1128
+ throw new Error("Missing Gerrit target branch.");
1129
+ }
1130
+ return normalized;
1131
+ }
1132
+ requireGithubBranch(branch) {
1133
+ const normalized = this.normalizeGitHubBaseBranch(branch);
1134
+ if (!normalized) {
1135
+ throw new Error("Missing GitHub base branch.");
1136
+ }
1137
+ return normalized;
1138
+ }
1139
+ buildPatchTarget(input) {
1140
+ return {
1141
+ label: input.label,
1142
+ projectPath: input.projectPath,
1143
+ repoDir: input.repoDir,
1144
+ ...(input.targetBranch ? { targetBranch: input.targetBranch } : {}),
1145
+ ...(input.upstreamRemote ? { upstreamRemote: input.upstreamRemote } : {}),
1146
+ ...(input.upstreamBranch ? { upstreamBranch: input.upstreamBranch } : {}),
1147
+ reviewSystem: input.reviewSystem,
1148
+ patchContent: input.patchContent
1149
+ };
1150
+ }
1151
+ buildExecutionRecord(input) {
1152
+ return {
1153
+ label: input.label,
1154
+ status: input.status,
1155
+ strategy: input.strategy,
1156
+ ...(input.topic ? { topic: input.topic } : {}),
1157
+ ...(input.topicUrl ? { topicUrl: input.topicUrl } : {}),
1158
+ ...(input.targetBranch ? { targetBranch: input.targetBranch } : {}),
1159
+ ...(input.remoteName ? { remoteName: input.remoteName } : {}),
1160
+ ...(input.submittedRef ? { submittedRef: input.submittedRef } : {}),
1161
+ ...(input.reviewUrl ? { reviewUrl: input.reviewUrl } : {}),
1162
+ ...(input.changeId ? { changeId: input.changeId } : {}),
1163
+ detail: input.detail
1164
+ };
1165
+ }
1166
+ buildPatchSection(raw, path) {
1167
+ return path ? { raw, path } : { raw };
1168
+ }
1169
+ resolveExecutionStatus(records) {
1170
+ const submittedCount = records.filter((record) => record.status === "submitted").length;
1171
+ const failedCount = records.filter((record) => record.status === "failed").length;
1172
+ const skippedCount = records.filter((record) => record.status === "skipped").length;
1173
+ if (submittedCount > 0 && failedCount === 0 && skippedCount === 0) {
1174
+ return "submitted";
1175
+ }
1176
+ if (submittedCount > 0) {
1177
+ return "partial";
1178
+ }
1179
+ if (failedCount > 0) {
1180
+ return "failed";
1181
+ }
1182
+ return "skipped";
1183
+ }
1184
+ buildExecutionSummary(records, strategy) {
1185
+ const submittedCount = records.filter((record) => record.status === "submitted").length;
1186
+ const failedCount = records.filter((record) => record.status === "failed").length;
1187
+ const skippedCount = records.filter((record) => record.status === "skipped").length;
1188
+ const total = records.length;
1189
+ if (submittedCount === total) {
1190
+ return `Review submission completed for ${submittedCount}/${total} target(s) via ${strategy}.`;
1191
+ }
1192
+ if (submittedCount > 0) {
1193
+ return `Review submission partially completed via ${strategy}: submitted=${submittedCount}, failed=${failedCount}, skipped=${skippedCount}.`;
1194
+ }
1195
+ if (failedCount > 0) {
1196
+ return `Review submission failed for ${failedCount}/${total} target(s) via ${strategy}.`;
1197
+ }
1198
+ return `Review submission skipped for ${skippedCount}/${total} target(s) via ${strategy}.`;
1199
+ }
1200
+ collectReviewMetadata(records) {
1201
+ const submittedGerritRecords = records.filter((record) => record.status === "submitted" && record.strategy === "gerrit_change");
1202
+ const topics = [...new Set(submittedGerritRecords.map((record) => record.topic).filter((value) => Boolean(value)))];
1203
+ const topicUrls = [...new Set(submittedGerritRecords.map((record) => record.topicUrl).filter((value) => Boolean(value)))];
1204
+ const reviewUrls = [...new Set(records.map((record) => record.reviewUrl).filter((value) => Boolean(value)))];
1205
+ const changeIds = [...new Set(records.map((record) => record.changeId).filter((value) => Boolean(value)))];
1206
+ const topic = topics.length === 1 ? topics[0] : undefined;
1207
+ const topicUrl = topicUrls.length === 1 ? topicUrls[0] : undefined;
1208
+ const primaryReviewUrl = submittedGerritRecords.length > 1 && topicUrl ? topicUrl : reviewUrls[0];
1209
+ const firstChangeId = changeIds[0];
1210
+ return {
1211
+ ...(topic ? { topic } : {}),
1212
+ ...(topicUrl ? { topicUrl } : {}),
1213
+ ...(primaryReviewUrl ? { reviewUrl: primaryReviewUrl } : {}),
1214
+ ...(reviewUrls.length > 0 ? { reviewUrls } : {}),
1215
+ ...(firstChangeId ? { changeId: firstChangeId } : {}),
1216
+ ...(changeIds.length > 0 ? { changeIds } : {})
1217
+ };
1218
+ }
1219
+ renderReviewPacket(bundle, plan, records) {
1220
+ const lines = [
1221
+ "# Review Packet",
1222
+ "",
1223
+ `- Task: ${bundle.taskId}`,
1224
+ `- Task Type: ${bundle.taskType}`,
1225
+ `- Outcome: ${bundle.outcome}`,
1226
+ `- Review System: ${this.renderReviewSystem(plan.reviewSystem)}`,
1227
+ `- Patch: ${bundle.artifacts.patchDiff ?? "n/a"}`,
1228
+ "",
1229
+ "## Submission Results",
1230
+ ""
1231
+ ];
1232
+ for (const record of records) {
1233
+ const parts = [`- ${record.label}: ${record.status}`, record.strategy];
1234
+ if (record.remoteName) {
1235
+ parts.push(record.remoteName);
1236
+ }
1237
+ if (record.targetBranch) {
1238
+ parts.push(record.targetBranch);
1239
+ }
1240
+ if (record.submittedRef) {
1241
+ parts.push(record.submittedRef);
1242
+ }
1243
+ if (record.topicUrl) {
1244
+ parts.push(`Topic=${record.topicUrl}`);
1245
+ }
1246
+ if (record.changeId) {
1247
+ parts.push(`Change-Id=${record.changeId}`);
1248
+ }
1249
+ if (record.reviewUrl) {
1250
+ parts.push(record.reviewUrl);
1251
+ }
1252
+ parts.push(this.compact(record.detail));
1253
+ lines.push(parts.join(" | "));
1254
+ }
1255
+ return lines.join("\n");
1256
+ }
1257
+ extractReviewUrl(output, reviewSystem) {
1258
+ const urls = [...output.matchAll(/https?:\/\/\S+/g)]
1259
+ .map((match) => match[0]?.replace(/[)\].,]+$/, ""))
1260
+ .filter((value) => Boolean(value));
1261
+ const githubUrl = urls.find((url) => /github\.com\/.+\/pull\/\d+/i.test(url));
1262
+ if (githubUrl) {
1263
+ return githubUrl;
1264
+ }
1265
+ const gerritUrl = urls.find((url) => /\/c\/.+\/\+\/\d+/i.test(url) || /\/c\/\d+/i.test(url));
1266
+ if (gerritUrl) {
1267
+ return gerritUrl;
1268
+ }
1269
+ const host = reviewSystem?.host;
1270
+ if (!host) {
1271
+ return undefined;
1272
+ }
1273
+ const changeMatch = output.match(/\b(\d{4,})\b/);
1274
+ if (!changeMatch?.[1]) {
1275
+ return undefined;
1276
+ }
1277
+ if (reviewSystem.type === "gerrit" && reviewSystem.repository) {
1278
+ return `https://${host}/c/${reviewSystem.repository}/+/${changeMatch[1]}`;
1279
+ }
1280
+ return `https://${host}/c/${changeMatch[1]}`;
1281
+ }
1282
+ buildGerritTopicUrl(reviewSystem, topic) {
1283
+ if (reviewSystem?.type !== "gerrit" || !topic) {
1284
+ return undefined;
1285
+ }
1286
+ const host = reviewSystem.host?.trim() || this.extractHostFromRemoteUrl(reviewSystem.remoteUrl);
1287
+ if (!host) {
1288
+ return undefined;
1289
+ }
1290
+ return `https://${host}/q/topic:${encodeURIComponent(topic)}`;
1291
+ }
1292
+ extractHostFromRemoteUrl(remoteUrl) {
1293
+ const normalized = remoteUrl?.trim();
1294
+ if (!normalized) {
1295
+ return undefined;
1296
+ }
1297
+ const sshMatch = normalized.match(/^[a-z]+:\/\/[^@]+@([^/:]+)(?::\d+)?\//i);
1298
+ if (sshMatch?.[1]) {
1299
+ return sshMatch[1];
1300
+ }
1301
+ const httpsMatch = normalized.match(/^[a-z]+:\/\/([^/:]+)(?::\d+)?\//i);
1302
+ return httpsMatch?.[1];
1303
+ }
1304
+ slugify(value) {
1305
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "target";
1306
+ }
1307
+ compact(value) {
1308
+ return value.replace(/\s+/g, " ").trim();
1309
+ }
1310
+ ensureTrailingNewline(value) {
1311
+ return value.endsWith("\n") ? value : `${value}\n`;
1312
+ }
1313
+ async readGitStatusEntries(repoDir) {
1314
+ try {
1315
+ const { stdout } = await execFileAsync("git", ["-C", repoDir, "status", "--porcelain=v1", "--untracked-files=all"], {
1316
+ maxBuffer: EXEC_MAX_BUFFER
1317
+ });
1318
+ return parseGitStatusOutput(stdout);
1319
+ }
1320
+ catch (error) {
1321
+ throw new Error(this.renderExecError("git", ["-C", repoDir, "status", "--porcelain=v1", "--untracked-files=all"], error));
1322
+ }
1323
+ }
1324
+ async stageBusinessChanges(repoDir, entries) {
1325
+ const paths = collectBusinessChangePaths(entries);
1326
+ if (paths.length === 0) {
1327
+ return;
1328
+ }
1329
+ await this.runCommand("git", ["-C", repoDir, "add", "-A", "--", ...paths]);
1330
+ }
1331
+ async runCommand(command, args, options) {
1332
+ try {
1333
+ await execFileAsync(command, args, {
1334
+ cwd: options?.cwd,
1335
+ maxBuffer: EXEC_MAX_BUFFER,
1336
+ ...(typeof options?.timeoutMs === "number" ? { timeout: options.timeoutMs } : {})
1337
+ });
1338
+ }
1339
+ catch (error) {
1340
+ throw new Error(this.renderExecError(command, args, error));
1341
+ }
1342
+ }
1343
+ async readCommand(command, args, options) {
1344
+ try {
1345
+ const { stdout, stderr } = await execFileAsync(command, args, {
1346
+ cwd: options?.cwd,
1347
+ maxBuffer: EXEC_MAX_BUFFER,
1348
+ ...(typeof options?.timeoutMs === "number" ? { timeout: options.timeoutMs } : {})
1349
+ });
1350
+ return `${stdout}${stderr}`.trim();
1351
+ }
1352
+ catch (error) {
1353
+ throw new Error(this.renderExecError(command, args, error));
1354
+ }
1355
+ }
1356
+ renderExecError(command, args, error) {
1357
+ if (error && typeof error === "object") {
1358
+ const failure = error;
1359
+ if (command === "gh" && (failure.code === "ENOENT" || failure.message?.includes("spawn ENOTDIR") || failure.message?.includes("spawn ENOENT"))) {
1360
+ return `gh ${args.join(" ")} failed: github_cli_missing. Install GitHub CLI and run 'gh auth login'.`;
1361
+ }
1362
+ const output = [failure.stdout, failure.stderr].filter(Boolean).join(" ").trim();
1363
+ const timeoutDetail = failure.killed && failure.signal === "SIGTERM" ? ": command timed out" : "";
1364
+ return `${command} ${args.join(" ")} failed${timeoutDetail}${failure.message ? `${timeoutDetail ? ";" : ":"} ${failure.message}` : ""}${output ? ` | ${this.compact(output)}` : ""}`;
1365
+ }
1366
+ return `${command} ${args.join(" ")} failed: ${String(error)}`;
1367
+ }
1368
+ }
1369
+ //# sourceMappingURL=task-publication-service.js.map