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,2462 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // plugins/autonomous-pr-loop/hooks/observe-runner.ts
4
+ import { readFileSync as readFileSync2 } from "node:fs";
5
+
6
+ // plugins/autonomous-pr-loop/core/hook-observer.ts
7
+ import { createHash as createHash2 } from "node:crypto";
8
+
9
+ // plugins/autonomous-pr-loop/core/config.ts
10
+ import { join } from "node:path";
11
+
12
+ // plugins/autonomous-pr-loop/core/errors.ts
13
+ var AgentLoopError = class extends Error {
14
+ code;
15
+ details;
16
+ exitCode;
17
+ constructor(code, message, options = {}) {
18
+ super(message);
19
+ this.name = "AgentLoopError";
20
+ this.code = code;
21
+ this.details = options.details;
22
+ this.exitCode = options.exitCode ?? (isGateCode(code) ? 2 : 1);
23
+ }
24
+ };
25
+ function isGateCode(code) {
26
+ return code === "needs_repo_init" || code === "unsupported_remote" || code === "needs_secret_or_login" || code === "policy_violation" || code === "ambiguous_next_pr" || code === "dirty_unowned_worktree" || code === "required_tool_unavailable" || code === "ci_required_checks_missing" || code === "ci_pending_timeout" || code === "merge_requires_confirmation" || code === "github_transient_failure" || code === "gitnexus_check_failed" || code === "github_resource_not_found" || code === "worker_failed" || code === "worker_output_invalid" || code === "review_out_of_scope" || code === "worker_timeout" || code === "worker_already_running" || code === "generic_goal_needs_confirmation" || code === "generic_human_gate" || code === "generic_scope_change_requested";
27
+ }
28
+
29
+ // plugins/autonomous-pr-loop/core/locale.ts
30
+ var DEFAULT_LOCALE = "zh-CN";
31
+
32
+ // plugins/autonomous-pr-loop/core/profiles.ts
33
+ var DEFAULT_LOOP_SHAPE_ID = "pr-loop";
34
+ var DEFAULT_WORKFLOW_PROFILE_ID = "default_pr_loop";
35
+ var DEFAULT_ROLE_PROFILE_ID = "default_pr_roles";
36
+
37
+ // plugins/autonomous-pr-loop/core/config.ts
38
+ var CONFIG_DIR = ".agent-loop";
39
+ var DEFAULT_PROTECTED_PATHS = [
40
+ ".git/**",
41
+ ".agent-loop/**",
42
+ ".claude/**",
43
+ "AGENTS.md",
44
+ "CLAUDE.md",
45
+ ".env*",
46
+ "**/*secret*"
47
+ ];
48
+ function statePath(repoRoot) {
49
+ return join(repoRoot, CONFIG_DIR, "state.sqlite");
50
+ }
51
+ function withConfigDefaults(input) {
52
+ const mergeMode = input.mergeMode ?? (input.allowAutoMerge ? "conditional" : "manual");
53
+ return {
54
+ repoId: input.repoId,
55
+ locale: input.locale ?? DEFAULT_LOCALE,
56
+ loopShape: input.loopShape ?? DEFAULT_LOOP_SHAPE_ID,
57
+ workflowProfile: input.workflowProfile ?? DEFAULT_WORKFLOW_PROFILE_ID,
58
+ roleProfile: input.roleProfile ?? DEFAULT_ROLE_PROFILE_ID,
59
+ baseBranch: input.baseBranch ?? "main",
60
+ branchPrefix: input.branchPrefix ?? "codex/",
61
+ plansDir: input.plansDir ?? "docs/plans",
62
+ ...input.lintCommand ? { lintCommand: input.lintCommand } : {},
63
+ ...input.testCommand ? { testCommand: input.testCommand } : {},
64
+ ...input.gitnexusRepo ? { gitnexusRepo: input.gitnexusRepo } : {},
65
+ gitnexusRequired: input.gitnexusRequired ?? true,
66
+ requiredChecks: input.requiredChecks ?? [],
67
+ requireReviewApproval: input.requireReviewApproval ?? true,
68
+ autonomyMode: input.autonomyMode ?? "autonomous_until_gate",
69
+ mergeMode,
70
+ notifyMode: input.notifyMode ?? "important_only",
71
+ reviewHandling: input.reviewHandling ?? "fix_scoped_and_carry_forward",
72
+ ...input.carryoverTarget ? { carryoverTarget: input.carryoverTarget } : {},
73
+ allowAutoMerge: mergeMode === "conditional",
74
+ maxReviewFixRounds: input.maxReviewFixRounds ?? 3,
75
+ maxTestFixRounds: input.maxTestFixRounds ?? 2,
76
+ maxCiReruns: input.maxCiReruns ?? 1,
77
+ commandTimeoutMs: input.commandTimeoutMs ?? 6e5,
78
+ commandOutputLimitBytes: input.commandOutputLimitBytes ?? 65536,
79
+ githubRetryMaxAttempts: input.githubRetryMaxAttempts ?? 3,
80
+ githubRetryBaseDelayMs: input.githubRetryBaseDelayMs ?? 1e3,
81
+ reviewCiPollIntervalMs: input.reviewCiPollIntervalMs ?? 3e4,
82
+ reviewCiMaxWaitMs: input.reviewCiMaxWaitMs ?? 18e5,
83
+ workerBackend: input.workerBackend ?? "codex-exec",
84
+ workerTimeoutMs: input.workerTimeoutMs ?? 18e5,
85
+ workerMaxRetries: input.workerMaxRetries ?? 1,
86
+ workerEphemeral: input.workerEphemeral ?? false,
87
+ protectedPaths: input.protectedPaths ?? DEFAULT_PROTECTED_PATHS,
88
+ ...input.dashboard ? { dashboard: input.dashboard } : {}
89
+ };
90
+ }
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+
95
+ // plugins/autonomous-pr-loop/core/hook-events.ts
96
+ var CODEX_HOOK_EVENTS = [
97
+ "PreToolUse",
98
+ "PostToolUse",
99
+ "UserPromptSubmit",
100
+ "Stop",
101
+ "SessionStart",
102
+ "PreCompact",
103
+ "PostCompact",
104
+ "PermissionRequest"
105
+ ];
106
+ var OBSERVE_ONLY_HOOK_EVENTS = CODEX_HOOK_EVENTS.filter((event) => event !== "PreToolUse");
107
+ function hookEventKind(event) {
108
+ return `hook_${event.replaceAll(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()}`;
109
+ }
110
+
111
+ // plugins/autonomous-pr-loop/core/hook-router.ts
112
+ import { createHash, randomUUID } from "node:crypto";
113
+ import { execFileSync } from "node:child_process";
114
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
115
+ import { homedir } from "node:os";
116
+ import { dirname, isAbsolute, join as join2, resolve } from "node:path";
117
+ function hookRegistryPath(codexHome = codexHomePath()) {
118
+ return join2(codexHome, "agent-loop", "hook-bindings.json");
119
+ }
120
+ function hookRegistryLockPath(codexHome = codexHomePath()) {
121
+ return `${hookRegistryPath(codexHome)}.lock`;
122
+ }
123
+ function codexHomePath() {
124
+ return process.env.CODEX_HOME ?? join2(homedir(), ".codex");
125
+ }
126
+ function resolveHookRoute(payload, options = {}) {
127
+ const context = hookContextFromPayload(payload, options.legacyRepoRoot);
128
+ let registry;
129
+ try {
130
+ registry = readRegistry(options.codexHome ?? codexHomePath());
131
+ } catch (error) {
132
+ return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
133
+ }
134
+ try {
135
+ const active = registry.bindings.filter((binding) => binding.status === "active");
136
+ const worktreeMatches = active.filter((binding) => bindingMatchesContext(binding, context));
137
+ const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
138
+ const sessionMatches = context.sessionId ? worktreeMatches.filter((binding) => binding.sessionIdHash === contextSessionHash) : [];
139
+ const candidates = sessionMatches.length > 0 ? sessionMatches : worktreeMatches.filter((binding) => binding.sessionIdHash === void 0);
140
+ if (candidates.length === 1) {
141
+ const binding = touchBinding(candidates[0], context, options.codexHome);
142
+ if (contextSessionHash && binding.sessionIdHash !== void 0 && binding.sessionIdHash !== contextSessionHash) {
143
+ return { status: "no_match", context, reason: "Hook binding was claimed by another Codex session.", worktreeBinding: true };
144
+ }
145
+ return { status: "matched", binding, context, legacy: false };
146
+ }
147
+ if (candidates.length > 1) {
148
+ return { status: "ambiguous", context, bindings: candidates, reason: "Multiple hook bindings match this Codex session context." };
149
+ }
150
+ if (worktreeMatches.length > 0) {
151
+ return { status: "no_match", context, reason: "Active hook bindings exist for this worktree, but none match this Codex session.", worktreeBinding: true };
152
+ }
153
+ const legacy = legacyRoute(options.legacyRepoRoot, context);
154
+ if (legacy) {
155
+ return { status: "matched", binding: legacy, context, legacy: true };
156
+ }
157
+ return { status: "no_match", context, reason: "No active agent-loop hook binding matches this Codex session context." };
158
+ } catch (error) {
159
+ return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
160
+ }
161
+ }
162
+ function hookContextFromPayload(payload, fallbackCwd = process.cwd()) {
163
+ const record = isRecord(payload) ? payload : {};
164
+ return resolveHookContext({
165
+ cwd: stringValue(record.cwd) ?? fallbackCwd,
166
+ sessionId: stringValue(record.session_id) ?? stringValue(record.sessionId),
167
+ turnId: stringValue(record.turn_id) ?? stringValue(record.turnId),
168
+ transcriptPath: stringValue(record.transcript_path) ?? stringValue(record.transcriptPath)
169
+ });
170
+ }
171
+ function resolveHookContext(input) {
172
+ const cwd = canonicalPath(input.cwd);
173
+ const worktreeRoot = gitOutput(["rev-parse", "--show-toplevel"], cwd);
174
+ const commonDir = gitOutput(["rev-parse", "--git-common-dir"], cwd);
175
+ const branch = gitOutput(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
176
+ const commonPath = commonDir ? canonicalPath(isAbsolute(commonDir) ? commonDir : join2(cwd, commonDir)) : void 0;
177
+ return {
178
+ cwd,
179
+ worktreeRoot: worktreeRoot ? canonicalPath(worktreeRoot) : cwd,
180
+ ...commonPath ? { gitCommonDir: commonPath } : {},
181
+ ...branch && branch !== "HEAD" ? { branch } : {},
182
+ ...input.sessionId ? { sessionId: input.sessionId } : {},
183
+ ...input.turnId ? { turnId: input.turnId } : {},
184
+ ...input.transcriptPath ? { transcriptPathSha256: sha256(input.transcriptPath) } : {}
185
+ };
186
+ }
187
+ function readRegistry(codexHome) {
188
+ const path = hookRegistryPath(codexHome);
189
+ if (!existsSync(path)) {
190
+ return { version: 1, bindings: [] };
191
+ }
192
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
193
+ if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.bindings)) {
194
+ throw new Error(`Invalid hook binding registry: expected { version: 1, bindings: [...] } in ${path}`);
195
+ }
196
+ const bindings = parsed.bindings.map(parseBinding);
197
+ const invalid = bindings.findIndex((binding) => binding === void 0);
198
+ if (invalid >= 0) {
199
+ throw new Error(`Invalid hook binding registry: invalid binding at index ${invalid} in ${path}`);
200
+ }
201
+ return {
202
+ version: 1,
203
+ bindings: bindings.filter((binding) => binding !== void 0)
204
+ };
205
+ }
206
+ function writeRegistry(registry, codexHome) {
207
+ const path = hookRegistryPath(codexHome);
208
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
209
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
210
+ writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
211
+ `, { mode: 384 });
212
+ renameSync(tmp, path);
213
+ }
214
+ function parseBinding(value) {
215
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.repoRoot !== "string" || typeof value.worktreeRoot !== "string") {
216
+ return void 0;
217
+ }
218
+ const status = value.status === "stale" || value.status === "disabled" ? value.status : "active";
219
+ if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
220
+ return void 0;
221
+ }
222
+ return {
223
+ id: value.id,
224
+ repoRoot: value.repoRoot,
225
+ worktreeRoot: value.worktreeRoot,
226
+ ...typeof value.gitCommonDir === "string" ? { gitCommonDir: value.gitCommonDir } : {},
227
+ ...typeof value.branch === "string" ? { branch: value.branch } : {},
228
+ ...typeof value.runId === "string" ? { runId: value.runId } : {},
229
+ ...typeof value.sessionIdHash === "string" ? { sessionIdHash: value.sessionIdHash } : typeof value.sessionId === "string" ? { sessionIdHash: sha256(value.sessionId) } : {},
230
+ ...typeof value.transcriptPathSha256 === "string" ? { transcriptPathSha256: value.transcriptPathSha256 } : {},
231
+ status,
232
+ createdAt: value.createdAt,
233
+ updatedAt: value.updatedAt,
234
+ ...typeof value.lastSeenAt === "string" ? { lastSeenAt: value.lastSeenAt } : {}
235
+ };
236
+ }
237
+ function touchBinding(binding, context, codexHome = codexHomePath()) {
238
+ return withRegistryLock(codexHome, () => {
239
+ const registry = readRegistry(codexHome);
240
+ const current = registry.bindings.find((item) => item.id === binding.id) ?? binding;
241
+ const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
242
+ if (current.sessionIdHash !== void 0 && contextSessionHash !== void 0 && current.sessionIdHash !== contextSessionHash) {
243
+ return current;
244
+ }
245
+ const nowMs = Date.now();
246
+ const shouldClaimSession = current.sessionIdHash === void 0 && contextSessionHash !== void 0;
247
+ const shouldClaimTranscript = current.transcriptPathSha256 === void 0 && context.transcriptPathSha256 !== void 0;
248
+ const lastSeenAtMs = current.lastSeenAt ? Date.parse(current.lastSeenAt) : 0;
249
+ const shouldRefreshLastSeen = !Number.isFinite(lastSeenAtMs) || nowMs - lastSeenAtMs > TOUCH_REFRESH_MS;
250
+ if (!shouldClaimSession && !shouldClaimTranscript && !shouldRefreshLastSeen) {
251
+ return current;
252
+ }
253
+ const now2 = new Date(nowMs).toISOString();
254
+ const updated = {
255
+ ...current,
256
+ ...shouldClaimSession ? { sessionIdHash: contextSessionHash } : {},
257
+ ...shouldClaimTranscript ? { transcriptPathSha256: context.transcriptPathSha256 } : {},
258
+ lastSeenAt: now2,
259
+ updatedAt: now2
260
+ };
261
+ registry.bindings = registry.bindings.map((item) => item.id === current.id ? updated : item);
262
+ writeRegistry(registry, codexHome);
263
+ return updated;
264
+ });
265
+ }
266
+ function legacyRoute(legacyRepoRoot, context) {
267
+ if (!legacyRepoRoot) return void 0;
268
+ const legacyContext = resolveHookContext({ cwd: legacyRepoRoot });
269
+ if (legacyContext.worktreeRoot !== context.worktreeRoot) {
270
+ return void 0;
271
+ }
272
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
273
+ return {
274
+ id: `legacy:${sha256(legacyContext.worktreeRoot).slice(0, 16)}`,
275
+ repoRoot: canonicalPath(legacyRepoRoot),
276
+ worktreeRoot: legacyContext.worktreeRoot,
277
+ ...legacyContext.gitCommonDir ? { gitCommonDir: legacyContext.gitCommonDir } : {},
278
+ ...legacyContext.branch ? { branch: legacyContext.branch } : {},
279
+ status: "active",
280
+ createdAt: now2,
281
+ updatedAt: now2
282
+ };
283
+ }
284
+ function bindingMatchesContext(binding, context) {
285
+ if (binding.worktreeRoot === context.worktreeRoot) {
286
+ return true;
287
+ }
288
+ return binding.gitCommonDir !== void 0 && context.gitCommonDir !== void 0 && binding.gitCommonDir === context.gitCommonDir && context.cwd.startsWith(`${binding.worktreeRoot}/`);
289
+ }
290
+ function canonicalPath(path) {
291
+ const resolved = resolve(path);
292
+ return existsSync(resolved) ? realpathSync(resolved) : resolved;
293
+ }
294
+ function gitOutput(args, cwd) {
295
+ try {
296
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || void 0;
297
+ } catch {
298
+ return void 0;
299
+ }
300
+ }
301
+ function sha256(value) {
302
+ return createHash("sha256").update(value).digest("hex");
303
+ }
304
+ function withRegistryLock(codexHome, fn) {
305
+ const path = hookRegistryPath(codexHome);
306
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
307
+ const lockPath = hookRegistryLockPath(codexHome);
308
+ let fd;
309
+ for (let attempt = 0; attempt < 100; attempt += 1) {
310
+ try {
311
+ fd = openSync(lockPath, "wx", 384);
312
+ writeFileSync(fd, `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
313
+ `);
314
+ break;
315
+ } catch (error) {
316
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
317
+ if (recoverStaleLock(lockPath)) {
318
+ continue;
319
+ }
320
+ sleepSync(20);
321
+ continue;
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+ if (fd === void 0) {
327
+ throw new Error(`Timed out waiting for hook registry lock: ${lockPath}`);
328
+ }
329
+ try {
330
+ return fn();
331
+ } finally {
332
+ closeSync(fd);
333
+ rmSync(lockPath, { force: true });
334
+ }
335
+ }
336
+ var LOCK_STALE_MS = 3e4;
337
+ var TOUCH_REFRESH_MS = 1e4;
338
+ function recoverStaleLock(lockPath) {
339
+ const report = inspectLockPath(lockPath);
340
+ if (!report.stale) {
341
+ return false;
342
+ }
343
+ rmSync(lockPath, { force: true });
344
+ return true;
345
+ }
346
+ function inspectLockPath(path) {
347
+ if (!existsSync(path)) {
348
+ return { path, exists: false, stale: false };
349
+ }
350
+ const metadata = readLockMetadata(path);
351
+ const stat = statSync(path);
352
+ const ageMs = Date.now() - (metadata.createdAtMs ?? stat.mtimeMs);
353
+ const alive = metadata.pid ? processAlive(metadata.pid) : void 0;
354
+ return {
355
+ path,
356
+ exists: true,
357
+ stale: ageMs > LOCK_STALE_MS && alive !== true,
358
+ ageMs,
359
+ ...metadata.pid ? { pid: metadata.pid } : {},
360
+ ...alive === void 0 ? {} : { processAlive: alive }
361
+ };
362
+ }
363
+ function readLockMetadata(path) {
364
+ try {
365
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
366
+ if (!isRecord(parsed)) return {};
367
+ const pid = typeof parsed.pid === "number" ? parsed.pid : void 0;
368
+ const createdAtMs = typeof parsed.createdAt === "string" ? Date.parse(parsed.createdAt) : void 0;
369
+ return {
370
+ ...pid && Number.isInteger(pid) && pid > 0 ? { pid } : {},
371
+ ...createdAtMs && Number.isFinite(createdAtMs) ? { createdAtMs } : {}
372
+ };
373
+ } catch {
374
+ return {};
375
+ }
376
+ }
377
+ function processAlive(pid) {
378
+ try {
379
+ process.kill(pid, 0);
380
+ return true;
381
+ } catch {
382
+ return false;
383
+ }
384
+ }
385
+ function sleepSync(ms) {
386
+ try {
387
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
388
+ } catch {
389
+ const end = Date.now() + ms;
390
+ while (Date.now() < end) {
391
+ }
392
+ }
393
+ }
394
+ function stringValue(value) {
395
+ return typeof value === "string" && value.length > 0 ? value : void 0;
396
+ }
397
+
398
+ // plugins/autonomous-pr-loop/core/redaction.ts
399
+ function redactSecrets(value) {
400
+ return value.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\b[A-Za-z0-9._%+-]+:[^@\s]+@/g, "[redacted]@").replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[redacted]").replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]").replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)(["'])(?:(?!\2).)*\2/gi, "$1$2[redacted]$2").replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)[^\n\r,;}]+/gi, "$1[redacted]");
401
+ }
402
+ function isSecretKey(key) {
403
+ return /token|api_key|authorization|password|secret/i.test(key);
404
+ }
405
+
406
+ // plugins/autonomous-pr-loop/core/storage.ts
407
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
408
+ import { dirname as dirname2 } from "node:path";
409
+ import { randomUUID as randomUUID2 } from "node:crypto";
410
+ import { DatabaseSync } from "node:sqlite";
411
+ var STORAGE_SCHEMA_VERSION = 8;
412
+ var SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, 4, 5, 6, 7, STORAGE_SCHEMA_VERSION];
413
+ var TIMELINE_SOURCES = ["event", "worker_event", "worker", "state", "gate", "artifact", "decision"];
414
+ var TIMELINE_TRIGGER_NAMES = [
415
+ "timeline_events_insert",
416
+ "timeline_worker_events_insert",
417
+ "timeline_workers_insert",
418
+ "timeline_workers_status_update",
419
+ "timeline_states_insert",
420
+ "timeline_gates_insert",
421
+ "timeline_artifacts_insert",
422
+ "timeline_decisions_insert"
423
+ ];
424
+ var PR_C_TABLES_SQL = `
425
+ create table if not exists pr_links (
426
+ id text primary key,
427
+ run_id text not null,
428
+ branch text not null,
429
+ pr_number integer not null,
430
+ url text not null,
431
+ head_ref text not null,
432
+ base_ref text not null,
433
+ state text not null,
434
+ draft integer not null,
435
+ created_at text not null,
436
+ updated_at text not null,
437
+ unique(run_id, pr_number),
438
+ foreign key(run_id) references runs(id)
439
+ );
440
+
441
+ create table if not exists ci_checks (
442
+ id text primary key,
443
+ run_id text not null,
444
+ pr_number integer not null,
445
+ name text not null,
446
+ status text not null,
447
+ conclusion text,
448
+ url text,
449
+ started_at text,
450
+ completed_at text,
451
+ observed_at text not null,
452
+ foreign key(run_id) references runs(id)
453
+ );
454
+
455
+ create table if not exists review_comments (
456
+ id text primary key,
457
+ run_id text not null,
458
+ pr_number integer not null,
459
+ comment_id text not null,
460
+ url text not null,
461
+ author text not null,
462
+ body text not null,
463
+ path text not null,
464
+ line integer,
465
+ diff_hunk text not null,
466
+ is_resolved integer not null,
467
+ is_outdated integer not null,
468
+ actionable integer not null,
469
+ status text not null,
470
+ observed_at text not null,
471
+ unique(run_id, comment_id),
472
+ foreign key(run_id) references runs(id)
473
+ );
474
+
475
+ create table if not exists decisions (
476
+ id text primary key,
477
+ run_id text not null,
478
+ kind text not null,
479
+ message text not null,
480
+ details_json text,
481
+ created_at text not null,
482
+ foreign key(run_id) references runs(id)
483
+ );
484
+ `;
485
+ var PR_D_TABLES_SQL = `
486
+ create table if not exists workers (
487
+ id text primary key,
488
+ run_id text not null,
489
+ type text not null,
490
+ backend text not null,
491
+ status text not null,
492
+ thread_id text,
493
+ attempt integer not null,
494
+ resume_used integer not null,
495
+ started_at text not null,
496
+ completed_at text,
497
+ exit_code integer,
498
+ result_artifact_id text,
499
+ raw_jsonl_artifact_id text,
500
+ error text,
501
+ foreign key(run_id) references runs(id)
502
+ );
503
+
504
+ create table if not exists worker_events (
505
+ seq integer primary key autoincrement,
506
+ id text not null unique,
507
+ worker_id text not null,
508
+ run_id text not null,
509
+ event_type text not null,
510
+ item_type text,
511
+ item_id text,
512
+ item_status text,
513
+ thread_id text,
514
+ backend text,
515
+ summary_json text,
516
+ usage_json text,
517
+ artifact_ids_json text,
518
+ created_at text not null,
519
+ foreign key(worker_id) references workers(id),
520
+ foreign key(run_id) references runs(id)
521
+ );
522
+
523
+ create unique index if not exists workers_single_running
524
+ on workers(status)
525
+ where status = 'running';
526
+ `;
527
+ var PR_E_INDEXES_SQL = `
528
+ create unique index if not exists runs_single_running
529
+ on runs(status)
530
+ where status = 'RUNNING';
531
+ `;
532
+ var PR_E_TABLES_SQL = `
533
+ create table if not exists run_checks (
534
+ run_id text not null,
535
+ kind text not null,
536
+ status text not null,
537
+ details_json text,
538
+ created_at text not null,
539
+ primary key(run_id, kind),
540
+ foreign key(run_id) references runs(id)
541
+ );
542
+ `;
543
+ var TIMELINE_INDEX_SQL = `
544
+ create table if not exists timeline_index (
545
+ timeline_seq integer primary key autoincrement,
546
+ source text not null,
547
+ source_id text not null,
548
+ source_seq integer,
549
+ run_id text,
550
+ worker_id text,
551
+ created_at text not null,
552
+ unique(source, source_id)
553
+ );
554
+
555
+ create index if not exists timeline_index_created
556
+ on timeline_index(created_at desc, timeline_seq desc);
557
+ create index if not exists timeline_index_run
558
+ on timeline_index(run_id, timeline_seq desc);
559
+ create index if not exists timeline_index_worker
560
+ on timeline_index(worker_id, timeline_seq desc);
561
+ `;
562
+ var TIMELINE_TRIGGERS_SQL = `
563
+ create trigger if not exists timeline_events_insert
564
+ after insert on events
565
+ begin
566
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
567
+ values ('event', new.id, new.seq, new.run_id, null, new.created_at);
568
+ end;
569
+
570
+ create trigger if not exists timeline_worker_events_insert
571
+ after insert on worker_events
572
+ begin
573
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
574
+ values ('worker_event', new.id, new.seq, new.run_id, new.worker_id, new.created_at);
575
+ end;
576
+
577
+ create trigger if not exists timeline_workers_insert
578
+ after insert on workers
579
+ begin
580
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
581
+ values ('worker', new.id || ':' || new.status, null, new.run_id, new.id, new.started_at);
582
+ end;
583
+
584
+ create trigger if not exists timeline_workers_status_update
585
+ after update of status on workers
586
+ when old.status is not new.status
587
+ begin
588
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
589
+ values (
590
+ 'worker',
591
+ new.id || ':' || new.status,
592
+ null,
593
+ new.run_id,
594
+ new.id,
595
+ coalesce(new.completed_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
596
+ );
597
+ end;
598
+
599
+ create trigger if not exists timeline_states_insert
600
+ after insert on states
601
+ begin
602
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
603
+ values ('state', cast(new.id as text), new.id, new.run_id, null, new.created_at);
604
+ end;
605
+
606
+ create trigger if not exists timeline_gates_insert
607
+ after insert on gates
608
+ begin
609
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
610
+ values ('gate', new.id, null, new.run_id, null, new.created_at);
611
+ end;
612
+
613
+ create trigger if not exists timeline_artifacts_insert
614
+ after insert on artifacts
615
+ begin
616
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
617
+ values ('artifact', new.id, null, new.run_id, null, new.created_at);
618
+ end;
619
+
620
+ create trigger if not exists timeline_decisions_insert
621
+ after insert on decisions
622
+ begin
623
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
624
+ values ('decision', new.id, null, new.run_id, null, new.created_at);
625
+ end;
626
+ `;
627
+ var SCHEMA_SQL = `
628
+ create table if not exists runs (
629
+ id text primary key,
630
+ status text not null,
631
+ current_state text,
632
+ version integer not null default 0,
633
+ branch text,
634
+ worktree_clean integer,
635
+ started_at text,
636
+ stopped_at text,
637
+ created_at text not null,
638
+ updated_at text not null
639
+ );
640
+
641
+ create table if not exists states (
642
+ id integer primary key autoincrement,
643
+ run_id text,
644
+ status text not null,
645
+ state text,
646
+ version integer not null,
647
+ payload_json text,
648
+ created_at text not null,
649
+ foreign key(run_id) references runs(id)
650
+ );
651
+
652
+ create table if not exists events (
653
+ seq integer primary key autoincrement,
654
+ id text not null unique,
655
+ run_id text,
656
+ kind text not null,
657
+ message text not null,
658
+ state_before text,
659
+ state_after text,
660
+ payload_json text,
661
+ artifact_ids_json text,
662
+ created_at text not null,
663
+ foreign key(run_id) references runs(id)
664
+ );
665
+
666
+ create table if not exists gates (
667
+ id text primary key,
668
+ run_id text,
669
+ kind text not null,
670
+ status text not null,
671
+ message text not null,
672
+ details_json text,
673
+ created_at text not null,
674
+ resolved_at text,
675
+ decision_note text,
676
+ decided_at text,
677
+ foreign key(run_id) references runs(id)
678
+ );
679
+
680
+ create table if not exists artifacts (
681
+ id text primary key,
682
+ run_id text,
683
+ kind text not null,
684
+ name text,
685
+ path text not null,
686
+ sha256 text,
687
+ metadata_json text,
688
+ created_at text not null,
689
+ foreign key(run_id) references runs(id)
690
+ );
691
+
692
+ create table if not exists repo_config (
693
+ id integer primary key check (id = 1),
694
+ schema_version integer not null,
695
+ config_json text not null,
696
+ updated_at text not null
697
+ );
698
+
699
+ ${PR_C_TABLES_SQL}
700
+ ${PR_D_TABLES_SQL}
701
+ ${PR_E_TABLES_SQL}
702
+ ${PR_E_INDEXES_SQL}
703
+ `;
704
+ var SqliteAgentLoopStorage = class {
705
+ constructor(path, options = {}) {
706
+ this.path = path;
707
+ this.mode = options.mode ?? "rw";
708
+ if (this.mode === "rw") {
709
+ mkdirSync2(dirname2(path), { recursive: true });
710
+ } else if (!existsSync2(path)) {
711
+ throw new AgentLoopError("storage_error", "Read-only storage file does not exist.", {
712
+ details: { path }
713
+ });
714
+ }
715
+ this.db = new DatabaseSync(path, {
716
+ readOnly: this.mode === "ro",
717
+ enableForeignKeyConstraints: true,
718
+ timeout: 5e3
719
+ });
720
+ try {
721
+ this.db.exec("PRAGMA foreign_keys=ON");
722
+ this.db.exec("PRAGMA busy_timeout=5000");
723
+ if (this.mode === "rw") {
724
+ this.db.exec("PRAGMA journal_mode=WAL");
725
+ }
726
+ this.ensureSchema();
727
+ if (this.mode === "rw") {
728
+ this.ensureRepoConfigVersion();
729
+ } else {
730
+ this.validateRepoConfigVersion();
731
+ }
732
+ const workersSql = `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
733
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
734
+ from workers`;
735
+ this.listWorkersByRunStatement = this.db.prepare(`${workersSql} where run_id = ? order by started_at desc limit ?`);
736
+ this.listWorkersStatement = this.db.prepare(`${workersSql} order by started_at desc limit ?`);
737
+ } catch (error) {
738
+ this.db.close();
739
+ throw toStorageError(error, "Failed to open agent-loop storage.");
740
+ }
741
+ }
742
+ path;
743
+ db;
744
+ mode;
745
+ listWorkersByRunStatement;
746
+ listWorkersStatement;
747
+ close() {
748
+ this.db.close();
749
+ }
750
+ writeRepoConfig(config) {
751
+ const snapshot = JSON.stringify({ schemaVersion: STORAGE_SCHEMA_VERSION, ...config });
752
+ this.transaction(() => {
753
+ this.db.prepare(
754
+ `insert into repo_config (id, schema_version, config_json, updated_at)
755
+ values (1, ?, ?, ?)
756
+ on conflict(id) do update set
757
+ schema_version = excluded.schema_version,
758
+ config_json = excluded.config_json,
759
+ updated_at = excluded.updated_at`
760
+ ).run(STORAGE_SCHEMA_VERSION, snapshot, now());
761
+ });
762
+ }
763
+ readRepoConfig() {
764
+ const row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
765
+ if (!row) {
766
+ return void 0;
767
+ }
768
+ if (!isSupportedSchemaVersion(row.schema_version)) {
769
+ throw new AgentLoopError(
770
+ "storage_schema_mismatch",
771
+ `Stored repo config schema version ${row.schema_version} is not supported.`,
772
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
773
+ );
774
+ }
775
+ const parsed = parseJson(row.config_json, "Stored repo config JSON is invalid.");
776
+ const { schemaVersion: _schemaVersion, ...config } = parsed;
777
+ return config;
778
+ }
779
+ createRun(status, options = {}) {
780
+ const createdAt = now();
781
+ const run = {
782
+ id: randomUUID2(),
783
+ status,
784
+ ...options.currentState ? { currentState: options.currentState } : {},
785
+ version: 0,
786
+ ...options.branch ? { branch: options.branch } : {},
787
+ ...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
788
+ createdAt,
789
+ updatedAt: createdAt,
790
+ startedAt: createdAt
791
+ };
792
+ try {
793
+ this.transaction(() => {
794
+ this.db.prepare(
795
+ `insert into runs (
796
+ id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
797
+ )
798
+ values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
799
+ ).run(
800
+ run.id,
801
+ run.status,
802
+ run.currentState ?? null,
803
+ run.version,
804
+ run.branch ?? null,
805
+ boolToDb(run.worktreeClean),
806
+ run.startedAt ?? null,
807
+ run.createdAt,
808
+ run.updatedAt
809
+ );
810
+ this.db.prepare(
811
+ `insert into states (run_id, status, state, version, payload_json, created_at)
812
+ values (?, ?, ?, ?, null, ?)`
813
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
814
+ });
815
+ } catch (error) {
816
+ if (isUniqueConstraintError(error)) {
817
+ throw new AgentLoopError("version_conflict", "Another active run already exists.", {
818
+ details: { status },
819
+ exitCode: 2
820
+ });
821
+ }
822
+ throw error;
823
+ }
824
+ return run;
825
+ }
826
+ getOrCreateActiveRun(options = {}) {
827
+ return this.transaction(() => {
828
+ const active = this.getActiveRun();
829
+ if (active) {
830
+ return { run: active, created: false };
831
+ }
832
+ const createdAt = now();
833
+ const run = {
834
+ id: randomUUID2(),
835
+ status: "RUNNING",
836
+ ...options.currentState ? { currentState: options.currentState } : {},
837
+ version: 0,
838
+ ...options.branch ? { branch: options.branch } : {},
839
+ ...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
840
+ createdAt,
841
+ updatedAt: createdAt,
842
+ startedAt: createdAt
843
+ };
844
+ this.db.prepare(
845
+ `insert into runs (
846
+ id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
847
+ )
848
+ values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
849
+ ).run(
850
+ run.id,
851
+ run.status,
852
+ run.currentState ?? null,
853
+ run.version,
854
+ run.branch ?? null,
855
+ boolToDb(run.worktreeClean),
856
+ run.startedAt ?? null,
857
+ run.createdAt,
858
+ run.updatedAt
859
+ );
860
+ this.db.prepare(
861
+ `insert into states (run_id, status, state, version, payload_json, created_at)
862
+ values (?, ?, ?, ?, null, ?)`
863
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
864
+ return { run, created: true };
865
+ });
866
+ }
867
+ recordRunCheck(check) {
868
+ const stored = { ...check, createdAt: now() };
869
+ this.transaction(() => {
870
+ this.db.prepare(
871
+ `insert into run_checks (run_id, kind, status, details_json, created_at)
872
+ values (?, ?, ?, ?, ?)
873
+ on conflict(run_id, kind) do update set
874
+ status = excluded.status,
875
+ details_json = excluded.details_json,
876
+ created_at = excluded.created_at`
877
+ ).run(
878
+ stored.runId,
879
+ stored.kind,
880
+ stored.status,
881
+ stored.details === void 0 ? null : JSON.stringify(stored.details),
882
+ stored.createdAt
883
+ );
884
+ });
885
+ return stored;
886
+ }
887
+ hasRunCheck(runId, kind) {
888
+ const row = this.db.prepare("select 1 from run_checks where run_id = ? and kind = ? and status in ('passed', 'skipped') limit 1").get(runId, kind);
889
+ return row !== void 0;
890
+ }
891
+ listRunChecks(runId) {
892
+ const rows = this.db.prepare("select run_id, kind, status, details_json, created_at from run_checks where run_id = ? order by created_at desc").all(runId);
893
+ return rows.map(fromRunCheckRow);
894
+ }
895
+ updateRunStatus(runId, expectedVersion, status, options = {}) {
896
+ const updatedAt = now();
897
+ return this.transaction(() => {
898
+ const result = this.db.prepare(
899
+ `update runs
900
+ set status = ?,
901
+ current_state = coalesce(?, current_state),
902
+ branch = coalesce(?, branch),
903
+ worktree_clean = coalesce(?, worktree_clean),
904
+ stopped_at = coalesce(?, stopped_at),
905
+ version = version + 1,
906
+ updated_at = ?
907
+ where id = ? and version = ?`
908
+ ).run(
909
+ status,
910
+ options.currentState ?? null,
911
+ options.branch ?? null,
912
+ boolToDb(options.worktreeClean),
913
+ options.stoppedAt ?? null,
914
+ updatedAt,
915
+ runId,
916
+ expectedVersion
917
+ );
918
+ if (result.changes !== 1) {
919
+ throw new AgentLoopError(
920
+ "version_conflict",
921
+ `Run ${runId} was updated by another writer.`,
922
+ { details: { runId, expectedVersion } }
923
+ );
924
+ }
925
+ const run = this.getRun(runId);
926
+ this.db.prepare(
927
+ `insert into states (run_id, status, state, version, payload_json, created_at)
928
+ values (?, ?, ?, ?, null, ?)`
929
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
930
+ return run;
931
+ });
932
+ }
933
+ appendEvent(event) {
934
+ const stored = {
935
+ id: randomUUID2(),
936
+ ...event,
937
+ createdAt: now()
938
+ };
939
+ let seq = 0;
940
+ this.transaction(() => {
941
+ this.db.prepare(
942
+ `insert into events (
943
+ id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
944
+ )
945
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?)`
946
+ ).run(
947
+ stored.id,
948
+ stored.runId ?? null,
949
+ stored.kind,
950
+ stored.message,
951
+ stored.stateBefore ?? null,
952
+ stored.stateAfter ?? null,
953
+ stored.payload === void 0 ? null : JSON.stringify(stored.payload),
954
+ stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
955
+ stored.createdAt
956
+ );
957
+ seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
958
+ });
959
+ return { seq, ...stored };
960
+ }
961
+ writeGate(gate) {
962
+ this.transaction(() => {
963
+ this.db.prepare(
964
+ `insert into gates (id, run_id, kind, status, message, details_json, created_at, resolved_at)
965
+ values (?, ?, ?, 'open', ?, ?, ?, null)`
966
+ ).run(
967
+ randomUUID2(),
968
+ gate.runId ?? null,
969
+ gate.kind,
970
+ gate.message,
971
+ gate.details === void 0 ? null : JSON.stringify(gate.details),
972
+ now()
973
+ );
974
+ });
975
+ }
976
+ resolveOpenGates(runId) {
977
+ this.transaction(() => {
978
+ this.db.prepare(
979
+ `update gates
980
+ set status = 'resolved', resolved_at = ?
981
+ where run_id = ? and status = 'open'`
982
+ ).run(now(), runId);
983
+ });
984
+ }
985
+ resolveOpenGatesByKind(kind, options = {}) {
986
+ const scope = options.scope ?? (options.runId ? "run" : "repo");
987
+ this.transaction(() => {
988
+ if (scope === "run") {
989
+ if (!options.runId) {
990
+ throw new AgentLoopError("storage_error", "runId is required for run-scoped gate recovery.");
991
+ }
992
+ this.db.prepare(
993
+ `update gates
994
+ set status = 'resolved', resolved_at = ?
995
+ where kind = ? and run_id = ? and status = 'open'`
996
+ ).run(now(), kind, options.runId);
997
+ return;
998
+ }
999
+ if (scope === "repo") {
1000
+ this.db.prepare(
1001
+ `update gates
1002
+ set status = 'resolved', resolved_at = ?
1003
+ where kind = ? and run_id is null and status = 'open'`
1004
+ ).run(now(), kind);
1005
+ return;
1006
+ }
1007
+ this.db.prepare(
1008
+ `update gates
1009
+ set status = 'resolved', resolved_at = ?
1010
+ where kind = ? and status = 'open'`
1011
+ ).run(now(), kind);
1012
+ });
1013
+ }
1014
+ listGates(runId) {
1015
+ const sql = `select id, run_id, kind, status, message, details_json, created_at,
1016
+ resolved_at, decision_note, decided_at
1017
+ from gates
1018
+ ${runId ? "where run_id = ?" : ""}
1019
+ order by created_at desc
1020
+ limit 100`;
1021
+ const rows = runId ? this.db.prepare(sql).all(runId) : this.db.prepare(sql).all();
1022
+ return rows.map(fromGateRow);
1023
+ }
1024
+ getGate(gateId) {
1025
+ const row = this.db.prepare(
1026
+ `select id, run_id, kind, status, message, details_json, created_at,
1027
+ resolved_at, decision_note, decided_at
1028
+ from gates
1029
+ where id = ?`
1030
+ ).get(gateId);
1031
+ return row ? fromGateRow(row) : void 0;
1032
+ }
1033
+ decideGate(gateId, decision, note) {
1034
+ if (note.trim().length === 0) {
1035
+ throw new AgentLoopError("invalid_config", "Gate decision note is required.");
1036
+ }
1037
+ const decidedAt = now();
1038
+ this.transaction(() => {
1039
+ const result = this.db.prepare(
1040
+ `update gates
1041
+ set status = ?, decision_note = ?, decided_at = ?, resolved_at = coalesce(resolved_at, ?)
1042
+ where id = ? and status = 'open'`
1043
+ ).run(decision, note, decidedAt, decidedAt, gateId);
1044
+ if (result.changes !== 1) {
1045
+ const gate2 = this.getGate(gateId);
1046
+ if (!gate2) {
1047
+ throw new AgentLoopError("storage_error", `Gate not found: ${gateId}`);
1048
+ }
1049
+ throw new AgentLoopError("storage_error", `Gate ${gateId} is not open.`, {
1050
+ details: { gateId, status: gate2.status }
1051
+ });
1052
+ }
1053
+ });
1054
+ const gate = this.getGate(gateId);
1055
+ if (!gate) {
1056
+ throw new AgentLoopError("storage_error", `Gate not found after decision: ${gateId}`);
1057
+ }
1058
+ return gate;
1059
+ }
1060
+ getCurrentStatus() {
1061
+ const repoGate = this.db.prepare(
1062
+ `select kind, message, details_json
1063
+ from gates
1064
+ where status = 'open' and run_id is null
1065
+ order by created_at desc
1066
+ limit 1`
1067
+ ).get();
1068
+ if (repoGate) {
1069
+ return {
1070
+ status: "BLOCKED",
1071
+ gate: statusGateFromRow(repoGate)
1072
+ };
1073
+ }
1074
+ const row = this.db.prepare(
1075
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1076
+ from runs
1077
+ order by updated_at desc, rowid desc
1078
+ limit 1`
1079
+ ).get();
1080
+ if (!row) {
1081
+ return { status: "IDLE" };
1082
+ }
1083
+ const run = fromRunRow(row);
1084
+ const runGate = this.db.prepare(
1085
+ `select kind, message, details_json
1086
+ from gates
1087
+ where status = 'open' and run_id = ?
1088
+ order by created_at desc
1089
+ limit 1`
1090
+ ).get(run.id);
1091
+ if (runGate) {
1092
+ return {
1093
+ status: "BLOCKED",
1094
+ run,
1095
+ gate: statusGateFromRow(runGate)
1096
+ };
1097
+ }
1098
+ if (run.status === "BLOCKED" && latestGateSatisfied(this.db, run.id)) {
1099
+ return { status: "READY", run: { ...run, status: "READY" } };
1100
+ }
1101
+ return { status: run.status, run };
1102
+ }
1103
+ listEvents(options = 50) {
1104
+ const limit = typeof options === "number" ? options : options.limit ?? 50;
1105
+ const sinceSeq = typeof options === "number" ? void 0 : options.sinceSeq;
1106
+ const rows = sinceSeq === void 0 ? this.db.prepare(
1107
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1108
+ from events
1109
+ order by seq desc
1110
+ limit ?`
1111
+ ).all(limit) : this.db.prepare(
1112
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1113
+ from events
1114
+ where seq > ?
1115
+ order by seq asc
1116
+ limit ?`
1117
+ ).all(sinceSeq, limit);
1118
+ return rows.map(fromEventRow);
1119
+ }
1120
+ findLatestEvent(runId, kind) {
1121
+ const row = this.db.prepare(
1122
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1123
+ from events
1124
+ where run_id = ? and kind = ?
1125
+ order by seq desc
1126
+ limit 1`
1127
+ ).get(runId, kind);
1128
+ return row ? fromEventRow(row) : void 0;
1129
+ }
1130
+ listAgentTimeline(query = {}) {
1131
+ const limit = clampLimit(query.limit ?? 50);
1132
+ const cursor = query.cursor ? decodeTimelineCursor(query.cursor) : void 0;
1133
+ const params = [];
1134
+ const where = [];
1135
+ if (cursor) {
1136
+ where.push("(created_at < ? or (created_at = ? and timeline_seq < ?))");
1137
+ params.push(cursor.occurredAt, cursor.occurredAt, cursor.timelineSeq);
1138
+ }
1139
+ if (query.sources?.length) {
1140
+ const sources = normalizeTimelineSources(query.sources);
1141
+ where.push(`source in (${sources.map(() => "?").join(", ")})`);
1142
+ params.push(...sources);
1143
+ }
1144
+ if (query.runId) {
1145
+ where.push("run_id = ?");
1146
+ params.push(query.runId);
1147
+ }
1148
+ if (query.workerId) {
1149
+ where.push("worker_id = ?");
1150
+ params.push(query.workerId);
1151
+ }
1152
+ params.push(limit + 1);
1153
+ const rows = this.db.prepare(
1154
+ `select timeline_seq, source, source_id, source_seq, run_id, worker_id, created_at
1155
+ from timeline_index
1156
+ ${where.length ? `where ${where.join(" and ")}` : ""}
1157
+ order by created_at desc, timeline_seq desc
1158
+ limit ?`
1159
+ ).all(...params);
1160
+ const pageRows = rows.slice(0, limit);
1161
+ const entries = pageRows.map((row) => this.timelineEntry(row)).filter((entry) => entry !== void 0);
1162
+ const last = pageRows[pageRows.length - 1];
1163
+ return {
1164
+ entries,
1165
+ ...rows.length > limit && last ? { nextCursor: encodeTimelineCursor(last.timeline_seq, last.created_at) } : {}
1166
+ };
1167
+ }
1168
+ checkTimelineIntegrity() {
1169
+ const missingTable = !hasTable(this.db, "timeline_index");
1170
+ const triggers = new Set(this.db.prepare("select name from sqlite_master where type = 'trigger' and name like 'timeline_%'").all().map((row) => row.name));
1171
+ const missingTriggers = TIMELINE_TRIGGER_NAMES.filter((name) => !triggers.has(name));
1172
+ const sourceCounts = Object.fromEntries(TIMELINE_SOURCES.map((source) => [source, 0]));
1173
+ const missingSourceRows = [];
1174
+ if (!missingTable) {
1175
+ const rows = this.db.prepare("select source, count(*) as count from timeline_index group by source").all();
1176
+ for (const row of rows) {
1177
+ if (TIMELINE_SOURCES.includes(row.source)) {
1178
+ sourceCounts[row.source] = row.count;
1179
+ }
1180
+ }
1181
+ missingSourceRows.push(...timelineMissingSourceRows(this.db));
1182
+ }
1183
+ const ok = !missingTable && missingTriggers.length === 0 && missingSourceRows.length === 0;
1184
+ return {
1185
+ ok,
1186
+ missingTable,
1187
+ missingTriggers,
1188
+ missingSourceRows,
1189
+ sourceCounts,
1190
+ repair: "Run storage migration or rebuild timeline_index by dropping timeline_index/triggers and reopening storage in read-write mode."
1191
+ };
1192
+ }
1193
+ upsertPrLink(link) {
1194
+ const createdAt = now();
1195
+ const id = randomUUID2();
1196
+ this.transaction(() => {
1197
+ this.db.prepare(
1198
+ `insert into pr_links (
1199
+ id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
1200
+ )
1201
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1202
+ on conflict(run_id, pr_number) do update set
1203
+ branch = excluded.branch,
1204
+ url = excluded.url,
1205
+ head_ref = excluded.head_ref,
1206
+ base_ref = excluded.base_ref,
1207
+ state = excluded.state,
1208
+ draft = excluded.draft,
1209
+ updated_at = excluded.updated_at`
1210
+ ).run(
1211
+ id,
1212
+ link.runId,
1213
+ link.branch,
1214
+ link.prNumber,
1215
+ link.url,
1216
+ link.headRef,
1217
+ link.baseRef,
1218
+ link.state,
1219
+ boolToDb(link.draft),
1220
+ createdAt,
1221
+ createdAt
1222
+ );
1223
+ });
1224
+ const stored = this.getPrLink(link.runId);
1225
+ if (!stored) {
1226
+ throw new AgentLoopError("storage_error", "PR link was not stored.");
1227
+ }
1228
+ return stored;
1229
+ }
1230
+ getPrLink(runId) {
1231
+ const row = this.db.prepare(
1232
+ `select id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
1233
+ from pr_links
1234
+ where run_id = ?
1235
+ order by updated_at desc
1236
+ limit 1`
1237
+ ).get(runId);
1238
+ return row ? fromPrLinkRow(row) : void 0;
1239
+ }
1240
+ replaceCiChecks(runId, prNumber, checks) {
1241
+ const observedAt = now();
1242
+ this.transaction(() => {
1243
+ this.db.prepare("delete from ci_checks where run_id = ? and pr_number = ?").run(runId, prNumber);
1244
+ for (const check of checks) {
1245
+ this.db.prepare(
1246
+ `insert into ci_checks (
1247
+ id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
1248
+ )
1249
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1250
+ ).run(
1251
+ randomUUID2(),
1252
+ runId,
1253
+ prNumber,
1254
+ check.name,
1255
+ check.status,
1256
+ check.conclusion ?? null,
1257
+ check.url ?? null,
1258
+ check.startedAt ?? null,
1259
+ check.completedAt ?? null,
1260
+ observedAt
1261
+ );
1262
+ }
1263
+ });
1264
+ return this.listCiChecks(runId);
1265
+ }
1266
+ listCiChecks(runId) {
1267
+ const rows = this.db.prepare(
1268
+ `select id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
1269
+ from ci_checks
1270
+ where run_id = ?
1271
+ order by observed_at desc, name asc`
1272
+ ).all(runId);
1273
+ return rows.map(fromCiCheckRow);
1274
+ }
1275
+ replaceReviewComments(runId, prNumber, comments) {
1276
+ const observedAt = now();
1277
+ this.transaction(() => {
1278
+ this.db.prepare("delete from review_comments where run_id = ? and pr_number = ?").run(runId, prNumber);
1279
+ for (const comment of comments) {
1280
+ this.db.prepare(
1281
+ `insert into review_comments (
1282
+ id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
1283
+ is_resolved, is_outdated, actionable, status, observed_at
1284
+ )
1285
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1286
+ ).run(
1287
+ randomUUID2(),
1288
+ runId,
1289
+ prNumber,
1290
+ comment.commentId,
1291
+ comment.url,
1292
+ comment.author,
1293
+ comment.body,
1294
+ comment.path,
1295
+ comment.line ?? null,
1296
+ comment.diffHunk,
1297
+ boolToDb(comment.isResolved),
1298
+ boolToDb(comment.isOutdated),
1299
+ boolToDb(comment.actionable),
1300
+ comment.status,
1301
+ observedAt
1302
+ );
1303
+ }
1304
+ });
1305
+ return this.listReviewComments(runId);
1306
+ }
1307
+ listReviewComments(runId) {
1308
+ const rows = this.db.prepare(
1309
+ `select id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
1310
+ is_resolved, is_outdated, actionable, status, observed_at
1311
+ from review_comments
1312
+ where run_id = ?
1313
+ order by observed_at desc, path asc`
1314
+ ).all(runId);
1315
+ return rows.map(fromReviewCommentRow);
1316
+ }
1317
+ appendDecision(decision) {
1318
+ const stored = { id: randomUUID2(), ...decision, createdAt: now() };
1319
+ this.transaction(() => {
1320
+ this.db.prepare(
1321
+ `insert into decisions (id, run_id, kind, message, details_json, created_at)
1322
+ values (?, ?, ?, ?, ?, ?)`
1323
+ ).run(
1324
+ stored.id,
1325
+ stored.runId,
1326
+ stored.kind,
1327
+ stored.message,
1328
+ stored.details === void 0 ? null : JSON.stringify(stored.details),
1329
+ stored.createdAt
1330
+ );
1331
+ });
1332
+ return stored;
1333
+ }
1334
+ listDecisions(runId) {
1335
+ const rows = this.db.prepare(
1336
+ `select id, run_id, kind, message, details_json, created_at
1337
+ from decisions
1338
+ where run_id = ?
1339
+ order by created_at desc`
1340
+ ).all(runId);
1341
+ return rows.map(fromDecisionRow);
1342
+ }
1343
+ createWorker(worker) {
1344
+ const id = randomUUID2();
1345
+ const startedAt = now();
1346
+ try {
1347
+ this.transaction(() => {
1348
+ this.db.prepare(
1349
+ `insert into workers (
1350
+ id, run_id, type, backend, status, thread_id, attempt, resume_used,
1351
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
1352
+ )
1353
+ values (?, ?, ?, ?, 'running', null, ?, ?, ?, null, null, null, null, null)`
1354
+ ).run(id, worker.runId, worker.type, worker.backend, worker.attempt, boolToDb(worker.resumeUsed), startedAt);
1355
+ });
1356
+ } catch (error) {
1357
+ if (isUniqueConstraintError(error)) {
1358
+ throw new AgentLoopError("worker_already_running", "Another worker is already running.", {
1359
+ details: { runId: worker.runId },
1360
+ exitCode: 2
1361
+ });
1362
+ }
1363
+ throw error;
1364
+ }
1365
+ return this.getWorker(id);
1366
+ }
1367
+ updateWorker(workerId, patch) {
1368
+ this.transaction(() => {
1369
+ this.db.prepare(
1370
+ `update workers
1371
+ set status = coalesce(?, status),
1372
+ thread_id = coalesce(?, thread_id),
1373
+ completed_at = coalesce(?, completed_at),
1374
+ exit_code = coalesce(?, exit_code),
1375
+ result_artifact_id = coalesce(?, result_artifact_id),
1376
+ raw_jsonl_artifact_id = coalesce(?, raw_jsonl_artifact_id),
1377
+ error = coalesce(?, error)
1378
+ where id = ?`
1379
+ ).run(
1380
+ patch.status ?? null,
1381
+ patch.threadId ?? null,
1382
+ patch.completedAt ?? null,
1383
+ patch.exitCode ?? null,
1384
+ patch.resultArtifactId ?? null,
1385
+ patch.rawJsonlArtifactId ?? null,
1386
+ patch.error ?? null,
1387
+ workerId
1388
+ );
1389
+ });
1390
+ return this.getWorker(workerId);
1391
+ }
1392
+ getRunningWorker() {
1393
+ const row = this.db.prepare(
1394
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
1395
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
1396
+ from workers
1397
+ where status = 'running'
1398
+ order by started_at desc
1399
+ limit 1`
1400
+ ).get();
1401
+ return row ? fromWorkerRow(row) : void 0;
1402
+ }
1403
+ listWorkers(runId, limit = 50) {
1404
+ const rows = runId ? this.listWorkersByRunStatement.all(runId, limit) : this.listWorkersStatement.all(limit);
1405
+ return rows.map(fromWorkerRow);
1406
+ }
1407
+ appendWorkerEvent(event) {
1408
+ const existing = this.findDuplicateWorkerEvent(event);
1409
+ if (existing) {
1410
+ return existing;
1411
+ }
1412
+ const stored = { id: randomUUID2(), ...event, createdAt: now() };
1413
+ let seq = 0;
1414
+ this.transaction(() => {
1415
+ this.db.prepare(
1416
+ `insert into worker_events (
1417
+ id, worker_id, run_id, event_type, item_type, item_id, item_status,
1418
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
1419
+ )
1420
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1421
+ ).run(
1422
+ stored.id,
1423
+ stored.workerId,
1424
+ stored.runId,
1425
+ stored.eventType,
1426
+ stored.itemType ?? null,
1427
+ stored.itemId ?? null,
1428
+ stored.itemStatus ?? null,
1429
+ stored.threadId ?? null,
1430
+ stored.backend ?? null,
1431
+ stored.summary === void 0 ? null : JSON.stringify(stored.summary),
1432
+ stored.usage === void 0 ? null : JSON.stringify(stored.usage),
1433
+ stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
1434
+ stored.createdAt
1435
+ );
1436
+ seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
1437
+ });
1438
+ return { seq, ...stored };
1439
+ }
1440
+ listWorkerEvents(workerId) {
1441
+ const rows = this.db.prepare(
1442
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
1443
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
1444
+ from worker_events
1445
+ where worker_id = ?
1446
+ order by seq asc`
1447
+ ).all(workerId);
1448
+ return rows.map(fromWorkerEventRow);
1449
+ }
1450
+ findDuplicateWorkerEvent(event) {
1451
+ if (!event.threadId) {
1452
+ return void 0;
1453
+ }
1454
+ const row = event.itemId ? this.db.prepare(
1455
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
1456
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
1457
+ from worker_events
1458
+ where thread_id = ? and item_id = ? and coalesce(item_status, '') = ?
1459
+ limit 1`
1460
+ ).get(event.threadId, event.itemId, event.itemStatus ?? "") : this.db.prepare(
1461
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
1462
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
1463
+ from worker_events
1464
+ where thread_id = ? and event_type = ? and item_id is null
1465
+ limit 1`
1466
+ ).get(event.threadId, event.eventType);
1467
+ return row ? fromWorkerEventRow(row) : void 0;
1468
+ }
1469
+ insertArtifact(record) {
1470
+ this.transaction(() => {
1471
+ this.db.prepare(
1472
+ `insert into artifacts (id, run_id, kind, name, path, sha256, metadata_json, created_at)
1473
+ values (?, ?, ?, ?, ?, ?, null, ?)`
1474
+ ).run(
1475
+ record.id,
1476
+ record.runId,
1477
+ record.kind,
1478
+ record.name,
1479
+ record.path,
1480
+ record.sha256,
1481
+ record.createdAt
1482
+ );
1483
+ });
1484
+ }
1485
+ getArtifact(artifactId) {
1486
+ const row = this.db.prepare(
1487
+ `select id, run_id, kind, name, path, sha256, created_at
1488
+ from artifacts
1489
+ where id = ?`
1490
+ ).get(artifactId);
1491
+ if (!row) {
1492
+ throw new AgentLoopError("storage_error", `Artifact not found: ${artifactId}`);
1493
+ }
1494
+ return fromArtifactRow(row);
1495
+ }
1496
+ listArtifacts(runId) {
1497
+ const rows = this.db.prepare(
1498
+ `select id, run_id, kind, name, path, sha256, created_at
1499
+ from artifacts
1500
+ where run_id = ?
1501
+ order by created_at asc`
1502
+ ).all(runId);
1503
+ return rows.map(fromArtifactRow);
1504
+ }
1505
+ linkArtifactToEvent(eventId, artifactId) {
1506
+ this.transaction(() => {
1507
+ const row = this.db.prepare("select artifact_ids_json from events where id = ?").get(eventId);
1508
+ if (!row) {
1509
+ throw new AgentLoopError("storage_error", `Event not found: ${eventId}`);
1510
+ }
1511
+ const ids = row.artifact_ids_json ? parseJson(row.artifact_ids_json, "Stored artifact id list is invalid.") : [];
1512
+ if (!ids.includes(artifactId)) {
1513
+ ids.push(artifactId);
1514
+ }
1515
+ this.db.prepare("update events set artifact_ids_json = ? where id = ?").run(JSON.stringify(ids), eventId);
1516
+ });
1517
+ }
1518
+ getCurrentRun() {
1519
+ const row = this.db.prepare(
1520
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1521
+ from runs
1522
+ order by updated_at desc
1523
+ limit 1`
1524
+ ).get();
1525
+ return row ? fromRunRow(row) : void 0;
1526
+ }
1527
+ listRuns(limit = 50) {
1528
+ const rows = this.db.prepare(
1529
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1530
+ from runs
1531
+ order by updated_at desc
1532
+ limit ?`
1533
+ ).all(limit);
1534
+ return rows.map(fromRunRow);
1535
+ }
1536
+ /** Run a group of read queries against one SQLite snapshot. */
1537
+ readTransaction(fn) {
1538
+ this.db.exec("BEGIN");
1539
+ try {
1540
+ const result = fn();
1541
+ this.db.exec("COMMIT");
1542
+ return result;
1543
+ } catch (error) {
1544
+ try {
1545
+ this.db.exec("ROLLBACK");
1546
+ } catch (rollbackError) {
1547
+ throw new AgentLoopError("storage_error", "Read transaction rollback failed.", {
1548
+ details: {
1549
+ cause: error instanceof Error ? error.message : String(error),
1550
+ rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
1551
+ }
1552
+ });
1553
+ }
1554
+ throw error;
1555
+ }
1556
+ }
1557
+ ensureSchema() {
1558
+ const currentVersion = this.getUserVersion();
1559
+ if (currentVersion !== 0 && !isSupportedSchemaVersion(currentVersion)) {
1560
+ throw new AgentLoopError(
1561
+ "storage_schema_mismatch",
1562
+ `SQLite schema version ${currentVersion} is not supported.`,
1563
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
1564
+ );
1565
+ }
1566
+ if (currentVersion === STORAGE_SCHEMA_VERSION) {
1567
+ if (this.mode !== "ro") {
1568
+ this.transaction(() => this.reconcileHighFidelityWorkerEventsV8());
1569
+ }
1570
+ return;
1571
+ }
1572
+ if (this.mode === "ro") {
1573
+ throw new AgentLoopError(
1574
+ "storage_schema_mismatch",
1575
+ `SQLite schema version ${currentVersion} requires migration before read-only use.`,
1576
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
1577
+ );
1578
+ }
1579
+ this.transaction(() => {
1580
+ const lockedVersion = this.getUserVersion();
1581
+ if (lockedVersion === STORAGE_SCHEMA_VERSION) {
1582
+ return;
1583
+ }
1584
+ if (lockedVersion !== 0 && !isSupportedSchemaVersion(lockedVersion)) {
1585
+ throw new AgentLoopError(
1586
+ "storage_schema_mismatch",
1587
+ `SQLite schema version ${lockedVersion} is not supported.`,
1588
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: lockedVersion } }
1589
+ );
1590
+ }
1591
+ this.db.exec(SCHEMA_SQL);
1592
+ this.migratePrC();
1593
+ this.migratePrD();
1594
+ this.migratePrE();
1595
+ this.migrateF0();
1596
+ this.migrateTimelineV7();
1597
+ this.migrateHighFidelityWorkerEventsV8();
1598
+ this.markSchemaVersion();
1599
+ });
1600
+ }
1601
+ migratePrC() {
1602
+ addColumnIfMissing(this.db, "runs", "current_state", "text");
1603
+ addColumnIfMissing(this.db, "runs", "branch", "text");
1604
+ addColumnIfMissing(this.db, "runs", "worktree_clean", "integer");
1605
+ addColumnIfMissing(this.db, "runs", "started_at", "text");
1606
+ addColumnIfMissing(this.db, "runs", "stopped_at", "text");
1607
+ addColumnIfMissing(this.db, "states", "state", "text");
1608
+ addColumnIfMissing(this.db, "states", "payload_json", "text");
1609
+ addColumnIfMissing(this.db, "events", "state_before", "text");
1610
+ addColumnIfMissing(this.db, "events", "state_after", "text");
1611
+ addColumnIfMissing(this.db, "events", "artifact_ids_json", "text");
1612
+ addColumnIfMissing(this.db, "artifacts", "name", "text");
1613
+ addColumnIfMissing(this.db, "artifacts", "sha256", "text");
1614
+ this.db.exec(PR_C_TABLES_SQL);
1615
+ }
1616
+ migratePrD() {
1617
+ this.db.exec(PR_D_TABLES_SQL);
1618
+ }
1619
+ migratePrE() {
1620
+ addColumnIfMissing(this.db, "gates", "decision_note", "text");
1621
+ addColumnIfMissing(this.db, "gates", "decided_at", "text");
1622
+ this.db.exec(PR_E_TABLES_SQL);
1623
+ this.db.exec(PR_E_INDEXES_SQL);
1624
+ }
1625
+ migrateF0() {
1626
+ rebuildEventsWithSeq(this.db);
1627
+ rebuildWorkerEventsWithSeq(this.db);
1628
+ }
1629
+ migrateTimelineV7() {
1630
+ this.db.exec(TIMELINE_INDEX_SQL);
1631
+ this.db.exec(TIMELINE_TRIGGERS_SQL);
1632
+ backfillTimelineIndex(this.db);
1633
+ }
1634
+ migrateHighFidelityWorkerEventsV8() {
1635
+ addColumnIfMissing(this.db, "worker_events", "item_id", "text");
1636
+ addColumnIfMissing(this.db, "worker_events", "item_status", "text");
1637
+ addColumnIfMissing(this.db, "worker_events", "thread_id", "text");
1638
+ addColumnIfMissing(this.db, "worker_events", "backend", "text");
1639
+ addColumnIfMissing(this.db, "worker_events", "artifact_ids_json", "text");
1640
+ this.reconcileHighFidelityWorkerEventsV8();
1641
+ }
1642
+ reconcileHighFidelityWorkerEventsV8() {
1643
+ dedupeHighFidelityWorkerEventsV8(this.db);
1644
+ this.db.exec(`
1645
+ drop index if exists worker_events_thread_item_unique;
1646
+ create unique index if not exists worker_events_thread_item_status_unique
1647
+ on worker_events(thread_id, item_id, coalesce(item_status, ''))
1648
+ where item_id is not null;
1649
+ create unique index if not exists worker_events_thread_event_unique
1650
+ on worker_events(thread_id, event_type)
1651
+ where item_id is null;
1652
+ `);
1653
+ }
1654
+ markSchemaVersion() {
1655
+ this.db.exec(`PRAGMA user_version = ${STORAGE_SCHEMA_VERSION}`);
1656
+ }
1657
+ ensureRepoConfigVersion() {
1658
+ this.validateRepoConfigVersion(true);
1659
+ }
1660
+ validateRepoConfigVersion(rewrite = false) {
1661
+ let row;
1662
+ try {
1663
+ row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
1664
+ } catch (error) {
1665
+ throw toStorageError(error, "Could not read stored repo config metadata.");
1666
+ }
1667
+ if (!row) {
1668
+ return;
1669
+ }
1670
+ if (!isSupportedSchemaVersion(row.schema_version)) {
1671
+ throw new AgentLoopError(
1672
+ "storage_schema_mismatch",
1673
+ `Stored repo config schema version ${row.schema_version} is not supported.`,
1674
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
1675
+ );
1676
+ }
1677
+ const parsed = parseJson(row.config_json, "Stored repo config snapshot JSON is invalid.");
1678
+ if (parsed.schemaVersion === STORAGE_SCHEMA_VERSION) {
1679
+ return;
1680
+ }
1681
+ if (rewrite && isSupportedSchemaVersion(parsed.schemaVersion ?? 0) && typeof parsed.repoId === "string") {
1682
+ this.writeRepoConfig(withConfigDefaults(parsed));
1683
+ return;
1684
+ }
1685
+ throw new AgentLoopError("storage_error", "Stored repo config snapshot schemaVersion is invalid.", {
1686
+ details: { expected: STORAGE_SCHEMA_VERSION, actual: parsed.schemaVersion }
1687
+ });
1688
+ }
1689
+ getUserVersion() {
1690
+ const row = this.db.prepare("PRAGMA user_version").get();
1691
+ return row.user_version;
1692
+ }
1693
+ getRun(runId) {
1694
+ const row = this.db.prepare(
1695
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1696
+ from runs
1697
+ where id = ?`
1698
+ ).get(runId);
1699
+ if (!row) {
1700
+ throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
1701
+ }
1702
+ return fromRunRow(row);
1703
+ }
1704
+ getActiveRun() {
1705
+ const row = this.db.prepare(
1706
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1707
+ from runs
1708
+ where status = 'RUNNING'
1709
+ order by updated_at desc
1710
+ limit 1`
1711
+ ).get();
1712
+ return row ? fromRunRow(row) : void 0;
1713
+ }
1714
+ getWorker(workerId) {
1715
+ const row = this.db.prepare(
1716
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
1717
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
1718
+ from workers
1719
+ where id = ?`
1720
+ ).get(workerId);
1721
+ if (!row) {
1722
+ throw new AgentLoopError("storage_error", `Worker not found: ${workerId}`);
1723
+ }
1724
+ return fromWorkerRow(row);
1725
+ }
1726
+ timelineEntry(row) {
1727
+ if (!isTimelineSource(row.source)) {
1728
+ return void 0;
1729
+ }
1730
+ if (row.source === "event") {
1731
+ const sourceRow2 = this.db.prepare(
1732
+ `select seq, id, run_id, kind, message, artifact_ids_json, created_at
1733
+ from events where id = ?`
1734
+ ).get(row.source_id);
1735
+ if (!sourceRow2) return void 0;
1736
+ const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored event artifact list JSON is invalid.") : void 0;
1737
+ return timelineEntry(row, {
1738
+ kind: sourceRow2.kind,
1739
+ title: sourceRow2.kind,
1740
+ summary: sourceRow2.message,
1741
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
1742
+ ...artifactIds ? { artifactIds } : {},
1743
+ rawRef: { table: "events", id: sourceRow2.id, seq: sourceRow2.seq }
1744
+ });
1745
+ }
1746
+ if (row.source === "worker_event") {
1747
+ const sourceRow2 = this.db.prepare(
1748
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
1749
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
1750
+ from worker_events where id = ?`
1751
+ ).get(row.source_id);
1752
+ if (!sourceRow2) return void 0;
1753
+ const worker = this.db.prepare("select thread_id from workers where id = ?").get(sourceRow2.worker_id);
1754
+ const summary = sourceRow2.summary_json ? summarizeTimelinePayload(parseJson(sourceRow2.summary_json, "Stored worker event summary JSON is invalid.")) : sourceRow2.event_type;
1755
+ const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") : void 0;
1756
+ return timelineEntry(row, {
1757
+ kind: sourceRow2.item_type ?? sourceRow2.event_type,
1758
+ title: workerEventTimelineTitle(sourceRow2),
1759
+ summary,
1760
+ runId: sourceRow2.run_id,
1761
+ workerId: sourceRow2.worker_id,
1762
+ ...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : worker?.thread_id ? { threadId: worker.thread_id } : {},
1763
+ ...sourceRow2.item_status ? { status: sourceRow2.item_status } : {},
1764
+ ...artifactIds?.length ? { artifactIds } : {},
1765
+ rawRef: { table: "worker_events", id: sourceRow2.id, seq: sourceRow2.seq }
1766
+ });
1767
+ }
1768
+ if (row.source === "worker") {
1769
+ const sourceRow2 = this.db.prepare(
1770
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
1771
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
1772
+ from workers where id = ?`
1773
+ ).get(row.worker_id ?? workerIdFromSourceId(row.source_id));
1774
+ if (!sourceRow2) return void 0;
1775
+ const status = statusFromWorkerSourceId(row.source_id) ?? sourceRow2.status;
1776
+ return timelineEntry(row, {
1777
+ kind: sourceRow2.type,
1778
+ title: `${sourceRow2.type} worker ${status}`,
1779
+ summary: summarizeTimelinePayload({
1780
+ status,
1781
+ attempt: sourceRow2.attempt,
1782
+ backend: sourceRow2.backend,
1783
+ exitCode: sourceRow2.exit_code,
1784
+ error: sourceRow2.error
1785
+ }),
1786
+ runId: sourceRow2.run_id,
1787
+ workerId: sourceRow2.id,
1788
+ ...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : {},
1789
+ status,
1790
+ artifactIds: [sourceRow2.result_artifact_id, sourceRow2.raw_jsonl_artifact_id].filter((id) => Boolean(id)),
1791
+ rawRef: { table: "workers", id: row.source_id }
1792
+ });
1793
+ }
1794
+ if (row.source === "state") {
1795
+ const sourceRow2 = this.db.prepare("select id, run_id, status, state, version, created_at from states where id = ?").get(Number(row.source_id));
1796
+ if (!sourceRow2) return void 0;
1797
+ return timelineEntry(row, {
1798
+ kind: sourceRow2.state ?? sourceRow2.status,
1799
+ title: "State changed",
1800
+ summary: summarizeTimelinePayload({ status: sourceRow2.status, state: sourceRow2.state, version: sourceRow2.version }),
1801
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
1802
+ status: sourceRow2.status,
1803
+ rawRef: { table: "states", id: String(sourceRow2.id), seq: sourceRow2.id }
1804
+ });
1805
+ }
1806
+ if (row.source === "gate") {
1807
+ const sourceRow2 = this.db.prepare(
1808
+ `select id, run_id, kind, status, message, details_json, created_at,
1809
+ resolved_at, decision_note, decided_at
1810
+ from gates where id = ?`
1811
+ ).get(row.source_id);
1812
+ if (!sourceRow2) return void 0;
1813
+ return timelineEntry(row, {
1814
+ kind: sourceRow2.kind,
1815
+ title: `Gate opened: ${sourceRow2.kind}`,
1816
+ summary: sourceRow2.message,
1817
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
1818
+ status: sourceRow2.status,
1819
+ rawRef: { table: "gates", id: sourceRow2.id }
1820
+ });
1821
+ }
1822
+ if (row.source === "artifact") {
1823
+ const sourceRow2 = this.db.prepare("select id, run_id, kind, name, path, sha256, created_at from artifacts where id = ?").get(row.source_id);
1824
+ if (!sourceRow2) return void 0;
1825
+ return timelineEntry(row, {
1826
+ kind: sourceRow2.kind,
1827
+ title: `Artifact: ${sourceRow2.name ?? sourceRow2.id}`,
1828
+ summary: summarizeTimelinePayload({ name: sourceRow2.name ?? sourceRow2.id, kind: sourceRow2.kind, sha256: sourceRow2.sha256 }),
1829
+ runId: sourceRow2.run_id,
1830
+ artifactIds: [sourceRow2.id],
1831
+ rawRef: { table: "artifacts", id: sourceRow2.id }
1832
+ });
1833
+ }
1834
+ const sourceRow = this.db.prepare("select id, run_id, kind, message, created_at from decisions where id = ?").get(row.source_id);
1835
+ if (!sourceRow) return void 0;
1836
+ return timelineEntry(row, {
1837
+ kind: sourceRow.kind,
1838
+ title: sourceRow.kind,
1839
+ summary: sourceRow.message,
1840
+ runId: sourceRow.run_id,
1841
+ rawRef: { table: "decisions", id: sourceRow.id }
1842
+ });
1843
+ }
1844
+ transaction(fn) {
1845
+ this.db.exec("BEGIN IMMEDIATE");
1846
+ try {
1847
+ const result = fn();
1848
+ this.db.exec("COMMIT");
1849
+ return result;
1850
+ } catch (error) {
1851
+ try {
1852
+ this.db.exec("ROLLBACK");
1853
+ } catch (rollbackError) {
1854
+ throw new AgentLoopError("storage_error", "Transaction rollback failed.", {
1855
+ details: {
1856
+ cause: error instanceof Error ? error.message : String(error),
1857
+ rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
1858
+ }
1859
+ });
1860
+ }
1861
+ throw error;
1862
+ }
1863
+ }
1864
+ };
1865
+ function timelineEntry(row, entry) {
1866
+ return {
1867
+ timelineSeq: row.timeline_seq,
1868
+ occurredAt: row.created_at,
1869
+ cursor: encodeTimelineCursor(row.timeline_seq, row.created_at),
1870
+ source: row.source,
1871
+ kind: entry.kind,
1872
+ ...entry.runId ? { runId: entry.runId } : {},
1873
+ ...entry.workerId ? { workerId: entry.workerId } : {},
1874
+ ...entry.threadId ? { threadId: entry.threadId } : {},
1875
+ title: truncateTimelineText(redactTimelineText(entry.title), 160),
1876
+ summary: truncateTimelineText(redactTimelineText(entry.summary), 1e3),
1877
+ ...entry.status ? { status: entry.status } : {},
1878
+ ...entry.artifactIds?.length ? { artifactIds: entry.artifactIds } : {},
1879
+ createdAt: row.created_at,
1880
+ rawRef: entry.rawRef
1881
+ };
1882
+ }
1883
+ function backfillTimelineIndex(db) {
1884
+ db.exec(`
1885
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1886
+ select source, source_id, source_seq, run_id, worker_id, created_at
1887
+ from (
1888
+ select 'event' as source, id as source_id, seq as source_seq, run_id, null as worker_id, created_at
1889
+ from events
1890
+ union all
1891
+ select 'worker_event' as source, id as source_id, seq as source_seq, run_id, worker_id, created_at
1892
+ from worker_events
1893
+ union all
1894
+ select 'worker' as source, id || ':' || status as source_id, null as source_seq, run_id, id as worker_id, started_at as created_at
1895
+ from workers
1896
+ union all
1897
+ select 'state' as source, cast(id as text) as source_id, id as source_seq, run_id, null as worker_id, created_at
1898
+ from states
1899
+ union all
1900
+ select 'gate' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
1901
+ from gates
1902
+ union all
1903
+ select 'artifact' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
1904
+ from artifacts
1905
+ union all
1906
+ select 'decision' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
1907
+ from decisions
1908
+ )
1909
+ order by created_at asc, source asc, source_id asc;
1910
+ `);
1911
+ }
1912
+ function normalizeTimelineSources(sources) {
1913
+ const unique = [...new Set(sources)];
1914
+ if (unique.some((source) => !isTimelineSource(source))) {
1915
+ throw new AgentLoopError("invalid_config", "Unsupported timeline source.", { details: { sources } });
1916
+ }
1917
+ return unique;
1918
+ }
1919
+ function isTimelineSource(value) {
1920
+ return TIMELINE_SOURCES.includes(value);
1921
+ }
1922
+ function clampLimit(value) {
1923
+ if (!Number.isFinite(value)) {
1924
+ return 50;
1925
+ }
1926
+ return Math.min(Math.max(Math.trunc(value), 1), 200);
1927
+ }
1928
+ function encodeTimelineCursor(timelineSeq, occurredAt) {
1929
+ return Buffer.from(JSON.stringify({ timelineSeq, ...occurredAt ? { occurredAt } : {} }), "utf8").toString("base64url");
1930
+ }
1931
+ function decodeTimelineCursor(cursor) {
1932
+ try {
1933
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
1934
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1935
+ const timelineSeq = parsed.timelineSeq;
1936
+ const occurredAt = parsed.occurredAt;
1937
+ if (typeof timelineSeq === "number" && Number.isInteger(timelineSeq) && timelineSeq > 0 && typeof occurredAt === "string" && occurredAt.length > 0) {
1938
+ return { timelineSeq, occurredAt };
1939
+ }
1940
+ }
1941
+ } catch {
1942
+ }
1943
+ throw new AgentLoopError("invalid_config", "Timeline cursor is invalid.");
1944
+ }
1945
+ function timelineMissingSourceRows(db) {
1946
+ const checks = [
1947
+ {
1948
+ source: "event",
1949
+ sql: `select count(*) as count
1950
+ from events source
1951
+ left join timeline_index ti on ti.source = 'event' and ti.source_id = source.id
1952
+ where ti.timeline_seq is null`
1953
+ },
1954
+ {
1955
+ source: "worker_event",
1956
+ sql: `select count(*) as count
1957
+ from worker_events source
1958
+ left join timeline_index ti on ti.source = 'worker_event' and ti.source_id = source.id
1959
+ where ti.timeline_seq is null`
1960
+ },
1961
+ {
1962
+ source: "worker",
1963
+ sql: `select count(*) as count
1964
+ from workers source
1965
+ left join timeline_index ti on ti.source = 'worker' and ti.source_id = source.id || ':' || source.status
1966
+ where ti.timeline_seq is null`
1967
+ },
1968
+ {
1969
+ source: "state",
1970
+ sql: `select count(*) as count
1971
+ from states source
1972
+ left join timeline_index ti on ti.source = 'state' and ti.source_id = cast(source.id as text)
1973
+ where ti.timeline_seq is null`
1974
+ },
1975
+ {
1976
+ source: "gate",
1977
+ sql: `select count(*) as count
1978
+ from gates source
1979
+ left join timeline_index ti on ti.source = 'gate' and ti.source_id = source.id
1980
+ where ti.timeline_seq is null`
1981
+ },
1982
+ {
1983
+ source: "artifact",
1984
+ sql: `select count(*) as count
1985
+ from artifacts source
1986
+ left join timeline_index ti on ti.source = 'artifact' and ti.source_id = source.id
1987
+ where ti.timeline_seq is null`
1988
+ },
1989
+ {
1990
+ source: "decision",
1991
+ sql: `select count(*) as count
1992
+ from decisions source
1993
+ left join timeline_index ti on ti.source = 'decision' and ti.source_id = source.id
1994
+ where ti.timeline_seq is null`
1995
+ }
1996
+ ];
1997
+ return checks.flatMap((check) => {
1998
+ const row = db.prepare(check.sql).get();
1999
+ const missing = row?.count ?? 0;
2000
+ return missing > 0 ? [{ source: check.source, missing }] : [];
2001
+ });
2002
+ }
2003
+ function summarizeTimelinePayload(value) {
2004
+ if (typeof value === "string") {
2005
+ return value;
2006
+ }
2007
+ if (value === void 0 || value === null) {
2008
+ return "";
2009
+ }
2010
+ return JSON.stringify(redactTimelineValue(value));
2011
+ }
2012
+ function redactTimelineValue(value) {
2013
+ if (Array.isArray(value)) {
2014
+ return value.slice(0, 20).map(redactTimelineValue);
2015
+ }
2016
+ if (typeof value !== "object" || value === null) {
2017
+ return value;
2018
+ }
2019
+ const redacted = {};
2020
+ for (const [key, nested] of Object.entries(value).slice(0, 40)) {
2021
+ redacted[key] = isSecretKey(key) ? "[redacted]" : redactTimelineValue(nested);
2022
+ }
2023
+ return redacted;
2024
+ }
2025
+ function redactTimelineText(value) {
2026
+ return redactSecrets(value);
2027
+ }
2028
+ function truncateTimelineText(value, maxLength) {
2029
+ return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
2030
+ }
2031
+ function statusFromWorkerSourceId(sourceId) {
2032
+ const status = sourceId.split(":").at(-1);
2033
+ return status && ["running", "succeeded", "failed", "timed_out", "invalid_output"].includes(status) ? status : void 0;
2034
+ }
2035
+ function workerIdFromSourceId(sourceId) {
2036
+ return sourceId.split(":")[0] ?? sourceId;
2037
+ }
2038
+ function fromRunRow(row) {
2039
+ return {
2040
+ id: row.id,
2041
+ status: row.status,
2042
+ ...row.current_state ? { currentState: row.current_state } : {},
2043
+ version: row.version,
2044
+ ...row.branch ? { branch: row.branch } : {},
2045
+ ...row.worktree_clean !== null ? { worktreeClean: row.worktree_clean === 1 } : {},
2046
+ createdAt: row.created_at,
2047
+ updatedAt: row.updated_at,
2048
+ ...row.started_at ? { startedAt: row.started_at } : {},
2049
+ ...row.stopped_at ? { stoppedAt: row.stopped_at } : {}
2050
+ };
2051
+ }
2052
+ function fromEventRow(row) {
2053
+ return {
2054
+ id: row.id,
2055
+ seq: row.seq,
2056
+ ...row.run_id ? { runId: row.run_id } : {},
2057
+ kind: row.kind,
2058
+ message: row.message,
2059
+ ...row.state_before ? { stateBefore: row.state_before } : {},
2060
+ ...row.state_after ? { stateAfter: row.state_after } : {},
2061
+ ...row.payload_json ? { payload: parseJson(row.payload_json, "Stored event payload JSON is invalid.") } : {},
2062
+ ...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored event artifact list JSON is invalid.") } : {},
2063
+ createdAt: row.created_at
2064
+ };
2065
+ }
2066
+ function statusGateFromRow(row) {
2067
+ return {
2068
+ kind: row.kind,
2069
+ message: row.message,
2070
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {}
2071
+ };
2072
+ }
2073
+ function latestGateSatisfied(db, runId) {
2074
+ const row = db.prepare(
2075
+ `select status
2076
+ from gates
2077
+ where run_id = ?
2078
+ order by created_at desc
2079
+ limit 1`
2080
+ ).get(runId);
2081
+ return row?.status === "approved" || row?.status === "resolved";
2082
+ }
2083
+ function fromGateRow(row) {
2084
+ return {
2085
+ id: row.id,
2086
+ ...row.run_id ? { runId: row.run_id } : {},
2087
+ kind: row.kind,
2088
+ status: row.status,
2089
+ message: row.message,
2090
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {},
2091
+ createdAt: row.created_at,
2092
+ ...row.resolved_at ? { resolvedAt: row.resolved_at } : {},
2093
+ ...row.decision_note ? { decisionNote: row.decision_note } : {},
2094
+ ...row.decided_at ? { decidedAt: row.decided_at } : {}
2095
+ };
2096
+ }
2097
+ function fromArtifactRow(row) {
2098
+ return {
2099
+ id: row.id,
2100
+ runId: row.run_id,
2101
+ kind: row.kind,
2102
+ name: row.name ?? row.id,
2103
+ path: row.path,
2104
+ sha256: row.sha256 ?? "",
2105
+ createdAt: row.created_at
2106
+ };
2107
+ }
2108
+ function fromPrLinkRow(row) {
2109
+ return {
2110
+ id: row.id,
2111
+ runId: row.run_id,
2112
+ branch: row.branch,
2113
+ prNumber: row.pr_number,
2114
+ url: row.url,
2115
+ headRef: row.head_ref,
2116
+ baseRef: row.base_ref,
2117
+ state: row.state,
2118
+ draft: row.draft === 1,
2119
+ createdAt: row.created_at,
2120
+ updatedAt: row.updated_at
2121
+ };
2122
+ }
2123
+ function fromCiCheckRow(row) {
2124
+ return {
2125
+ id: row.id,
2126
+ runId: row.run_id,
2127
+ prNumber: row.pr_number,
2128
+ name: row.name,
2129
+ status: row.status,
2130
+ ...row.conclusion ? { conclusion: row.conclusion } : {},
2131
+ ...row.url ? { url: row.url } : {},
2132
+ ...row.started_at ? { startedAt: row.started_at } : {},
2133
+ ...row.completed_at ? { completedAt: row.completed_at } : {},
2134
+ observedAt: row.observed_at
2135
+ };
2136
+ }
2137
+ function fromReviewCommentRow(row) {
2138
+ return {
2139
+ id: row.id,
2140
+ runId: row.run_id,
2141
+ prNumber: row.pr_number,
2142
+ commentId: row.comment_id,
2143
+ url: row.url,
2144
+ author: row.author,
2145
+ body: row.body,
2146
+ path: row.path,
2147
+ ...row.line === null ? {} : { line: row.line },
2148
+ diffHunk: row.diff_hunk,
2149
+ isResolved: row.is_resolved === 1,
2150
+ isOutdated: row.is_outdated === 1,
2151
+ actionable: row.actionable === 1,
2152
+ status: row.status,
2153
+ observedAt: row.observed_at
2154
+ };
2155
+ }
2156
+ function fromDecisionRow(row) {
2157
+ return {
2158
+ id: row.id,
2159
+ runId: row.run_id,
2160
+ kind: row.kind,
2161
+ message: row.message,
2162
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored decision details JSON is invalid.") } : {},
2163
+ createdAt: row.created_at
2164
+ };
2165
+ }
2166
+ function fromRunCheckRow(row) {
2167
+ return {
2168
+ runId: row.run_id,
2169
+ kind: row.kind,
2170
+ status: row.status,
2171
+ ...row.details_json ? { details: JSON.parse(row.details_json) } : {},
2172
+ createdAt: row.created_at
2173
+ };
2174
+ }
2175
+ function fromWorkerRow(row) {
2176
+ return {
2177
+ id: row.id,
2178
+ runId: row.run_id,
2179
+ type: row.type,
2180
+ backend: row.backend,
2181
+ status: row.status,
2182
+ ...row.thread_id ? { threadId: row.thread_id } : {},
2183
+ attempt: row.attempt,
2184
+ resumeUsed: row.resume_used === 1,
2185
+ startedAt: row.started_at,
2186
+ ...row.completed_at ? { completedAt: row.completed_at } : {},
2187
+ ...row.exit_code === null ? {} : { exitCode: row.exit_code },
2188
+ ...row.result_artifact_id ? { resultArtifactId: row.result_artifact_id } : {},
2189
+ ...row.raw_jsonl_artifact_id ? { rawJsonlArtifactId: row.raw_jsonl_artifact_id } : {},
2190
+ ...row.error ? { error: row.error } : {}
2191
+ };
2192
+ }
2193
+ function fromWorkerEventRow(row) {
2194
+ return {
2195
+ id: row.id,
2196
+ seq: row.seq,
2197
+ workerId: row.worker_id,
2198
+ runId: row.run_id,
2199
+ eventType: row.event_type,
2200
+ ...row.item_type ? { itemType: row.item_type } : {},
2201
+ ...row.item_id ? { itemId: row.item_id } : {},
2202
+ ...row.item_status ? { itemStatus: row.item_status } : {},
2203
+ ...row.thread_id ? { threadId: row.thread_id } : {},
2204
+ ...row.backend ? { backend: row.backend } : {},
2205
+ ...row.summary_json ? { summary: parseJson(row.summary_json, "Stored worker event summary JSON is invalid.") } : {},
2206
+ ...row.usage_json ? { usage: parseJson(row.usage_json, "Stored worker event usage JSON is invalid.") } : {},
2207
+ ...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") } : {},
2208
+ createdAt: row.created_at
2209
+ };
2210
+ }
2211
+ function workerEventTimelineTitle(row) {
2212
+ const item = row.item_type ?? row.event_type;
2213
+ return row.item_status ? `${row.item_status} ${item}` : item;
2214
+ }
2215
+ function isSupportedSchemaVersion(value) {
2216
+ return SUPPORTED_SCHEMA_VERSIONS.includes(value);
2217
+ }
2218
+ function rebuildEventsWithSeq(db) {
2219
+ if (hasColumn(db, "events", "seq")) {
2220
+ return;
2221
+ }
2222
+ db.exec(`
2223
+ alter table events rename to events_legacy_v6;
2224
+ create table events (
2225
+ seq integer primary key autoincrement,
2226
+ id text not null unique,
2227
+ run_id text,
2228
+ kind text not null,
2229
+ message text not null,
2230
+ state_before text,
2231
+ state_after text,
2232
+ payload_json text,
2233
+ artifact_ids_json text,
2234
+ created_at text not null,
2235
+ foreign key(run_id) references runs(id)
2236
+ );
2237
+ insert into events (
2238
+ id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
2239
+ )
2240
+ select id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
2241
+ from events_legacy_v6
2242
+ order by created_at asc, id asc;
2243
+ drop table events_legacy_v6;
2244
+ `);
2245
+ }
2246
+ function rebuildWorkerEventsWithSeq(db) {
2247
+ if (hasColumn(db, "worker_events", "seq")) {
2248
+ return;
2249
+ }
2250
+ db.exec(`
2251
+ alter table worker_events rename to worker_events_legacy_v6;
2252
+ create table worker_events (
2253
+ seq integer primary key autoincrement,
2254
+ id text not null unique,
2255
+ worker_id text not null,
2256
+ run_id text not null,
2257
+ event_type text not null,
2258
+ item_type text,
2259
+ summary_json text,
2260
+ usage_json text,
2261
+ created_at text not null,
2262
+ foreign key(worker_id) references workers(id),
2263
+ foreign key(run_id) references runs(id)
2264
+ );
2265
+ insert into worker_events (
2266
+ id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
2267
+ )
2268
+ select id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
2269
+ from worker_events_legacy_v6
2270
+ order by created_at asc, id asc;
2271
+ drop table worker_events_legacy_v6;
2272
+ `);
2273
+ }
2274
+ function dedupeHighFidelityWorkerEventsV8(db) {
2275
+ db.exec(`
2276
+ create temp table if not exists worker_event_dedupe_ids (
2277
+ id text primary key
2278
+ );
2279
+ delete from worker_event_dedupe_ids;
2280
+ insert or ignore into worker_event_dedupe_ids (id)
2281
+ select id from (
2282
+ select id from (
2283
+ select id,
2284
+ seq,
2285
+ row_number() over (
2286
+ partition by thread_id, item_id, coalesce(item_status, '')
2287
+ order by seq asc
2288
+ ) as duplicate_rank
2289
+ from worker_events
2290
+ where thread_id is not null and item_id is not null
2291
+ )
2292
+ where duplicate_rank > 1
2293
+ );
2294
+ insert or ignore into worker_event_dedupe_ids (id)
2295
+ select id from (
2296
+ select id from (
2297
+ select id,
2298
+ seq,
2299
+ row_number() over (
2300
+ partition by thread_id, event_type
2301
+ order by seq asc
2302
+ ) as duplicate_rank
2303
+ from worker_events
2304
+ where thread_id is not null and item_id is null
2305
+ )
2306
+ where duplicate_rank > 1
2307
+ );
2308
+ delete from timeline_index
2309
+ where source = 'worker_event'
2310
+ and source_id in (select id from worker_event_dedupe_ids);
2311
+ delete from worker_events
2312
+ where id in (select id from worker_event_dedupe_ids);
2313
+ delete from worker_event_dedupe_ids;
2314
+ `);
2315
+ }
2316
+ function hasColumn(db, tableName, columnName) {
2317
+ validateSqlIdentifier(tableName);
2318
+ validateSqlIdentifier(columnName);
2319
+ const columns = db.prepare(`pragma table_info(${tableName})`).all();
2320
+ return columns.some((column) => column.name === columnName);
2321
+ }
2322
+ function hasTable(db, tableName) {
2323
+ validateSqlIdentifier(tableName);
2324
+ const row = db.prepare("select 1 from sqlite_master where type = 'table' and name = ? limit 1").get(tableName);
2325
+ return row !== void 0;
2326
+ }
2327
+ function boolToDb(value) {
2328
+ if (value === void 0) {
2329
+ return null;
2330
+ }
2331
+ return value ? 1 : 0;
2332
+ }
2333
+ function addColumnIfMissing(db, tableName, columnName, definition) {
2334
+ validateSqlIdentifier(tableName);
2335
+ validateSqlIdentifier(columnName);
2336
+ if (!hasColumn(db, tableName, columnName)) {
2337
+ db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
2338
+ }
2339
+ }
2340
+ function validateSqlIdentifier(value) {
2341
+ if (!/^[a-z0-9_]+$/.test(value)) {
2342
+ throw new AgentLoopError("storage_error", `Unsafe SQLite identifier: ${value}`);
2343
+ }
2344
+ }
2345
+ function now() {
2346
+ return (/* @__PURE__ */ new Date()).toISOString();
2347
+ }
2348
+ function parseJson(value, message) {
2349
+ try {
2350
+ return JSON.parse(value);
2351
+ } catch (error) {
2352
+ throw new AgentLoopError("storage_error", message, {
2353
+ details: { cause: error instanceof Error ? error.message : String(error) }
2354
+ });
2355
+ }
2356
+ }
2357
+ function isUniqueConstraintError(error) {
2358
+ return error instanceof Error && /unique constraint/i.test(error.message);
2359
+ }
2360
+ function toStorageError(error, message) {
2361
+ if (error instanceof AgentLoopError) {
2362
+ return error;
2363
+ }
2364
+ return new AgentLoopError("storage_error", message, {
2365
+ details: { cause: error instanceof Error ? error.message : String(error) }
2366
+ });
2367
+ }
2368
+
2369
+ // plugins/autonomous-pr-loop/core/hook-observer.ts
2370
+ function observeCodexHook(event, payload, repoRoot) {
2371
+ try {
2372
+ const route = resolveHookRoute(payload, { legacyRepoRoot: repoRoot });
2373
+ if (route.status === "no_match") {
2374
+ return { continue: true, observed: false };
2375
+ }
2376
+ if (route.status === "ambiguous") {
2377
+ return { continue: true, observed: false, error: route.reason };
2378
+ }
2379
+ if (route.status === "route_error") {
2380
+ return { continue: true, observed: false, error: route.reason };
2381
+ }
2382
+ const storage = new SqliteAgentLoopStorage(statePath(route.binding.repoRoot));
2383
+ try {
2384
+ const run = route.binding.runId ? storage.listRuns(200).find((item) => item.id === route.binding.runId) : storage.getCurrentRun();
2385
+ storage.appendEvent({
2386
+ ...run ? { runId: run.id } : {},
2387
+ kind: hookEventKind(event),
2388
+ message: `Codex ${event} hook observed.`,
2389
+ payload: {
2390
+ ...normalizeHookPayload(event, payload),
2391
+ hookRouting: route.legacy ? "legacy" : "binding",
2392
+ worktreeRoot: route.context.worktreeRoot
2393
+ }
2394
+ });
2395
+ } finally {
2396
+ storage.close();
2397
+ }
2398
+ return { continue: true, observed: true };
2399
+ } catch (error) {
2400
+ return { continue: true, observed: false, error: error instanceof Error ? error.message : String(error) };
2401
+ }
2402
+ }
2403
+ function normalizeHookPayload(event, payload) {
2404
+ const text = JSON.stringify(payload ?? {});
2405
+ const base = {
2406
+ event,
2407
+ payloadLength: text.length,
2408
+ payloadSha256: createHash2("sha256").update(text).digest("hex")
2409
+ };
2410
+ if (event === "UserPromptSubmit" || event === "PermissionRequest") {
2411
+ return { ...base, redacted: true };
2412
+ }
2413
+ if (!isRecord(payload)) {
2414
+ return base;
2415
+ }
2416
+ return {
2417
+ ...base,
2418
+ redacted: true,
2419
+ toolName: stringValue2(payload.tool_name) ?? stringValue2(payload.toolName) ?? stringValue2(payload.tool),
2420
+ matcher: stringValue2(payload.matcher),
2421
+ sessionIdHash: hashOptional(stringValue2(payload.session_id) ?? stringValue2(payload.sessionId)),
2422
+ command: summarizeCommand(payload)
2423
+ };
2424
+ }
2425
+ function summarizeCommand(payload) {
2426
+ const toolInput = isRecord(payload.tool_input) ? payload.tool_input : payload;
2427
+ const command = stringValue2(toolInput.command) ?? stringValue2(toolInput.cmd) ?? stringValue2(toolInput.input);
2428
+ return command ? redactSecrets(command.slice(0, 500)) : void 0;
2429
+ }
2430
+ function stringValue2(value) {
2431
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2432
+ }
2433
+ function hashOptional(value) {
2434
+ return value ? createHash2("sha256").update(value).digest("hex") : void 0;
2435
+ }
2436
+
2437
+ // plugins/autonomous-pr-loop/hooks/observe-runner.ts
2438
+ function runObserveOnlyHook(event) {
2439
+ const input = readStdinJson();
2440
+ const repoRoot = process.env.AGENT_LOOP_REPO_ROOT;
2441
+ const result = observeCodexHook(event, input, repoRoot);
2442
+ if (result.error) {
2443
+ process.stderr.write(`agent-loop ${event} observe failed: ${result.error}
2444
+ `);
2445
+ }
2446
+ process.stdout.write(`${JSON.stringify({ continue: true })}
2447
+ `);
2448
+ }
2449
+ function readStdinJson() {
2450
+ const text = readFileSync2(0, "utf8");
2451
+ if (text.trim().length === 0) {
2452
+ return {};
2453
+ }
2454
+ try {
2455
+ return JSON.parse(text);
2456
+ } catch {
2457
+ return { rawLength: text.length };
2458
+ }
2459
+ }
2460
+
2461
+ // plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts
2462
+ runObserveOnlyHook("UserPromptSubmit");