@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.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- 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
|
+
}
|