@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,164 @@
1
+ import { createHash } from "node:crypto";
2
+ import { SmitherySdkClient } from "./smithery-client.js";
3
+ export function deriveSmitheryConnectionId(handle, smitheryId) {
4
+ const hash = createHash("sha256")
5
+ .update(`${handle}|${smitheryId}`)
6
+ .digest("hex")
7
+ .slice(0, 8);
8
+ const sanitizedHandle = handle.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 20);
9
+ return `backburner-${sanitizedHandle}-${hash}`;
10
+ }
11
+ function isOwnedConnection(connection, handle, smitheryId, localConnectionIdentity) {
12
+ const meta = connection.metadata;
13
+ return (meta?.backburner === "backburner" &&
14
+ meta.backburnerHandle === handle &&
15
+ meta.providerMcpId === smitheryId &&
16
+ meta.localConnectionIdentity === localConnectionIdentity);
17
+ }
18
+ export class SmitheryMcpResolver {
19
+ client;
20
+ runtimeConfig;
21
+ constructor(client, runtimeConfig) {
22
+ this.client = client;
23
+ this.runtimeConfig = runtimeConfig;
24
+ }
25
+ async resolveDependencies(dependencies, previous = []) {
26
+ const previousByHandle = new Map(previous.map((r) => [r.handle, r]));
27
+ return Promise.all(dependencies.map((dependency) => this.resolveOne(dependency, previousByHandle.get(dependency.handle))));
28
+ }
29
+ async resolveOne(dependency, prev) {
30
+ const result = {
31
+ handle: dependency.handle,
32
+ provider: "smithery",
33
+ providerMcpId: dependency.smitheryId,
34
+ status: "error", // Default to error
35
+ };
36
+ if (!this.runtimeConfig.apiKey) {
37
+ result.status = "error";
38
+ result.message = "Missing Smithery API key. Set SMITHERY_API_KEY to resolve Smithery MCP dependencies.";
39
+ return result;
40
+ }
41
+ if (!this.runtimeConfig.namespace) {
42
+ result.status = "error";
43
+ result.message = "Missing Smithery namespace. Set SMITHERY_NAMESPACE to resolve Smithery MCP dependencies.";
44
+ return result;
45
+ }
46
+ try {
47
+ // 1. Verify server exists
48
+ const lookup = await this.client.getServerByExactId(dependency.smitheryId);
49
+ if (lookup.status === "not_found") {
50
+ result.status = "resolution_failed";
51
+ result.message = lookup.message || `Smithery server "${dependency.smitheryId}" not found.`;
52
+ return result;
53
+ }
54
+ if (lookup.status === "error") {
55
+ result.status = "error";
56
+ result.message = lookup.message || "Unknown Smithery error during server lookup.";
57
+ return result;
58
+ }
59
+ if (lookup.status === "action_required") {
60
+ result.status = "action_required";
61
+ result.actionRequired = lookup.actionRequired;
62
+ result.message = lookup.message;
63
+ return result;
64
+ }
65
+ result.discovered = lookup.server;
66
+ if (!lookup.connection) {
67
+ result.status = "resolved";
68
+ result.message = "Server lookup succeeded but no usable hosted HTTP connection was found.";
69
+ return result;
70
+ }
71
+ // 2. Manage connection
72
+ const namespace = this.runtimeConfig.namespace;
73
+ const localConnectionIdentity = deriveSmitheryConnectionId(dependency.handle, dependency.smitheryId);
74
+ const candidateIds = [];
75
+ if (prev?.connection?.localConnectionIdentity === localConnectionIdentity && prev.connection.providerConnectionId) {
76
+ candidateIds.push(prev.connection.providerConnectionId);
77
+ }
78
+ if (!candidateIds.includes(localConnectionIdentity)) {
79
+ candidateIds.push(localConnectionIdentity);
80
+ }
81
+ let connection;
82
+ for (const candidateId of candidateIds) {
83
+ const candidate = await this.client.getConnection(namespace, candidateId);
84
+ if (candidate.status === "not_found") {
85
+ continue;
86
+ }
87
+ if (candidate.status === "connected" ||
88
+ candidate.status === "auth_required" ||
89
+ candidate.status === "input_required" ||
90
+ candidate.status === "disconnected") {
91
+ if (!isOwnedConnection(candidate, dependency.handle, dependency.smitheryId, localConnectionIdentity)) {
92
+ continue;
93
+ }
94
+ }
95
+ connection = candidate;
96
+ break;
97
+ }
98
+ if (!connection || connection.status === "disconnected") {
99
+ connection = await this.client.createOrUpdateConnection({
100
+ namespace,
101
+ connectionId: localConnectionIdentity,
102
+ smitheryId: dependency.smitheryId,
103
+ handle: dependency.handle,
104
+ transport: lookup.connection?.transport,
105
+ mcpUrl: lookup.connection?.mcpUrl,
106
+ });
107
+ }
108
+ if (connection.status === "connected" &&
109
+ !isOwnedConnection(connection, dependency.handle, dependency.smitheryId, localConnectionIdentity)) {
110
+ result.status = "error";
111
+ result.message = `Smithery connection "${localConnectionIdentity}" is connected but its metadata does not match Backburner handle "${dependency.handle}" and Smithery id "${dependency.smitheryId}"; refusing to mark ready.`;
112
+ return result;
113
+ }
114
+ result.connection = {
115
+ localConnectionIdentity,
116
+ readiness: connection.status,
117
+ ...(connection.connectionId ? { providerConnectionId: connection.connectionId } : {}),
118
+ namespace: connection.namespace || namespace,
119
+ ...(connection.transport ? { transport: connection.transport } : {}),
120
+ ...(connection.mcpUrl ? { mcpUrl: connection.mcpUrl } : {}),
121
+ ...(connection.message ? { message: connection.message } : {}),
122
+ };
123
+ switch (connection.status) {
124
+ case "connected":
125
+ result.status = "ready";
126
+ result.message = connection.message || `Smithery connection is ready for "${dependency.handle}".`;
127
+ break;
128
+ case "auth_required":
129
+ case "input_required":
130
+ result.status = "action_required";
131
+ result.actionRequired = {
132
+ reason: connection.status === "auth_required" ? "auth_required" : "input_required",
133
+ message: connection.message || `Smithery connection for "${dependency.handle}" requires user action.`,
134
+ ...(connection.setupUrl ? { url: connection.setupUrl } : {}),
135
+ ...(connection.fields ? { fields: connection.fields } : {}),
136
+ };
137
+ break;
138
+ case "disconnected":
139
+ result.status = "resolved";
140
+ result.message = connection.message || `Smithery connection for "${dependency.handle}" exists but is disconnected.`;
141
+ break;
142
+ case "error":
143
+ result.status = "error";
144
+ result.message = connection.message || "Unknown Smithery connection error.";
145
+ break;
146
+ default:
147
+ result.status = "error";
148
+ result.message = `Unexpected Smithery connection status: ${connection.status}`;
149
+ }
150
+ }
151
+ catch (err) {
152
+ result.status = "error";
153
+ result.message = err instanceof Error ? err.message : String(err);
154
+ }
155
+ return result;
156
+ }
157
+ }
158
+ export function createSmitheryMcpResolverFromEnv(env = process.env) {
159
+ const apiKey = env.SMITHERY_API_KEY;
160
+ const namespace = env.SMITHERY_NAMESPACE;
161
+ const config = { apiKey, namespace };
162
+ const client = new SmitherySdkClient(config);
163
+ return new SmitheryMcpResolver(client, config);
164
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readJsonFileIfExists, writeJsonFileAtomic } from "../utils/json.js";
3
+ export class JsonJobMemoryStore {
4
+ filePath;
5
+ generateId;
6
+ now;
7
+ constructor(options) {
8
+ this.filePath = options.filePath;
9
+ this.generateId = options.generateId ?? (() => randomUUID());
10
+ this.now = options.now ?? (() => new Date().toISOString());
11
+ }
12
+ async add(scope, bankId, taskId, input) {
13
+ const state = await this.load();
14
+ const now = this.now();
15
+ const entry = {
16
+ id: this.generateId(),
17
+ bankId,
18
+ scope,
19
+ type: input.type,
20
+ text: input.text,
21
+ source: {
22
+ taskId,
23
+ ...input.source
24
+ },
25
+ createdAt: now,
26
+ updatedAt: now
27
+ };
28
+ if (input.confidence !== undefined) {
29
+ entry.confidence = input.confidence;
30
+ }
31
+ state.entries.push(entry);
32
+ // Sort entries deterministically by bankId and createdAt
33
+ state.entries.sort((a, b) => {
34
+ if (a.bankId !== b.bankId) {
35
+ return a.bankId.localeCompare(b.bankId);
36
+ }
37
+ return a.createdAt.localeCompare(b.createdAt);
38
+ });
39
+ await this.save(state);
40
+ return entry;
41
+ }
42
+ async search(bankId, input) {
43
+ const state = await this.load();
44
+ const query = input.query?.toLowerCase();
45
+ const results = state.entries.filter((entry) => {
46
+ if (entry.bankId !== bankId) {
47
+ return false;
48
+ }
49
+ if (input.type && entry.type !== input.type) {
50
+ return false;
51
+ }
52
+ if (query && !entry.text.toLowerCase().includes(query)) {
53
+ return false;
54
+ }
55
+ return true;
56
+ });
57
+ // Sort by recency (newest first) for search results
58
+ results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
59
+ if (input.limit !== undefined) {
60
+ return results.slice(0, input.limit);
61
+ }
62
+ return results;
63
+ }
64
+ async load() {
65
+ const data = await readJsonFileIfExists(this.filePath);
66
+ return data ?? { entries: [] };
67
+ }
68
+ async save(state) {
69
+ await writeJsonFileAtomic(this.filePath, state);
70
+ }
71
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Builds a compact memory prelude for inclusion in an agent prompt.
3
+ * Returns an empty string if there are no entries.
4
+ */
5
+ export function buildMemoryPrelude(entries) {
6
+ if (entries.length === 0) {
7
+ return "";
8
+ }
9
+ const lines = [
10
+ "Durable memory background for this scoped job:",
11
+ ...entries.map((entry) => `- [${entry.type}] ${entry.text}`),
12
+ "",
13
+ "Memory is prior background, not current truth. Current GitHub and repository state are authoritative; verify conflicts before acting."
14
+ ];
15
+ return lines.join("\n");
16
+ }
@@ -0,0 +1,31 @@
1
+ import { workflowRegistry } from "../workflows/registry.js";
2
+ import { roleForTask } from "../agents/selection.js";
3
+ /**
4
+ * Resolves the memory scope for a given task and agent configuration.
5
+ * Fails if workstreamId is missing.
6
+ */
7
+ export function resolveJobMemoryScope(task, agentConfig) {
8
+ if (!task.workstreamId) {
9
+ throw new Error(`Memory access requires a workstreamId for task ${task.id}`);
10
+ }
11
+ const definition = workflowRegistry.getDefinition(task.kind);
12
+ const job = roleForTask(task);
13
+ const modelClass = agentConfig.modelClass;
14
+ // Assert that the selected agent's model class matches the workflow definition
15
+ // This is a safety check from the plan.
16
+ if (modelClass !== definition.modelClass) {
17
+ throw new Error(`Model class mismatch for task ${task.id}: agent model class is ${modelClass}, but workflow definition requires ${definition.modelClass}`);
18
+ }
19
+ return {
20
+ repoKey: task.repoSlug,
21
+ workstreamId: task.workstreamId,
22
+ job,
23
+ modelClass
24
+ };
25
+ }
26
+ /**
27
+ * Generates the canonical bank ID for a given scope.
28
+ */
29
+ export function getJobMemoryBankId(scope) {
30
+ return `backburner:repo:${scope.repoKey}:workstream:${scope.workstreamId}:job:${scope.job}:model:${scope.modelClass}`;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,142 @@
1
+ import { JOB_MEMORY_ENTRY_TYPES } from "./types.js";
2
+ export class JobMemoryToolService {
3
+ store;
4
+ logger;
5
+ activeBinding;
6
+ constructor(store, logger) {
7
+ this.store = store;
8
+ this.logger = logger;
9
+ }
10
+ /**
11
+ * Binds the service to a specific job scope and task for the duration of a dispatch.
12
+ */
13
+ bind(scope, bankId, taskId) {
14
+ this.activeBinding = { scope, bankId, taskId };
15
+ }
16
+ /**
17
+ * Clears the current job scope binding.
18
+ */
19
+ unbind() {
20
+ this.activeBinding = undefined;
21
+ }
22
+ listTools() {
23
+ return [
24
+ {
25
+ name: "memory_add",
26
+ title: "Add to Job Memory",
27
+ description: "Adds a concise, typed fact, decision, or risk to the current scoped job memory. Do not store secrets or raw logs.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ type: {
32
+ type: "string",
33
+ enum: [...JOB_MEMORY_ENTRY_TYPES]
34
+ },
35
+ text: { type: "string", minLength: 1, maxLength: 2000 },
36
+ confidence: { type: "string", enum: ["low", "medium", "high"] },
37
+ source: {
38
+ type: "object",
39
+ properties: {
40
+ commitSha: { type: "string" },
41
+ filePath: { type: "string" }
42
+ }
43
+ }
44
+ },
45
+ required: ["type", "text"]
46
+ }
47
+ },
48
+ {
49
+ name: "memory_search",
50
+ title: "Search Job Memory",
51
+ description: "Searches for relevant background facts, decisions, or risks in the current scoped job memory.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ query: { type: "string" },
56
+ type: {
57
+ type: "string",
58
+ enum: [...JOB_MEMORY_ENTRY_TYPES]
59
+ },
60
+ limit: { type: "number", minimum: 1, maximum: 50 }
61
+ }
62
+ }
63
+ }
64
+ ];
65
+ }
66
+ async callTool(name, rawArguments) {
67
+ const timestamp = new Date().toISOString();
68
+ if (!this.activeBinding) {
69
+ this.logger.warn(`[${timestamp}] [job-memory-tool-service] DENIED call=${name} status=unbound`);
70
+ return {
71
+ ok: false,
72
+ tool: name,
73
+ error: {
74
+ code: "memory_scope_unbound",
75
+ message: "Memory tools are only available when a task is actively being dispatched."
76
+ }
77
+ };
78
+ }
79
+ const { scope, bankId, taskId } = this.activeBinding;
80
+ try {
81
+ if (name === "memory_add") {
82
+ const input = rawArguments;
83
+ // Runtime validation
84
+ if (!input.type || !JOB_MEMORY_ENTRY_TYPES.includes(input.type)) {
85
+ throw new Error(`Invalid memory type: ${input.type}. Must be one of: ${JOB_MEMORY_ENTRY_TYPES.join(", ")}`);
86
+ }
87
+ if (!input.text || input.text.length === 0) {
88
+ throw new Error("Memory text is required.");
89
+ }
90
+ if (input.text.length > 2000) {
91
+ throw new Error("Memory text exceeds maximum length of 2000 characters.");
92
+ }
93
+ const entry = await this.store.add(scope, bankId, taskId, input);
94
+ this.logger.info(`[${timestamp}] [job-memory-tool-service] call=memory_add bank=${bankId} task=${taskId} status=success entryId=${entry.id}`);
95
+ return {
96
+ ok: true,
97
+ tool: name,
98
+ result: { entryId: entry.id },
99
+ summary: `Added ${input.type} memory entry: ${entry.id}`
100
+ };
101
+ }
102
+ if (name === "memory_search") {
103
+ const input = rawArguments;
104
+ // Runtime validation for type if provided
105
+ if (input.type && !JOB_MEMORY_ENTRY_TYPES.includes(input.type)) {
106
+ throw new Error(`Invalid search type: ${input.type}. Must be one of: ${JOB_MEMORY_ENTRY_TYPES.join(", ")}`);
107
+ }
108
+ const entries = await this.store.search(bankId, input);
109
+ this.logger.info(`[${timestamp}] [job-memory-tool-service] call=memory_search bank=${bankId} task=${taskId} status=success results=${entries.length}`);
110
+ return {
111
+ ok: true,
112
+ tool: name,
113
+ result: {
114
+ entries,
115
+ note: "Memory is prior background, not current truth. Current GitHub and repository state are authoritative."
116
+ },
117
+ summary: `Found ${entries.length} memory entries in this bank.`
118
+ };
119
+ }
120
+ return {
121
+ ok: false,
122
+ tool: name,
123
+ error: {
124
+ code: "tool_not_found",
125
+ message: `Tool "${name}" not found in memory service.`
126
+ }
127
+ };
128
+ }
129
+ catch (error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ this.logger.warn(`[${timestamp}] [job-memory-tool-service] FAILED call=${name} bank=${bankId} task=${taskId} error=${message}`);
132
+ return {
133
+ ok: false,
134
+ tool: name,
135
+ error: {
136
+ code: "memory_error",
137
+ message
138
+ }
139
+ };
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,12 @@
1
+ export const JOB_MEMORY_ENTRY_TYPES = [
2
+ "decision",
3
+ "assumption",
4
+ "fact",
5
+ "risk",
6
+ "open_question",
7
+ "file_note",
8
+ "review_note",
9
+ "implementation_note",
10
+ "human_preference",
11
+ "other"
12
+ ];
@@ -0,0 +1,96 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { writeJsonFileAtomic } from "../utils/json.js";
4
+ export class ConfigWriter {
5
+ async detectExisting(managementDir) {
6
+ const reposFile = path.join(managementDir, "repos.json");
7
+ const agentsFile = path.join(managementDir, "agents.json");
8
+ const [reposExists, agentsExists] = await Promise.all([
9
+ fileExists(reposFile),
10
+ fileExists(agentsFile)
11
+ ]);
12
+ return { reposExists, agentsExists, reposFile, agentsFile };
13
+ }
14
+ /**
15
+ * Write repos.json and agents.json into managementDir.
16
+ *
17
+ * @param overwriteRepos When false, skips repos.json if it already exists.
18
+ * @param overwriteAgents When false, skips agents.json if it already exists.
19
+ */
20
+ async write(draft, options = {}) {
21
+ const { managementDir } = draft;
22
+ await fs.mkdir(managementDir, { recursive: true });
23
+ const reposFile = path.join(managementDir, "repos.json");
24
+ const agentsFile = path.join(managementDir, "agents.json");
25
+ const skippedExisting = [];
26
+ const reposAlreadyExists = await fileExists(reposFile);
27
+ if (!reposAlreadyExists || options.overwriteRepos) {
28
+ await writeJsonFileAtomic(reposFile, buildReposJson(draft));
29
+ }
30
+ else {
31
+ skippedExisting.push(reposFile);
32
+ }
33
+ const agentsAlreadyExists = await fileExists(agentsFile);
34
+ if (!agentsAlreadyExists || options.overwriteAgents) {
35
+ await writeJsonFileAtomic(agentsFile, buildAgentsJson(draft));
36
+ }
37
+ else {
38
+ skippedExisting.push(agentsFile);
39
+ }
40
+ return { reposFile, agentsFile, skippedExisting };
41
+ }
42
+ }
43
+ function buildReposJson(draft) {
44
+ return {
45
+ allowedUsers: draft.allowedUsers,
46
+ repos: draft.repos.map(buildRepoEntry)
47
+ };
48
+ }
49
+ function buildRepoEntry(repo) {
50
+ const id = `${repo.owner}-${repo.name}`;
51
+ return {
52
+ id,
53
+ owner: repo.owner,
54
+ name: repo.name,
55
+ defaultBranch: repo.defaultBranch,
56
+ localPath: repo.localPath,
57
+ enabled: repo.enabled,
58
+ labels: {
59
+ agentInProgress: "agent-in-progress",
60
+ agentPlanApproved: "agent-plan-approved",
61
+ agentProductApproved: "agent-product-approved",
62
+ agentReview: "agent-review",
63
+ planBreakdownNeeded: "plan-breakdown-needed",
64
+ agentSyncParentBranch: "agent-sync-parent-branch",
65
+ prepareForMerge: "prepare-for-merge"
66
+ }
67
+ };
68
+ }
69
+ function buildAgentsJson(draft) {
70
+ return {
71
+ agents: draft.agents.map(buildAgentEntry)
72
+ };
73
+ }
74
+ function buildAgentEntry(agent) {
75
+ const base = {
76
+ id: agent.id,
77
+ provider: agent.provider,
78
+ command: agent.command,
79
+ modelClass: agent.modelClass,
80
+ roles: agent.roles,
81
+ enabled: agent.enabled
82
+ };
83
+ if (agent.model !== undefined) {
84
+ base["model"] = agent.model;
85
+ }
86
+ return base;
87
+ }
88
+ async function fileExists(filePath) {
89
+ try {
90
+ await fs.access(filePath, fs.constants.F_OK);
91
+ return true;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
@@ -0,0 +1,86 @@
1
+ export class OnboardingEngine {
2
+ steps;
3
+ prompter;
4
+ options;
5
+ constructor(steps, prompter, options = {}) {
6
+ this.steps = steps;
7
+ this.prompter = prompter;
8
+ this.options = options;
9
+ }
10
+ async run(ctx) {
11
+ const results = [];
12
+ for (const step of this.steps) {
13
+ process.stdout.write(`\n--- ${step.description} ---\n`);
14
+ let result;
15
+ try {
16
+ result = await step.run(ctx, this.prompter);
17
+ }
18
+ catch (err) {
19
+ result = {
20
+ status: "failed",
21
+ message: err instanceof Error ? err.message : String(err),
22
+ issues: [
23
+ {
24
+ title: "Unexpected error in onboarding step",
25
+ cause: err instanceof Error ? err.message : String(err),
26
+ suggestedFix: "Check the error message above and retry",
27
+ canAutoRemediate: false
28
+ }
29
+ ]
30
+ };
31
+ }
32
+ ctx.stepResults.set(step.name, result);
33
+ results.push({ stepName: step.name, result });
34
+ const icon = statusIcon(result.status);
35
+ process.stdout.write(`${icon} ${result.message}\n`);
36
+ if (result.details) {
37
+ for (const detail of result.details) {
38
+ process.stdout.write(` ${detail}\n`);
39
+ }
40
+ }
41
+ if (result.issues) {
42
+ for (const issue of result.issues) {
43
+ process.stdout.write(`\n Issue: ${issue.title}\n`);
44
+ process.stdout.write(` Cause: ${issue.cause}\n`);
45
+ process.stdout.write(` Fix: ${issue.suggestedFix}\n`);
46
+ if (issue.manualSteps) {
47
+ process.stdout.write(" Steps:\n");
48
+ for (const s of issue.manualSteps) {
49
+ process.stdout.write(` - ${s}\n`);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ if (result.status === "failed" && this.options.stopOnFailure) {
55
+ break;
56
+ }
57
+ }
58
+ const overallStatus = deriveOverallStatus(results.map((r) => r.result));
59
+ return { context: ctx, results, overallStatus };
60
+ }
61
+ }
62
+ function statusIcon(status) {
63
+ switch (status) {
64
+ case "passed":
65
+ return "✓";
66
+ case "warning":
67
+ return "⚠";
68
+ case "failed":
69
+ return "✗";
70
+ case "skipped":
71
+ return "-";
72
+ case "requiresUserAction":
73
+ return "!";
74
+ }
75
+ }
76
+ export function deriveOverallStatus(results) {
77
+ if (results.some((r) => r.status === "failed"))
78
+ return "failed";
79
+ if (results.some((r) => r.status === "requiresUserAction"))
80
+ return "requiresUserAction";
81
+ if (results.some((r) => r.status === "warning"))
82
+ return "warning";
83
+ if (results.every((r) => r.status === "skipped"))
84
+ return "skipped";
85
+ return "passed";
86
+ }