@backburner/cli 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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,136 @@
1
+ import * as path from "node:path";
2
+ import { AgentsConfigFileSchema, CapabilitiesConfigFileSchema, DEFAULT_GITHUB_BROKER_SERVER_NAME, DEFAULT_GITHUB_BROKER_TOOLS, McpsConfigFileSchema, PromptsConfigFileSchema, ReposConfigFileSchema } from "./schemas.js";
3
+ import { readJsonFile } from "../utils/json.js";
4
+ import { managementRoot } from "../utils/paths.js";
5
+ function formatZodPath(pathParts) {
6
+ return pathParts
7
+ .map((segment, index) => {
8
+ if (typeof segment === "number")
9
+ return `[${segment}]`;
10
+ return index === 0 ? segment : `.${segment}`;
11
+ })
12
+ .join("");
13
+ }
14
+ function parseConfig(schema, value, filePath) {
15
+ const result = schema.safeParse(value);
16
+ if (result.success)
17
+ return result.data;
18
+ const issue = result.error.issues[0];
19
+ const schemaPath = issue ? formatZodPath(issue.path) : "";
20
+ const message = issue?.message ?? "invalid configuration";
21
+ const detail = schemaPath ? `${schemaPath} ${message}` : message;
22
+ throw new Error(`${filePath}: ${detail}`);
23
+ }
24
+ function defaultGitHubBrokerConfig() {
25
+ return {
26
+ serverName: DEFAULT_GITHUB_BROKER_SERVER_NAME,
27
+ toolAllowList: [...DEFAULT_GITHUB_BROKER_TOOLS]
28
+ };
29
+ }
30
+ export async function loadConfig(baseDir = managementRoot) {
31
+ const reposFile = path.join(baseDir, "repos.json");
32
+ const agentsFile = path.join(baseDir, "agents.json");
33
+ const reposConfig = parseConfig(ReposConfigFileSchema, await readJsonFile(reposFile), reposFile);
34
+ const agentsConfig = parseConfig(AgentsConfigFileSchema, await readJsonFile(agentsFile), agentsFile);
35
+ const prompts = await loadPromptsConfig(baseDir);
36
+ const mcps = await loadMcpsConfig(baseDir);
37
+ const capabilities = await loadCapabilitiesConfig(baseDir);
38
+ const maxConcurrentAgents = resolveMaxConcurrentAgents(agentsConfig.maxConcurrentAgents);
39
+ return {
40
+ allowedUsers: reposConfig.allowedUsers,
41
+ repos: reposConfig.repos,
42
+ agents: agentsConfig.agents,
43
+ codex: {
44
+ githubBroker: agentsConfig.codex?.githubBroker ?? defaultGitHubBrokerConfig()
45
+ },
46
+ gemini: {
47
+ githubBroker: agentsConfig.gemini?.githubBroker ?? defaultGitHubBrokerConfig()
48
+ },
49
+ claude: {
50
+ githubBroker: agentsConfig.claude?.githubBroker ?? defaultGitHubBrokerConfig()
51
+ },
52
+ opencode: {
53
+ githubBroker: agentsConfig.opencode?.githubBroker ?? defaultGitHubBrokerConfig()
54
+ },
55
+ antigravity: {
56
+ githubBroker: agentsConfig.antigravity?.githubBroker ?? defaultGitHubBrokerConfig()
57
+ },
58
+ ...(prompts ? { prompts } : {}),
59
+ mcps,
60
+ capabilities,
61
+ maxConcurrentAgents
62
+ };
63
+ }
64
+ const DEFAULT_MAX_CONCURRENT_AGENTS = 1;
65
+ function resolveMaxConcurrentAgents(fromConfig) {
66
+ const fromEnv = process.env["BACKBURNER_MAX_CONCURRENT_AGENTS"];
67
+ if (fromEnv !== undefined) {
68
+ const parsed = parseInt(fromEnv, 10);
69
+ if (!isNaN(parsed) && parsed >= 1) {
70
+ return parsed;
71
+ }
72
+ }
73
+ return fromConfig ?? DEFAULT_MAX_CONCURRENT_AGENTS;
74
+ }
75
+ export async function loadMcpsConfig(baseDir) {
76
+ const mcpsFile = path.join(baseDir, "mcps.json");
77
+ let raw;
78
+ try {
79
+ raw = await readJsonFile(mcpsFile);
80
+ }
81
+ catch (err) {
82
+ if (err instanceof Error && err.message.includes("file not found")) {
83
+ return [];
84
+ }
85
+ throw err;
86
+ }
87
+ const config = parseConfig(McpsConfigFileSchema, raw, mcpsFile);
88
+ return config.mcps.map((entry) => {
89
+ if (typeof entry === "string") {
90
+ return { handle: entry, smitheryId: entry };
91
+ }
92
+ return { handle: entry.as, smitheryId: entry.id };
93
+ });
94
+ }
95
+ export async function loadCapabilitiesConfig(baseDir) {
96
+ const capabilitiesFile = path.join(baseDir, "capabilities.json");
97
+ let raw;
98
+ try {
99
+ raw = await readJsonFile(capabilitiesFile);
100
+ }
101
+ catch (err) {
102
+ if (err instanceof Error && err.message.includes("file not found")) {
103
+ return { sensors: [], controllers: [] };
104
+ }
105
+ throw err;
106
+ }
107
+ const config = parseConfig(CapabilitiesConfigFileSchema, raw, capabilitiesFile);
108
+ const sensors = Object.entries(config.sensors).map(([name, sensor]) => ({
109
+ name,
110
+ uses: sensor.uses,
111
+ observes: sensor.observes,
112
+ ...(sensor.cadence !== undefined ? { cadence: sensor.cadence } : {}),
113
+ ...(sensor.scope !== undefined ? { scope: sensor.scope } : {})
114
+ }));
115
+ const controllers = Object.entries(config.controllers).map(([name, controller]) => ({
116
+ name,
117
+ uses: controller.uses,
118
+ grants: controller.grants,
119
+ approval: controller.approval
120
+ }));
121
+ return { sensors, controllers };
122
+ }
123
+ export async function loadPromptsConfig(baseDir) {
124
+ const promptsFile = path.join(baseDir, "prompts.json");
125
+ let raw;
126
+ try {
127
+ raw = await readJsonFile(promptsFile);
128
+ }
129
+ catch (err) {
130
+ if (err instanceof Error && err.message.includes("file not found")) {
131
+ return undefined;
132
+ }
133
+ throw err;
134
+ }
135
+ return parseConfig(PromptsConfigFileSchema, raw, promptsFile);
136
+ }
@@ -0,0 +1,403 @@
1
+ import * as path from "node:path";
2
+ import { z } from "zod";
3
+ export const DEFAULT_GITHUB_BROKER_SERVER_NAME = "githubBroker";
4
+ export const DEFAULT_GITHUB_BROKER_TOOLS = [
5
+ "get_issue",
6
+ "get_issue_comments",
7
+ "get_pr",
8
+ "get_pr_comments",
9
+ "get_pr_checks",
10
+ "search_code",
11
+ "post_issue_comment",
12
+ "post_pr_review_comment_reply",
13
+ "create_pr"
14
+ ];
15
+ // Helper for non-empty strings that matches existing manual validation (trim + length check)
16
+ const nonEmptyString = (message) => z.string({ message })
17
+ .trim()
18
+ .min(1, { message });
19
+ const optionalNonEmptyString = (message) => z.string({ message })
20
+ .trim()
21
+ .min(1, { message })
22
+ .optional();
23
+ const relativePath = (message, traversalMessage) => z.string({ message })
24
+ .trim()
25
+ .min(1, { message })
26
+ .refine((val) => !isAbsolutePathOnAnySupportedPlatform(val), { message })
27
+ .transform((val) => normalizeRelativePathForCurrentPlatform(val))
28
+ .refine((val) => val !== ".." && !val.startsWith(`..${path.sep}`), {
29
+ message: traversalMessage
30
+ });
31
+ function isAbsolutePathOnAnySupportedPlatform(value) {
32
+ return path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || value.startsWith("\\");
33
+ }
34
+ function normalizeRelativePathForCurrentPlatform(value) {
35
+ return path.normalize(value.replaceAll("\\", path.sep));
36
+ }
37
+ export const GitHubBrokerSchema = z.object({
38
+ serverName: nonEmptyString("must be a non-empty string").default(DEFAULT_GITHUB_BROKER_SERVER_NAME),
39
+ toolAllowList: z
40
+ .array(z.string().trim().min(1), {
41
+ message: "must be an array of strings"
42
+ })
43
+ .default([...DEFAULT_GITHUB_BROKER_TOOLS])
44
+ });
45
+ const ProviderBlockSchema = z.object({
46
+ githubBroker: GitHubBrokerSchema.optional()
47
+ });
48
+ export const RepoLabelsSchema = z.object({
49
+ agentInProgress: nonEmptyString("must be a non-empty string"),
50
+ agentPlanApproved: nonEmptyString("must be a non-empty string"),
51
+ agentProductApproved: optionalNonEmptyString("must be a non-empty string when provided").default("agent-product-approved"),
52
+ agentReview: nonEmptyString("must be a non-empty string"),
53
+ planBreakdownNeeded: optionalNonEmptyString("must be a non-empty string when provided").default("plan-breakdown-needed"),
54
+ agentSyncParentBranch: optionalNonEmptyString("must be a non-empty string when provided").default("agent-sync-parent-branch"),
55
+ prepareForMerge: optionalNonEmptyString("must be a non-empty string when provided").default("prepare-for-merge")
56
+ });
57
+ export const RepoConfigSchema = z.object({
58
+ id: nonEmptyString("must be a non-empty string"),
59
+ owner: nonEmptyString("must be a non-empty string"),
60
+ name: nonEmptyString("must be a non-empty string"),
61
+ defaultBranch: optionalNonEmptyString("must be a non-empty string when provided"),
62
+ localPath: relativePath("must be relative", "must stay within the provided code root"),
63
+ enabled: z.boolean({ message: "must be a boolean" }),
64
+ labels: RepoLabelsSchema
65
+ });
66
+ export const ReposConfigFileSchema = z.object({
67
+ allowedUsers: z.array(z.string().trim().min(1), {
68
+ message: '"allowedUsers" must be an array of strings'
69
+ }),
70
+ repos: z.array(RepoConfigSchema, { message: '"repos" must be an array' })
71
+ });
72
+ export const AgentProviderSchema = z.enum(["codex", "gemini", "claude", "opencode", "antigravity"], {
73
+ message: 'must be "codex", "gemini", "claude", "opencode" or "antigravity"'
74
+ });
75
+ export const ProviderLimitRecordSchema = z.object({
76
+ provider: AgentProviderSchema,
77
+ status: z.enum(["available", "limited"]),
78
+ limitType: z.enum(["session", "weekly"]).optional(),
79
+ rawError: z.string().optional(),
80
+ detectedAt: z.string().datetime().optional(),
81
+ resetTime: z.string().datetime().optional(),
82
+ updatedAt: z.string().datetime()
83
+ }).strict();
84
+ export const ProviderLimitStateFileSchema = z.object({
85
+ providerLimits: z.array(ProviderLimitRecordSchema)
86
+ }).strict();
87
+ const ModelClassSchema = z.enum(["strong", "cheap"], {
88
+ message: 'must be "strong" or "cheap"'
89
+ });
90
+ const TrimmedModelClassSchema = z
91
+ .string()
92
+ .trim()
93
+ .pipe(ModelClassSchema);
94
+ const BaseAgentConfigSchema = z.object({
95
+ id: nonEmptyString("must be a non-empty string"),
96
+ modelClass: TrimmedModelClassSchema,
97
+ roles: z.array(z.string().trim().min(1), {
98
+ message: "must be an array of strings"
99
+ }),
100
+ command: nonEmptyString("must be a non-empty string"),
101
+ enabled: z.boolean({ message: "must be a boolean" })
102
+ });
103
+ const CodexConfigSchema = BaseAgentConfigSchema.extend({
104
+ provider: z.literal("codex"),
105
+ model: optionalNonEmptyString("must be a non-empty string when provided")
106
+ });
107
+ const GeminiConfigSchema = BaseAgentConfigSchema.extend({
108
+ provider: z.literal("gemini"),
109
+ model: optionalNonEmptyString("must be a non-empty string when provided")
110
+ });
111
+ const ClaudeConfigSchema = BaseAgentConfigSchema.extend({
112
+ provider: z.literal("claude"),
113
+ model: optionalNonEmptyString("must be a non-empty string when provided")
114
+ });
115
+ const OpenCodeConfigSchema = BaseAgentConfigSchema.extend({
116
+ provider: z.literal("opencode"),
117
+ model: nonEmptyString("must be a non-empty string")
118
+ });
119
+ const AntigravityConfigSchema = BaseAgentConfigSchema.extend({
120
+ provider: z.literal("antigravity"),
121
+ model: optionalNonEmptyString("must be a non-empty string when provided")
122
+ });
123
+ const AgentConfigDiscriminatedSchema = z.discriminatedUnion("provider", [
124
+ CodexConfigSchema,
125
+ GeminiConfigSchema,
126
+ ClaudeConfigSchema,
127
+ OpenCodeConfigSchema,
128
+ AntigravityConfigSchema
129
+ ]);
130
+ export const AgentConfigSchema = z.preprocess((value) => {
131
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
132
+ return value;
133
+ }
134
+ const record = value;
135
+ if (typeof record.provider !== "string") {
136
+ return value;
137
+ }
138
+ return { ...record, provider: record.provider.trim() };
139
+ }, AgentConfigDiscriminatedSchema);
140
+ export const AgentsConfigFileSchema = z.object({
141
+ agents: z.array(AgentConfigSchema, { message: '"agents" must be an array' }),
142
+ codex: ProviderBlockSchema.optional(),
143
+ gemini: ProviderBlockSchema.optional(),
144
+ claude: ProviderBlockSchema.optional(),
145
+ opencode: ProviderBlockSchema.optional(),
146
+ antigravity: ProviderBlockSchema.optional(),
147
+ maxConcurrentAgents: z.number({ message: '"maxConcurrentAgents" must be a number' })
148
+ .int({ message: '"maxConcurrentAgents" must be an integer' })
149
+ .min(1, { message: '"maxConcurrentAgents" must be at least 1' })
150
+ .optional()
151
+ });
152
+ export const PromptProfileSchema = z.object({
153
+ plan: relativePath("must be relative", "must stay within the management directory").optional(),
154
+ implement: relativePath("must be relative", "must stay within the management directory").optional(),
155
+ review: relativePath("must be relative", "must stay within the management directory").optional(),
156
+ discuss: relativePath("must be relative", "must stay within the management directory").optional(),
157
+ product: relativePath("must be relative", "must stay within the management directory").optional(),
158
+ breakdown: relativePath("must be relative", "must stay within the management directory").optional(),
159
+ waveAssessment: relativePath("must be relative", "must stay within the management directory").optional(),
160
+ waveReplanning: relativePath("must be relative", "must stay within the management directory").optional(),
161
+ retrospectiveExtract: relativePath("must be relative", "must stay within the management directory").optional(),
162
+ retrospectiveInvestigate: relativePath("must be relative", "must stay within the management directory").optional(),
163
+ retrospectiveProposal: relativePath("must be relative", "must stay within the management directory").optional()
164
+ });
165
+ export const PromptsConfigFileSchema = z.object({
166
+ promptProfiles: z.object({
167
+ default: PromptProfileSchema
168
+ }, { message: '"promptProfiles" must be an object' })
169
+ });
170
+ const BANNED_MCP_FIELDS = [
171
+ "auth",
172
+ "token",
173
+ "key",
174
+ "secret",
175
+ "credential",
176
+ "credentials",
177
+ "password",
178
+ "apiKey",
179
+ "accessToken",
180
+ "provider",
181
+ "url"
182
+ ];
183
+ const BANNED_CAPABILITY_FIELDS = [
184
+ "auth",
185
+ "token",
186
+ "key",
187
+ "secret",
188
+ "credential",
189
+ "credentials",
190
+ "password",
191
+ "apiKey",
192
+ "accessToken",
193
+ "provider",
194
+ "url"
195
+ ];
196
+ const MCP_HANDLE_PATTERN = /^[A-Za-z0-9_-]+$/;
197
+ function validateMcpHandle(handle, ctx, path) {
198
+ if (!MCP_HANDLE_PATTERN.test(handle.trim())) {
199
+ ctx.addIssue({
200
+ code: z.ZodIssueCode.custom,
201
+ message: "must contain only letters, numbers, underscores, or hyphens",
202
+ ...(path ? { path } : {})
203
+ });
204
+ }
205
+ }
206
+ export const SensorObservesSchema = z.enum([
207
+ "issues-and-prs",
208
+ "package-docs",
209
+ "repository-context"
210
+ ]);
211
+ export const ControllerGrantsSchema = z.enum([
212
+ "comment-on-prs",
213
+ "comment-on-issues",
214
+ "open-issues",
215
+ "edit-issue-labels"
216
+ ]);
217
+ export const CapabilityApprovalSchema = z.enum([
218
+ "required",
219
+ "not-required",
220
+ "forbidden"
221
+ ]);
222
+ export const CapabilityScopeSchema = z.enum([
223
+ "repository",
224
+ "task-context",
225
+ "workstream"
226
+ ]);
227
+ export const CapabilityCadenceSchema = z.enum([
228
+ "every-orchestrator-run",
229
+ "on-demand"
230
+ ]);
231
+ const BackburnerSensorCapabilitySchema = z.object({
232
+ uses: nonEmptyString('sensor "uses" must be a non-empty string'),
233
+ observes: SensorObservesSchema,
234
+ cadence: CapabilityCadenceSchema.optional(),
235
+ scope: CapabilityScopeSchema.optional(),
236
+ // banned credential-like / provider / URL fields
237
+ auth: z.never({ message: 'banned field "auth"' }).optional(),
238
+ token: z.never({ message: 'banned field "token"' }).optional(),
239
+ key: z.never({ message: 'banned field "key"' }).optional(),
240
+ secret: z.never({ message: 'banned field "secret"' }).optional(),
241
+ credential: z.never({ message: 'banned field "credential"' }).optional(),
242
+ credentials: z.never({ message: 'banned field "credentials"' }).optional(),
243
+ password: z.never({ message: 'banned field "password"' }).optional(),
244
+ apiKey: z.never({ message: 'banned field "apiKey"' }).optional(),
245
+ accessToken: z.never({ message: 'banned field "accessToken"' }).optional(),
246
+ provider: z.never({ message: 'banned field "provider"' }).optional(),
247
+ url: z.never({ message: 'banned field "url"' }).optional(),
248
+ // cross-type field: sensors must not have grants
249
+ grants: z.never({ message: 'sensor capability must not include "grants"' }).optional(),
250
+ }).strict();
251
+ const BackburnerControllerCapabilitySchema = z.object({
252
+ uses: nonEmptyString('controller "uses" must be a non-empty string'),
253
+ grants: z.array(ControllerGrantsSchema, {
254
+ message: 'controller "grants" must be an array of valid grant values',
255
+ }).min(1, { message: 'controller "grants" must have at least one value' }),
256
+ approval: CapabilityApprovalSchema.default("required"),
257
+ // banned credential-like / provider / URL fields
258
+ auth: z.never({ message: 'banned field "auth"' }).optional(),
259
+ token: z.never({ message: 'banned field "token"' }).optional(),
260
+ key: z.never({ message: 'banned field "key"' }).optional(),
261
+ secret: z.never({ message: 'banned field "secret"' }).optional(),
262
+ credential: z.never({ message: 'banned field "credential"' }).optional(),
263
+ credentials: z.never({ message: 'banned field "credentials"' }).optional(),
264
+ password: z.never({ message: 'banned field "password"' }).optional(),
265
+ apiKey: z.never({ message: 'banned field "apiKey"' }).optional(),
266
+ accessToken: z.never({ message: 'banned field "accessToken"' }).optional(),
267
+ provider: z.never({ message: 'banned field "provider"' }).optional(),
268
+ url: z.never({ message: 'banned field "url"' }).optional(),
269
+ // cross-type field: controllers must not have observes
270
+ observes: z.never({ message: 'controller capability must not include "observes"' }).optional(),
271
+ }).strict();
272
+ export const CapabilitiesConfigFileSchema = z
273
+ .object({
274
+ sensors: z
275
+ .record(z.string(), BackburnerSensorCapabilitySchema)
276
+ .optional()
277
+ .default({}),
278
+ controllers: z
279
+ .record(z.string(), BackburnerControllerCapabilitySchema)
280
+ .optional()
281
+ .default({}),
282
+ })
283
+ .strict()
284
+ .superRefine((data, ctx) => {
285
+ for (const name of Object.keys(data.sensors)) {
286
+ if (name.trim().length === 0) {
287
+ ctx.addIssue({
288
+ code: z.ZodIssueCode.custom,
289
+ message: "capability name must be a non-empty string",
290
+ path: ["sensors", name],
291
+ });
292
+ }
293
+ }
294
+ for (const name of Object.keys(data.controllers)) {
295
+ if (name.trim().length === 0) {
296
+ ctx.addIssue({
297
+ code: z.ZodIssueCode.custom,
298
+ message: "capability name must be a non-empty string",
299
+ path: ["controllers", name],
300
+ });
301
+ }
302
+ }
303
+ const sensorNames = new Set(Object.keys(data.sensors));
304
+ for (const name of Object.keys(data.controllers)) {
305
+ if (sensorNames.has(name)) {
306
+ ctx.addIssue({
307
+ code: z.ZodIssueCode.custom,
308
+ message: `duplicate capability name "${name}" across sensors and controllers`,
309
+ path: ["controllers", name],
310
+ });
311
+ }
312
+ }
313
+ });
314
+ export const McpsConfigFileSchema = z
315
+ .object({
316
+ mcps: z.array(z.any().superRefine((val, ctx) => {
317
+ if (typeof val === "string") {
318
+ if (val.trim().length === 0) {
319
+ ctx.addIssue({
320
+ code: z.ZodIssueCode.custom,
321
+ message: 'must be a non-empty string or an object with "as" and "id"'
322
+ });
323
+ }
324
+ else {
325
+ validateMcpHandle(val, ctx);
326
+ }
327
+ return;
328
+ }
329
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
330
+ const data = val;
331
+ let hasCriticalError = false;
332
+ if (typeof data.as !== "string" || data.as.trim().length === 0) {
333
+ ctx.addIssue({
334
+ code: z.ZodIssueCode.custom,
335
+ message: "must be a non-empty string",
336
+ path: ["as"]
337
+ });
338
+ hasCriticalError = true;
339
+ }
340
+ else {
341
+ validateMcpHandle(data.as, ctx, ["as"]);
342
+ }
343
+ if (typeof data.id !== "string" || data.id.trim().length === 0) {
344
+ ctx.addIssue({
345
+ code: z.ZodIssueCode.custom,
346
+ message: "must be a non-empty string",
347
+ path: ["id"]
348
+ });
349
+ hasCriticalError = true;
350
+ }
351
+ if (hasCriticalError)
352
+ return;
353
+ for (const key of Object.keys(data)) {
354
+ if (key !== "as" && key !== "id") {
355
+ if (BANNED_MCP_FIELDS.includes(key)) {
356
+ ctx.addIssue({
357
+ code: z.ZodIssueCode.custom,
358
+ message: `has unsupported field "${key}"`,
359
+ path: [key]
360
+ });
361
+ }
362
+ else {
363
+ ctx.addIssue({
364
+ code: z.ZodIssueCode.custom,
365
+ message: `has unknown field "${key}"`,
366
+ path: [key]
367
+ });
368
+ }
369
+ }
370
+ }
371
+ return;
372
+ }
373
+ ctx.addIssue({
374
+ code: z.ZodIssueCode.custom,
375
+ message: 'must be a non-empty string or an object with "as" and "id"'
376
+ });
377
+ }), { message: '"mcps" must be an array' })
378
+ })
379
+ .strict()
380
+ .superRefine((data, ctx) => {
381
+ const handles = new Set();
382
+ data.mcps.forEach((entry, index) => {
383
+ let handle;
384
+ if (typeof entry === "string") {
385
+ handle = entry;
386
+ }
387
+ else if (typeof entry === "object" &&
388
+ entry !== null &&
389
+ !Array.isArray(entry)) {
390
+ handle = entry.as;
391
+ }
392
+ if (typeof handle === "string" && handle.trim().length > 0) {
393
+ if (handles.has(handle)) {
394
+ ctx.addIssue({
395
+ code: z.ZodIssueCode.custom,
396
+ message: `duplicate MCP handle "${handle}"`,
397
+ path: ["mcps", index]
398
+ });
399
+ }
400
+ handles.add(handle);
401
+ }
402
+ });
403
+ });
@@ -0,0 +1 @@
1
+ export {};