@made-by-moonlight/athene-core 0.9.1
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/LICENSE +22 -0
- package/README.md +241 -0
- package/dist/activity-events.d.ts +42 -0
- package/dist/activity-events.d.ts.map +1 -0
- package/dist/activity-events.js +192 -0
- package/dist/activity-events.js.map +1 -0
- package/dist/activity-log.d.ts +71 -0
- package/dist/activity-log.d.ts.map +1 -0
- package/dist/activity-log.js +203 -0
- package/dist/activity-log.js.map +1 -0
- package/dist/activity-signal.d.ts +20 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +91 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/agent-report.d.ts +148 -0
- package/dist/agent-report.d.ts.map +1 -0
- package/dist/agent-report.js +516 -0
- package/dist/agent-report.js.map +1 -0
- package/dist/agent-selection.d.ts +31 -0
- package/dist/agent-selection.d.ts.map +1 -0
- package/dist/agent-selection.js +69 -0
- package/dist/agent-selection.js.map +1 -0
- package/dist/agent-workspace-hooks.d.ts +74 -0
- package/dist/agent-workspace-hooks.d.ts.map +1 -0
- package/dist/agent-workspace-hooks.js +988 -0
- package/dist/agent-workspace-hooks.js.map +1 -0
- package/dist/atomic-write.d.ts +6 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +49 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/cleanup-stack.d.ts +37 -0
- package/dist/cleanup-stack.d.ts.map +1 -0
- package/dist/cleanup-stack.js +45 -0
- package/dist/cleanup-stack.js.map +1 -0
- package/dist/code-review-manager.d.ts +118 -0
- package/dist/code-review-manager.d.ts.map +1 -0
- package/dist/code-review-manager.js +719 -0
- package/dist/code-review-manager.js.map +1 -0
- package/dist/code-review-store.d.ts +114 -0
- package/dist/code-review-store.d.ts.map +1 -0
- package/dist/code-review-store.js +346 -0
- package/dist/code-review-store.js.map +1 -0
- package/dist/config-generator.d.ts +84 -0
- package/dist/config-generator.d.ts.map +1 -0
- package/dist/config-generator.js +295 -0
- package/dist/config-generator.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +852 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon-children.d.ts +55 -0
- package/dist/daemon-children.d.ts.map +1 -0
- package/dist/daemon-children.js +435 -0
- package/dist/daemon-children.js.map +1 -0
- package/dist/dashboard-notifications.d.ts +42 -0
- package/dist/dashboard-notifications.d.ts.map +1 -0
- package/dist/dashboard-notifications.js +123 -0
- package/dist/dashboard-notifications.js.map +1 -0
- package/dist/events-db.d.ts +39 -0
- package/dist/events-db.d.ts.map +1 -0
- package/dist/events-db.js +185 -0
- package/dist/events-db.js.map +1 -0
- package/dist/feature-flags.d.ts +2 -0
- package/dist/feature-flags.d.ts.map +1 -0
- package/dist/feature-flags.js +9 -0
- package/dist/feature-flags.js.map +1 -0
- package/dist/feedback-tools.d.ts +97 -0
- package/dist/feedback-tools.d.ts.map +1 -0
- package/dist/feedback-tools.js +161 -0
- package/dist/feedback-tools.js.map +1 -0
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +59 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/format-automated-comments.d.ts +18 -0
- package/dist/format-automated-comments.d.ts.map +1 -0
- package/dist/gh-trace.d.ts +57 -0
- package/dist/gh-trace.d.ts.map +1 -0
- package/dist/gh-trace.js +320 -0
- package/dist/gh-trace.js.map +1 -0
- package/dist/git-activity.d.ts +10 -0
- package/dist/git-activity.d.ts.map +1 -0
- package/dist/git-activity.js +30 -0
- package/dist/git-activity.js.map +1 -0
- package/dist/global-config.d.ts +1085 -0
- package/dist/global-config.d.ts.map +1 -0
- package/dist/global-config.js +1067 -0
- package/dist/global-config.js.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/key-value.d.ts +7 -0
- package/dist/key-value.d.ts.map +1 -0
- package/dist/key-value.js +24 -0
- package/dist/key-value.js.map +1 -0
- package/dist/lifecycle-manager.d.ts +22 -0
- package/dist/lifecycle-manager.d.ts.map +1 -0
- package/dist/lifecycle-manager.js +2813 -0
- package/dist/lifecycle-manager.js.map +1 -0
- package/dist/lifecycle-state.d.ts +28 -0
- package/dist/lifecycle-state.d.ts.map +1 -0
- package/dist/lifecycle-state.js +446 -0
- package/dist/lifecycle-state.js.map +1 -0
- package/dist/lifecycle-status-decisions.d.ts +85 -0
- package/dist/lifecycle-status-decisions.d.ts.map +1 -0
- package/dist/lifecycle-status-decisions.js +262 -0
- package/dist/lifecycle-status-decisions.js.map +1 -0
- package/dist/lifecycle-transition.d.ts +81 -0
- package/dist/lifecycle-transition.d.ts.map +1 -0
- package/dist/lifecycle-transition.js +207 -0
- package/dist/lifecycle-transition.js.map +1 -0
- package/dist/metadata.d.ts +54 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +484 -0
- package/dist/metadata.js.map +1 -0
- package/dist/migration/storage-v2.d.ts +76 -0
- package/dist/migration/storage-v2.d.ts.map +1 -0
- package/dist/migration/storage-v2.js +1614 -0
- package/dist/migration/storage-v2.js.map +1 -0
- package/dist/notification-data.d.ts +135 -0
- package/dist/notification-data.d.ts.map +1 -0
- package/dist/notification-data.js +204 -0
- package/dist/notification-data.js.map +1 -0
- package/dist/notification-observability.d.ts +21 -0
- package/dist/notification-observability.d.ts.map +1 -0
- package/dist/notification-observability.js +154 -0
- package/dist/notification-observability.js.map +1 -0
- package/dist/notifier-resolution.d.ts +14 -0
- package/dist/notifier-resolution.d.ts.map +1 -0
- package/dist/notifier-resolution.js +23 -0
- package/dist/notifier-resolution.js.map +1 -0
- package/dist/observability.d.ts +100 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +535 -0
- package/dist/observability.js.map +1 -0
- package/dist/opencode-agents-md.d.ts +3 -0
- package/dist/opencode-agents-md.d.ts.map +1 -0
- package/dist/opencode-agents-md.js +40 -0
- package/dist/opencode-agents-md.js.map +1 -0
- package/dist/opencode-config.d.ts +2 -0
- package/dist/opencode-config.d.ts.map +1 -0
- package/dist/opencode-config.js +17 -0
- package/dist/opencode-config.js.map +1 -0
- package/dist/opencode-session-id.d.ts +2 -0
- package/dist/opencode-session-id.d.ts.map +1 -0
- package/dist/opencode-session-id.js +12 -0
- package/dist/opencode-session-id.js.map +1 -0
- package/dist/opencode-shared.d.ts +80 -0
- package/dist/opencode-shared.d.ts.map +1 -0
- package/dist/opencode-shared.js +202 -0
- package/dist/opencode-shared.js.map +1 -0
- package/dist/orchestrator-prompt.d.ts +19 -0
- package/dist/orchestrator-prompt.d.ts.map +1 -0
- package/dist/orchestrator-prompt.js +130 -0
- package/dist/orchestrator-prompt.js.map +1 -0
- package/dist/orchestrator-session-strategy.d.ts +5 -0
- package/dist/orchestrator-session-strategy.d.ts.map +1 -0
- package/dist/orchestrator-session-strategy.js +13 -0
- package/dist/orchestrator-session-strategy.js.map +1 -0
- package/dist/paths.d.ts +145 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +288 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform.d.ts +32 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +211 -0
- package/dist/platform.js.map +1 -0
- package/dist/plugin-registry.d.ts +15 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +499 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/portfolio-projects.d.ts +7 -0
- package/dist/portfolio-projects.d.ts.map +1 -0
- package/dist/portfolio-projects.js +65 -0
- package/dist/portfolio-projects.js.map +1 -0
- package/dist/portfolio-registry.d.ts +42 -0
- package/dist/portfolio-registry.d.ts.map +1 -0
- package/dist/portfolio-registry.js +311 -0
- package/dist/portfolio-registry.js.map +1 -0
- package/dist/portfolio-routing.d.ts +5 -0
- package/dist/portfolio-routing.d.ts.map +1 -0
- package/dist/portfolio-routing.js +24 -0
- package/dist/portfolio-routing.js.map +1 -0
- package/dist/portfolio-session-service.d.ts +15 -0
- package/dist/portfolio-session-service.d.ts.map +1 -0
- package/dist/portfolio-session-service.js +206 -0
- package/dist/portfolio-session-service.js.map +1 -0
- package/dist/process-cache.d.ts +32 -0
- package/dist/process-cache.d.ts.map +1 -0
- package/dist/process-cache.js +44 -0
- package/dist/process-cache.js.map +1 -0
- package/dist/project-resolver.d.ts +5 -0
- package/dist/project-resolver.d.ts.map +1 -0
- package/dist/project-resolver.js +20 -0
- package/dist/project-resolver.js.map +1 -0
- package/dist/prompt-builder.d.ts +42 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +182 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/prompts/orchestrator.md.js +4 -0
- package/dist/prompts/orchestrator.md.js.map +1 -0
- package/dist/query-activity-events.d.ts +42 -0
- package/dist/query-activity-events.d.ts.map +1 -0
- package/dist/query-activity-events.js +170 -0
- package/dist/query-activity-events.js.map +1 -0
- package/dist/recovery/actions.d.ts +7 -0
- package/dist/recovery/actions.d.ts.map +1 -0
- package/dist/recovery/index.d.ts +8 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/logger.d.ts +12 -0
- package/dist/recovery/logger.d.ts.map +1 -0
- package/dist/recovery/manager.d.ts +24 -0
- package/dist/recovery/manager.d.ts.map +1 -0
- package/dist/recovery/scanner.d.ts +11 -0
- package/dist/recovery/scanner.d.ts.map +1 -0
- package/dist/recovery/types.d.ts +170 -0
- package/dist/recovery/types.d.ts.map +1 -0
- package/dist/recovery/validator.d.ts +8 -0
- package/dist/recovery/validator.d.ts.map +1 -0
- package/dist/report-watcher.d.ts +93 -0
- package/dist/report-watcher.d.ts.map +1 -0
- package/dist/report-watcher.js +182 -0
- package/dist/report-watcher.js.map +1 -0
- package/dist/scm-webhook-utils.d.ts +6 -0
- package/dist/scm-webhook-utils.d.ts.map +1 -0
- package/dist/scm-webhook-utils.js +36 -0
- package/dist/scm-webhook-utils.js.map +1 -0
- package/dist/session-manager.d.ts +22 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +3077 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/spawn-target.d.ts +23 -0
- package/dist/spawn-target.d.ts.map +1 -0
- package/dist/spawn-target.js +39 -0
- package/dist/spawn-target.js.map +1 -0
- package/dist/storage-key.d.ts +9 -0
- package/dist/storage-key.d.ts.map +1 -0
- package/dist/storage-key.js +59 -0
- package/dist/storage-key.js.map +1 -0
- package/dist/tmux.d.ts +39 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +141 -0
- package/dist/tmux.js.map +1 -0
- package/dist/types.d.ts +1496 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +215 -0
- package/dist/types.js.map +1 -0
- package/dist/update-cache.d.ts +59 -0
- package/dist/update-cache.d.ts.map +1 -0
- package/dist/update-cache.js +77 -0
- package/dist/update-cache.js.map +1 -0
- package/dist/utils/metadata-flatten.d.ts +3 -0
- package/dist/utils/metadata-flatten.d.ts.map +1 -0
- package/dist/utils/metadata-flatten.js +18 -0
- package/dist/utils/metadata-flatten.js.map +1 -0
- package/dist/utils/pr.d.ts +7 -0
- package/dist/utils/pr.d.ts.map +1 -0
- package/dist/utils/pr.js +97 -0
- package/dist/utils/pr.js.map +1 -0
- package/dist/utils/session-from-metadata.d.ts +16 -0
- package/dist/utils/session-from-metadata.d.ts.map +1 -0
- package/dist/utils/session-from-metadata.js +87 -0
- package/dist/utils/session-from-metadata.js.map +1 -0
- package/dist/utils/session-id.d.ts +4 -0
- package/dist/utils/session-id.d.ts.map +1 -0
- package/dist/utils/session-id.js +9 -0
- package/dist/utils/session-id.js.map +1 -0
- package/dist/utils/validation.d.ts +9 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +45 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils.d.ts +65 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +189 -0
- package/dist/utils.js.map +1 -0
- package/dist/version-compare.d.ts +27 -0
- package/dist/version-compare.d.ts.map +1 -0
- package/dist/version-compare.js +121 -0
- package/dist/version-compare.js.map +1 -0
- package/dist/windows-pty-registry.d.ts +27 -0
- package/dist/windows-pty-registry.d.ts.map +1 -0
- package/dist/windows-pty-registry.js +109 -0
- package/dist/windows-pty-registry.js.map +1 -0
- package/package.json +110 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { resolve, dirname, basename, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { ConfigNotFoundError, ProjectResolveError } from './types.js';
|
|
8
|
+
import { generateSessionPrefix } from './paths.js';
|
|
9
|
+
import { getDefaultRuntime } from './platform.js';
|
|
10
|
+
import { getGlobalConfigPath, isCanonicalGlobalConfigPath, loadGlobalConfig } from './global-config.js';
|
|
11
|
+
import { loadEffectiveProjectConfig } from './project-resolver.js';
|
|
12
|
+
import { recordActivityEvent } from './activity-events.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configuration loader — reads agent-orchestrator.yaml and validates with Zod.
|
|
16
|
+
*
|
|
17
|
+
* Minimal config that just works:
|
|
18
|
+
* projects:
|
|
19
|
+
* my-app:
|
|
20
|
+
* repo: org/repo
|
|
21
|
+
* path: ~/my-app
|
|
22
|
+
*
|
|
23
|
+
* Everything else has sensible defaults.
|
|
24
|
+
*/
|
|
25
|
+
function inferScmPlugin(project) {
|
|
26
|
+
const scmPlugin = project.scm?.["plugin"];
|
|
27
|
+
if (scmPlugin === "gitlab") {
|
|
28
|
+
return "gitlab";
|
|
29
|
+
}
|
|
30
|
+
const scmHost = project.scm?.["host"];
|
|
31
|
+
if (typeof scmHost === "string" && scmHost.toLowerCase().includes("gitlab")) {
|
|
32
|
+
return "gitlab";
|
|
33
|
+
}
|
|
34
|
+
const trackerPlugin = project.tracker?.["plugin"];
|
|
35
|
+
if (trackerPlugin === "gitlab") {
|
|
36
|
+
return "gitlab";
|
|
37
|
+
}
|
|
38
|
+
const trackerHost = project.tracker?.["host"];
|
|
39
|
+
if (typeof trackerHost === "string" && trackerHost.toLowerCase().includes("gitlab")) {
|
|
40
|
+
return "gitlab";
|
|
41
|
+
}
|
|
42
|
+
return "github";
|
|
43
|
+
}
|
|
44
|
+
function classifyConfigShape(configPath) {
|
|
45
|
+
if (!existsSync(configPath)) {
|
|
46
|
+
return "missing";
|
|
47
|
+
}
|
|
48
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
49
|
+
const parsed = parse(raw);
|
|
50
|
+
return parsed && typeof parsed === "object" && "projects" in parsed
|
|
51
|
+
? "wrapped"
|
|
52
|
+
: "flat-or-nonobject";
|
|
53
|
+
}
|
|
54
|
+
function generateLegacyWrappedStorageKey(configPath, projectPath) {
|
|
55
|
+
const resolvedConfigPath = realpathSync(configPath);
|
|
56
|
+
const configDir = dirname(resolvedConfigPath);
|
|
57
|
+
const hash = createHash("sha256").update(configDir).digest("hex").slice(0, 12);
|
|
58
|
+
return `${hash}-${basename(projectPath)}`;
|
|
59
|
+
}
|
|
60
|
+
function applyWrappedLocalStorageKeys(configPath, parsed) {
|
|
61
|
+
if (!parsed || typeof parsed !== "object")
|
|
62
|
+
return parsed;
|
|
63
|
+
const parsedObject = parsed;
|
|
64
|
+
if (!("projects" in parsedObject) ||
|
|
65
|
+
!parsedObject["projects"] ||
|
|
66
|
+
typeof parsedObject["projects"] !== "object") {
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...parsedObject,
|
|
71
|
+
projects: Object.fromEntries(Object.entries(parsedObject["projects"]).map(([projectId, value]) => {
|
|
72
|
+
if (!value || typeof value !== "object") {
|
|
73
|
+
return [projectId, value];
|
|
74
|
+
}
|
|
75
|
+
const project = value;
|
|
76
|
+
if (typeof project["storageKey"] === "string" || typeof project["path"] !== "string") {
|
|
77
|
+
return [projectId, value];
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
projectId,
|
|
81
|
+
{
|
|
82
|
+
...project,
|
|
83
|
+
storageKey: generateLegacyWrappedStorageKey(configPath, project["path"]),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// ZOD SCHEMAS
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Common validation for plugin config fields (tracker, scm, notifier).
|
|
94
|
+
* Must have either plugin (for built-ins) or package/path (for external plugins).
|
|
95
|
+
* Cannot have both package and path.
|
|
96
|
+
*/
|
|
97
|
+
function validatePluginConfigFields(value, ctx, configType) {
|
|
98
|
+
// Must have either plugin or package/path
|
|
99
|
+
if (!value.plugin && !value.package && !value.path) {
|
|
100
|
+
ctx.addIssue({
|
|
101
|
+
code: z.ZodIssueCode.custom,
|
|
102
|
+
message: `${configType} config requires either 'plugin' (for built-ins) or 'package'/'path' (for external plugins)`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Cannot have both package and path
|
|
106
|
+
if (value.package && value.path) {
|
|
107
|
+
ctx.addIssue({
|
|
108
|
+
code: z.ZodIssueCode.custom,
|
|
109
|
+
message: `${configType} config cannot have both 'package' and 'path' - use one or the other`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const ReactionConfigSchema = z.object({
|
|
114
|
+
auto: z.boolean().default(true),
|
|
115
|
+
action: z.enum(["send-to-agent", "notify", "auto-merge"]).default("notify"),
|
|
116
|
+
message: z.string().optional(),
|
|
117
|
+
priority: z.enum(["urgent", "action", "warning", "info"]).optional(),
|
|
118
|
+
retries: z.number().optional(),
|
|
119
|
+
escalateAfter: z.union([z.number(), z.string()]).optional(),
|
|
120
|
+
threshold: z.string().optional(),
|
|
121
|
+
includeSummary: z.boolean().optional(),
|
|
122
|
+
});
|
|
123
|
+
const TrackerConfigSchema = z
|
|
124
|
+
.object({
|
|
125
|
+
plugin: z.string().optional(),
|
|
126
|
+
package: z.string().optional(),
|
|
127
|
+
path: z.string().optional(),
|
|
128
|
+
})
|
|
129
|
+
.passthrough()
|
|
130
|
+
.superRefine((value, ctx) => validatePluginConfigFields(value, ctx, "Tracker"));
|
|
131
|
+
const SCMConfigSchema = z
|
|
132
|
+
.object({
|
|
133
|
+
plugin: z.string().optional(),
|
|
134
|
+
package: z.string().optional(),
|
|
135
|
+
path: z.string().optional(),
|
|
136
|
+
webhook: z
|
|
137
|
+
.object({
|
|
138
|
+
enabled: z.boolean().default(true),
|
|
139
|
+
path: z.string().optional(),
|
|
140
|
+
secretEnvVar: z.string().optional(),
|
|
141
|
+
signatureHeader: z.string().optional(),
|
|
142
|
+
eventHeader: z.string().optional(),
|
|
143
|
+
deliveryHeader: z.string().optional(),
|
|
144
|
+
maxBodyBytes: z.number().int().positive().optional(),
|
|
145
|
+
})
|
|
146
|
+
.optional(),
|
|
147
|
+
})
|
|
148
|
+
.passthrough()
|
|
149
|
+
.superRefine((value, ctx) => validatePluginConfigFields(value, ctx, "SCM"));
|
|
150
|
+
const NotifierConfigSchema = z
|
|
151
|
+
.object({
|
|
152
|
+
plugin: z.string().optional(),
|
|
153
|
+
package: z.string().optional(),
|
|
154
|
+
path: z.string().optional(),
|
|
155
|
+
})
|
|
156
|
+
.passthrough()
|
|
157
|
+
.superRefine((value, ctx) => validatePluginConfigFields(value, ctx, "Notifier"));
|
|
158
|
+
const ObservabilityConfigSchema = z
|
|
159
|
+
.object({
|
|
160
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("warn"),
|
|
161
|
+
stderr: z.boolean().default(false),
|
|
162
|
+
})
|
|
163
|
+
.default({});
|
|
164
|
+
const AgentPermissionSchema = z
|
|
165
|
+
.enum(["permissionless", "default", "auto-edit", "suggest", "skip"])
|
|
166
|
+
.default("permissionless")
|
|
167
|
+
.transform((value) => (value === "skip" ? "permissionless" : value));
|
|
168
|
+
const AgentSpecificConfigSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
permissions: AgentPermissionSchema,
|
|
171
|
+
model: z.string().optional(),
|
|
172
|
+
orchestratorModel: z.string().optional(),
|
|
173
|
+
opencodeSessionId: z.string().optional(),
|
|
174
|
+
})
|
|
175
|
+
.passthrough();
|
|
176
|
+
const RoleAgentSpecificConfigSchema = z
|
|
177
|
+
.object({
|
|
178
|
+
permissions: z
|
|
179
|
+
.union([z.enum(["permissionless", "default", "auto-edit", "suggest"]), z.literal("skip")])
|
|
180
|
+
.optional(),
|
|
181
|
+
model: z.string().optional(),
|
|
182
|
+
orchestratorModel: z.string().optional(),
|
|
183
|
+
opencodeSessionId: z.string().optional(),
|
|
184
|
+
})
|
|
185
|
+
.passthrough();
|
|
186
|
+
const RoleAgentDefaultsSchema = z
|
|
187
|
+
.object({
|
|
188
|
+
agent: z.string().optional(),
|
|
189
|
+
})
|
|
190
|
+
.optional();
|
|
191
|
+
const RoleAgentConfigSchema = z
|
|
192
|
+
.object({
|
|
193
|
+
agent: z.string().optional(),
|
|
194
|
+
agentConfig: RoleAgentSpecificConfigSchema.optional(),
|
|
195
|
+
})
|
|
196
|
+
.optional();
|
|
197
|
+
const ProjectConfigSchema = z.object({
|
|
198
|
+
name: z.string().optional(),
|
|
199
|
+
repo: z.string().optional(),
|
|
200
|
+
path: z.string(),
|
|
201
|
+
defaultBranch: z.string().default("main"),
|
|
202
|
+
sessionPrefix: z
|
|
203
|
+
.string()
|
|
204
|
+
.regex(/^[a-zA-Z0-9_-]+$/, "sessionPrefix must match [a-zA-Z0-9_-]+")
|
|
205
|
+
.optional(),
|
|
206
|
+
/** Per-project resolution failure captured without aborting global load. */
|
|
207
|
+
resolveError: z.string().optional(),
|
|
208
|
+
runtime: z.string().optional(),
|
|
209
|
+
agent: z.string().optional(),
|
|
210
|
+
workspace: z.string().optional(),
|
|
211
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
212
|
+
tracker: TrackerConfigSchema.optional(),
|
|
213
|
+
scm: SCMConfigSchema.optional(),
|
|
214
|
+
symlinks: z.array(z.string()).optional(),
|
|
215
|
+
postCreate: z.array(z.string()).optional(),
|
|
216
|
+
agentConfig: AgentSpecificConfigSchema.default({}),
|
|
217
|
+
orchestrator: RoleAgentConfigSchema,
|
|
218
|
+
worker: RoleAgentConfigSchema,
|
|
219
|
+
reactions: z.record(ReactionConfigSchema.partial()).optional(),
|
|
220
|
+
agentRules: z.string().optional(),
|
|
221
|
+
agentRulesFile: z.string().optional(),
|
|
222
|
+
orchestratorRules: z.string().optional(),
|
|
223
|
+
orchestratorSessionStrategy: z
|
|
224
|
+
.enum(["reuse", "delete", "ignore", "delete-new", "ignore-new", "kill-previous"])
|
|
225
|
+
.optional(),
|
|
226
|
+
opencodeIssueSessionStrategy: z.enum(["reuse", "delete", "ignore"]).optional(),
|
|
227
|
+
});
|
|
228
|
+
const DefaultPluginsSchema = z.object({
|
|
229
|
+
runtime: z.string().default(() => getDefaultRuntime()),
|
|
230
|
+
agent: z.string().default("claude-code"),
|
|
231
|
+
workspace: z.string().default("worktree"),
|
|
232
|
+
notifiers: z.array(z.string()).default([]),
|
|
233
|
+
orchestrator: RoleAgentDefaultsSchema,
|
|
234
|
+
worker: RoleAgentDefaultsSchema,
|
|
235
|
+
});
|
|
236
|
+
const InstalledPluginConfigSchema = z
|
|
237
|
+
.object({
|
|
238
|
+
name: z.string(),
|
|
239
|
+
source: z.enum(["registry", "npm", "local"]),
|
|
240
|
+
package: z.string().optional(),
|
|
241
|
+
version: z.string().optional(),
|
|
242
|
+
path: z.string().optional(),
|
|
243
|
+
enabled: z.boolean().default(true),
|
|
244
|
+
})
|
|
245
|
+
.superRefine((value, ctx) => {
|
|
246
|
+
if (value.source === "local" && !value.path) {
|
|
247
|
+
ctx.addIssue({
|
|
248
|
+
code: z.ZodIssueCode.custom,
|
|
249
|
+
path: ["path"],
|
|
250
|
+
message: "Local plugins require a path",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if ((value.source === "registry" || value.source === "npm") && !value.package) {
|
|
254
|
+
ctx.addIssue({
|
|
255
|
+
code: z.ZodIssueCode.custom,
|
|
256
|
+
path: ["package"],
|
|
257
|
+
message: "Registry and npm plugins require a package name",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
const PowerConfigSchema = z
|
|
262
|
+
.object({
|
|
263
|
+
/**
|
|
264
|
+
* Prevent macOS idle sleep while AO is running.
|
|
265
|
+
* Uses `caffeinate -i -w <pid>` to hold an assertion.
|
|
266
|
+
* Defaults to true on macOS, no-op on other platforms.
|
|
267
|
+
*/
|
|
268
|
+
preventIdleSleep: z.boolean().default(process.platform === "darwin"),
|
|
269
|
+
})
|
|
270
|
+
.default({});
|
|
271
|
+
const DashboardConfigSchema = z.object({
|
|
272
|
+
attentionZones: z.enum(["simple", "detailed"]).default("simple"),
|
|
273
|
+
});
|
|
274
|
+
const LifecycleConfigSchema = z
|
|
275
|
+
.object({
|
|
276
|
+
/**
|
|
277
|
+
* When a session's PR is detected as merged, automatically tear down the
|
|
278
|
+
* tmux runtime, remove the worktree, and archive the session metadata.
|
|
279
|
+
* Defaults to true so `athene status` does not retain stale merged entries.
|
|
280
|
+
*/
|
|
281
|
+
autoCleanupOnMerge: z.boolean().default(true),
|
|
282
|
+
/**
|
|
283
|
+
* Maximum time (ms) to wait after a session enters `merged` before forcing
|
|
284
|
+
* cleanup regardless of agent activity. Defaults to 5 minutes. Use `0` to
|
|
285
|
+
* disable the grace window (cleanup runs immediately even if the agent is
|
|
286
|
+
* still active). Values between 1 and 9999 are rejected to catch the common
|
|
287
|
+
* mistake of writing seconds (e.g. `5`) when milliseconds are expected.
|
|
288
|
+
*/
|
|
289
|
+
mergeCleanupIdleGraceMs: z
|
|
290
|
+
.number()
|
|
291
|
+
.int()
|
|
292
|
+
.nonnegative()
|
|
293
|
+
.refine((v) => v === 0 || v >= 10_000, {
|
|
294
|
+
message: "mergeCleanupIdleGraceMs is in milliseconds; values between 1 and 9999 are likely a units mistake (use 0 to disable the gate, or e.g. 10000 for 10s, 300000 for 5min)",
|
|
295
|
+
})
|
|
296
|
+
.default(300_000),
|
|
297
|
+
})
|
|
298
|
+
.default({});
|
|
299
|
+
const OrchestratorConfigSchema = z.object({
|
|
300
|
+
$schema: z.string().optional(),
|
|
301
|
+
port: z.number().int().default(3000),
|
|
302
|
+
terminalPort: z.number().int().optional(),
|
|
303
|
+
directTerminalPort: z.number().int().optional(),
|
|
304
|
+
readyThresholdMs: z.number().int().nonnegative().default(300_000),
|
|
305
|
+
power: PowerConfigSchema,
|
|
306
|
+
lifecycle: LifecycleConfigSchema,
|
|
307
|
+
observability: ObservabilityConfigSchema,
|
|
308
|
+
defaults: DefaultPluginsSchema.default({}),
|
|
309
|
+
plugins: z.array(InstalledPluginConfigSchema).default([]),
|
|
310
|
+
dashboard: DashboardConfigSchema.optional(),
|
|
311
|
+
projects: z.record(z
|
|
312
|
+
.string()
|
|
313
|
+
.regex(/^[a-zA-Z0-9_-]+$/, "Project ID must match [a-zA-Z0-9_-]+ (no dots, slashes, or special characters)"), ProjectConfigSchema),
|
|
314
|
+
notifiers: z.record(NotifierConfigSchema).default({}),
|
|
315
|
+
notificationRouting: z.record(z.array(z.string())).default({}),
|
|
316
|
+
reactions: z.record(ReactionConfigSchema).default({}),
|
|
317
|
+
});
|
|
318
|
+
// =============================================================================
|
|
319
|
+
// CONFIG LOADING
|
|
320
|
+
// =============================================================================
|
|
321
|
+
/** Expand ~ to home directory */
|
|
322
|
+
function expandHome(filepath) {
|
|
323
|
+
if (filepath.startsWith("~/")) {
|
|
324
|
+
return join(homedir(), filepath.slice(2));
|
|
325
|
+
}
|
|
326
|
+
return filepath;
|
|
327
|
+
}
|
|
328
|
+
/** Expand all path fields in the config */
|
|
329
|
+
function expandPaths(config) {
|
|
330
|
+
for (const project of Object.values(config.projects)) {
|
|
331
|
+
project.path = expandHome(project.path);
|
|
332
|
+
}
|
|
333
|
+
for (const plugin of config.plugins ?? []) {
|
|
334
|
+
if (plugin.path) {
|
|
335
|
+
plugin.path = expandHome(plugin.path);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return config;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Generate a temporary plugin name from a package or path specifier.
|
|
342
|
+
* This name is used until the actual manifest.name is discovered during plugin loading.
|
|
343
|
+
* Format: extract the plugin name from the package/path, removing common prefixes.
|
|
344
|
+
* e.g., "@acme/ao-plugin-tracker-jira" -> "jira"
|
|
345
|
+
* e.g., "@acme/ao-plugin-tracker-jira-cloud" -> "jira-cloud"
|
|
346
|
+
* e.g., "./plugins/my-tracker" -> "my-tracker"
|
|
347
|
+
* e.g., "my-tracker" (local path without slashes) -> "my-tracker"
|
|
348
|
+
*/
|
|
349
|
+
function generateTempPluginName(pkg, path) {
|
|
350
|
+
if (pkg) {
|
|
351
|
+
// Extract package name without scope: "@acme/ao-plugin-tracker-jira" -> "ao-plugin-tracker-jira"
|
|
352
|
+
const slashParts = pkg.split("/");
|
|
353
|
+
const packageName = slashParts[slashParts.length - 1] ?? pkg;
|
|
354
|
+
// Extract plugin name after [ao-]plugin-{slot}- prefix, preserving multi-word names like "jira-cloud".
|
|
355
|
+
// The optional `ao-` prefix keeps backward-compat with legacy `ao-plugin-*` external packages
|
|
356
|
+
// while supporting the current `plugin-*` naming (e.g. @made-by-moonlight/athene-plugin-tracker-foo).
|
|
357
|
+
const prefixMatch = packageName.match(/^(?:ao-)?plugin-(?:runtime|agent|workspace|tracker|scm|notifier|terminal)-(.+)$/);
|
|
358
|
+
if (prefixMatch?.[1]) {
|
|
359
|
+
return prefixMatch[1];
|
|
360
|
+
}
|
|
361
|
+
// Non-standard package name (doesn't follow ao-plugin convention): use the full package name
|
|
362
|
+
// to avoid collisions. "plugin" from "custom-tracker-plugin" would collide with other packages
|
|
363
|
+
// that also end in "-plugin". The temp name is replaced with manifest.name after loading anyway.
|
|
364
|
+
return packageName;
|
|
365
|
+
}
|
|
366
|
+
// Handle local paths: use the basename
|
|
367
|
+
// ./plugins/my-tracker -> my-tracker
|
|
368
|
+
// my-tracker -> my-tracker (no slashes is still a valid path)
|
|
369
|
+
if (path) {
|
|
370
|
+
const segments = path.split("/").filter((s) => s && s !== "." && s !== "..");
|
|
371
|
+
return segments[segments.length - 1] ?? path;
|
|
372
|
+
}
|
|
373
|
+
return "unknown";
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Helper to process a single external plugin config entry.
|
|
377
|
+
* Expands home paths, generates temp plugin name if needed, and returns the entry ref.
|
|
378
|
+
*/
|
|
379
|
+
function processExternalPluginConfig(pluginConfig, source, location, slot) {
|
|
380
|
+
if (!pluginConfig.package && !pluginConfig.path)
|
|
381
|
+
return null;
|
|
382
|
+
// Expand home paths (~/...) for consistency with config.plugins
|
|
383
|
+
if (pluginConfig.path) {
|
|
384
|
+
pluginConfig.path = expandHome(pluginConfig.path);
|
|
385
|
+
}
|
|
386
|
+
// Track if user explicitly specified plugin name (for validation)
|
|
387
|
+
const userSpecifiedPlugin = pluginConfig.plugin;
|
|
388
|
+
// If plugin name not specified, generate a temporary one from package/path
|
|
389
|
+
if (!pluginConfig.plugin) {
|
|
390
|
+
pluginConfig.plugin = generateTempPluginName(pluginConfig.package, pluginConfig.path);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
source,
|
|
394
|
+
location,
|
|
395
|
+
slot,
|
|
396
|
+
package: pluginConfig.package,
|
|
397
|
+
path: pluginConfig.path,
|
|
398
|
+
expectedPluginName: userSpecifiedPlugin,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Collect external plugin configs from tracker, scm, and notifier inline configs.
|
|
403
|
+
* These will be auto-added to config.plugins for loading.
|
|
404
|
+
*
|
|
405
|
+
* Also sets a temporary plugin name on configs that only have package/path,
|
|
406
|
+
* so that resolvePlugins() can look up the plugin by name.
|
|
407
|
+
*
|
|
408
|
+
* IMPORTANT: Only sets expectedPluginName when user explicitly specified `plugin`.
|
|
409
|
+
* When plugin is auto-generated, expectedPluginName is left undefined so that
|
|
410
|
+
* any manifest.name is accepted and the config is updated with it.
|
|
411
|
+
*/
|
|
412
|
+
function collectExternalPluginConfigs(config) {
|
|
413
|
+
const entries = [];
|
|
414
|
+
// Collect from project tracker and scm configs
|
|
415
|
+
for (const [projectId, project] of Object.entries(config.projects)) {
|
|
416
|
+
if (project.tracker) {
|
|
417
|
+
const entry = processExternalPluginConfig(project.tracker, `projects.${projectId}.tracker`, { kind: "project", projectId, configType: "tracker" }, "tracker");
|
|
418
|
+
if (entry)
|
|
419
|
+
entries.push(entry);
|
|
420
|
+
}
|
|
421
|
+
if (project.scm) {
|
|
422
|
+
const entry = processExternalPluginConfig(project.scm, `projects.${projectId}.scm`, { kind: "project", projectId, configType: "scm" }, "scm");
|
|
423
|
+
if (entry)
|
|
424
|
+
entries.push(entry);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Collect from global notifier configs
|
|
428
|
+
for (const [notifierId, notifierConfig] of Object.entries(config.notifiers ?? {})) {
|
|
429
|
+
if (notifierConfig) {
|
|
430
|
+
const entry = processExternalPluginConfig(notifierConfig, `notifiers.${notifierId}`, { kind: "notifier", notifierId }, "notifier");
|
|
431
|
+
if (entry)
|
|
432
|
+
entries.push(entry);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return entries;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Generate InstalledPluginConfig entries from external plugin entries.
|
|
439
|
+
* Merges with existing plugins, avoiding duplicates by package/path.
|
|
440
|
+
*/
|
|
441
|
+
function mergeExternalPlugins(existingPlugins, externalEntries) {
|
|
442
|
+
const plugins = [...(existingPlugins ?? [])];
|
|
443
|
+
const seen = new Set();
|
|
444
|
+
// Track existing plugins by package/path
|
|
445
|
+
for (const plugin of plugins) {
|
|
446
|
+
if (plugin.package)
|
|
447
|
+
seen.add(`package:${plugin.package}`);
|
|
448
|
+
if (plugin.path)
|
|
449
|
+
seen.add(`path:${plugin.path}`);
|
|
450
|
+
}
|
|
451
|
+
// Add external entries that aren't already present, or enable if disabled
|
|
452
|
+
for (const entry of externalEntries) {
|
|
453
|
+
const key = entry.package ? `package:${entry.package}` : `path:${entry.path}`;
|
|
454
|
+
if (seen.has(key)) {
|
|
455
|
+
// If the existing plugin is disabled but there's an inline reference, enable it
|
|
456
|
+
const existingPlugin = plugins.find((p) => (entry.package && p.package === entry.package) || (entry.path && p.path === entry.path));
|
|
457
|
+
if (existingPlugin && existingPlugin.enabled === false) {
|
|
458
|
+
existingPlugin.enabled = true;
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
seen.add(key);
|
|
463
|
+
// Generate a temporary name - will be replaced with manifest.name during loading
|
|
464
|
+
const tempName = entry.expectedPluginName ?? generateTempPluginName(entry.package, entry.path);
|
|
465
|
+
plugins.push({
|
|
466
|
+
name: tempName,
|
|
467
|
+
source: entry.package ? "npm" : "local",
|
|
468
|
+
package: entry.package,
|
|
469
|
+
path: entry.path,
|
|
470
|
+
enabled: true,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return plugins;
|
|
474
|
+
}
|
|
475
|
+
/** Apply defaults to project configs */
|
|
476
|
+
function applyProjectDefaults(config) {
|
|
477
|
+
for (const [id, project] of Object.entries(config.projects)) {
|
|
478
|
+
// Derive name from project ID if not set
|
|
479
|
+
if (!project.name) {
|
|
480
|
+
project.name = id;
|
|
481
|
+
}
|
|
482
|
+
// Derive session prefix from the project path basename if not set.
|
|
483
|
+
// This preserves the long-standing semantics on this branch, where
|
|
484
|
+
// `/repos/integrator` becomes `int` regardless of the config key.
|
|
485
|
+
if (!project.sessionPrefix) {
|
|
486
|
+
project.sessionPrefix = generateSessionPrefix(basename(project.path));
|
|
487
|
+
}
|
|
488
|
+
const inferredPlugin = inferScmPlugin(project);
|
|
489
|
+
// Infer SCM from repo if not set
|
|
490
|
+
if (!project.scm && project.repo?.includes("/")) {
|
|
491
|
+
project.scm = { plugin: inferredPlugin };
|
|
492
|
+
}
|
|
493
|
+
// Infer tracker from repo if not set (default to github issues)
|
|
494
|
+
if (!project.tracker && project.repo?.includes("/")) {
|
|
495
|
+
project.tracker = { plugin: inferredPlugin };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return config;
|
|
499
|
+
}
|
|
500
|
+
/** Validate project uniqueness and session prefix collisions */
|
|
501
|
+
function validateProjectUniqueness(config) {
|
|
502
|
+
const projectIds = new Set();
|
|
503
|
+
for (const [projectId] of Object.entries(config.projects)) {
|
|
504
|
+
if (projectIds.has(projectId)) {
|
|
505
|
+
throw new Error(`Duplicate project ID detected: "${projectId}"\n` +
|
|
506
|
+
`Each project entry must use a unique registry key.`);
|
|
507
|
+
}
|
|
508
|
+
projectIds.add(projectId);
|
|
509
|
+
}
|
|
510
|
+
// Check for duplicate session prefixes
|
|
511
|
+
const prefixes = new Set();
|
|
512
|
+
const prefixToProject = {};
|
|
513
|
+
for (const [projectId, project] of Object.entries(config.projects)) {
|
|
514
|
+
const prefix = project.sessionPrefix || generateSessionPrefix(projectId);
|
|
515
|
+
if (prefixes.has(prefix)) {
|
|
516
|
+
const firstProjectKey = prefixToProject[prefix];
|
|
517
|
+
throw new Error(`Duplicate session prefix detected: "${prefix}"\n` +
|
|
518
|
+
`Projects "${firstProjectKey}" and "${projectId}" would generate the same prefix.\n\n` +
|
|
519
|
+
`To fix this, add an explicit sessionPrefix to one of these projects:\n\n` +
|
|
520
|
+
`projects:\n` +
|
|
521
|
+
` ${firstProjectKey}:\n` +
|
|
522
|
+
` path: ${config.projects[firstProjectKey]?.path}\n` +
|
|
523
|
+
` sessionPrefix: ${prefix}1 # Add explicit prefix\n` +
|
|
524
|
+
` ${projectId}:\n` +
|
|
525
|
+
` path: ${project.path}\n` +
|
|
526
|
+
` sessionPrefix: ${prefix}2 # Add explicit prefix\n`);
|
|
527
|
+
}
|
|
528
|
+
prefixes.add(prefix);
|
|
529
|
+
prefixToProject[prefix] = projectId;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** Apply default reactions */
|
|
533
|
+
function applyDefaultReactions(config) {
|
|
534
|
+
const defaults = {
|
|
535
|
+
"pr-closed": {
|
|
536
|
+
auto: true,
|
|
537
|
+
action: "notify",
|
|
538
|
+
priority: "action",
|
|
539
|
+
message: "A PR was closed without merging. Decide whether to learn from the closure, resume the work, or terminate the session.",
|
|
540
|
+
},
|
|
541
|
+
"ci-failed": {
|
|
542
|
+
auto: true,
|
|
543
|
+
action: "send-to-agent",
|
|
544
|
+
retries: 2,
|
|
545
|
+
escalateAfter: 2,
|
|
546
|
+
},
|
|
547
|
+
"changes-requested": {
|
|
548
|
+
auto: true,
|
|
549
|
+
action: "send-to-agent",
|
|
550
|
+
message: "There are new review comments on your PR requesting changes.",
|
|
551
|
+
escalateAfter: "30m",
|
|
552
|
+
},
|
|
553
|
+
"bugbot-comments": {
|
|
554
|
+
auto: true,
|
|
555
|
+
action: "send-to-agent",
|
|
556
|
+
message: "Automated review comments found on your PR. Details will follow shortly.",
|
|
557
|
+
escalateAfter: "30m",
|
|
558
|
+
},
|
|
559
|
+
"merge-conflicts": {
|
|
560
|
+
auto: true,
|
|
561
|
+
action: "send-to-agent",
|
|
562
|
+
message: "Your branch has merge conflicts. Rebase on the default branch and resolve them.",
|
|
563
|
+
escalateAfter: "15m",
|
|
564
|
+
},
|
|
565
|
+
"approved-and-green": {
|
|
566
|
+
auto: false,
|
|
567
|
+
action: "notify",
|
|
568
|
+
priority: "action",
|
|
569
|
+
message: "PR is ready to merge",
|
|
570
|
+
},
|
|
571
|
+
"agent-idle": {
|
|
572
|
+
auto: true,
|
|
573
|
+
action: "send-to-agent",
|
|
574
|
+
message: "You appear to be idle. If your task is not complete, continue working — write the code, commit, push, and create a PR. If you are blocked, explain what is blocking you.",
|
|
575
|
+
retries: 2,
|
|
576
|
+
escalateAfter: "15m",
|
|
577
|
+
},
|
|
578
|
+
"agent-stuck": {
|
|
579
|
+
auto: true,
|
|
580
|
+
action: "notify",
|
|
581
|
+
priority: "urgent",
|
|
582
|
+
threshold: "10m",
|
|
583
|
+
},
|
|
584
|
+
"agent-needs-input": {
|
|
585
|
+
auto: true,
|
|
586
|
+
action: "notify",
|
|
587
|
+
priority: "urgent",
|
|
588
|
+
},
|
|
589
|
+
"agent-exited": {
|
|
590
|
+
auto: true,
|
|
591
|
+
action: "notify",
|
|
592
|
+
priority: "urgent",
|
|
593
|
+
},
|
|
594
|
+
"all-complete": {
|
|
595
|
+
auto: true,
|
|
596
|
+
action: "notify",
|
|
597
|
+
priority: "info",
|
|
598
|
+
includeSummary: true,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
// Merge defaults with user-specified reactions (user wins)
|
|
602
|
+
config.reactions = { ...defaults, ...config.reactions };
|
|
603
|
+
return config;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Search for config file in standard locations.
|
|
607
|
+
*
|
|
608
|
+
* Search order:
|
|
609
|
+
* 1. AO_CONFIG_PATH environment variable (if set)
|
|
610
|
+
* 2. Search up directory tree from CWD (like git)
|
|
611
|
+
* 3. Explicit startDir (if provided)
|
|
612
|
+
* 4. Home directory locations
|
|
613
|
+
*/
|
|
614
|
+
function findConfigFile(startDir) {
|
|
615
|
+
// 1. Check environment variable override
|
|
616
|
+
if (process.env["AO_CONFIG_PATH"]) {
|
|
617
|
+
const envPath = resolve(process.env["AO_CONFIG_PATH"]);
|
|
618
|
+
if (existsSync(envPath)) {
|
|
619
|
+
return envPath;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// 2. Search up directory tree from CWD (like git)
|
|
623
|
+
const searchUpTree = (dir) => {
|
|
624
|
+
const configFiles = ["agent-orchestrator.yaml", "agent-orchestrator.yml"];
|
|
625
|
+
for (const filename of configFiles) {
|
|
626
|
+
const configPath = resolve(dir, filename);
|
|
627
|
+
if (!existsSync(configPath))
|
|
628
|
+
continue;
|
|
629
|
+
return configPath;
|
|
630
|
+
}
|
|
631
|
+
const parent = resolve(dir, "..");
|
|
632
|
+
if (parent === dir) {
|
|
633
|
+
// Reached root
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return searchUpTree(parent);
|
|
637
|
+
};
|
|
638
|
+
const cwd = process.cwd();
|
|
639
|
+
const foundInTree = searchUpTree(cwd);
|
|
640
|
+
if (foundInTree) {
|
|
641
|
+
return foundInTree;
|
|
642
|
+
}
|
|
643
|
+
// 3. Check explicit startDir if provided
|
|
644
|
+
if (startDir) {
|
|
645
|
+
const files = ["agent-orchestrator.yaml", "agent-orchestrator.yml"];
|
|
646
|
+
for (const filename of files) {
|
|
647
|
+
const path = resolve(startDir, filename);
|
|
648
|
+
if (!existsSync(path))
|
|
649
|
+
continue;
|
|
650
|
+
return path;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// 4. Check global config path (new hybrid mode: ~/.agent-orchestrator/config.yaml)
|
|
654
|
+
// This takes priority over legacy home-directory locations so that users who
|
|
655
|
+
// have migrated to the hybrid model always load from the canonical global path.
|
|
656
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
657
|
+
if (existsSync(globalConfigPath)) {
|
|
658
|
+
return globalConfigPath;
|
|
659
|
+
}
|
|
660
|
+
// 5. Legacy home directory locations (backward compatibility)
|
|
661
|
+
const homePaths = [
|
|
662
|
+
resolve(homedir(), ".agent-orchestrator.yaml"),
|
|
663
|
+
resolve(homedir(), ".agent-orchestrator.yml"),
|
|
664
|
+
resolve(homedir(), ".config", "agent-orchestrator", "config.yaml"),
|
|
665
|
+
];
|
|
666
|
+
for (const path of homePaths) {
|
|
667
|
+
if (existsSync(path)) {
|
|
668
|
+
return path;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
function buildEffectiveConfigFromFlatLocalPath(configPath, _localParsed) {
|
|
674
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
675
|
+
const globalConfig = loadGlobalConfig(globalConfigPath);
|
|
676
|
+
if (!globalConfig)
|
|
677
|
+
return null;
|
|
678
|
+
const canonicalProjectDir = (() => {
|
|
679
|
+
try {
|
|
680
|
+
return realpathSync(resolve(dirname(configPath)));
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return resolve(dirname(configPath));
|
|
684
|
+
}
|
|
685
|
+
})();
|
|
686
|
+
const entry = Object.entries(globalConfig.projects).find(([, project]) => {
|
|
687
|
+
if (typeof project.path !== "string")
|
|
688
|
+
return false;
|
|
689
|
+
try {
|
|
690
|
+
return realpathSync(resolve(project.path)) === canonicalProjectDir;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return resolve(project.path) === canonicalProjectDir;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
if (!entry)
|
|
697
|
+
return null;
|
|
698
|
+
const [projectId] = entry;
|
|
699
|
+
const project = loadEffectiveProjectConfig(projectId, globalConfig);
|
|
700
|
+
const config = validateConfig({
|
|
701
|
+
port: globalConfig.port,
|
|
702
|
+
terminalPort: globalConfig.terminalPort,
|
|
703
|
+
directTerminalPort: globalConfig.directTerminalPort,
|
|
704
|
+
readyThresholdMs: globalConfig.readyThresholdMs,
|
|
705
|
+
observability: globalConfig.observability,
|
|
706
|
+
defaults: globalConfig.defaults,
|
|
707
|
+
notifiers: globalConfig.notifiers,
|
|
708
|
+
notificationRouting: globalConfig.notificationRouting,
|
|
709
|
+
reactions: globalConfig.reactions,
|
|
710
|
+
projects: {
|
|
711
|
+
[projectId]: {
|
|
712
|
+
...project,
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
return { ...config, degradedProjects: {} };
|
|
717
|
+
}
|
|
718
|
+
function buildEffectiveConfigFromGlobalConfigPath(configPath) {
|
|
719
|
+
const globalConfig = loadGlobalConfig(configPath);
|
|
720
|
+
if (!globalConfig)
|
|
721
|
+
return null;
|
|
722
|
+
const projects = {};
|
|
723
|
+
const degradedProjects = {};
|
|
724
|
+
for (const [projectId, entry] of Object.entries(globalConfig.projects)) {
|
|
725
|
+
try {
|
|
726
|
+
projects[projectId] = loadEffectiveProjectConfig(projectId, globalConfig, configPath);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
if (!(error instanceof ProjectResolveError)) {
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
degradedProjects[projectId] = {
|
|
733
|
+
projectId,
|
|
734
|
+
path: entry.path,
|
|
735
|
+
resolveError: error.message,
|
|
736
|
+
};
|
|
737
|
+
if (error.reasonKind === "malformed" || error.reasonKind === "invalid") {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
recordActivityEvent({
|
|
741
|
+
projectId,
|
|
742
|
+
source: "config",
|
|
743
|
+
kind: "config.project_resolve_failed",
|
|
744
|
+
level: "error",
|
|
745
|
+
summary: `project ${projectId} failed to resolve`,
|
|
746
|
+
data: { path: entry.path, error: error.message },
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const config = validateConfig({
|
|
751
|
+
port: globalConfig.port,
|
|
752
|
+
terminalPort: globalConfig.terminalPort,
|
|
753
|
+
directTerminalPort: globalConfig.directTerminalPort,
|
|
754
|
+
readyThresholdMs: globalConfig.readyThresholdMs,
|
|
755
|
+
observability: globalConfig.observability,
|
|
756
|
+
defaults: globalConfig.defaults,
|
|
757
|
+
notifiers: globalConfig.notifiers,
|
|
758
|
+
notificationRouting: globalConfig.notificationRouting,
|
|
759
|
+
reactions: globalConfig.reactions,
|
|
760
|
+
projects,
|
|
761
|
+
});
|
|
762
|
+
return { ...config, degradedProjects };
|
|
763
|
+
}
|
|
764
|
+
// =============================================================================
|
|
765
|
+
// PUBLIC API
|
|
766
|
+
// =============================================================================
|
|
767
|
+
/** Find config file path (exported for use in hash generation) */
|
|
768
|
+
function findConfig(startDir) {
|
|
769
|
+
return findConfigFile(startDir);
|
|
770
|
+
}
|
|
771
|
+
/** Load and validate config from a YAML file */
|
|
772
|
+
function loadConfig(configPath) {
|
|
773
|
+
// Priority: 1. Explicit param, 2. Search (including AO_CONFIG_PATH env var)
|
|
774
|
+
// findConfigFile treats AO_CONFIG_PATH as authoritative when present.
|
|
775
|
+
const path = configPath ?? findConfigFile();
|
|
776
|
+
if (!path) {
|
|
777
|
+
throw new ConfigNotFoundError();
|
|
778
|
+
}
|
|
779
|
+
const raw = readFileSync(path, "utf-8");
|
|
780
|
+
const parsed = parse(raw);
|
|
781
|
+
const shape = classifyConfigShape(path);
|
|
782
|
+
const isCanonicalGlobalConfig = isCanonicalGlobalConfigPath(path);
|
|
783
|
+
const normalizedParsed = !isCanonicalGlobalConfig && shape === "wrapped"
|
|
784
|
+
? applyWrappedLocalStorageKeys(path, parsed)
|
|
785
|
+
: parsed;
|
|
786
|
+
const config = isCanonicalGlobalConfig
|
|
787
|
+
? (buildEffectiveConfigFromGlobalConfigPath(path) ?? validateConfig(normalizedParsed))
|
|
788
|
+
: shape === "wrapped"
|
|
789
|
+
? validateConfig(normalizedParsed)
|
|
790
|
+
: (buildEffectiveConfigFromFlatLocalPath(path) ??
|
|
791
|
+
validateConfig(normalizedParsed));
|
|
792
|
+
// Set the config path in the config object for hash generation
|
|
793
|
+
config.configPath = path;
|
|
794
|
+
if (!("degradedProjects" in config)) {
|
|
795
|
+
config.degradedProjects = {};
|
|
796
|
+
}
|
|
797
|
+
return config;
|
|
798
|
+
}
|
|
799
|
+
/** Load config and return both config and resolved path */
|
|
800
|
+
function loadConfigWithPath(configPath) {
|
|
801
|
+
const path = configPath ?? findConfigFile();
|
|
802
|
+
if (!path) {
|
|
803
|
+
throw new ConfigNotFoundError();
|
|
804
|
+
}
|
|
805
|
+
const raw = readFileSync(path, "utf-8");
|
|
806
|
+
const parsed = parse(raw);
|
|
807
|
+
const shape = classifyConfigShape(path);
|
|
808
|
+
const isCanonicalGlobalConfig = isCanonicalGlobalConfigPath(path);
|
|
809
|
+
const normalizedParsed = !isCanonicalGlobalConfig && shape === "wrapped"
|
|
810
|
+
? applyWrappedLocalStorageKeys(path, parsed)
|
|
811
|
+
: parsed;
|
|
812
|
+
const config = isCanonicalGlobalConfig
|
|
813
|
+
? (buildEffectiveConfigFromGlobalConfigPath(path) ?? validateConfig(normalizedParsed))
|
|
814
|
+
: shape === "wrapped"
|
|
815
|
+
? validateConfig(normalizedParsed)
|
|
816
|
+
: (buildEffectiveConfigFromFlatLocalPath(path) ??
|
|
817
|
+
validateConfig(normalizedParsed));
|
|
818
|
+
// Set the config path in the config object for hash generation
|
|
819
|
+
config.configPath = path;
|
|
820
|
+
if (!("degradedProjects" in config)) {
|
|
821
|
+
config.degradedProjects = {};
|
|
822
|
+
}
|
|
823
|
+
return { config: config, path };
|
|
824
|
+
}
|
|
825
|
+
/** Validate a raw config object */
|
|
826
|
+
function validateConfig(raw) {
|
|
827
|
+
const validated = OrchestratorConfigSchema.parse(raw);
|
|
828
|
+
let config = validated;
|
|
829
|
+
config = expandPaths(config);
|
|
830
|
+
config = applyProjectDefaults(config);
|
|
831
|
+
config = applyDefaultReactions(config);
|
|
832
|
+
// Collect external plugin configs from inline tracker/scm/notifier configs
|
|
833
|
+
// and merge them into config.plugins for loading
|
|
834
|
+
const externalPluginEntries = collectExternalPluginConfigs(config);
|
|
835
|
+
if (externalPluginEntries.length > 0) {
|
|
836
|
+
config.plugins = mergeExternalPlugins(config.plugins, externalPluginEntries);
|
|
837
|
+
// Store entries for manifest validation during plugin loading
|
|
838
|
+
config._externalPluginEntries = externalPluginEntries;
|
|
839
|
+
}
|
|
840
|
+
// Validate project uniqueness and prefix collisions
|
|
841
|
+
validateProjectUniqueness(config);
|
|
842
|
+
return config;
|
|
843
|
+
}
|
|
844
|
+
/** Get the default config (useful for first-run setup) */
|
|
845
|
+
function getDefaultConfig() {
|
|
846
|
+
return validateConfig({
|
|
847
|
+
projects: {},
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export { collectExternalPluginConfigs, findConfig, findConfigFile, getDefaultConfig, loadConfig, loadConfigWithPath, validateConfig };
|
|
852
|
+
//# sourceMappingURL=config.js.map
|