@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,380 @@
1
+ import * as path from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { resolveScopePath } from "./scopes.js";
4
+ import { parseGitHubRemoteUrl } from "../github/utils.js";
5
+ export async function materializeManagementLearningProposals(input) {
6
+ const { proposals, managementPrs: existingManagementPrs, candidates, problems, opportunities, managementRoot, gitGateway, githubGateway, commandRunner, now, } = input;
7
+ const updatedProposals = [...proposals];
8
+ const updatedManagementPrs = [...existingManagementPrs];
9
+ for (let i = 0; i < updatedProposals.length; i++) {
10
+ const proposal = updatedProposals[i];
11
+ if (proposal.status !== "validated" || proposal.validationStatus !== "passed") {
12
+ continue;
13
+ }
14
+ // Idempotence check
15
+ const existingPrRecord = updatedManagementPrs.find((pr) => pr.proposalId === proposal.id);
16
+ if (existingPrRecord) {
17
+ if (proposal.status === "validated") {
18
+ updatedProposals[i] = {
19
+ ...proposal,
20
+ status: existingPrRecord.mode === "audit_only" ? "audit_only" : "materialized",
21
+ materializationStatus: "completed",
22
+ updatedAt: now,
23
+ };
24
+ }
25
+ continue;
26
+ }
27
+ const materializationResult = await materializeSingleProposal({
28
+ proposal,
29
+ candidates,
30
+ problems,
31
+ opportunities,
32
+ managementRoot,
33
+ gitGateway,
34
+ githubGateway,
35
+ commandRunner,
36
+ now,
37
+ });
38
+ updatedProposals[i] = materializationResult.proposal;
39
+ if (materializationResult.managementPr) {
40
+ updatedManagementPrs.push(materializationResult.managementPr);
41
+ }
42
+ }
43
+ return {
44
+ proposals: updatedProposals,
45
+ managementPrs: updatedManagementPrs,
46
+ };
47
+ }
48
+ async function materializeSingleProposal(input) {
49
+ const { proposal, candidates, problems, opportunities, managementRoot, gitGateway, githubGateway, commandRunner, now, } = input;
50
+ const auditOnly = (reason) => {
51
+ const record = {
52
+ id: `mgmt-pr-audit-${proposal.id}`,
53
+ proposalId: proposal.id,
54
+ mode: "audit_only",
55
+ status: "audit_only",
56
+ reason,
57
+ changedArtifactPaths: proposal.resolvedArtifactPath ? [proposal.resolvedArtifactPath] : [],
58
+ detectedAt: now,
59
+ updatedAt: now,
60
+ };
61
+ return {
62
+ proposal: {
63
+ ...proposal,
64
+ status: "audit_only",
65
+ materializationStatus: "completed",
66
+ updatedAt: now,
67
+ },
68
+ managementPr: record,
69
+ };
70
+ };
71
+ // 1. Safety Checks
72
+ let originalBranch = null;
73
+ try {
74
+ if (!(await gitGateway.isInsideWorkingTree(managementRoot))) {
75
+ return auditOnly("Management root is not inside a git working tree");
76
+ }
77
+ if ((await gitGateway.getActiveMergeState(managementRoot)) !== "none") {
78
+ return auditOnly("Management repository has active merge/rebase/revert state");
79
+ }
80
+ if (!(await gitGateway.isWorkingTreeClean(managementRoot))) {
81
+ return auditOnly("Management repository working tree is dirty");
82
+ }
83
+ originalBranch = await gitGateway.getCurrentBranch(managementRoot);
84
+ }
85
+ catch (err) {
86
+ return auditOnly(`Failed to check management repo git state: ${err.message}`);
87
+ }
88
+ // 2. Resolve Remote and Repository Identity
89
+ let remoteUrl;
90
+ try {
91
+ const result = await commandRunner.run("git", ["remote", "get-url", "origin"], { cwd: managementRoot });
92
+ remoteUrl = result.stdout.trim();
93
+ }
94
+ catch (err) {
95
+ return auditOnly(`Failed to resolve management repository remote URL: ${err.message}`);
96
+ }
97
+ const repoIdentity = parseGitHubRemoteUrl(remoteUrl);
98
+ if (!repoIdentity) {
99
+ return auditOnly(`Unsupported or unparsable management repository remote URL: ${remoteUrl}`);
100
+ }
101
+ // 3. Resolve Base Branch
102
+ let baseBranch;
103
+ try {
104
+ baseBranch = await gitGateway.getRemoteDefaultBranch(managementRoot);
105
+ }
106
+ catch (err) {
107
+ try {
108
+ const result = await commandRunner.run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: managementRoot });
109
+ baseBranch = result.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
110
+ }
111
+ catch {
112
+ return auditOnly("Could not safely resolve management repository default branch");
113
+ }
114
+ }
115
+ // 4. Deterministic Branch Name
116
+ const branchName = buildManagementLearningBranchName(proposal);
117
+ // 5. Apply Edits
118
+ if (!proposal.resolvedArtifactPath || !proposal.selectedScope) {
119
+ return auditOnly("Proposal is missing resolved artifact path or selected scope");
120
+ }
121
+ const learningsRoot = path.resolve(managementRoot, "learnings");
122
+ const normalizedPath = path.resolve(proposal.resolvedArtifactPath);
123
+ if (!normalizedPath.startsWith(learningsRoot + path.sep) && normalizedPath !== learningsRoot) {
124
+ return auditOnly(`Resolved artifact path "${normalizedPath}" is outside learnings root "${learningsRoot}"`);
125
+ }
126
+ const expectedPath = resolveScopePath(proposal.selectedScope, managementRoot);
127
+ if (normalizedPath !== expectedPath) {
128
+ return auditOnly(`Resolved artifact path "${normalizedPath}" does not match scope-to-path resolution "${expectedPath}"`);
129
+ }
130
+ // Checkout branch (reuse existing or create new)
131
+ try {
132
+ await commandRunner.run("git", ["checkout", branchName], { cwd: managementRoot });
133
+ }
134
+ catch {
135
+ try {
136
+ await commandRunner.run("git", ["checkout", "-b", branchName, `origin/${baseBranch}`], { cwd: managementRoot });
137
+ }
138
+ catch (err) {
139
+ return auditOnly(`Failed to checkout management learning branch: ${err.message}`);
140
+ }
141
+ }
142
+ try {
143
+ const fs = await import("node:fs/promises");
144
+ let existingContent = "";
145
+ if (proposal.targetArtifactExists) {
146
+ existingContent = await fs.readFile(normalizedPath, "utf8");
147
+ }
148
+ else {
149
+ await fs.mkdir(path.dirname(normalizedPath), { recursive: true });
150
+ }
151
+ const newContent = applyLearningArtifactEdit(existingContent, proposal, now);
152
+ if (newContent === existingContent) {
153
+ // Check if the branch already has the content (idempotence)
154
+ if (!existingContent.includes(`_Proposal ID: ${proposal.id}_`)) {
155
+ // Restore state if it was a genuine "no change"
156
+ if (originalBranch) {
157
+ await commandRunner.run("git", ["checkout", originalBranch], { cwd: managementRoot });
158
+ }
159
+ await commandRunner.run("git", ["branch", "-D", branchName], { cwd: managementRoot });
160
+ return auditOnly("Proposed edit resulted in no changes to the artifact");
161
+ }
162
+ // If it has the ID, it was already applied. Continue to PR check.
163
+ }
164
+ else {
165
+ await fs.writeFile(normalizedPath, newContent, "utf8");
166
+ // 6. Commit Only Allowed Changes
167
+ await commandRunner.run("git", ["add", normalizedPath], { cwd: managementRoot });
168
+ const diffCheck = await commandRunner.run("git", ["diff", "--cached", "--name-only"], { cwd: managementRoot });
169
+ const changedFiles = diffCheck.stdout.trim().split(/\r?\n/).filter(Boolean);
170
+ for (const file of changedFiles) {
171
+ const fullPath = path.resolve(managementRoot, file);
172
+ if (!fullPath.startsWith(learningsRoot + path.sep)) {
173
+ // Safety violation: something staged outside learnings/
174
+ await commandRunner.run("git", ["reset", "HEAD", "--", "."], { cwd: managementRoot });
175
+ await commandRunner.run("git", ["checkout", "--", "."], { cwd: managementRoot });
176
+ if (originalBranch) {
177
+ await commandRunner.run("git", ["checkout", originalBranch], { cwd: managementRoot });
178
+ }
179
+ await commandRunner.run("git", ["branch", "-D", branchName], { cwd: managementRoot });
180
+ return auditOnly(`Detected attempt to commit file outside learnings root: ${file}`);
181
+ }
182
+ }
183
+ const commitMessage = buildManagementLearningCommitMessage(proposal);
184
+ await commandRunner.run("git", ["commit", "-m", commitMessage], { cwd: managementRoot });
185
+ }
186
+ // 7. Push and Open PR
187
+ await gitGateway.pushBranch(managementRoot, branchName);
188
+ const prBody = buildManagementLearningPullRequestBody(proposal, candidates, problems, opportunities);
189
+ const prTitle = `Add management learning for ${proposal.selectedScope.kind}`;
190
+ let prUrl;
191
+ let prNumber;
192
+ const existingPr = await githubGateway.findOpenPullRequestByHeadBranch(repoIdentity.owner, repoIdentity.name, branchName);
193
+ if (existingPr) {
194
+ prUrl = existingPr.url;
195
+ prNumber = existingPr.number;
196
+ }
197
+ else {
198
+ const createdPr = await githubGateway.createPullRequest({
199
+ owner: repoIdentity.owner,
200
+ name: repoIdentity.name,
201
+ title: prTitle,
202
+ body: prBody,
203
+ headBranch: branchName,
204
+ baseBranch: baseBranch,
205
+ });
206
+ prUrl = createdPr.url;
207
+ prNumber = createdPr.number;
208
+ }
209
+ // Cleanup: return to original branch locally
210
+ if (originalBranch) {
211
+ await commandRunner.run("git", ["checkout", originalBranch], { cwd: managementRoot });
212
+ }
213
+ const record = {
214
+ id: `mgmt-pr-${proposal.id}`,
215
+ proposalId: proposal.id,
216
+ mode: "pull_request",
217
+ status: "opened",
218
+ branchName,
219
+ prUrl,
220
+ prNumber,
221
+ repoSlug: `${repoIdentity.owner}/${repoIdentity.name}`,
222
+ changedArtifactPaths: [normalizedPath],
223
+ detectedAt: now,
224
+ updatedAt: now,
225
+ };
226
+ return {
227
+ proposal: {
228
+ ...proposal,
229
+ status: "materialized",
230
+ materializationStatus: "completed",
231
+ updatedAt: now,
232
+ },
233
+ managementPr: record,
234
+ };
235
+ }
236
+ catch (err) {
237
+ // Cleanup on error
238
+ try {
239
+ await commandRunner.run("git", ["reset", "HEAD", "--", "."], { cwd: managementRoot });
240
+ await commandRunner.run("git", ["checkout", "--", "."], { cwd: managementRoot });
241
+ if (originalBranch) {
242
+ await commandRunner.run("git", ["checkout", originalBranch], { cwd: managementRoot });
243
+ }
244
+ }
245
+ catch { /* ignore cleanup errors */ }
246
+ return auditOnly(`Failed during materialization: ${err.message}`);
247
+ }
248
+ }
249
+ export function buildManagementLearningBranchName(proposal) {
250
+ const idHash = createHash("sha256").update(proposal.id).digest("hex").slice(0, 7);
251
+ const scopeSlug = proposal.selectedScope
252
+ ? proposal.selectedScope.kind.toLowerCase().replace(/[^a-z0-9]/g, "-")
253
+ : "unknown-scope";
254
+ return `agent/management-learning/${scopeSlug}-${idHash}`;
255
+ }
256
+ export function buildManagementLearningCommitMessage(proposal) {
257
+ const scopeDesc = proposal.selectedScope ? proposal.selectedScope.kind : "unknown scope";
258
+ return `Add management learning for ${scopeDesc}\n\nProposal: ${proposal.id}`;
259
+ }
260
+ export function applyLearningArtifactEdit(existingContent, proposal, now) {
261
+ if (existingContent.includes(getProposalMarker(proposal.id))) {
262
+ return existingContent;
263
+ }
264
+ const section = renderManagementLearningSection(proposal, now);
265
+ if (!existingContent.trim()) {
266
+ return section;
267
+ }
268
+ const editMode = proposal.proposedEditMode ?? "append";
269
+ if (editMode === "append" || editMode === "create") {
270
+ return existingContent.trimEnd() + "\n\n" + section;
271
+ }
272
+ const sections = findManagementLearningSections(existingContent);
273
+ const mostRecentSection = sections.at(-1);
274
+ if (!mostRecentSection) {
275
+ return existingContent.trimEnd() + "\n\n" + section;
276
+ }
277
+ const prefix = existingContent.slice(0, mostRecentSection.start);
278
+ const suffix = existingContent.slice(mostRecentSection.end);
279
+ if (editMode === "refine") {
280
+ return composeManagementLearningContent(prefix, section, suffix);
281
+ }
282
+ if (editMode === "supersede") {
283
+ return composeManagementLearningContent(prefix, suffix, section);
284
+ }
285
+ return existingContent.trimEnd() + "\n\n" + section;
286
+ }
287
+ function renderManagementLearningSection(proposal, now) {
288
+ const guidance = proposal.proposedGuidanceText || "";
289
+ const rationale = proposal.rationale || "";
290
+ const behaviorChange = proposal.expectedBehaviorChange || "";
291
+ return [
292
+ `### Management Learning: ${now.split("T")[0]}`,
293
+ "",
294
+ "#### Guidance",
295
+ guidance.trim(),
296
+ "",
297
+ "#### Rationale",
298
+ rationale.trim(),
299
+ "",
300
+ "#### Expected Behavior Change",
301
+ behaviorChange.trim(),
302
+ "",
303
+ getProposalMarker(proposal.id),
304
+ "",
305
+ ].join("\n");
306
+ }
307
+ function getProposalMarker(proposalId) {
308
+ return `_Proposal ID: ${proposalId}_`;
309
+ }
310
+ function findManagementLearningSections(content) {
311
+ const headerMatches = [...content.matchAll(/^### Management Learning:.*$/gm)];
312
+ return headerMatches.map((match, index) => ({
313
+ start: match.index ?? 0,
314
+ end: findProposalFooterEnd(content, (match.index ?? 0) + match[0].length) ?? headerMatches[index + 1]?.index ?? content.length,
315
+ }));
316
+ }
317
+ function findProposalFooterEnd(content, searchStart) {
318
+ const footerRegex = /^_Proposal ID: .+_$/gm;
319
+ footerRegex.lastIndex = searchStart;
320
+ const footerMatch = footerRegex.exec(content);
321
+ if (!footerMatch) {
322
+ return undefined;
323
+ }
324
+ return (footerMatch.index ?? searchStart) + footerMatch[0].length;
325
+ }
326
+ function composeManagementLearningContent(prefix, middle, suffix) {
327
+ const parts = [prefix.trimEnd(), middle, suffix.trimStart()].filter((part) => part.length > 0);
328
+ return parts.join("\n\n");
329
+ }
330
+ export function buildManagementLearningPullRequestBody(proposal, candidates, problems, opportunities) {
331
+ const opportunity = opportunities.find((o) => o.id === proposal.opportunityId);
332
+ const problem = problems.find((p) => p.id === proposal.problemId || p.id === opportunity?.problemId);
333
+ const candidate = candidates.find((c) => c.id === problem?.candidateId);
334
+ const evidence = [
335
+ ...(problem?.evidenceReferences || []),
336
+ ...(opportunity?.evidenceReferences || []),
337
+ ];
338
+ const uniqueEvidence = Array.from(new Map(evidence.map((e) => [e.id, e])).values());
339
+ const affectedWorkstreams = new Set();
340
+ if (candidate?.sourceWorkstreamId)
341
+ affectedWorkstreams.add(candidate.sourceWorkstreamId);
342
+ const affectedExecutions = new Set();
343
+ if (proposal.sourceExecutionId)
344
+ affectedExecutions.add(proposal.sourceExecutionId);
345
+ if (opportunity?.sourceExecutionId)
346
+ affectedExecutions.add(opportunity.sourceExecutionId);
347
+ if (problem?.sourceExecutionId)
348
+ affectedExecutions.add(problem.sourceExecutionId);
349
+ if (candidate?.sourceExecutionId)
350
+ affectedExecutions.add(candidate.sourceExecutionId);
351
+ const artifactPath = proposal.resolvedArtifactPath || "Unknown";
352
+ return [
353
+ "🤖 This management learning proposal was automatically materialized by the orchestrator after a retrospective run.",
354
+ "",
355
+ "### Retrospective Summary",
356
+ proposal.rationale || "No rationale provided.",
357
+ "",
358
+ "### Proposed Behavioral Change",
359
+ proposal.expectedBehaviorChange || "No behavior change described.",
360
+ "",
361
+ "### Selected Scope",
362
+ `\`${JSON.stringify(proposal.selectedScope, null, 2)}\``,
363
+ "",
364
+ "### Evidence Audit Trail",
365
+ uniqueEvidence.length > 0
366
+ ? uniqueEvidence.map((e) => `- [${e.kind}] ${e.summary || e.id}${e.url ? ` (${e.url})` : ""}`).join("\n")
367
+ : "No direct evidence references provided.",
368
+ "",
369
+ "### Affected Context",
370
+ `- **Workstreams**: ${affectedWorkstreams.size > 0 ? Array.from(affectedWorkstreams).join(", ") : "None identified"}`,
371
+ `- **Executions**: ${affectedExecutions.size > 0 ? Array.from(affectedExecutions).join(", ") : "None identified"}`,
372
+ "",
373
+ "### Implementation Details",
374
+ `- **Edit Mode**: ${proposal.proposedEditMode}`,
375
+ `- **Modified Artifact**: \`${artifactPath}\``,
376
+ "",
377
+ "---",
378
+ "**Human Review Boundary**: This PR is for human review and merge only. The orchestrator will not approve, review, prepare, or merge this PR automatically.",
379
+ ].join("\n");
380
+ }
@@ -0,0 +1,199 @@
1
+ import { SCOPE_PRECEDENCE_ORDER, validateLearningScope, resolveScopePath, } from "./scopes.js";
2
+ export async function validateLearningProposals(input) {
3
+ const { proposals, opportunities, problems, scopeValidationConfig, managementRoot, readArtifact, now, } = input;
4
+ const validatedProposals = [];
5
+ for (const proposal of proposals) {
6
+ if (proposal.status !== "pending") {
7
+ validatedProposals.push(proposal);
8
+ continue;
9
+ }
10
+ const result = await validateLearningProposal({
11
+ proposal,
12
+ opportunities,
13
+ problems,
14
+ scopeValidationConfig,
15
+ managementRoot,
16
+ readArtifact,
17
+ now,
18
+ });
19
+ validatedProposals.push(result);
20
+ }
21
+ return { proposals: validatedProposals };
22
+ }
23
+ async function validateLearningProposal(input) {
24
+ const { proposal, opportunities, problems, scopeValidationConfig, managementRoot, readArtifact, now, } = input;
25
+ const updated = { ...proposal, updatedAt: now };
26
+ try {
27
+ // 1. Find source opportunity and problem
28
+ const opportunity = opportunities.find((o) => o.id === proposal.opportunityId);
29
+ if (!opportunity) {
30
+ return decline(updated, "Source opportunity not found", now);
31
+ }
32
+ const problem = problems.find((p) => p.id === proposal.problemId || p.id === opportunity.problemId);
33
+ if (!problem) {
34
+ return decline(updated, "Source retrospective problem not found", now);
35
+ }
36
+ // 2. Bound content
37
+ const contentError = validateContentBounds(updated);
38
+ if (contentError) {
39
+ return decline(updated, contentError, now);
40
+ }
41
+ // 3. Deterministic Scope Validation And Narrowing
42
+ if (!updated.selectedScope) {
43
+ return decline(updated, "Missing selected scope", now);
44
+ }
45
+ const scopeResult = selectHighestPrecedenceSupportedScope({
46
+ suggestedScope: updated.selectedScope,
47
+ opportunity,
48
+ problem,
49
+ config: scopeValidationConfig,
50
+ });
51
+ if (!scopeResult.ok) {
52
+ return decline(updated, `Scope validation failed: ${scopeResult.error}`, now);
53
+ }
54
+ updated.selectedScope = scopeResult.scope;
55
+ if (scopeResult.reason) {
56
+ updated.validationReason = scopeResult.reason;
57
+ }
58
+ // 4. Resolve Artifact Path
59
+ let resolvedPath;
60
+ try {
61
+ if (!updated.selectedScope) {
62
+ return decline(updated, "Missing selected scope for path resolution", now);
63
+ }
64
+ resolvedPath = resolveScopePath(updated.selectedScope, managementRoot);
65
+ updated.resolvedArtifactPath = resolvedPath;
66
+ }
67
+ catch (err) {
68
+ return decline(updated, `Path resolution failed: ${err.message}`, now);
69
+ }
70
+ // 5. Load Existing Artifact Content For Validation Context
71
+ if (readArtifact) {
72
+ try {
73
+ const content = await readArtifact(resolvedPath);
74
+ if (content !== undefined) {
75
+ updated.targetArtifactExists = true;
76
+ updated.existingArtifactContentExcerpt = content.slice(0, 500);
77
+ // Edit-mode validation
78
+ if (updated.proposedEditMode === "create") {
79
+ return decline(updated, `Cannot use edit mode "create" because artifact already exists at ${resolvedPath}`, now);
80
+ }
81
+ if (updated.proposedGuidanceText && content.includes(updated.proposedGuidanceText.trim())) {
82
+ return decline(updated, "Proposed guidance text is already present in the existing artifact", now);
83
+ }
84
+ }
85
+ else {
86
+ updated.targetArtifactExists = false;
87
+ if (updated.proposedEditMode !== "create") {
88
+ // Normalize to create if it doesn't exist? Plan says "normalize to create only if the guidance is otherwise valid... If this feels too broad... decline instead"
89
+ // I'll be conservative and normalize.
90
+ // Let's normalize but record it.
91
+ updated.validationReason = (updated.validationReason ? updated.validationReason + ". " : "") +
92
+ `Normalized edit mode from "${updated.proposedEditMode}" to "create" because artifact does not exist`;
93
+ updated.proposedEditMode = "create";
94
+ }
95
+ }
96
+ }
97
+ catch (err) {
98
+ return decline(updated, `Failed to read existing artifact: ${err.message}`, now);
99
+ }
100
+ }
101
+ updated.status = "validated";
102
+ updated.validationStatus = "passed";
103
+ }
104
+ catch (err) {
105
+ return decline(updated, `Unexpected validation error: ${err.message}`, now);
106
+ }
107
+ return updated;
108
+ }
109
+ function decline(proposal, reason, now) {
110
+ return {
111
+ ...proposal,
112
+ status: "declined",
113
+ validationStatus: "failed",
114
+ declineReason: reason,
115
+ updatedAt: now,
116
+ };
117
+ }
118
+ const LIMITS = {
119
+ guidance: 4000,
120
+ rationale: 2000,
121
+ expectedBehaviorChange: 1000,
122
+ };
123
+ function validateContentBounds(proposal) {
124
+ if (!proposal.proposedGuidanceText || !proposal.proposedGuidanceText.trim())
125
+ return "Proposed guidance text is empty";
126
+ if (proposal.proposedGuidanceText.length > LIMITS.guidance)
127
+ return `Proposed guidance text exceeds limit of ${LIMITS.guidance} characters`;
128
+ if (!proposal.rationale || !proposal.rationale.trim())
129
+ return "Rationale is empty";
130
+ if (proposal.rationale.length > LIMITS.rationale)
131
+ return `Rationale exceeds limit of ${LIMITS.rationale} characters`;
132
+ if (!proposal.expectedBehaviorChange || !proposal.expectedBehaviorChange.trim())
133
+ return "Expected behavior change is empty";
134
+ if (proposal.expectedBehaviorChange.length > LIMITS.expectedBehaviorChange)
135
+ return `Expected behavior change exceeds limit of ${LIMITS.expectedBehaviorChange} characters`;
136
+ return undefined;
137
+ }
138
+ export function selectHighestPrecedenceSupportedScope(input) {
139
+ const { suggestedScope, opportunity, problem, config } = input;
140
+ // 1. Build candidate scopes
141
+ const candidates = [];
142
+ // Always consider suggested scope first (it's what the agent wanted)
143
+ candidates.push(suggestedScope);
144
+ if (opportunity.selectedScope)
145
+ candidates.push(opportunity.selectedScope);
146
+ if (problem.selectedScope)
147
+ candidates.push(problem.selectedScope);
148
+ if (problem.suggestedScopes)
149
+ candidates.push(...problem.suggestedScopes);
150
+ // 2. Validate and filter by evidence
151
+ const validSupported = [];
152
+ for (const candidate of candidates) {
153
+ const val = validateLearningScope(candidate, config);
154
+ if (!val.ok)
155
+ continue;
156
+ if (isScopeSupportedByEvidence(val.scope, problem)) {
157
+ validSupported.push(val.scope);
158
+ }
159
+ }
160
+ if (validSupported.length === 0) {
161
+ return { ok: false, scope: suggestedScope, error: "No valid scopes supported by problem evidence" };
162
+ }
163
+ // 3. Sort by precedence
164
+ validSupported.sort((a, b) => {
165
+ const idxA = SCOPE_PRECEDENCE_ORDER.indexOf(a.kind);
166
+ const idxB = SCOPE_PRECEDENCE_ORDER.indexOf(b.kind);
167
+ return idxA - idxB;
168
+ });
169
+ const selected = validSupported[0];
170
+ if (!selected) {
171
+ // Should be impossible given length check above, but for TS:
172
+ return { ok: false, scope: suggestedScope, error: "No valid scopes supported by problem evidence" };
173
+ }
174
+ let reason = `Selected highest-precedence valid scope: ${selected.kind}`;
175
+ if (selected.kind !== suggestedScope.kind) {
176
+ reason = `Narrowed scope from ${suggestedScope.kind} to ${selected.kind} based on evidence`;
177
+ }
178
+ return { ok: true, scope: selected, reason };
179
+ }
180
+ function isScopeSupportedByEvidence(scope, problem) {
181
+ switch (scope.kind) {
182
+ case "global":
183
+ return true;
184
+ case "provider":
185
+ return problem.providers.includes(scope.provider);
186
+ case "role":
187
+ return problem.roles.includes(scope.role);
188
+ case "task_kind":
189
+ return problem.taskKinds.includes(scope.taskKind);
190
+ case "repo":
191
+ return problem.affectedRepoIds.includes(scope.repoId);
192
+ case "repo_provider":
193
+ return problem.affectedRepoIds.includes(scope.repoId) && problem.providers.includes(scope.provider);
194
+ case "repo_role":
195
+ return problem.affectedRepoIds.includes(scope.repoId) && problem.roles.includes(scope.role);
196
+ case "repo_task_kind":
197
+ return problem.affectedRepoIds.includes(scope.repoId) && problem.taskKinds.includes(scope.taskKind);
198
+ }
199
+ }