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,1152 @@
1
+ import type {
2
+ AgentTimelineEntry,
3
+ ArtifactSummary,
4
+ ConfigSnapshot,
5
+ DashboardApi,
6
+ DashboardResult,
7
+ DryRunPreviewData,
8
+ EventSummary,
9
+ MissionControlData,
10
+ WorkflowBoard,
11
+ WorkflowBoardStage,
12
+ WorkflowStageId,
13
+ WorkflowStageStatus
14
+ } from "./api.js";
15
+
16
+ type FixtureName =
17
+ | "blocked"
18
+ | "running"
19
+ | "success"
20
+ | "error"
21
+ | "invalid-profile"
22
+ | "empty"
23
+ | "long-content"
24
+ | "many-events"
25
+ | "mobile-stress"
26
+ | "mobile-table-stress"
27
+ | "observability"
28
+ | "many-agent-events"
29
+ | "worker-event-expand"
30
+ | "long-command-summary"
31
+ | "stale-gate"
32
+ | "historical-worker-failure"
33
+ | "historical-blocked-run"
34
+ | "workflow-build-active"
35
+ | "workflow-verify-failed"
36
+ | "workflow-pr-published"
37
+ | "workflow-review-active"
38
+ | "workflow-merge-blocked"
39
+ | "workflow-cleanup-active"
40
+ | "workflow-unknown-state"
41
+ | "generic-deliverable-ready"
42
+ | "generic-human-gate"
43
+ | "generic-scope-change"
44
+ | "generic-completed";
45
+
46
+ const timestamp = "2026-06-12T10:00:00.000Z";
47
+
48
+ export const dashboardFixtureNames: FixtureName[] = [
49
+ "blocked",
50
+ "running",
51
+ "success",
52
+ "error",
53
+ "invalid-profile",
54
+ "empty",
55
+ "long-content",
56
+ "many-events",
57
+ "mobile-stress",
58
+ "mobile-table-stress",
59
+ "observability",
60
+ "many-agent-events",
61
+ "worker-event-expand",
62
+ "long-command-summary",
63
+ "stale-gate",
64
+ "historical-worker-failure",
65
+ "historical-blocked-run",
66
+ "workflow-build-active",
67
+ "workflow-verify-failed",
68
+ "workflow-pr-published",
69
+ "workflow-review-active",
70
+ "workflow-merge-blocked",
71
+ "workflow-cleanup-active",
72
+ "workflow-unknown-state",
73
+ "generic-deliverable-ready",
74
+ "generic-human-gate",
75
+ "generic-scope-change",
76
+ "generic-completed"
77
+ ];
78
+
79
+ export function dashboardFixture(name: string | undefined): MissionControlData | undefined {
80
+ if (!name) return undefined;
81
+ if (!dashboardFixtureNames.includes(name as FixtureName)) return undefined;
82
+ return buildFixture(name as FixtureName);
83
+ }
84
+
85
+ export function createFixtureDashboardApi(data: MissionControlData): DashboardApi {
86
+ const artifact = data.artifacts[0] ?? artifactRecord("artifact-empty", "log", "empty.log");
87
+ const ok = <T>(payload: T): Promise<DashboardResult<T>> => Promise.resolve({ ok: true, data: payload });
88
+ return {
89
+ dashboardMeta: () => ok({
90
+ appName: "HOLO-Codex",
91
+ surface: "dashboard",
92
+ targetRepo: { root: "/fixture/repo", repoId: "example/fixture" }
93
+ }),
94
+ missionControl: () => ok(data),
95
+ observe: () => ok({
96
+ dashboard: { url: "http://127.0.0.1:0/", host: "127.0.0.1", port: 0, loopbackOnly: true },
97
+ happy: { installed: false, supportsNotify: false },
98
+ current: data.current,
99
+ timeline: { entries: timelineFromData(data).slice(0, 20) }
100
+ }),
101
+ events: (since) => ok({ events: data.events.filter((event) => since === undefined || event.seq > since) }),
102
+ agentTimeline: (options) => {
103
+ const timeline = timelineFromData(data).filter((entry) =>
104
+ (!options?.runId || entry.runId === options.runId) &&
105
+ (!options?.workerId || entry.workerId === options.workerId) &&
106
+ (!options?.sources?.length || options.sources.includes(entry.source))
107
+ );
108
+ return ok({ entries: timeline.slice(0, options?.limit ?? 50) });
109
+ },
110
+ mutate: (path) => {
111
+ if (path.endsWith("/re-evaluate")) {
112
+ const gate = data.gates.find((item) => item.activity === "historical") ?? data.gates[0];
113
+ return ok({
114
+ fixture: true,
115
+ gate,
116
+ result: gate?.activityReason === "marked_handled" ? "manually_handled" : "overridden_by_current_reality",
117
+ reevaluated: true
118
+ });
119
+ }
120
+ return ok({ fixture: true });
121
+ },
122
+ artifact: (id) => {
123
+ const record = data.artifacts.find((item) => item.id === id) ?? artifact;
124
+ return ok({
125
+ record,
126
+ contentBase64: btoa(`fixture artifact: ${record.name}\n\n${longSentence("Artifact content", 18)}`)
127
+ });
128
+ },
129
+ plan: () => ok({ plan: data.plan ?? baseFixture().plan! }),
130
+ policyConfig: () => ok(policyConfigFixture()),
131
+ dryRunPreview: () => ok(dryRunFixture(data)),
132
+ notifications: () => ok({ notifications: data.notifications ?? [] }),
133
+ workflowBoard: () => ok(workflowBoardFixture(data)),
134
+ appendWorkflowEvidence: () => ok({ fixture: true }),
135
+ auditExport: (options) => ok({
136
+ runId: options.runId,
137
+ format: options.format,
138
+ content: options.format === "json" ? { runId: options.runId, fixture: true } : `# Fixture audit ${options.runId}\n`
139
+ })
140
+ };
141
+ }
142
+
143
+ function buildFixture(name: FixtureName): MissionControlData {
144
+ const base = baseFixture();
145
+ if (name === "running") {
146
+ const { gate: _gate, ...current } = base.current;
147
+ return {
148
+ ...base,
149
+ current: {
150
+ ...current,
151
+ status: "RUNNING",
152
+ nextAction: "Continue autonomous loop until the next material gate."
153
+ },
154
+ gates: [],
155
+ notifications: [{
156
+ id: "note-progress",
157
+ severity: "informational",
158
+ title: "Loop progress",
159
+ reason: "Worker completed scoped edits and supervisor is running checks.",
160
+ source: "event",
161
+ sourceId: "event-2",
162
+ createdAt: timestamp
163
+ }],
164
+ mergeReadiness: {
165
+ state: "collecting_evidence",
166
+ ready: false,
167
+ missingConditions: ["required check green: ci", "review approval observed"],
168
+ evidence: ["scope guard passed"],
169
+ carryoverRecords: []
170
+ }
171
+ };
172
+ }
173
+ if (name === "success") {
174
+ const { gate: _gate, ...current } = base.current;
175
+ return {
176
+ ...base,
177
+ current: {
178
+ ...current,
179
+ status: "READY",
180
+ nextAction: "Ready for operator review or merge confirmation.",
181
+ ...(base.current.run ? { run: { ...base.current.run, status: "READY", currentState: "MERGE" } } : {})
182
+ },
183
+ gates: [],
184
+ notifications: [],
185
+ mergeReadiness: {
186
+ state: "ready",
187
+ ready: true,
188
+ missingConditions: [],
189
+ evidence: ["ci green", "review approved", "self check passed"],
190
+ carryoverRecords: []
191
+ }
192
+ };
193
+ }
194
+ if (name === "error") {
195
+ return {
196
+ ...base,
197
+ current: {
198
+ ...base.current,
199
+ status: "BLOCKED",
200
+ nextAction: "Inspect worker failure and decide whether to resume or recover.",
201
+ gate: { kind: "worker_failed", message: "Worker failed while rendering dashboard polish." }
202
+ },
203
+ gates: [{
204
+ id: "gate-worker-failed",
205
+ kind: "worker_failed",
206
+ status: "open",
207
+ message: "Worker failed while rendering dashboard polish.",
208
+ details: { workerId: "worker-error", exitCode: 1 },
209
+ createdAt: timestamp
210
+ }],
211
+ workers: [{
212
+ id: "worker-error",
213
+ type: "implementation",
214
+ status: "failed",
215
+ startedAt: timestamp,
216
+ completedAt: timestamp,
217
+ error: "dashboard render failed"
218
+ }],
219
+ notifications: [{
220
+ id: "workerfailed:worker-error",
221
+ severity: "blocked",
222
+ title: "worker_failed",
223
+ reason: "Worker failed while rendering dashboard polish.",
224
+ source: "worker",
225
+ sourceId: "worker-error",
226
+ createdAt: timestamp
227
+ }]
228
+ };
229
+ }
230
+ if (name === "invalid-profile") {
231
+ const { profile: _profile, ...withoutProfile } = base;
232
+ return {
233
+ ...withoutProfile,
234
+ current: {
235
+ ...base.current,
236
+ nextAction: "Profile summary is unavailable; dashboard should still render."
237
+ },
238
+ recoveryWarnings: ["Profile summary missing from fixture response."]
239
+ };
240
+ }
241
+ if (name === "observability" || name === "worker-event-expand") {
242
+ return {
243
+ ...base,
244
+ timelineSummary: {
245
+ latest: timelineFromData(base)[0]!,
246
+ activeWorker: { id: "worker-running", type: "implementation", status: "running", startedAt: timestamp },
247
+ hasObservationGap: false
248
+ },
249
+ workers: [
250
+ ...base.workers,
251
+ {
252
+ id: "worker-running",
253
+ type: "implementation",
254
+ status: "running",
255
+ startedAt: "2026-06-12T09:59:00.000Z"
256
+ }
257
+ ]
258
+ };
259
+ }
260
+ if (name === "many-agent-events") {
261
+ return {
262
+ ...base,
263
+ events: numberedEvents(64, "Agent timeline fixture event."),
264
+ timelineSummary: {
265
+ latest: timelineFromData({ ...base, events: numberedEvents(64, "Agent timeline fixture event.") })[0]!,
266
+ hasObservationGap: false
267
+ }
268
+ };
269
+ }
270
+ if (name === "long-command-summary") {
271
+ return {
272
+ ...base,
273
+ workers: [{
274
+ id: "worker-long-command",
275
+ type: "implementation",
276
+ status: "succeeded",
277
+ startedAt: timestamp,
278
+ completedAt: timestamp,
279
+ resultArtifactId: "artifact-fixture"
280
+ }],
281
+ events: numberedEvents(12, longSentence("Long command summary with nested output", 16)),
282
+ timelineSummary: {
283
+ latest: timelineFromData({ ...base, events: numberedEvents(12, longSentence("Long command summary with nested output", 16)) })[0]!,
284
+ hasObservationGap: false
285
+ }
286
+ };
287
+ }
288
+ if (name === "empty") {
289
+ const { pr: _pr, ...withoutPr } = base;
290
+ return {
291
+ ...withoutPr,
292
+ current: {
293
+ status: "IDLE",
294
+ nextAction: "Initialize or start the loop."
295
+ },
296
+ gates: [],
297
+ ci: [],
298
+ reviewComments: [],
299
+ workers: [],
300
+ artifacts: [],
301
+ events: [],
302
+ notifications: [],
303
+ recoveryWarnings: [],
304
+ mergeReadiness: {
305
+ state: "not_started",
306
+ ready: false,
307
+ missingConditions: ["no active run"],
308
+ evidence: [],
309
+ carryoverRecords: []
310
+ }
311
+ };
312
+ }
313
+ if (name === "long-content" || name === "mobile-stress" || name === "mobile-table-stress") {
314
+ const extra = longSentence("A very long dashboard field should wrap without forcing horizontal scroll", 12);
315
+ return {
316
+ ...base,
317
+ current: {
318
+ ...base.current,
319
+ nextAction: extra,
320
+ gate: {
321
+ kind: "confirmation_required",
322
+ message: extra,
323
+ details: { reason: extra, path: "docs/local-release-readiness.md" }
324
+ }
325
+ },
326
+ gates: [
327
+ ...base.gates,
328
+ {
329
+ id: "gate-long-confirmation-required",
330
+ kind: "confirmation_required_with_extremely_long_identifier_for_layout_testing",
331
+ status: "open",
332
+ message: extra,
333
+ details: { evidence: extra },
334
+ createdAt: timestamp
335
+ }
336
+ ],
337
+ reviewComments: [
338
+ ...base.reviewComments,
339
+ {
340
+ id: "comment-long",
341
+ author: "reviewer-with-a-very-long-handle-for-layout",
342
+ path: "plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx",
343
+ body: extra,
344
+ actionable: true,
345
+ isResolved: false,
346
+ isOutdated: false,
347
+ status: "open"
348
+ }
349
+ ],
350
+ artifacts: [
351
+ ...base.artifacts,
352
+ artifactRecord("artifact-long", "command-output", "very-long-command-output-name-that-must-not-break-layout.log"),
353
+ ...(name === "mobile-table-stress"
354
+ ? Array.from({ length: 4 }, (_, index) => artifactRecord(`artifact-mobile-${index}`, "command-output", `extremely-long-artifact-path-${index}-that-should-be-readable-in-a-card.log`))
355
+ : [])
356
+ ],
357
+ events: numberedEvents(name === "mobile-table-stress" ? 28 : 18, extra),
358
+ workers: name === "mobile-table-stress"
359
+ ? Array.from({ length: 6 }, (_, index) => ({
360
+ id: `worker-mobile-stress-${index}-with-long-id`,
361
+ type: index % 2 === 0 ? "implementation" : "reviewer",
362
+ status: index % 3 === 0 ? "running" : "succeeded",
363
+ startedAt: timestamp,
364
+ resultArtifactId: `artifact-mobile-${index}`
365
+ }))
366
+ : base.workers,
367
+ notifications: [
368
+ ...base.notifications!,
369
+ {
370
+ id: "note-long",
371
+ severity: "confirmation_required",
372
+ title: "Confirmation required for conditional merge with carryover",
373
+ reason: extra,
374
+ source: "gate",
375
+ sourceId: "gate-long-confirmation-required",
376
+ createdAt: timestamp,
377
+ payload: { evidence: extra }
378
+ }
379
+ ],
380
+ recoveryWarnings: [extra]
381
+ };
382
+ }
383
+ if (name === "stale-gate" || name === "historical-worker-failure" || name === "historical-blocked-run") {
384
+ return historicalFixture(name, base);
385
+ }
386
+ if (name.startsWith("workflow-")) {
387
+ return workflowMissionFixture(name, base);
388
+ }
389
+ if (name.startsWith("generic-")) {
390
+ return genericFixture(name, base);
391
+ }
392
+ if (name === "many-events") {
393
+ return {
394
+ ...base,
395
+ events: numberedEvents(36, "Fixture event generated for scroll and ledger density checks."),
396
+ workers: Array.from({ length: 10 }, (_, index) => ({
397
+ id: `worker-${index + 1}`,
398
+ type: index % 2 === 0 ? "implementation" : "reviewer",
399
+ status: index % 3 === 0 ? "running" : "succeeded",
400
+ startedAt: timestamp,
401
+ resultArtifactId: `artifact-${index + 1}`
402
+ })),
403
+ artifacts: Array.from({ length: 10 }, (_, index) => artifactRecord(`artifact-${index + 1}`, "log", `worker-${index + 1}.log`))
404
+ };
405
+ }
406
+ return base;
407
+ }
408
+
409
+ function historicalFixture(name: FixtureName, base: MissionControlData): MissionControlData {
410
+ const historicalGate = {
411
+ id: "gate-historical-worker-failed",
412
+ kind: "worker_failed",
413
+ status: "open",
414
+ message: "Worker failed in a previous run; a newer run has already superseded it.",
415
+ details: { workerId: "worker-old-failed", exitCode: 1 },
416
+ createdAt: "2026-06-12T08:30:00.000Z",
417
+ activity: "historical" as const,
418
+ activityReason: name === "stale-gate" ? "overridden_by_reality" : "historical_run"
419
+ };
420
+ const activeGate = {
421
+ id: "gate-active-confirmation",
422
+ kind: "confirmation_required",
423
+ status: "open",
424
+ message: "Current run needs a fresh operator decision.",
425
+ createdAt: timestamp,
426
+ activity: "active" as const,
427
+ activityReason: "current_run"
428
+ };
429
+ const staleWorker = {
430
+ id: "worker-old-failed",
431
+ type: "implementation",
432
+ status: "failed",
433
+ startedAt: "2026-06-12T08:10:00.000Z",
434
+ completedAt: "2026-06-12T08:25:00.000Z",
435
+ error: "Old worker failed before the current run started.",
436
+ activity: "historical" as const,
437
+ activityReason: "stale_worker_failure"
438
+ };
439
+ const current = name === "historical-blocked-run"
440
+ ? {
441
+ status: "READY",
442
+ nextAction: "Historical gates are visible for recovery, but the current run is not blocked.",
443
+ run: {
444
+ id: "run-current-ready",
445
+ status: "READY",
446
+ currentState: "MERGE",
447
+ branch: "codex/current-ready",
448
+ worktreeClean: true,
449
+ updatedAt: timestamp,
450
+ startedAt: "2026-06-12T09:30:00.000Z"
451
+ }
452
+ }
453
+ : {
454
+ ...base.current,
455
+ status: "BLOCKED",
456
+ nextAction: "Handle the current active gate; historical gates are shown separately.",
457
+ gate: { kind: activeGate.kind, message: activeGate.message },
458
+ ...(base.current.run ? {
459
+ run: {
460
+ ...base.current.run,
461
+ id: "run-current-active",
462
+ status: "RUNNING",
463
+ startedAt: "2026-06-12T09:30:00.000Z"
464
+ }
465
+ } : {})
466
+ };
467
+ return {
468
+ ...base,
469
+ current,
470
+ gates: name === "historical-blocked-run" ? [historicalGate] : [activeGate, historicalGate],
471
+ workers: name === "stale-gate"
472
+ ? base.workers
473
+ : [staleWorker, ...base.workers.map((worker) => ({ ...worker, activity: "active" as const, activityReason: "current_run" }))],
474
+ recoveryWarnings: [
475
+ "1 historical open gate belongs to an inactive or superseded run.",
476
+ ...(name === "historical-worker-failure" ? ["1 stale worker failure is from an older run."] : [])
477
+ ],
478
+ notifications: name === "historical-blocked-run" ? [] : [{
479
+ id: "note-active-confirmation",
480
+ severity: "confirmation_required",
481
+ title: activeGate.kind,
482
+ reason: activeGate.message,
483
+ source: "gate",
484
+ sourceId: activeGate.id,
485
+ createdAt: timestamp
486
+ }]
487
+ };
488
+ }
489
+
490
+ function workflowMissionFixture(name: FixtureName, base: MissionControlData): MissionControlData {
491
+ const currentStateByName: Partial<Record<FixtureName, string>> = {
492
+ "workflow-build-active": "IMPLEMENT",
493
+ "workflow-verify-failed": "SELF_CHECK",
494
+ "workflow-pr-published": "COMMIT_PUSH_PR",
495
+ "workflow-review-active": "WAIT_REVIEW_OR_CI",
496
+ "workflow-merge-blocked": "READY_TO_MERGE",
497
+ "workflow-cleanup-active": "MERGE",
498
+ "workflow-unknown-state": "UNEXPECTED_STATE"
499
+ };
500
+ const currentState = currentStateByName[name] ?? "IMPLEMENT";
501
+ const pr = ["workflow-pr-published", "workflow-review-active", "workflow-merge-blocked", "workflow-cleanup-active"].includes(name)
502
+ ? { ...base.pr!, prNumber: 49, url: "https://github.com/example/fixture/pull/49", state: name === "workflow-cleanup-active" ? "MERGED" : "OPEN" }
503
+ : undefined;
504
+ const ci = name === "workflow-verify-failed"
505
+ ? [{ id: "ci-failed", name: "Node 22.x", status: "completed", conclusion: "failure", observedAt: timestamp }]
506
+ : name === "workflow-merge-blocked"
507
+ ? [{ id: "ci-pending", name: "Node 24.x", status: "queued", observedAt: timestamp }]
508
+ : base.ci;
509
+ return {
510
+ ...base,
511
+ current: {
512
+ ...base.current,
513
+ status: name === "workflow-merge-blocked" ? "BLOCKED" : "RUNNING",
514
+ nextAction: "Observe the PR delivery workflow stage.",
515
+ ...(base.current.run ? { run: { ...base.current.run, currentState, status: name === "workflow-merge-blocked" ? "BLOCKED" : "RUNNING" } } : {})
516
+ },
517
+ ...(pr ? { pr } : {}),
518
+ ci,
519
+ reviewComments: name === "workflow-review-active" ? base.reviewComments : [],
520
+ gates: name === "workflow-merge-blocked"
521
+ ? [{ id: "gate-ci-missing", kind: "ci_required_checks_missing", status: "open", message: "Required CI check is still pending.", createdAt: timestamp, activity: "active", activityReason: "current_run" }]
522
+ : [],
523
+ events: [
524
+ ...base.events,
525
+ eventRecord("event-workflow-plan", 20, "workflow_stage_evidence", "Plan accepted for PR O.", "plan"),
526
+ eventRecord("event-workflow-build", 21, "workflow_stage_evidence", "Build stage implementation is active.", "build"),
527
+ ...(name === "workflow-review-active" ? [eventRecord("event-workflow-review", 22, "workflow_stage_evidence", "Claude ACP review evidence source is unknown in fixture.", "review")] : []),
528
+ ...(name === "workflow-cleanup-active" ? [eventRecord("event-workflow-cleanup", 23, "workflow_stage_evidence", "PR merged; cleanup is active.", "cleanup")] : [])
529
+ ],
530
+ ...(name === "workflow-merge-blocked"
531
+ ? { mergeReadiness: { state: "missing_evidence", ready: false, missingConditions: ["required check green: Node 24.x"], evidence: ["review report posted"], carryoverRecords: [] } }
532
+ : base.mergeReadiness ? { mergeReadiness: base.mergeReadiness } : {})
533
+ };
534
+ }
535
+
536
+ function genericFixture(name: FixtureName, base: MissionControlData): MissionControlData {
537
+ const genericState = name === "generic-completed"
538
+ ? "COMPLETE"
539
+ : name === "generic-deliverable-ready"
540
+ ? "DELIVER"
541
+ : name === "generic-scope-change"
542
+ ? "EXECUTE_STEP"
543
+ : "HUMAN_GATE";
544
+ const gateKind = name === "generic-scope-change"
545
+ ? "generic_scope_change_requested"
546
+ : name === "generic-human-gate"
547
+ ? "generic_human_gate"
548
+ : "generic_goal_needs_confirmation";
549
+ const gateDetails = name === "generic-scope-change"
550
+ ? { allowedNextStates: ["PLAN_WORK", "STOPPED"], defaultNextState: "PLAN_WORK", state: "EXECUTE_STEP" }
551
+ : { allowedNextStates: ["DELIVER", "EXECUTE_STEP", "STOPPED"], defaultNextState: "DELIVER", state: "HUMAN_GATE" };
552
+ const blockedByGate = name === "generic-human-gate" || name === "generic-scope-change";
553
+ const { pr: _pr, ...withoutPr } = base;
554
+ return {
555
+ ...withoutPr,
556
+ current: {
557
+ status: name === "generic-completed" ? "READY" : blockedByGate ? "BLOCKED" : "RUNNING",
558
+ nextAction: name === "generic-completed"
559
+ ? "Generic workflow completed and deliverable is ready for audit."
560
+ : blockedByGate
561
+ ? "Operator confirmation is needed before the generic workflow continues."
562
+ : "Package the deliverable and prepare completion evidence.",
563
+ run: {
564
+ id: "run-generic-fixture",
565
+ status: name === "generic-completed" ? "READY" : blockedByGate ? "BLOCKED" : "RUNNING",
566
+ currentState: genericState,
567
+ branch: "main",
568
+ worktreeClean: true,
569
+ updatedAt: timestamp,
570
+ startedAt: "2026-06-12T09:00:00.000Z"
571
+ },
572
+ ...(blockedByGate ? { gate: { kind: gateKind, message: "Generic loop needs operator confirmation." } } : {})
573
+ },
574
+ gates: blockedByGate ? [{
575
+ id: `gate-${gateKind}`,
576
+ kind: gateKind,
577
+ status: "open",
578
+ message: "Generic loop needs operator confirmation.",
579
+ details: gateDetails,
580
+ createdAt: timestamp
581
+ }] : [],
582
+ workers: [{
583
+ id: "worker-generic-deliverable",
584
+ type: "implementation",
585
+ status: name === "generic-completed" || name === "generic-deliverable-ready" ? "succeeded" : "running",
586
+ startedAt: timestamp,
587
+ resultArtifactId: "artifact-generic-deliverable"
588
+ }],
589
+ artifacts: [
590
+ artifactRecord("artifact-generic-plan", "dry-run-plan", "generic-loop-plan.md"),
591
+ artifactRecord("artifact-generic-deliverable", "worker-result", "repo-hygiene-audit.md")
592
+ ],
593
+ events: numberedEvents(10, "Generic loop fixture event."),
594
+ timelineSummary: {
595
+ latest: timelineFromData({ ...base, events: numberedEvents(10, "Generic loop fixture event.") })[0]!,
596
+ ...(name === "generic-completed" ? {} : { activeWorker: { id: "worker-generic-deliverable", type: "implementation", status: "running", startedAt: timestamp } }),
597
+ hasObservationGap: false
598
+ },
599
+ mergeReadiness: {
600
+ state: "not_applicable",
601
+ ready: name === "generic-completed" || name === "generic-deliverable-ready",
602
+ missingConditions: blockedByGate ? ["human confirmation"] : [],
603
+ evidence: ["context collected", "plan reviewed", "deliverable artifact registered"],
604
+ carryoverRecords: []
605
+ },
606
+ notifications: blockedByGate ? [{
607
+ id: `generic:${gateKind}`,
608
+ severity: "confirmation_required",
609
+ title: gateKind,
610
+ reason: "Generic loop needs operator confirmation.",
611
+ source: "gate",
612
+ sourceId: `gate-${gateKind}`,
613
+ createdAt: timestamp
614
+ }] : [],
615
+ profile: genericProfileFixture(genericState),
616
+ selection: {
617
+ mode: "generic_loop",
618
+ ambiguous: false,
619
+ loopShape: "generic-loop",
620
+ workflowProfile: "repo_hygiene_loop",
621
+ reason: "Generic loop fixture does not select a PR.",
622
+ evidence: ["Configured loopShape=generic-loop."]
623
+ },
624
+ plan: {
625
+ convention: base.plan!.convention,
626
+ currentMilestone: base.plan!.currentMilestone,
627
+ completed: base.plan!.completed,
628
+ candidates: [],
629
+ ambiguous: false,
630
+ evidence: ["Generic loop does not use PR spec selection."]
631
+ },
632
+ recoveryWarnings: []
633
+ };
634
+ }
635
+
636
+ function baseFixture(): MissionControlData {
637
+ return {
638
+ current: {
639
+ status: "BLOCKED",
640
+ nextAction: "Review gate evidence, then approve or reject with a note.",
641
+ run: {
642
+ id: "run-fixture",
643
+ status: "BLOCKED",
644
+ currentState: "WAIT_REVIEW_OR_CI",
645
+ branch: "codex/pr-h-bilingual-i18n",
646
+ worktreeClean: true,
647
+ updatedAt: timestamp,
648
+ startedAt: "2026-06-12T09:00:00.000Z"
649
+ },
650
+ gate: { kind: "policy_violation", message: "Self check evidence is required before merge." }
651
+ },
652
+ gates: [{
653
+ id: "gate-fixture",
654
+ kind: "policy_violation",
655
+ status: "open",
656
+ message: "Self check evidence is required before merge.",
657
+ createdAt: timestamp
658
+ }],
659
+ pr: {
660
+ prNumber: 8,
661
+ url: "https://github.test/pr/8",
662
+ branch: "codex/pr-h-bilingual-i18n",
663
+ state: "OPEN",
664
+ draft: false,
665
+ updatedAt: timestamp
666
+ },
667
+ ci: [{
668
+ id: "ci-fixture",
669
+ name: "ci",
670
+ status: "completed",
671
+ conclusion: "success",
672
+ observedAt: timestamp
673
+ }],
674
+ reviewComments: [{
675
+ id: "comment-fixture",
676
+ author: "reviewer",
677
+ path: "plugins/autonomous-pr-loop/ui/src/app.tsx",
678
+ body: "Carry forward low priority visual polish only after current PR scope is complete.",
679
+ actionable: true,
680
+ isResolved: false,
681
+ isOutdated: false,
682
+ status: "open"
683
+ }],
684
+ workers: [{
685
+ id: "worker-fixture",
686
+ type: "reviewer",
687
+ status: "succeeded",
688
+ startedAt: timestamp,
689
+ completedAt: timestamp,
690
+ resultArtifactId: "artifact-fixture"
691
+ }],
692
+ artifacts: [artifactRecord("artifact-fixture", "dry-run-plan", "dry-run-plan.json")],
693
+ events: numberedEvents(8, "Dashboard fixture event."),
694
+ decisions: [{ id: "decision-fixture", kind: "gate_approved", message: "Fixture decision.", createdAt: timestamp }],
695
+ timelineSummary: {
696
+ hasObservationGap: false
697
+ },
698
+ autonomy: {
699
+ autonomyMode: "autonomous_until_gate",
700
+ mergeMode: "conditional",
701
+ notifyMode: "important_only",
702
+ reviewHandling: "fix_scoped_and_carry_forward",
703
+ summary: "Agent advances under configured workflow boundaries; operator watches and intervenes only on material gates.",
704
+ notifyWhen: ["blocked", "confirmation_required", "policy_violation"],
705
+ requiresConfirmation: ["dangerous policy changes", "merge without complete evidence"],
706
+ allowConditionalMerge: true
707
+ },
708
+ mergeReadiness: {
709
+ state: "missing_evidence",
710
+ ready: false,
711
+ missingConditions: ["review approval observed"],
712
+ evidence: ["scope guard passed", "ci green"],
713
+ carryoverRecords: ["docs/local-release-readiness.md"]
714
+ },
715
+ notifications: [{
716
+ id: "note-fixture",
717
+ severity: "blocked",
718
+ title: "policy_violation",
719
+ reason: "A policy guard blocked unsafe progress.",
720
+ source: "gate",
721
+ sourceId: "gate-fixture",
722
+ createdAt: timestamp
723
+ }],
724
+ profile: profileFixture("WAIT_REVIEW_OR_CI"),
725
+ plan: {
726
+ convention: "PR specs use pr-<letter> filenames.",
727
+ currentMilestone: "PR H",
728
+ selectedNext: {
729
+ id: "PR H",
730
+ title: "PR H Bilingual i18n",
731
+ status: "next",
732
+ file: "docs/local-release-readiness.md",
733
+ dependsOn: ["PR G"],
734
+ issueRefs: [],
735
+ whySelected: "Add bilingual dashboard and CLI i18n."
736
+ },
737
+ completed: [],
738
+ candidates: [],
739
+ ambiguous: false,
740
+ evidence: ["Parsed PR H SPEC.", "PR G is complete."]
741
+ },
742
+ recoveryWarnings: []
743
+ };
744
+ }
745
+
746
+ function timelineFromData(data: Pick<MissionControlData, "current" | "events" | "workers">): AgentTimelineEntry[] {
747
+ const runId = data.current.run?.id;
748
+ const eventEntries = data.events.map((event) => ({
749
+ timelineSeq: event.seq,
750
+ occurredAt: event.createdAt,
751
+ cursor: btoa(JSON.stringify({ timelineSeq: event.seq })),
752
+ source: "event" as const,
753
+ kind: event.kind,
754
+ ...(runId ? { runId } : {}),
755
+ ...(event.stateAfter ? { status: event.stateAfter } : {}),
756
+ title: event.kind,
757
+ summary: event.message,
758
+ createdAt: event.createdAt,
759
+ rawRef: { table: "events", id: event.id, seq: event.seq }
760
+ }));
761
+ const workerEntries = data.workers.map((worker, index) => ({
762
+ timelineSeq: 100 + index,
763
+ occurredAt: worker.startedAt,
764
+ cursor: btoa(JSON.stringify({ timelineSeq: 100 + index })),
765
+ source: "worker" as const,
766
+ kind: worker.type,
767
+ ...(runId ? { runId } : {}),
768
+ workerId: worker.id,
769
+ title: `${worker.type} worker ${worker.status}`,
770
+ summary: worker.error ?? worker.resultArtifactId ?? worker.status,
771
+ status: worker.status,
772
+ createdAt: worker.startedAt,
773
+ rawRef: { table: "workers", id: `${worker.id}:${worker.status}` }
774
+ }));
775
+ const workerEventEntries = data.workers.flatMap((worker, workerIndex) =>
776
+ workerEventFixtures(worker.id).map((event, eventIndex) => {
777
+ const timelineSeq = 200 + workerIndex * 10 + eventIndex;
778
+ return {
779
+ timelineSeq,
780
+ occurredAt: worker.completedAt ?? worker.startedAt,
781
+ cursor: btoa(JSON.stringify({ timelineSeq })),
782
+ source: "worker_event" as const,
783
+ kind: event.kind,
784
+ ...(runId ? { runId } : {}),
785
+ workerId: worker.id,
786
+ title: event.title,
787
+ summary: event.summary,
788
+ status: worker.status,
789
+ createdAt: worker.completedAt ?? worker.startedAt,
790
+ rawRef: { table: "worker_events", id: `${worker.id}-${event.kind}`, seq: eventIndex + 1 }
791
+ };
792
+ })
793
+ );
794
+ return [...workerEventEntries, ...workerEntries, ...eventEntries].sort((a, b) =>
795
+ Date.parse(b.occurredAt) - Date.parse(a.occurredAt) || b.timelineSeq - a.timelineSeq
796
+ );
797
+ }
798
+
799
+ function workerEventFixtures(workerId: string): Array<{ kind: string; title: string; summary: string }> {
800
+ if (workerId !== "worker-running") {
801
+ return [{
802
+ kind: "command_execution",
803
+ title: "command_execution",
804
+ summary: "pnpm test completed with fixture output"
805
+ }];
806
+ }
807
+ return [
808
+ { kind: "command_execution", title: "command_execution", summary: "pnpm test is running" },
809
+ { kind: "file_change", title: "file_change", summary: "src/index.ts updated" },
810
+ { kind: "mcp_tool_call", title: "mcp_tool_call", summary: "gitnexus impact query completed" },
811
+ { kind: "web_search", title: "web_search", summary: "searched official docs" },
812
+ { kind: "todo_list", title: "todo_list", summary: "3 todos, 1 in progress" },
813
+ { kind: "error", title: "error", summary: "worker command failed with exit code 1" }
814
+ ];
815
+ }
816
+
817
+ function policyConfigFixture(): ConfigSnapshot {
818
+ return {
819
+ path: ".agent-loop/config.json",
820
+ hash: "fixture-hash",
821
+ mtimeMs: 1,
822
+ config: {
823
+ repoId: "example/fixture",
824
+ locale: "zh-CN",
825
+ baseBranch: "main",
826
+ branchPrefix: "codex/",
827
+ plansDir: "docs/plans",
828
+ gitnexusRequired: true,
829
+ requiredChecks: ["ci"],
830
+ requireReviewApproval: true,
831
+ autonomyMode: "autonomous_until_gate",
832
+ mergeMode: "conditional",
833
+ notifyMode: "important_only",
834
+ reviewHandling: "fix_scoped_and_carry_forward",
835
+ carryoverTarget: "docs/local-release-readiness.md",
836
+ allowAutoMerge: false,
837
+ maxReviewFixRounds: 3,
838
+ maxTestFixRounds: 2,
839
+ maxCiReruns: 1,
840
+ protectedPaths: [".agent-loop/**"]
841
+ }
842
+ };
843
+ }
844
+
845
+ function dryRunFixture(data: MissionControlData): DryRunPreviewData {
846
+ const nextPr = data.plan?.selectedNext;
847
+ return {
848
+ ...(nextPr ? { nextPr } : {}),
849
+ branchName: "codex/pr-h-bilingual-i18n",
850
+ commandsPlanned: ["pnpm test", "pnpm lint", "pnpm agent-loop dashboard --help"],
851
+ workerType: "implementation",
852
+ possibleGates: ["policy_violation", "confirmation_required"],
853
+ missingConditions: data.mergeReadiness?.missingConditions ?? [],
854
+ filesLikelyTouched: ["plugins/autonomous-pr-loop/ui", "docs/checklists"],
855
+ autonomyForecast: data.autonomy!,
856
+ mergeForecast: data.mergeReadiness!,
857
+ profile: data.profile ?? profileFixture(),
858
+ workflowStages: workflowStageFixtures()
859
+ };
860
+ }
861
+
862
+ const workflowStageOrder: Array<{ id: WorkflowStageId; label: string }> = [
863
+ { id: "work_item", label: "Work Item" },
864
+ { id: "plan", label: "Plan" },
865
+ { id: "build", label: "Build" },
866
+ { id: "verify", label: "Verify" },
867
+ { id: "pr", label: "PR" },
868
+ { id: "review", label: "Review" },
869
+ { id: "merge_readiness", label: "Merge Readiness" },
870
+ { id: "cleanup", label: "Cleanup" }
871
+ ];
872
+
873
+ function workflowBoardFixture(data: MissionControlData): WorkflowBoard {
874
+ const state = data.current.run?.currentState;
875
+ const unsupported = data.profile?.loopShape === "generic-loop";
876
+ const unknown = Boolean(state && !["SYNC_MAIN", "DISCOVER_PROGRESS", "SELECT_NEXT_PR", "WRITE_SPEC", "CREATE_BRANCH", "IMPLEMENT", "SELF_CHECK", "COMMIT_PUSH_PR", "WAIT_REVIEW_OR_CI", "FIX_REVIEW", "PUSH_FIX", "READY_TO_MERGE", "MERGE", "BLOCKED", "STOPPED"].includes(state));
877
+ const activeStageId = unsupported || unknown ? "work_item" : stageForFixtureState(state, data);
878
+ const stages = workflowStageOrder.map((stage, index) => fixtureStage(stage, index, activeStageId, data));
879
+ return {
880
+ runId: data.current.run?.id,
881
+ mode: unsupported ? "unsupported" : unknown ? "unknown_state" : data.current.run ? "active" : "empty",
882
+ activeStageId,
883
+ selectedStageId: activeStageId,
884
+ workItem: {
885
+ runId: data.current.run?.id,
886
+ branch: data.current.run?.branch,
887
+ currentState: data.current.run?.currentState,
888
+ status: data.current.status,
889
+ loopShape: data.profile?.loopShape ?? "pr-loop",
890
+ workflowProfile: data.profile?.workflowProfile,
891
+ prUrl: data.pr?.url,
892
+ prNumber: data.pr?.prNumber,
893
+ lastUpdate: data.current.run?.updatedAt,
894
+ activeGate: data.gates.find((gate) => gate.status === "open")?.kind,
895
+ readOnly: false
896
+ },
897
+ stages,
898
+ evidenceRefs: data.events.map((event) => ({
899
+ id: event.id,
900
+ kind: "event",
901
+ label: event.kind,
902
+ summary: event.message,
903
+ interaction: "drill_down_link" as const,
904
+ drillDownTarget: { page: "Event Ledger" },
905
+ createdAt: event.createdAt,
906
+ source: fixtureEventStage(event)
907
+ })),
908
+ reviewReports: [
909
+ { id: "review-claude", agent: "Claude ACP", status: "unknown", prComment: "unknown", severitySummary: "no requirement source", requirement: "unknown", progress: "unknown", result: "unknown", reason: "No required Claude review source in fixture.", evidenceRefIds: [] },
910
+ { id: "review-agy", agent: "AGY/Gemini", status: "unknown", prComment: "unknown", severitySummary: "no requirement source", requirement: "unknown", progress: "unknown", result: "unknown", reason: "No required AGY/Gemini review source in fixture.", evidenceRefIds: [] }
911
+ ],
912
+ verificationChecks: [
913
+ { id: "lint", label: "Lint", status: "unknown", evidence: "no appended evidence", owner: "Codex" },
914
+ { id: "tests", label: "Tests", status: data.ci.some((check) => check.conclusion === "failure") ? "failed" : "unknown", evidence: data.ci[0]?.conclusion ?? "no appended evidence", owner: "Codex" },
915
+ { id: "gitnexus", label: "GitNexus detect", status: "unknown", evidence: "no appended evidence", owner: "GitNexus" }
916
+ ],
917
+ mergeReadinessChecks: [
918
+ ...(data.mergeReadiness?.evidence ?? []).map((item, index) => ({ id: `merge-ok-${index}`, label: item, status: "passed" as const, evidence: item, owner: "Codex" })),
919
+ ...(data.mergeReadiness?.missingConditions ?? []).map((item, index) => ({ id: `merge-missing-${index}`, label: item, status: "blocked" as const, evidence: item, owner: "Codex" })),
920
+ { id: "severity", label: "No unresolved P0/P1/P2", status: "unknown", evidence: "no severity evidence", owner: "Reviewer" }
921
+ ],
922
+ cleanupChecks: [
923
+ { id: "merged", label: "PR merged", status: data.pr?.state === "MERGED" ? "passed" : "pending", evidence: data.pr?.state ?? "no PR", owner: "GitHub" },
924
+ { id: "clean", label: "Worktree clean", status: data.current.run?.worktreeClean ? "passed" : "unknown", evidence: String(data.current.run?.worktreeClean ?? "unknown"), owner: "Codex" }
925
+ ],
926
+ appendEvidenceEnabled: !unsupported && !unknown && Boolean(data.current.run),
927
+ ...(unsupported ? { message: "PR O observes only pr-loop runs." } : unknown ? { message: `Unknown PR loop state: ${state}` } : {})
928
+ };
929
+ }
930
+
931
+ function fixtureStage(stage: { id: WorkflowStageId; label: string }, index: number, activeStageId: WorkflowStageId, data: MissionControlData): WorkflowBoardStage {
932
+ const activeIndex = workflowStageOrder.findIndex((item) => item.id === activeStageId);
933
+ const baseStatus: WorkflowStageStatus = index < activeIndex ? "done" : index === activeIndex ? "active" : "pending";
934
+ const blocked = data.gates.some((gate) => gate.status === "open") && stage.id === activeStageId;
935
+ const failed = stage.id === "verify" && data.ci.some((check) => check.conclusion === "failure");
936
+ const status: WorkflowStageStatus = blocked ? "blocked" : failed ? "failed" : baseStatus;
937
+ return {
938
+ id: stage.id,
939
+ label: stage.label,
940
+ status,
941
+ actorChips: [{ actor: "codex", label: "Codex", status }],
942
+ evidenceCounts: { events: data.events.filter((event) => fixtureEventStage(event) === stage.id).length, artifacts: 0, gates: blocked ? 1 : 0, prComments: stage.id === "review" ? data.reviewComments.length : 0, gitnexus: 0, browser: 0, ci: stage.id === "merge_readiness" ? data.ci.length : 0, reports: 0 },
943
+ substages: [{ id: `${stage.id}-summary`, label: `${stage.label} summary`, status, evidenceCounts: { events: 0, artifacts: 0, gates: 0, prComments: 0, gitnexus: 0, browser: 0, ci: 0, reports: 0 }, latestEvidence: [], requiredEvidence: [] }],
944
+ latestAction: { label: "Inspect stage evidence", safeToRunFromDashboard: false, requiresConfirmation: false },
945
+ blockers: blocked ? [{ id: data.gates[0]?.id ?? "gate", severity: "ci", title: data.gates[0]?.kind ?? "gate", reason: data.gates[0]?.message ?? "blocked", owner: "Codex", nextAction: "Resolve the blocking gate.", evidenceRefIds: [] }] : [],
946
+ nextAction: "Inspect stage evidence"
947
+ };
948
+ }
949
+
950
+ function stageForFixtureState(state: string | undefined, data: MissionControlData): WorkflowStageId {
951
+ if (state === "IMPLEMENT" || state === "CREATE_BRANCH") return "build";
952
+ if (state === "SELF_CHECK") return "verify";
953
+ if (state === "COMMIT_PUSH_PR" || state === "PUSH_FIX") return "pr";
954
+ if (state === "WAIT_REVIEW_OR_CI" || state === "FIX_REVIEW") return data.reviewComments.length > 0 ? "review" : "merge_readiness";
955
+ if (state === "READY_TO_MERGE") return "merge_readiness";
956
+ if (state === "MERGE") return "cleanup";
957
+ if (state === "WRITE_SPEC") return "plan";
958
+ return "work_item";
959
+ }
960
+
961
+ function fixtureEventStage(event: EventSummary): WorkflowStageId {
962
+ const payload = event as EventSummary & { payload?: { stageId?: WorkflowStageId } };
963
+ if (payload.payload?.stageId) return payload.payload.stageId;
964
+ const message = event.message.toLowerCase();
965
+ if (message.includes("cleanup")) return "cleanup";
966
+ if (message.includes("review")) return "review";
967
+ if (message.includes("build")) return "build";
968
+ if (message.includes("plan")) return "plan";
969
+ return "work_item";
970
+ }
971
+
972
+ function profileFixture(currentState?: string): NonNullable<MissionControlData["profile"]> {
973
+ const roleMapping = [
974
+ {
975
+ state: "WRITE_SPEC",
976
+ alias: "planner",
977
+ label: "Planner",
978
+ workerType: "planner",
979
+ sandbox: "workspace-write"
980
+ },
981
+ {
982
+ state: "IMPLEMENT",
983
+ alias: "implementer",
984
+ label: "Implementer",
985
+ workerType: "implementation",
986
+ sandbox: "workspace-write"
987
+ },
988
+ {
989
+ state: "FIX_REVIEW",
990
+ alias: "review-fix",
991
+ label: "Review fix",
992
+ workerType: "review-fix",
993
+ sandbox: "workspace-write"
994
+ },
995
+ {
996
+ state: "SELF_CHECK",
997
+ alias: "reviewer",
998
+ label: "Reviewer",
999
+ workerType: "reviewer",
1000
+ sandbox: "read-only"
1001
+ }
1002
+ ];
1003
+ const currentRole = currentState ? roleMapping.find((role) => role.state === currentState) : undefined;
1004
+ return {
1005
+ loopShape: "pr-loop",
1006
+ workflowProfile: "default_pr_loop",
1007
+ workflowLabel: "Default PR loop",
1008
+ workflowDescription: "The HOLO-Codex PR delivery behavior with explicit profile audit.",
1009
+ roleProfile: "default_pr_roles",
1010
+ ...(currentRole ? { currentRole } : {}),
1011
+ roleMapping,
1012
+ autonomyBoundary: "Autonomous until configured gates, policy violations, CI/review blockers, or unsafe git actions.",
1013
+ handoffSummary: "Follow the selected PR spec and hand off concise evidence to the next role.",
1014
+ validationPosture: "Use configured lint, tests, GitNexus, CI, and review gates.",
1015
+ likelyGates: ["ambiguous_next_pr", "worker_failed", "ci_required_checks_missing", "merge_requires_confirmation"],
1016
+ availableWorkflows: [
1017
+ { id: "default_pr_loop", label: "Default PR loop", description: "The HOLO-Codex PR delivery behavior with explicit profile audit." },
1018
+ { id: "docs_only_loop", label: "Docs-only loop", description: "Bias validation toward documentation consistency while preserving policy and configured checks." },
1019
+ { id: "review_fix_loop", label: "Review-fix loop", description: "Focus on scoped PR review repair and carryover discipline." },
1020
+ { id: "release_ready_loop", label: "Release-ready loop", description: "Tighten merge readiness explanation without adding a release-manager worker." }
1021
+ ],
1022
+ availableRoleProfiles: [
1023
+ { id: "default_pr_roles", label: "Default PR roles", description: "Readable role aliases mapped onto the existing PR loop worker types." }
1024
+ ]
1025
+ };
1026
+ }
1027
+
1028
+ function genericProfileFixture(currentState: string): NonNullable<MissionControlData["profile"]> {
1029
+ const roleMapping = [
1030
+ {
1031
+ state: "DEFINE_GOAL",
1032
+ alias: "planner",
1033
+ label: "Goal planner",
1034
+ workerType: "planner",
1035
+ sandbox: "read-only"
1036
+ },
1037
+ {
1038
+ state: "COLLECT_CONTEXT",
1039
+ alias: "planner",
1040
+ label: "Context collector",
1041
+ workerType: "planner",
1042
+ sandbox: "read-only"
1043
+ },
1044
+ {
1045
+ state: "PLAN_WORK",
1046
+ alias: "planner",
1047
+ label: "Work planner",
1048
+ workerType: "planner",
1049
+ sandbox: "read-only"
1050
+ },
1051
+ {
1052
+ state: "EXECUTE_STEP",
1053
+ alias: "implementer",
1054
+ label: "Executor",
1055
+ workerType: "implementation",
1056
+ sandbox: "workspace-write"
1057
+ },
1058
+ {
1059
+ state: "SELF_REVIEW",
1060
+ alias: "reviewer",
1061
+ label: "Reviewer",
1062
+ workerType: "reviewer",
1063
+ sandbox: "read-only"
1064
+ },
1065
+ {
1066
+ state: "DELIVER",
1067
+ alias: "implementer",
1068
+ label: "Deliverer",
1069
+ workerType: "implementation",
1070
+ sandbox: "workspace-write"
1071
+ }
1072
+ ];
1073
+ const currentRole = roleMapping.find((role) => role.state === currentState);
1074
+ return {
1075
+ loopShape: "generic-loop",
1076
+ workflowProfile: "repo_hygiene_loop",
1077
+ workflowLabel: "Repo hygiene loop",
1078
+ workflowDescription: "Audit repository hygiene and produce a scoped report artifact.",
1079
+ roleProfile: "default_pr_roles",
1080
+ ...(currentRole ? { currentRole } : {}),
1081
+ roleMapping,
1082
+ autonomyBoundary: "Read-only until execute/deliver states; write access limited by profile allowed roots.",
1083
+ handoffSummary: "List inspected areas, hygiene findings, safe fixes, deferred risks, and deliverable path.",
1084
+ validationPosture: "Use profile checklist, self-review, human gate, and audit timeline.",
1085
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
1086
+ lifecycleKind: "generic",
1087
+ expectedDeliverable: "Repo hygiene audit report",
1088
+ allowedWriteRoots: ["docs", "reports"],
1089
+ availableWorkflows: [
1090
+ { id: "research_report_loop", label: "Research report loop", description: "Gather sources and deliver a report." },
1091
+ { id: "document_preparation_loop", label: "Document preparation loop", description: "Prepare a structured document artifact." },
1092
+ { id: "repo_hygiene_loop", label: "Repo hygiene loop", description: "Audit and clean repository hygiene tasks." },
1093
+ { id: "weekly_review_loop", label: "Weekly review loop", description: "Collect context and produce a weekly review deliverable." },
1094
+ { id: "data_extraction_loop", label: "Data extraction loop", description: "Extract data and request human approval before delivery." }
1095
+ ],
1096
+ availableRoleProfiles: [
1097
+ { id: "default_pr_roles", label: "Default PR roles", description: "Readable role aliases mapped onto the existing worker types." }
1098
+ ]
1099
+ };
1100
+ }
1101
+
1102
+ function workflowStageFixtures(): NonNullable<DryRunPreviewData["workflowStages"]> {
1103
+ return [
1104
+ { state: "SELECT_NEXT_PR", gateExpected: true },
1105
+ { state: "WRITE_SPEC", roleAlias: "planner", workerType: "planner", gateExpected: false },
1106
+ { state: "IMPLEMENT", roleAlias: "implementer", workerType: "implementation", gateExpected: false },
1107
+ { state: "SELF_CHECK", roleAlias: "reviewer", workerType: "reviewer", gateExpected: false },
1108
+ { state: "WAIT_REVIEW_OR_CI", gateExpected: true },
1109
+ { state: "FIX_REVIEW", roleAlias: "review-fix", workerType: "review-fix", gateExpected: false },
1110
+ { state: "MERGE", gateExpected: false }
1111
+ ];
1112
+ }
1113
+
1114
+ function numberedEvents(count: number, message: string): EventSummary[] {
1115
+ return Array.from({ length: count }, (_, index) => ({
1116
+ id: `event-${index + 1}`,
1117
+ seq: index + 1,
1118
+ kind: index % 3 === 0 ? "scope.guard.checked" : "dashboard.fixture",
1119
+ message,
1120
+ stateBefore: "IMPLEMENT",
1121
+ stateAfter: index % 3 === 0 ? "SELF_CHECK" : "WAIT_REVIEW_OR_CI",
1122
+ createdAt: timestamp,
1123
+ artifactIds: index % 4 === 0 ? ["artifact-fixture"] : []
1124
+ }));
1125
+ }
1126
+
1127
+ function eventRecord(id: string, seq: number, kind: string, message: string, stageId: WorkflowStageId): EventSummary & { payload: { stageId: WorkflowStageId } } {
1128
+ return {
1129
+ id,
1130
+ seq,
1131
+ kind,
1132
+ message,
1133
+ stateBefore: "IMPLEMENT",
1134
+ stateAfter: "SELF_CHECK",
1135
+ createdAt: timestamp,
1136
+ payload: { stageId }
1137
+ };
1138
+ }
1139
+
1140
+ function artifactRecord(id: string, kind: ArtifactSummary["kind"], name: string): ArtifactSummary {
1141
+ return {
1142
+ id,
1143
+ kind,
1144
+ name,
1145
+ path: `.agent-loop/artifacts/run-fixture/${kind}/${name}`,
1146
+ createdAt: timestamp
1147
+ };
1148
+ }
1149
+
1150
+ function longSentence(prefix: string, repeats: number): string {
1151
+ return Array.from({ length: repeats }, (_, index) => `${prefix} segment-${index + 1}`).join(" / ");
1152
+ }