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,439 @@
1
+ import { AgentLoopError } from "./errors.js";
2
+ import { resolveLoopShape, sandboxForShapeState } from "./loop-shapes.js";
3
+ import { workerSandbox } from "./worker-prompts.js";
4
+ import type { AgentLoopConfig, AgentLoopProfileSummary, LoopShapeId, RoleProfileId, WorkerType, WorkflowProfileId, WorkflowStageSummary } from "./types.js";
5
+ import type { AgentLoopState } from "./state-types.js";
6
+
7
+ export interface RoleAlias {
8
+ id: string;
9
+ label: string;
10
+ aliasFor: WorkerType;
11
+ description: string;
12
+ systemPrompt: string;
13
+ scope: "read-only" | "workspace-write";
14
+ }
15
+
16
+ export interface RoleProfile {
17
+ id: RoleProfileId;
18
+ label: string;
19
+ description: string;
20
+ aliases: Record<string, RoleAlias>;
21
+ }
22
+
23
+ export interface WorkflowProfile {
24
+ id: WorkflowProfileId;
25
+ label: string;
26
+ description: string;
27
+ loopShape: LoopShapeId;
28
+ shapeConfig: { roleOverrides: Partial<Record<AgentLoopState, string>> };
29
+ configOverrides: Partial<Pick<
30
+ AgentLoopConfig,
31
+ "requireReviewApproval" | "maxCiReruns" | "protectedPaths" | "lintCommand" | "workerTimeoutMs" | "commandTimeoutMs" | "autonomyMode"
32
+ >>;
33
+ validationPosture: string;
34
+ likelyGates: string[];
35
+ handoffTemplate: string;
36
+ autonomyBoundary: string;
37
+ expectedDeliverable?: string;
38
+ allowedWriteRoots?: string[];
39
+ requiredEvidence?: string[];
40
+ reviewChecklist?: string[];
41
+ maxExecutionReviewCycles?: number;
42
+ }
43
+
44
+ export const WORKFLOW_PROFILE_IDS = [
45
+ "default_pr_loop",
46
+ "docs_only_loop",
47
+ "review_fix_loop",
48
+ "release_ready_loop",
49
+ "research_report_loop",
50
+ "document_preparation_loop",
51
+ "repo_hygiene_loop",
52
+ "weekly_review_loop",
53
+ "data_extraction_loop"
54
+ ] as const;
55
+
56
+ export const ROLE_PROFILE_IDS = ["default_pr_roles"] as const;
57
+
58
+ export const DEFAULT_LOOP_SHAPE_ID = "pr-loop";
59
+ export const DEFAULT_WORKFLOW_PROFILE_ID: WorkflowProfileId = "default_pr_loop";
60
+ export const DEFAULT_ROLE_PROFILE_ID: RoleProfileId = "default_pr_roles";
61
+
62
+ const DEFAULT_ROLE_PROFILE: RoleProfile = {
63
+ id: "default_pr_roles",
64
+ label: "Default PR roles",
65
+ description: "Readable role aliases mapped onto the existing PR loop worker types.",
66
+ aliases: {
67
+ planner: {
68
+ id: "planner",
69
+ label: "Planner",
70
+ aliasFor: "planner",
71
+ description: "Plan the next PR scope and produce spec-level handoff.",
72
+ systemPrompt: "Plan narrowly, cite repository evidence, and hand off a scoped implementation target.",
73
+ scope: "workspace-write"
74
+ },
75
+ implementer: {
76
+ id: "implementer",
77
+ label: "Implementer",
78
+ aliasFor: "implementation",
79
+ description: "Implement the selected PR without owning Git or GitHub lifecycle actions.",
80
+ systemPrompt: "Implement only the selected PR scope. Keep changes small, tested, and ready for review.",
81
+ scope: "workspace-write"
82
+ },
83
+ reviewer: {
84
+ id: "reviewer",
85
+ label: "Reviewer",
86
+ aliasFor: "reviewer",
87
+ description: "Perform read-only self-review using repository evidence.",
88
+ systemPrompt: "Review read-only. Prioritize correctness, safety boundaries, and missing tests.",
89
+ scope: "read-only"
90
+ },
91
+ "review-fix": {
92
+ id: "review-fix",
93
+ label: "Review fix",
94
+ aliasFor: "review-fix",
95
+ description: "Fix scoped review findings and carry forward out-of-scope work.",
96
+ systemPrompt: "Address only scoped review findings. Record out-of-scope items as follow-ups.",
97
+ scope: "workspace-write"
98
+ },
99
+ "ci-fix": {
100
+ id: "ci-fix",
101
+ label: "CI fix",
102
+ aliasFor: "ci-fix",
103
+ description: "Fix failing checks without expanding feature scope.",
104
+ systemPrompt: "Focus on test and CI failures. Avoid unrelated refactors.",
105
+ scope: "workspace-write"
106
+ },
107
+ "release-manager": {
108
+ id: "release-manager",
109
+ label: "Release manager",
110
+ aliasFor: "reviewer",
111
+ description: "Display-only release readiness posture; not an executable worker in PR L.",
112
+ systemPrompt: "Summarize release readiness. Do not execute as a worker.",
113
+ scope: "read-only"
114
+ }
115
+ }
116
+ };
117
+
118
+ const WORKFLOW_PROFILES: Record<WorkflowProfileId, WorkflowProfile> = {
119
+ default_pr_loop: {
120
+ id: "default_pr_loop",
121
+ label: "Default PR loop",
122
+ description: "The HOLO-Codex PR delivery behavior with explicit profile audit.",
123
+ loopShape: "pr-loop",
124
+ shapeConfig: { roleOverrides: {} },
125
+ configOverrides: {},
126
+ validationPosture: "Use configured lint, tests, GitNexus, CI, and review gates.",
127
+ likelyGates: ["ambiguous_next_pr", "worker_failed", "ci_required_checks_missing", "merge_requires_confirmation"],
128
+ handoffTemplate: "Follow the selected PR spec and hand off concise evidence to the next role.",
129
+ autonomyBoundary: "Autonomous until configured gates, policy violations, CI/review blockers, or unsafe git actions."
130
+ },
131
+ docs_only_loop: {
132
+ id: "docs_only_loop",
133
+ label: "Docs-only loop",
134
+ description: "Bias validation toward documentation consistency while preserving policy and configured checks.",
135
+ loopShape: "pr-loop",
136
+ shapeConfig: { roleOverrides: {} },
137
+ configOverrides: { maxCiReruns: 0 },
138
+ validationPosture: "Prefer docs consistency checks; if code or config changes, existing tests and policy still decide.",
139
+ likelyGates: ["ambiguous_next_pr", "worker_failed", "policy_violation"],
140
+ handoffTemplate: "Call out docs touched, references updated, and any code/config spillover.",
141
+ autonomyBoundary: "Docs-focused autonomy; policy guards and explicit configured checks remain authoritative."
142
+ },
143
+ review_fix_loop: {
144
+ id: "review_fix_loop",
145
+ label: "Review-fix loop",
146
+ description: "Focus on scoped PR review repair and carryover discipline.",
147
+ loopShape: "pr-loop",
148
+ shapeConfig: { roleOverrides: {} },
149
+ configOverrides: { maxCiReruns: 0 },
150
+ validationPosture: "Prioritize review comments, scoped fixes, and targeted validation.",
151
+ likelyGates: ["review_out_of_scope", "worker_failed", "ci_pending_timeout"],
152
+ handoffTemplate: "Summarize handled findings, unresolved carryover, and validation evidence.",
153
+ autonomyBoundary: "Repair only review-scoped issues; defer unrelated requests through carryover."
154
+ },
155
+ release_ready_loop: {
156
+ id: "release_ready_loop",
157
+ label: "Release-ready loop",
158
+ description: "Tighten merge readiness explanation without adding a release-manager worker.",
159
+ loopShape: "pr-loop",
160
+ shapeConfig: { roleOverrides: {} },
161
+ configOverrides: { autonomyMode: "supervised" },
162
+ validationPosture: "Favor readiness evidence, review status, CI status, and explicit merge confirmation.",
163
+ likelyGates: ["merge_requires_confirmation", "ci_required_checks_missing", "github_transient_failure"],
164
+ handoffTemplate: "List readiness evidence, missing conditions, and any merge risk.",
165
+ autonomyBoundary: "Supervised release posture; merge-related actions require visible confirmation."
166
+ },
167
+ research_report_loop: {
168
+ id: "research_report_loop",
169
+ label: "Research report loop",
170
+ description: "Collect evidence, draft a report, review it, and request human approval before delivery.",
171
+ loopShape: "generic-loop",
172
+ shapeConfig: { roleOverrides: {} },
173
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
174
+ validationPosture: "Require cited evidence, a coherent report artifact, and human approval before delivery.",
175
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
176
+ handoffTemplate: "Summarize research question, sources checked, claims supported, gaps, and deliverable path.",
177
+ autonomyBoundary: "Autonomous for research and drafting inside allowed write roots; final delivery waits for human approval.",
178
+ expectedDeliverable: "Markdown research report",
179
+ allowedWriteRoots: ["docs", "reports"],
180
+ requiredEvidence: ["source list", "claim-to-evidence summary", "known gaps"],
181
+ reviewChecklist: ["Claims cite evidence", "Uncertainty is explicit", "No raw secrets or prompt content included"],
182
+ maxExecutionReviewCycles: 3
183
+ },
184
+ document_preparation_loop: {
185
+ id: "document_preparation_loop",
186
+ label: "Document preparation loop",
187
+ description: "Prepare a structured document from provided context and deliver it after review.",
188
+ loopShape: "generic-loop",
189
+ shapeConfig: { roleOverrides: {} },
190
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
191
+ validationPosture: "Validate structure, completeness, and requested format before human approval.",
192
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
193
+ handoffTemplate: "List document purpose, audience, sections prepared, missing input, and deliverable path.",
194
+ autonomyBoundary: "May write only document artifacts under allowed roots; final delivery waits for human approval.",
195
+ expectedDeliverable: "Prepared Markdown document",
196
+ allowedWriteRoots: ["docs", "reports"],
197
+ requiredEvidence: ["source context summary", "document outline", "completion checklist"],
198
+ reviewChecklist: ["Audience and format match the goal", "Sections are complete", "No unsupported claims"],
199
+ maxExecutionReviewCycles: 3
200
+ },
201
+ repo_hygiene_loop: {
202
+ id: "repo_hygiene_loop",
203
+ label: "Repo hygiene loop",
204
+ description: "Audit repository hygiene and produce a scoped report or safe cleanup artifact.",
205
+ loopShape: "generic-loop",
206
+ shapeConfig: { roleOverrides: {} },
207
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
208
+ validationPosture: "Prefer read-only audit; write only report artifacts unless the goal explicitly asks for safe cleanup.",
209
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "policy_violation"],
210
+ handoffTemplate: "List inspected areas, hygiene findings, safe fixes, deferred risks, and deliverable path.",
211
+ autonomyBoundary: "Repository inspection is read-only by default; write actions are limited to allowed report roots.",
212
+ expectedDeliverable: "Repo hygiene audit report",
213
+ allowedWriteRoots: ["docs", "reports"],
214
+ requiredEvidence: ["checked files/commands", "finding severity", "recommended action"],
215
+ reviewChecklist: ["No destructive commands", "Findings have repo evidence", "Out-of-scope cleanup is deferred"],
216
+ maxExecutionReviewCycles: 2
217
+ },
218
+ weekly_review_loop: {
219
+ id: "weekly_review_loop",
220
+ label: "Weekly review loop",
221
+ description: "Collect activity signals, summarize progress, and deliver a weekly review artifact.",
222
+ loopShape: "generic-loop",
223
+ shapeConfig: { roleOverrides: {} },
224
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
225
+ validationPosture: "Require summarized evidence and a concise deliverable suitable for human review.",
226
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
227
+ handoffTemplate: "List timeframe, activity sources, decisions, blockers, follow-ups, and deliverable path.",
228
+ autonomyBoundary: "May summarize local/repo facts and write review artifacts; final delivery waits for approval.",
229
+ expectedDeliverable: "Weekly review Markdown summary",
230
+ allowedWriteRoots: ["docs", "reports"],
231
+ requiredEvidence: ["timeframe", "activity source summary", "follow-up list"],
232
+ reviewChecklist: ["Timeframe is explicit", "Actions are separated from FYI", "No private raw logs included"],
233
+ maxExecutionReviewCycles: 2
234
+ },
235
+ data_extraction_loop: {
236
+ id: "data_extraction_loop",
237
+ label: "Data extraction loop",
238
+ description: "Extract structured data into an auditable artifact, then wait for human approval.",
239
+ loopShape: "generic-loop",
240
+ shapeConfig: { roleOverrides: {} },
241
+ configOverrides: { autonomyMode: "autonomous_until_gate" },
242
+ validationPosture: "Require extraction criteria, sample validation, and artifact metadata before delivery.",
243
+ likelyGates: ["generic_goal_needs_confirmation", "generic_human_gate", "generic_scope_change_requested", "worker_failed"],
244
+ handoffTemplate: "List extraction target, criteria, row/item count, validation sample, and deliverable path.",
245
+ autonomyBoundary: "May write extracted artifacts only under allowed roots; no external side effects.",
246
+ expectedDeliverable: "Structured extraction artifact",
247
+ allowedWriteRoots: ["docs", "reports", "data"],
248
+ requiredEvidence: ["extraction criteria", "sample validation", "count summary"],
249
+ reviewChecklist: ["Schema is documented", "Sample rows match source", "Secrets are redacted"],
250
+ maxExecutionReviewCycles: 3
251
+ }
252
+ };
253
+
254
+ /** Resolve and validate the configured workflow and role profile. */
255
+ export function resolveProfile(config: AgentLoopConfig, currentState?: AgentLoopState): AgentLoopProfileSummary {
256
+ const shape = resolveLoopShape(config.loopShape);
257
+ const workflow = workflowProfile(config.workflowProfile);
258
+ const roleProfile = roleProfileById(config.roleProfile);
259
+ if (workflow.loopShape !== shape.id) {
260
+ throw new AgentLoopError("invalid_config", "Workflow profile loopShape does not match config loopShape.");
261
+ }
262
+ validateRoleProfile(roleProfile);
263
+ validateWorkflowProfile(workflow, roleProfile);
264
+ const roleMapping = shape.states
265
+ .map((state) => roleMappingForState(state, workflow, roleProfile))
266
+ .filter((item): item is NonNullable<typeof item> => item !== undefined);
267
+ const currentRole = currentState ? roleMappingForState(currentState, workflow, roleProfile) : undefined;
268
+ return {
269
+ loopShape: shape.id,
270
+ workflowProfile: workflow.id,
271
+ workflowLabel: workflow.label,
272
+ workflowDescription: workflow.description,
273
+ roleProfile: roleProfile.id,
274
+ lifecycleKind: shape.lifecycleKind,
275
+ ...(workflow.expectedDeliverable ? { expectedDeliverable: workflow.expectedDeliverable } : {}),
276
+ ...(workflow.allowedWriteRoots ? { allowedWriteRoots: workflow.allowedWriteRoots } : {}),
277
+ ...(currentRole ? { currentRole } : {}),
278
+ roleMapping,
279
+ autonomyBoundary: workflow.autonomyBoundary,
280
+ handoffSummary: workflow.handoffTemplate,
281
+ validationPosture: workflow.validationPosture,
282
+ likelyGates: workflow.likelyGates,
283
+ availableWorkflows: Object.values(WORKFLOW_PROFILES).map((item) => ({
284
+ id: item.id,
285
+ label: item.label,
286
+ description: item.description
287
+ })),
288
+ availableRoleProfiles: [{
289
+ id: roleProfile.id,
290
+ label: roleProfile.label,
291
+ description: roleProfile.description
292
+ }]
293
+ };
294
+ }
295
+
296
+ /** Return an effective config with profile presets applied conservatively. */
297
+ export function applyProfileConfig(config: AgentLoopConfig): AgentLoopConfig {
298
+ const workflow = workflowProfile(config.workflowProfile);
299
+ validateWorkflowProfile(workflow, roleProfileById(config.roleProfile));
300
+ const override = workflow.configOverrides;
301
+ return {
302
+ ...config,
303
+ ...(override.lintCommand && !config.lintCommand ? { lintCommand: override.lintCommand } : {}),
304
+ requireReviewApproval: config.requireReviewApproval || override.requireReviewApproval === true,
305
+ maxCiReruns: minNumber(config.maxCiReruns, override.maxCiReruns),
306
+ workerTimeoutMs: minNumber(config.workerTimeoutMs, override.workerTimeoutMs),
307
+ commandTimeoutMs: minNumber(config.commandTimeoutMs, override.commandTimeoutMs),
308
+ autonomyMode: tighterAutonomy(config.autonomyMode, override.autonomyMode),
309
+ protectedPaths: [...new Set([...config.protectedPaths, ...(override.protectedPaths ?? [])])]
310
+ };
311
+ }
312
+
313
+ /** Return dry-run stage summaries from the registered PR loop shape. */
314
+ export function workflowStages(config: AgentLoopConfig): WorkflowStageSummary[] {
315
+ const workflow = workflowProfile(config.workflowProfile);
316
+ const roleProfile = roleProfileById(config.roleProfile);
317
+ const shape = resolveLoopShape(workflow.loopShape);
318
+ return shape.states
319
+ .filter((state) => !shape.terminalStates.includes(state))
320
+ .map((state) => {
321
+ const role = roleMappingForState(state, workflow, roleProfile);
322
+ return {
323
+ state,
324
+ ...(role ? { roleAlias: role.alias, workerType: role.workerType } : {}),
325
+ ...(role ? { sandbox: role.sandbox } : {}),
326
+ gateExpected: workflow.likelyGates.length > 0 && gateExpectedForState(workflow.loopShape, state),
327
+ ...(workflow.expectedDeliverable && ["HUMAN_GATE", "DELIVER"].includes(state) ? { deliverable: workflow.expectedDeliverable } : {})
328
+ };
329
+ });
330
+ }
331
+
332
+ export function workflowProfileIds(): WorkflowProfileId[] {
333
+ return [...WORKFLOW_PROFILE_IDS];
334
+ }
335
+
336
+ export function roleProfileIds(): RoleProfileId[] {
337
+ return [...ROLE_PROFILE_IDS];
338
+ }
339
+
340
+ function workflowProfile(id: WorkflowProfileId): WorkflowProfile {
341
+ const profile = WORKFLOW_PROFILES[id];
342
+ if (!profile) {
343
+ throw new AgentLoopError("invalid_config", "Config workflowProfile is invalid.");
344
+ }
345
+ return profile;
346
+ }
347
+
348
+ function roleProfileById(id: RoleProfileId): RoleProfile {
349
+ if (id !== DEFAULT_ROLE_PROFILE.id) {
350
+ throw new AgentLoopError("invalid_config", "Config roleProfile is invalid.");
351
+ }
352
+ return DEFAULT_ROLE_PROFILE;
353
+ }
354
+
355
+ function validateRoleProfile(profile: RoleProfile): void {
356
+ for (const alias of Object.values(profile.aliases)) {
357
+ if (alias.id === "release-manager") {
358
+ continue;
359
+ }
360
+ if (alias.scope !== workerSandbox(alias.aliasFor)) {
361
+ throw new AgentLoopError("invalid_config", "Role profile scope cannot change worker sandbox.", {
362
+ details: { role: alias.id, aliasFor: alias.aliasFor, scope: alias.scope, sandbox: workerSandbox(alias.aliasFor) }
363
+ });
364
+ }
365
+ }
366
+ }
367
+
368
+ function validateWorkflowProfile(workflow: WorkflowProfile, profile: RoleProfile): void {
369
+ const shape = resolveLoopShape(workflow.loopShape);
370
+ for (const [state, roleAlias] of Object.entries(workflow.shapeConfig.roleOverrides)) {
371
+ if (!shape.states.includes(state as AgentLoopState)) {
372
+ throw new AgentLoopError("invalid_config", "Workflow profile references an unknown state.", { details: { state } });
373
+ }
374
+ if (roleAlias === "release-manager") {
375
+ throw new AgentLoopError("invalid_config", "release-manager is display-only and cannot be used as an executable role.");
376
+ }
377
+ const role = profile.aliases[roleAlias ?? ""];
378
+ const defaultRole = shape.defaultRoleForState(state as AgentLoopState);
379
+ if (!role || role.aliasFor !== defaultRole) {
380
+ throw new AgentLoopError("invalid_config", "Workflow role override cannot change the state's worker sandbox.", {
381
+ details: { state, roleAlias, aliasFor: role?.aliasFor, defaultRole }
382
+ });
383
+ }
384
+ }
385
+ }
386
+
387
+ function roleMappingForState(state: AgentLoopState, workflow: WorkflowProfile, profile: RoleProfile): AgentLoopProfileSummary["roleMapping"][number] | undefined {
388
+ const shape = resolveLoopShape(workflow.loopShape);
389
+ const workerType = shape.defaultRoleForState(state);
390
+ if (!workerType) return undefined;
391
+ const aliasId = workflow.shapeConfig.roleOverrides[state] ?? defaultAliasFor(workerType);
392
+ const role = profile.aliases[aliasId];
393
+ if (!role || role.aliasFor !== workerType) {
394
+ throw new AgentLoopError("invalid_config", "Could not resolve role mapping for workflow profile.", { details: { state, aliasId, workerType } });
395
+ }
396
+ return {
397
+ state,
398
+ alias: role.id,
399
+ workerType,
400
+ label: role.label,
401
+ sandbox: sandboxForShapeState(workflow.loopShape, state, workerType)
402
+ };
403
+ }
404
+
405
+ export function workflowProfileDefinition(id: WorkflowProfileId): WorkflowProfile {
406
+ return workflowProfile(id);
407
+ }
408
+
409
+ function defaultAliasFor(workerType: WorkerType): string {
410
+ const aliases: Record<WorkerType, string> = {
411
+ planner: "planner",
412
+ implementation: "implementer",
413
+ reviewer: "reviewer",
414
+ "review-fix": "review-fix",
415
+ "ci-fix": "ci-fix"
416
+ };
417
+ return aliases[workerType];
418
+ }
419
+
420
+ function minNumber(current: number, override: number | undefined): number {
421
+ return override === undefined ? current : Math.min(current, override);
422
+ }
423
+
424
+ function tighterAutonomy(current: AgentLoopConfig["autonomyMode"], override: AgentLoopConfig["autonomyMode"] | undefined): AgentLoopConfig["autonomyMode"] {
425
+ if (!override) return current;
426
+ const rank: Record<AgentLoopConfig["autonomyMode"], number> = {
427
+ supervised: 0,
428
+ autonomous_until_gate: 1,
429
+ autonomous_until_terminal: 2
430
+ };
431
+ return rank[override] < rank[current] ? override : current;
432
+ }
433
+
434
+ function gateExpectedForState(loopShape: LoopShapeId, state: AgentLoopState): boolean {
435
+ if (loopShape === "pr-loop") {
436
+ return ["SELECT_NEXT_PR", "WAIT_REVIEW_OR_CI", "READY_TO_MERGE"].includes(state);
437
+ }
438
+ return ["DEFINE_GOAL", "EXECUTE_STEP", "HUMAN_GATE"].includes(state);
439
+ }
@@ -0,0 +1,17 @@
1
+ /** Redact common secret shapes before persisting user-visible summaries. */
2
+ export function redactSecrets(value: string): string {
3
+ return value
4
+ .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
5
+ .replace(/\b[A-Za-z0-9._%+-]+:[^@\s]+@/g, "[redacted]@")
6
+ .replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]")
7
+ .replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[redacted]")
8
+ .replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted]")
9
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]")
10
+ .replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)(["'])(?:(?!\2).)*\2/gi, "$1$2[redacted]$2")
11
+ .replace(/((?:token|api_key|authorization|password|secret)\s*[:=]\s*)[^\n\r,;}]+/gi, "$1[redacted]");
12
+ }
13
+
14
+ /** Return true when an object key names a likely secret field. */
15
+ export function isSecretKey(key: string): boolean {
16
+ return /token|api_key|authorization|password|secret/i.test(key);
17
+ }
@@ -0,0 +1,22 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { AgentLoopError } from "./errors.js";
5
+
6
+ /** Resolve a path inside a git repository to its canonical repository root. */
7
+ export function resolveRepoRoot(path: string): string {
8
+ const targetPath = resolve(path);
9
+ try {
10
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
11
+ cwd: targetPath,
12
+ encoding: "utf8",
13
+ stdio: ["ignore", "pipe", "pipe"]
14
+ }).trim();
15
+ return realpathSync(repoRoot);
16
+ } catch {
17
+ throw new AgentLoopError("not_git_repo", "Target path is not inside a git repository.", {
18
+ details: { targetPath },
19
+ exitCode: 2
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,77 @@
1
+ import type { AgentLoopReviewComment } from "./types.js";
2
+ import { isRecord } from "./config.js";
3
+
4
+ export type ReviewCommentInput = Omit<
5
+ AgentLoopReviewComment,
6
+ "id" | "runId" | "prNumber" | "observedAt"
7
+ >;
8
+
9
+ /** Parse GraphQL reviewThreads into normalized actionable review comments. */
10
+ export function parseReviewThreads(payload: unknown): ReviewCommentInput[] {
11
+ const threads = findNodes(payload, "reviewThreads");
12
+ return threads.flatMap(parseThread);
13
+ }
14
+
15
+ /** Return only comments that PR C should route to review-fix handling. */
16
+ export function actionableReviewComments(comments: ReviewCommentInput[]): ReviewCommentInput[] {
17
+ return comments.filter((comment) => comment.actionable && comment.status === "open");
18
+ }
19
+
20
+ function parseThread(thread: unknown): ReviewCommentInput[] {
21
+ if (!isRecord(thread)) {
22
+ return [];
23
+ }
24
+ const isResolved = Boolean(thread.isResolved);
25
+ const isOutdated = Boolean(thread.isOutdated);
26
+ return findNodes(thread, "comments").map((comment) => normalizeComment(comment, isResolved, isOutdated));
27
+ }
28
+
29
+ function normalizeComment(comment: unknown, isResolved: boolean, isOutdated: boolean): ReviewCommentInput {
30
+ const row = isRecord(comment) ? comment : {};
31
+ const actionable = !isResolved && !isOutdated && stringValue(row.body).trim().length > 0;
32
+ const line = numberValue(row.line);
33
+ const normalized: ReviewCommentInput = {
34
+ commentId: stringValue(row.id),
35
+ url: stringValue(row.url),
36
+ author: authorLogin(row.author),
37
+ body: stringValue(row.body),
38
+ path: stringValue(row.path),
39
+ diffHunk: stringValue(row.diffHunk),
40
+ isResolved,
41
+ isOutdated,
42
+ actionable,
43
+ status: actionable ? "open" : isOutdated ? "stale" : "handled"
44
+ };
45
+ if (line !== undefined) {
46
+ normalized.line = line;
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ function findNodes(value: unknown, key: string): unknown[] {
52
+ if (!isRecord(value)) {
53
+ return [];
54
+ }
55
+ const direct = value[key];
56
+ if (isRecord(direct) && Array.isArray(direct.nodes)) {
57
+ return direct.nodes;
58
+ }
59
+ const repository = value.repository;
60
+ const pullRequest = isRecord(repository) ? repository.pullRequest : undefined;
61
+ if (isRecord(pullRequest)) {
62
+ return findNodes(pullRequest, key);
63
+ }
64
+ return [];
65
+ }
66
+
67
+ function authorLogin(value: unknown): string {
68
+ return isRecord(value) ? stringValue(value.login) : "";
69
+ }
70
+
71
+ function numberValue(value: unknown): number | undefined {
72
+ return typeof value === "number" ? value : undefined;
73
+ }
74
+
75
+ function stringValue(value: unknown): string {
76
+ return typeof value === "string" ? value : "";
77
+ }