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,3460 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // plugins/autonomous-pr-loop/hooks/pre-tool-use.ts
4
+ import { readFileSync as readFileSync3 } from "node:fs";
5
+
6
+ // plugins/autonomous-pr-loop/core/hook-policy.ts
7
+ import { createHash as createHash2 } from "node:crypto";
8
+
9
+ // plugins/autonomous-pr-loop/core/config.ts
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ // plugins/autonomous-pr-loop/core/errors.ts
14
+ var AgentLoopError = class extends Error {
15
+ code;
16
+ details;
17
+ exitCode;
18
+ constructor(code, message, options = {}) {
19
+ super(message);
20
+ this.name = "AgentLoopError";
21
+ this.code = code;
22
+ this.details = options.details;
23
+ this.exitCode = options.exitCode ?? (isGateCode(code) ? 2 : 1);
24
+ }
25
+ };
26
+ function isGateCode(code) {
27
+ 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";
28
+ }
29
+
30
+ // plugins/autonomous-pr-loop/core/locale.ts
31
+ var LOCALE_SETTINGS = ["zh-CN", "en-US", "system"];
32
+ var DEFAULT_LOCALE = "zh-CN";
33
+
34
+ // plugins/autonomous-pr-loop/core/loop-shapes.ts
35
+ var PR_LOOP_STATES = [
36
+ "SYNC_MAIN",
37
+ "DISCOVER_PROGRESS",
38
+ "SELECT_NEXT_PR",
39
+ "WRITE_SPEC",
40
+ "CREATE_BRANCH",
41
+ "IMPLEMENT",
42
+ "SELF_CHECK",
43
+ "COMMIT_PUSH_PR",
44
+ "WAIT_REVIEW_OR_CI",
45
+ "FIX_REVIEW",
46
+ "PUSH_FIX",
47
+ "READY_TO_MERGE",
48
+ "MERGE",
49
+ "BLOCKED",
50
+ "STOPPED"
51
+ ];
52
+ var PR_LOOP_TERMINAL_STATES = ["BLOCKED", "STOPPED"];
53
+ var PR_LOOP_TRANSITIONS = [
54
+ { from: "SYNC_MAIN", to: "DISCOVER_PROGRESS", trigger: "step", guard: "config_present" },
55
+ { from: "DISCOVER_PROGRESS", to: "SELECT_NEXT_PR", trigger: "step", guard: "config_present" },
56
+ { from: "SELECT_NEXT_PR", to: "WRITE_SPEC", trigger: "step", guard: "next_pr_unique" },
57
+ { from: "WRITE_SPEC", to: "CREATE_BRANCH", trigger: "step", guard: "always" },
58
+ { from: "CREATE_BRANCH", to: "IMPLEMENT", trigger: "step", guard: "always" },
59
+ { from: "IMPLEMENT", to: "SELF_CHECK", trigger: "step", guard: "always" },
60
+ { from: "SELF_CHECK", to: "COMMIT_PUSH_PR", trigger: "step", guard: "always" },
61
+ { from: "COMMIT_PUSH_PR", to: "WAIT_REVIEW_OR_CI", trigger: "step", guard: "always" },
62
+ { from: "WAIT_REVIEW_OR_CI", to: "FIX_REVIEW", trigger: "step", guard: "always" },
63
+ { from: "FIX_REVIEW", to: "PUSH_FIX", trigger: "step", guard: "always" },
64
+ { from: "PUSH_FIX", to: "WAIT_REVIEW_OR_CI", trigger: "step", guard: "always" },
65
+ { from: "WAIT_REVIEW_OR_CI", to: "READY_TO_MERGE", trigger: "step", guard: "always" },
66
+ { from: "READY_TO_MERGE", to: "MERGE", trigger: "step", guard: "always" },
67
+ { from: "MERGE", to: "SYNC_MAIN", trigger: "step", guard: "always" },
68
+ { from: "SYNC_MAIN", to: "STOPPED", trigger: "stop", guard: "always" },
69
+ { from: "DISCOVER_PROGRESS", to: "STOPPED", trigger: "stop", guard: "always" },
70
+ { from: "SELECT_NEXT_PR", to: "STOPPED", trigger: "stop", guard: "always" },
71
+ { from: "WRITE_SPEC", to: "STOPPED", trigger: "stop", guard: "always" },
72
+ { from: "CREATE_BRANCH", to: "STOPPED", trigger: "stop", guard: "always" },
73
+ { from: "IMPLEMENT", to: "STOPPED", trigger: "stop", guard: "always" },
74
+ { from: "SELF_CHECK", to: "STOPPED", trigger: "stop", guard: "always" },
75
+ { from: "COMMIT_PUSH_PR", to: "STOPPED", trigger: "stop", guard: "always" },
76
+ { from: "WAIT_REVIEW_OR_CI", to: "STOPPED", trigger: "stop", guard: "always" },
77
+ { from: "FIX_REVIEW", to: "STOPPED", trigger: "stop", guard: "always" },
78
+ { from: "PUSH_FIX", to: "STOPPED", trigger: "stop", guard: "always" },
79
+ { from: "READY_TO_MERGE", to: "STOPPED", trigger: "stop", guard: "always" },
80
+ { from: "MERGE", to: "STOPPED", trigger: "stop", guard: "always" },
81
+ { from: "BLOCKED", to: "STOPPED", trigger: "stop", guard: "always" }
82
+ ];
83
+ function prLoopDefaultRoleForState(state) {
84
+ if (state === "WRITE_SPEC") {
85
+ return "planner";
86
+ }
87
+ if (state === "IMPLEMENT") {
88
+ return "implementation";
89
+ }
90
+ if (state === "FIX_REVIEW") {
91
+ return "review-fix";
92
+ }
93
+ if (state === "SELF_CHECK") {
94
+ return "reviewer";
95
+ }
96
+ return void 0;
97
+ }
98
+ var PR_LOOP_SHAPE = {
99
+ id: "pr-loop",
100
+ label: "PR Loop",
101
+ lifecycleKind: "pr",
102
+ initialState: "SYNC_MAIN",
103
+ states: PR_LOOP_STATES,
104
+ transitions: PR_LOOP_TRANSITIONS,
105
+ terminalStates: PR_LOOP_TERMINAL_STATES,
106
+ defaultRoleForState: prLoopDefaultRoleForState
107
+ };
108
+ var GENERIC_LOOP_STATES = [
109
+ "DEFINE_GOAL",
110
+ "COLLECT_CONTEXT",
111
+ "PLAN_WORK",
112
+ "EXECUTE_STEP",
113
+ "SELF_REVIEW",
114
+ "HUMAN_GATE",
115
+ "DELIVER",
116
+ "COMPLETE",
117
+ "BLOCKED",
118
+ "STOPPED"
119
+ ];
120
+ var GENERIC_LOOP_TERMINAL_STATES = ["COMPLETE", "BLOCKED", "STOPPED"];
121
+ var GENERIC_LOOP_TRANSITIONS = [
122
+ // Raised by a missing goal-confirmation decision before lifecycle returns a guard.
123
+ { from: "DEFINE_GOAL", to: "BLOCKED", trigger: "step", guard: "goal_unclear" },
124
+ { from: "DEFINE_GOAL", to: "COLLECT_CONTEXT", trigger: "step", guard: "goal_clear" },
125
+ { from: "DEFINE_GOAL", to: "PLAN_WORK", trigger: "step", guard: "skip_context" },
126
+ { from: "DEFINE_GOAL", to: "STOPPED", trigger: "step", guard: "rejected" },
127
+ { from: "COLLECT_CONTEXT", to: "PLAN_WORK", trigger: "step", guard: "always" },
128
+ { from: "PLAN_WORK", to: "EXECUTE_STEP", trigger: "step", guard: "always" },
129
+ { from: "EXECUTE_STEP", to: "BLOCKED", trigger: "step", guard: "scope_change_requested" },
130
+ { from: "EXECUTE_STEP", to: "PLAN_WORK", trigger: "step", guard: "scope_change_approved" },
131
+ { from: "EXECUTE_STEP", to: "SELF_REVIEW", trigger: "step", guard: "always" },
132
+ { from: "EXECUTE_STEP", to: "STOPPED", trigger: "step", guard: "rejected" },
133
+ { from: "SELF_REVIEW", to: "EXECUTE_STEP", trigger: "step", guard: "fix_needed_cycles_remain" },
134
+ { from: "SELF_REVIEW", to: "HUMAN_GATE", trigger: "step", guard: "review_passed" },
135
+ { from: "SELF_REVIEW", to: "HUMAN_GATE", trigger: "step", guard: "review_cycles_exhausted" },
136
+ { from: "HUMAN_GATE", to: "DELIVER", trigger: "step", guard: "deliverable_approved" },
137
+ { from: "HUMAN_GATE", to: "EXECUTE_STEP", trigger: "step", guard: "request_changes" },
138
+ { from: "HUMAN_GATE", to: "STOPPED", trigger: "step", guard: "rejected" },
139
+ { from: "DELIVER", to: "COMPLETE", trigger: "step", guard: "always" },
140
+ { from: "DEFINE_GOAL", to: "STOPPED", trigger: "stop", guard: "always" },
141
+ { from: "COLLECT_CONTEXT", to: "STOPPED", trigger: "stop", guard: "always" },
142
+ { from: "PLAN_WORK", to: "STOPPED", trigger: "stop", guard: "always" },
143
+ { from: "EXECUTE_STEP", to: "STOPPED", trigger: "stop", guard: "always" },
144
+ { from: "SELF_REVIEW", to: "STOPPED", trigger: "stop", guard: "always" },
145
+ { from: "HUMAN_GATE", to: "STOPPED", trigger: "stop", guard: "always" },
146
+ { from: "DELIVER", to: "STOPPED", trigger: "stop", guard: "always" },
147
+ { from: "BLOCKED", to: "STOPPED", trigger: "stop", guard: "always" }
148
+ ];
149
+ function genericLoopDefaultRoleForState(state) {
150
+ if (state === "DEFINE_GOAL" || state === "COLLECT_CONTEXT" || state === "PLAN_WORK") {
151
+ return "planner";
152
+ }
153
+ if (state === "EXECUTE_STEP" || state === "DELIVER") {
154
+ return "implementation";
155
+ }
156
+ if (state === "SELF_REVIEW") {
157
+ return "reviewer";
158
+ }
159
+ return void 0;
160
+ }
161
+ var GENERIC_LOOP_SHAPE = {
162
+ id: "generic-loop",
163
+ label: "Generic Loop",
164
+ lifecycleKind: "generic",
165
+ initialState: "DEFINE_GOAL",
166
+ states: GENERIC_LOOP_STATES,
167
+ transitions: GENERIC_LOOP_TRANSITIONS,
168
+ terminalStates: GENERIC_LOOP_TERMINAL_STATES,
169
+ defaultRoleForState: genericLoopDefaultRoleForState
170
+ };
171
+ var LOOP_SHAPES = {
172
+ "pr-loop": PR_LOOP_SHAPE,
173
+ "generic-loop": GENERIC_LOOP_SHAPE
174
+ };
175
+ function resolveLoopShape(id) {
176
+ if (id in LOOP_SHAPES) {
177
+ return LOOP_SHAPES[id];
178
+ }
179
+ throw new AgentLoopError("invalid_config", "Config loopShape is invalid.");
180
+ }
181
+ function loopShapeIds() {
182
+ return Object.keys(LOOP_SHAPES);
183
+ }
184
+ function sandboxForShapeState(shapeId, state, workerType) {
185
+ if (shapeId === "generic-loop" && ["DEFINE_GOAL", "COLLECT_CONTEXT", "PLAN_WORK", "SELF_REVIEW"].includes(state)) {
186
+ return "read-only";
187
+ }
188
+ return workerType === "reviewer" ? "read-only" : "workspace-write";
189
+ }
190
+
191
+ // plugins/autonomous-pr-loop/core/worker-prompts.ts
192
+ function workerSandbox(type) {
193
+ return type === "reviewer" ? "read-only" : "workspace-write";
194
+ }
195
+
196
+ // plugins/autonomous-pr-loop/core/profiles.ts
197
+ var WORKFLOW_PROFILE_IDS = [
198
+ "default_pr_loop",
199
+ "docs_only_loop",
200
+ "review_fix_loop",
201
+ "release_ready_loop",
202
+ "research_report_loop",
203
+ "document_preparation_loop",
204
+ "repo_hygiene_loop",
205
+ "weekly_review_loop",
206
+ "data_extraction_loop"
207
+ ];
208
+ var ROLE_PROFILE_IDS = ["default_pr_roles"];
209
+ var DEFAULT_LOOP_SHAPE_ID = "pr-loop";
210
+ var DEFAULT_WORKFLOW_PROFILE_ID = "default_pr_loop";
211
+ var DEFAULT_ROLE_PROFILE_ID = "default_pr_roles";
212
+ var DEFAULT_ROLE_PROFILE = {
213
+ id: "default_pr_roles",
214
+ label: "Default PR roles",
215
+ description: "Readable role aliases mapped onto the existing PR loop worker types.",
216
+ aliases: {
217
+ planner: {
218
+ id: "planner",
219
+ label: "Planner",
220
+ aliasFor: "planner",
221
+ description: "Plan the next PR scope and produce spec-level handoff.",
222
+ systemPrompt: "Plan narrowly, cite repository evidence, and hand off a scoped implementation target.",
223
+ scope: "workspace-write"
224
+ },
225
+ implementer: {
226
+ id: "implementer",
227
+ label: "Implementer",
228
+ aliasFor: "implementation",
229
+ description: "Implement the selected PR without owning Git or GitHub lifecycle actions.",
230
+ systemPrompt: "Implement only the selected PR scope. Keep changes small, tested, and ready for review.",
231
+ scope: "workspace-write"
232
+ },
233
+ reviewer: {
234
+ id: "reviewer",
235
+ label: "Reviewer",
236
+ aliasFor: "reviewer",
237
+ description: "Perform read-only self-review using repository evidence.",
238
+ systemPrompt: "Review read-only. Prioritize correctness, safety boundaries, and missing tests.",
239
+ scope: "read-only"
240
+ },
241
+ "review-fix": {
242
+ id: "review-fix",
243
+ label: "Review fix",
244
+ aliasFor: "review-fix",
245
+ description: "Fix scoped review findings and carry forward out-of-scope work.",
246
+ systemPrompt: "Address only scoped review findings. Record out-of-scope items as follow-ups.",
247
+ scope: "workspace-write"
248
+ },
249
+ "ci-fix": {
250
+ id: "ci-fix",
251
+ label: "CI fix",
252
+ aliasFor: "ci-fix",
253
+ description: "Fix failing checks without expanding feature scope.",
254
+ systemPrompt: "Focus on test and CI failures. Avoid unrelated refactors.",
255
+ scope: "workspace-write"
256
+ },
257
+ "release-manager": {
258
+ id: "release-manager",
259
+ label: "Release manager",
260
+ aliasFor: "reviewer",
261
+ description: "Display-only release readiness posture; not an executable worker in PR L.",
262
+ systemPrompt: "Summarize release readiness. Do not execute as a worker.",
263
+ scope: "read-only"
264
+ }
265
+ }
266
+ };
267
+ var WORKFLOW_PROFILES = {
268
+ default_pr_loop: {
269
+ id: "default_pr_loop",
270
+ label: "Default PR loop",
271
+ description: "The HOLO-Codex PR delivery behavior with explicit profile audit.",
272
+ loopShape: "pr-loop",
273
+ shapeConfig: { roleOverrides: {} },
274
+ configOverrides: {},
275
+ validationPosture: "Use configured lint, tests, GitNexus, CI, and review gates.",
276
+ likelyGates: ["ambiguous_next_pr", "worker_failed", "ci_required_checks_missing", "merge_requires_confirmation"],
277
+ handoffTemplate: "Follow the selected PR spec and hand off concise evidence to the next role.",
278
+ autonomyBoundary: "Autonomous until configured gates, policy violations, CI/review blockers, or unsafe git actions."
279
+ },
280
+ docs_only_loop: {
281
+ id: "docs_only_loop",
282
+ label: "Docs-only loop",
283
+ description: "Bias validation toward documentation consistency while preserving policy and configured checks.",
284
+ loopShape: "pr-loop",
285
+ shapeConfig: { roleOverrides: {} },
286
+ configOverrides: { maxCiReruns: 0 },
287
+ validationPosture: "Prefer docs consistency checks; if code or config changes, existing tests and policy still decide.",
288
+ likelyGates: ["ambiguous_next_pr", "worker_failed", "policy_violation"],
289
+ handoffTemplate: "Call out docs touched, references updated, and any code/config spillover.",
290
+ autonomyBoundary: "Docs-focused autonomy; policy guards and explicit configured checks remain authoritative."
291
+ },
292
+ review_fix_loop: {
293
+ id: "review_fix_loop",
294
+ label: "Review-fix loop",
295
+ description: "Focus on scoped PR review repair and carryover discipline.",
296
+ loopShape: "pr-loop",
297
+ shapeConfig: { roleOverrides: {} },
298
+ configOverrides: { maxCiReruns: 0 },
299
+ validationPosture: "Prioritize review comments, scoped fixes, and targeted validation.",
300
+ likelyGates: ["review_out_of_scope", "worker_failed", "ci_pending_timeout"],
301
+ handoffTemplate: "Summarize handled findings, unresolved carryover, and validation evidence.",
302
+ autonomyBoundary: "Repair only review-scoped issues; defer unrelated requests through carryover."
303
+ },
304
+ release_ready_loop: {
305
+ id: "release_ready_loop",
306
+ label: "Release-ready loop",
307
+ description: "Tighten merge readiness explanation without adding a release-manager worker.",
308
+ loopShape: "pr-loop",
309
+ shapeConfig: { roleOverrides: {} },
310
+ configOverrides: { autonomyMode: "supervised" },
311
+ validationPosture: "Favor readiness evidence, review status, CI status, and explicit merge confirmation.",
312
+ likelyGates: ["merge_requires_confirmation", "ci_required_checks_missing", "github_transient_failure"],
313
+ handoffTemplate: "List readiness evidence, missing conditions, and any merge risk.",
314
+ autonomyBoundary: "Supervised release posture; merge-related actions require visible confirmation."
315
+ },
316
+ research_report_loop: {
317
+ id: "research_report_loop",
318
+ label: "Research report loop",
319
+ description: "Collect evidence, draft a report, review it, and request human approval before delivery.",
320
+ loopShape: "generic-loop",
321
+ shapeConfig: { roleOverrides: {} },
322
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
323
+ validationPosture: "Require cited evidence, a coherent report artifact, and human approval before delivery.",
324
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
325
+ handoffTemplate: "Summarize research question, sources checked, claims supported, gaps, and deliverable path.",
326
+ autonomyBoundary: "Autonomous for research and drafting inside allowed write roots; final delivery waits for human approval.",
327
+ expectedDeliverable: "Markdown research report",
328
+ allowedWriteRoots: ["docs", "reports"],
329
+ requiredEvidence: ["source list", "claim-to-evidence summary", "known gaps"],
330
+ reviewChecklist: ["Claims cite evidence", "Uncertainty is explicit", "No raw secrets or prompt content included"],
331
+ maxExecutionReviewCycles: 3
332
+ },
333
+ document_preparation_loop: {
334
+ id: "document_preparation_loop",
335
+ label: "Document preparation loop",
336
+ description: "Prepare a structured document from provided context and deliver it after review.",
337
+ loopShape: "generic-loop",
338
+ shapeConfig: { roleOverrides: {} },
339
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
340
+ validationPosture: "Validate structure, completeness, and requested format before human approval.",
341
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
342
+ handoffTemplate: "List document purpose, audience, sections prepared, missing input, and deliverable path.",
343
+ autonomyBoundary: "May write only document artifacts under allowed roots; final delivery waits for human approval.",
344
+ expectedDeliverable: "Prepared Markdown document",
345
+ allowedWriteRoots: ["docs", "reports"],
346
+ requiredEvidence: ["source context summary", "document outline", "completion checklist"],
347
+ reviewChecklist: ["Audience and format match the goal", "Sections are complete", "No unsupported claims"],
348
+ maxExecutionReviewCycles: 3
349
+ },
350
+ repo_hygiene_loop: {
351
+ id: "repo_hygiene_loop",
352
+ label: "Repo hygiene loop",
353
+ description: "Audit repository hygiene and produce a scoped report or safe cleanup artifact.",
354
+ loopShape: "generic-loop",
355
+ shapeConfig: { roleOverrides: {} },
356
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
357
+ validationPosture: "Prefer read-only audit; write only report artifacts unless the goal explicitly asks for safe cleanup.",
358
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "policy_violation"],
359
+ handoffTemplate: "List inspected areas, hygiene findings, safe fixes, deferred risks, and deliverable path.",
360
+ autonomyBoundary: "Repository inspection is read-only by default; write actions are limited to allowed report roots.",
361
+ expectedDeliverable: "Repo hygiene audit report",
362
+ allowedWriteRoots: ["docs", "reports"],
363
+ requiredEvidence: ["checked files/commands", "finding severity", "recommended action"],
364
+ reviewChecklist: ["No destructive commands", "Findings have repo evidence", "Out-of-scope cleanup is deferred"],
365
+ maxExecutionReviewCycles: 2
366
+ },
367
+ weekly_review_loop: {
368
+ id: "weekly_review_loop",
369
+ label: "Weekly review loop",
370
+ description: "Collect activity signals, summarize progress, and deliver a weekly review artifact.",
371
+ loopShape: "generic-loop",
372
+ shapeConfig: { roleOverrides: {} },
373
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
374
+ validationPosture: "Require summarized evidence and a concise deliverable suitable for human review.",
375
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
376
+ handoffTemplate: "List timeframe, activity sources, decisions, blockers, follow-ups, and deliverable path.",
377
+ autonomyBoundary: "May summarize local/repo facts and write review artifacts; final delivery waits for approval.",
378
+ expectedDeliverable: "Weekly review Markdown summary",
379
+ allowedWriteRoots: ["docs", "reports"],
380
+ requiredEvidence: ["timeframe", "activity source summary", "follow-up list"],
381
+ reviewChecklist: ["Timeframe is explicit", "Actions are separated from FYI", "No private raw logs included"],
382
+ maxExecutionReviewCycles: 2
383
+ },
384
+ data_extraction_loop: {
385
+ id: "data_extraction_loop",
386
+ label: "Data extraction loop",
387
+ description: "Extract structured data into an auditable artifact, then wait for human approval.",
388
+ loopShape: "generic-loop",
389
+ shapeConfig: { roleOverrides: {} },
390
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
391
+ validationPosture: "Require extraction criteria, sample validation, and artifact metadata before delivery.",
392
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
393
+ handoffTemplate: "List extraction target, criteria, row/item count, validation sample, and deliverable path.",
394
+ autonomyBoundary: "May write extracted artifacts only under allowed roots; no external side effects.",
395
+ expectedDeliverable: "Structured extraction artifact",
396
+ allowedWriteRoots: ["docs", "reports", "data"],
397
+ requiredEvidence: ["extraction criteria", "sample validation", "count summary"],
398
+ reviewChecklist: ["Schema is documented", "Sample rows match source", "Secrets are redacted"],
399
+ maxExecutionReviewCycles: 3
400
+ }
401
+ };
402
+ function resolveProfile(config, currentState) {
403
+ const shape = resolveLoopShape(config.loopShape);
404
+ const workflow = workflowProfile(config.workflowProfile);
405
+ const roleProfile = roleProfileById(config.roleProfile);
406
+ if (workflow.loopShape !== shape.id) {
407
+ throw new AgentLoopError("invalid_config", "Workflow profile loopShape does not match config loopShape.");
408
+ }
409
+ validateRoleProfile(roleProfile);
410
+ validateWorkflowProfile(workflow, roleProfile);
411
+ const roleMapping = shape.states.map((state) => roleMappingForState(state, workflow, roleProfile)).filter((item) => item !== void 0);
412
+ const currentRole = currentState ? roleMappingForState(currentState, workflow, roleProfile) : void 0;
413
+ return {
414
+ loopShape: shape.id,
415
+ workflowProfile: workflow.id,
416
+ workflowLabel: workflow.label,
417
+ workflowDescription: workflow.description,
418
+ roleProfile: roleProfile.id,
419
+ lifecycleKind: shape.lifecycleKind,
420
+ ...workflow.expectedDeliverable ? { expectedDeliverable: workflow.expectedDeliverable } : {},
421
+ ...workflow.allowedWriteRoots ? { allowedWriteRoots: workflow.allowedWriteRoots } : {},
422
+ ...currentRole ? { currentRole } : {},
423
+ roleMapping,
424
+ autonomyBoundary: workflow.autonomyBoundary,
425
+ handoffSummary: workflow.handoffTemplate,
426
+ validationPosture: workflow.validationPosture,
427
+ likelyGates: workflow.likelyGates,
428
+ availableWorkflows: Object.values(WORKFLOW_PROFILES).map((item) => ({
429
+ id: item.id,
430
+ label: item.label,
431
+ description: item.description
432
+ })),
433
+ availableRoleProfiles: [{
434
+ id: roleProfile.id,
435
+ label: roleProfile.label,
436
+ description: roleProfile.description
437
+ }]
438
+ };
439
+ }
440
+ function workflowProfile(id) {
441
+ const profile = WORKFLOW_PROFILES[id];
442
+ if (!profile) {
443
+ throw new AgentLoopError("invalid_config", "Config workflowProfile is invalid.");
444
+ }
445
+ return profile;
446
+ }
447
+ function roleProfileById(id) {
448
+ if (id !== DEFAULT_ROLE_PROFILE.id) {
449
+ throw new AgentLoopError("invalid_config", "Config roleProfile is invalid.");
450
+ }
451
+ return DEFAULT_ROLE_PROFILE;
452
+ }
453
+ function validateRoleProfile(profile) {
454
+ for (const alias of Object.values(profile.aliases)) {
455
+ if (alias.id === "release-manager") {
456
+ continue;
457
+ }
458
+ if (alias.scope !== workerSandbox(alias.aliasFor)) {
459
+ throw new AgentLoopError("invalid_config", "Role profile scope cannot change worker sandbox.", {
460
+ details: { role: alias.id, aliasFor: alias.aliasFor, scope: alias.scope, sandbox: workerSandbox(alias.aliasFor) }
461
+ });
462
+ }
463
+ }
464
+ }
465
+ function validateWorkflowProfile(workflow, profile) {
466
+ const shape = resolveLoopShape(workflow.loopShape);
467
+ for (const [state, roleAlias] of Object.entries(workflow.shapeConfig.roleOverrides)) {
468
+ if (!shape.states.includes(state)) {
469
+ throw new AgentLoopError("invalid_config", "Workflow profile references an unknown state.", { details: { state } });
470
+ }
471
+ if (roleAlias === "release-manager") {
472
+ throw new AgentLoopError("invalid_config", "release-manager is display-only and cannot be used as an executable role.");
473
+ }
474
+ const role = profile.aliases[roleAlias ?? ""];
475
+ const defaultRole = shape.defaultRoleForState(state);
476
+ if (!role || role.aliasFor !== defaultRole) {
477
+ throw new AgentLoopError("invalid_config", "Workflow role override cannot change the state's worker sandbox.", {
478
+ details: { state, roleAlias, aliasFor: role?.aliasFor, defaultRole }
479
+ });
480
+ }
481
+ }
482
+ }
483
+ function roleMappingForState(state, workflow, profile) {
484
+ const shape = resolveLoopShape(workflow.loopShape);
485
+ const workerType = shape.defaultRoleForState(state);
486
+ if (!workerType) return void 0;
487
+ const aliasId = workflow.shapeConfig.roleOverrides[state] ?? defaultAliasFor(workerType);
488
+ const role = profile.aliases[aliasId];
489
+ if (!role || role.aliasFor !== workerType) {
490
+ throw new AgentLoopError("invalid_config", "Could not resolve role mapping for workflow profile.", { details: { state, aliasId, workerType } });
491
+ }
492
+ return {
493
+ state,
494
+ alias: role.id,
495
+ workerType,
496
+ label: role.label,
497
+ sandbox: sandboxForShapeState(workflow.loopShape, state, workerType)
498
+ };
499
+ }
500
+ function defaultAliasFor(workerType) {
501
+ const aliases = {
502
+ planner: "planner",
503
+ implementation: "implementer",
504
+ reviewer: "reviewer",
505
+ "review-fix": "review-fix",
506
+ "ci-fix": "ci-fix"
507
+ };
508
+ return aliases[workerType];
509
+ }
510
+
511
+ // plugins/autonomous-pr-loop/core/config.ts
512
+ var CONFIG_DIR = ".agent-loop";
513
+ var CONFIG_FILE = "config.json";
514
+ var DEFAULT_PROTECTED_PATHS = [
515
+ ".git/**",
516
+ ".agent-loop/**",
517
+ ".claude/**",
518
+ "AGENTS.md",
519
+ "CLAUDE.md",
520
+ ".env*",
521
+ "**/*secret*"
522
+ ];
523
+ var AUTONOMY_MODES = ["supervised", "autonomous_until_gate", "autonomous_until_terminal"];
524
+ var MERGE_MODES = ["manual", "conditional", "disabled"];
525
+ var NOTIFY_MODES = ["all_gates", "important_only", "blockers_only"];
526
+ var WORKER_BACKENDS = ["codex-exec", "codex-app-server"];
527
+ var REVIEW_HANDLING_MODES = [
528
+ "fix_scoped_and_carry_forward",
529
+ "ask_on_any_review",
530
+ "require_zero_open_findings"
531
+ ];
532
+ function configPath(repoRoot2) {
533
+ return join(repoRoot2, CONFIG_DIR, CONFIG_FILE);
534
+ }
535
+ function statePath(repoRoot2) {
536
+ return join(repoRoot2, CONFIG_DIR, "state.sqlite");
537
+ }
538
+ function withConfigDefaults(input2) {
539
+ const mergeMode = input2.mergeMode ?? (input2.allowAutoMerge ? "conditional" : "manual");
540
+ return {
541
+ repoId: input2.repoId,
542
+ locale: input2.locale ?? DEFAULT_LOCALE,
543
+ loopShape: input2.loopShape ?? DEFAULT_LOOP_SHAPE_ID,
544
+ workflowProfile: input2.workflowProfile ?? DEFAULT_WORKFLOW_PROFILE_ID,
545
+ roleProfile: input2.roleProfile ?? DEFAULT_ROLE_PROFILE_ID,
546
+ baseBranch: input2.baseBranch ?? "main",
547
+ branchPrefix: input2.branchPrefix ?? "codex/",
548
+ plansDir: input2.plansDir ?? "docs/plans",
549
+ ...input2.lintCommand ? { lintCommand: input2.lintCommand } : {},
550
+ ...input2.testCommand ? { testCommand: input2.testCommand } : {},
551
+ ...input2.gitnexusRepo ? { gitnexusRepo: input2.gitnexusRepo } : {},
552
+ gitnexusRequired: input2.gitnexusRequired ?? true,
553
+ requiredChecks: input2.requiredChecks ?? [],
554
+ requireReviewApproval: input2.requireReviewApproval ?? true,
555
+ autonomyMode: input2.autonomyMode ?? "autonomous_until_gate",
556
+ mergeMode,
557
+ notifyMode: input2.notifyMode ?? "important_only",
558
+ reviewHandling: input2.reviewHandling ?? "fix_scoped_and_carry_forward",
559
+ ...input2.carryoverTarget ? { carryoverTarget: input2.carryoverTarget } : {},
560
+ allowAutoMerge: mergeMode === "conditional",
561
+ maxReviewFixRounds: input2.maxReviewFixRounds ?? 3,
562
+ maxTestFixRounds: input2.maxTestFixRounds ?? 2,
563
+ maxCiReruns: input2.maxCiReruns ?? 1,
564
+ commandTimeoutMs: input2.commandTimeoutMs ?? 6e5,
565
+ commandOutputLimitBytes: input2.commandOutputLimitBytes ?? 65536,
566
+ githubRetryMaxAttempts: input2.githubRetryMaxAttempts ?? 3,
567
+ githubRetryBaseDelayMs: input2.githubRetryBaseDelayMs ?? 1e3,
568
+ reviewCiPollIntervalMs: input2.reviewCiPollIntervalMs ?? 3e4,
569
+ reviewCiMaxWaitMs: input2.reviewCiMaxWaitMs ?? 18e5,
570
+ workerBackend: input2.workerBackend ?? "codex-exec",
571
+ workerTimeoutMs: input2.workerTimeoutMs ?? 18e5,
572
+ workerMaxRetries: input2.workerMaxRetries ?? 1,
573
+ workerEphemeral: input2.workerEphemeral ?? false,
574
+ protectedPaths: input2.protectedPaths ?? DEFAULT_PROTECTED_PATHS,
575
+ ...input2.dashboard ? { dashboard: input2.dashboard } : {}
576
+ };
577
+ }
578
+ function loadConfig(repoRoot2) {
579
+ const path = configPath(repoRoot2);
580
+ if (!existsSync(path)) {
581
+ throw new AgentLoopError(
582
+ "needs_repo_init",
583
+ "Missing .agent-loop/config.json. Run `pnpm agent-loop init`.",
584
+ { details: { path }, exitCode: 2 }
585
+ );
586
+ }
587
+ let parsed;
588
+ try {
589
+ parsed = JSON.parse(readFileSync(path, "utf8"));
590
+ } catch (error) {
591
+ throw new AgentLoopError("invalid_config", "Config is not valid JSON.", {
592
+ details: { path, cause: error instanceof Error ? error.message : String(error) }
593
+ });
594
+ }
595
+ const config = validateConfig(parsed);
596
+ return { path, config };
597
+ }
598
+ function validateConfig(value) {
599
+ if (!isRecord(value)) {
600
+ throw new AgentLoopError("invalid_config", "Config must be a JSON object.");
601
+ }
602
+ assertKnownTopLevelKeys(value);
603
+ if (typeof value.repoId !== "string" || value.repoId.length === 0) {
604
+ throw new AgentLoopError("invalid_config", "Config repoId is required.");
605
+ }
606
+ const config = withConfigDefaults(value);
607
+ const stringFields = ["baseBranch", "branchPrefix", "plansDir"];
608
+ for (const field of stringFields) {
609
+ if (typeof config[field] !== "string" || config[field].length === 0) {
610
+ throw new AgentLoopError("invalid_config", `Config ${field} must be a non-empty string.`);
611
+ }
612
+ }
613
+ const optionalStrings = ["lintCommand", "testCommand", "gitnexusRepo"];
614
+ for (const field of optionalStrings) {
615
+ if (config[field] !== void 0 && typeof config[field] !== "string") {
616
+ throw new AgentLoopError("invalid_config", `Config ${field} must be a string.`);
617
+ }
618
+ }
619
+ if (!WORKER_BACKENDS.includes(config.workerBackend)) {
620
+ throw new AgentLoopError("invalid_config", "Config workerBackend is invalid.");
621
+ }
622
+ if (!AUTONOMY_MODES.includes(config.autonomyMode)) {
623
+ throw new AgentLoopError("invalid_config", "Config autonomyMode is invalid.");
624
+ }
625
+ if (!MERGE_MODES.includes(config.mergeMode)) {
626
+ throw new AgentLoopError("invalid_config", "Config mergeMode is invalid.");
627
+ }
628
+ if (!NOTIFY_MODES.includes(config.notifyMode)) {
629
+ throw new AgentLoopError("invalid_config", "Config notifyMode is invalid.");
630
+ }
631
+ if (!REVIEW_HANDLING_MODES.includes(config.reviewHandling)) {
632
+ throw new AgentLoopError("invalid_config", "Config reviewHandling is invalid.");
633
+ }
634
+ if (!LOCALE_SETTINGS.includes(config.locale)) {
635
+ throw new AgentLoopError("invalid_config", "Config locale is invalid.");
636
+ }
637
+ if (!loopShapeIds().includes(config.loopShape)) {
638
+ throw new AgentLoopError("invalid_config", "Config loopShape is invalid.");
639
+ }
640
+ if (!WORKFLOW_PROFILE_IDS.includes(config.workflowProfile)) {
641
+ throw new AgentLoopError("invalid_config", "Config workflowProfile is invalid.");
642
+ }
643
+ if (!ROLE_PROFILE_IDS.includes(config.roleProfile)) {
644
+ throw new AgentLoopError("invalid_config", "Config roleProfile is invalid.");
645
+ }
646
+ resolveProfile(config);
647
+ if (config.carryoverTarget !== void 0 && typeof config.carryoverTarget !== "string") {
648
+ throw new AgentLoopError("invalid_config", "Config carryoverTarget must be a string.");
649
+ }
650
+ const booleans = ["gitnexusRequired", "requireReviewApproval", "allowAutoMerge", "workerEphemeral"];
651
+ for (const field of booleans) {
652
+ if (typeof config[field] !== "boolean") {
653
+ throw new AgentLoopError("invalid_config", `Config ${field} must be a boolean.`);
654
+ }
655
+ }
656
+ const numbers = ["maxReviewFixRounds", "maxTestFixRounds", "maxCiReruns", "workerMaxRetries"];
657
+ for (const field of numbers) {
658
+ if (!Number.isInteger(config[field]) || config[field] < 0) {
659
+ throw new AgentLoopError("invalid_config", `Config ${field} must be a non-negative integer.`);
660
+ }
661
+ }
662
+ const positiveNumbers = [
663
+ "commandTimeoutMs",
664
+ "commandOutputLimitBytes",
665
+ "githubRetryMaxAttempts",
666
+ "githubRetryBaseDelayMs",
667
+ "reviewCiPollIntervalMs",
668
+ "reviewCiMaxWaitMs",
669
+ "workerTimeoutMs"
670
+ ];
671
+ for (const field of positiveNumbers) {
672
+ if (!Number.isInteger(config[field]) || config[field] < 1) {
673
+ throw new AgentLoopError("invalid_config", `Config ${field} must be a positive integer.`);
674
+ }
675
+ }
676
+ if (!Array.isArray(config.requiredChecks) || !config.requiredChecks.every(isString)) {
677
+ throw new AgentLoopError("invalid_config", "Config requiredChecks must be a string array.");
678
+ }
679
+ if (!Array.isArray(config.protectedPaths) || !config.protectedPaths.every(isString)) {
680
+ throw new AgentLoopError("invalid_config", "Config protectedPaths must be a string array.");
681
+ }
682
+ if (config.dashboard) {
683
+ assertKnownDashboardKeys(config.dashboard);
684
+ if (typeof config.dashboard.enabled !== "boolean" || typeof config.dashboard.host !== "string" || config.dashboard.host.length === 0) {
685
+ throw new AgentLoopError("invalid_config", "Config dashboard is invalid.");
686
+ }
687
+ if (config.dashboard.port !== void 0 && (!Number.isInteger(config.dashboard.port) || config.dashboard.port < 1 || config.dashboard.port > 65535)) {
688
+ throw new AgentLoopError("invalid_config", "Config dashboard.port is invalid.");
689
+ }
690
+ }
691
+ return config;
692
+ }
693
+ function assertKnownTopLevelKeys(value) {
694
+ const allowed = /* @__PURE__ */ new Set([
695
+ "repoId",
696
+ "locale",
697
+ "loopShape",
698
+ "workflowProfile",
699
+ "roleProfile",
700
+ "baseBranch",
701
+ "branchPrefix",
702
+ "plansDir",
703
+ "lintCommand",
704
+ "testCommand",
705
+ "gitnexusRepo",
706
+ "gitnexusRequired",
707
+ "requiredChecks",
708
+ "requireReviewApproval",
709
+ "autonomyMode",
710
+ "mergeMode",
711
+ "notifyMode",
712
+ "reviewHandling",
713
+ "carryoverTarget",
714
+ "allowAutoMerge",
715
+ "maxReviewFixRounds",
716
+ "maxTestFixRounds",
717
+ "maxCiReruns",
718
+ "commandTimeoutMs",
719
+ "commandOutputLimitBytes",
720
+ "githubRetryMaxAttempts",
721
+ "githubRetryBaseDelayMs",
722
+ "reviewCiPollIntervalMs",
723
+ "reviewCiMaxWaitMs",
724
+ "workerBackend",
725
+ "workerTimeoutMs",
726
+ "workerMaxRetries",
727
+ "workerEphemeral",
728
+ "protectedPaths",
729
+ "dashboard"
730
+ ]);
731
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
732
+ if (unknown.length > 0) {
733
+ throw new AgentLoopError("invalid_config", "Config contains unknown fields.", {
734
+ details: { fields: unknown }
735
+ });
736
+ }
737
+ }
738
+ function assertKnownDashboardKeys(value) {
739
+ const allowed = /* @__PURE__ */ new Set(["enabled", "host", "port"]);
740
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
741
+ if (unknown.length > 0) {
742
+ throw new AgentLoopError("invalid_config", "Config dashboard contains unknown fields.", {
743
+ details: { fields: unknown }
744
+ });
745
+ }
746
+ }
747
+ function isRecord(value) {
748
+ return typeof value === "object" && value !== null && !Array.isArray(value);
749
+ }
750
+ function isString(value) {
751
+ return typeof value === "string";
752
+ }
753
+
754
+ // plugins/autonomous-pr-loop/core/hook-events.ts
755
+ var CODEX_HOOK_EVENTS = [
756
+ "PreToolUse",
757
+ "PostToolUse",
758
+ "UserPromptSubmit",
759
+ "Stop",
760
+ "SessionStart",
761
+ "PreCompact",
762
+ "PostCompact",
763
+ "PermissionRequest"
764
+ ];
765
+ var OBSERVE_ONLY_HOOK_EVENTS = CODEX_HOOK_EVENTS.filter((event) => event !== "PreToolUse");
766
+ function hookEventKind(event) {
767
+ return `hook_${event.replaceAll(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()}`;
768
+ }
769
+
770
+ // plugins/autonomous-pr-loop/core/hook-router.ts
771
+ import { createHash, randomUUID } from "node:crypto";
772
+ import { execFileSync } from "node:child_process";
773
+ import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync2, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
774
+ import { homedir } from "node:os";
775
+ import { dirname, isAbsolute, join as join2, resolve } from "node:path";
776
+ function hookRegistryPath(codexHome = codexHomePath()) {
777
+ return join2(codexHome, "agent-loop", "hook-bindings.json");
778
+ }
779
+ function hookRegistryLockPath(codexHome = codexHomePath()) {
780
+ return `${hookRegistryPath(codexHome)}.lock`;
781
+ }
782
+ function codexHomePath() {
783
+ return process.env.CODEX_HOME ?? join2(homedir(), ".codex");
784
+ }
785
+ function resolveHookRoute(payload, options = {}) {
786
+ const context = hookContextFromPayload(payload, options.legacyRepoRoot);
787
+ let registry;
788
+ try {
789
+ registry = readRegistry(options.codexHome ?? codexHomePath());
790
+ } catch (error) {
791
+ return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
792
+ }
793
+ try {
794
+ const active = registry.bindings.filter((binding) => binding.status === "active");
795
+ const worktreeMatches = active.filter((binding) => bindingMatchesContext(binding, context));
796
+ const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
797
+ const sessionMatches = context.sessionId ? worktreeMatches.filter((binding) => binding.sessionIdHash === contextSessionHash) : [];
798
+ const candidates = sessionMatches.length > 0 ? sessionMatches : worktreeMatches.filter((binding) => binding.sessionIdHash === void 0);
799
+ if (candidates.length === 1) {
800
+ const binding = touchBinding(candidates[0], context, options.codexHome);
801
+ if (contextSessionHash && binding.sessionIdHash !== void 0 && binding.sessionIdHash !== contextSessionHash) {
802
+ return { status: "no_match", context, reason: "Hook binding was claimed by another Codex session.", worktreeBinding: true };
803
+ }
804
+ return { status: "matched", binding, context, legacy: false };
805
+ }
806
+ if (candidates.length > 1) {
807
+ return { status: "ambiguous", context, bindings: candidates, reason: "Multiple hook bindings match this Codex session context." };
808
+ }
809
+ if (worktreeMatches.length > 0) {
810
+ return { status: "no_match", context, reason: "Active hook bindings exist for this worktree, but none match this Codex session.", worktreeBinding: true };
811
+ }
812
+ const legacy = legacyRoute(options.legacyRepoRoot, context);
813
+ if (legacy) {
814
+ return { status: "matched", binding: legacy, context, legacy: true };
815
+ }
816
+ return { status: "no_match", context, reason: "No active agent-loop hook binding matches this Codex session context." };
817
+ } catch (error) {
818
+ return { status: "route_error", context, reason: error instanceof Error ? error.message : String(error) };
819
+ }
820
+ }
821
+ function hookContextFromPayload(payload, fallbackCwd = process.cwd()) {
822
+ const record = isRecord(payload) ? payload : {};
823
+ return resolveHookContext({
824
+ cwd: stringValue(record.cwd) ?? fallbackCwd,
825
+ sessionId: stringValue(record.session_id) ?? stringValue(record.sessionId),
826
+ turnId: stringValue(record.turn_id) ?? stringValue(record.turnId),
827
+ transcriptPath: stringValue(record.transcript_path) ?? stringValue(record.transcriptPath)
828
+ });
829
+ }
830
+ function resolveHookContext(input2) {
831
+ const cwd = canonicalPath(input2.cwd);
832
+ const worktreeRoot = gitOutput(["rev-parse", "--show-toplevel"], cwd);
833
+ const commonDir = gitOutput(["rev-parse", "--git-common-dir"], cwd);
834
+ const branch = gitOutput(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
835
+ const commonPath = commonDir ? canonicalPath(isAbsolute(commonDir) ? commonDir : join2(cwd, commonDir)) : void 0;
836
+ return {
837
+ cwd,
838
+ worktreeRoot: worktreeRoot ? canonicalPath(worktreeRoot) : cwd,
839
+ ...commonPath ? { gitCommonDir: commonPath } : {},
840
+ ...branch && branch !== "HEAD" ? { branch } : {},
841
+ ...input2.sessionId ? { sessionId: input2.sessionId } : {},
842
+ ...input2.turnId ? { turnId: input2.turnId } : {},
843
+ ...input2.transcriptPath ? { transcriptPathSha256: sha256(input2.transcriptPath) } : {}
844
+ };
845
+ }
846
+ function readRegistry(codexHome) {
847
+ const path = hookRegistryPath(codexHome);
848
+ if (!existsSync2(path)) {
849
+ return { version: 1, bindings: [] };
850
+ }
851
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
852
+ if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.bindings)) {
853
+ throw new Error(`Invalid hook binding registry: expected { version: 1, bindings: [...] } in ${path}`);
854
+ }
855
+ const bindings = parsed.bindings.map(parseBinding);
856
+ const invalid = bindings.findIndex((binding) => binding === void 0);
857
+ if (invalid >= 0) {
858
+ throw new Error(`Invalid hook binding registry: invalid binding at index ${invalid} in ${path}`);
859
+ }
860
+ return {
861
+ version: 1,
862
+ bindings: bindings.filter((binding) => binding !== void 0)
863
+ };
864
+ }
865
+ function writeRegistry(registry, codexHome) {
866
+ const path = hookRegistryPath(codexHome);
867
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
868
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
869
+ writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}
870
+ `, { mode: 384 });
871
+ renameSync(tmp, path);
872
+ }
873
+ function parseBinding(value) {
874
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.repoRoot !== "string" || typeof value.worktreeRoot !== "string") {
875
+ return void 0;
876
+ }
877
+ const status = value.status === "stale" || value.status === "disabled" ? value.status : "active";
878
+ if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
879
+ return void 0;
880
+ }
881
+ return {
882
+ id: value.id,
883
+ repoRoot: value.repoRoot,
884
+ worktreeRoot: value.worktreeRoot,
885
+ ...typeof value.gitCommonDir === "string" ? { gitCommonDir: value.gitCommonDir } : {},
886
+ ...typeof value.branch === "string" ? { branch: value.branch } : {},
887
+ ...typeof value.runId === "string" ? { runId: value.runId } : {},
888
+ ...typeof value.sessionIdHash === "string" ? { sessionIdHash: value.sessionIdHash } : typeof value.sessionId === "string" ? { sessionIdHash: sha256(value.sessionId) } : {},
889
+ ...typeof value.transcriptPathSha256 === "string" ? { transcriptPathSha256: value.transcriptPathSha256 } : {},
890
+ status,
891
+ createdAt: value.createdAt,
892
+ updatedAt: value.updatedAt,
893
+ ...typeof value.lastSeenAt === "string" ? { lastSeenAt: value.lastSeenAt } : {}
894
+ };
895
+ }
896
+ function touchBinding(binding, context, codexHome = codexHomePath()) {
897
+ return withRegistryLock(codexHome, () => {
898
+ const registry = readRegistry(codexHome);
899
+ const current = registry.bindings.find((item) => item.id === binding.id) ?? binding;
900
+ const contextSessionHash = context.sessionId ? sha256(context.sessionId) : void 0;
901
+ if (current.sessionIdHash !== void 0 && contextSessionHash !== void 0 && current.sessionIdHash !== contextSessionHash) {
902
+ return current;
903
+ }
904
+ const nowMs = Date.now();
905
+ const shouldClaimSession = current.sessionIdHash === void 0 && contextSessionHash !== void 0;
906
+ const shouldClaimTranscript = current.transcriptPathSha256 === void 0 && context.transcriptPathSha256 !== void 0;
907
+ const lastSeenAtMs = current.lastSeenAt ? Date.parse(current.lastSeenAt) : 0;
908
+ const shouldRefreshLastSeen = !Number.isFinite(lastSeenAtMs) || nowMs - lastSeenAtMs > TOUCH_REFRESH_MS;
909
+ if (!shouldClaimSession && !shouldClaimTranscript && !shouldRefreshLastSeen) {
910
+ return current;
911
+ }
912
+ const now2 = new Date(nowMs).toISOString();
913
+ const updated = {
914
+ ...current,
915
+ ...shouldClaimSession ? { sessionIdHash: contextSessionHash } : {},
916
+ ...shouldClaimTranscript ? { transcriptPathSha256: context.transcriptPathSha256 } : {},
917
+ lastSeenAt: now2,
918
+ updatedAt: now2
919
+ };
920
+ registry.bindings = registry.bindings.map((item) => item.id === current.id ? updated : item);
921
+ writeRegistry(registry, codexHome);
922
+ return updated;
923
+ });
924
+ }
925
+ function legacyRoute(legacyRepoRoot, context) {
926
+ if (!legacyRepoRoot) return void 0;
927
+ const legacyContext = resolveHookContext({ cwd: legacyRepoRoot });
928
+ if (legacyContext.worktreeRoot !== context.worktreeRoot) {
929
+ return void 0;
930
+ }
931
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
932
+ return {
933
+ id: `legacy:${sha256(legacyContext.worktreeRoot).slice(0, 16)}`,
934
+ repoRoot: canonicalPath(legacyRepoRoot),
935
+ worktreeRoot: legacyContext.worktreeRoot,
936
+ ...legacyContext.gitCommonDir ? { gitCommonDir: legacyContext.gitCommonDir } : {},
937
+ ...legacyContext.branch ? { branch: legacyContext.branch } : {},
938
+ status: "active",
939
+ createdAt: now2,
940
+ updatedAt: now2
941
+ };
942
+ }
943
+ function bindingMatchesContext(binding, context) {
944
+ if (binding.worktreeRoot === context.worktreeRoot) {
945
+ return true;
946
+ }
947
+ return binding.gitCommonDir !== void 0 && context.gitCommonDir !== void 0 && binding.gitCommonDir === context.gitCommonDir && context.cwd.startsWith(`${binding.worktreeRoot}/`);
948
+ }
949
+ function canonicalPath(path) {
950
+ const resolved = resolve(path);
951
+ return existsSync2(resolved) ? realpathSync(resolved) : resolved;
952
+ }
953
+ function gitOutput(args, cwd) {
954
+ try {
955
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || void 0;
956
+ } catch {
957
+ return void 0;
958
+ }
959
+ }
960
+ function sha256(value) {
961
+ return createHash("sha256").update(value).digest("hex");
962
+ }
963
+ function withRegistryLock(codexHome, fn) {
964
+ const path = hookRegistryPath(codexHome);
965
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
966
+ const lockPath = hookRegistryLockPath(codexHome);
967
+ let fd;
968
+ for (let attempt = 0; attempt < 100; attempt += 1) {
969
+ try {
970
+ fd = openSync(lockPath, "wx", 384);
971
+ writeFileSync(fd, `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
972
+ `);
973
+ break;
974
+ } catch (error) {
975
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
976
+ if (recoverStaleLock(lockPath)) {
977
+ continue;
978
+ }
979
+ sleepSync(20);
980
+ continue;
981
+ }
982
+ throw error;
983
+ }
984
+ }
985
+ if (fd === void 0) {
986
+ throw new Error(`Timed out waiting for hook registry lock: ${lockPath}`);
987
+ }
988
+ try {
989
+ return fn();
990
+ } finally {
991
+ closeSync(fd);
992
+ rmSync(lockPath, { force: true });
993
+ }
994
+ }
995
+ var LOCK_STALE_MS = 3e4;
996
+ var TOUCH_REFRESH_MS = 1e4;
997
+ function recoverStaleLock(lockPath) {
998
+ const report = inspectLockPath(lockPath);
999
+ if (!report.stale) {
1000
+ return false;
1001
+ }
1002
+ rmSync(lockPath, { force: true });
1003
+ return true;
1004
+ }
1005
+ function inspectLockPath(path) {
1006
+ if (!existsSync2(path)) {
1007
+ return { path, exists: false, stale: false };
1008
+ }
1009
+ const metadata = readLockMetadata(path);
1010
+ const stat = statSync(path);
1011
+ const ageMs = Date.now() - (metadata.createdAtMs ?? stat.mtimeMs);
1012
+ const alive = metadata.pid ? processAlive(metadata.pid) : void 0;
1013
+ return {
1014
+ path,
1015
+ exists: true,
1016
+ stale: ageMs > LOCK_STALE_MS && alive !== true,
1017
+ ageMs,
1018
+ ...metadata.pid ? { pid: metadata.pid } : {},
1019
+ ...alive === void 0 ? {} : { processAlive: alive }
1020
+ };
1021
+ }
1022
+ function readLockMetadata(path) {
1023
+ try {
1024
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
1025
+ if (!isRecord(parsed)) return {};
1026
+ const pid = typeof parsed.pid === "number" ? parsed.pid : void 0;
1027
+ const createdAtMs = typeof parsed.createdAt === "string" ? Date.parse(parsed.createdAt) : void 0;
1028
+ return {
1029
+ ...pid && Number.isInteger(pid) && pid > 0 ? { pid } : {},
1030
+ ...createdAtMs && Number.isFinite(createdAtMs) ? { createdAtMs } : {}
1031
+ };
1032
+ } catch {
1033
+ return {};
1034
+ }
1035
+ }
1036
+ function processAlive(pid) {
1037
+ try {
1038
+ process.kill(pid, 0);
1039
+ return true;
1040
+ } catch {
1041
+ return false;
1042
+ }
1043
+ }
1044
+ function sleepSync(ms) {
1045
+ try {
1046
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1047
+ } catch {
1048
+ const end = Date.now() + ms;
1049
+ while (Date.now() < end) {
1050
+ }
1051
+ }
1052
+ }
1053
+ function stringValue(value) {
1054
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1055
+ }
1056
+
1057
+ // plugins/autonomous-pr-loop/core/policy.ts
1058
+ function matchesProtectedPath(pattern, path) {
1059
+ const normalizedPattern = normalizePath(pattern);
1060
+ const normalizedPath = normalizePath(path);
1061
+ if (!normalizedPattern.includes("/")) {
1062
+ const basename2 = normalizedPath.split("/").at(-1) ?? normalizedPath;
1063
+ return globToRegExp(normalizedPattern).test(basename2);
1064
+ }
1065
+ if (normalizedPattern.endsWith("/**")) {
1066
+ const prefix = normalizedPattern.slice(0, -3);
1067
+ if (normalizedPath === prefix) {
1068
+ return true;
1069
+ }
1070
+ }
1071
+ return globToRegExp(normalizedPattern).test(normalizedPath);
1072
+ }
1073
+ function normalizePath(path) {
1074
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
1075
+ }
1076
+ function globToRegExp(pattern) {
1077
+ let source = "";
1078
+ for (let index = 0; index < pattern.length; index += 1) {
1079
+ const char = pattern[index];
1080
+ const next = pattern[index + 1];
1081
+ const afterNext = pattern[index + 2];
1082
+ if (char === "*" && next === "*" && afterNext === "/") {
1083
+ source += "(?:.*/)?";
1084
+ index += 2;
1085
+ continue;
1086
+ }
1087
+ if (char === "*" && next === "*") {
1088
+ source += ".*";
1089
+ index += 1;
1090
+ continue;
1091
+ }
1092
+ if (char === "*") {
1093
+ source += "[^/]*";
1094
+ continue;
1095
+ }
1096
+ source += escapeRegExp(char ?? "");
1097
+ }
1098
+ return new RegExp(`^${source}$`);
1099
+ }
1100
+ function escapeRegExp(value) {
1101
+ return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
1102
+ }
1103
+
1104
+ // plugins/autonomous-pr-loop/core/redaction.ts
1105
+ function redactSecrets(value) {
1106
+ 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]");
1107
+ }
1108
+ function isSecretKey(key) {
1109
+ return /token|api_key|authorization|password|secret/i.test(key);
1110
+ }
1111
+
1112
+ // plugins/autonomous-pr-loop/core/storage.ts
1113
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
1114
+ import { dirname as dirname2 } from "node:path";
1115
+ import { randomUUID as randomUUID2 } from "node:crypto";
1116
+ import { DatabaseSync } from "node:sqlite";
1117
+ var STORAGE_SCHEMA_VERSION = 8;
1118
+ var SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, 4, 5, 6, 7, STORAGE_SCHEMA_VERSION];
1119
+ var TIMELINE_SOURCES = ["event", "worker_event", "worker", "state", "gate", "artifact", "decision"];
1120
+ var TIMELINE_TRIGGER_NAMES = [
1121
+ "timeline_events_insert",
1122
+ "timeline_worker_events_insert",
1123
+ "timeline_workers_insert",
1124
+ "timeline_workers_status_update",
1125
+ "timeline_states_insert",
1126
+ "timeline_gates_insert",
1127
+ "timeline_artifacts_insert",
1128
+ "timeline_decisions_insert"
1129
+ ];
1130
+ var PR_C_TABLES_SQL = `
1131
+ create table if not exists pr_links (
1132
+ id text primary key,
1133
+ run_id text not null,
1134
+ branch text not null,
1135
+ pr_number integer not null,
1136
+ url text not null,
1137
+ head_ref text not null,
1138
+ base_ref text not null,
1139
+ state text not null,
1140
+ draft integer not null,
1141
+ created_at text not null,
1142
+ updated_at text not null,
1143
+ unique(run_id, pr_number),
1144
+ foreign key(run_id) references runs(id)
1145
+ );
1146
+
1147
+ create table if not exists ci_checks (
1148
+ id text primary key,
1149
+ run_id text not null,
1150
+ pr_number integer not null,
1151
+ name text not null,
1152
+ status text not null,
1153
+ conclusion text,
1154
+ url text,
1155
+ started_at text,
1156
+ completed_at text,
1157
+ observed_at text not null,
1158
+ foreign key(run_id) references runs(id)
1159
+ );
1160
+
1161
+ create table if not exists review_comments (
1162
+ id text primary key,
1163
+ run_id text not null,
1164
+ pr_number integer not null,
1165
+ comment_id text not null,
1166
+ url text not null,
1167
+ author text not null,
1168
+ body text not null,
1169
+ path text not null,
1170
+ line integer,
1171
+ diff_hunk text not null,
1172
+ is_resolved integer not null,
1173
+ is_outdated integer not null,
1174
+ actionable integer not null,
1175
+ status text not null,
1176
+ observed_at text not null,
1177
+ unique(run_id, comment_id),
1178
+ foreign key(run_id) references runs(id)
1179
+ );
1180
+
1181
+ create table if not exists decisions (
1182
+ id text primary key,
1183
+ run_id text not null,
1184
+ kind text not null,
1185
+ message text not null,
1186
+ details_json text,
1187
+ created_at text not null,
1188
+ foreign key(run_id) references runs(id)
1189
+ );
1190
+ `;
1191
+ var PR_D_TABLES_SQL = `
1192
+ create table if not exists workers (
1193
+ id text primary key,
1194
+ run_id text not null,
1195
+ type text not null,
1196
+ backend text not null,
1197
+ status text not null,
1198
+ thread_id text,
1199
+ attempt integer not null,
1200
+ resume_used integer not null,
1201
+ started_at text not null,
1202
+ completed_at text,
1203
+ exit_code integer,
1204
+ result_artifact_id text,
1205
+ raw_jsonl_artifact_id text,
1206
+ error text,
1207
+ foreign key(run_id) references runs(id)
1208
+ );
1209
+
1210
+ create table if not exists worker_events (
1211
+ seq integer primary key autoincrement,
1212
+ id text not null unique,
1213
+ worker_id text not null,
1214
+ run_id text not null,
1215
+ event_type text not null,
1216
+ item_type text,
1217
+ item_id text,
1218
+ item_status text,
1219
+ thread_id text,
1220
+ backend text,
1221
+ summary_json text,
1222
+ usage_json text,
1223
+ artifact_ids_json text,
1224
+ created_at text not null,
1225
+ foreign key(worker_id) references workers(id),
1226
+ foreign key(run_id) references runs(id)
1227
+ );
1228
+
1229
+ create unique index if not exists workers_single_running
1230
+ on workers(status)
1231
+ where status = 'running';
1232
+ `;
1233
+ var PR_E_INDEXES_SQL = `
1234
+ create unique index if not exists runs_single_running
1235
+ on runs(status)
1236
+ where status = 'RUNNING';
1237
+ `;
1238
+ var PR_E_TABLES_SQL = `
1239
+ create table if not exists run_checks (
1240
+ run_id text not null,
1241
+ kind text not null,
1242
+ status text not null,
1243
+ details_json text,
1244
+ created_at text not null,
1245
+ primary key(run_id, kind),
1246
+ foreign key(run_id) references runs(id)
1247
+ );
1248
+ `;
1249
+ var TIMELINE_INDEX_SQL = `
1250
+ create table if not exists timeline_index (
1251
+ timeline_seq integer primary key autoincrement,
1252
+ source text not null,
1253
+ source_id text not null,
1254
+ source_seq integer,
1255
+ run_id text,
1256
+ worker_id text,
1257
+ created_at text not null,
1258
+ unique(source, source_id)
1259
+ );
1260
+
1261
+ create index if not exists timeline_index_created
1262
+ on timeline_index(created_at desc, timeline_seq desc);
1263
+ create index if not exists timeline_index_run
1264
+ on timeline_index(run_id, timeline_seq desc);
1265
+ create index if not exists timeline_index_worker
1266
+ on timeline_index(worker_id, timeline_seq desc);
1267
+ `;
1268
+ var TIMELINE_TRIGGERS_SQL = `
1269
+ create trigger if not exists timeline_events_insert
1270
+ after insert on events
1271
+ begin
1272
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1273
+ values ('event', new.id, new.seq, new.run_id, null, new.created_at);
1274
+ end;
1275
+
1276
+ create trigger if not exists timeline_worker_events_insert
1277
+ after insert on worker_events
1278
+ begin
1279
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1280
+ values ('worker_event', new.id, new.seq, new.run_id, new.worker_id, new.created_at);
1281
+ end;
1282
+
1283
+ create trigger if not exists timeline_workers_insert
1284
+ after insert on workers
1285
+ begin
1286
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1287
+ values ('worker', new.id || ':' || new.status, null, new.run_id, new.id, new.started_at);
1288
+ end;
1289
+
1290
+ create trigger if not exists timeline_workers_status_update
1291
+ after update of status on workers
1292
+ when old.status is not new.status
1293
+ begin
1294
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1295
+ values (
1296
+ 'worker',
1297
+ new.id || ':' || new.status,
1298
+ null,
1299
+ new.run_id,
1300
+ new.id,
1301
+ coalesce(new.completed_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
1302
+ );
1303
+ end;
1304
+
1305
+ create trigger if not exists timeline_states_insert
1306
+ after insert on states
1307
+ begin
1308
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1309
+ values ('state', cast(new.id as text), new.id, new.run_id, null, new.created_at);
1310
+ end;
1311
+
1312
+ create trigger if not exists timeline_gates_insert
1313
+ after insert on gates
1314
+ begin
1315
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1316
+ values ('gate', new.id, null, new.run_id, null, new.created_at);
1317
+ end;
1318
+
1319
+ create trigger if not exists timeline_artifacts_insert
1320
+ after insert on artifacts
1321
+ begin
1322
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1323
+ values ('artifact', new.id, null, new.run_id, null, new.created_at);
1324
+ end;
1325
+
1326
+ create trigger if not exists timeline_decisions_insert
1327
+ after insert on decisions
1328
+ begin
1329
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
1330
+ values ('decision', new.id, null, new.run_id, null, new.created_at);
1331
+ end;
1332
+ `;
1333
+ var SCHEMA_SQL = `
1334
+ create table if not exists runs (
1335
+ id text primary key,
1336
+ status text not null,
1337
+ current_state text,
1338
+ version integer not null default 0,
1339
+ branch text,
1340
+ worktree_clean integer,
1341
+ started_at text,
1342
+ stopped_at text,
1343
+ created_at text not null,
1344
+ updated_at text not null
1345
+ );
1346
+
1347
+ create table if not exists states (
1348
+ id integer primary key autoincrement,
1349
+ run_id text,
1350
+ status text not null,
1351
+ state text,
1352
+ version integer not null,
1353
+ payload_json text,
1354
+ created_at text not null,
1355
+ foreign key(run_id) references runs(id)
1356
+ );
1357
+
1358
+ create table if not exists events (
1359
+ seq integer primary key autoincrement,
1360
+ id text not null unique,
1361
+ run_id text,
1362
+ kind text not null,
1363
+ message text not null,
1364
+ state_before text,
1365
+ state_after text,
1366
+ payload_json text,
1367
+ artifact_ids_json text,
1368
+ created_at text not null,
1369
+ foreign key(run_id) references runs(id)
1370
+ );
1371
+
1372
+ create table if not exists gates (
1373
+ id text primary key,
1374
+ run_id text,
1375
+ kind text not null,
1376
+ status text not null,
1377
+ message text not null,
1378
+ details_json text,
1379
+ created_at text not null,
1380
+ resolved_at text,
1381
+ decision_note text,
1382
+ decided_at text,
1383
+ foreign key(run_id) references runs(id)
1384
+ );
1385
+
1386
+ create table if not exists artifacts (
1387
+ id text primary key,
1388
+ run_id text,
1389
+ kind text not null,
1390
+ name text,
1391
+ path text not null,
1392
+ sha256 text,
1393
+ metadata_json text,
1394
+ created_at text not null,
1395
+ foreign key(run_id) references runs(id)
1396
+ );
1397
+
1398
+ create table if not exists repo_config (
1399
+ id integer primary key check (id = 1),
1400
+ schema_version integer not null,
1401
+ config_json text not null,
1402
+ updated_at text not null
1403
+ );
1404
+
1405
+ ${PR_C_TABLES_SQL}
1406
+ ${PR_D_TABLES_SQL}
1407
+ ${PR_E_TABLES_SQL}
1408
+ ${PR_E_INDEXES_SQL}
1409
+ `;
1410
+ var SqliteAgentLoopStorage = class {
1411
+ constructor(path, options = {}) {
1412
+ this.path = path;
1413
+ this.mode = options.mode ?? "rw";
1414
+ if (this.mode === "rw") {
1415
+ mkdirSync2(dirname2(path), { recursive: true });
1416
+ } else if (!existsSync3(path)) {
1417
+ throw new AgentLoopError("storage_error", "Read-only storage file does not exist.", {
1418
+ details: { path }
1419
+ });
1420
+ }
1421
+ this.db = new DatabaseSync(path, {
1422
+ readOnly: this.mode === "ro",
1423
+ enableForeignKeyConstraints: true,
1424
+ timeout: 5e3
1425
+ });
1426
+ try {
1427
+ this.db.exec("PRAGMA foreign_keys=ON");
1428
+ this.db.exec("PRAGMA busy_timeout=5000");
1429
+ if (this.mode === "rw") {
1430
+ this.db.exec("PRAGMA journal_mode=WAL");
1431
+ }
1432
+ this.ensureSchema();
1433
+ if (this.mode === "rw") {
1434
+ this.ensureRepoConfigVersion();
1435
+ } else {
1436
+ this.validateRepoConfigVersion();
1437
+ }
1438
+ const workersSql = `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
1439
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
1440
+ from workers`;
1441
+ this.listWorkersByRunStatement = this.db.prepare(`${workersSql} where run_id = ? order by started_at desc limit ?`);
1442
+ this.listWorkersStatement = this.db.prepare(`${workersSql} order by started_at desc limit ?`);
1443
+ } catch (error) {
1444
+ this.db.close();
1445
+ throw toStorageError(error, "Failed to open agent-loop storage.");
1446
+ }
1447
+ }
1448
+ path;
1449
+ db;
1450
+ mode;
1451
+ listWorkersByRunStatement;
1452
+ listWorkersStatement;
1453
+ close() {
1454
+ this.db.close();
1455
+ }
1456
+ writeRepoConfig(config) {
1457
+ const snapshot = JSON.stringify({ schemaVersion: STORAGE_SCHEMA_VERSION, ...config });
1458
+ this.transaction(() => {
1459
+ this.db.prepare(
1460
+ `insert into repo_config (id, schema_version, config_json, updated_at)
1461
+ values (1, ?, ?, ?)
1462
+ on conflict(id) do update set
1463
+ schema_version = excluded.schema_version,
1464
+ config_json = excluded.config_json,
1465
+ updated_at = excluded.updated_at`
1466
+ ).run(STORAGE_SCHEMA_VERSION, snapshot, now());
1467
+ });
1468
+ }
1469
+ readRepoConfig() {
1470
+ const row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
1471
+ if (!row) {
1472
+ return void 0;
1473
+ }
1474
+ if (!isSupportedSchemaVersion(row.schema_version)) {
1475
+ throw new AgentLoopError(
1476
+ "storage_schema_mismatch",
1477
+ `Stored repo config schema version ${row.schema_version} is not supported.`,
1478
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
1479
+ );
1480
+ }
1481
+ const parsed = parseJson(row.config_json, "Stored repo config JSON is invalid.");
1482
+ const { schemaVersion: _schemaVersion, ...config } = parsed;
1483
+ return config;
1484
+ }
1485
+ createRun(status, options = {}) {
1486
+ const createdAt = now();
1487
+ const run = {
1488
+ id: randomUUID2(),
1489
+ status,
1490
+ ...options.currentState ? { currentState: options.currentState } : {},
1491
+ version: 0,
1492
+ ...options.branch ? { branch: options.branch } : {},
1493
+ ...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
1494
+ createdAt,
1495
+ updatedAt: createdAt,
1496
+ startedAt: createdAt
1497
+ };
1498
+ try {
1499
+ this.transaction(() => {
1500
+ this.db.prepare(
1501
+ `insert into runs (
1502
+ id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1503
+ )
1504
+ values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
1505
+ ).run(
1506
+ run.id,
1507
+ run.status,
1508
+ run.currentState ?? null,
1509
+ run.version,
1510
+ run.branch ?? null,
1511
+ boolToDb(run.worktreeClean),
1512
+ run.startedAt ?? null,
1513
+ run.createdAt,
1514
+ run.updatedAt
1515
+ );
1516
+ this.db.prepare(
1517
+ `insert into states (run_id, status, state, version, payload_json, created_at)
1518
+ values (?, ?, ?, ?, null, ?)`
1519
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
1520
+ });
1521
+ } catch (error) {
1522
+ if (isUniqueConstraintError(error)) {
1523
+ throw new AgentLoopError("version_conflict", "Another active run already exists.", {
1524
+ details: { status },
1525
+ exitCode: 2
1526
+ });
1527
+ }
1528
+ throw error;
1529
+ }
1530
+ return run;
1531
+ }
1532
+ getOrCreateActiveRun(options = {}) {
1533
+ return this.transaction(() => {
1534
+ const active = this.getActiveRun();
1535
+ if (active) {
1536
+ return { run: active, created: false };
1537
+ }
1538
+ const createdAt = now();
1539
+ const run = {
1540
+ id: randomUUID2(),
1541
+ status: "RUNNING",
1542
+ ...options.currentState ? { currentState: options.currentState } : {},
1543
+ version: 0,
1544
+ ...options.branch ? { branch: options.branch } : {},
1545
+ ...options.worktreeClean !== void 0 ? { worktreeClean: options.worktreeClean } : {},
1546
+ createdAt,
1547
+ updatedAt: createdAt,
1548
+ startedAt: createdAt
1549
+ };
1550
+ this.db.prepare(
1551
+ `insert into runs (
1552
+ id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1553
+ )
1554
+ values (?, ?, ?, ?, ?, ?, ?, null, ?, ?)`
1555
+ ).run(
1556
+ run.id,
1557
+ run.status,
1558
+ run.currentState ?? null,
1559
+ run.version,
1560
+ run.branch ?? null,
1561
+ boolToDb(run.worktreeClean),
1562
+ run.startedAt ?? null,
1563
+ run.createdAt,
1564
+ run.updatedAt
1565
+ );
1566
+ this.db.prepare(
1567
+ `insert into states (run_id, status, state, version, payload_json, created_at)
1568
+ values (?, ?, ?, ?, null, ?)`
1569
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
1570
+ return { run, created: true };
1571
+ });
1572
+ }
1573
+ recordRunCheck(check) {
1574
+ const stored = { ...check, createdAt: now() };
1575
+ this.transaction(() => {
1576
+ this.db.prepare(
1577
+ `insert into run_checks (run_id, kind, status, details_json, created_at)
1578
+ values (?, ?, ?, ?, ?)
1579
+ on conflict(run_id, kind) do update set
1580
+ status = excluded.status,
1581
+ details_json = excluded.details_json,
1582
+ created_at = excluded.created_at`
1583
+ ).run(
1584
+ stored.runId,
1585
+ stored.kind,
1586
+ stored.status,
1587
+ stored.details === void 0 ? null : JSON.stringify(stored.details),
1588
+ stored.createdAt
1589
+ );
1590
+ });
1591
+ return stored;
1592
+ }
1593
+ hasRunCheck(runId, kind) {
1594
+ 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);
1595
+ return row !== void 0;
1596
+ }
1597
+ listRunChecks(runId) {
1598
+ 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);
1599
+ return rows.map(fromRunCheckRow);
1600
+ }
1601
+ updateRunStatus(runId, expectedVersion, status, options = {}) {
1602
+ const updatedAt = now();
1603
+ return this.transaction(() => {
1604
+ const result = this.db.prepare(
1605
+ `update runs
1606
+ set status = ?,
1607
+ current_state = coalesce(?, current_state),
1608
+ branch = coalesce(?, branch),
1609
+ worktree_clean = coalesce(?, worktree_clean),
1610
+ stopped_at = coalesce(?, stopped_at),
1611
+ version = version + 1,
1612
+ updated_at = ?
1613
+ where id = ? and version = ?`
1614
+ ).run(
1615
+ status,
1616
+ options.currentState ?? null,
1617
+ options.branch ?? null,
1618
+ boolToDb(options.worktreeClean),
1619
+ options.stoppedAt ?? null,
1620
+ updatedAt,
1621
+ runId,
1622
+ expectedVersion
1623
+ );
1624
+ if (result.changes !== 1) {
1625
+ throw new AgentLoopError(
1626
+ "version_conflict",
1627
+ `Run ${runId} was updated by another writer.`,
1628
+ { details: { runId, expectedVersion } }
1629
+ );
1630
+ }
1631
+ const run = this.getRun(runId);
1632
+ this.db.prepare(
1633
+ `insert into states (run_id, status, state, version, payload_json, created_at)
1634
+ values (?, ?, ?, ?, null, ?)`
1635
+ ).run(run.id, run.status, run.currentState ?? run.status, run.version, run.updatedAt);
1636
+ return run;
1637
+ });
1638
+ }
1639
+ appendEvent(event) {
1640
+ const stored = {
1641
+ id: randomUUID2(),
1642
+ ...event,
1643
+ createdAt: now()
1644
+ };
1645
+ let seq = 0;
1646
+ this.transaction(() => {
1647
+ this.db.prepare(
1648
+ `insert into events (
1649
+ id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1650
+ )
1651
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1652
+ ).run(
1653
+ stored.id,
1654
+ stored.runId ?? null,
1655
+ stored.kind,
1656
+ stored.message,
1657
+ stored.stateBefore ?? null,
1658
+ stored.stateAfter ?? null,
1659
+ stored.payload === void 0 ? null : JSON.stringify(stored.payload),
1660
+ stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
1661
+ stored.createdAt
1662
+ );
1663
+ seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
1664
+ });
1665
+ return { seq, ...stored };
1666
+ }
1667
+ writeGate(gate) {
1668
+ this.transaction(() => {
1669
+ this.db.prepare(
1670
+ `insert into gates (id, run_id, kind, status, message, details_json, created_at, resolved_at)
1671
+ values (?, ?, ?, 'open', ?, ?, ?, null)`
1672
+ ).run(
1673
+ randomUUID2(),
1674
+ gate.runId ?? null,
1675
+ gate.kind,
1676
+ gate.message,
1677
+ gate.details === void 0 ? null : JSON.stringify(gate.details),
1678
+ now()
1679
+ );
1680
+ });
1681
+ }
1682
+ resolveOpenGates(runId) {
1683
+ this.transaction(() => {
1684
+ this.db.prepare(
1685
+ `update gates
1686
+ set status = 'resolved', resolved_at = ?
1687
+ where run_id = ? and status = 'open'`
1688
+ ).run(now(), runId);
1689
+ });
1690
+ }
1691
+ resolveOpenGatesByKind(kind, options = {}) {
1692
+ const scope = options.scope ?? (options.runId ? "run" : "repo");
1693
+ this.transaction(() => {
1694
+ if (scope === "run") {
1695
+ if (!options.runId) {
1696
+ throw new AgentLoopError("storage_error", "runId is required for run-scoped gate recovery.");
1697
+ }
1698
+ this.db.prepare(
1699
+ `update gates
1700
+ set status = 'resolved', resolved_at = ?
1701
+ where kind = ? and run_id = ? and status = 'open'`
1702
+ ).run(now(), kind, options.runId);
1703
+ return;
1704
+ }
1705
+ if (scope === "repo") {
1706
+ this.db.prepare(
1707
+ `update gates
1708
+ set status = 'resolved', resolved_at = ?
1709
+ where kind = ? and run_id is null and status = 'open'`
1710
+ ).run(now(), kind);
1711
+ return;
1712
+ }
1713
+ this.db.prepare(
1714
+ `update gates
1715
+ set status = 'resolved', resolved_at = ?
1716
+ where kind = ? and status = 'open'`
1717
+ ).run(now(), kind);
1718
+ });
1719
+ }
1720
+ listGates(runId) {
1721
+ const sql = `select id, run_id, kind, status, message, details_json, created_at,
1722
+ resolved_at, decision_note, decided_at
1723
+ from gates
1724
+ ${runId ? "where run_id = ?" : ""}
1725
+ order by created_at desc
1726
+ limit 100`;
1727
+ const rows = runId ? this.db.prepare(sql).all(runId) : this.db.prepare(sql).all();
1728
+ return rows.map(fromGateRow);
1729
+ }
1730
+ getGate(gateId) {
1731
+ const row = this.db.prepare(
1732
+ `select id, run_id, kind, status, message, details_json, created_at,
1733
+ resolved_at, decision_note, decided_at
1734
+ from gates
1735
+ where id = ?`
1736
+ ).get(gateId);
1737
+ return row ? fromGateRow(row) : void 0;
1738
+ }
1739
+ decideGate(gateId, decision2, note) {
1740
+ if (note.trim().length === 0) {
1741
+ throw new AgentLoopError("invalid_config", "Gate decision note is required.");
1742
+ }
1743
+ const decidedAt = now();
1744
+ this.transaction(() => {
1745
+ const result = this.db.prepare(
1746
+ `update gates
1747
+ set status = ?, decision_note = ?, decided_at = ?, resolved_at = coalesce(resolved_at, ?)
1748
+ where id = ? and status = 'open'`
1749
+ ).run(decision2, note, decidedAt, decidedAt, gateId);
1750
+ if (result.changes !== 1) {
1751
+ const gate2 = this.getGate(gateId);
1752
+ if (!gate2) {
1753
+ throw new AgentLoopError("storage_error", `Gate not found: ${gateId}`);
1754
+ }
1755
+ throw new AgentLoopError("storage_error", `Gate ${gateId} is not open.`, {
1756
+ details: { gateId, status: gate2.status }
1757
+ });
1758
+ }
1759
+ });
1760
+ const gate = this.getGate(gateId);
1761
+ if (!gate) {
1762
+ throw new AgentLoopError("storage_error", `Gate not found after decision: ${gateId}`);
1763
+ }
1764
+ return gate;
1765
+ }
1766
+ getCurrentStatus() {
1767
+ const repoGate = this.db.prepare(
1768
+ `select kind, message, details_json
1769
+ from gates
1770
+ where status = 'open' and run_id is null
1771
+ order by created_at desc
1772
+ limit 1`
1773
+ ).get();
1774
+ if (repoGate) {
1775
+ return {
1776
+ status: "BLOCKED",
1777
+ gate: statusGateFromRow(repoGate)
1778
+ };
1779
+ }
1780
+ const row = this.db.prepare(
1781
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
1782
+ from runs
1783
+ order by updated_at desc, rowid desc
1784
+ limit 1`
1785
+ ).get();
1786
+ if (!row) {
1787
+ return { status: "IDLE" };
1788
+ }
1789
+ const run = fromRunRow(row);
1790
+ const runGate = this.db.prepare(
1791
+ `select kind, message, details_json
1792
+ from gates
1793
+ where status = 'open' and run_id = ?
1794
+ order by created_at desc
1795
+ limit 1`
1796
+ ).get(run.id);
1797
+ if (runGate) {
1798
+ return {
1799
+ status: "BLOCKED",
1800
+ run,
1801
+ gate: statusGateFromRow(runGate)
1802
+ };
1803
+ }
1804
+ if (run.status === "BLOCKED" && latestGateSatisfied(this.db, run.id)) {
1805
+ return { status: "READY", run: { ...run, status: "READY" } };
1806
+ }
1807
+ return { status: run.status, run };
1808
+ }
1809
+ listEvents(options = 50) {
1810
+ const limit = typeof options === "number" ? options : options.limit ?? 50;
1811
+ const sinceSeq = typeof options === "number" ? void 0 : options.sinceSeq;
1812
+ const rows = sinceSeq === void 0 ? this.db.prepare(
1813
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1814
+ from events
1815
+ order by seq desc
1816
+ limit ?`
1817
+ ).all(limit) : this.db.prepare(
1818
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1819
+ from events
1820
+ where seq > ?
1821
+ order by seq asc
1822
+ limit ?`
1823
+ ).all(sinceSeq, limit);
1824
+ return rows.map(fromEventRow);
1825
+ }
1826
+ findLatestEvent(runId, kind) {
1827
+ const row = this.db.prepare(
1828
+ `select seq, id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
1829
+ from events
1830
+ where run_id = ? and kind = ?
1831
+ order by seq desc
1832
+ limit 1`
1833
+ ).get(runId, kind);
1834
+ return row ? fromEventRow(row) : void 0;
1835
+ }
1836
+ listAgentTimeline(query = {}) {
1837
+ const limit = clampLimit(query.limit ?? 50);
1838
+ const cursor = query.cursor ? decodeTimelineCursor(query.cursor) : void 0;
1839
+ const params = [];
1840
+ const where = [];
1841
+ if (cursor) {
1842
+ where.push("(created_at < ? or (created_at = ? and timeline_seq < ?))");
1843
+ params.push(cursor.occurredAt, cursor.occurredAt, cursor.timelineSeq);
1844
+ }
1845
+ if (query.sources?.length) {
1846
+ const sources = normalizeTimelineSources(query.sources);
1847
+ where.push(`source in (${sources.map(() => "?").join(", ")})`);
1848
+ params.push(...sources);
1849
+ }
1850
+ if (query.runId) {
1851
+ where.push("run_id = ?");
1852
+ params.push(query.runId);
1853
+ }
1854
+ if (query.workerId) {
1855
+ where.push("worker_id = ?");
1856
+ params.push(query.workerId);
1857
+ }
1858
+ params.push(limit + 1);
1859
+ const rows = this.db.prepare(
1860
+ `select timeline_seq, source, source_id, source_seq, run_id, worker_id, created_at
1861
+ from timeline_index
1862
+ ${where.length ? `where ${where.join(" and ")}` : ""}
1863
+ order by created_at desc, timeline_seq desc
1864
+ limit ?`
1865
+ ).all(...params);
1866
+ const pageRows = rows.slice(0, limit);
1867
+ const entries = pageRows.map((row) => this.timelineEntry(row)).filter((entry) => entry !== void 0);
1868
+ const last = pageRows[pageRows.length - 1];
1869
+ return {
1870
+ entries,
1871
+ ...rows.length > limit && last ? { nextCursor: encodeTimelineCursor(last.timeline_seq, last.created_at) } : {}
1872
+ };
1873
+ }
1874
+ checkTimelineIntegrity() {
1875
+ const missingTable = !hasTable(this.db, "timeline_index");
1876
+ const triggers = new Set(this.db.prepare("select name from sqlite_master where type = 'trigger' and name like 'timeline_%'").all().map((row) => row.name));
1877
+ const missingTriggers = TIMELINE_TRIGGER_NAMES.filter((name) => !triggers.has(name));
1878
+ const sourceCounts = Object.fromEntries(TIMELINE_SOURCES.map((source) => [source, 0]));
1879
+ const missingSourceRows = [];
1880
+ if (!missingTable) {
1881
+ const rows = this.db.prepare("select source, count(*) as count from timeline_index group by source").all();
1882
+ for (const row of rows) {
1883
+ if (TIMELINE_SOURCES.includes(row.source)) {
1884
+ sourceCounts[row.source] = row.count;
1885
+ }
1886
+ }
1887
+ missingSourceRows.push(...timelineMissingSourceRows(this.db));
1888
+ }
1889
+ const ok = !missingTable && missingTriggers.length === 0 && missingSourceRows.length === 0;
1890
+ return {
1891
+ ok,
1892
+ missingTable,
1893
+ missingTriggers,
1894
+ missingSourceRows,
1895
+ sourceCounts,
1896
+ repair: "Run storage migration or rebuild timeline_index by dropping timeline_index/triggers and reopening storage in read-write mode."
1897
+ };
1898
+ }
1899
+ upsertPrLink(link) {
1900
+ const createdAt = now();
1901
+ const id = randomUUID2();
1902
+ this.transaction(() => {
1903
+ this.db.prepare(
1904
+ `insert into pr_links (
1905
+ id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
1906
+ )
1907
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1908
+ on conflict(run_id, pr_number) do update set
1909
+ branch = excluded.branch,
1910
+ url = excluded.url,
1911
+ head_ref = excluded.head_ref,
1912
+ base_ref = excluded.base_ref,
1913
+ state = excluded.state,
1914
+ draft = excluded.draft,
1915
+ updated_at = excluded.updated_at`
1916
+ ).run(
1917
+ id,
1918
+ link.runId,
1919
+ link.branch,
1920
+ link.prNumber,
1921
+ link.url,
1922
+ link.headRef,
1923
+ link.baseRef,
1924
+ link.state,
1925
+ boolToDb(link.draft),
1926
+ createdAt,
1927
+ createdAt
1928
+ );
1929
+ });
1930
+ const stored = this.getPrLink(link.runId);
1931
+ if (!stored) {
1932
+ throw new AgentLoopError("storage_error", "PR link was not stored.");
1933
+ }
1934
+ return stored;
1935
+ }
1936
+ getPrLink(runId) {
1937
+ const row = this.db.prepare(
1938
+ `select id, run_id, branch, pr_number, url, head_ref, base_ref, state, draft, created_at, updated_at
1939
+ from pr_links
1940
+ where run_id = ?
1941
+ order by updated_at desc
1942
+ limit 1`
1943
+ ).get(runId);
1944
+ return row ? fromPrLinkRow(row) : void 0;
1945
+ }
1946
+ replaceCiChecks(runId, prNumber, checks) {
1947
+ const observedAt = now();
1948
+ this.transaction(() => {
1949
+ this.db.prepare("delete from ci_checks where run_id = ? and pr_number = ?").run(runId, prNumber);
1950
+ for (const check of checks) {
1951
+ this.db.prepare(
1952
+ `insert into ci_checks (
1953
+ id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
1954
+ )
1955
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1956
+ ).run(
1957
+ randomUUID2(),
1958
+ runId,
1959
+ prNumber,
1960
+ check.name,
1961
+ check.status,
1962
+ check.conclusion ?? null,
1963
+ check.url ?? null,
1964
+ check.startedAt ?? null,
1965
+ check.completedAt ?? null,
1966
+ observedAt
1967
+ );
1968
+ }
1969
+ });
1970
+ return this.listCiChecks(runId);
1971
+ }
1972
+ listCiChecks(runId) {
1973
+ const rows = this.db.prepare(
1974
+ `select id, run_id, pr_number, name, status, conclusion, url, started_at, completed_at, observed_at
1975
+ from ci_checks
1976
+ where run_id = ?
1977
+ order by observed_at desc, name asc`
1978
+ ).all(runId);
1979
+ return rows.map(fromCiCheckRow);
1980
+ }
1981
+ replaceReviewComments(runId, prNumber, comments) {
1982
+ const observedAt = now();
1983
+ this.transaction(() => {
1984
+ this.db.prepare("delete from review_comments where run_id = ? and pr_number = ?").run(runId, prNumber);
1985
+ for (const comment of comments) {
1986
+ this.db.prepare(
1987
+ `insert into review_comments (
1988
+ id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
1989
+ is_resolved, is_outdated, actionable, status, observed_at
1990
+ )
1991
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1992
+ ).run(
1993
+ randomUUID2(),
1994
+ runId,
1995
+ prNumber,
1996
+ comment.commentId,
1997
+ comment.url,
1998
+ comment.author,
1999
+ comment.body,
2000
+ comment.path,
2001
+ comment.line ?? null,
2002
+ comment.diffHunk,
2003
+ boolToDb(comment.isResolved),
2004
+ boolToDb(comment.isOutdated),
2005
+ boolToDb(comment.actionable),
2006
+ comment.status,
2007
+ observedAt
2008
+ );
2009
+ }
2010
+ });
2011
+ return this.listReviewComments(runId);
2012
+ }
2013
+ listReviewComments(runId) {
2014
+ const rows = this.db.prepare(
2015
+ `select id, run_id, pr_number, comment_id, url, author, body, path, line, diff_hunk,
2016
+ is_resolved, is_outdated, actionable, status, observed_at
2017
+ from review_comments
2018
+ where run_id = ?
2019
+ order by observed_at desc, path asc`
2020
+ ).all(runId);
2021
+ return rows.map(fromReviewCommentRow);
2022
+ }
2023
+ appendDecision(decision2) {
2024
+ const stored = { id: randomUUID2(), ...decision2, createdAt: now() };
2025
+ this.transaction(() => {
2026
+ this.db.prepare(
2027
+ `insert into decisions (id, run_id, kind, message, details_json, created_at)
2028
+ values (?, ?, ?, ?, ?, ?)`
2029
+ ).run(
2030
+ stored.id,
2031
+ stored.runId,
2032
+ stored.kind,
2033
+ stored.message,
2034
+ stored.details === void 0 ? null : JSON.stringify(stored.details),
2035
+ stored.createdAt
2036
+ );
2037
+ });
2038
+ return stored;
2039
+ }
2040
+ listDecisions(runId) {
2041
+ const rows = this.db.prepare(
2042
+ `select id, run_id, kind, message, details_json, created_at
2043
+ from decisions
2044
+ where run_id = ?
2045
+ order by created_at desc`
2046
+ ).all(runId);
2047
+ return rows.map(fromDecisionRow);
2048
+ }
2049
+ createWorker(worker) {
2050
+ const id = randomUUID2();
2051
+ const startedAt = now();
2052
+ try {
2053
+ this.transaction(() => {
2054
+ this.db.prepare(
2055
+ `insert into workers (
2056
+ id, run_id, type, backend, status, thread_id, attempt, resume_used,
2057
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
2058
+ )
2059
+ values (?, ?, ?, ?, 'running', null, ?, ?, ?, null, null, null, null, null)`
2060
+ ).run(id, worker.runId, worker.type, worker.backend, worker.attempt, boolToDb(worker.resumeUsed), startedAt);
2061
+ });
2062
+ } catch (error) {
2063
+ if (isUniqueConstraintError(error)) {
2064
+ throw new AgentLoopError("worker_already_running", "Another worker is already running.", {
2065
+ details: { runId: worker.runId },
2066
+ exitCode: 2
2067
+ });
2068
+ }
2069
+ throw error;
2070
+ }
2071
+ return this.getWorker(id);
2072
+ }
2073
+ updateWorker(workerId, patch) {
2074
+ this.transaction(() => {
2075
+ this.db.prepare(
2076
+ `update workers
2077
+ set status = coalesce(?, status),
2078
+ thread_id = coalesce(?, thread_id),
2079
+ completed_at = coalesce(?, completed_at),
2080
+ exit_code = coalesce(?, exit_code),
2081
+ result_artifact_id = coalesce(?, result_artifact_id),
2082
+ raw_jsonl_artifact_id = coalesce(?, raw_jsonl_artifact_id),
2083
+ error = coalesce(?, error)
2084
+ where id = ?`
2085
+ ).run(
2086
+ patch.status ?? null,
2087
+ patch.threadId ?? null,
2088
+ patch.completedAt ?? null,
2089
+ patch.exitCode ?? null,
2090
+ patch.resultArtifactId ?? null,
2091
+ patch.rawJsonlArtifactId ?? null,
2092
+ patch.error ?? null,
2093
+ workerId
2094
+ );
2095
+ });
2096
+ return this.getWorker(workerId);
2097
+ }
2098
+ getRunningWorker() {
2099
+ const row = this.db.prepare(
2100
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
2101
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
2102
+ from workers
2103
+ where status = 'running'
2104
+ order by started_at desc
2105
+ limit 1`
2106
+ ).get();
2107
+ return row ? fromWorkerRow(row) : void 0;
2108
+ }
2109
+ listWorkers(runId, limit = 50) {
2110
+ const rows = runId ? this.listWorkersByRunStatement.all(runId, limit) : this.listWorkersStatement.all(limit);
2111
+ return rows.map(fromWorkerRow);
2112
+ }
2113
+ appendWorkerEvent(event) {
2114
+ const existing = this.findDuplicateWorkerEvent(event);
2115
+ if (existing) {
2116
+ return existing;
2117
+ }
2118
+ const stored = { id: randomUUID2(), ...event, createdAt: now() };
2119
+ let seq = 0;
2120
+ this.transaction(() => {
2121
+ this.db.prepare(
2122
+ `insert into worker_events (
2123
+ id, worker_id, run_id, event_type, item_type, item_id, item_status,
2124
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
2125
+ )
2126
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2127
+ ).run(
2128
+ stored.id,
2129
+ stored.workerId,
2130
+ stored.runId,
2131
+ stored.eventType,
2132
+ stored.itemType ?? null,
2133
+ stored.itemId ?? null,
2134
+ stored.itemStatus ?? null,
2135
+ stored.threadId ?? null,
2136
+ stored.backend ?? null,
2137
+ stored.summary === void 0 ? null : JSON.stringify(stored.summary),
2138
+ stored.usage === void 0 ? null : JSON.stringify(stored.usage),
2139
+ stored.artifactIds === void 0 ? null : JSON.stringify(stored.artifactIds),
2140
+ stored.createdAt
2141
+ );
2142
+ seq = Number(this.db.prepare("select last_insert_rowid() as seq").get().seq);
2143
+ });
2144
+ return { seq, ...stored };
2145
+ }
2146
+ listWorkerEvents(workerId) {
2147
+ const rows = this.db.prepare(
2148
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
2149
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
2150
+ from worker_events
2151
+ where worker_id = ?
2152
+ order by seq asc`
2153
+ ).all(workerId);
2154
+ return rows.map(fromWorkerEventRow);
2155
+ }
2156
+ findDuplicateWorkerEvent(event) {
2157
+ if (!event.threadId) {
2158
+ return void 0;
2159
+ }
2160
+ const row = event.itemId ? this.db.prepare(
2161
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
2162
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
2163
+ from worker_events
2164
+ where thread_id = ? and item_id = ? and coalesce(item_status, '') = ?
2165
+ limit 1`
2166
+ ).get(event.threadId, event.itemId, event.itemStatus ?? "") : this.db.prepare(
2167
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
2168
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
2169
+ from worker_events
2170
+ where thread_id = ? and event_type = ? and item_id is null
2171
+ limit 1`
2172
+ ).get(event.threadId, event.eventType);
2173
+ return row ? fromWorkerEventRow(row) : void 0;
2174
+ }
2175
+ insertArtifact(record) {
2176
+ this.transaction(() => {
2177
+ this.db.prepare(
2178
+ `insert into artifacts (id, run_id, kind, name, path, sha256, metadata_json, created_at)
2179
+ values (?, ?, ?, ?, ?, ?, null, ?)`
2180
+ ).run(
2181
+ record.id,
2182
+ record.runId,
2183
+ record.kind,
2184
+ record.name,
2185
+ record.path,
2186
+ record.sha256,
2187
+ record.createdAt
2188
+ );
2189
+ });
2190
+ }
2191
+ getArtifact(artifactId) {
2192
+ const row = this.db.prepare(
2193
+ `select id, run_id, kind, name, path, sha256, created_at
2194
+ from artifacts
2195
+ where id = ?`
2196
+ ).get(artifactId);
2197
+ if (!row) {
2198
+ throw new AgentLoopError("storage_error", `Artifact not found: ${artifactId}`);
2199
+ }
2200
+ return fromArtifactRow(row);
2201
+ }
2202
+ listArtifacts(runId) {
2203
+ const rows = this.db.prepare(
2204
+ `select id, run_id, kind, name, path, sha256, created_at
2205
+ from artifacts
2206
+ where run_id = ?
2207
+ order by created_at asc`
2208
+ ).all(runId);
2209
+ return rows.map(fromArtifactRow);
2210
+ }
2211
+ linkArtifactToEvent(eventId, artifactId) {
2212
+ this.transaction(() => {
2213
+ const row = this.db.prepare("select artifact_ids_json from events where id = ?").get(eventId);
2214
+ if (!row) {
2215
+ throw new AgentLoopError("storage_error", `Event not found: ${eventId}`);
2216
+ }
2217
+ const ids = row.artifact_ids_json ? parseJson(row.artifact_ids_json, "Stored artifact id list is invalid.") : [];
2218
+ if (!ids.includes(artifactId)) {
2219
+ ids.push(artifactId);
2220
+ }
2221
+ this.db.prepare("update events set artifact_ids_json = ? where id = ?").run(JSON.stringify(ids), eventId);
2222
+ });
2223
+ }
2224
+ getCurrentRun() {
2225
+ const row = this.db.prepare(
2226
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
2227
+ from runs
2228
+ order by updated_at desc
2229
+ limit 1`
2230
+ ).get();
2231
+ return row ? fromRunRow(row) : void 0;
2232
+ }
2233
+ listRuns(limit = 50) {
2234
+ const rows = this.db.prepare(
2235
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
2236
+ from runs
2237
+ order by updated_at desc
2238
+ limit ?`
2239
+ ).all(limit);
2240
+ return rows.map(fromRunRow);
2241
+ }
2242
+ /** Run a group of read queries against one SQLite snapshot. */
2243
+ readTransaction(fn) {
2244
+ this.db.exec("BEGIN");
2245
+ try {
2246
+ const result = fn();
2247
+ this.db.exec("COMMIT");
2248
+ return result;
2249
+ } catch (error) {
2250
+ try {
2251
+ this.db.exec("ROLLBACK");
2252
+ } catch (rollbackError) {
2253
+ throw new AgentLoopError("storage_error", "Read transaction rollback failed.", {
2254
+ details: {
2255
+ cause: error instanceof Error ? error.message : String(error),
2256
+ rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
2257
+ }
2258
+ });
2259
+ }
2260
+ throw error;
2261
+ }
2262
+ }
2263
+ ensureSchema() {
2264
+ const currentVersion = this.getUserVersion();
2265
+ if (currentVersion !== 0 && !isSupportedSchemaVersion(currentVersion)) {
2266
+ throw new AgentLoopError(
2267
+ "storage_schema_mismatch",
2268
+ `SQLite schema version ${currentVersion} is not supported.`,
2269
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
2270
+ );
2271
+ }
2272
+ if (currentVersion === STORAGE_SCHEMA_VERSION) {
2273
+ if (this.mode !== "ro") {
2274
+ this.transaction(() => this.reconcileHighFidelityWorkerEventsV8());
2275
+ }
2276
+ return;
2277
+ }
2278
+ if (this.mode === "ro") {
2279
+ throw new AgentLoopError(
2280
+ "storage_schema_mismatch",
2281
+ `SQLite schema version ${currentVersion} requires migration before read-only use.`,
2282
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: currentVersion } }
2283
+ );
2284
+ }
2285
+ this.transaction(() => {
2286
+ const lockedVersion = this.getUserVersion();
2287
+ if (lockedVersion === STORAGE_SCHEMA_VERSION) {
2288
+ return;
2289
+ }
2290
+ if (lockedVersion !== 0 && !isSupportedSchemaVersion(lockedVersion)) {
2291
+ throw new AgentLoopError(
2292
+ "storage_schema_mismatch",
2293
+ `SQLite schema version ${lockedVersion} is not supported.`,
2294
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: lockedVersion } }
2295
+ );
2296
+ }
2297
+ this.db.exec(SCHEMA_SQL);
2298
+ this.migratePrC();
2299
+ this.migratePrD();
2300
+ this.migratePrE();
2301
+ this.migrateF0();
2302
+ this.migrateTimelineV7();
2303
+ this.migrateHighFidelityWorkerEventsV8();
2304
+ this.markSchemaVersion();
2305
+ });
2306
+ }
2307
+ migratePrC() {
2308
+ addColumnIfMissing(this.db, "runs", "current_state", "text");
2309
+ addColumnIfMissing(this.db, "runs", "branch", "text");
2310
+ addColumnIfMissing(this.db, "runs", "worktree_clean", "integer");
2311
+ addColumnIfMissing(this.db, "runs", "started_at", "text");
2312
+ addColumnIfMissing(this.db, "runs", "stopped_at", "text");
2313
+ addColumnIfMissing(this.db, "states", "state", "text");
2314
+ addColumnIfMissing(this.db, "states", "payload_json", "text");
2315
+ addColumnIfMissing(this.db, "events", "state_before", "text");
2316
+ addColumnIfMissing(this.db, "events", "state_after", "text");
2317
+ addColumnIfMissing(this.db, "events", "artifact_ids_json", "text");
2318
+ addColumnIfMissing(this.db, "artifacts", "name", "text");
2319
+ addColumnIfMissing(this.db, "artifacts", "sha256", "text");
2320
+ this.db.exec(PR_C_TABLES_SQL);
2321
+ }
2322
+ migratePrD() {
2323
+ this.db.exec(PR_D_TABLES_SQL);
2324
+ }
2325
+ migratePrE() {
2326
+ addColumnIfMissing(this.db, "gates", "decision_note", "text");
2327
+ addColumnIfMissing(this.db, "gates", "decided_at", "text");
2328
+ this.db.exec(PR_E_TABLES_SQL);
2329
+ this.db.exec(PR_E_INDEXES_SQL);
2330
+ }
2331
+ migrateF0() {
2332
+ rebuildEventsWithSeq(this.db);
2333
+ rebuildWorkerEventsWithSeq(this.db);
2334
+ }
2335
+ migrateTimelineV7() {
2336
+ this.db.exec(TIMELINE_INDEX_SQL);
2337
+ this.db.exec(TIMELINE_TRIGGERS_SQL);
2338
+ backfillTimelineIndex(this.db);
2339
+ }
2340
+ migrateHighFidelityWorkerEventsV8() {
2341
+ addColumnIfMissing(this.db, "worker_events", "item_id", "text");
2342
+ addColumnIfMissing(this.db, "worker_events", "item_status", "text");
2343
+ addColumnIfMissing(this.db, "worker_events", "thread_id", "text");
2344
+ addColumnIfMissing(this.db, "worker_events", "backend", "text");
2345
+ addColumnIfMissing(this.db, "worker_events", "artifact_ids_json", "text");
2346
+ this.reconcileHighFidelityWorkerEventsV8();
2347
+ }
2348
+ reconcileHighFidelityWorkerEventsV8() {
2349
+ dedupeHighFidelityWorkerEventsV8(this.db);
2350
+ this.db.exec(`
2351
+ drop index if exists worker_events_thread_item_unique;
2352
+ create unique index if not exists worker_events_thread_item_status_unique
2353
+ on worker_events(thread_id, item_id, coalesce(item_status, ''))
2354
+ where item_id is not null;
2355
+ create unique index if not exists worker_events_thread_event_unique
2356
+ on worker_events(thread_id, event_type)
2357
+ where item_id is null;
2358
+ `);
2359
+ }
2360
+ markSchemaVersion() {
2361
+ this.db.exec(`PRAGMA user_version = ${STORAGE_SCHEMA_VERSION}`);
2362
+ }
2363
+ ensureRepoConfigVersion() {
2364
+ this.validateRepoConfigVersion(true);
2365
+ }
2366
+ validateRepoConfigVersion(rewrite = false) {
2367
+ let row;
2368
+ try {
2369
+ row = this.db.prepare("select schema_version, config_json from repo_config where id = 1").get();
2370
+ } catch (error) {
2371
+ throw toStorageError(error, "Could not read stored repo config metadata.");
2372
+ }
2373
+ if (!row) {
2374
+ return;
2375
+ }
2376
+ if (!isSupportedSchemaVersion(row.schema_version)) {
2377
+ throw new AgentLoopError(
2378
+ "storage_schema_mismatch",
2379
+ `Stored repo config schema version ${row.schema_version} is not supported.`,
2380
+ { details: { expected: STORAGE_SCHEMA_VERSION, actual: row.schema_version } }
2381
+ );
2382
+ }
2383
+ const parsed = parseJson(row.config_json, "Stored repo config snapshot JSON is invalid.");
2384
+ if (parsed.schemaVersion === STORAGE_SCHEMA_VERSION) {
2385
+ return;
2386
+ }
2387
+ if (rewrite && isSupportedSchemaVersion(parsed.schemaVersion ?? 0) && typeof parsed.repoId === "string") {
2388
+ this.writeRepoConfig(withConfigDefaults(parsed));
2389
+ return;
2390
+ }
2391
+ throw new AgentLoopError("storage_error", "Stored repo config snapshot schemaVersion is invalid.", {
2392
+ details: { expected: STORAGE_SCHEMA_VERSION, actual: parsed.schemaVersion }
2393
+ });
2394
+ }
2395
+ getUserVersion() {
2396
+ const row = this.db.prepare("PRAGMA user_version").get();
2397
+ return row.user_version;
2398
+ }
2399
+ getRun(runId) {
2400
+ const row = this.db.prepare(
2401
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
2402
+ from runs
2403
+ where id = ?`
2404
+ ).get(runId);
2405
+ if (!row) {
2406
+ throw new AgentLoopError("storage_error", `Run not found: ${runId}`);
2407
+ }
2408
+ return fromRunRow(row);
2409
+ }
2410
+ getActiveRun() {
2411
+ const row = this.db.prepare(
2412
+ `select id, status, current_state, version, branch, worktree_clean, started_at, stopped_at, created_at, updated_at
2413
+ from runs
2414
+ where status = 'RUNNING'
2415
+ order by updated_at desc
2416
+ limit 1`
2417
+ ).get();
2418
+ return row ? fromRunRow(row) : void 0;
2419
+ }
2420
+ getWorker(workerId) {
2421
+ const row = this.db.prepare(
2422
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
2423
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
2424
+ from workers
2425
+ where id = ?`
2426
+ ).get(workerId);
2427
+ if (!row) {
2428
+ throw new AgentLoopError("storage_error", `Worker not found: ${workerId}`);
2429
+ }
2430
+ return fromWorkerRow(row);
2431
+ }
2432
+ timelineEntry(row) {
2433
+ if (!isTimelineSource(row.source)) {
2434
+ return void 0;
2435
+ }
2436
+ if (row.source === "event") {
2437
+ const sourceRow2 = this.db.prepare(
2438
+ `select seq, id, run_id, kind, message, artifact_ids_json, created_at
2439
+ from events where id = ?`
2440
+ ).get(row.source_id);
2441
+ if (!sourceRow2) return void 0;
2442
+ const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored event artifact list JSON is invalid.") : void 0;
2443
+ return timelineEntry(row, {
2444
+ kind: sourceRow2.kind,
2445
+ title: sourceRow2.kind,
2446
+ summary: sourceRow2.message,
2447
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
2448
+ ...artifactIds ? { artifactIds } : {},
2449
+ rawRef: { table: "events", id: sourceRow2.id, seq: sourceRow2.seq }
2450
+ });
2451
+ }
2452
+ if (row.source === "worker_event") {
2453
+ const sourceRow2 = this.db.prepare(
2454
+ `select seq, id, worker_id, run_id, event_type, item_type, item_id, item_status,
2455
+ thread_id, backend, summary_json, usage_json, artifact_ids_json, created_at
2456
+ from worker_events where id = ?`
2457
+ ).get(row.source_id);
2458
+ if (!sourceRow2) return void 0;
2459
+ const worker = this.db.prepare("select thread_id from workers where id = ?").get(sourceRow2.worker_id);
2460
+ const summary = sourceRow2.summary_json ? summarizeTimelinePayload(parseJson(sourceRow2.summary_json, "Stored worker event summary JSON is invalid.")) : sourceRow2.event_type;
2461
+ const artifactIds = sourceRow2.artifact_ids_json ? parseJson(sourceRow2.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") : void 0;
2462
+ return timelineEntry(row, {
2463
+ kind: sourceRow2.item_type ?? sourceRow2.event_type,
2464
+ title: workerEventTimelineTitle(sourceRow2),
2465
+ summary,
2466
+ runId: sourceRow2.run_id,
2467
+ workerId: sourceRow2.worker_id,
2468
+ ...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : worker?.thread_id ? { threadId: worker.thread_id } : {},
2469
+ ...sourceRow2.item_status ? { status: sourceRow2.item_status } : {},
2470
+ ...artifactIds?.length ? { artifactIds } : {},
2471
+ rawRef: { table: "worker_events", id: sourceRow2.id, seq: sourceRow2.seq }
2472
+ });
2473
+ }
2474
+ if (row.source === "worker") {
2475
+ const sourceRow2 = this.db.prepare(
2476
+ `select id, run_id, type, backend, status, thread_id, attempt, resume_used,
2477
+ started_at, completed_at, exit_code, result_artifact_id, raw_jsonl_artifact_id, error
2478
+ from workers where id = ?`
2479
+ ).get(row.worker_id ?? workerIdFromSourceId(row.source_id));
2480
+ if (!sourceRow2) return void 0;
2481
+ const status = statusFromWorkerSourceId(row.source_id) ?? sourceRow2.status;
2482
+ return timelineEntry(row, {
2483
+ kind: sourceRow2.type,
2484
+ title: `${sourceRow2.type} worker ${status}`,
2485
+ summary: summarizeTimelinePayload({
2486
+ status,
2487
+ attempt: sourceRow2.attempt,
2488
+ backend: sourceRow2.backend,
2489
+ exitCode: sourceRow2.exit_code,
2490
+ error: sourceRow2.error
2491
+ }),
2492
+ runId: sourceRow2.run_id,
2493
+ workerId: sourceRow2.id,
2494
+ ...sourceRow2.thread_id ? { threadId: sourceRow2.thread_id } : {},
2495
+ status,
2496
+ artifactIds: [sourceRow2.result_artifact_id, sourceRow2.raw_jsonl_artifact_id].filter((id) => Boolean(id)),
2497
+ rawRef: { table: "workers", id: row.source_id }
2498
+ });
2499
+ }
2500
+ if (row.source === "state") {
2501
+ const sourceRow2 = this.db.prepare("select id, run_id, status, state, version, created_at from states where id = ?").get(Number(row.source_id));
2502
+ if (!sourceRow2) return void 0;
2503
+ return timelineEntry(row, {
2504
+ kind: sourceRow2.state ?? sourceRow2.status,
2505
+ title: "State changed",
2506
+ summary: summarizeTimelinePayload({ status: sourceRow2.status, state: sourceRow2.state, version: sourceRow2.version }),
2507
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
2508
+ status: sourceRow2.status,
2509
+ rawRef: { table: "states", id: String(sourceRow2.id), seq: sourceRow2.id }
2510
+ });
2511
+ }
2512
+ if (row.source === "gate") {
2513
+ const sourceRow2 = this.db.prepare(
2514
+ `select id, run_id, kind, status, message, details_json, created_at,
2515
+ resolved_at, decision_note, decided_at
2516
+ from gates where id = ?`
2517
+ ).get(row.source_id);
2518
+ if (!sourceRow2) return void 0;
2519
+ return timelineEntry(row, {
2520
+ kind: sourceRow2.kind,
2521
+ title: `Gate opened: ${sourceRow2.kind}`,
2522
+ summary: sourceRow2.message,
2523
+ ...sourceRow2.run_id ? { runId: sourceRow2.run_id } : {},
2524
+ status: sourceRow2.status,
2525
+ rawRef: { table: "gates", id: sourceRow2.id }
2526
+ });
2527
+ }
2528
+ if (row.source === "artifact") {
2529
+ const sourceRow2 = this.db.prepare("select id, run_id, kind, name, path, sha256, created_at from artifacts where id = ?").get(row.source_id);
2530
+ if (!sourceRow2) return void 0;
2531
+ return timelineEntry(row, {
2532
+ kind: sourceRow2.kind,
2533
+ title: `Artifact: ${sourceRow2.name ?? sourceRow2.id}`,
2534
+ summary: summarizeTimelinePayload({ name: sourceRow2.name ?? sourceRow2.id, kind: sourceRow2.kind, sha256: sourceRow2.sha256 }),
2535
+ runId: sourceRow2.run_id,
2536
+ artifactIds: [sourceRow2.id],
2537
+ rawRef: { table: "artifacts", id: sourceRow2.id }
2538
+ });
2539
+ }
2540
+ const sourceRow = this.db.prepare("select id, run_id, kind, message, created_at from decisions where id = ?").get(row.source_id);
2541
+ if (!sourceRow) return void 0;
2542
+ return timelineEntry(row, {
2543
+ kind: sourceRow.kind,
2544
+ title: sourceRow.kind,
2545
+ summary: sourceRow.message,
2546
+ runId: sourceRow.run_id,
2547
+ rawRef: { table: "decisions", id: sourceRow.id }
2548
+ });
2549
+ }
2550
+ transaction(fn) {
2551
+ this.db.exec("BEGIN IMMEDIATE");
2552
+ try {
2553
+ const result = fn();
2554
+ this.db.exec("COMMIT");
2555
+ return result;
2556
+ } catch (error) {
2557
+ try {
2558
+ this.db.exec("ROLLBACK");
2559
+ } catch (rollbackError) {
2560
+ throw new AgentLoopError("storage_error", "Transaction rollback failed.", {
2561
+ details: {
2562
+ cause: error instanceof Error ? error.message : String(error),
2563
+ rollback: rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
2564
+ }
2565
+ });
2566
+ }
2567
+ throw error;
2568
+ }
2569
+ }
2570
+ };
2571
+ function timelineEntry(row, entry) {
2572
+ return {
2573
+ timelineSeq: row.timeline_seq,
2574
+ occurredAt: row.created_at,
2575
+ cursor: encodeTimelineCursor(row.timeline_seq, row.created_at),
2576
+ source: row.source,
2577
+ kind: entry.kind,
2578
+ ...entry.runId ? { runId: entry.runId } : {},
2579
+ ...entry.workerId ? { workerId: entry.workerId } : {},
2580
+ ...entry.threadId ? { threadId: entry.threadId } : {},
2581
+ title: truncateTimelineText(redactTimelineText(entry.title), 160),
2582
+ summary: truncateTimelineText(redactTimelineText(entry.summary), 1e3),
2583
+ ...entry.status ? { status: entry.status } : {},
2584
+ ...entry.artifactIds?.length ? { artifactIds: entry.artifactIds } : {},
2585
+ createdAt: row.created_at,
2586
+ rawRef: entry.rawRef
2587
+ };
2588
+ }
2589
+ function backfillTimelineIndex(db) {
2590
+ db.exec(`
2591
+ insert or ignore into timeline_index (source, source_id, source_seq, run_id, worker_id, created_at)
2592
+ select source, source_id, source_seq, run_id, worker_id, created_at
2593
+ from (
2594
+ select 'event' as source, id as source_id, seq as source_seq, run_id, null as worker_id, created_at
2595
+ from events
2596
+ union all
2597
+ select 'worker_event' as source, id as source_id, seq as source_seq, run_id, worker_id, created_at
2598
+ from worker_events
2599
+ union all
2600
+ select 'worker' as source, id || ':' || status as source_id, null as source_seq, run_id, id as worker_id, started_at as created_at
2601
+ from workers
2602
+ union all
2603
+ select 'state' as source, cast(id as text) as source_id, id as source_seq, run_id, null as worker_id, created_at
2604
+ from states
2605
+ union all
2606
+ select 'gate' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
2607
+ from gates
2608
+ union all
2609
+ select 'artifact' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
2610
+ from artifacts
2611
+ union all
2612
+ select 'decision' as source, id as source_id, null as source_seq, run_id, null as worker_id, created_at
2613
+ from decisions
2614
+ )
2615
+ order by created_at asc, source asc, source_id asc;
2616
+ `);
2617
+ }
2618
+ function normalizeTimelineSources(sources) {
2619
+ const unique = [...new Set(sources)];
2620
+ if (unique.some((source) => !isTimelineSource(source))) {
2621
+ throw new AgentLoopError("invalid_config", "Unsupported timeline source.", { details: { sources } });
2622
+ }
2623
+ return unique;
2624
+ }
2625
+ function isTimelineSource(value) {
2626
+ return TIMELINE_SOURCES.includes(value);
2627
+ }
2628
+ function clampLimit(value) {
2629
+ if (!Number.isFinite(value)) {
2630
+ return 50;
2631
+ }
2632
+ return Math.min(Math.max(Math.trunc(value), 1), 200);
2633
+ }
2634
+ function encodeTimelineCursor(timelineSeq, occurredAt) {
2635
+ return Buffer.from(JSON.stringify({ timelineSeq, ...occurredAt ? { occurredAt } : {} }), "utf8").toString("base64url");
2636
+ }
2637
+ function decodeTimelineCursor(cursor) {
2638
+ try {
2639
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
2640
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2641
+ const timelineSeq = parsed.timelineSeq;
2642
+ const occurredAt = parsed.occurredAt;
2643
+ if (typeof timelineSeq === "number" && Number.isInteger(timelineSeq) && timelineSeq > 0 && typeof occurredAt === "string" && occurredAt.length > 0) {
2644
+ return { timelineSeq, occurredAt };
2645
+ }
2646
+ }
2647
+ } catch {
2648
+ }
2649
+ throw new AgentLoopError("invalid_config", "Timeline cursor is invalid.");
2650
+ }
2651
+ function timelineMissingSourceRows(db) {
2652
+ const checks = [
2653
+ {
2654
+ source: "event",
2655
+ sql: `select count(*) as count
2656
+ from events source
2657
+ left join timeline_index ti on ti.source = 'event' and ti.source_id = source.id
2658
+ where ti.timeline_seq is null`
2659
+ },
2660
+ {
2661
+ source: "worker_event",
2662
+ sql: `select count(*) as count
2663
+ from worker_events source
2664
+ left join timeline_index ti on ti.source = 'worker_event' and ti.source_id = source.id
2665
+ where ti.timeline_seq is null`
2666
+ },
2667
+ {
2668
+ source: "worker",
2669
+ sql: `select count(*) as count
2670
+ from workers source
2671
+ left join timeline_index ti on ti.source = 'worker' and ti.source_id = source.id || ':' || source.status
2672
+ where ti.timeline_seq is null`
2673
+ },
2674
+ {
2675
+ source: "state",
2676
+ sql: `select count(*) as count
2677
+ from states source
2678
+ left join timeline_index ti on ti.source = 'state' and ti.source_id = cast(source.id as text)
2679
+ where ti.timeline_seq is null`
2680
+ },
2681
+ {
2682
+ source: "gate",
2683
+ sql: `select count(*) as count
2684
+ from gates source
2685
+ left join timeline_index ti on ti.source = 'gate' and ti.source_id = source.id
2686
+ where ti.timeline_seq is null`
2687
+ },
2688
+ {
2689
+ source: "artifact",
2690
+ sql: `select count(*) as count
2691
+ from artifacts source
2692
+ left join timeline_index ti on ti.source = 'artifact' and ti.source_id = source.id
2693
+ where ti.timeline_seq is null`
2694
+ },
2695
+ {
2696
+ source: "decision",
2697
+ sql: `select count(*) as count
2698
+ from decisions source
2699
+ left join timeline_index ti on ti.source = 'decision' and ti.source_id = source.id
2700
+ where ti.timeline_seq is null`
2701
+ }
2702
+ ];
2703
+ return checks.flatMap((check) => {
2704
+ const row = db.prepare(check.sql).get();
2705
+ const missing = row?.count ?? 0;
2706
+ return missing > 0 ? [{ source: check.source, missing }] : [];
2707
+ });
2708
+ }
2709
+ function summarizeTimelinePayload(value) {
2710
+ if (typeof value === "string") {
2711
+ return value;
2712
+ }
2713
+ if (value === void 0 || value === null) {
2714
+ return "";
2715
+ }
2716
+ return JSON.stringify(redactTimelineValue(value));
2717
+ }
2718
+ function redactTimelineValue(value) {
2719
+ if (Array.isArray(value)) {
2720
+ return value.slice(0, 20).map(redactTimelineValue);
2721
+ }
2722
+ if (typeof value !== "object" || value === null) {
2723
+ return value;
2724
+ }
2725
+ const redacted = {};
2726
+ for (const [key, nested] of Object.entries(value).slice(0, 40)) {
2727
+ redacted[key] = isSecretKey(key) ? "[redacted]" : redactTimelineValue(nested);
2728
+ }
2729
+ return redacted;
2730
+ }
2731
+ function redactTimelineText(value) {
2732
+ return redactSecrets(value);
2733
+ }
2734
+ function truncateTimelineText(value, maxLength) {
2735
+ return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
2736
+ }
2737
+ function statusFromWorkerSourceId(sourceId) {
2738
+ const status = sourceId.split(":").at(-1);
2739
+ return status && ["running", "succeeded", "failed", "timed_out", "invalid_output"].includes(status) ? status : void 0;
2740
+ }
2741
+ function workerIdFromSourceId(sourceId) {
2742
+ return sourceId.split(":")[0] ?? sourceId;
2743
+ }
2744
+ function fromRunRow(row) {
2745
+ return {
2746
+ id: row.id,
2747
+ status: row.status,
2748
+ ...row.current_state ? { currentState: row.current_state } : {},
2749
+ version: row.version,
2750
+ ...row.branch ? { branch: row.branch } : {},
2751
+ ...row.worktree_clean !== null ? { worktreeClean: row.worktree_clean === 1 } : {},
2752
+ createdAt: row.created_at,
2753
+ updatedAt: row.updated_at,
2754
+ ...row.started_at ? { startedAt: row.started_at } : {},
2755
+ ...row.stopped_at ? { stoppedAt: row.stopped_at } : {}
2756
+ };
2757
+ }
2758
+ function fromEventRow(row) {
2759
+ return {
2760
+ id: row.id,
2761
+ seq: row.seq,
2762
+ ...row.run_id ? { runId: row.run_id } : {},
2763
+ kind: row.kind,
2764
+ message: row.message,
2765
+ ...row.state_before ? { stateBefore: row.state_before } : {},
2766
+ ...row.state_after ? { stateAfter: row.state_after } : {},
2767
+ ...row.payload_json ? { payload: parseJson(row.payload_json, "Stored event payload JSON is invalid.") } : {},
2768
+ ...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored event artifact list JSON is invalid.") } : {},
2769
+ createdAt: row.created_at
2770
+ };
2771
+ }
2772
+ function statusGateFromRow(row) {
2773
+ return {
2774
+ kind: row.kind,
2775
+ message: row.message,
2776
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {}
2777
+ };
2778
+ }
2779
+ function latestGateSatisfied(db, runId) {
2780
+ const row = db.prepare(
2781
+ `select status
2782
+ from gates
2783
+ where run_id = ?
2784
+ order by created_at desc
2785
+ limit 1`
2786
+ ).get(runId);
2787
+ return row?.status === "approved" || row?.status === "resolved";
2788
+ }
2789
+ function fromGateRow(row) {
2790
+ return {
2791
+ id: row.id,
2792
+ ...row.run_id ? { runId: row.run_id } : {},
2793
+ kind: row.kind,
2794
+ status: row.status,
2795
+ message: row.message,
2796
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored gate details JSON is invalid.") } : {},
2797
+ createdAt: row.created_at,
2798
+ ...row.resolved_at ? { resolvedAt: row.resolved_at } : {},
2799
+ ...row.decision_note ? { decisionNote: row.decision_note } : {},
2800
+ ...row.decided_at ? { decidedAt: row.decided_at } : {}
2801
+ };
2802
+ }
2803
+ function fromArtifactRow(row) {
2804
+ return {
2805
+ id: row.id,
2806
+ runId: row.run_id,
2807
+ kind: row.kind,
2808
+ name: row.name ?? row.id,
2809
+ path: row.path,
2810
+ sha256: row.sha256 ?? "",
2811
+ createdAt: row.created_at
2812
+ };
2813
+ }
2814
+ function fromPrLinkRow(row) {
2815
+ return {
2816
+ id: row.id,
2817
+ runId: row.run_id,
2818
+ branch: row.branch,
2819
+ prNumber: row.pr_number,
2820
+ url: row.url,
2821
+ headRef: row.head_ref,
2822
+ baseRef: row.base_ref,
2823
+ state: row.state,
2824
+ draft: row.draft === 1,
2825
+ createdAt: row.created_at,
2826
+ updatedAt: row.updated_at
2827
+ };
2828
+ }
2829
+ function fromCiCheckRow(row) {
2830
+ return {
2831
+ id: row.id,
2832
+ runId: row.run_id,
2833
+ prNumber: row.pr_number,
2834
+ name: row.name,
2835
+ status: row.status,
2836
+ ...row.conclusion ? { conclusion: row.conclusion } : {},
2837
+ ...row.url ? { url: row.url } : {},
2838
+ ...row.started_at ? { startedAt: row.started_at } : {},
2839
+ ...row.completed_at ? { completedAt: row.completed_at } : {},
2840
+ observedAt: row.observed_at
2841
+ };
2842
+ }
2843
+ function fromReviewCommentRow(row) {
2844
+ return {
2845
+ id: row.id,
2846
+ runId: row.run_id,
2847
+ prNumber: row.pr_number,
2848
+ commentId: row.comment_id,
2849
+ url: row.url,
2850
+ author: row.author,
2851
+ body: row.body,
2852
+ path: row.path,
2853
+ ...row.line === null ? {} : { line: row.line },
2854
+ diffHunk: row.diff_hunk,
2855
+ isResolved: row.is_resolved === 1,
2856
+ isOutdated: row.is_outdated === 1,
2857
+ actionable: row.actionable === 1,
2858
+ status: row.status,
2859
+ observedAt: row.observed_at
2860
+ };
2861
+ }
2862
+ function fromDecisionRow(row) {
2863
+ return {
2864
+ id: row.id,
2865
+ runId: row.run_id,
2866
+ kind: row.kind,
2867
+ message: row.message,
2868
+ ...row.details_json ? { details: parseJson(row.details_json, "Stored decision details JSON is invalid.") } : {},
2869
+ createdAt: row.created_at
2870
+ };
2871
+ }
2872
+ function fromRunCheckRow(row) {
2873
+ return {
2874
+ runId: row.run_id,
2875
+ kind: row.kind,
2876
+ status: row.status,
2877
+ ...row.details_json ? { details: JSON.parse(row.details_json) } : {},
2878
+ createdAt: row.created_at
2879
+ };
2880
+ }
2881
+ function fromWorkerRow(row) {
2882
+ return {
2883
+ id: row.id,
2884
+ runId: row.run_id,
2885
+ type: row.type,
2886
+ backend: row.backend,
2887
+ status: row.status,
2888
+ ...row.thread_id ? { threadId: row.thread_id } : {},
2889
+ attempt: row.attempt,
2890
+ resumeUsed: row.resume_used === 1,
2891
+ startedAt: row.started_at,
2892
+ ...row.completed_at ? { completedAt: row.completed_at } : {},
2893
+ ...row.exit_code === null ? {} : { exitCode: row.exit_code },
2894
+ ...row.result_artifact_id ? { resultArtifactId: row.result_artifact_id } : {},
2895
+ ...row.raw_jsonl_artifact_id ? { rawJsonlArtifactId: row.raw_jsonl_artifact_id } : {},
2896
+ ...row.error ? { error: row.error } : {}
2897
+ };
2898
+ }
2899
+ function fromWorkerEventRow(row) {
2900
+ return {
2901
+ id: row.id,
2902
+ seq: row.seq,
2903
+ workerId: row.worker_id,
2904
+ runId: row.run_id,
2905
+ eventType: row.event_type,
2906
+ ...row.item_type ? { itemType: row.item_type } : {},
2907
+ ...row.item_id ? { itemId: row.item_id } : {},
2908
+ ...row.item_status ? { itemStatus: row.item_status } : {},
2909
+ ...row.thread_id ? { threadId: row.thread_id } : {},
2910
+ ...row.backend ? { backend: row.backend } : {},
2911
+ ...row.summary_json ? { summary: parseJson(row.summary_json, "Stored worker event summary JSON is invalid.") } : {},
2912
+ ...row.usage_json ? { usage: parseJson(row.usage_json, "Stored worker event usage JSON is invalid.") } : {},
2913
+ ...row.artifact_ids_json ? { artifactIds: parseJson(row.artifact_ids_json, "Stored worker event artifact list JSON is invalid.") } : {},
2914
+ createdAt: row.created_at
2915
+ };
2916
+ }
2917
+ function workerEventTimelineTitle(row) {
2918
+ const item = row.item_type ?? row.event_type;
2919
+ return row.item_status ? `${row.item_status} ${item}` : item;
2920
+ }
2921
+ function isSupportedSchemaVersion(value) {
2922
+ return SUPPORTED_SCHEMA_VERSIONS.includes(value);
2923
+ }
2924
+ function rebuildEventsWithSeq(db) {
2925
+ if (hasColumn(db, "events", "seq")) {
2926
+ return;
2927
+ }
2928
+ db.exec(`
2929
+ alter table events rename to events_legacy_v6;
2930
+ create table events (
2931
+ seq integer primary key autoincrement,
2932
+ id text not null unique,
2933
+ run_id text,
2934
+ kind text not null,
2935
+ message text not null,
2936
+ state_before text,
2937
+ state_after text,
2938
+ payload_json text,
2939
+ artifact_ids_json text,
2940
+ created_at text not null,
2941
+ foreign key(run_id) references runs(id)
2942
+ );
2943
+ insert into events (
2944
+ id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
2945
+ )
2946
+ select id, run_id, kind, message, state_before, state_after, payload_json, artifact_ids_json, created_at
2947
+ from events_legacy_v6
2948
+ order by created_at asc, id asc;
2949
+ drop table events_legacy_v6;
2950
+ `);
2951
+ }
2952
+ function rebuildWorkerEventsWithSeq(db) {
2953
+ if (hasColumn(db, "worker_events", "seq")) {
2954
+ return;
2955
+ }
2956
+ db.exec(`
2957
+ alter table worker_events rename to worker_events_legacy_v6;
2958
+ create table worker_events (
2959
+ seq integer primary key autoincrement,
2960
+ id text not null unique,
2961
+ worker_id text not null,
2962
+ run_id text not null,
2963
+ event_type text not null,
2964
+ item_type text,
2965
+ summary_json text,
2966
+ usage_json text,
2967
+ created_at text not null,
2968
+ foreign key(worker_id) references workers(id),
2969
+ foreign key(run_id) references runs(id)
2970
+ );
2971
+ insert into worker_events (
2972
+ id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
2973
+ )
2974
+ select id, worker_id, run_id, event_type, item_type, summary_json, usage_json, created_at
2975
+ from worker_events_legacy_v6
2976
+ order by created_at asc, id asc;
2977
+ drop table worker_events_legacy_v6;
2978
+ `);
2979
+ }
2980
+ function dedupeHighFidelityWorkerEventsV8(db) {
2981
+ db.exec(`
2982
+ create temp table if not exists worker_event_dedupe_ids (
2983
+ id text primary key
2984
+ );
2985
+ delete from worker_event_dedupe_ids;
2986
+ insert or ignore into worker_event_dedupe_ids (id)
2987
+ select id from (
2988
+ select id from (
2989
+ select id,
2990
+ seq,
2991
+ row_number() over (
2992
+ partition by thread_id, item_id, coalesce(item_status, '')
2993
+ order by seq asc
2994
+ ) as duplicate_rank
2995
+ from worker_events
2996
+ where thread_id is not null and item_id is not null
2997
+ )
2998
+ where duplicate_rank > 1
2999
+ );
3000
+ insert or ignore into worker_event_dedupe_ids (id)
3001
+ select id from (
3002
+ select id from (
3003
+ select id,
3004
+ seq,
3005
+ row_number() over (
3006
+ partition by thread_id, event_type
3007
+ order by seq asc
3008
+ ) as duplicate_rank
3009
+ from worker_events
3010
+ where thread_id is not null and item_id is null
3011
+ )
3012
+ where duplicate_rank > 1
3013
+ );
3014
+ delete from timeline_index
3015
+ where source = 'worker_event'
3016
+ and source_id in (select id from worker_event_dedupe_ids);
3017
+ delete from worker_events
3018
+ where id in (select id from worker_event_dedupe_ids);
3019
+ delete from worker_event_dedupe_ids;
3020
+ `);
3021
+ }
3022
+ function hasColumn(db, tableName, columnName) {
3023
+ validateSqlIdentifier(tableName);
3024
+ validateSqlIdentifier(columnName);
3025
+ const columns = db.prepare(`pragma table_info(${tableName})`).all();
3026
+ return columns.some((column) => column.name === columnName);
3027
+ }
3028
+ function hasTable(db, tableName) {
3029
+ validateSqlIdentifier(tableName);
3030
+ const row = db.prepare("select 1 from sqlite_master where type = 'table' and name = ? limit 1").get(tableName);
3031
+ return row !== void 0;
3032
+ }
3033
+ function boolToDb(value) {
3034
+ if (value === void 0) {
3035
+ return null;
3036
+ }
3037
+ return value ? 1 : 0;
3038
+ }
3039
+ function addColumnIfMissing(db, tableName, columnName, definition) {
3040
+ validateSqlIdentifier(tableName);
3041
+ validateSqlIdentifier(columnName);
3042
+ if (!hasColumn(db, tableName, columnName)) {
3043
+ db.exec(`alter table ${tableName} add column ${columnName} ${definition}`);
3044
+ }
3045
+ }
3046
+ function validateSqlIdentifier(value) {
3047
+ if (!/^[a-z0-9_]+$/.test(value)) {
3048
+ throw new AgentLoopError("storage_error", `Unsafe SQLite identifier: ${value}`);
3049
+ }
3050
+ }
3051
+ function now() {
3052
+ return (/* @__PURE__ */ new Date()).toISOString();
3053
+ }
3054
+ function parseJson(value, message) {
3055
+ try {
3056
+ return JSON.parse(value);
3057
+ } catch (error) {
3058
+ throw new AgentLoopError("storage_error", message, {
3059
+ details: { cause: error instanceof Error ? error.message : String(error) }
3060
+ });
3061
+ }
3062
+ }
3063
+ function isUniqueConstraintError(error) {
3064
+ return error instanceof Error && /unique constraint/i.test(error.message);
3065
+ }
3066
+ function toStorageError(error, message) {
3067
+ if (error instanceof AgentLoopError) {
3068
+ return error;
3069
+ }
3070
+ return new AgentLoopError("storage_error", message, {
3071
+ details: { cause: error instanceof Error ? error.message : String(error) }
3072
+ });
3073
+ }
3074
+
3075
+ // plugins/autonomous-pr-loop/core/hook-policy.ts
3076
+ function commandFromHookPayload(payload) {
3077
+ if (!isRecord(payload)) {
3078
+ return void 0;
3079
+ }
3080
+ const toolInput = isRecord(payload.tool_input) ? payload.tool_input : payload;
3081
+ const file = stringValue2(toolInput.file ?? toolInput.cmd ?? toolInput.executable);
3082
+ const args = Array.isArray(toolInput.args) ? toolInput.args.filter((arg) => typeof arg === "string") : void 0;
3083
+ if (file && args) {
3084
+ return { file: basename(file), args, raw: [file, ...args].join(" ") };
3085
+ }
3086
+ const command = stringValue2(toolInput.command ?? toolInput.cmd ?? toolInput.input);
3087
+ if (!command) {
3088
+ return void 0;
3089
+ }
3090
+ return tokenizeCommand(command);
3091
+ }
3092
+ function evaluateHookPolicy(input2) {
3093
+ const command = unwrapCommand(normalizeCommand(input2.command));
3094
+ const blockedCommand = renderCommand(command);
3095
+ const destructive = destructivePolicy(command);
3096
+ if (destructive) {
3097
+ return deny(blockedCommand, destructive, "policy_violation", "Stop using the destructive command and continue through agent-loop.");
3098
+ }
3099
+ const worker = input2.isWorker === true || process.env.AGENT_LOOP_WORKER_POLICY === "1" || command.raw?.includes("AGENT_LOOP_WORKER_POLICY=1") === true;
3100
+ const workerPolicy = workerLifecyclePolicy(command);
3101
+ if (worker && workerPolicy) {
3102
+ return deny(blockedCommand, workerPolicy, "policy_violation", "Let the supervisor own commit, push, PR, and merge actions.");
3103
+ }
3104
+ const protectedPath = protectedPathPolicy(command, input2.protectedPaths ?? []);
3105
+ if (protectedPath) {
3106
+ return deny(blockedCommand, protectedPath, "policy_violation", "Remove protected path changes from the command.");
3107
+ }
3108
+ const gate = gatedLifecyclePolicy(command, input2.storage);
3109
+ if (gate) {
3110
+ return deny(blockedCommand, gate.policy, gate.gate, gate.nextAction);
3111
+ }
3112
+ if (!matchesHookAllowlist(command)) {
3113
+ return deny(blockedCommand, "command_not_in_hook_allowlist", "policy_violation", "Use agent-loop MCP/CLI control surfaces or an allowlisted read/check command.");
3114
+ }
3115
+ return {
3116
+ allow: true,
3117
+ matchedPolicy: "allow",
3118
+ blockedCommand,
3119
+ nextAction: "Continue.",
3120
+ reason: "No hook policy matched."
3121
+ };
3122
+ }
3123
+ function evaluatePreToolUseHook(payload, repoRoot2) {
3124
+ const command = commandFromHookPayload(payload);
3125
+ if (!command) {
3126
+ return {
3127
+ allow: true,
3128
+ matchedPolicy: "allow_unparsed",
3129
+ blockedCommand: "",
3130
+ nextAction: "Continue.",
3131
+ reason: "Hook payload did not contain a command."
3132
+ };
3133
+ }
3134
+ const route = resolveHookRoute(payload, { legacyRepoRoot: repoRoot2 });
3135
+ if (route.status === "no_match") {
3136
+ return route.worktreeBinding ? routeSessionMismatchDecision(command, route.reason) : {
3137
+ allow: true,
3138
+ matchedPolicy: "hook_routing_no_match",
3139
+ blockedCommand: renderCommand(command),
3140
+ nextAction: "Continue.",
3141
+ reason: route.reason
3142
+ };
3143
+ }
3144
+ if (route.status === "ambiguous") {
3145
+ return {
3146
+ allow: false,
3147
+ matchedPolicy: "hook_routing_ambiguous",
3148
+ gate: "policy_violation",
3149
+ blockedCommand: renderCommand(command),
3150
+ nextAction: "Run `agent-loop hooks doctor` and bind this Codex session to exactly one agent-loop target.",
3151
+ reason: route.reason
3152
+ };
3153
+ }
3154
+ if (route.status === "route_error") {
3155
+ return routeErrorDecision(command, route.reason);
3156
+ }
3157
+ let storage;
3158
+ try {
3159
+ const config = loadConfig(route.binding.repoRoot).config;
3160
+ storage = new SqliteAgentLoopStorage(statePath(route.binding.repoRoot));
3161
+ const decision2 = evaluateHookPolicy({ repoRoot: route.binding.repoRoot, command, storage, protectedPaths: config.protectedPaths });
3162
+ recordHookDecision(storage, decision2, route.binding.runId);
3163
+ return decision2;
3164
+ } catch (error) {
3165
+ const failSafe = evaluateHookPolicy({ repoRoot: route.binding.repoRoot, command });
3166
+ if (!failSafe.allow) {
3167
+ return {
3168
+ ...failSafe,
3169
+ matchedPolicy: `fail_safe:${failSafe.matchedPolicy}`,
3170
+ reason: `Storage unavailable; denied dangerous command. ${error instanceof Error ? error.message : String(error)}`
3171
+ };
3172
+ }
3173
+ return failSafe;
3174
+ } finally {
3175
+ storage?.close();
3176
+ }
3177
+ }
3178
+ function toCodexHookResponse(decision2) {
3179
+ if (decision2.allow) {
3180
+ return { continue: true };
3181
+ }
3182
+ return {
3183
+ decision: "deny",
3184
+ permissionDecision: "deny",
3185
+ continue: false,
3186
+ stopReason: decision2.reason,
3187
+ systemMessage: formatHookMessage(decision2)
3188
+ };
3189
+ }
3190
+ function recordHookDecision(storage, decision2, runId) {
3191
+ const run = runId ? storage.listRuns(200).find((item) => item.id === runId) : storage.getCurrentRun();
3192
+ const command = decision2.blockedCommand;
3193
+ storage.appendEvent({
3194
+ ...run ? { runId: run.id } : {},
3195
+ kind: hookEventKind("PreToolUse"),
3196
+ message: decision2.reason,
3197
+ payload: {
3198
+ allow: decision2.allow,
3199
+ matchedPolicy: decision2.matchedPolicy,
3200
+ ...decision2.gate ? { gate: decision2.gate } : {},
3201
+ nextAction: decision2.nextAction,
3202
+ commandLength: command.length,
3203
+ commandSha256: createHash2("sha256").update(command).digest("hex"),
3204
+ commandPreview: redactSecrets(command.slice(0, 500))
3205
+ }
3206
+ });
3207
+ }
3208
+ function routeErrorDecision(command, reason) {
3209
+ const normalized = unwrapCommand(normalizeCommand(command));
3210
+ const blockedCommand = renderCommand(normalized);
3211
+ const destructive = destructivePolicy(normalized);
3212
+ if (destructive || lifecycleCommand(normalized)) {
3213
+ return deny(
3214
+ blockedCommand,
3215
+ `hook_routing_error${destructive ? `:${destructive}` : ""}`,
3216
+ "policy_violation",
3217
+ "Fix agent-loop hook routing with `agent-loop hooks doctor` before running lifecycle or destructive commands."
3218
+ );
3219
+ }
3220
+ return {
3221
+ allow: true,
3222
+ matchedPolicy: "hook_routing_error_noop",
3223
+ blockedCommand,
3224
+ nextAction: "Continue.",
3225
+ reason: `Hook routing unavailable; no-op for non-lifecycle command. ${reason}`
3226
+ };
3227
+ }
3228
+ function routeSessionMismatchDecision(command, reason) {
3229
+ const normalized = unwrapCommand(normalizeCommand(command));
3230
+ const blockedCommand = renderCommand(normalized);
3231
+ const destructive = destructivePolicy(normalized);
3232
+ if (destructive || lifecycleCommand(normalized)) {
3233
+ return deny(
3234
+ blockedCommand,
3235
+ `hook_routing_session_mismatch${destructive ? `:${destructive}` : ""}`,
3236
+ "policy_violation",
3237
+ "Bind this Codex session explicitly with `agent-loop hooks bind --session ...` before running lifecycle or destructive commands."
3238
+ );
3239
+ }
3240
+ return {
3241
+ allow: true,
3242
+ matchedPolicy: "hook_routing_no_match",
3243
+ blockedCommand,
3244
+ nextAction: "Continue.",
3245
+ reason
3246
+ };
3247
+ }
3248
+ function lifecycleCommand(command) {
3249
+ const args = stripGitGlobalOptions(command.args);
3250
+ return command.file === "git" && ["commit", "push", "merge"].includes(args[0] ?? "") || command.file === "gh" && command.args[0] === "pr" && ["create", "ready", "merge"].includes(command.args[1] ?? "");
3251
+ }
3252
+ function gatedLifecyclePolicy(command, storage) {
3253
+ const args = stripGitGlobalOptions(command.args);
3254
+ const lifecycleCommand2 = command.file === "git" && args[0] === "commit" || command.file === "git" && args[0] === "push" || command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge";
3255
+ if (!lifecycleCommand2) {
3256
+ return void 0;
3257
+ }
3258
+ if (!storage) {
3259
+ return {
3260
+ policy: "storage_required_for_lifecycle",
3261
+ gate: "policy_violation",
3262
+ nextAction: "Run `pnpm agent-loop status` after restoring .agent-loop/state.sqlite."
3263
+ };
3264
+ }
3265
+ const current = storage.getCurrentStatus();
3266
+ const state = current.run?.currentState;
3267
+ if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && state !== "COMMIT_PUSH_PR") {
3268
+ return {
3269
+ policy: "commit_push_state_gate",
3270
+ gate: current.gate?.kind ?? "policy_violation",
3271
+ nextAction: "Resume agent-loop until COMMIT_PUSH_PR owns publishing."
3272
+ };
3273
+ }
3274
+ if (command.file === "git" && (args[0] === "commit" || args[0] === "push") && !publishPrerequisitesSatisfied(storage)) {
3275
+ return {
3276
+ policy: "commit_push_prerequisite_gate",
3277
+ gate: "policy_violation",
3278
+ nextAction: "Run SELF_CHECK and GitNexus detect_changes through agent-loop before publishing."
3279
+ };
3280
+ }
3281
+ if (command.file === "gh" && command.args[0] === "pr" && command.args[1] === "merge" && state !== "MERGE") {
3282
+ return {
3283
+ policy: "merge_state_gate",
3284
+ gate: current.gate?.kind ?? "merge_requires_confirmation",
3285
+ nextAction: "Wait for READY_TO_MERGE/MERGE and explicit approval."
3286
+ };
3287
+ }
3288
+ return void 0;
3289
+ }
3290
+ function destructivePolicy(command) {
3291
+ const args = stripGitGlobalOptions(command.args);
3292
+ if (command.file === "git" && args[0] === "reset" && args.includes("--hard")) {
3293
+ return "destructive_git_reset_hard";
3294
+ }
3295
+ if (command.file === "git" && args[0] === "clean" && args.some((arg) => /^-.*f/.test(arg))) {
3296
+ return "destructive_git_clean";
3297
+ }
3298
+ if (command.file === "git" && args[0] === "push" && args.some((arg) => ["-f", "--force", "--force-with-lease"].includes(arg))) {
3299
+ return "destructive_git_force_push";
3300
+ }
3301
+ if (command.file === "gh" && command.args[0] === "repo" && command.args[1] === "delete") {
3302
+ return "destructive_gh_repo_delete";
3303
+ }
3304
+ return void 0;
3305
+ }
3306
+ function workerLifecyclePolicy(command) {
3307
+ const args = stripGitGlobalOptions(command.args);
3308
+ if (command.file === "git" && ["commit", "push", "merge"].includes(args[0] ?? "")) {
3309
+ return "worker_git_lifecycle_forbidden";
3310
+ }
3311
+ if (command.file === "gh" && command.args[0] === "pr" && ["create", "ready", "merge"].includes(command.args[1] ?? "")) {
3312
+ return "worker_gh_lifecycle_forbidden";
3313
+ }
3314
+ return void 0;
3315
+ }
3316
+ function protectedPathPolicy(command, protectedPaths) {
3317
+ const args = stripGitGlobalOptions(command.args);
3318
+ if (command.file !== "git" || args[0] !== "add") {
3319
+ return void 0;
3320
+ }
3321
+ const separator = args.indexOf("--");
3322
+ const paths = separator >= 0 ? args.slice(separator + 1) : args.slice(1);
3323
+ const hit = paths.find((path) => protectedPaths.some((pattern) => matchesProtectedPath(pattern, path)));
3324
+ return hit ? `protected_path:${hit}` : void 0;
3325
+ }
3326
+ function matchesHookAllowlist(command) {
3327
+ const args = stripGitGlobalOptions(command.args);
3328
+ if (command.file === "git") {
3329
+ return args[0] === "status" || args[0] === "branch" && args[1] === "--show-current" || args[0] === "rev-parse" || args[0] === "diff" || args[0] === "add" && args[1] === "--" || args[0] === "commit" && args[1] === "-m" || args[0] === "push" && args[1] === "-u";
3330
+ }
3331
+ if (command.file === "gh") {
3332
+ return command.args[0] === "auth" && command.args[1] === "status" || command.args[0] === "pr" && ["list", "view"].includes(command.args[1] ?? "") || command.args[0] === "api" && command.args[1] === "graphql";
3333
+ }
3334
+ if (command.file === "pnpm") {
3335
+ return command.args[0] === "test" || command.args[0] === "lint" || command.args[0] === "agent-loop" && ["status", "doctor", "logs"].includes(command.args[1] ?? "");
3336
+ }
3337
+ if (command.file === "npx") {
3338
+ return command.args[0] === "gitnexus" && ["--version", "status", "analyze", "detect_changes", "impact"].includes(command.args[1] ?? "");
3339
+ }
3340
+ if (command.file === "codex") {
3341
+ return command.args[0] === "--version";
3342
+ }
3343
+ return false;
3344
+ }
3345
+ function deny(blockedCommand, matchedPolicy, gate, nextAction) {
3346
+ return {
3347
+ allow: false,
3348
+ matchedPolicy,
3349
+ gate,
3350
+ blockedCommand,
3351
+ nextAction,
3352
+ reason: `${matchedPolicy} blocked ${blockedCommand}`
3353
+ };
3354
+ }
3355
+ function formatHookMessage(decision2) {
3356
+ return [
3357
+ `blocked command: ${decision2.blockedCommand}`,
3358
+ `matched policy: ${decision2.matchedPolicy}`,
3359
+ decision2.gate ? `gate: ${decision2.gate}` : void 0,
3360
+ `next action: ${decision2.nextAction}`
3361
+ ].filter(Boolean).join("\n");
3362
+ }
3363
+ function normalizeCommand(command) {
3364
+ return { ...command, file: basename(command.file) };
3365
+ }
3366
+ function unwrapCommand(command) {
3367
+ if (command.file === "env") {
3368
+ const index = command.args.findIndex((arg) => !arg.includes("="));
3369
+ if (index >= 0) {
3370
+ return unwrapCommand({ file: command.args[index] ?? "", args: command.args.slice(index + 1), raw: renderCommand(command) });
3371
+ }
3372
+ }
3373
+ if ((command.file === "sh" || command.file === "bash") && command.args[0] === "-c" && command.args[1]) {
3374
+ return unwrapCommand(tokenizeCommand(command.args[1]));
3375
+ }
3376
+ return command;
3377
+ }
3378
+ function renderCommand(command) {
3379
+ return command.raw ?? [command.file, ...command.args].join(" ");
3380
+ }
3381
+ function tokenizeCommand(command) {
3382
+ const parts = command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((part) => part.replace(/^["']|["']$/g, "")) ?? [];
3383
+ const [file = "", ...args] = parts;
3384
+ return { file: basename(file), args, raw: command };
3385
+ }
3386
+ function stripGitGlobalOptions(args) {
3387
+ const result = [...args];
3388
+ while (result.length > 0) {
3389
+ const first = result[0];
3390
+ if (first === "-C" || first === "--git-dir" || first === "--work-tree" || first === "-c") {
3391
+ result.splice(0, 2);
3392
+ continue;
3393
+ }
3394
+ if (first === "--no-pager" || first === "--paginate") {
3395
+ result.shift();
3396
+ continue;
3397
+ }
3398
+ if (first?.startsWith("--git-dir=") || first?.startsWith("--work-tree=") || first?.startsWith("-c")) {
3399
+ result.shift();
3400
+ continue;
3401
+ }
3402
+ break;
3403
+ }
3404
+ return result;
3405
+ }
3406
+ function publishPrerequisitesSatisfied(storage) {
3407
+ const run = storage.getCurrentRun();
3408
+ if (!run) {
3409
+ return false;
3410
+ }
3411
+ return storage.hasRunCheck(run.id, "self_check") && storage.hasRunCheck(run.id, "gitnexus_detect_changes");
3412
+ }
3413
+ function basename(value) {
3414
+ return value.replaceAll("\\", "/").split("/").at(-1) ?? value;
3415
+ }
3416
+ function stringValue2(value) {
3417
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3418
+ }
3419
+
3420
+ // plugins/autonomous-pr-loop/hooks/pre-tool-use.ts
3421
+ var repoRoot = process.env.AGENT_LOOP_REPO_ROOT;
3422
+ var input = readStdinJson();
3423
+ var decision = safeEvaluate(input, repoRoot);
3424
+ process.stdout.write(`${JSON.stringify(toCodexHookResponse(decision))}
3425
+ `);
3426
+ function safeEvaluate(input2, repoRoot2) {
3427
+ try {
3428
+ return evaluatePreToolUseHook(input2, repoRoot2);
3429
+ } catch (error) {
3430
+ return {
3431
+ allow: false,
3432
+ matchedPolicy: "hook_runner_error",
3433
+ gate: "policy_violation",
3434
+ blockedCommand: "<hook runner error>",
3435
+ nextAction: "Run `agent-loop hooks doctor` and fix hook routing before retrying.",
3436
+ reason: error instanceof Error ? error.message : String(error)
3437
+ };
3438
+ }
3439
+ }
3440
+ function readStdinJson() {
3441
+ const text = readFileSync3(0, "utf8");
3442
+ if (text.trim().length === 0) {
3443
+ return {};
3444
+ }
3445
+ try {
3446
+ return JSON.parse(text);
3447
+ } catch {
3448
+ const decision2 = {
3449
+ allow: false,
3450
+ matchedPolicy: "malformed_hook_payload",
3451
+ gate: "policy_violation",
3452
+ blockedCommand: "<unparseable hook payload>",
3453
+ nextAction: "Retry the tool call with a valid PreToolUse payload.",
3454
+ reason: "PreToolUse payload was not valid JSON."
3455
+ };
3456
+ process.stdout.write(`${JSON.stringify(toCodexHookResponse(decision2))}
3457
+ `);
3458
+ process.exit(0);
3459
+ }
3460
+ }