@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,153 @@
1
+ const REQUIRED_TOOLS = [
2
+ {
3
+ name: "Git",
4
+ command: "git",
5
+ versionArgs: ["--version"],
6
+ versionExtract: (out) => out.trim().replace("git version ", ""),
7
+ installHint: getInstallHint("git"),
8
+ required: true
9
+ },
10
+ {
11
+ name: "GitHub CLI",
12
+ command: "gh",
13
+ versionArgs: ["--version"],
14
+ versionExtract: (out) => out.trim().split("\n")[0] ?? out.trim(),
15
+ installHint: getInstallHint("gh"),
16
+ required: true
17
+ },
18
+ {
19
+ name: "Node.js",
20
+ command: "node",
21
+ versionArgs: ["--version"],
22
+ versionExtract: (out) => out.trim(),
23
+ versionCheck: (version) => {
24
+ const major = parseNodeMajor(version);
25
+ if (major === undefined) {
26
+ return `Could not parse Node.js version string: "${version}"`;
27
+ }
28
+ if (major < 20) {
29
+ return `Node.js >= 20 is required (found ${version}).`;
30
+ }
31
+ return undefined;
32
+ },
33
+ installHint: getInstallHint("node"),
34
+ required: true
35
+ }
36
+ ];
37
+ export class ToolChecker {
38
+ commandRunner;
39
+ platformHelper;
40
+ constructor(commandRunner, platformHelper = getDefaultPlatform) {
41
+ this.commandRunner = commandRunner;
42
+ this.platformHelper = platformHelper;
43
+ }
44
+ async checkTool(tool) {
45
+ const installHint = getInstallHint(toInstallTarget(tool.command), this.platformHelper());
46
+ try {
47
+ const result = await this.commandRunner.run(tool.command, tool.versionArgs, {
48
+ timeoutMs: 5_000
49
+ });
50
+ const version = tool.versionExtract?.(result.stdout) ?? result.stdout.trim();
51
+ const versionError = tool.versionCheck?.(version);
52
+ if (versionError !== undefined) {
53
+ return {
54
+ name: tool.name,
55
+ command: tool.command,
56
+ available: false,
57
+ version,
58
+ issue: {
59
+ title: `${tool.name} version requirement not met`,
60
+ cause: versionError,
61
+ suggestedFix: installHint,
62
+ canAutoRemediate: false,
63
+ manualSteps: [installHint]
64
+ }
65
+ };
66
+ }
67
+ return { name: tool.name, command: tool.command, available: true, version };
68
+ }
69
+ catch {
70
+ return {
71
+ name: tool.name,
72
+ command: tool.command,
73
+ available: false,
74
+ issue: {
75
+ title: `${tool.name} not found`,
76
+ cause: `The command "${tool.command}" is not available on PATH`,
77
+ suggestedFix: installHint,
78
+ canAutoRemediate: false,
79
+ manualSteps: [installHint]
80
+ }
81
+ };
82
+ }
83
+ }
84
+ async checkAll() {
85
+ return Promise.all(REQUIRED_TOOLS.map((tool) => this.checkTool(tool)));
86
+ }
87
+ static getRequiredTools() {
88
+ return REQUIRED_TOOLS;
89
+ }
90
+ }
91
+ /** Parse the major version number from a Node.js version string like "v18.20.0". */
92
+ export function parseNodeMajor(version) {
93
+ const match = version.replace(/^v/, "").match(/^(\d+)/);
94
+ if (!match?.[1])
95
+ return undefined;
96
+ const n = parseInt(match[1], 10);
97
+ return Number.isFinite(n) ? n : undefined;
98
+ }
99
+ export function getDefaultPlatform() {
100
+ return process.platform;
101
+ }
102
+ export function getInstallHint(tool, platform = getDefaultPlatform()) {
103
+ switch (tool) {
104
+ case "git":
105
+ return gitInstallHint(platform);
106
+ case "gh":
107
+ return ghInstallHint(platform);
108
+ case "node":
109
+ return nodeInstallHint(platform);
110
+ }
111
+ }
112
+ function gitInstallHint(platform) {
113
+ if (platform === "darwin") {
114
+ return "Install Git with Homebrew (`brew install git`) or download it from https://git-scm.com/downloads";
115
+ }
116
+ if (platform === "win32") {
117
+ return "Install Git with winget (`winget install Git.Git`) or download Git for Windows from https://git-scm.com/downloads/win";
118
+ }
119
+ if (platform === "linux") {
120
+ return "Install Git with your distro package manager, for example `sudo apt install git` or `sudo dnf install git`; downloads: https://git-scm.com/downloads";
121
+ }
122
+ return "Install Git: https://git-scm.com/downloads";
123
+ }
124
+ function ghInstallHint(platform) {
125
+ if (platform === "darwin") {
126
+ return "Install GitHub CLI with Homebrew (`brew install gh`) or follow https://cli.github.com";
127
+ }
128
+ if (platform === "win32") {
129
+ return "Install GitHub CLI with winget (`winget install GitHub.cli`) or use the official installer/docs at https://cli.github.com";
130
+ }
131
+ if (platform === "linux") {
132
+ return "Install GitHub CLI using the official Linux package instructions at https://cli.github.com";
133
+ }
134
+ return "Install GitHub CLI: https://cli.github.com";
135
+ }
136
+ function nodeInstallHint(platform) {
137
+ if (platform === "win32") {
138
+ return "Install Node.js >= 20 with winget (`winget install OpenJS.NodeJS.LTS`), fnm, or the official installer from https://nodejs.org";
139
+ }
140
+ if (platform === "darwin" || platform === "linux") {
141
+ return "Install Node.js >= 20 with nvm, fnm, or the official downloads from https://nodejs.org";
142
+ }
143
+ return "Install Node.js >= 20: https://nodejs.org";
144
+ }
145
+ function toInstallTarget(command) {
146
+ if (command === "git")
147
+ return "git";
148
+ if (command === "gh")
149
+ return "gh";
150
+ if (command === "node")
151
+ return "node";
152
+ throw new Error(`No install hint configured for required tool command: ${command}`);
153
+ }
@@ -0,0 +1,83 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { getDefaultPlatform } from "./tool-checker.js";
4
+ export class WorkspaceChecker {
5
+ platformHelper;
6
+ constructor(platformHelper = getDefaultPlatform) {
7
+ this.platformHelper = platformHelper;
8
+ }
9
+ async check(dirPath) {
10
+ let exists = false;
11
+ let writable = false;
12
+ try {
13
+ await fs.access(dirPath, fs.constants.F_OK);
14
+ exists = true;
15
+ try {
16
+ await fs.access(dirPath, fs.constants.W_OK);
17
+ writable = true;
18
+ }
19
+ catch {
20
+ // exists but not writable
21
+ }
22
+ }
23
+ catch {
24
+ // Does not exist. Check the nearest existing ancestor that mkdir -p would use.
25
+ const parent = await findExistingAncestor(path.dirname(dirPath));
26
+ try {
27
+ await fs.access(parent, fs.constants.W_OK);
28
+ writable = true;
29
+ }
30
+ catch {
31
+ // parent also not writable
32
+ }
33
+ }
34
+ if (!writable) {
35
+ return {
36
+ path: dirPath,
37
+ exists,
38
+ writable: false,
39
+ issue: {
40
+ title: `Cannot write to directory: ${dirPath}`,
41
+ cause: `The directory or its parent is not writable by the current user`,
42
+ suggestedFix: "Choose a different directory or fix the permissions",
43
+ canAutoRemediate: false,
44
+ manualSteps: buildWorkspacePermissionManualSteps(dirPath, this.platformHelper())
45
+ }
46
+ };
47
+ }
48
+ return { path: dirPath, exists, writable: true };
49
+ }
50
+ async ensureDir(dirPath) {
51
+ await fs.mkdir(dirPath, { recursive: true });
52
+ }
53
+ }
54
+ async function findExistingAncestor(startPath) {
55
+ let current = startPath;
56
+ while (true) {
57
+ try {
58
+ await fs.access(current, fs.constants.F_OK);
59
+ return current;
60
+ }
61
+ catch {
62
+ const parent = path.dirname(current);
63
+ if (parent === current) {
64
+ return current;
65
+ }
66
+ current = parent;
67
+ }
68
+ }
69
+ }
70
+ export function buildWorkspacePermissionManualSteps(dirPath, platform = getDefaultPlatform()) {
71
+ if (platform === "win32") {
72
+ const parentDir = path.win32.dirname(dirPath);
73
+ return [
74
+ `Open the directory properties for "${parentDir}" and confirm your user can write there`,
75
+ `Open the directory properties for "${dirPath}" and confirm your user can write there`,
76
+ "Or choose a different directory that your Windows user can write to"
77
+ ];
78
+ }
79
+ return [
80
+ `chmod u+w ${path.dirname(dirPath)} # if parent not writable`,
81
+ `chmod u+w ${dirPath} # if directory not writable`
82
+ ];
83
+ }
@@ -0,0 +1,106 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { resolveScopePath } from "../retrospectives/scopes.js";
4
+ export function mapTaskKindToPromptKey(taskKind) {
5
+ switch (taskKind) {
6
+ case "product_discovery": return "product";
7
+ case "write_product_spec": return "product";
8
+ case "write_plan": return "plan";
9
+ case "implement_plan": return "implement";
10
+ case "review_pr": return "review";
11
+ case "respond_issue_comment": return "discuss";
12
+ case "plan_breakdown": return "breakdown";
13
+ case "wave_plan_assessment": return "waveAssessment";
14
+ case "wave_plan_replanning": return "waveReplanning";
15
+ case "extract_retrospective_problems": return "retrospectiveExtract";
16
+ case "investigate_learning_opportunity": return "retrospectiveInvestigate";
17
+ case "draft_management_learning_proposal": return "retrospectiveProposal";
18
+ default: return undefined;
19
+ }
20
+ }
21
+ export function mapTaskToPromptKey(task) {
22
+ if (task.kind === "respond_issue_comment" &&
23
+ (task.workflowPhase === "product_discovery" || task.workflowPhase === "product_specification")) {
24
+ return "product";
25
+ }
26
+ return mapTaskKindToPromptKey(task.kind);
27
+ }
28
+ export async function resolveTaskPromptContent(taskOrKind, promptsConfig, managementRoot, logger) {
29
+ const taskKind = typeof taskOrKind === "string" ? taskOrKind : taskOrKind.kind;
30
+ const key = typeof taskOrKind === "string"
31
+ ? mapTaskKindToPromptKey(taskOrKind)
32
+ : mapTaskToPromptKey(taskOrKind);
33
+ if (!key || !promptsConfig) {
34
+ logger?.info(`[prompt-resolver] Using built-in constraints for ${taskKind}`);
35
+ return { source: "built-in" };
36
+ }
37
+ const relativeFilePath = promptsConfig.promptProfiles.default[key];
38
+ if (!relativeFilePath) {
39
+ logger?.info(`[prompt-resolver] No config entry for "${key}" (${taskKind}); using built-in`);
40
+ return { source: "built-in" };
41
+ }
42
+ const absoluteFilePath = path.join(managementRoot, relativeFilePath);
43
+ try {
44
+ const content = await readFile(absoluteFilePath, "utf8");
45
+ logger?.info(`[prompt-resolver] Using config-file for "${key}" (${taskKind}): ${relativeFilePath}`);
46
+ return { source: "config-file", filePath: absoluteFilePath, content };
47
+ }
48
+ catch (err) {
49
+ if (err.code === "ENOENT") {
50
+ logger?.warn(`[prompt-resolver] Prompt file not found for "${key}" (${taskKind}): ${absoluteFilePath}; falling back to built-in`);
51
+ return { source: "built-in" };
52
+ }
53
+ throw err;
54
+ }
55
+ }
56
+ export async function loadLearningOverlays(ctx, managementRoot, logger) {
57
+ // Build scopes in broad-to-narrow order:
58
+ // global → provider → role → task_kind → repo → repo+provider → repo+role → repo+task_kind
59
+ const scopes = [];
60
+ scopes.push({ kind: "global" });
61
+ if (ctx.provider !== undefined) {
62
+ scopes.push({ kind: "provider", provider: ctx.provider });
63
+ }
64
+ if (ctx.role !== undefined) {
65
+ scopes.push({ kind: "role", role: ctx.role });
66
+ }
67
+ scopes.push({ kind: "task_kind", taskKind: ctx.taskKind });
68
+ if (ctx.repoId !== undefined) {
69
+ scopes.push({ kind: "repo", repoId: ctx.repoId });
70
+ if (ctx.provider !== undefined) {
71
+ scopes.push({ kind: "repo_provider", repoId: ctx.repoId, provider: ctx.provider });
72
+ }
73
+ if (ctx.role !== undefined) {
74
+ scopes.push({ kind: "repo_role", repoId: ctx.repoId, role: ctx.role });
75
+ }
76
+ scopes.push({ kind: "repo_task_kind", repoId: ctx.repoId, taskKind: ctx.taskKind });
77
+ }
78
+ const overlays = [];
79
+ for (const scope of scopes) {
80
+ let filePath;
81
+ try {
82
+ filePath = resolveScopePath(scope, managementRoot);
83
+ }
84
+ catch {
85
+ // Should never happen for internally-constructed scopes, but log and skip defensively
86
+ logger?.warn(`[learning-overlays] Failed to resolve path for scope ${JSON.stringify(scope)}`);
87
+ continue;
88
+ }
89
+ try {
90
+ const content = await readFile(filePath, "utf8");
91
+ if (content.trim().length > 0) {
92
+ overlays.push(content.trimEnd());
93
+ logger?.info(`[learning-overlays] Loaded overlay: ${filePath}`);
94
+ }
95
+ }
96
+ catch (err) {
97
+ if (err.code === "ENOENT") {
98
+ // Missing file is expected for most scopes; silently skip
99
+ }
100
+ else {
101
+ throw err;
102
+ }
103
+ }
104
+ }
105
+ return overlays;
106
+ }
@@ -0,0 +1,224 @@
1
+ import { classifyManagementPullRequest } from "../github/pr-classification.js";
2
+ import { workflowRegistry } from "../workflows/registry.js";
3
+ export function registerRetrospectiveCandidates(input) {
4
+ const { previousCandidates, workstreams, issues, pullRequests, tasks, executions, managementPrs, classifyPullRequest = classifyManagementPullRequest, managementRepo, now, } = input;
5
+ const candidatesMap = new Map();
6
+ for (const prev of previousCandidates) {
7
+ candidatesMap.set(prev.id, prev);
8
+ }
9
+ const registered = [];
10
+ const skipped = [];
11
+ for (const workstream of workstreams) {
12
+ // 1. Consider only terminal workstreams
13
+ const isPhaseTerminal = workstream.phase === "resolved" || workstream.phase === "abandoned";
14
+ const isStatusMergedOrClosed = (workstream.status === "closed" && workstream.resolution === "closed") ||
15
+ (workstream.status === "merged" && workstream.resolution === "merged");
16
+ if (!isPhaseTerminal && !isStatusMergedOrClosed) {
17
+ // 2. Do not register open/unresolved workstreams
18
+ continue;
19
+ }
20
+ if (workstream.resolution !== "closed" && workstream.resolution !== "merged") {
21
+ continue; // Extra safety check based on plan text
22
+ }
23
+ // Identify candidate PRs to classify
24
+ const prsToClassify = [];
25
+ if (workstream.origin.type === "pr") {
26
+ const pr = pullRequests.find((p) => p.number === workstream.origin.number && p.repoSlug === workstream.repoKey);
27
+ if (pr) {
28
+ prsToClassify.push({
29
+ number: pr.number,
30
+ headBranch: pr.headBranch,
31
+ baseBranch: pr.baseBranch,
32
+ });
33
+ }
34
+ else {
35
+ prsToClassify.push({ number: workstream.origin.number });
36
+ }
37
+ }
38
+ else {
39
+ for (const linkedPr of workstream.linkedPrs) {
40
+ const pr = pullRequests.find((p) => p.number === linkedPr.number && p.repoSlug === workstream.repoKey);
41
+ if (pr) {
42
+ prsToClassify.push({
43
+ number: pr.number,
44
+ headBranch: pr.headBranch,
45
+ baseBranch: pr.baseBranch,
46
+ });
47
+ }
48
+ else {
49
+ prsToClassify.push({ number: linkedPr.number });
50
+ }
51
+ }
52
+ if (workstream.activePrNumber) {
53
+ const alreadyIncluded = prsToClassify.some(p => p.number === workstream.activePrNumber);
54
+ if (!alreadyIncluded) {
55
+ const pr = pullRequests.find((p) => p.number === workstream.activePrNumber && p.repoSlug === workstream.repoKey);
56
+ if (pr) {
57
+ prsToClassify.push({
58
+ number: pr.number,
59
+ headBranch: pr.headBranch,
60
+ baseBranch: pr.baseBranch,
61
+ });
62
+ }
63
+ else {
64
+ prsToClassify.push({ number: workstream.activePrNumber });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ // 3. Skip management PR workstreams using early classifier
70
+ let skipReason = "";
71
+ for (const prData of prsToClassify) {
72
+ const classificationInput = {
73
+ repoId: workstream.repoKey,
74
+ repoSlug: workstream.repoKey,
75
+ pullRequestNumber: prData.number,
76
+ managementPrs,
77
+ };
78
+ if (prData.headBranch !== undefined)
79
+ classificationInput.headBranch = prData.headBranch;
80
+ if (prData.baseBranch !== undefined)
81
+ classificationInput.baseBranch = prData.baseBranch;
82
+ if (managementRepo !== undefined)
83
+ classificationInput.managementRepo = managementRepo;
84
+ const classification = classifyPullRequest(classificationInput);
85
+ if (classification.kind === "management") {
86
+ skipReason = `Origin or linked PR #${prData.number} classified as management: ${classification.reason}`;
87
+ break;
88
+ }
89
+ }
90
+ if (skipReason) {
91
+ skipped.push({ workstreamId: workstream.id, reason: skipReason });
92
+ continue;
93
+ }
94
+ const repoSlug = workstream.repoKey;
95
+ const originType = workstream.origin.type;
96
+ const originNumber = workstream.origin.number;
97
+ const resolution = workstream.resolution;
98
+ // Stable candidate key
99
+ const id = `retrospective-candidate:${repoSlug}:${workstream.id}:${originType}-${originNumber}:${resolution}`;
100
+ // 5. Preserve existing candidate records
101
+ if (candidatesMap.has(id)) {
102
+ continue;
103
+ }
104
+ const workstreamTasks = tasks.filter((t) => t.workstreamId === workstream.id);
105
+ const taskIds = workstreamTasks.map((t) => t.id);
106
+ const workstreamExecutions = executions.filter((e) => taskIds.includes(e.taskId));
107
+ const taskSummary = {
108
+ total: workstreamTasks.length,
109
+ completed: workstreamTasks.filter((t) => t.status === "completed").length,
110
+ failed: workstreamTasks.filter((t) => t.status === "failed").length,
111
+ kinds: Array.from(new Set(workstreamTasks.map((t) => t.kind))),
112
+ taskIds,
113
+ };
114
+ const lastExecutionId = workstreamExecutions.length > 0
115
+ ? workstreamExecutions.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0]?.id
116
+ : undefined;
117
+ if (lastExecutionId !== undefined) {
118
+ taskSummary.lastExecutionId = lastExecutionId;
119
+ }
120
+ const executionRoles = new Set();
121
+ for (const execution of workstreamExecutions) {
122
+ const task = workstreamTasks.find((t) => t.id === execution.taskId);
123
+ if (task) {
124
+ try {
125
+ const definition = workflowRegistry.getDefinition(task.kind);
126
+ executionRoles.add(definition.role);
127
+ }
128
+ catch (e) {
129
+ // Ignore unregistered task kinds for role derivation
130
+ }
131
+ }
132
+ }
133
+ const executionSummary = {
134
+ total: workstreamExecutions.length,
135
+ providers: Array.from(new Set(workstreamExecutions.map((e) => e.agentProvider).filter(p => p !== "unassigned"))),
136
+ models: Array.from(new Set(workstreamExecutions.map((e) => e.agentModel).filter(Boolean))),
137
+ roles: Array.from(executionRoles),
138
+ executionIds: workstreamExecutions.map((e) => e.id),
139
+ };
140
+ let title = "";
141
+ let url = "";
142
+ const labels = [];
143
+ let repoIdForCandidate = repoSlug;
144
+ if (originType === "issue") {
145
+ const issue = issues.find(i => i.number === originNumber && i.repoSlug === repoSlug);
146
+ if (issue) {
147
+ title = issue.title;
148
+ url = issue.url;
149
+ labels.push(...issue.labels);
150
+ repoIdForCandidate = issue.repoId;
151
+ }
152
+ }
153
+ else {
154
+ const pr = pullRequests.find(p => p.number === originNumber && p.repoSlug === repoSlug);
155
+ if (pr) {
156
+ title = pr.title;
157
+ url = pr.url;
158
+ labels.push(...pr.labels);
159
+ repoIdForCandidate = pr.repoId;
160
+ }
161
+ }
162
+ const artifactReferences = [];
163
+ workstreamExecutions.forEach((e) => {
164
+ artifactReferences.push(e.promptArtifactPath);
165
+ artifactReferences.push(e.stdoutArtifactPath);
166
+ artifactReferences.push(e.stderrArtifactPath);
167
+ artifactReferences.push(e.promptPacketPath);
168
+ if (e.streamArtifactPath)
169
+ artifactReferences.push(e.streamArtifactPath);
170
+ if (e.settingsArtifactPath)
171
+ artifactReferences.push(e.settingsArtifactPath);
172
+ });
173
+ const evidenceReferences = [];
174
+ evidenceReferences.push({
175
+ kind: "workstream",
176
+ id: workstream.id,
177
+ });
178
+ evidenceReferences.push({
179
+ kind: originType === "issue" ? "issue" : "pull_request",
180
+ id: `${repoSlug}#${originNumber}`,
181
+ url,
182
+ });
183
+ const candidate = {
184
+ id,
185
+ sourceWorkstreamId: workstream.id,
186
+ repoId: repoIdForCandidate,
187
+ repoSlug,
188
+ repoKey: workstream.repoKey,
189
+ origin: {
190
+ type: originType,
191
+ number: originNumber,
192
+ },
193
+ terminalResolution: resolution,
194
+ detectedAt: now,
195
+ resolvedAt: now, // Since we only pick terminal workstreams
196
+ status: "new",
197
+ title,
198
+ url,
199
+ labels,
200
+ sourceSummary: `Terminal operational workstream ${workstream.id}`,
201
+ taskSummary,
202
+ executionSummary,
203
+ artifactReferences,
204
+ evidenceReferences,
205
+ };
206
+ candidatesMap.set(id, candidate);
207
+ registered.push(candidate);
208
+ }
209
+ // 6. Sort the returned candidates deterministically
210
+ const candidates = Array.from(candidatesMap.values()).sort((a, b) => {
211
+ const repoComparison = a.repoSlug.localeCompare(b.repoSlug);
212
+ if (repoComparison !== 0)
213
+ return repoComparison;
214
+ const workstreamComparison = a.sourceWorkstreamId.localeCompare(b.sourceWorkstreamId);
215
+ if (workstreamComparison !== 0)
216
+ return workstreamComparison;
217
+ return a.id.localeCompare(b.id);
218
+ });
219
+ return {
220
+ candidates,
221
+ registered,
222
+ skipped,
223
+ };
224
+ }