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,206 @@
1
+ import type {
2
+ AgentLoopCiCheck,
3
+ AgentLoopConfig,
4
+ AgentLoopDecision,
5
+ AgentLoopGate,
6
+ AgentLoopReviewComment,
7
+ AgentLoopRunCheck
8
+ } from "./types.js";
9
+
10
+ export interface AutonomyPosture {
11
+ autonomyMode: AgentLoopConfig["autonomyMode"];
12
+ mergeMode: AgentLoopConfig["mergeMode"];
13
+ notifyMode: AgentLoopConfig["notifyMode"];
14
+ reviewHandling: AgentLoopConfig["reviewHandling"];
15
+ summary: string;
16
+ notifyWhen: string[];
17
+ requiresConfirmation: string[];
18
+ allowConditionalMerge: boolean;
19
+ }
20
+
21
+ export interface MergeReadiness {
22
+ state: "manual" | "disabled" | "ready" | "missing_evidence" | "confirmation_required";
23
+ ready: boolean;
24
+ missingConditions: string[];
25
+ evidence: string[];
26
+ carryoverRecords: string[];
27
+ }
28
+
29
+ export interface MergeReadinessInput {
30
+ config: AgentLoopConfig;
31
+ ci: AgentLoopCiCheck[];
32
+ reviewComments: AgentLoopReviewComment[];
33
+ gates: AgentLoopGate[];
34
+ decisions: AgentLoopDecision[];
35
+ runChecks: AgentLoopRunCheck[];
36
+ }
37
+
38
+ /** Explain the repository's current Human On Loop autonomy posture. */
39
+ export function describeAutonomyPosture(config: AgentLoopConfig): AutonomyPosture {
40
+ return {
41
+ autonomyMode: config.autonomyMode,
42
+ mergeMode: config.mergeMode,
43
+ notifyMode: config.notifyMode,
44
+ reviewHandling: config.reviewHandling,
45
+ summary: postureSummary(config),
46
+ notifyWhen: notifyRules(config),
47
+ requiresConfirmation: confirmationRules(config),
48
+ allowConditionalMerge: config.mergeMode === "conditional"
49
+ };
50
+ }
51
+
52
+ /** Evaluate whether policy evidence is sufficient for conditional merge. */
53
+ export function evaluateMergeReadiness(input: MergeReadinessInput): MergeReadiness {
54
+ const missing: string[] = [];
55
+ const evidence: string[] = [];
56
+ const carryoverRecords = input.decisions
57
+ .filter((decision) => decision.kind.includes("carryover") || decision.kind.includes("follow_up"))
58
+ .map((decision) => decision.message);
59
+
60
+ if (input.config.mergeMode === "disabled") {
61
+ return baseReadiness("disabled", false, ["merge mode disabled"], evidence, carryoverRecords);
62
+ }
63
+ if (input.config.mergeMode === "manual") {
64
+ return baseReadiness("manual", false, ["manual merge mode"], evidence, carryoverRecords);
65
+ }
66
+
67
+ const requiredChecks = new Set(input.config.requiredChecks);
68
+ if (requiredChecks.size === 0) {
69
+ if (input.ci.length === 0) {
70
+ missing.push("CI checks observed or required checks configured");
71
+ } else {
72
+ for (const check of input.ci) {
73
+ if (!ciCheckGreen(check)) {
74
+ missing.push(`observed check green: ${check.name}`);
75
+ } else {
76
+ evidence.push(`observed check green: ${check.name}`);
77
+ }
78
+ }
79
+ }
80
+ } else {
81
+ for (const checkName of requiredChecks) {
82
+ const latest = input.ci.find((check) => check.name === checkName);
83
+ if (!latest || !ciCheckGreen(latest)) {
84
+ missing.push(`required check green: ${checkName}`);
85
+ } else {
86
+ evidence.push(`check green: ${checkName}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ const openActionable = input.reviewComments.filter((comment) =>
92
+ comment.actionable && !comment.isResolved && !comment.isOutdated && comment.status === "open"
93
+ );
94
+ if (openActionable.length > 0) {
95
+ missing.push("no open actionable review comments");
96
+ } else {
97
+ evidence.push("review comments clear");
98
+ }
99
+
100
+ if (input.config.requireReviewApproval) {
101
+ const approved = input.decisions.some((decision) =>
102
+ decision.kind.includes("review") && decision.kind.includes("approved")
103
+ );
104
+ if (!approved) {
105
+ missing.push("required review approval recorded");
106
+ } else {
107
+ evidence.push("review approval recorded");
108
+ }
109
+ }
110
+
111
+ if (input.gates.some((gate) => gate.status === "open")) {
112
+ missing.push("no open gates");
113
+ } else {
114
+ evidence.push("no open gates");
115
+ }
116
+
117
+ const gitnexusPassed = input.runChecks.some((check) =>
118
+ check.kind === "gitnexus_detect_changes" && check.status === "passed"
119
+ );
120
+ if (input.config.gitnexusRequired && !gitnexusPassed) {
121
+ missing.push("GitNexus detect_changes passed");
122
+ } else {
123
+ evidence.push(input.config.gitnexusRequired ? "GitNexus passed" : "GitNexus not required");
124
+ }
125
+
126
+ const scopePassed = input.runChecks.some((check) => check.kind === "self_check" && check.status === "passed");
127
+ if (!scopePassed) {
128
+ missing.push("self check passed");
129
+ } else {
130
+ evidence.push("self check passed");
131
+ }
132
+
133
+ const scopeGuardPassed = input.runChecks.some((check) => check.kind === "scope_guard" && check.status === "passed");
134
+ if (!scopeGuardPassed) {
135
+ missing.push("scope guard passed");
136
+ } else {
137
+ evidence.push("scope guard passed");
138
+ }
139
+
140
+ const protectedPathsPassed = input.runChecks.some((check) => check.kind === "protected_paths" && check.status === "passed");
141
+ if (!protectedPathsPassed) {
142
+ missing.push("protected paths clear");
143
+ } else {
144
+ evidence.push("protected paths clear");
145
+ }
146
+
147
+ const carryoverChecked = input.runChecks.some((check) =>
148
+ check.kind === "carryover_recorded" && (check.status === "passed" || check.status === "skipped")
149
+ );
150
+ if (input.config.reviewHandling === "fix_scoped_and_carry_forward") {
151
+ if (!carryoverChecked && carryoverRecords.length === 0) {
152
+ missing.push("carryover evaluated or recorded");
153
+ } else {
154
+ evidence.push(carryoverRecords.length > 0 ? "carryover recorded" : "carryover evaluated");
155
+ }
156
+ }
157
+
158
+ return baseReadiness(
159
+ missing.length === 0 ? "ready" : "missing_evidence",
160
+ missing.length === 0,
161
+ missing,
162
+ evidence,
163
+ carryoverRecords
164
+ );
165
+ }
166
+
167
+ function ciCheckGreen(check: AgentLoopCiCheck): boolean {
168
+ return check.conclusion?.toLowerCase() === "success" && check.status.toLowerCase() === "completed";
169
+ }
170
+
171
+ function baseReadiness(
172
+ state: MergeReadiness["state"],
173
+ ready: boolean,
174
+ missingConditions: string[],
175
+ evidence: string[],
176
+ carryoverRecords: string[]
177
+ ): MergeReadiness {
178
+ return { state, ready, missingConditions, evidence, carryoverRecords };
179
+ }
180
+
181
+ function postureSummary(config: AgentLoopConfig): string {
182
+ const autonomy = config.autonomyMode.replaceAll("_", " ");
183
+ const merge = config.mergeMode === "conditional" ? "conditional merge when evidence passes" : `${config.mergeMode} merge`;
184
+ return `Agent runs ${autonomy}; ${merge}; notifications are ${config.notifyMode.replaceAll("_", " ")}.`;
185
+ }
186
+
187
+ function notifyRules(config: AgentLoopConfig): string[] {
188
+ if (config.notifyMode === "blockers_only") {
189
+ return ["blocked", "confirmation_required"];
190
+ }
191
+ if (config.notifyMode === "all_gates") {
192
+ return ["all gates", "CI/review attention", "worker failures", "merge completion"];
193
+ }
194
+ return ["blocked", "confirmation_required", "high-risk policy changes", "external reviewer or CI failures"];
195
+ }
196
+
197
+ function confirmationRules(config: AgentLoopConfig): string[] {
198
+ const rules = ["dangerous policy changes", "protected path changes"];
199
+ if (config.mergeMode !== "conditional") {
200
+ rules.push("manual merge decision");
201
+ }
202
+ if (config.autonomyMode === "supervised") {
203
+ rules.push("run progression beyond one step");
204
+ }
205
+ return rules;
206
+ }
@@ -0,0 +1,131 @@
1
+ import type { AgentLoopCiCheck, AgentLoopConfig, AgentLoopGateKind } from "./types.js";
2
+ import { isRecord } from "./config.js";
3
+
4
+ export type CiState = "green" | "failed" | "pending" | "missing";
5
+
6
+ export interface CiEvaluation {
7
+ state: CiState;
8
+ gate?: AgentLoopGateKind;
9
+ checks: Array<Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt">>;
10
+ missingRequiredChecks: string[];
11
+ }
12
+
13
+ /** Normalize GitHub statusCheckRollup entries and evaluate required checks. */
14
+ export function evaluateCiChecks(config: AgentLoopConfig, rollup: unknown[]): CiEvaluation {
15
+ const checks = latestByName(rollup.map(normalizeCheck).filter((check) => check.name.length > 0));
16
+ if (config.requiredChecks.length === 0) {
17
+ return evaluateObservedChecks(checks);
18
+ }
19
+ const byName = new Map(checks.map((check) => [check.name, check]));
20
+ const missingRequiredChecks = config.requiredChecks.filter((name) => !byName.has(name));
21
+ if (missingRequiredChecks.length > 0) {
22
+ return {
23
+ state: "missing",
24
+ gate: "ci_required_checks_missing",
25
+ checks,
26
+ missingRequiredChecks
27
+ };
28
+ }
29
+ const required = config.requiredChecks.map((name) => byName.get(name)).filter(isDefined);
30
+ if (required.some((check) => isFailure(check.conclusion))) {
31
+ return { state: "failed", checks, missingRequiredChecks: [] };
32
+ }
33
+ if (required.some((check) => !isSuccess(check.conclusion) || check.status.toLowerCase() !== "completed")) {
34
+ return { state: "pending", checks, missingRequiredChecks: [] };
35
+ }
36
+ return { state: "green", checks, missingRequiredChecks: [] };
37
+ }
38
+
39
+ function evaluateObservedChecks(
40
+ checks: Array<Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt">>
41
+ ): CiEvaluation {
42
+ if (checks.length === 0) {
43
+ return {
44
+ state: "missing",
45
+ gate: "ci_required_checks_missing",
46
+ checks,
47
+ missingRequiredChecks: []
48
+ };
49
+ }
50
+ if (checks.some((check) => isFailure(check.conclusion))) {
51
+ return { state: "failed", checks, missingRequiredChecks: [] };
52
+ }
53
+ if (checks.some((check) => !isSuccess(check.conclusion) || check.status.toLowerCase() !== "completed")) {
54
+ return { state: "pending", checks, missingRequiredChecks: [] };
55
+ }
56
+ return { state: "green", checks, missingRequiredChecks: [] };
57
+ }
58
+
59
+ function normalizeCheck(value: unknown): Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt"> {
60
+ if (!isRecord(value)) {
61
+ return { name: "", status: "unknown" };
62
+ }
63
+ const name = stringValue(value.name) || stringValue(value.context) || stringValue(value.workflowName);
64
+ const state = stringValue(value.state);
65
+ return {
66
+ name,
67
+ status: stringValue(value.status) || statusFromState(state),
68
+ ...(stringValue(value.conclusion) || conclusionFromState(state)
69
+ ? { conclusion: stringValue(value.conclusion) || conclusionFromState(state) }
70
+ : {}),
71
+ ...(stringValue(value.url) || stringValue(value.detailsUrl)
72
+ ? { url: stringValue(value.url) || stringValue(value.detailsUrl) }
73
+ : {}),
74
+ ...(stringValue(value.startedAt) ? { startedAt: stringValue(value.startedAt) } : {}),
75
+ ...(stringValue(value.completedAt) ? { completedAt: stringValue(value.completedAt) } : {})
76
+ };
77
+ }
78
+
79
+ function statusFromState(state: string): string {
80
+ const normalized = state.toLowerCase();
81
+ if (["success", "failure", "failed", "error", "cancelled", "skipped"].includes(normalized)) {
82
+ return "COMPLETED";
83
+ }
84
+ if (["pending", "queued", "in_progress", "requested", "waiting"].includes(normalized)) {
85
+ return "IN_PROGRESS";
86
+ }
87
+ return "unknown";
88
+ }
89
+
90
+ function conclusionFromState(state: string): string {
91
+ const normalized = state.toLowerCase();
92
+ if (normalized === "success") return "SUCCESS";
93
+ if (normalized === "failure" || normalized === "failed" || normalized === "error") return "FAILURE";
94
+ if (normalized === "cancelled") return "CANCELLED";
95
+ if (normalized === "skipped") return "SKIPPED";
96
+ return "";
97
+ }
98
+
99
+ function latestByName(
100
+ checks: Array<Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt">>
101
+ ): Array<Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt">> {
102
+ const byName = new Map<string, Omit<AgentLoopCiCheck, "id" | "runId" | "prNumber" | "observedAt">>();
103
+ for (const check of checks) {
104
+ const previous = byName.get(check.name);
105
+ if (!previous || timestamp(check) >= timestamp(previous)) {
106
+ byName.set(check.name, check);
107
+ }
108
+ }
109
+ return [...byName.values()];
110
+ }
111
+
112
+ function timestamp(check: { completedAt?: string; startedAt?: string }): number {
113
+ return Date.parse(check.completedAt ?? check.startedAt ?? "") || 0;
114
+ }
115
+
116
+ function isSuccess(value: string | undefined): boolean {
117
+ return value?.toLowerCase() === "success";
118
+ }
119
+
120
+ function isFailure(value: string | undefined): boolean {
121
+ const normalized = value?.toLowerCase();
122
+ return normalized === "failure" || normalized === "failed" || normalized === "timed_out";
123
+ }
124
+
125
+ function stringValue(value: unknown): string {
126
+ return typeof value === "string" ? value : "";
127
+ }
128
+
129
+ function isDefined<T>(value: T | undefined): value is T {
130
+ return value !== undefined;
131
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ DEFAULT_LOCALE,
3
+ normalizeLocaleSetting,
4
+ processLocaleCandidates,
5
+ resolveEffectiveLocale,
6
+ type EffectiveLocale,
7
+ type LocaleSetting
8
+ } from "./locale.js";
9
+
10
+ type CliTextKey =
11
+ | "approvedGate"
12
+ | "baseBranch"
13
+ | "config"
14
+ | "currentBranch"
15
+ | "dashboardHelp"
16
+ | "dashboardStarted"
17
+ | "doctor"
18
+ | "gate"
19
+ | "hooks"
20
+ | "hooksInstalled"
21
+ | "initDone"
22
+ | "initDryRun"
23
+ | "kind"
24
+ | "note"
25
+ | "plansDir"
26
+ | "recovered"
27
+ | "repoId"
28
+ | "runId"
29
+ | "state"
30
+ | "status"
31
+ | "storage"
32
+ | "url";
33
+
34
+ const CLI_TEXT: Record<EffectiveLocale, Record<CliTextKey, string>> = {
35
+ "en-US": {
36
+ approvedGate: "gate approved",
37
+ baseBranch: "baseBranch",
38
+ config: "config",
39
+ currentBranch: "currentBranch",
40
+ dashboardHelp: "Starts the local HOLO-Codex dashboard.",
41
+ dashboardStarted: "dashboard started",
42
+ doctor: "doctor",
43
+ gate: "gate",
44
+ hooks: "hooks",
45
+ hooksInstalled: "hooks installed",
46
+ initDone: "init: .agent-loop initialized",
47
+ initDryRun: "init dry-run: no files written",
48
+ kind: "kind",
49
+ note: "note",
50
+ plansDir: "plansDir",
51
+ recovered: "recovered",
52
+ repoId: "repoId",
53
+ runId: "runId",
54
+ state: "state",
55
+ status: "status",
56
+ storage: "storage",
57
+ url: "url"
58
+ },
59
+ "zh-CN": {
60
+ approvedGate: "gate 已批准",
61
+ baseBranch: "基础分支",
62
+ config: "配置",
63
+ currentBranch: "当前分支",
64
+ dashboardHelp: "启动本地 HOLO-Codex dashboard。",
65
+ dashboardStarted: "dashboard 已启动",
66
+ doctor: "诊断",
67
+ gate: "gate",
68
+ hooks: "hooks",
69
+ hooksInstalled: "hooks 已安装",
70
+ initDone: "init: .agent-loop 已初始化",
71
+ initDryRun: "init dry-run: 未写入文件",
72
+ kind: "类型",
73
+ note: "备注",
74
+ plansDir: "计划目录",
75
+ recovered: "已恢复",
76
+ repoId: "repoId",
77
+ runId: "runId",
78
+ state: "状态",
79
+ status: "状态",
80
+ storage: "存储",
81
+ url: "url"
82
+ }
83
+ };
84
+
85
+ /** Resolve CLI locale from an optional override and repository config setting. */
86
+ export function resolveCliLocale(
87
+ override: LocaleSetting | undefined,
88
+ configLocale: LocaleSetting | undefined
89
+ ): EffectiveLocale {
90
+ return resolveEffectiveLocale(override ?? configLocale ?? DEFAULT_LOCALE, processLocaleCandidates());
91
+ }
92
+
93
+ /** Parse and validate a `--locale` option from raw CLI args. */
94
+ export function parseLocaleOverride(args: string[]): LocaleSetting | undefined {
95
+ const value = optionValue(args, "--locale");
96
+ if (value === undefined) {
97
+ return undefined;
98
+ }
99
+ return normalizeLocaleSetting(value);
100
+ }
101
+
102
+ /** Remove locale option tokens before command-specific parsing. */
103
+ export function stripLocaleArgs(args: string[]): string[] {
104
+ const next: string[] = [];
105
+ for (let index = 0; index < args.length; index += 1) {
106
+ if (args[index] === "--locale") {
107
+ index += 1;
108
+ continue;
109
+ }
110
+ next.push(args[index]!);
111
+ }
112
+ return next;
113
+ }
114
+
115
+ /** Return display text for CLI human output. */
116
+ export function cliText(locale: EffectiveLocale, key: CliTextKey): string {
117
+ return CLI_TEXT[locale][key];
118
+ }
119
+
120
+ function optionValue(args: string[], name: string): string | undefined {
121
+ const index = args.indexOf(name);
122
+ return index >= 0 ? args[index + 1] : undefined;
123
+ }