holo-codex 0.1.0

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 (149) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +215 -0
  5. package/README.zh-CN.md +215 -0
  6. package/SECURITY.md +39 -0
  7. package/assets/brand/README.md +35 -0
  8. package/assets/brand/holo-codex-icon.svg +28 -0
  9. package/assets/brand/holo-codex-lockup.svg +49 -0
  10. package/assets/brand/holo-codex-mark.svg +33 -0
  11. package/assets/brand/holo-codex-plugin-card.png +0 -0
  12. package/assets/brand/holo-codex-plugin-card.svg +81 -0
  13. package/assets/brand/holo-codex-readme-hero.png +0 -0
  14. package/assets/brand/holo-codex-readme-hero.svg +140 -0
  15. package/assets/brand/holo-codex-social-preview.png +0 -0
  16. package/assets/brand/holo-codex-social-preview.svg +130 -0
  17. package/assets/brand/holo-codex-wordmark-options.svg +52 -0
  18. package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
  19. package/docs/examples/generic-loop-repo-hygiene.md +168 -0
  20. package/docs/install.md +190 -0
  21. package/docs/local-release-readiness.md +206 -0
  22. package/docs/release-checklist.md +144 -0
  23. package/docs/self-bootstrap.md +150 -0
  24. package/docs/trust-and-safety.md +45 -0
  25. package/package.json +83 -0
  26. package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
  27. package/plugins/autonomous-pr-loop/.mcp.json +13 -0
  28. package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
  29. package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
  30. package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
  31. package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
  32. package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
  33. package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
  34. package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
  35. package/plugins/autonomous-pr-loop/core/command.ts +47 -0
  36. package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
  37. package/plugins/autonomous-pr-loop/core/config.ts +293 -0
  38. package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
  39. package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
  40. package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
  41. package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
  42. package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
  43. package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
  44. package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
  45. package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
  46. package/plugins/autonomous-pr-loop/core/git.ts +213 -0
  47. package/plugins/autonomous-pr-loop/core/github.ts +269 -0
  48. package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
  49. package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
  50. package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
  51. package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
  52. package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
  53. package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
  54. package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
  55. package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
  56. package/plugins/autonomous-pr-loop/core/index.ts +32 -0
  57. package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
  58. package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
  59. package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
  60. package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
  61. package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
  62. package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
  63. package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
  64. package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
  65. package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
  66. package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
  67. package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
  68. package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
  69. package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
  70. package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
  71. package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
  72. package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
  73. package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
  74. package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
  75. package/plugins/autonomous-pr-loop/core/types.ts +567 -0
  76. package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
  77. package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
  78. package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
  79. package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
  80. package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
  81. package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
  82. package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
  83. package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
  84. package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
  85. package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
  86. package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
  87. package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
  88. package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
  89. package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
  90. package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
  91. package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
  92. package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
  93. package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
  94. package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
  95. package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
  96. package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
  97. package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
  98. package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
  99. package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
  100. package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
  101. package/plugins/autonomous-pr-loop/package.json +9 -0
  102. package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
  103. package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
  104. package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
  105. package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
  106. package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
  107. package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
  108. package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
  109. package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
  110. package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
  111. package/plugins/autonomous-pr-loop/ui/index.html +26 -0
  112. package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
  113. package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
  114. package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
  115. package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
  116. package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
  117. package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
  118. package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
  119. package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
  120. package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
  121. package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
  122. package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
  123. package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
  124. package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
  125. package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
  126. package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
  127. package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
  128. package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
  129. package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
  130. package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
  131. package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
  132. package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
  133. package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
  134. package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
  135. package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
  136. package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
  137. package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
  138. package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
  139. package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
  140. package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
  141. package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
  142. package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
  143. package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
  144. package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
  145. package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
  146. package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
  147. package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
  148. package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
  149. package/tsconfig.json +18 -0
