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,412 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createArtifactWriter, writeArtifact, type ArtifactWriter } from "./artifacts.js";
3
+ import { isSecretKey, redactSecrets } from "./redaction.js";
4
+ import type { AgentLoopStorage, WorkerBackend, WorkerEvent } from "./types.js";
5
+
6
+ const KNOWN_ITEM_TYPES = [
7
+ "agent_message",
8
+ "command_execution",
9
+ "file_change",
10
+ "mcp_tool_call",
11
+ "web_search",
12
+ "todo_list",
13
+ "error"
14
+ ] as const;
15
+ const SUMMARY_LIMIT_BYTES = 8 * 1024;
16
+
17
+ export interface WorkerEventIngestResult {
18
+ threadId?: string;
19
+ unknownEventCount: number;
20
+ usage?: unknown;
21
+ rawJsonlArtifactId: string;
22
+ }
23
+
24
+ export interface WorkerJsonlStreamIngestor {
25
+ ingestChunk(chunk: string): void;
26
+ finalize(): WorkerEventIngestResult;
27
+ readonly threadId: string | undefined;
28
+ readonly unknownEventCount: number;
29
+ readonly rawJsonl: string;
30
+ }
31
+
32
+ /** Create a line-oriented Codex JSONL ingestor that appends worker events as lines arrive. */
33
+ export function createWorkerJsonlStreamIngestor(input: {
34
+ repoRoot: string;
35
+ storage: AgentLoopStorage;
36
+ runId: string;
37
+ workerId: string;
38
+ backend: WorkerBackend;
39
+ }): WorkerJsonlStreamIngestor {
40
+ return new StreamingWorkerEventIngestor(input);
41
+ }
42
+
43
+ /** Ingest Codex JSONL events into storage summaries and a raw JSONL artifact. */
44
+ export function ingestWorkerJsonl(input: {
45
+ repoRoot: string;
46
+ storage: AgentLoopStorage;
47
+ runId: string;
48
+ workerId: string;
49
+ jsonl: string;
50
+ backend?: WorkerBackend;
51
+ }): WorkerEventIngestResult {
52
+ const ingestor = createWorkerJsonlStreamIngestor({
53
+ repoRoot: input.repoRoot,
54
+ storage: input.storage,
55
+ runId: input.runId,
56
+ workerId: input.workerId,
57
+ backend: input.backend ?? "codex-exec"
58
+ });
59
+ ingestor.ingestChunk(input.jsonl);
60
+ return ingestor.finalize();
61
+ }
62
+
63
+ class StreamingWorkerEventIngestor implements WorkerJsonlStreamIngestor {
64
+ private buffer = "";
65
+ private readonly rawWriter: ArtifactWriter;
66
+ private currentThreadId: string | undefined;
67
+ private currentUsage: unknown;
68
+ private unknownCount = 0;
69
+ private finalized = false;
70
+
71
+ constructor(private readonly input: {
72
+ repoRoot: string;
73
+ storage: AgentLoopStorage;
74
+ runId: string;
75
+ workerId: string;
76
+ backend: WorkerBackend;
77
+ }) {
78
+ this.rawWriter = createArtifactWriter(
79
+ input.repoRoot,
80
+ input.storage,
81
+ input.runId,
82
+ "worker-jsonl",
83
+ `${input.workerId}.jsonl`
84
+ );
85
+ }
86
+
87
+ get threadId(): string | undefined {
88
+ return this.currentThreadId;
89
+ }
90
+
91
+ get unknownEventCount(): number {
92
+ return this.unknownCount;
93
+ }
94
+
95
+ get rawJsonl(): string {
96
+ return "";
97
+ }
98
+
99
+ ingestChunk(chunk: string): void {
100
+ if (this.finalized || chunk.length === 0) {
101
+ return;
102
+ }
103
+ this.rawWriter.append(chunk);
104
+ this.buffer += chunk;
105
+ let newline = this.buffer.search(/\r?\n/);
106
+ while (newline >= 0) {
107
+ const line = this.buffer.slice(0, newline);
108
+ const delimiterLength = this.buffer[newline] === "\r" && this.buffer[newline + 1] === "\n" ? 2 : 1;
109
+ this.buffer = this.buffer.slice(newline + delimiterLength);
110
+ this.ingestLine(line);
111
+ newline = this.buffer.search(/\r?\n/);
112
+ }
113
+ }
114
+
115
+ finalize(): WorkerEventIngestResult {
116
+ if (!this.finalized) {
117
+ if (this.buffer.trim().length > 0) {
118
+ if (parseLine(this.buffer)) {
119
+ this.ingestLine(this.buffer);
120
+ } else {
121
+ this.appendEvent("worker_jsonl_partial_line", undefined, {
122
+ truncated: true,
123
+ length: this.buffer.length,
124
+ sha256: sha256(this.buffer)
125
+ });
126
+ }
127
+ this.buffer = "";
128
+ }
129
+ this.finalized = true;
130
+ }
131
+ const raw = this.rawWriter.finalize();
132
+ return {
133
+ ...(this.currentThreadId ? { threadId: this.currentThreadId } : {}),
134
+ unknownEventCount: this.unknownCount,
135
+ ...(this.currentUsage ? { usage: this.currentUsage } : {}),
136
+ rawJsonlArtifactId: raw.id
137
+ };
138
+ }
139
+
140
+ private ingestLine(line: string): void {
141
+ if (line.trim().length === 0) {
142
+ return;
143
+ }
144
+ const parsed = parseLine(line);
145
+ if (!parsed) {
146
+ this.unknownCount += 1;
147
+ return;
148
+ }
149
+ const type = eventType(parsed);
150
+ const threadId = extractThreadId(parsed) ?? this.currentThreadId;
151
+ this.currentThreadId = threadId;
152
+ const usage = extractUsage(parsed);
153
+ this.currentUsage = usage ?? this.currentUsage;
154
+ if (type === "thread.started") {
155
+ this.appendEvent(type, undefined, { threadId }, { ...(threadId ? { threadId } : {}) });
156
+ return;
157
+ }
158
+ if (type === "turn.started" || type === "turn.completed" || type === "turn.failed") {
159
+ this.appendEvent(type, undefined, summarizeTurn(parsed), {
160
+ ...(threadId ? { threadId } : {}),
161
+ ...(usage ? { usage } : {})
162
+ });
163
+ return;
164
+ }
165
+ if (type === "item.started" || type === "item.updated" || type === "item.completed" || type === "item.failed") {
166
+ const itemType = extractItemType(parsed);
167
+ if (!isKnownItemType(itemType)) {
168
+ this.unknownCount += 1;
169
+ return;
170
+ }
171
+ this.appendEvent(type, itemType, summarizeItem(parsed, itemType), {
172
+ ...(threadId ? { threadId } : {}),
173
+ ...optionalString("itemId", extractItemId(parsed)),
174
+ ...optionalString("itemStatus", extractItemStatus(parsed, type)),
175
+ ...(usage ? { usage } : {})
176
+ });
177
+ return;
178
+ }
179
+ this.unknownCount += 1;
180
+ }
181
+
182
+ private appendEvent(
183
+ eventTypeValue: string,
184
+ itemType: string | undefined,
185
+ summary: unknown,
186
+ options: {
187
+ threadId?: string;
188
+ itemId?: string;
189
+ itemStatus?: string;
190
+ usage?: unknown;
191
+ } = {}
192
+ ): WorkerEvent {
193
+ const normalized = normalizeSummary(summary, {
194
+ repoRoot: this.input.repoRoot,
195
+ storage: this.input.storage,
196
+ runId: this.input.runId,
197
+ workerId: this.input.workerId,
198
+ eventType: eventTypeValue,
199
+ ...(options.itemId ? { itemId: options.itemId } : {})
200
+ });
201
+ return this.input.storage.appendWorkerEvent({
202
+ workerId: this.input.workerId,
203
+ runId: this.input.runId,
204
+ eventType: eventTypeValue,
205
+ ...(itemType ? { itemType } : {}),
206
+ ...(options.itemId ? { itemId: options.itemId } : {}),
207
+ ...(options.itemStatus ? { itemStatus: options.itemStatus } : {}),
208
+ ...(options.threadId ? { threadId: options.threadId } : this.currentThreadId ? { threadId: this.currentThreadId } : {}),
209
+ backend: this.input.backend,
210
+ summary: normalized.summary,
211
+ ...(options.usage ? { usage: options.usage } : {}),
212
+ ...(normalized.artifactIds.length ? { artifactIds: normalized.artifactIds } : {})
213
+ });
214
+ }
215
+ }
216
+
217
+ function parseLine(line: string): Record<string, unknown> | undefined {
218
+ try {
219
+ const parsed = JSON.parse(line) as unknown;
220
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
221
+ ? parsed as Record<string, unknown>
222
+ : undefined;
223
+ } catch {
224
+ return undefined;
225
+ }
226
+ }
227
+
228
+ function eventType(value: Record<string, unknown>): string {
229
+ return stringValue(value.type) ?? stringValue(value.event) ?? stringValue(value.eventType) ?? "unknown";
230
+ }
231
+
232
+ function extractThreadId(value: Record<string, unknown>): string | undefined {
233
+ return stringValue(value.thread_id) ??
234
+ stringValue(value.threadId) ??
235
+ stringValue(value.id) ??
236
+ stringValue(recordValue(value.thread)?.id) ??
237
+ stringValue(recordValue(value.session)?.id);
238
+ }
239
+
240
+ function extractItemType(value: Record<string, unknown>): string | undefined {
241
+ const item = recordValue(value.item);
242
+ return stringValue(value.item_type) ?? stringValue(value.itemType) ?? stringValue(item?.type);
243
+ }
244
+
245
+ function extractItemId(value: Record<string, unknown>): string | undefined {
246
+ const item = recordValue(value.item);
247
+ return stringValue(value.item_id) ?? stringValue(value.itemId) ?? stringValue(item?.id);
248
+ }
249
+
250
+ function extractItemStatus(value: Record<string, unknown>, eventTypeValue: string): string | undefined {
251
+ const item = recordValue(value.item);
252
+ return stringValue(value.item_status) ??
253
+ stringValue(value.itemStatus) ??
254
+ stringValue(item?.status) ??
255
+ eventTypeValue.split(".").at(-1);
256
+ }
257
+
258
+ function extractUsage(value: Record<string, unknown>): unknown {
259
+ return value.usage ?? recordValue(value.turn)?.usage;
260
+ }
261
+
262
+ function summarizeTurn(value: Record<string, unknown>): unknown {
263
+ return redactSummary({
264
+ type: eventType(value),
265
+ threadId: extractThreadId(value),
266
+ usage: extractUsage(value),
267
+ error: stringValue(value.error) ?? stringValue(recordValue(value.turn)?.error)
268
+ });
269
+ }
270
+
271
+ function summarizeItem(value: Record<string, unknown>, itemType: string): unknown {
272
+ const item = recordValue(value.item) ?? value;
273
+ const base = {
274
+ id: extractItemId(value),
275
+ type: itemType,
276
+ status: extractItemStatus(value, eventType(value))
277
+ };
278
+ if (itemType === "agent_message") {
279
+ const text = stringValue(item.text) ?? stringValue(item.message) ?? stringValue(item.content);
280
+ return redactSummary({ ...base, message: text ? summarizeText(text) : undefined });
281
+ }
282
+ if (itemType === "command_execution") {
283
+ return redactSummary({
284
+ ...base,
285
+ command: item.command,
286
+ exitCode: item.exit_code ?? item.exitCode,
287
+ stdout: summarizeMaybeText(item.stdout),
288
+ stderr: summarizeMaybeText(item.stderr),
289
+ startedAt: stringValue(item.started_at) ?? stringValue(item.startedAt),
290
+ completedAt: stringValue(item.completed_at) ?? stringValue(item.completedAt)
291
+ });
292
+ }
293
+ if (itemType === "file_change") {
294
+ return redactSummary({ ...base, path: item.path, operation: item.operation, changes: summarizeOpaquePayload(item.changes) });
295
+ }
296
+ if (itemType === "mcp_tool_call") {
297
+ return redactSummary({ ...base, server: item.server, tool: item.tool ?? item.name, result: summarizeMaybeText(item.result) });
298
+ }
299
+ if (itemType === "web_search") {
300
+ return redactSummary({ ...base, query: item.query, url: item.url, resultCount: item.result_count ?? item.resultCount });
301
+ }
302
+ if (itemType === "todo_list") {
303
+ const todos = Array.isArray(item.todos) ? item.todos : Array.isArray(item.items) ? item.items : undefined;
304
+ return redactSummary({ ...base, count: todos?.length, todos: todos?.slice(0, 20) });
305
+ }
306
+ return redactSummary({ ...base, message: item.message, error: item.error });
307
+ }
308
+
309
+ function summarizeMaybeText(value: unknown): unknown {
310
+ return typeof value === "string" ? summarizeText(value) : value;
311
+ }
312
+
313
+ function summarizeOpaquePayload(value: unknown): unknown {
314
+ if (value === undefined) {
315
+ return undefined;
316
+ }
317
+ const redacted = redactSummary(value);
318
+ const json = JSON.stringify(redacted);
319
+ return {
320
+ length: json.length,
321
+ sha256: sha256(json),
322
+ type: Array.isArray(value) ? "array" : typeof value
323
+ };
324
+ }
325
+
326
+ function summarizeText(text: string): Record<string, unknown> {
327
+ return {
328
+ length: text.length,
329
+ sha256: sha256(text),
330
+ preview: redactSecrets(text.slice(0, 240)),
331
+ truncated: text.length > 240
332
+ };
333
+ }
334
+
335
+ function normalizeSummary(
336
+ summary: unknown,
337
+ artifactContext: {
338
+ repoRoot: string;
339
+ storage: AgentLoopStorage;
340
+ runId: string;
341
+ workerId: string;
342
+ eventType: string;
343
+ itemId?: string;
344
+ }
345
+ ): { summary: unknown; artifactIds: string[] } {
346
+ const redacted = redactSummary(summary);
347
+ const json = JSON.stringify(redacted);
348
+ if (Buffer.byteLength(json, "utf8") <= SUMMARY_LIMIT_BYTES) {
349
+ return { summary: redacted, artifactIds: [] };
350
+ }
351
+ const hash = sha256(json);
352
+ const artifact = writeArtifact(
353
+ artifactContext.repoRoot,
354
+ artifactContext.storage,
355
+ artifactContext.runId,
356
+ "log",
357
+ `${artifactContext.workerId}-${safeName(artifactContext.eventType)}-${safeName(artifactContext.itemId ?? hash.slice(0, 12))}.summary.json`,
358
+ json
359
+ );
360
+ return {
361
+ summary: {
362
+ truncated: true,
363
+ length: json.length,
364
+ sha256: hash,
365
+ artifactId: artifact.id
366
+ },
367
+ artifactIds: [artifact.id]
368
+ };
369
+ }
370
+
371
+ function redactSummary(value: unknown): unknown {
372
+ if (Array.isArray(value)) {
373
+ return value.slice(0, 20).map(redactSummary);
374
+ }
375
+ if (typeof value === "string") {
376
+ return redactSecrets(value);
377
+ }
378
+ if (typeof value !== "object" || value === null) {
379
+ return value;
380
+ }
381
+ const redacted: Record<string, unknown> = {};
382
+ for (const [key, nested] of Object.entries(value as Record<string, unknown>).slice(0, 40)) {
383
+ redacted[key] = isSecretKey(key) ? "[redacted]" : redactSummary(nested);
384
+ }
385
+ return redacted;
386
+ }
387
+
388
+ function isKnownItemType(value: string | undefined): value is typeof KNOWN_ITEM_TYPES[number] {
389
+ return value !== undefined && (KNOWN_ITEM_TYPES as readonly string[]).includes(value);
390
+ }
391
+
392
+ function recordValue(value: unknown): Record<string, unknown> | undefined {
393
+ return typeof value === "object" && value !== null && !Array.isArray(value)
394
+ ? value as Record<string, unknown>
395
+ : undefined;
396
+ }
397
+
398
+ function stringValue(value: unknown): string | undefined {
399
+ return typeof value === "string" && value.length > 0 ? value : undefined;
400
+ }
401
+
402
+ function optionalString<K extends string>(key: K, value: string | undefined): { [P in K]?: string } {
403
+ return value ? { [key]: value } as { [P in K]?: string } : {};
404
+ }
405
+
406
+ function sha256(value: string): string {
407
+ return createHash("sha256").update(value).digest("hex");
408
+ }
409
+
410
+ function safeName(value: string): string {
411
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "event";
412
+ }
@@ -0,0 +1,72 @@
1
+ import { AgentLoopError } from "./errors.js";
2
+ import { resolveLoopShape, sandboxForShapeState } from "./loop-shapes.js";
3
+ import { workflowProfileDefinition } from "./profiles.js";
4
+ import type { AgentLoopConfig, WorkerType } from "./types.js";
5
+ import type { AgentLoopState } from "./state-types.js";
6
+
7
+ export interface WorkerPolicy {
8
+ sandbox: "read-only" | "workspace-write";
9
+ allowedPaths?: string[];
10
+ protectedPaths: string[];
11
+ commandPolicy: {
12
+ lifecycleOwnedBySupervisor: boolean;
13
+ allowedWriteRoots: string[];
14
+ genericReadOnlyState: boolean;
15
+ };
16
+ }
17
+
18
+ /** Resolve the single worker policy used by prompts, command execution, hooks, and scope guard. */
19
+ export function resolveWorkerPolicy(input: {
20
+ config: AgentLoopConfig;
21
+ state: AgentLoopState;
22
+ workerType: WorkerType;
23
+ }): WorkerPolicy {
24
+ const shape = resolveLoopShape(input.config.loopShape);
25
+ const sandbox = sandboxForShapeState(shape.id, input.state, input.workerType);
26
+ const allowedPaths = allowedPathsFor(input.config, input.state, input.workerType, sandbox);
27
+ if (shape.id === "generic-loop" && sandbox === "workspace-write" && (!allowedPaths || allowedPaths.length === 0)) {
28
+ throw new AgentLoopError("generic_scope_change_requested", "Generic write state has no allowed write roots.", {
29
+ details: {
30
+ loopShape: shape.id,
31
+ state: input.state,
32
+ workflowProfile: input.config.workflowProfile,
33
+ required: "Configure a generic workflow profile with allowed write roots or approve a scoped change."
34
+ },
35
+ exitCode: 2
36
+ });
37
+ }
38
+ return {
39
+ sandbox,
40
+ ...(allowedPaths ? { allowedPaths } : {}),
41
+ protectedPaths: input.config.protectedPaths,
42
+ commandPolicy: {
43
+ lifecycleOwnedBySupervisor: true,
44
+ allowedWriteRoots: allowedPaths ?? [],
45
+ genericReadOnlyState: shape.id === "generic-loop" && sandbox === "read-only"
46
+ }
47
+ };
48
+ }
49
+
50
+ function allowedPathsFor(
51
+ config: AgentLoopConfig,
52
+ state: AgentLoopState,
53
+ workerType: WorkerType,
54
+ sandbox: "read-only" | "workspace-write"
55
+ ): string[] | undefined {
56
+ if (config.loopShape === "pr-loop") {
57
+ if (workerType === "planner") {
58
+ return [config.plansDir];
59
+ }
60
+ if (workerType === "reviewer") {
61
+ return [];
62
+ }
63
+ return undefined;
64
+ }
65
+ if (sandbox === "read-only") {
66
+ return [];
67
+ }
68
+ if (state === "EXECUTE_STEP" || state === "DELIVER") {
69
+ return workflowProfileDefinition(config.workflowProfile).allowedWriteRoots ?? [];
70
+ }
71
+ return [];
72
+ }
@@ -0,0 +1,182 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { AgentLoopConfig, AgentLoopProfileSummary, AgentLoopRun, WorkerType } from "./types.js";
4
+ import type { AgentLoopState } from "./state-types.js";
5
+ import type { WorkerPolicy } from "./worker-policy.js";
6
+
7
+ export interface WorkerPromptInput {
8
+ repoRoot: string;
9
+ run: AgentLoopRun;
10
+ state: AgentLoopState;
11
+ type: WorkerType;
12
+ config: AgentLoopConfig;
13
+ profile?: AgentLoopProfileSummary;
14
+ policy?: WorkerPolicy;
15
+ context?: unknown;
16
+ }
17
+
18
+ /** Build the controlled prompt passed to a Codex worker. */
19
+ export function buildWorkerPrompt(input: WorkerPromptInput): string {
20
+ const requiredCommands = requiredCommandsFor(input.type, input.config);
21
+ const genericLoop = input.config.loopShape === "generic-loop";
22
+ return [
23
+ `# ${genericLoop ? "Generic Loop Worker" : "HOLO-Codex PR Loop Worker"}: ${input.type}`,
24
+ "",
25
+ genericLoop ? "You are a delegated worker inside the configured Generic Loop." : "You are a delegated worker inside the HOLO-Codex PR delivery loop.",
26
+ "You may edit repository files within the allowed scope, then return only the structured final result.",
27
+ "",
28
+ "## Current Run",
29
+ `- runId: ${input.run.id}`,
30
+ `- state: ${input.state}`,
31
+ `- workerType: ${input.type}`,
32
+ ...(genericLoop ? [] : [
33
+ `- baseBranch: ${input.config.baseBranch}`,
34
+ `- plansDir: ${input.config.plansDir}`
35
+ ]),
36
+ `- loopShape: ${input.profile?.loopShape ?? input.config.loopShape}`,
37
+ `- workflowProfile: ${input.profile?.workflowProfile ?? input.config.workflowProfile}`,
38
+ `- roleProfile: ${input.profile?.roleProfile ?? input.config.roleProfile}`,
39
+ `- sandbox: ${input.policy?.sandbox ?? workerSandbox(input.type)}`,
40
+ "",
41
+ "## Workflow Profile",
42
+ ...profileLines(input),
43
+ "",
44
+ "## Allowed Scope",
45
+ ...allowedScope(input).map((line) => `- ${line}`),
46
+ "",
47
+ "## Required Commands",
48
+ ...requiredCommands.map((line) => `- ${line}`),
49
+ "",
50
+ "## AGENTS.md Summary",
51
+ summarizeAgents(input.repoRoot),
52
+ "",
53
+ "## Hard Prohibitions",
54
+ ...(genericLoop ? [
55
+ "- Do not treat this as PR automation unless the workflow explicitly asks for PR-related documentation.",
56
+ "- Do not create, update, ready, merge, or close pull requests.",
57
+ "- Do not run release, deploy, publishing, notification, payment, or production-control side effects."
58
+ ] : [
59
+ "- Do not commit.",
60
+ "- Do not push.",
61
+ "- Do not create, update, ready, merge, or close pull requests."
62
+ ]),
63
+ "- Do not run git reset, git clean, git rebase, force push, or history rewriting commands.",
64
+ "- Do not request danger-full-access or bypass sandbox approvals.",
65
+ "",
66
+ "## GitNexus Requirements",
67
+ input.config.gitnexusRequired
68
+ ? "- GitNexus impact and detect changes are required. Set gitnexus.impactRun and gitnexus.detectChangesRun truthfully."
69
+ : "- GitNexus is best-effort. Record notes if unavailable.",
70
+ "",
71
+ "## Context",
72
+ JSON.stringify(input.context ?? {}, null, 2),
73
+ "",
74
+ "## Output Schema",
75
+ "Return a JSON object matching worker-result.schema.json with these fields:",
76
+ "`ok`, `summary`, `changedFiles`, `commandsRun`, `testsRun`, `gitnexus`, `outOfScope`, `followUps`, optional `error`.",
77
+ "Do not include Markdown fences in the final answer."
78
+ ].join("\n");
79
+ }
80
+
81
+ /** Return the sandbox required for a worker type. */
82
+ export function workerSandbox(type: WorkerType): "read-only" | "workspace-write" {
83
+ return type === "reviewer" ? "read-only" : "workspace-write";
84
+ }
85
+
86
+ function allowedScope(input: WorkerPromptInput): string[] {
87
+ const currentRole = input.profile?.currentRole;
88
+ const rolePrefix = currentRole ? `${currentRole.label} (${currentRole.alias} -> ${currentRole.workerType}). ` : "";
89
+ if (input.policy?.sandbox === "read-only") {
90
+ return [
91
+ `${rolePrefix}Read-only state. Do not modify files.`,
92
+ `Protected paths still apply: ${input.policy.protectedPaths.join(", ")}`
93
+ ];
94
+ }
95
+ if (input.config.loopShape === "generic-loop") {
96
+ return [
97
+ `${rolePrefix}Work only toward the generic-loop deliverable: ${input.profile?.expectedDeliverable ?? "deliverable"}.`,
98
+ `Allowed write roots: ${(input.policy?.allowedPaths ?? []).join(", ") || "none"}.`,
99
+ `Do not touch protected paths: ${(input.policy?.protectedPaths ?? input.config.protectedPaths).join(", ")}`
100
+ ];
101
+ }
102
+ if (input.type === "planner") {
103
+ return [`${rolePrefix}Write specs under ${input.config.plansDir}/`, "Do not modify runtime state or secrets."];
104
+ }
105
+ if (input.type === "reviewer") {
106
+ return [`${rolePrefix}Read-only review. Do not modify files.`];
107
+ }
108
+ return [
109
+ `${rolePrefix}Modify only files needed for the current PR.`,
110
+ `Do not touch protected paths: ${input.config.protectedPaths.join(", ")}`
111
+ ];
112
+ }
113
+
114
+ function requiredCommandsFor(type: WorkerType, config: AgentLoopConfig): string[] {
115
+ const commands = [];
116
+ if (config.lintCommand) {
117
+ commands.push(config.lintCommand);
118
+ }
119
+ if (config.testCommand) {
120
+ commands.push(config.testCommand);
121
+ }
122
+ if (config.gitnexusRequired && type !== "planner") {
123
+ commands.push("npx gitnexus impact");
124
+ commands.push("npx gitnexus detect_changes");
125
+ }
126
+ return commands.length > 0 ? commands : ["No configured commands; explain why none were run."];
127
+ }
128
+
129
+ function summarizeAgents(repoRoot: string): string {
130
+ const path = join(repoRoot, "AGENTS.md");
131
+ if (!existsSync(path)) {
132
+ return "No AGENTS.md found.";
133
+ }
134
+ const content = readFileSync(path, "utf8").trim();
135
+ if (content.length <= 4_000) {
136
+ return content;
137
+ }
138
+ return `${content.slice(0, 4_000)}\n[truncated]`;
139
+ }
140
+
141
+ function profileLines(input: WorkerPromptInput): string[] {
142
+ const profile = input.profile;
143
+ if (!profile) {
144
+ return ["- No resolved profile summary was provided; follow the configured PR loop defaults."];
145
+ }
146
+ return [
147
+ `- Workflow: ${profile.workflowLabel} (${profile.workflowProfile})`,
148
+ `- Description: ${profile.workflowDescription}`,
149
+ `- Current role: ${profile.currentRole ? `${profile.currentRole.label} (${profile.currentRole.alias})` : "none"}`,
150
+ `- Autonomy boundary: ${profile.autonomyBoundary}`,
151
+ `- Handoff: ${profile.handoffSummary}`,
152
+ `- Validation posture: ${profile.validationPosture}`,
153
+ ...(profile.expectedDeliverable ? [`- Expected deliverable: ${profile.expectedDeliverable}`] : []),
154
+ ...(profile.allowedWriteRoots ? [`- Allowed write roots: ${profile.allowedWriteRoots.join(", ")}`] : []),
155
+ `- Likely gates: ${profile.likelyGates.join(", ") || "none"}`,
156
+ "- Role instruction:",
157
+ ` ${roleInstruction(input)}`
158
+ ];
159
+ }
160
+
161
+ function roleInstruction(input: WorkerPromptInput): string {
162
+ const role = input.profile?.currentRole;
163
+ if (!role) {
164
+ return "Use the default worker instructions for this state.";
165
+ }
166
+ if (input.config.workflowProfile === "docs_only_loop") {
167
+ return "Treat documentation consistency as primary; if code or config changes are involved, still run the configured validation and report the spillover.";
168
+ }
169
+ if (input.config.loopShape === "generic-loop") {
170
+ return `Complete only the generic-loop responsibility for ${input.state}. Produce evidence for ${input.profile?.expectedDeliverable ?? "the configured deliverable"}; follow the handoff and validation posture, and record any remaining work as followUps. For SELF_REVIEW, prefix blocking repair items with \`fix:\`; use \`note:\` for non-blocking carryover.`;
171
+ }
172
+ if (role.workerType === "reviewer") {
173
+ return "Read evidence directly, do not trust worker self-report, and avoid all file writes.";
174
+ }
175
+ if (role.workerType === "review-fix") {
176
+ return "Fix only scoped review findings and record out-of-scope carryover.";
177
+ }
178
+ if (role.workerType === "ci-fix") {
179
+ return "Focus on failing checks and avoid feature expansion.";
180
+ }
181
+ return "Stay inside the selected PR scope and hand off concise evidence.";
182
+ }