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.
- package/.agents/plugins/marketplace.json +20 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/README.zh-CN.md +215 -0
- package/SECURITY.md +39 -0
- package/assets/brand/README.md +35 -0
- package/assets/brand/holo-codex-icon.svg +28 -0
- package/assets/brand/holo-codex-lockup.svg +49 -0
- package/assets/brand/holo-codex-mark.svg +33 -0
- package/assets/brand/holo-codex-plugin-card.png +0 -0
- package/assets/brand/holo-codex-plugin-card.svg +81 -0
- package/assets/brand/holo-codex-readme-hero.png +0 -0
- package/assets/brand/holo-codex-readme-hero.svg +140 -0
- package/assets/brand/holo-codex-social-preview.png +0 -0
- package/assets/brand/holo-codex-social-preview.svg +130 -0
- package/assets/brand/holo-codex-wordmark-options.svg +52 -0
- package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
- package/docs/examples/generic-loop-repo-hygiene.md +168 -0
- package/docs/install.md +190 -0
- package/docs/local-release-readiness.md +206 -0
- package/docs/release-checklist.md +144 -0
- package/docs/self-bootstrap.md +150 -0
- package/docs/trust-and-safety.md +45 -0
- package/package.json +83 -0
- package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
- package/plugins/autonomous-pr-loop/.mcp.json +13 -0
- package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
- package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
- package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
- package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
- package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
- package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
- package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
- package/plugins/autonomous-pr-loop/core/command.ts +47 -0
- package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
- package/plugins/autonomous-pr-loop/core/config.ts +293 -0
- package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
- package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
- package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
- package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
- package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
- package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
- package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
- package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
- package/plugins/autonomous-pr-loop/core/git.ts +213 -0
- package/plugins/autonomous-pr-loop/core/github.ts +269 -0
- package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
- package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
- package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
- package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
- package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
- package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
- package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
- package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
- package/plugins/autonomous-pr-loop/core/index.ts +32 -0
- package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
- package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
- package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
- package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
- package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
- package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
- package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
- package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
- package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
- package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
- package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
- package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
- package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
- package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
- package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
- package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
- package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
- package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
- package/plugins/autonomous-pr-loop/core/types.ts +567 -0
- package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
- package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
- package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
- package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
- package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
- package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
- package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
- package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
- package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
- package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
- package/plugins/autonomous-pr-loop/package.json +9 -0
- package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
- package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
- package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
- package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
- package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
- package/plugins/autonomous-pr-loop/ui/index.html +26 -0
- package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
- package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
- package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
- package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
- package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
- package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
- package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
- package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
- package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
- package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
- package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
- package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
- package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
- package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AgentLoopError } from "./errors.js";
|
|
4
|
+
import { DEFAULT_LOCALE, LOCALE_SETTINGS } from "./locale.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LOOP_SHAPE_ID,
|
|
7
|
+
DEFAULT_ROLE_PROFILE_ID,
|
|
8
|
+
DEFAULT_WORKFLOW_PROFILE_ID,
|
|
9
|
+
ROLE_PROFILE_IDS,
|
|
10
|
+
WORKFLOW_PROFILE_IDS,
|
|
11
|
+
resolveProfile
|
|
12
|
+
} from "./profiles.js";
|
|
13
|
+
import { loopShapeIds } from "./loop-shapes.js";
|
|
14
|
+
import type { AgentLoopConfig, LoadedConfig } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export const CONFIG_DIR = ".agent-loop";
|
|
17
|
+
export const CONFIG_FILE = "config.json";
|
|
18
|
+
/** Default protected path globs used when a repository config omits policy paths. */
|
|
19
|
+
export const DEFAULT_PROTECTED_PATHS = [
|
|
20
|
+
".git/**",
|
|
21
|
+
".agent-loop/**",
|
|
22
|
+
".claude/**",
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"CLAUDE.md",
|
|
25
|
+
".env*",
|
|
26
|
+
"**/*secret*"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const AUTONOMY_MODES = ["supervised", "autonomous_until_gate", "autonomous_until_terminal"] as const;
|
|
30
|
+
export const MERGE_MODES = ["manual", "conditional", "disabled"] as const;
|
|
31
|
+
export const NOTIFY_MODES = ["all_gates", "important_only", "blockers_only"] as const;
|
|
32
|
+
export const WORKER_BACKENDS = ["codex-exec", "codex-app-server"] as const;
|
|
33
|
+
export const REVIEW_HANDLING_MODES = [
|
|
34
|
+
"fix_scoped_and_carry_forward",
|
|
35
|
+
"ask_on_any_review",
|
|
36
|
+
"require_zero_open_findings"
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/** Return the canonical agent-loop config path for a repository root. */
|
|
40
|
+
export function configPath(repoRoot: string): string {
|
|
41
|
+
return join(repoRoot, CONFIG_DIR, CONFIG_FILE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Return the canonical SQLite state path for a repository root. */
|
|
45
|
+
export function statePath(repoRoot: string): string {
|
|
46
|
+
return join(repoRoot, CONFIG_DIR, "state.sqlite");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Merge a partial repository config with PR A defaults. */
|
|
50
|
+
export function withConfigDefaults(
|
|
51
|
+
input: Partial<AgentLoopConfig> & { repoId: string }
|
|
52
|
+
): AgentLoopConfig {
|
|
53
|
+
const mergeMode = input.mergeMode ?? (input.allowAutoMerge ? "conditional" : "manual");
|
|
54
|
+
return {
|
|
55
|
+
repoId: input.repoId,
|
|
56
|
+
locale: input.locale ?? DEFAULT_LOCALE,
|
|
57
|
+
loopShape: input.loopShape ?? DEFAULT_LOOP_SHAPE_ID,
|
|
58
|
+
workflowProfile: input.workflowProfile ?? DEFAULT_WORKFLOW_PROFILE_ID,
|
|
59
|
+
roleProfile: input.roleProfile ?? DEFAULT_ROLE_PROFILE_ID,
|
|
60
|
+
baseBranch: input.baseBranch ?? "main",
|
|
61
|
+
branchPrefix: input.branchPrefix ?? "codex/",
|
|
62
|
+
plansDir: input.plansDir ?? "docs/plans",
|
|
63
|
+
...(input.lintCommand ? { lintCommand: input.lintCommand } : {}),
|
|
64
|
+
...(input.testCommand ? { testCommand: input.testCommand } : {}),
|
|
65
|
+
...(input.gitnexusRepo ? { gitnexusRepo: input.gitnexusRepo } : {}),
|
|
66
|
+
gitnexusRequired: input.gitnexusRequired ?? true,
|
|
67
|
+
requiredChecks: input.requiredChecks ?? [],
|
|
68
|
+
requireReviewApproval: input.requireReviewApproval ?? true,
|
|
69
|
+
autonomyMode: input.autonomyMode ?? "autonomous_until_gate",
|
|
70
|
+
mergeMode,
|
|
71
|
+
notifyMode: input.notifyMode ?? "important_only",
|
|
72
|
+
reviewHandling: input.reviewHandling ?? "fix_scoped_and_carry_forward",
|
|
73
|
+
...(input.carryoverTarget ? { carryoverTarget: input.carryoverTarget } : {}),
|
|
74
|
+
allowAutoMerge: mergeMode === "conditional",
|
|
75
|
+
maxReviewFixRounds: input.maxReviewFixRounds ?? 3,
|
|
76
|
+
maxTestFixRounds: input.maxTestFixRounds ?? 2,
|
|
77
|
+
maxCiReruns: input.maxCiReruns ?? 1,
|
|
78
|
+
commandTimeoutMs: input.commandTimeoutMs ?? 600_000,
|
|
79
|
+
commandOutputLimitBytes: input.commandOutputLimitBytes ?? 65_536,
|
|
80
|
+
githubRetryMaxAttempts: input.githubRetryMaxAttempts ?? 3,
|
|
81
|
+
githubRetryBaseDelayMs: input.githubRetryBaseDelayMs ?? 1_000,
|
|
82
|
+
reviewCiPollIntervalMs: input.reviewCiPollIntervalMs ?? 30_000,
|
|
83
|
+
reviewCiMaxWaitMs: input.reviewCiMaxWaitMs ?? 1_800_000,
|
|
84
|
+
workerBackend: input.workerBackend ?? "codex-exec",
|
|
85
|
+
workerTimeoutMs: input.workerTimeoutMs ?? 1_800_000,
|
|
86
|
+
workerMaxRetries: input.workerMaxRetries ?? 1,
|
|
87
|
+
workerEphemeral: input.workerEphemeral ?? false,
|
|
88
|
+
protectedPaths: input.protectedPaths ?? DEFAULT_PROTECTED_PATHS,
|
|
89
|
+
...(input.dashboard ? { dashboard: input.dashboard } : {})
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Load and validate `.agent-loop/config.json`, or throw a structured gate error. */
|
|
94
|
+
export function loadConfig(repoRoot: string): LoadedConfig {
|
|
95
|
+
const path = configPath(repoRoot);
|
|
96
|
+
if (!existsSync(path)) {
|
|
97
|
+
throw new AgentLoopError(
|
|
98
|
+
"needs_repo_init",
|
|
99
|
+
"Missing .agent-loop/config.json. Run `pnpm agent-loop init`.",
|
|
100
|
+
{ details: { path }, exitCode: 2 }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let parsed: unknown;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new AgentLoopError("invalid_config", "Config is not valid JSON.", {
|
|
109
|
+
details: { path, cause: error instanceof Error ? error.message : String(error) }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const config = validateConfig(parsed);
|
|
114
|
+
return { path, config };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Validate user config and return a default-filled normalized config. */
|
|
118
|
+
export function validateConfig(value: unknown): AgentLoopConfig {
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
throw new AgentLoopError("invalid_config", "Config must be a JSON object.");
|
|
121
|
+
}
|
|
122
|
+
assertKnownTopLevelKeys(value);
|
|
123
|
+
if (typeof value.repoId !== "string" || value.repoId.length === 0) {
|
|
124
|
+
throw new AgentLoopError("invalid_config", "Config repoId is required.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const config = withConfigDefaults(value as Partial<AgentLoopConfig> & { repoId: string });
|
|
128
|
+
const stringFields = ["baseBranch", "branchPrefix", "plansDir"] as const;
|
|
129
|
+
for (const field of stringFields) {
|
|
130
|
+
if (typeof config[field] !== "string" || config[field].length === 0) {
|
|
131
|
+
throw new AgentLoopError("invalid_config", `Config ${field} must be a non-empty string.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const optionalStrings = ["lintCommand", "testCommand", "gitnexusRepo"] as const;
|
|
136
|
+
for (const field of optionalStrings) {
|
|
137
|
+
if (config[field] !== undefined && typeof config[field] !== "string") {
|
|
138
|
+
throw new AgentLoopError("invalid_config", `Config ${field} must be a string.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!WORKER_BACKENDS.includes(config.workerBackend)) {
|
|
143
|
+
throw new AgentLoopError("invalid_config", "Config workerBackend is invalid.");
|
|
144
|
+
}
|
|
145
|
+
if (!AUTONOMY_MODES.includes(config.autonomyMode)) {
|
|
146
|
+
throw new AgentLoopError("invalid_config", "Config autonomyMode is invalid.");
|
|
147
|
+
}
|
|
148
|
+
if (!MERGE_MODES.includes(config.mergeMode)) {
|
|
149
|
+
throw new AgentLoopError("invalid_config", "Config mergeMode is invalid.");
|
|
150
|
+
}
|
|
151
|
+
if (!NOTIFY_MODES.includes(config.notifyMode)) {
|
|
152
|
+
throw new AgentLoopError("invalid_config", "Config notifyMode is invalid.");
|
|
153
|
+
}
|
|
154
|
+
if (!REVIEW_HANDLING_MODES.includes(config.reviewHandling)) {
|
|
155
|
+
throw new AgentLoopError("invalid_config", "Config reviewHandling is invalid.");
|
|
156
|
+
}
|
|
157
|
+
if (!LOCALE_SETTINGS.includes(config.locale)) {
|
|
158
|
+
throw new AgentLoopError("invalid_config", "Config locale is invalid.");
|
|
159
|
+
}
|
|
160
|
+
if (!loopShapeIds().includes(config.loopShape)) {
|
|
161
|
+
throw new AgentLoopError("invalid_config", "Config loopShape is invalid.");
|
|
162
|
+
}
|
|
163
|
+
if (!WORKFLOW_PROFILE_IDS.includes(config.workflowProfile)) {
|
|
164
|
+
throw new AgentLoopError("invalid_config", "Config workflowProfile is invalid.");
|
|
165
|
+
}
|
|
166
|
+
if (!ROLE_PROFILE_IDS.includes(config.roleProfile)) {
|
|
167
|
+
throw new AgentLoopError("invalid_config", "Config roleProfile is invalid.");
|
|
168
|
+
}
|
|
169
|
+
resolveProfile(config);
|
|
170
|
+
if (config.carryoverTarget !== undefined && typeof config.carryoverTarget !== "string") {
|
|
171
|
+
throw new AgentLoopError("invalid_config", "Config carryoverTarget must be a string.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const booleans = ["gitnexusRequired", "requireReviewApproval", "allowAutoMerge", "workerEphemeral"] as const;
|
|
175
|
+
for (const field of booleans) {
|
|
176
|
+
if (typeof config[field] !== "boolean") {
|
|
177
|
+
throw new AgentLoopError("invalid_config", `Config ${field} must be a boolean.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const numbers = ["maxReviewFixRounds", "maxTestFixRounds", "maxCiReruns", "workerMaxRetries"] as const;
|
|
182
|
+
for (const field of numbers) {
|
|
183
|
+
if (!Number.isInteger(config[field]) || config[field] < 0) {
|
|
184
|
+
throw new AgentLoopError("invalid_config", `Config ${field} must be a non-negative integer.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const positiveNumbers = [
|
|
189
|
+
"commandTimeoutMs",
|
|
190
|
+
"commandOutputLimitBytes",
|
|
191
|
+
"githubRetryMaxAttempts",
|
|
192
|
+
"githubRetryBaseDelayMs",
|
|
193
|
+
"reviewCiPollIntervalMs",
|
|
194
|
+
"reviewCiMaxWaitMs",
|
|
195
|
+
"workerTimeoutMs"
|
|
196
|
+
] as const;
|
|
197
|
+
for (const field of positiveNumbers) {
|
|
198
|
+
if (!Number.isInteger(config[field]) || config[field] < 1) {
|
|
199
|
+
throw new AgentLoopError("invalid_config", `Config ${field} must be a positive integer.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!Array.isArray(config.requiredChecks) || !config.requiredChecks.every(isString)) {
|
|
204
|
+
throw new AgentLoopError("invalid_config", "Config requiredChecks must be a string array.");
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(config.protectedPaths) || !config.protectedPaths.every(isString)) {
|
|
207
|
+
throw new AgentLoopError("invalid_config", "Config protectedPaths must be a string array.");
|
|
208
|
+
}
|
|
209
|
+
if (config.dashboard) {
|
|
210
|
+
assertKnownDashboardKeys(config.dashboard);
|
|
211
|
+
if (
|
|
212
|
+
typeof config.dashboard.enabled !== "boolean" ||
|
|
213
|
+
typeof config.dashboard.host !== "string" ||
|
|
214
|
+
config.dashboard.host.length === 0
|
|
215
|
+
) {
|
|
216
|
+
throw new AgentLoopError("invalid_config", "Config dashboard is invalid.");
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
config.dashboard.port !== undefined &&
|
|
220
|
+
(!Number.isInteger(config.dashboard.port) ||
|
|
221
|
+
config.dashboard.port < 1 ||
|
|
222
|
+
config.dashboard.port > 65_535)
|
|
223
|
+
) {
|
|
224
|
+
throw new AgentLoopError("invalid_config", "Config dashboard.port is invalid.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return config;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function assertKnownTopLevelKeys(value: Record<string, unknown>): void {
|
|
232
|
+
const allowed = new Set([
|
|
233
|
+
"repoId",
|
|
234
|
+
"locale",
|
|
235
|
+
"loopShape",
|
|
236
|
+
"workflowProfile",
|
|
237
|
+
"roleProfile",
|
|
238
|
+
"baseBranch",
|
|
239
|
+
"branchPrefix",
|
|
240
|
+
"plansDir",
|
|
241
|
+
"lintCommand",
|
|
242
|
+
"testCommand",
|
|
243
|
+
"gitnexusRepo",
|
|
244
|
+
"gitnexusRequired",
|
|
245
|
+
"requiredChecks",
|
|
246
|
+
"requireReviewApproval",
|
|
247
|
+
"autonomyMode",
|
|
248
|
+
"mergeMode",
|
|
249
|
+
"notifyMode",
|
|
250
|
+
"reviewHandling",
|
|
251
|
+
"carryoverTarget",
|
|
252
|
+
"allowAutoMerge",
|
|
253
|
+
"maxReviewFixRounds",
|
|
254
|
+
"maxTestFixRounds",
|
|
255
|
+
"maxCiReruns",
|
|
256
|
+
"commandTimeoutMs",
|
|
257
|
+
"commandOutputLimitBytes",
|
|
258
|
+
"githubRetryMaxAttempts",
|
|
259
|
+
"githubRetryBaseDelayMs",
|
|
260
|
+
"reviewCiPollIntervalMs",
|
|
261
|
+
"reviewCiMaxWaitMs",
|
|
262
|
+
"workerBackend",
|
|
263
|
+
"workerTimeoutMs",
|
|
264
|
+
"workerMaxRetries",
|
|
265
|
+
"workerEphemeral",
|
|
266
|
+
"protectedPaths",
|
|
267
|
+
"dashboard"
|
|
268
|
+
]);
|
|
269
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
270
|
+
if (unknown.length > 0) {
|
|
271
|
+
throw new AgentLoopError("invalid_config", "Config contains unknown fields.", {
|
|
272
|
+
details: { fields: unknown }
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function assertKnownDashboardKeys(value: Record<string, unknown>): void {
|
|
278
|
+
const allowed = new Set(["enabled", "host", "port"]);
|
|
279
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
280
|
+
if (unknown.length > 0) {
|
|
281
|
+
throw new AgentLoopError("invalid_config", "Config dashboard contains unknown fields.", {
|
|
282
|
+
details: { fields: unknown }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
288
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isString(value: unknown): value is string {
|
|
292
|
+
return typeof value === "string";
|
|
293
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpController, type McpControllerOptions } from "./mcp-controller.js";
|
|
2
|
+
|
|
3
|
+
export interface ControllerHost {
|
|
4
|
+
controller: McpController;
|
|
5
|
+
getController(): McpController;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Create a shared controller host for MCP and future dashboard consumers. */
|
|
10
|
+
export function createControllerHost(options: McpControllerOptions): ControllerHost {
|
|
11
|
+
const controller = new McpController(options);
|
|
12
|
+
return {
|
|
13
|
+
controller,
|
|
14
|
+
getController: () => controller,
|
|
15
|
+
dispose: () => {
|
|
16
|
+
// Reserved for future shared storage/session resources.
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|