@@ -0,0 +1,464 @@
1
+ import { CommandRunner } from "./command-runner.js";
2
+ import { evaluateCiChecks } from "./ci.js";
3
+ import {
4
+ createBranch,
5
+ getChangedFiles,
6
+ getCurrentBranch,
7
+ pushBranch,
8
+ stagePaths,
9
+ commit,
10
+ syncBaseBranch
11
+ } from "./git.js";
12
+ import {
13
+ createDraftPullRequest,
14
+ fetchReviewThreads,
15
+ type GitHubCommandOptions,
16
+ listPullRequestsByHead,
17
+ mergePullRequest,
18
+ viewPullRequest
19
+ } from "./github.js";
20
+ import { gitnexusAnalyze, gitnexusDetectChanges, gitnexusStatus } from "./gitnexus.js";
21
+ import { actionableReviewComments, parseReviewThreads } from "./review-comments.js";
22
+ import { assertAllowedPath } from "./policy.js";
23
+ import { AgentLoopError } from "./errors.js";
24
+ import { evaluateMergeReadiness } from "./autonomy-policy.js";
25
+ import { resolvePrSelection } from "./pr-selector.js";
26
+ import { getDeliveryWorkItem } from "./delivery-work-item.js";
27
+ import type { AgentLoopRun, AgentLoopConfig, AgentLoopStorage, AgentLoopCiCheck, AgentLoopDecision, AgentLoopReviewComment } from "./types.js";
28
+ import type { AgentLoopState } from "./state-types.js";
29
+
30
+ export interface LifecycleStepResult {
31
+ nextState?: AgentLoopState;
32
+ branch?: string;
33
+ worktreeClean?: boolean;
34
+ message: string;
35
+ }
36
+
37
+ interface LifecycleInput {
38
+ repoRoot: string;
39
+ storage: AgentLoopStorage;
40
+ run: AgentLoopRun;
41
+ config: AgentLoopConfig;
42
+ state?: AgentLoopState;
43
+ signal?: AbortSignal | undefined;
44
+ }
45
+
46
+ /** Execute PR C real lifecycle behavior for one state-machine state. */
47
+ export async function executePrLifecycleStep(input: LifecycleInput & { state: AgentLoopState }): Promise<LifecycleStepResult> {
48
+ if (input.state === "SYNC_MAIN") {
49
+ syncBaseBranch(input.repoRoot, input.config.baseBranch);
50
+ gitnexusAnalyze(input.repoRoot, input.config);
51
+ gitnexusStatus(input.repoRoot, input.config);
52
+ return { message: "Base branch synced.", branch: input.config.baseBranch, worktreeClean: true };
53
+ }
54
+ if (input.state === "CREATE_BRANCH") {
55
+ const branch = createBranch(input.repoRoot, branchName(input), {
56
+ storage: input.storage,
57
+ runId: input.run.id
58
+ }).branch;
59
+ return { message: "Lifecycle branch ready.", ...(branch ? { branch } : {}), worktreeClean: true };
60
+ }
61
+ if (input.state === "SELF_CHECK") {
62
+ await runSelfChecks(input.repoRoot, input.storage, input.run.id, input.config, input.signal);
63
+ const detect = gitnexusDetectChanges(input.repoRoot, input.config, input.storage, input.run.id);
64
+ input.storage.recordRunCheck({
65
+ runId: input.run.id,
66
+ kind: "gitnexus_detect_changes",
67
+ status: detect.ok ? "passed" : "skipped",
68
+ details: { ok: detect.ok, skipped: !input.config.gitnexusRequired }
69
+ });
70
+ if (detect.ok) {
71
+ input.storage.appendEvent({
72
+ runId: input.run.id,
73
+ kind: "gitnexus_detect_changes_passed",
74
+ message: "GitNexus detect_changes passed during SELF_CHECK."
75
+ });
76
+ }
77
+ input.storage.recordRunCheck({
78
+ runId: input.run.id,
79
+ kind: "self_check",
80
+ status: "passed"
81
+ });
82
+ input.storage.appendEvent({
83
+ runId: input.run.id,
84
+ kind: "self_check_passed",
85
+ message: "SELF_CHECK passed before publish."
86
+ });
87
+ return { message: "Self checks passed." };
88
+ }
89
+ if (input.state === "COMMIT_PUSH_PR") {
90
+ return await commitPushPr(input);
91
+ }
92
+ if (input.state === "WAIT_REVIEW_OR_CI") {
93
+ return await waitReviewOrCi(input);
94
+ }
95
+ if (input.state === "READY_TO_MERGE") {
96
+ if (input.config.mergeMode === "conditional") {
97
+ assertConditionalMergeReadiness(input);
98
+ return { nextState: "MERGE", message: "Auto-merge enabled; advancing to MERGE." };
99
+ }
100
+ throw new AgentLoopError("merge_requires_confirmation", "Ready to merge; explicit MERGE state required.", {
101
+ exitCode: 2
102
+ });
103
+ }
104
+ if (input.state === "MERGE") {
105
+ return await maybeMerge(input);
106
+ }
107
+ return { message: `No PR C lifecycle action for ${input.state}.` };
108
+ }
109
+
110
+ async function runSelfChecks(
111
+ repoRoot: string,
112
+ storage: AgentLoopStorage,
113
+ runId: string,
114
+ config: AgentLoopConfig,
115
+ signal?: AbortSignal
116
+ ): Promise<void> {
117
+ const runner = new CommandRunner({ repoRoot, storage, runId, config, signal });
118
+ for (const command of [config.lintCommand, config.testCommand].filter(isDefined)) {
119
+ const plan = parseConfiguredCommand(command, repoRoot);
120
+ const result = await runner.run(plan, false);
121
+ if (result.exitCode !== 0) {
122
+ throw new AgentLoopError("policy_violation", "Configured self-check command failed.", {
123
+ details: { command, exitCode: result.exitCode },
124
+ exitCode: 2
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ async function commitPushPr(input: LifecycleInput): Promise<LifecycleStepResult> {
131
+ const branch = getCurrentBranch(input.repoRoot);
132
+ const existing = (await listPullRequestsByHead({ repoRoot: input.repoRoot, config: input.config }, branch))[0];
133
+ if (existing) {
134
+ input.storage.upsertPrLink({
135
+ runId: input.run.id,
136
+ branch,
137
+ prNumber: existing.number,
138
+ url: existing.url,
139
+ headRef: existing.headRefName,
140
+ baseRef: existing.baseRefName,
141
+ state: existing.state,
142
+ draft: existing.isDraft
143
+ });
144
+ input.storage.appendDecision({
145
+ runId: input.run.id,
146
+ kind: "pr_reused",
147
+ message: `Reused existing PR #${existing.number}.`,
148
+ details: { branch }
149
+ });
150
+ return { nextState: "WAIT_REVIEW_OR_CI", branch, message: "Existing PR reused." };
151
+ }
152
+
153
+ assertPublishPrerequisites(input);
154
+ gitnexusDetectChanges(input.repoRoot, input.config, input.storage, input.run.id);
155
+ const changedFiles = getChangedFiles(input.repoRoot).filter((file) => !isRuntimePath(file));
156
+ const branchHasChanges = branchDiffersFromBase(input.repoRoot, input.config.baseBranch);
157
+ if (changedFiles.length === 0 && !branchHasChanges) {
158
+ input.storage.appendDecision({
159
+ runId: input.run.id,
160
+ kind: "no_diff",
161
+ message: "No repository diff; skipped commit, push, and PR creation."
162
+ });
163
+ return { message: "No diff to publish.", branch };
164
+ }
165
+ if (changedFiles.length > 0) {
166
+ for (const file of changedFiles) {
167
+ assertAllowedPath(input.config, file);
168
+ }
169
+ stagePaths(input.repoRoot, changedFiles);
170
+ commit(input.repoRoot, `agent-loop: ${branch}`);
171
+ } else {
172
+ input.storage.appendDecision({
173
+ runId: input.run.id,
174
+ kind: "existing_branch_diff",
175
+ message: "No worktree diff, but branch differs from base; continuing push/PR recovery.",
176
+ details: { branch, baseBranch: input.config.baseBranch }
177
+ });
178
+ }
179
+ pushBranch(input.repoRoot, branch);
180
+ const createdUrl = createDraftPullRequest({
181
+ repoRoot: input.repoRoot,
182
+ config: input.config
183
+ }, {
184
+ title: `Agent Loop: ${branch}`,
185
+ body: "Draft PR created by agent-loop PR C lifecycle.",
186
+ head: branch,
187
+ base: input.config.baseBranch
188
+ });
189
+ input.storage.appendDecision({
190
+ runId: input.run.id,
191
+ kind: "draft_pr_create_returned",
192
+ message: "gh pr create returned a draft PR URL; re-querying by head branch to persist PR metadata.",
193
+ details: { url: createdUrl, branch }
194
+ });
195
+ const created = (await listPullRequestsByHead({ repoRoot: input.repoRoot, config: input.config }, branch))[0];
196
+ if (created) {
197
+ input.storage.upsertPrLink({
198
+ runId: input.run.id,
199
+ branch,
200
+ prNumber: created.number,
201
+ url: created.url,
202
+ headRef: created.headRefName,
203
+ baseRef: created.baseRefName,
204
+ state: created.state,
205
+ draft: created.isDraft
206
+ });
207
+ }
208
+ return { nextState: "WAIT_REVIEW_OR_CI", branch, worktreeClean: true, message: "Draft PR published." };
209
+ }
210
+
211
+ function assertPublishPrerequisites(input: LifecycleInput): void {
212
+ const selfCheckPassed = input.storage.hasRunCheck(input.run.id, "self_check");
213
+ const detectRecorded = input.storage.hasRunCheck(input.run.id, "gitnexus_detect_changes");
214
+ if (!selfCheckPassed || !detectRecorded) {
215
+ throw new AgentLoopError("policy_violation", "Publish prerequisites are not satisfied.", {
216
+ details: { selfCheckPassed, detectRecorded },
217
+ exitCode: 2
218
+ });
219
+ }
220
+ }
221
+
222
+ function assertConditionalMergeReadiness(
223
+ input: LifecycleInput,
224
+ overrides: {
225
+ ci?: AgentLoopCiCheck[];
226
+ reviewComments?: AgentLoopReviewComment[];
227
+ decisions?: AgentLoopDecision[];
228
+ } = {}
229
+ ): void {
230
+ const readiness = evaluateMergeReadiness({
231
+ config: input.config,
232
+ ci: overrides.ci ?? input.storage.listCiChecks(input.run.id),
233
+ reviewComments: overrides.reviewComments ?? input.storage.listReviewComments(input.run.id),
234
+ gates: input.storage.listGates(input.run.id),
235
+ decisions: overrides.decisions ?? input.storage.listDecisions(input.run.id),
236
+ runChecks: input.storage.listRunChecks(input.run.id)
237
+ });
238
+ if (!readiness.ready) {
239
+ throw new AgentLoopError("merge_requires_confirmation", "Conditional merge evidence is incomplete.", {
240
+ details: {
241
+ state: readiness.state,
242
+ missingConditions: readiness.missingConditions,
243
+ evidence: readiness.evidence
244
+ },
245
+ exitCode: 2
246
+ });
247
+ }
248
+ }
249
+
250
+ function recordReviewApproval(input: LifecycleInput, reviewDecision: string | undefined): void {
251
+ if (!approvalSatisfied(input.config, reviewDecision)) {
252
+ return;
253
+ }
254
+ if (input.storage.listDecisions(input.run.id).some((decision) => decision.kind === "review_approved")) {
255
+ return;
256
+ }
257
+ input.storage.appendDecision({
258
+ runId: input.run.id,
259
+ kind: "review_approved",
260
+ message: "GitHub review decision approved.",
261
+ details: { reviewDecision }
262
+ });
263
+ }
264
+
265
+ async function waitReviewOrCi(input: LifecycleInput): Promise<LifecycleStepResult> {
266
+ const link = input.storage.getPrLink(input.run.id);
267
+ if (!link) {
268
+ throw new AgentLoopError("storage_error", "No PR link exists for WAIT_REVIEW_OR_CI.");
269
+ }
270
+ const deadline = Date.now() + input.config.reviewCiMaxWaitMs;
271
+ while (Date.now() <= deadline) {
272
+ const ghOptions = githubOptions(input);
273
+ const pr = await viewPullRequest(ghOptions, link.prNumber);
274
+ const reviewComments = parseReviewThreads(
275
+ await fetchReviewThreads(ghOptions, link.prNumber)
276
+ );
277
+ input.storage.replaceReviewComments(input.run.id, link.prNumber, reviewComments);
278
+ if (actionableReviewComments(reviewComments).length > 0) {
279
+ return { nextState: "FIX_REVIEW", message: "Review comments need handling." };
280
+ }
281
+ const ci = evaluateCiChecks(input.config, Array.isArray(pr.statusCheckRollup) ? pr.statusCheckRollup : []);
282
+ input.storage.replaceCiChecks(input.run.id, link.prNumber, ci.checks);
283
+ if (ci.state === "missing" && ci.gate) {
284
+ throw new AgentLoopError(ci.gate, "Required CI checks are missing or unspecified.", {
285
+ details: { missingRequiredChecks: ci.missingRequiredChecks },
286
+ exitCode: 2
287
+ });
288
+ }
289
+ if (ci.state === "failed") {
290
+ return { nextState: "FIX_REVIEW", message: "CI failed; later PRs will repair." };
291
+ }
292
+ if (ci.state === "green" && approvalSatisfied(input.config, pr.reviewDecision)) {
293
+ recordReviewApproval(input, pr.reviewDecision);
294
+ return { nextState: "READY_TO_MERGE", message: "Review and CI are ready." };
295
+ }
296
+ if (Date.now() + input.config.reviewCiPollIntervalMs > deadline) {
297
+ break;
298
+ }
299
+ await sleep(input.config.reviewCiPollIntervalMs, input.signal);
300
+ }
301
+ throw new AgentLoopError("ci_pending_timeout", "Timed out waiting for review or CI.", { exitCode: 2 });
302
+ }
303
+
304
+ async function maybeMerge(input: LifecycleInput): Promise<LifecycleStepResult> {
305
+ const link = input.storage.getPrLink(input.run.id);
306
+ if (!link) {
307
+ throw new AgentLoopError("storage_error", "No PR link exists for merge.");
308
+ }
309
+ if (input.config.mergeMode !== "conditional") {
310
+ throw new AgentLoopError("merge_requires_confirmation", "Auto-merge is disabled.", {
311
+ details: { prNumber: link.prNumber },
312
+ exitCode: 2
313
+ });
314
+ }
315
+ const pr = await viewPullRequest(githubOptions(input), link.prNumber);
316
+ if (pr.state === "MERGED") {
317
+ input.storage.appendDecision({
318
+ runId: input.run.id,
319
+ kind: "merge_reused",
320
+ message: `PR #${link.prNumber} was already merged.`,
321
+ details: { prNumber: link.prNumber }
322
+ });
323
+ return { nextState: "SYNC_MAIN", message: `PR #${link.prNumber} already merged.` };
324
+ }
325
+ const ci = evaluateCiChecks(input.config, Array.isArray(pr.statusCheckRollup) ? pr.statusCheckRollup : []);
326
+ if (ci.state !== "green" || !approvalSatisfied(input.config, pr.reviewDecision)) {
327
+ throw new AgentLoopError("merge_requires_confirmation", "Merge guards are not satisfied.", {
328
+ details: { ciState: ci.state, reviewDecision: pr.reviewDecision },
329
+ exitCode: 2
330
+ });
331
+ }
332
+ recordReviewApproval(input, pr.reviewDecision);
333
+ assertConditionalMergeReadiness(input, {
334
+ ci: ci.checks.map((check) => ({
335
+ id: `${link.prNumber}-${check.name}`,
336
+ runId: input.run.id,
337
+ prNumber: link.prNumber,
338
+ name: check.name,
339
+ status: check.status,
340
+ ...(check.conclusion ? { conclusion: check.conclusion } : {}),
341
+ observedAt: new Date().toISOString()
342
+ })),
343
+ decisions: input.storage.listDecisions(input.run.id)
344
+ });
345
+ mergePullRequest(input.repoRoot, link.prNumber);
346
+ input.storage.appendDecision({
347
+ runId: input.run.id,
348
+ kind: "pr_merged",
349
+ message: `Merged PR #${link.prNumber}.`,
350
+ details: { prNumber: link.prNumber }
351
+ });
352
+ return { nextState: "SYNC_MAIN", message: `Merged PR #${link.prNumber}.` };
353
+ }
354
+
355
+ function branchName(input: LifecycleInput): string {
356
+ const workItem = getDeliveryWorkItem(input.storage, input.run.id);
357
+ const selection = resolvePrSelection(input.repoRoot, input.config, {
358
+ githubRequired: true,
359
+ ...(workItem ? { workItem } : {})
360
+ });
361
+ if (selection.ambiguous) {
362
+ throw new AgentLoopError("ambiguous_next_pr", "Could not uniquely identify the next PR plan.", {
363
+ details: {
364
+ plansDir: input.config.plansDir,
365
+ reason: selection.reason,
366
+ candidates: selection.candidates,
367
+ evidence: selection.evidence
368
+ },
369
+ exitCode: 2
370
+ });
371
+ }
372
+ return selection.branchName;
373
+ }
374
+
375
+ function parseConfiguredCommand(command: string, cwd: string) {
376
+ const [file, ...args] = tokenizeCommand(command);
377
+ if (!file) {
378
+ throw new AgentLoopError("invalid_config", "Configured command is empty.");
379
+ }
380
+ return {
381
+ id: `configured-${file}`,
382
+ file,
383
+ args,
384
+ cwd,
385
+ purpose: "Run configured self-check."
386
+ };
387
+ }
388
+
389
+ function approvalSatisfied(config: AgentLoopConfig, reviewDecision: string | undefined): boolean {
390
+ return !config.requireReviewApproval || reviewDecision === "APPROVED";
391
+ }
392
+
393
+ function githubOptions(input: LifecycleInput): GitHubCommandOptions {
394
+ return input.signal
395
+ ? { repoRoot: input.repoRoot, config: input.config, signal: input.signal }
396
+ : { repoRoot: input.repoRoot, config: input.config };
397
+ }
398
+
399
+ function branchDiffersFromBase(repoRoot: string, baseBranch: string): boolean {
400
+ return getChangedFiles(repoRoot, `${baseBranch}...HEAD`).filter((file) => !isRuntimePath(file)).length > 0;
401
+ }
402
+
403
+ function isRuntimePath(path: string): boolean {
404
+ return path === ".agent-loop" || path.startsWith(".agent-loop/");
405
+ }
406
+
407
+
408
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
409
+ return new Promise((resolve, reject) => {
410
+ const cleanup = (): void => signal?.removeEventListener("abort", abort);
411
+ const timer = setTimeout(() => {
412
+ cleanup();
413
+ resolve();
414
+ }, ms);
415
+ const abort = (): void => {
416
+ clearTimeout(timer);
417
+ cleanup();
418
+ reject(new AgentLoopError("ci_pending_timeout", "Timed out waiting for review or CI was aborted.", {
419
+ exitCode: 2
420
+ }));
421
+ };
422
+ signal?.addEventListener("abort", abort, { once: true });
423
+ });
424
+ }
425
+
426
+ function tokenizeCommand(command: string): string[] {
427
+ const tokens: string[] = [];
428
+ let current = "";
429
+ let quote: "\"" | "'" | undefined;
430
+ for (let index = 0; index < command.length; index += 1) {
431
+ const char = command[index] ?? "";
432
+ if (quote) {
433
+ if (char === quote) {
434
+ quote = undefined;
435
+ } else {
436
+ current += char;
437
+ }
438
+ continue;
439
+ }
440
+ if (char === "\"" || char === "'") {
441
+ quote = char;
442
+ continue;
443
+ }
444
+ if (/\s/.test(char)) {
445
+ if (current.length > 0) {
446
+ tokens.push(current);
447
+ current = "";
448
+ }
449
+ continue;
450
+ }
451
+ current += char;
452
+ }
453
+ if (quote) {
454
+ throw new AgentLoopError("invalid_config", "Configured command contains an unterminated quote.");
455
+ }
456
+ if (current.length > 0) {
457
+ tokens.push(current);
458
+ }
459
+ return tokens;
460
+ }
461
+
462
+ function isDefined<T>(value: T | undefined): value is T {
463
+ return value !== undefined;
464
+ }