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,217 @@
1
+ import { AgentLoopError } from "./errors.js";
2
+ import { redactSecrets } from "./redaction.js";
3
+ import type { AgentLoopEvent, AgentLoopRun, AgentLoopStorage } from "./types.js";
4
+
5
+ export const DELIVERY_WORK_ITEM_BOUND_KIND = "delivery_work_item_bound";
6
+ export const WORKFLOW_STAGE_EVIDENCE_KIND = "workflow_stage_evidence";
7
+
8
+ export interface DeliveryWorkItem {
9
+ issue: number;
10
+ title: string;
11
+ url: string;
12
+ branch?: string;
13
+ source: "cli" | "dashboard" | "state_machine";
14
+ }
15
+
16
+ export interface BindDeliveryWorkItemInput {
17
+ issue?: string;
18
+ title?: string;
19
+ url?: string;
20
+ branch?: string;
21
+ runId?: string;
22
+ source?: "cli" | "dashboard" | "state_machine";
23
+ }
24
+
25
+ export interface BindDeliveryWorkItemResult {
26
+ run: AgentLoopRun;
27
+ workItem: DeliveryWorkItem;
28
+ reused: boolean;
29
+ bound: boolean;
30
+ event?: AgentLoopEvent;
31
+ }
32
+
33
+ export function bindDeliveryWorkItem(
34
+ storage: AgentLoopStorage,
35
+ input: BindDeliveryWorkItemInput
36
+ ): BindDeliveryWorkItemResult {
37
+ const workItem = normalizeWorkItemInput(input);
38
+ const run = selectBindableRun(storage, workItem, input.runId);
39
+ const existing = getDeliveryWorkItem(storage, run.id);
40
+ if (existing && sameIssue(existing, workItem)) {
41
+ return { run, workItem: existing, reused: true, bound: false };
42
+ }
43
+
44
+ const event = storage.appendEvent({
45
+ runId: run.id,
46
+ kind: DELIVERY_WORK_ITEM_BOUND_KIND,
47
+ message: `Bound delivery work item #${workItem.issue}: ${redactSecrets(workItem.title)}`,
48
+ payload: workItem
49
+ });
50
+ storage.appendDecision({
51
+ runId: run.id,
52
+ kind: DELIVERY_WORK_ITEM_BOUND_KIND,
53
+ message: `Bound delivery work item #${workItem.issue}.`,
54
+ details: workItem
55
+ });
56
+ appendWorkItemStageEvidence(storage, run.id, workItem);
57
+ return { run, workItem, reused: false, bound: true, event };
58
+ }
59
+
60
+ export function getDeliveryWorkItem(storage: AgentLoopStorage, runId: string | undefined): DeliveryWorkItem | undefined {
61
+ if (!runId) return undefined;
62
+ const event = latestEventLookup(storage)?.findLatestEvent(runId, DELIVERY_WORK_ITEM_BOUND_KIND)
63
+ ?? storage
64
+ .listEvents(100_000)
65
+ .find((item) => item.runId === runId && item.kind === DELIVERY_WORK_ITEM_BOUND_KIND);
66
+ return parseDeliveryWorkItem(event?.payload);
67
+ }
68
+
69
+ export function selectDefaultDeliveryRun(storage: AgentLoopStorage): AgentLoopRun | undefined {
70
+ return storage
71
+ .listRuns(200)
72
+ .find((run) => isLiveRun(run) && getDeliveryWorkItem(storage, run.id) !== undefined);
73
+ }
74
+
75
+ export function defaultIssueBranch(issue: number, title: string, prefix: string): string {
76
+ const slug = `${title}`
77
+ .toLowerCase()
78
+ .replace(/[^a-z0-9]+/g, "-")
79
+ .replace(/^-+|-+$/g, "")
80
+ .slice(0, 48);
81
+ return `${prefix}issue-${issue}${slug ? `-${slug}` : ""}`;
82
+ }
83
+
84
+ function selectBindableRun(
85
+ storage: AgentLoopStorage,
86
+ workItem: DeliveryWorkItem,
87
+ runId: string | undefined
88
+ ): AgentLoopRun {
89
+ if (runId) {
90
+ const run = storage.listRuns(200).find((item) => item.id === runId);
91
+ if (!run) {
92
+ throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
93
+ }
94
+ assertCanBindRun(storage, run, workItem);
95
+ return run;
96
+ }
97
+
98
+ const liveRuns = storage.listRuns(200).filter(isLiveRun);
99
+ const sameIssueRun = liveRuns.find((run) => {
100
+ const existing = getDeliveryWorkItem(storage, run.id);
101
+ return existing && sameIssue(existing, workItem);
102
+ });
103
+ if (sameIssueRun) return sameIssueRun;
104
+
105
+ const unboundLiveRun = liveRuns.find((run) => run.status === "RUNNING" && getDeliveryWorkItem(storage, run.id) === undefined);
106
+ if (unboundLiveRun) return unboundLiveRun;
107
+
108
+ const different = liveRuns.find((run) => {
109
+ const existing = getDeliveryWorkItem(storage, run.id);
110
+ return existing !== undefined && !sameIssue(existing, workItem);
111
+ });
112
+ if (different) {
113
+ const existing = getDeliveryWorkItem(storage, different.id);
114
+ throw new AgentLoopError("policy_violation", "Another delivery work item is already bound to an active run.", {
115
+ details: { runId: different.id, existingIssue: existing?.issue, requestedIssue: workItem.issue },
116
+ exitCode: 2
117
+ });
118
+ }
119
+
120
+ return storage.createRun("RUNNING", {
121
+ currentState: "SELECT_NEXT_PR"
122
+ });
123
+ }
124
+
125
+ function assertCanBindRun(storage: AgentLoopStorage, run: AgentLoopRun, workItem: DeliveryWorkItem): void {
126
+ if (!isLiveRun(run)) {
127
+ throw new AgentLoopError("policy_violation", "Delivery work item can only be bound to a running or blocked run.", {
128
+ details: { runId: run.id, status: run.status },
129
+ exitCode: 2
130
+ });
131
+ }
132
+ const existing = getDeliveryWorkItem(storage, run.id);
133
+ if (existing && !sameIssue(existing, workItem)) {
134
+ throw new AgentLoopError("policy_violation", "The requested run is already bound to a different delivery work item.", {
135
+ details: { runId: run.id, existingIssue: existing.issue, requestedIssue: workItem.issue },
136
+ exitCode: 2
137
+ });
138
+ }
139
+ }
140
+
141
+ function normalizeWorkItemInput(input: BindDeliveryWorkItemInput): DeliveryWorkItem {
142
+ const issue = Number(input.issue);
143
+ if (!Number.isInteger(issue) || issue < 1) {
144
+ throw new AgentLoopError("invalid_config", "delivery bind requires --issue with a positive integer.");
145
+ }
146
+ const title = typeof input.title === "string" ? input.title.trim() : "";
147
+ if (!title) {
148
+ throw new AgentLoopError("invalid_config", "delivery bind requires --title.");
149
+ }
150
+ const url = typeof input.url === "string" ? input.url.trim() : "";
151
+ if (!/^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/i.test(url)) {
152
+ throw new AgentLoopError("invalid_config", "delivery bind requires --url pointing to a GitHub issue.");
153
+ }
154
+ const branch = typeof input.branch === "string" && input.branch.trim().length > 0 ? input.branch.trim() : undefined;
155
+ return {
156
+ issue,
157
+ title: redactSecrets(title),
158
+ url,
159
+ ...(branch ? { branch } : {}),
160
+ source: input.source ?? "cli"
161
+ };
162
+ }
163
+
164
+ function appendWorkItemStageEvidence(storage: AgentLoopStorage, runId: string, workItem: DeliveryWorkItem): void {
165
+ storage.appendEvent({
166
+ runId,
167
+ kind: WORKFLOW_STAGE_EVIDENCE_KIND,
168
+ message: `Selected issue #${workItem.issue}: ${workItem.title}`,
169
+ payload: {
170
+ stageId: "work_item",
171
+ substageId: "issue_selected",
172
+ evidenceRefIds: [workItem.url],
173
+ artifactIds: [],
174
+ actor: "codex",
175
+ status: "done",
176
+ source: "delivery"
177
+ }
178
+ });
179
+ }
180
+
181
+ function sameIssue(left: DeliveryWorkItem, right: DeliveryWorkItem): boolean {
182
+ return left.issue === right.issue;
183
+ }
184
+
185
+ function isLiveRun(run: AgentLoopRun): boolean {
186
+ return run.status === "RUNNING" || run.status === "BLOCKED";
187
+ }
188
+
189
+ function latestEventLookup(storage: AgentLoopStorage): { findLatestEvent(runId: string, kind: string): AgentLoopEvent | undefined } | undefined {
190
+ const candidate = storage as AgentLoopStorage & { findLatestEvent?: unknown };
191
+ return typeof candidate.findLatestEvent === "function"
192
+ ? { findLatestEvent: candidate.findLatestEvent.bind(storage) as (runId: string, kind: string) => AgentLoopEvent | undefined }
193
+ : undefined;
194
+ }
195
+
196
+ function parseDeliveryWorkItem(payload: unknown): DeliveryWorkItem | undefined {
197
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
198
+ return undefined;
199
+ }
200
+ const record = payload as Record<string, unknown>;
201
+ if (
202
+ typeof record.issue !== "number" ||
203
+ !Number.isInteger(record.issue) ||
204
+ typeof record.title !== "string" ||
205
+ typeof record.url !== "string"
206
+ ) {
207
+ return undefined;
208
+ }
209
+ const source = record.source === "dashboard" || record.source === "state_machine" ? record.source : "cli";
210
+ return {
211
+ issue: record.issue,
212
+ title: record.title,
213
+ url: record.url,
214
+ ...(typeof record.branch === "string" ? { branch: record.branch } : {}),
215
+ source
216
+ };
217
+ }
@@ -0,0 +1,335 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { redactRemote, runCommand } from "./command.js";
5
+ import { loadConfig, statePath } from "./config.js";
6
+ import { AgentLoopError } from "./errors.js";
7
+ import { inspectHookCapture } from "./hook-capture.js";
8
+ import { agentLoopRouterHookCommand, collectHookCommands, isLegacyAgentLoopHookCommand } from "./hook-installation.js";
9
+ import { CODEX_HOOK_EVENTS, hookScriptName } from "./hook-events.js";
10
+ import { hookRegistryPath, inspectHookRegistryLock, listHookBindings } from "./hook-router.js";
11
+ import { defaultPackageRoot, hookDistRoot, hookSourceRoot } from "./plugin-paths.js";
12
+ import { SqliteAgentLoopStorage } from "./storage.js";
13
+ import type { AgentTimelineIntegrityReport, DoctorCheck, DoctorReport } from "./types.js";
14
+
15
+ /** Run PR A environment diagnostics for the current repository. */
16
+ export function runDoctor(repoRoot: string): DoctorReport {
17
+ const checks: DoctorCheck[] = [];
18
+ const gitRoot = runCommand("git", ["rev-parse", "--show-toplevel"], repoRoot);
19
+ pushCheck(checks, "git repo", gitRoot.ok, "Current directory is inside a git repository.");
20
+
21
+ const remote = runCommand("git", ["remote", "get-url", "origin"], repoRoot);
22
+ const githubRemote = remote.ok && remote.stdout.includes("github.com");
23
+ pushCheck(checks, "github remote", githubRemote, "origin remote points to GitHub.", {
24
+ remote: remote.ok ? redactRemote(remote.stdout) : redactRemote(remote.stderr)
25
+ });
26
+
27
+ const ghAuth = runCommand("gh", ["auth", "status", "-h", "github.com"], repoRoot);
28
+ const ghAuthed = ghAuth.ok;
29
+ pushCheck(checks, "gh auth", ghAuthed, "GitHub CLI is authenticated.");
30
+ if (ghAuth.ok) {
31
+ const hasRepo = ghAuth.combined.includes("'repo'") || ghAuth.combined.includes(" repo");
32
+ const hasWorkflow =
33
+ ghAuth.combined.includes("'workflow'") || ghAuth.combined.includes(" workflow");
34
+ checks.push({
35
+ name: "gh token scopes",
36
+ status: hasRepo ? (hasWorkflow ? "pass" : "warn") : "fail",
37
+ message: hasRepo
38
+ ? hasWorkflow
39
+ ? "gh token has repo and workflow scopes."
40
+ : "gh token has repo scope; workflow scope is recommended."
41
+ : "gh token must include repo scope."
42
+ });
43
+ }
44
+
45
+ const codex = runCommand("codex", ["--version"], repoRoot);
46
+ pushCheck(checks, "codex cli", codex.ok, "codex CLI is available.", {
47
+ version: codex.ok ? codex.stdout : codex.stderr
48
+ });
49
+
50
+ const packageRoot = defaultPackageRoot();
51
+ checks.push(checkHooksBuilt(packageRoot, repoRoot));
52
+ checks.push(checkHooksInstalled(repoRoot, packageRoot));
53
+
54
+ const nodeMajorMinor = process.versions.node.split(".").slice(0, 2).map(Number);
55
+ const nodeOk =
56
+ (nodeMajorMinor[0] ?? 0) > 22 ||
57
+ ((nodeMajorMinor[0] ?? 0) === 22 && (nodeMajorMinor[1] ?? 0) >= 5);
58
+ pushCheck(checks, "node version", nodeOk, "Node version supports node:sqlite.", {
59
+ version: process.version
60
+ });
61
+
62
+ let missingConfig = false;
63
+ try {
64
+ const { config } = loadConfig(repoRoot);
65
+ checks.push({
66
+ name: "config schema",
67
+ status: "pass",
68
+ message: ".agent-loop/config.json is valid."
69
+ });
70
+ checks.push({
71
+ name: "plansDir",
72
+ status: existsSync(join(repoRoot, config.plansDir)) ? "pass" : "fail",
73
+ message: `plansDir exists: ${config.plansDir}`
74
+ });
75
+
76
+ const storage = new SqliteAgentLoopStorage(statePath(repoRoot));
77
+ let timelineIntegrity: AgentTimelineIntegrityReport;
78
+ try {
79
+ storage.writeRepoConfig(config);
80
+ try {
81
+ timelineIntegrity = storage.checkTimelineIntegrity();
82
+ } catch (error) {
83
+ timelineIntegrity = failedTimelineIntegrity(error);
84
+ }
85
+ } finally {
86
+ storage.close();
87
+ }
88
+ checks.push({
89
+ name: "storage",
90
+ status: "pass",
91
+ message: "Storage can be opened and written."
92
+ });
93
+ checks.push({
94
+ name: "timeline integrity",
95
+ status: timelineIntegrity.ok ? "pass" : "fail",
96
+ message: timelineIntegrity.ok
97
+ ? "Timeline index and triggers are installed."
98
+ : "Timeline index or triggers are missing.",
99
+ details: timelineIntegrity
100
+ });
101
+
102
+ const gitnexus = runCommand("npx", ["gitnexus", "--version"], repoRoot);
103
+ checks.push({
104
+ name: "gitnexus",
105
+ status: gitnexus.ok ? "pass" : config.gitnexusRequired ? "fail" : "warn",
106
+ message: gitnexus.ok
107
+ ? "GitNexus is available."
108
+ : "GitNexus is not available from npx gitnexus --version."
109
+ });
110
+ } catch (error) {
111
+ if (error instanceof AgentLoopError && error.code === "needs_repo_init") {
112
+ missingConfig = true;
113
+ checks.push({
114
+ name: "config schema",
115
+ status: "fail",
116
+ message: "Missing .agent-loop/config.json.",
117
+ details: { gate: "needs_repo_init" }
118
+ });
119
+ } else {
120
+ checks.push({
121
+ name: "config schema",
122
+ status: "fail",
123
+ message: error instanceof Error ? error.message : String(error)
124
+ });
125
+ }
126
+ }
127
+
128
+ const status = checks.some((check) => check.status === "fail")
129
+ ? "fail"
130
+ : checks.some((check) => check.status === "warn")
131
+ ? "warn"
132
+ : "pass";
133
+ return {
134
+ status,
135
+ checks,
136
+ ...(missingConfig ? { gate: "needs_repo_init" } : {})
137
+ };
138
+ }
139
+
140
+ function checkHooksBuilt(packageRoot: string, targetRepoRoot: string): DoctorCheck {
141
+ const sourceDir = hookSourceRoot(packageRoot);
142
+ const distDir = hookDistRoot(packageRoot);
143
+ const installCommand = installHooksCommand(targetRepoRoot);
144
+ const missing = CODEX_HOOK_EVENTS
145
+ .map((event) => hookScriptName(event))
146
+ .filter((script) => !existsSync(join(distDir, script)));
147
+ if (missing.length > 0) {
148
+ return {
149
+ name: "codex hooks build",
150
+ status: "warn",
151
+ message: `Compiled hook runners are missing. Run \`pnpm build:hooks\` from ${packageRoot}, then \`${installCommand}\`.`,
152
+ details: { packageRoot, distDir, missing, installCommand }
153
+ };
154
+ }
155
+ if (!existsSync(join(packageRoot, "pnpm-lock.yaml"))) {
156
+ return {
157
+ name: "codex hooks build",
158
+ status: "pass",
159
+ message: "Packaged hook runners are available.",
160
+ details: { packageRoot, distDir }
161
+ };
162
+ }
163
+ const stale = CODEX_HOOK_EVENTS
164
+ .map((event) => ({
165
+ source: join(sourceDir, hookScriptName(event).replace(/\.js$/, ".ts")),
166
+ dist: join(distDir, hookScriptName(event))
167
+ }))
168
+ .filter((entry) => existsSync(entry.source) && statSync(entry.dist).mtimeMs < statSync(entry.source).mtimeMs);
169
+ return {
170
+ name: "codex hooks build",
171
+ status: stale.length === 0 ? "pass" : "warn",
172
+ message: stale.length === 0
173
+ ? "Compiled hook runners are available."
174
+ : `Compiled hook runners are older than sources. Run \`pnpm build:hooks\` from ${packageRoot}, then \`${installCommand}\`.`,
175
+ ...(stale.length > 0 ? { details: { packageRoot, stale, installCommand } } : {})
176
+ };
177
+ }
178
+
179
+ function checkHooksInstalled(repoRoot: string, pluginRoot: string): DoctorCheck {
180
+ const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
181
+ const hooksPath = join(codexHome, "hooks.json");
182
+ const installCommand = installHooksCommand(repoRoot);
183
+ if (!existsSync(hooksPath)) {
184
+ return {
185
+ name: "codex hooks",
186
+ status: "warn",
187
+ message: `Codex hooks are not installed. Run \`${installCommand}\`.`,
188
+ details: { hooksPath, targetRepoRoot: repoRoot, pluginRoot, installCommand }
189
+ };
190
+ }
191
+ const text = readFileSync(hooksPath, "utf8");
192
+ let parsedHooks: unknown;
193
+ try {
194
+ parsedHooks = JSON.parse(text) as unknown;
195
+ } catch (error) {
196
+ return {
197
+ name: "codex hooks",
198
+ status: "warn",
199
+ message: `Codex hooks config is not valid JSON. Fix ${hooksPath}, then run \`${installCommand}\`.`,
200
+ details: { hooksPath, error: error instanceof Error ? error.message : String(error), installCommand }
201
+ };
202
+ }
203
+ const commands = collectHookCommands(parsedHooks);
204
+ const missing = CODEX_HOOK_EVENTS.filter((event) => !commands.includes(agentLoopRouterHookCommand(event, pluginRoot)));
205
+ const legacyCommands = commands.filter(isLegacyAgentLoopHookCommand);
206
+ const expectedDist = hookDistRoot(pluginRoot);
207
+ const routerCommands = commands.filter((command) => command.includes("autonomous-pr-loop/hooks/dist/"));
208
+ const unexpectedRouterCommands = routerCommands.filter((command) => !command.includes(expectedDist));
209
+ let bindings: ReturnType<typeof listHookBindings>;
210
+ let registryError: string | undefined;
211
+ try {
212
+ bindings = listHookBindings(codexHome);
213
+ } catch (error) {
214
+ bindings = [];
215
+ registryError = error instanceof Error ? error.message : String(error);
216
+ }
217
+ const activeBindings = bindings.filter((binding) => binding.status === "active");
218
+ const currentRepoBindings = activeBindings.filter((binding) => binding.repoRoot === repoRoot);
219
+ const lock = inspectHookRegistryLock(codexHome);
220
+ const capture = inspectHookCapture(repoRoot, codexHome);
221
+ const installed = missing.length === 0;
222
+ const routerDistDrift = unexpectedRouterCommands.length > 0;
223
+ const captureWarn = capture.status === "ambiguous" || capture.status === "unavailable";
224
+ const status = !installed ? "warn" : routerDistDrift || registryError || lock.stale || legacyCommands.length > 0 || currentRepoBindings.length === 0 || captureWarn ? "warn" : "pass";
225
+ const message = hookInstallMessage({
226
+ installed,
227
+ routerDistDrift,
228
+ registryError,
229
+ lockStale: lock.stale,
230
+ lockPath: lock.path,
231
+ legacyCommands,
232
+ currentRepoBindings,
233
+ installCommand,
234
+ codexHome
235
+ });
236
+ return {
237
+ name: "codex hooks",
238
+ status,
239
+ message,
240
+ details: {
241
+ hooksPath,
242
+ registryPath: hookRegistryPath(codexHome),
243
+ targetRepoRoot: repoRoot,
244
+ pluginRoot,
245
+ expectedDist,
246
+ missing,
247
+ legacyCommands,
248
+ routerCommandsPointToExpectedDist: routerCommands.length > 0 && unexpectedRouterCommands.length === 0,
249
+ unexpectedRouterCommands,
250
+ activeBindings: activeBindings.length,
251
+ currentRepoBindings: currentRepoBindings.length,
252
+ lock,
253
+ hookCapture: capture,
254
+ ...(registryError ? { registryError } : {}),
255
+ installCommand
256
+ }
257
+ };
258
+ }
259
+
260
+ function hookInstallMessage(input: {
261
+ installed: boolean;
262
+ routerDistDrift: boolean;
263
+ registryError: string | undefined;
264
+ lockStale: boolean;
265
+ lockPath: string;
266
+ legacyCommands: string[];
267
+ currentRepoBindings: unknown[];
268
+ installCommand: string;
269
+ codexHome: string;
270
+ }): string {
271
+ if (!input.installed && input.routerDistDrift) {
272
+ return `Codex hook router is not installed at the expected hook dist; existing router commands point outside the expected hook dist. Run \`${input.installCommand}\` to refresh router hooks and bind this repo.`;
273
+ }
274
+ if (!input.installed) {
275
+ return `Codex hook router is not installed. Run \`${input.installCommand}\` to install router hooks and bind this repo.`;
276
+ }
277
+ if (input.routerDistDrift) {
278
+ return `Codex hook router includes commands outside the expected hook dist. Run \`${input.installCommand}\` to refresh router hooks.`;
279
+ }
280
+ if (input.registryError) {
281
+ return `Codex hook binding registry is not valid. Fix ${hookRegistryPath(input.codexHome)}, then run \`${input.installCommand}\`.`;
282
+ }
283
+ if (input.lockStale) {
284
+ return `Codex hook binding registry lock appears stale. Remove ${input.lockPath} or rerun after the stale writer exits.`;
285
+ }
286
+ if (input.legacyCommands.length > 0) {
287
+ return `Codex hook router is installed, but legacy per-repo agent-loop hooks remain. Run \`${input.installCommand}\` to migrate them.`;
288
+ }
289
+ if (input.currentRepoBindings.length === 0) {
290
+ return `Codex hook router is installed, but this repo is not bound. Run \`${input.installCommand}\`.`;
291
+ }
292
+ return "HOLO-Codex hook router is installed and this repo has an active binding.";
293
+ }
294
+
295
+ function installHooksCommand(repoRoot: string): string {
296
+ return `agent-loop install-hooks --repo ${shellQuote(repoRoot)}`;
297
+ }
298
+
299
+ function shellQuote(value: string): string {
300
+ return `'${value.replaceAll("'", "'\\''")}'`;
301
+ }
302
+
303
+ function pushCheck(
304
+ checks: DoctorCheck[],
305
+ name: string,
306
+ pass: boolean,
307
+ message: string,
308
+ details?: unknown
309
+ ): void {
310
+ checks.push({
311
+ name,
312
+ status: pass ? "pass" : "fail",
313
+ message: pass ? message : `${message} Check failed.`,
314
+ ...(details ? { details } : {})
315
+ });
316
+ }
317
+
318
+ function failedTimelineIntegrity(error: unknown): AgentTimelineIntegrityReport {
319
+ return {
320
+ ok: false,
321
+ missingTable: false,
322
+ missingTriggers: [],
323
+ missingSourceRows: [],
324
+ sourceCounts: {
325
+ event: 0,
326
+ worker_event: 0,
327
+ worker: 0,
328
+ state: 0,
329
+ gate: 0,
330
+ artifact: 0,
331
+ decision: 0
332
+ },
333
+ repair: `Timeline integrity check failed: ${error instanceof Error ? error.message : String(error)}`
334
+ };
335
+ }
@@ -0,0 +1,82 @@
1
+ import type { AgentLoopGateKind } from "./types.js";
2
+
3
+ export type AgentLoopErrorCode =
4
+ | AgentLoopGateKind
5
+ | "invalid_config"
6
+ | "not_git_repo"
7
+ | "config_exists"
8
+ | "storage_schema_mismatch"
9
+ | "version_conflict"
10
+ | "storage_error"
11
+ | "artifact_integrity_error"
12
+ | "command_rejected"
13
+ | "command_timeout"
14
+ | "unknown_command";
15
+
16
+ /** Structured error used by CLI and core APIs to preserve stable machine-readable codes. */
17
+ export class AgentLoopError extends Error {
18
+ readonly code: AgentLoopErrorCode;
19
+ readonly details?: unknown;
20
+ readonly exitCode: 0 | 1 | 2;
21
+
22
+ constructor(
23
+ code: AgentLoopErrorCode,
24
+ message: string,
25
+ options: { details?: unknown; exitCode?: 0 | 1 | 2 } = {}
26
+ ) {
27
+ super(message);
28
+ this.name = "AgentLoopError";
29
+ this.code = code;
30
+ this.details = options.details;
31
+ this.exitCode = options.exitCode ?? (isGateCode(code) ? 2 : 1);
32
+ }
33
+ }
34
+
35
+ /** Return true when an error code represents a gate rather than an unexpected failure. */
36
+ export function isGateCode(code: AgentLoopErrorCode): code is AgentLoopGateKind {
37
+ return (
38
+ code === "needs_repo_init" ||
39
+ code === "unsupported_remote" ||
40
+ code === "needs_secret_or_login" ||
41
+ code === "policy_violation" ||
42
+ code === "ambiguous_next_pr" ||
43
+ code === "dirty_unowned_worktree" ||
44
+ code === "required_tool_unavailable" ||
45
+ code === "ci_required_checks_missing" ||
46
+ code === "ci_pending_timeout" ||
47
+ code === "merge_requires_confirmation" ||
48
+ code === "github_transient_failure" ||
49
+ code === "gitnexus_check_failed" ||
50
+ code === "github_resource_not_found" ||
51
+ code === "worker_failed" ||
52
+ code === "worker_output_invalid" ||
53
+ code === "review_out_of_scope" ||
54
+ code === "worker_timeout" ||
55
+ code === "worker_already_running" ||
56
+ code === "generic_goal_needs_confirmation" ||
57
+ code === "generic_human_gate" ||
58
+ code === "generic_scope_change_requested"
59
+ );
60
+ }
61
+
62
+ /** Convert unknown thrown values into stable JSON-safe CLI error payloads. */
63
+ export function toErrorPayload(error: unknown): {
64
+ code: string;
65
+ message: string;
66
+ details?: unknown;
67
+ } {
68
+ if (error instanceof AgentLoopError) {
69
+ const payload: { code: string; message: string; details?: unknown } = {
70
+ code: error.code,
71
+ message: error.message
72
+ };
73
+ if (error.details !== undefined) {
74
+ payload.details = error.details;
75
+ }
76
+ return payload;
77
+ }
78
+ if (error instanceof Error) {
79
+ return { code: "error", message: error.message };
80
+ }
81
+ return { code: "error", message: String(error) };
82
+ }