@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,169 @@
|
|
|
1
|
+
import { workflowRegistry } from "../workflows/registry.js";
|
|
2
|
+
const RETROSPECTIVE_TASK_KINDS = [
|
|
3
|
+
"extract_retrospective_problems",
|
|
4
|
+
"investigate_learning_opportunity",
|
|
5
|
+
"draft_management_learning_proposal",
|
|
6
|
+
];
|
|
7
|
+
export function deriveRetrospectiveTasks(input) {
|
|
8
|
+
const { existingTasks, candidates, problems, opportunities, agents, contextBuilder, now, } = input;
|
|
9
|
+
const resultTasks = [];
|
|
10
|
+
const resultCandidates = [...candidates];
|
|
11
|
+
const resultProblems = [...problems];
|
|
12
|
+
const resultOpportunities = [...opportunities];
|
|
13
|
+
const blocked = [];
|
|
14
|
+
// 1. Check idleness
|
|
15
|
+
const hasPendingOperationalTasks = existingTasks.some((t) => (t.status === "pending" || t.status === "dispatched") &&
|
|
16
|
+
!RETROSPECTIVE_TASK_KINDS.includes(t.kind) &&
|
|
17
|
+
t.relevant);
|
|
18
|
+
if (hasPendingOperationalTasks) {
|
|
19
|
+
return {
|
|
20
|
+
tasks: [],
|
|
21
|
+
candidates: resultCandidates,
|
|
22
|
+
problems: resultProblems,
|
|
23
|
+
opportunities: resultOpportunities,
|
|
24
|
+
blocked: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// 2. Check if we already have a retrospective task in flight
|
|
28
|
+
const hasInFlightRetrospectiveTask = existingTasks.some((t) => (t.status === "pending" || t.status === "dispatched") &&
|
|
29
|
+
RETROSPECTIVE_TASK_KINDS.includes(t.kind));
|
|
30
|
+
if (hasInFlightRetrospectiveTask) {
|
|
31
|
+
return {
|
|
32
|
+
tasks: [],
|
|
33
|
+
candidates: resultCandidates,
|
|
34
|
+
problems: resultProblems,
|
|
35
|
+
opportunities: resultOpportunities,
|
|
36
|
+
blocked: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// 3. Check for retrospective agent role
|
|
40
|
+
const hasRetrospectiveAgent = agents.some((a) => a.enabled && a.modelClass === "strong" && a.roles.includes("retrospective"));
|
|
41
|
+
const NO_AGENT_REASON = 'No enabled strong agent is configured for role "retrospective".';
|
|
42
|
+
// 4. Derivation in priority order: Extraction > Investigation > Proposal
|
|
43
|
+
// A. Candidate to Extraction
|
|
44
|
+
const eligibleCandidates = resultCandidates
|
|
45
|
+
.filter((c) => c.status === "new" || c.status === "blocked")
|
|
46
|
+
.sort((a, b) => a.detectedAt.localeCompare(b.detectedAt) || a.id.localeCompare(b.id));
|
|
47
|
+
for (const candidate of eligibleCandidates) {
|
|
48
|
+
if (!hasRetrospectiveAgent) {
|
|
49
|
+
candidate.status = "blocked";
|
|
50
|
+
candidate.skipReason = NO_AGENT_REASON;
|
|
51
|
+
candidate.updatedAt = now;
|
|
52
|
+
blocked.push({ sourceId: candidate.id, reason: NO_AGENT_REASON });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const taskId = `extract_retrospective_problems:${candidate.id}`;
|
|
56
|
+
if (existingTasks.some((t) => t.id === taskId))
|
|
57
|
+
continue;
|
|
58
|
+
const task = createTask(taskId, "extract_retrospective_problems", "retrospective_candidate", candidate.id, candidate.url || `https://github.com/${candidate.repoSlug}`, candidate.repoId, candidate.repoSlug, `Extracting problems from candidate ${candidate.id}`, contextBuilder, now, candidate);
|
|
59
|
+
candidate.status = "extracting";
|
|
60
|
+
candidate.updatedAt = now;
|
|
61
|
+
return {
|
|
62
|
+
tasks: [task],
|
|
63
|
+
candidates: resultCandidates,
|
|
64
|
+
problems: resultProblems,
|
|
65
|
+
opportunities: resultOpportunities,
|
|
66
|
+
blocked: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// B. Problem to Investigation
|
|
70
|
+
const eligibleProblems = resultProblems
|
|
71
|
+
.filter((p) => (p.status === "pending" || p.status === "blocked") && p.thresholdDecision === "pass")
|
|
72
|
+
.sort((a, b) => a.detectedAt.localeCompare(b.detectedAt) || a.id.localeCompare(b.id));
|
|
73
|
+
for (const problem of eligibleProblems) {
|
|
74
|
+
if (!hasRetrospectiveAgent) {
|
|
75
|
+
problem.status = "blocked";
|
|
76
|
+
problem.decisionReason = NO_AGENT_REASON;
|
|
77
|
+
problem.updatedAt = now;
|
|
78
|
+
blocked.push({ sourceId: problem.id, reason: NO_AGENT_REASON });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const taskId = `investigate_learning_opportunity:${problem.id}`;
|
|
82
|
+
if (existingTasks.some((t) => t.id === taskId))
|
|
83
|
+
continue;
|
|
84
|
+
const candidate = resultCandidates.find(c => c.id === problem.candidateId);
|
|
85
|
+
const repoId = candidate?.repoId || problem.affectedRepoIds[0] || "unknown";
|
|
86
|
+
const repoSlug = candidate?.repoSlug || problem.affectedRepoIds[0] || "unknown";
|
|
87
|
+
const task = createTask(taskId, "investigate_learning_opportunity", "retrospective_problem", problem.id, `https://github.com/${repoSlug}`, repoId, repoSlug, `Investigating learning opportunity for problem ${problem.id}`, contextBuilder, now, undefined, problem);
|
|
88
|
+
problem.status = "investigating";
|
|
89
|
+
problem.updatedAt = now;
|
|
90
|
+
return {
|
|
91
|
+
tasks: [task],
|
|
92
|
+
candidates: resultCandidates,
|
|
93
|
+
problems: resultProblems,
|
|
94
|
+
opportunities: resultOpportunities,
|
|
95
|
+
blocked: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// C. Opportunity to Proposal
|
|
99
|
+
const eligibleOpportunities = resultOpportunities
|
|
100
|
+
.filter((o) => (o.status === "investigated" || o.status === "blocked"))
|
|
101
|
+
.sort((a, b) => a.detectedAt.localeCompare(b.detectedAt) || a.id.localeCompare(b.id));
|
|
102
|
+
for (const opportunity of eligibleOpportunities) {
|
|
103
|
+
// Check if proposal already exists
|
|
104
|
+
if (input.proposals.some(p => p.opportunityId === opportunity.id))
|
|
105
|
+
continue;
|
|
106
|
+
if (!hasRetrospectiveAgent) {
|
|
107
|
+
opportunity.status = "blocked";
|
|
108
|
+
opportunity.decisionReason = NO_AGENT_REASON;
|
|
109
|
+
opportunity.updatedAt = now;
|
|
110
|
+
blocked.push({ sourceId: opportunity.id, reason: NO_AGENT_REASON });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const taskId = `draft_management_learning_proposal:${opportunity.id}`;
|
|
114
|
+
if (existingTasks.some((t) => t.id === taskId))
|
|
115
|
+
continue;
|
|
116
|
+
// We need a repo for the task. We use the one from the problem/candidate if possible.
|
|
117
|
+
const problem = problems.find(p => p.id === opportunity.problemId);
|
|
118
|
+
const candidate = resultCandidates.find(c => c.id === problem?.candidateId);
|
|
119
|
+
const repoId = candidate?.repoId || problem?.affectedRepoIds[0] || "unknown";
|
|
120
|
+
const repoSlug = candidate?.repoSlug || problem?.affectedRepoIds[0] || "unknown";
|
|
121
|
+
const task = createTask(taskId, "draft_management_learning_proposal", "learning_opportunity", opportunity.id, `https://github.com/${repoSlug}`, repoId, repoSlug, `Drafting learning proposal for opportunity ${opportunity.id}`, contextBuilder, now, undefined, undefined, opportunity);
|
|
122
|
+
return {
|
|
123
|
+
tasks: [task],
|
|
124
|
+
candidates: resultCandidates,
|
|
125
|
+
problems: resultProblems,
|
|
126
|
+
opportunities: resultOpportunities,
|
|
127
|
+
blocked: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
tasks: [],
|
|
132
|
+
candidates: resultCandidates,
|
|
133
|
+
problems: resultProblems,
|
|
134
|
+
opportunities: resultOpportunities,
|
|
135
|
+
blocked,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function createTask(id, kind, sourceKind, sourceId, sourceUrl, repoId, repoSlug, summary, contextBuilder, now, candidate, problem, opportunity) {
|
|
139
|
+
const policy = workflowRegistry.resolveTaskExecutionPolicy(kind);
|
|
140
|
+
const context = contextBuilder.build({
|
|
141
|
+
taskKind: kind,
|
|
142
|
+
repoIdentifier: repoSlug,
|
|
143
|
+
sourceKind,
|
|
144
|
+
sourceUrl,
|
|
145
|
+
labels: [],
|
|
146
|
+
...(candidate ? { retrospectiveCandidate: candidate } : {}),
|
|
147
|
+
...(problem ? { retrospectiveProblem: problem } : {}),
|
|
148
|
+
...(opportunity ? { learningOpportunity: opportunity } : {}),
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
id,
|
|
152
|
+
kind,
|
|
153
|
+
status: "pending",
|
|
154
|
+
executionPermission: policy.executionPermission,
|
|
155
|
+
handoffPolicy: policy.handoffPolicy,
|
|
156
|
+
repoId,
|
|
157
|
+
repoName: repoSlug,
|
|
158
|
+
repoSlug,
|
|
159
|
+
sourceKind,
|
|
160
|
+
sourceId,
|
|
161
|
+
sourceUrl,
|
|
162
|
+
relevant: true,
|
|
163
|
+
reason: "Idle retrospective work",
|
|
164
|
+
summary,
|
|
165
|
+
context,
|
|
166
|
+
createdAt: now,
|
|
167
|
+
updatedAt: now,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { normalizeRetrospectiveScopes } from "./scopes.js";
|
|
2
|
+
export async function ingestRetrospectiveExecutions(input) {
|
|
3
|
+
const { tasks, executions, candidates, problems, opportunities, proposals, readArtifact, now, } = input;
|
|
4
|
+
const resultCandidates = [...candidates];
|
|
5
|
+
const resultProblems = [...problems];
|
|
6
|
+
const resultOpportunities = [...opportunities];
|
|
7
|
+
const resultProposals = [...proposals];
|
|
8
|
+
const consumedExecutionIds = [];
|
|
9
|
+
const declined = [];
|
|
10
|
+
// Filter for retrospective tasks and completed executions
|
|
11
|
+
const retrospectiveTasks = tasks.filter((t) => [
|
|
12
|
+
"extract_retrospective_problems",
|
|
13
|
+
"investigate_learning_opportunity",
|
|
14
|
+
"draft_management_learning_proposal",
|
|
15
|
+
].includes(t.kind));
|
|
16
|
+
const completedExecutions = executions.filter((e) => e.taskStatus === "completed" &&
|
|
17
|
+
retrospectiveTasks.some((t) => t.id === e.taskId));
|
|
18
|
+
for (const execution of completedExecutions) {
|
|
19
|
+
const task = retrospectiveTasks.find((t) => t.id === execution.taskId);
|
|
20
|
+
// Skip if already consumed
|
|
21
|
+
if (isExecutionConsumed(execution.id, resultCandidates, resultProblems, resultOpportunities, resultProposals)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const stdout = await readArtifact(execution.stdoutArtifactPath);
|
|
26
|
+
const json = extractJson(stdout);
|
|
27
|
+
if (!json) {
|
|
28
|
+
throw new Error("No valid JSON found in stdout");
|
|
29
|
+
}
|
|
30
|
+
switch (task.kind) {
|
|
31
|
+
case "extract_retrospective_problems":
|
|
32
|
+
await ingestExtraction(json, execution, task, resultCandidates, resultProblems, now);
|
|
33
|
+
break;
|
|
34
|
+
case "investigate_learning_opportunity":
|
|
35
|
+
await ingestInvestigation(json, execution, task, resultProblems, resultOpportunities, now);
|
|
36
|
+
break;
|
|
37
|
+
case "draft_management_learning_proposal":
|
|
38
|
+
await ingestProposal(json, execution, task, resultOpportunities, resultProposals, now);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
consumedExecutionIds.push(execution.id);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const reason = err.message;
|
|
45
|
+
declined.push({ executionId: execution.id, taskId: execution.taskId, reason });
|
|
46
|
+
handleIngestionFailure(execution, task, resultCandidates, resultProblems, resultOpportunities, resultProposals, reason, now);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
candidates: resultCandidates,
|
|
51
|
+
problems: resultProblems,
|
|
52
|
+
opportunities: resultOpportunities,
|
|
53
|
+
proposals: resultProposals,
|
|
54
|
+
consumedExecutionIds,
|
|
55
|
+
declined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function isExecutionConsumed(executionId, candidates, problems, opportunities, proposals) {
|
|
59
|
+
return (candidates.some((c) => c.sourceExecutionId === executionId) ||
|
|
60
|
+
problems.some((p) => p.sourceExecutionId === executionId) ||
|
|
61
|
+
opportunities.some((o) => o.sourceExecutionId === executionId) ||
|
|
62
|
+
proposals.some((p) => p.sourceExecutionId === executionId));
|
|
63
|
+
}
|
|
64
|
+
function extractJson(stdout) {
|
|
65
|
+
const match = stdout.match(/\{[\s\S]*\}/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(match[0]);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function ingestExtraction(json, execution, task, candidates, problems, now) {
|
|
76
|
+
const problemsArray = json.problems;
|
|
77
|
+
let sourceItems;
|
|
78
|
+
let explicitEmpty = false;
|
|
79
|
+
if (problemsArray !== undefined) {
|
|
80
|
+
if (!Array.isArray(problemsArray)) {
|
|
81
|
+
throw new Error("Field 'problems' must be an array");
|
|
82
|
+
}
|
|
83
|
+
sourceItems = problemsArray;
|
|
84
|
+
if (sourceItems.length === 0) {
|
|
85
|
+
explicitEmpty = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Fallback: treat the whole object as a single problem result
|
|
90
|
+
sourceItems = [json];
|
|
91
|
+
}
|
|
92
|
+
// Validate all items if not explicitly empty
|
|
93
|
+
if (!explicitEmpty) {
|
|
94
|
+
for (const [index, prob] of sourceItems.entries()) {
|
|
95
|
+
if (!validateExtractionProblem(prob, task.sourceId)) {
|
|
96
|
+
const location = problemsArray !== undefined ? `at index ${index}` : "at top-level";
|
|
97
|
+
throw new Error(`Invalid problem record ${location}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// All valid, ingest them
|
|
101
|
+
for (const prob of sourceItems) {
|
|
102
|
+
const { semanticScopes, candidateLearningScopes } = normalizeRetrospectiveScopes({
|
|
103
|
+
rawSuggestedScopes: prob.suggestedScopes,
|
|
104
|
+
affectedRepoIds: prob.affectedRepos,
|
|
105
|
+
roles: prob.affectedRoles,
|
|
106
|
+
taskKinds: prob.affectedTaskKinds,
|
|
107
|
+
providers: prob.affectedProviders,
|
|
108
|
+
});
|
|
109
|
+
const problemRecord = {
|
|
110
|
+
id: `retrospective-problem:${prob.candidateId}:${hashProse(prob.problemSummary).slice(0, 8)}`,
|
|
111
|
+
candidateId: prob.candidateId,
|
|
112
|
+
status: "pending",
|
|
113
|
+
evidenceReferences: prob.evidenceReferences,
|
|
114
|
+
affectedRepoIds: prob.affectedRepos,
|
|
115
|
+
roles: prob.affectedRoles,
|
|
116
|
+
taskKinds: prob.affectedTaskKinds,
|
|
117
|
+
providers: prob.affectedProviders,
|
|
118
|
+
confidence: prob.confidence,
|
|
119
|
+
impact: prob.impact,
|
|
120
|
+
transferability: prob.transferability,
|
|
121
|
+
improvementLikelihood: prob.improvementLikelihood,
|
|
122
|
+
overfittingRisk: prob.overfittingRisk,
|
|
123
|
+
suggestedScopes: candidateLearningScopes,
|
|
124
|
+
rawSuggestedScopes: prob.suggestedScopes,
|
|
125
|
+
semanticSuggestedScopes: semanticScopes,
|
|
126
|
+
detectedAt: now,
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
sourceTaskId: task.id,
|
|
129
|
+
sourceExecutionId: execution.id,
|
|
130
|
+
structuredObservations: prob.problemSummary + "\n\n" + prob.reasonItMightRecur,
|
|
131
|
+
};
|
|
132
|
+
problems.push(problemRecord);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const candidate = candidates.find((c) => c.id === task.sourceId);
|
|
136
|
+
if (candidate) {
|
|
137
|
+
candidate.status = explicitEmpty ? "skipped" : "extracted";
|
|
138
|
+
if (explicitEmpty) {
|
|
139
|
+
candidate.skipReason = "Agent reported no problems found";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
delete candidate.skipReason;
|
|
143
|
+
}
|
|
144
|
+
candidate.sourceExecutionId = execution.id;
|
|
145
|
+
candidate.sourceTaskId = task.id;
|
|
146
|
+
candidate.updatedAt = now;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function validateExtractionProblem(prob, expectedCandidateId) {
|
|
150
|
+
return (prob.candidateId === expectedCandidateId &&
|
|
151
|
+
typeof prob.problemSummary === "string" &&
|
|
152
|
+
Array.isArray(prob.evidenceReferences) &&
|
|
153
|
+
Array.isArray(prob.affectedRepos) &&
|
|
154
|
+
Array.isArray(prob.affectedRoles) &&
|
|
155
|
+
Array.isArray(prob.affectedTaskKinds) &&
|
|
156
|
+
Array.isArray(prob.affectedProviders) &&
|
|
157
|
+
Array.isArray(prob.suggestedScopes) &&
|
|
158
|
+
typeof prob.reasonItMightRecur === "string");
|
|
159
|
+
}
|
|
160
|
+
async function ingestInvestigation(json, execution, task, problems, opportunities, now) {
|
|
161
|
+
if (!validateInvestigation(json, task.sourceId)) {
|
|
162
|
+
throw new Error("Invalid investigation JSON");
|
|
163
|
+
}
|
|
164
|
+
const opportunityRecord = {
|
|
165
|
+
id: `learning-opportunity:${json.problemId}`,
|
|
166
|
+
problemId: json.problemId,
|
|
167
|
+
status: "investigated",
|
|
168
|
+
investigationSummary: json.investigationSummary,
|
|
169
|
+
investigationResult: json.investigationResult,
|
|
170
|
+
selectedScope: json.selectedScope,
|
|
171
|
+
evidenceReferences: json.evidenceReferences,
|
|
172
|
+
detectedAt: now,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
sourceTaskId: task.id,
|
|
175
|
+
sourceExecutionId: execution.id,
|
|
176
|
+
};
|
|
177
|
+
opportunities.push(opportunityRecord);
|
|
178
|
+
const problem = problems.find((p) => p.id === task.sourceId);
|
|
179
|
+
if (problem) {
|
|
180
|
+
problem.status = "completed";
|
|
181
|
+
problem.sourceExecutionId = execution.id;
|
|
182
|
+
problem.sourceTaskId = task.id;
|
|
183
|
+
problem.updatedAt = now;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateInvestigation(json, expectedProblemId) {
|
|
187
|
+
return (json.problemId === expectedProblemId &&
|
|
188
|
+
typeof json.investigationSummary === "string" &&
|
|
189
|
+
typeof json.investigationResult === "string" &&
|
|
190
|
+
Array.isArray(json.evidenceReferences) &&
|
|
191
|
+
validateLearningScopeStructure(json.selectedScope));
|
|
192
|
+
}
|
|
193
|
+
async function ingestProposal(json, execution, task, opportunities, proposals, now) {
|
|
194
|
+
if (!validateProposal(json, task.sourceId)) {
|
|
195
|
+
throw new Error("Invalid proposal JSON");
|
|
196
|
+
}
|
|
197
|
+
// Avoid duplicate proposals for the same opportunity
|
|
198
|
+
if (proposals.some(p => p.opportunityId === json.opportunityId)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const proposalRecord = {
|
|
202
|
+
id: `learning-proposal:${json.opportunityId}`,
|
|
203
|
+
opportunityId: json.opportunityId,
|
|
204
|
+
problemId: json.problemId,
|
|
205
|
+
status: "pending",
|
|
206
|
+
selectedScope: json.selectedScope,
|
|
207
|
+
proposedEditMode: json.proposedEditMode,
|
|
208
|
+
proposedGuidanceText: json.proposedGuidanceText,
|
|
209
|
+
rationale: json.rationale,
|
|
210
|
+
expectedBehaviorChange: json.expectedBehaviorChange,
|
|
211
|
+
detectedAt: now,
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
sourceTaskId: task.id,
|
|
214
|
+
sourceExecutionId: execution.id,
|
|
215
|
+
};
|
|
216
|
+
proposals.push(proposalRecord);
|
|
217
|
+
// Mark execution as consumed on the opportunity to prevent re-derivation
|
|
218
|
+
const opportunity = opportunities.find(o => o.id === task.sourceId);
|
|
219
|
+
if (opportunity) {
|
|
220
|
+
opportunity.sourceExecutionId = execution.id;
|
|
221
|
+
opportunity.sourceTaskId = task.id;
|
|
222
|
+
opportunity.updatedAt = now;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validateProposal(json, expectedOpportunityId) {
|
|
226
|
+
const validEditModes = ["append", "refine", "supersede", "create"];
|
|
227
|
+
return (json.opportunityId === expectedOpportunityId &&
|
|
228
|
+
validateLearningScopeStructure(json.selectedScope) &&
|
|
229
|
+
validEditModes.includes(json.proposedEditMode) &&
|
|
230
|
+
typeof json.proposedGuidanceText === "string" &&
|
|
231
|
+
json.proposedGuidanceText.trim().length > 0 &&
|
|
232
|
+
typeof json.rationale === "string" &&
|
|
233
|
+
json.rationale.trim().length > 0 &&
|
|
234
|
+
typeof json.expectedBehaviorChange === "string" &&
|
|
235
|
+
json.expectedBehaviorChange.trim().length > 0);
|
|
236
|
+
}
|
|
237
|
+
function handleIngestionFailure(execution, task, candidates, problems, opportunities, proposals, reason, now) {
|
|
238
|
+
switch (task.kind) {
|
|
239
|
+
case "extract_retrospective_problems":
|
|
240
|
+
const candidate = candidates.find((c) => c.id === task.sourceId);
|
|
241
|
+
if (candidate) {
|
|
242
|
+
candidate.status = "failed";
|
|
243
|
+
candidate.skipReason = `Ingestion failed: ${reason}`;
|
|
244
|
+
candidate.sourceExecutionId = execution.id;
|
|
245
|
+
candidate.sourceTaskId = task.id;
|
|
246
|
+
candidate.updatedAt = now;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "investigate_learning_opportunity":
|
|
250
|
+
const problem = problems.find((p) => p.id === task.sourceId);
|
|
251
|
+
if (problem) {
|
|
252
|
+
problem.status = "failed";
|
|
253
|
+
problem.decisionReason = `Ingestion failed: ${reason}`;
|
|
254
|
+
problem.sourceExecutionId = execution.id;
|
|
255
|
+
problem.sourceTaskId = task.id;
|
|
256
|
+
problem.updatedAt = now;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "draft_management_learning_proposal":
|
|
260
|
+
const opportunity = opportunities.find((o) => o.id === task.sourceId);
|
|
261
|
+
if (opportunity) {
|
|
262
|
+
opportunity.status = "declined";
|
|
263
|
+
opportunity.decisionReason = `Ingestion failed: ${reason}`;
|
|
264
|
+
opportunity.sourceExecutionId = execution.id;
|
|
265
|
+
opportunity.sourceTaskId = task.id;
|
|
266
|
+
opportunity.updatedAt = now;
|
|
267
|
+
}
|
|
268
|
+
// Also persist a declined proposal record if it doesn't already exist
|
|
269
|
+
if (!proposals.some(p => p.opportunityId === task.sourceId)) {
|
|
270
|
+
proposals.push({
|
|
271
|
+
id: `learning-proposal:${task.sourceId}`,
|
|
272
|
+
opportunityId: task.sourceId,
|
|
273
|
+
status: "declined",
|
|
274
|
+
declineReason: `Ingestion failed: ${reason}`,
|
|
275
|
+
detectedAt: now,
|
|
276
|
+
updatedAt: now,
|
|
277
|
+
sourceTaskId: task.id,
|
|
278
|
+
sourceExecutionId: execution.id,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function validateLearningScopeStructure(scope) {
|
|
285
|
+
if (!scope || typeof scope !== "object" || Array.isArray(scope))
|
|
286
|
+
return false;
|
|
287
|
+
if (typeof scope.kind !== "string")
|
|
288
|
+
return false;
|
|
289
|
+
switch (scope.kind) {
|
|
290
|
+
case "global":
|
|
291
|
+
return true;
|
|
292
|
+
case "provider":
|
|
293
|
+
return typeof scope.provider === "string" && scope.provider.length > 0;
|
|
294
|
+
case "role":
|
|
295
|
+
return typeof scope.role === "string" && scope.role.length > 0;
|
|
296
|
+
case "task_kind":
|
|
297
|
+
return typeof scope.taskKind === "string" && scope.taskKind.length > 0;
|
|
298
|
+
case "repo":
|
|
299
|
+
return typeof scope.repoId === "string" && scope.repoId.length > 0;
|
|
300
|
+
case "repo_provider":
|
|
301
|
+
return (typeof scope.repoId === "string" && scope.repoId.length > 0 &&
|
|
302
|
+
typeof scope.provider === "string" && scope.provider.length > 0);
|
|
303
|
+
case "repo_role":
|
|
304
|
+
return (typeof scope.repoId === "string" && scope.repoId.length > 0 &&
|
|
305
|
+
typeof scope.role === "string" && scope.role.length > 0);
|
|
306
|
+
case "repo_task_kind":
|
|
307
|
+
return (typeof scope.repoId === "string" && scope.repoId.length > 0 &&
|
|
308
|
+
typeof scope.taskKind === "string" && scope.taskKind.length > 0);
|
|
309
|
+
default:
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function hashProse(text) {
|
|
314
|
+
let hash = 0;
|
|
315
|
+
for (let i = 0; i < text.length; i++) {
|
|
316
|
+
const char = text.charCodeAt(i);
|
|
317
|
+
hash = ((hash << 5) - hash) + char;
|
|
318
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
319
|
+
}
|
|
320
|
+
return Math.abs(hash).toString(16);
|
|
321
|
+
}
|