@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,125 @@
|
|
|
1
|
+
import { createEntityId } from "../../../github/normalize.js";
|
|
2
|
+
import { extractRetryCommentId } from "../../retry.js";
|
|
3
|
+
export const retryFailedTaskHandler = {
|
|
4
|
+
name: "retry_failed_task",
|
|
5
|
+
derive(context, env, sink) {
|
|
6
|
+
for (const task of deriveRetryTasks(context, env, sink)) {
|
|
7
|
+
sink.replace(task);
|
|
8
|
+
}
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const RETRY_REASON_SUFFIX = "Retry requested by a human comment after a technical failure.";
|
|
13
|
+
function deriveRetryTasks(context, env, sink) {
|
|
14
|
+
const legacyRetryTargetCommentId = getLegacyRetryTargetCommentId(context, env);
|
|
15
|
+
if (!context.attention?.reasons.includes("retry_requested") && !legacyRetryTargetCommentId) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const retryRequests = collectRetryRequests(context, env, legacyRetryTargetCommentId);
|
|
19
|
+
if (retryRequests.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const requeuedTasks = new Map();
|
|
23
|
+
for (const request of retryRequests) {
|
|
24
|
+
for (const failedTask of findMatchingFailedTasks(request, context, env)) {
|
|
25
|
+
// prepare_pr_for_merge retry depends on normal derivation having already built
|
|
26
|
+
// the current-cycle task; keep retryFailedTaskHandler after parent handlers.
|
|
27
|
+
const currentDesiredTask = sink.get(failedTask.id);
|
|
28
|
+
if (failedTask.kind === "prepare_pr_for_merge" && !currentDesiredTask) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const baseTask = currentDesiredTask ?? failedTask;
|
|
32
|
+
requeuedTasks.set(failedTask.id, {
|
|
33
|
+
...baseTask,
|
|
34
|
+
status: "pending",
|
|
35
|
+
context: {
|
|
36
|
+
...baseTask.context,
|
|
37
|
+
recoveryNeeded: true
|
|
38
|
+
},
|
|
39
|
+
reason: appendRetryReason(baseTask.reason),
|
|
40
|
+
...(failedTask.lastExecutionId ? { lastExecutionId: failedTask.lastExecutionId } : {}),
|
|
41
|
+
...(failedTask.lastFailureCommentId ? { lastFailureCommentId: failedTask.lastFailureCommentId } : {}),
|
|
42
|
+
updatedAt: env.input.now
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...requeuedTasks.values()];
|
|
47
|
+
}
|
|
48
|
+
function collectRetryRequests(context, env, legacyRetryTargetCommentId) {
|
|
49
|
+
const requests = new Map();
|
|
50
|
+
for (const commentId of context.attention?.commentIds ?? []) {
|
|
51
|
+
const comment = env.currentCommentsById.get(commentId);
|
|
52
|
+
const targetFailureCommentId = comment ? extractRetryCommentId(comment.body) : undefined;
|
|
53
|
+
if (!targetFailureCommentId || requests.has(targetFailureCommentId)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
requests.set(targetFailureCommentId, {
|
|
57
|
+
targetFailureCommentId
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (requests.size === 0 && legacyRetryTargetCommentId) {
|
|
61
|
+
requests.set(legacyRetryTargetCommentId, {
|
|
62
|
+
targetFailureCommentId: legacyRetryTargetCommentId
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return [...requests.values()];
|
|
66
|
+
}
|
|
67
|
+
function findMatchingFailedTasks(request, context, env) {
|
|
68
|
+
const candidates = env.existingTasks.filter((task) => task.status === "failed" &&
|
|
69
|
+
task.lastFailureCommentId === request.targetFailureCommentId &&
|
|
70
|
+
taskMatchesRepo(task, context));
|
|
71
|
+
const modernMatches = candidates.filter((task) => task.workstreamId === context.workstream.id);
|
|
72
|
+
if (modernMatches.length > 1) {
|
|
73
|
+
console.warn(`[retry_failed_task] Multiple modern tasks matched failure comment ${request.targetFailureCommentId} for workstream ${context.workstream.id}; retrying all: ${modernMatches.map((task) => task.id).join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
if (modernMatches.length > 0) {
|
|
76
|
+
return modernMatches;
|
|
77
|
+
}
|
|
78
|
+
const legacySourceIds = buildLegacyWorkstreamSourceIds(context);
|
|
79
|
+
const legacyMatches = candidates.filter((task) => task.workstreamId === undefined && legacyTaskMatchesCurrentWorkstreamSource(task, legacySourceIds));
|
|
80
|
+
if (legacyMatches.length > 1) {
|
|
81
|
+
console.warn(`[retry_failed_task] Ignoring ambiguous legacy retry target ${request.targetFailureCommentId} for workstream ${context.workstream.id}; matched tasks: ${legacyMatches.map((task) => task.id).join(", ")}`);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return legacyMatches.length === 1 ? legacyMatches : [];
|
|
85
|
+
}
|
|
86
|
+
function appendRetryReason(reason) {
|
|
87
|
+
const suffix = ` ${RETRY_REASON_SUFFIX}`;
|
|
88
|
+
let baseReason = reason;
|
|
89
|
+
while (baseReason.endsWith(suffix)) {
|
|
90
|
+
baseReason = baseReason.slice(0, -suffix.length);
|
|
91
|
+
}
|
|
92
|
+
return `${baseReason}${suffix}`;
|
|
93
|
+
}
|
|
94
|
+
function getLegacyRetryTargetCommentId(context, env) {
|
|
95
|
+
return env.input.previousState.attention.find((record) => record.workstreamId === context.workstream.id)?.retryTargetCommentId;
|
|
96
|
+
}
|
|
97
|
+
function taskMatchesRepo(task, context) {
|
|
98
|
+
if (task.repoSlug === context.workstream.repoKey) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return Boolean(context.repoConfig && task.repoId === context.repoConfig.id);
|
|
102
|
+
}
|
|
103
|
+
function legacyTaskMatchesCurrentWorkstreamSource(task, sourceIds) {
|
|
104
|
+
return (sourceIds.has(task.sourceId) ||
|
|
105
|
+
(task.parentSourceId !== undefined && sourceIds.has(task.parentSourceId)));
|
|
106
|
+
}
|
|
107
|
+
function buildLegacyWorkstreamSourceIds(context) {
|
|
108
|
+
const sourceIds = new Set();
|
|
109
|
+
if (context.issue) {
|
|
110
|
+
sourceIds.add(context.issue.id);
|
|
111
|
+
}
|
|
112
|
+
if (context.activePullRequest) {
|
|
113
|
+
sourceIds.add(context.activePullRequest.id);
|
|
114
|
+
}
|
|
115
|
+
for (const prNumber of context.attention?.pullRequestNumbers ?? []) {
|
|
116
|
+
sourceIds.add(createEntityId("pull_request", context.workstream.repoKey, prNumber));
|
|
117
|
+
}
|
|
118
|
+
if (context.workstream.origin.type === "pr") {
|
|
119
|
+
sourceIds.add(createEntityId("pull_request", context.workstream.repoKey, context.workstream.origin.number));
|
|
120
|
+
}
|
|
121
|
+
if (context.workstream.origin.type === "issue") {
|
|
122
|
+
sourceIds.add(createEntityId("issue", context.workstream.repoKey, context.workstream.origin.number));
|
|
123
|
+
}
|
|
124
|
+
return sourceIds;
|
|
125
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createPullRequestTask } from "../builders.js";
|
|
2
|
+
export const reviewPullRequestHandler = {
|
|
3
|
+
name: "review_pr",
|
|
4
|
+
derive(context, env) {
|
|
5
|
+
const task = deriveReviewTask(context, env);
|
|
6
|
+
return task ? [task] : [];
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function deriveReviewTask(context, env) {
|
|
10
|
+
const { workstream, attention, activePullRequest, activeWorktree, repoConfig, repoSyncRecord } = context;
|
|
11
|
+
if (!attention || !activePullRequest || !repoConfig) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const reviewRequested = attention.reasons.includes("pr_review_requested");
|
|
15
|
+
const reviewNeedsAction = reviewRequested &&
|
|
16
|
+
(!attention.lastHandledReviewHeadSha ||
|
|
17
|
+
attention.lastHandledReviewHeadSha !== attention.currentPullRequestHeadSha);
|
|
18
|
+
const isReviewWorkstream = workstream.origin.type === "pr" || workstream.phase === "pr_review";
|
|
19
|
+
if (!reviewNeedsAction || !isReviewWorkstream) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return createPullRequestTask("review_pr", activePullRequest, repoSyncRecord, {
|
|
23
|
+
reason: `Pull request has the "${repoConfig.labels.agentReview}" label and code changed (head: ${activePullRequest.headSha || "unknown"}).`,
|
|
24
|
+
summary: `Review PR #${activePullRequest.number}: ${activePullRequest.title}`,
|
|
25
|
+
contextBuilder: env.input.contextBuilder,
|
|
26
|
+
now: env.input.now
|
|
27
|
+
}, activeWorktree, workstream.id);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function isActiveParentWorkstream(workstream) {
|
|
2
|
+
return (workstream.status === "open" &&
|
|
3
|
+
workstream.phase !== "abandoned" &&
|
|
4
|
+
workstream.phase !== "resolved");
|
|
5
|
+
}
|
|
6
|
+
export function resolvePacketPullRequest(worktree, currentPrsByRepoAndNumber) {
|
|
7
|
+
if (worktree.prNumber !== undefined) {
|
|
8
|
+
const pr = currentPrsByRepoAndNumber.get(`${worktree.repoSlug}:${worktree.prNumber}`);
|
|
9
|
+
if (pr) {
|
|
10
|
+
return pr;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (worktree.prUrl) {
|
|
14
|
+
const match = worktree.prUrl.match(/\/pull\/(\d+)$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const prNumber = Number(match[1]);
|
|
17
|
+
const pr = currentPrsByRepoAndNumber.get(`${worktree.repoSlug}:${prNumber}`);
|
|
18
|
+
if (pr) {
|
|
19
|
+
return pr;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { buildParentBranchSyncPreflight } from "../../../workstreams/packet-plan.js";
|
|
2
|
+
import { buildTaskRecord } from "../builders.js";
|
|
3
|
+
export const syncParentBranchPreflightHandler = {
|
|
4
|
+
name: "sync_parent_branch_preflight",
|
|
5
|
+
derive(context, env) {
|
|
6
|
+
const task = deriveSyncParentBranchPreflightTask(context, env);
|
|
7
|
+
return task ? [task] : [];
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const syncParentBranchHandler = {
|
|
11
|
+
name: "sync_parent_branch",
|
|
12
|
+
derive(context, env) {
|
|
13
|
+
const task = deriveSyncParentBranchTask(context, env);
|
|
14
|
+
return task ? [task] : [];
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function deriveSyncParentBranchPreflightTask(context, env) {
|
|
18
|
+
const { workstream, attention, issue, repoConfig, repoSyncRecord } = context;
|
|
19
|
+
if (workstream.origin.type !== "issue" ||
|
|
20
|
+
!issue ||
|
|
21
|
+
!attention?.reasons.includes("packet_wave_complete_sync_needed") ||
|
|
22
|
+
!workstream.packetPlan) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const defaultBranch = repoConfig?.defaultBranch ?? repoSyncRecord?.defaultBranch ?? undefined;
|
|
26
|
+
const preflight = buildParentBranchSyncPreflight({ workstream, defaultBranch });
|
|
27
|
+
if (!preflight) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const reason = [
|
|
31
|
+
`Current packet wave ${preflight.currentWaveIndex} is complete and a future wave exists.`,
|
|
32
|
+
`Parent integration branch: ${preflight.parentIntegrationBranch}`,
|
|
33
|
+
`Default branch: ${preflight.defaultBranch}`,
|
|
34
|
+
`Next wave index: ${preflight.nextWaveIndex}`,
|
|
35
|
+
`Completed packet IDs: ${preflight.completedPacketIds.join(", ")}`,
|
|
36
|
+
`Pending future packet IDs: ${preflight.pendingFuturePacketIds.join(", ")}`,
|
|
37
|
+
preflight.parentPrNumber ? `Parent PR: #${preflight.parentPrNumber}` : undefined
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(" ");
|
|
41
|
+
return buildTaskRecord(`task:sync_parent_branch_preflight:${workstream.id}`, "sync_parent_branch_preflight", issue, repoSyncRecord, {
|
|
42
|
+
reason,
|
|
43
|
+
summary: `Inspect parent branch sync before packet wave ${preflight.nextWaveIndex} for issue #${issue.number}`,
|
|
44
|
+
contextBuilder: env.input.contextBuilder,
|
|
45
|
+
now: env.input.now
|
|
46
|
+
}, env.input.contextBuilder.build({
|
|
47
|
+
taskKind: "sync_parent_branch_preflight",
|
|
48
|
+
repoIdentifier: issue.repoSlug,
|
|
49
|
+
sourceKind: "issue",
|
|
50
|
+
sourceUrl: issue.url,
|
|
51
|
+
labels: issue.labels,
|
|
52
|
+
issueTitle: issue.title,
|
|
53
|
+
issueBody: issue.body,
|
|
54
|
+
preflight,
|
|
55
|
+
...(repoSyncRecord?.localPath ? { localRepoPath: repoSyncRecord.localPath } : {})
|
|
56
|
+
}), workstream.id);
|
|
57
|
+
}
|
|
58
|
+
function deriveSyncParentBranchTask(context, env) {
|
|
59
|
+
const { workstream, attention, issue, repoConfig, repoSyncRecord } = context;
|
|
60
|
+
if (workstream.origin.type !== "issue" ||
|
|
61
|
+
!issue ||
|
|
62
|
+
issue.state !== "open" ||
|
|
63
|
+
!attention?.reasons.includes("packet_wave_complete_sync_needed") ||
|
|
64
|
+
!workstream.packetPlan ||
|
|
65
|
+
!repoConfig) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const defaultBranch = repoConfig.defaultBranch ?? repoSyncRecord?.defaultBranch ?? undefined;
|
|
69
|
+
const preflight = buildParentBranchSyncPreflight({ workstream, defaultBranch });
|
|
70
|
+
if (!preflight || !preflight.parentPrNumber) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const parentPr = env.currentPrsByRepoAndNumber.get(`${workstream.repoKey}:${preflight.parentPrNumber}`);
|
|
74
|
+
if (!parentPr || parentPr.state !== "open" || !parentPr.labels.includes(repoConfig.labels.agentSyncParentBranch)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const reason = [
|
|
78
|
+
`Explicit sync requested via label "${repoConfig.labels.agentSyncParentBranch}" on parent PR #${parentPr.number}.`,
|
|
79
|
+
`Current packet wave ${preflight.currentWaveIndex} is complete and a future wave exists.`,
|
|
80
|
+
`Parent integration branch: ${preflight.parentIntegrationBranch}`,
|
|
81
|
+
`Default branch: ${preflight.defaultBranch}`,
|
|
82
|
+
`Next wave index: ${preflight.nextWaveIndex}`,
|
|
83
|
+
`Completed packet IDs: ${preflight.completedPacketIds.join(", ")}`,
|
|
84
|
+
`Pending future packet IDs: ${preflight.pendingFuturePacketIds.join(", ")}`
|
|
85
|
+
].join(" ");
|
|
86
|
+
return buildTaskRecord(`task:sync_parent_branch:${parentPr.id}`, "sync_parent_branch", parentPr, repoSyncRecord, {
|
|
87
|
+
reason,
|
|
88
|
+
summary: `Sync parent branch for issue #${issue.number} before packet wave ${preflight.nextWaveIndex}`,
|
|
89
|
+
contextBuilder: env.input.contextBuilder,
|
|
90
|
+
now: env.input.now
|
|
91
|
+
}, env.input.contextBuilder.build({
|
|
92
|
+
taskKind: "sync_parent_branch",
|
|
93
|
+
repoIdentifier: parentPr.repoSlug,
|
|
94
|
+
sourceKind: "pull_request",
|
|
95
|
+
sourceUrl: parentPr.url,
|
|
96
|
+
labels: parentPr.labels,
|
|
97
|
+
preflight,
|
|
98
|
+
...(repoSyncRecord?.localPath ? { localRepoPath: repoSyncRecord.localPath } : {})
|
|
99
|
+
}), workstream.id);
|
|
100
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { buildParentBranchSyncPreflight } from "../../../workstreams/packet-plan.js";
|
|
2
|
+
import { buildWaveAssessmentCheckpoint, buildWaveAssessmentCheckpointIdentity } from "../../../workstreams/wave-assessment/checkpoint.js";
|
|
3
|
+
import { buildTaskRecord } from "../builders.js";
|
|
4
|
+
export const waveAssessmentHandler = {
|
|
5
|
+
name: "wave_assessment",
|
|
6
|
+
derive(context, env) {
|
|
7
|
+
const assessmentTask = deriveWavePlanAssessmentTask(context, env);
|
|
8
|
+
if (assessmentTask) {
|
|
9
|
+
return [assessmentTask];
|
|
10
|
+
}
|
|
11
|
+
const replanningTask = deriveWavePlanReplanningTask(context, env);
|
|
12
|
+
return replanningTask ? [replanningTask] : [];
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export function isWaveAssessmentCheckpointAllowed(context, env) {
|
|
16
|
+
const checkpoint = resolveWaveAssessmentCheckpoint(context, env);
|
|
17
|
+
if (!checkpoint) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const assessment = findMatchingPublishedFinalizedWaveAssessment(context.workstream, env, checkpoint.identity);
|
|
21
|
+
return assessment?.decision === "continue";
|
|
22
|
+
}
|
|
23
|
+
export function hasOpenWaveAssessmentParentPullRequest(context, env) {
|
|
24
|
+
const checkpoint = resolveWaveAssessmentCheckpoint(context, env);
|
|
25
|
+
if (!checkpoint) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const parentPrNumber = context.workstream.integration?.prNumber;
|
|
29
|
+
const parentPr = parentPrNumber !== undefined
|
|
30
|
+
? env.currentPrsByRepoAndNumber.get(`${context.workstream.repoKey}:${parentPrNumber}`)
|
|
31
|
+
: undefined;
|
|
32
|
+
return parentPr?.state === "open";
|
|
33
|
+
}
|
|
34
|
+
function deriveWavePlanAssessmentTask(context, env) {
|
|
35
|
+
const resolved = resolveWaveAssessmentTaskContext(context, env);
|
|
36
|
+
if (!resolved) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const { checkpoint, issue, parentPr, repoSyncRecord, workstream } = resolved;
|
|
40
|
+
if (findMatchingPublishedFinalizedWaveAssessment(workstream, env, checkpoint.identity)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const unpublishedAssessment = findMatchingFinalizedWaveAssessment(workstream, env, checkpoint.identity, { requirePostedComment: false });
|
|
44
|
+
const taskId = buildWaveAssessmentTaskId(checkpoint);
|
|
45
|
+
if (hasUnresolvedTask(env, taskId)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return buildTaskRecord(taskId, "wave_plan_assessment", parentPr, repoSyncRecord, {
|
|
49
|
+
reason: [
|
|
50
|
+
`Current packet wave ${checkpoint.identity.currentWaveIndex} is complete and future packets remain.`,
|
|
51
|
+
`Parent PR #${parentPr.number} is open.`,
|
|
52
|
+
unpublishedAssessment && !unpublishedAssessment.postedCommentId
|
|
53
|
+
? "A finalized wave assessment exists, but its parent PR comment was not posted; rerun the assessment publication path."
|
|
54
|
+
: "No finalized wave assessment exists for this checkpoint."
|
|
55
|
+
].join(" "),
|
|
56
|
+
summary: `Assess completed packet wave ${checkpoint.identity.currentWaveIndex} before wave ${checkpoint.identity.nextWaveIndex}`,
|
|
57
|
+
contextBuilder: env.input.contextBuilder,
|
|
58
|
+
now: env.input.now
|
|
59
|
+
}, env.input.contextBuilder.build({
|
|
60
|
+
taskKind: "wave_plan_assessment",
|
|
61
|
+
repoIdentifier: parentPr.repoSlug,
|
|
62
|
+
sourceKind: "pull_request",
|
|
63
|
+
sourceUrl: parentPr.url,
|
|
64
|
+
parentUrl: issue.url,
|
|
65
|
+
labels: parentPr.labels,
|
|
66
|
+
issueTitle: issue.title,
|
|
67
|
+
issueBody: issue.body,
|
|
68
|
+
waveAssessmentCheckpoint: checkpoint,
|
|
69
|
+
...(repoSyncRecord?.localPath ? { localRepoPath: repoSyncRecord.localPath } : {})
|
|
70
|
+
}), workstream.id);
|
|
71
|
+
}
|
|
72
|
+
function deriveWavePlanReplanningTask(context, env) {
|
|
73
|
+
const resolved = resolveWaveAssessmentTaskContext(context, env);
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const { checkpoint, issue, parentPr, repoSyncRecord, workstream } = resolved;
|
|
78
|
+
const assessment = findMatchingPublishedFinalizedWaveAssessment(workstream, env, checkpoint.identity);
|
|
79
|
+
if (!assessment || assessment.decision !== "replan") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (findMatchingPublishedFinalizedWaveAssessment(workstream, env, checkpoint.identity, { decision: "continue" })) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const taskId = buildWaveReplanningTaskId(checkpoint);
|
|
86
|
+
const existing = env.existingTasksById.get(taskId);
|
|
87
|
+
if (hasUnresolvedTask(env, taskId)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return buildTaskRecord(taskId, "wave_plan_replanning", parentPr, repoSyncRecord, {
|
|
91
|
+
reason: [
|
|
92
|
+
`Finalized wave assessment ${assessment.id} requires replanning.`,
|
|
93
|
+
`Current packet wave ${checkpoint.identity.currentWaveIndex} is complete before wave ${checkpoint.identity.nextWaveIndex}.`,
|
|
94
|
+
existing?.status === "completed"
|
|
95
|
+
? "A previous replanning task completed, but no superseding continue assessment was published; rerun replanning completion."
|
|
96
|
+
: undefined
|
|
97
|
+
].filter((line) => line !== undefined).join(" "),
|
|
98
|
+
summary: `Replan packet workstream after wave assessment ${assessment.id}`,
|
|
99
|
+
contextBuilder: env.input.contextBuilder,
|
|
100
|
+
now: env.input.now
|
|
101
|
+
}, env.input.contextBuilder.build({
|
|
102
|
+
taskKind: "wave_plan_replanning",
|
|
103
|
+
repoIdentifier: parentPr.repoSlug,
|
|
104
|
+
sourceKind: "pull_request",
|
|
105
|
+
sourceUrl: parentPr.url,
|
|
106
|
+
parentUrl: issue.url,
|
|
107
|
+
labels: parentPr.labels,
|
|
108
|
+
issueTitle: issue.title,
|
|
109
|
+
issueBody: issue.body,
|
|
110
|
+
waveAssessmentCheckpoint: checkpoint,
|
|
111
|
+
finalizedWaveAssessment: assessment,
|
|
112
|
+
...(repoSyncRecord?.localPath ? { localRepoPath: repoSyncRecord.localPath } : {})
|
|
113
|
+
}), workstream.id);
|
|
114
|
+
}
|
|
115
|
+
function resolveWaveAssessmentTaskContext(context, env) {
|
|
116
|
+
const { workstream, attention, issue, repoSyncRecord } = context;
|
|
117
|
+
if (workstream.origin.type !== "issue" ||
|
|
118
|
+
!issue ||
|
|
119
|
+
issue.state !== "open" ||
|
|
120
|
+
!attention?.reasons.includes("packet_wave_complete_sync_needed")) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const checkpoint = resolveWaveAssessmentCheckpoint(context, env);
|
|
124
|
+
if (!checkpoint) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const parentPrNumber = workstream.integration?.prNumber;
|
|
128
|
+
const parentPr = parentPrNumber !== undefined
|
|
129
|
+
? env.currentPrsByRepoAndNumber.get(`${workstream.repoKey}:${parentPrNumber}`)
|
|
130
|
+
: undefined;
|
|
131
|
+
if (!parentPr || parentPr.state !== "open") {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return { checkpoint, issue, parentPr, repoSyncRecord, workstream };
|
|
135
|
+
}
|
|
136
|
+
function resolveWaveAssessmentCheckpoint(context, env) {
|
|
137
|
+
const { workstream, repoConfig, repoSyncRecord } = context;
|
|
138
|
+
const defaultBranch = repoConfig?.defaultBranch ?? repoSyncRecord?.defaultBranch ?? undefined;
|
|
139
|
+
const preflight = buildParentBranchSyncPreflight({ workstream, defaultBranch });
|
|
140
|
+
if (!preflight || !preflight.parentPrNumber) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const parentPr = env.currentPrsByRepoAndNumber.get(`${workstream.repoKey}:${preflight.parentPrNumber}`);
|
|
144
|
+
if (!parentPr?.headSha) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return buildWaveAssessmentCheckpoint(buildWaveAssessmentCheckpointIdentity(preflight, {
|
|
148
|
+
parentHeadSha: parentPr.headSha,
|
|
149
|
+
planRevision: resolvePlanRevision(workstream),
|
|
150
|
+
originalPlanningSessionId: resolveOriginalPlanningSessionId(workstream, env),
|
|
151
|
+
planNodeIds: workstream.packetPlan ? Object.keys(workstream.packetPlan.packets).sort() : []
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
function buildWaveAssessmentTaskId(checkpoint) {
|
|
155
|
+
return `task:wave_plan_assessment:${checkpoint.id}`;
|
|
156
|
+
}
|
|
157
|
+
function buildWaveReplanningTaskId(checkpoint) {
|
|
158
|
+
return `task:wave_plan_replanning:${checkpoint.id}`;
|
|
159
|
+
}
|
|
160
|
+
function hasUnresolvedTask(env, taskId) {
|
|
161
|
+
const task = env.existingTasksById.get(taskId);
|
|
162
|
+
return task?.status === "pending" || task?.status === "dispatched" || task?.status === "failed";
|
|
163
|
+
}
|
|
164
|
+
function findMatchingPublishedFinalizedWaveAssessment(workstream, env, identity, options = {}) {
|
|
165
|
+
return findMatchingFinalizedWaveAssessment(workstream, env, identity, { ...options, requirePostedComment: true });
|
|
166
|
+
}
|
|
167
|
+
function findMatchingFinalizedWaveAssessment(workstream, env, identity, options) {
|
|
168
|
+
const persistedAssessments = workstream.packetPlan?.waveAssessments ?? [];
|
|
169
|
+
const executionAssessments = env.input.previousState.executions
|
|
170
|
+
.map(readWaveAssessmentResult)
|
|
171
|
+
.filter((assessment) => assessment !== undefined);
|
|
172
|
+
const assessments = [...persistedAssessments, ...executionAssessments]
|
|
173
|
+
.filter((assessment) => assessment.status === "finalized")
|
|
174
|
+
.filter((assessment) => !options.requirePostedComment || Boolean(assessment.postedCommentId))
|
|
175
|
+
.filter((assessment) => options.decision === undefined || assessment.decision === options.decision)
|
|
176
|
+
.filter((assessment) => checkpointIdentitiesEqual(assessment.checkpoint.identity, identity))
|
|
177
|
+
.sort(compareAssessmentsNewestFirst);
|
|
178
|
+
return assessments[0];
|
|
179
|
+
}
|
|
180
|
+
function readWaveAssessmentResult(execution) {
|
|
181
|
+
const result = execution.result;
|
|
182
|
+
return result?.waveAssessment;
|
|
183
|
+
}
|
|
184
|
+
function compareAssessmentsNewestFirst(left, right) {
|
|
185
|
+
return getAssessmentTimestamp(right).localeCompare(getAssessmentTimestamp(left));
|
|
186
|
+
}
|
|
187
|
+
function getAssessmentTimestamp(assessment) {
|
|
188
|
+
return assessment.finalizedAt ?? assessment.updatedAt ?? assessment.createdAt ?? "";
|
|
189
|
+
}
|
|
190
|
+
function checkpointIdentitiesEqual(left, right) {
|
|
191
|
+
return (left.repoKey === right.repoKey &&
|
|
192
|
+
left.workstreamId === right.workstreamId &&
|
|
193
|
+
left.waveId === right.waveId &&
|
|
194
|
+
left.parentPrNumber === right.parentPrNumber &&
|
|
195
|
+
left.parentHeadSha === right.parentHeadSha &&
|
|
196
|
+
left.parentIntegrationBranch === right.parentIntegrationBranch &&
|
|
197
|
+
left.defaultBranch === right.defaultBranch &&
|
|
198
|
+
left.planRevision === right.planRevision &&
|
|
199
|
+
left.originalPlanningSessionId === right.originalPlanningSessionId &&
|
|
200
|
+
left.currentWaveIndex === right.currentWaveIndex &&
|
|
201
|
+
left.nextWaveIndex === right.nextWaveIndex &&
|
|
202
|
+
arraysEqual(left.completedPacketIds, right.completedPacketIds) &&
|
|
203
|
+
arraysEqual(left.pendingFuturePacketIds, right.pendingFuturePacketIds) &&
|
|
204
|
+
arraysEqual(left.planNodeIds, right.planNodeIds));
|
|
205
|
+
}
|
|
206
|
+
function arraysEqual(left, right) {
|
|
207
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
208
|
+
}
|
|
209
|
+
function resolvePlanRevision(workstream) {
|
|
210
|
+
return (workstream.packetPlan?.source?.generatedAt ??
|
|
211
|
+
workstream.packetPlan?.updatedAt ??
|
|
212
|
+
`${workstream.packetPlan?.currentWaveIndex ?? 0}:${Object.keys(workstream.packetPlan?.packets ?? {}).sort().join(",")}`);
|
|
213
|
+
}
|
|
214
|
+
function resolveOriginalPlanningSessionId(workstream, env) {
|
|
215
|
+
const planBreakdownExecutions = env.input.previousState.executions
|
|
216
|
+
.filter((execution) => execution.taskKind === "plan_breakdown" && execution.taskStatus === "completed")
|
|
217
|
+
.filter((execution) => {
|
|
218
|
+
const task = env.input.previousState.tasks.find((candidate) => candidate.id === execution.taskId);
|
|
219
|
+
return task?.workstreamId === workstream.id;
|
|
220
|
+
})
|
|
221
|
+
.sort((left, right) => right.endedAt.localeCompare(left.endedAt));
|
|
222
|
+
return planBreakdownExecutions[0]?.result?.planBreakdownSessionId ?? "unknown";
|
|
223
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { buildTaskRecord, createPullRequestTask } from "../builders.js";
|
|
2
|
+
export const writePlanHandler = {
|
|
3
|
+
name: "write_plan",
|
|
4
|
+
derive(context, env) {
|
|
5
|
+
const task = deriveWritePlanTask(context, env);
|
|
6
|
+
return task ? [task] : [];
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
export function deriveWritePlanTask(context, env) {
|
|
10
|
+
const { workstream, attention, issue, activePullRequest, activeWorktree, repoSyncRecord, packetPullRequest } = context;
|
|
11
|
+
if (workstream.status !== "open" ||
|
|
12
|
+
workstream.phase === "abandoned" ||
|
|
13
|
+
workstream.phase === "resolved") {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (packetPullRequest && packetPullRequest.state !== "open") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const isPacketPlanning = activeWorktree?.packet && activeWorktree.status === "planning";
|
|
20
|
+
if (!isPacketPlanning && workstream.origin.type === "issue") {
|
|
21
|
+
if (workstream.phase === "product_discovery" || workstream.phase === "product_specification") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (workstream.product && workstream.product.status !== "spec_written") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (workstream.phase !== "planning" && workstream.phase !== "plan_feedback" && !isPacketPlanning) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (!isPacketPlanning) {
|
|
32
|
+
if (attention?.reasons.includes("pr_review_requested") ||
|
|
33
|
+
(attention?.reasons.includes("plan_approved") && activeWorktree?.status !== "planning")) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (activeWorktree?.status !== "planning") {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (activeWorktree) {
|
|
41
|
+
const parentSource = issue ?? activePullRequest;
|
|
42
|
+
const source = (isPacketPlanning ? packetPullRequest : undefined) ?? parentSource;
|
|
43
|
+
if (source) {
|
|
44
|
+
return createWorktreeWritePlanTask(source, activeWorktree, {
|
|
45
|
+
contextBuilder: env.input.contextBuilder,
|
|
46
|
+
now: env.input.now
|
|
47
|
+
}, parentSource, workstream.id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!activePullRequest) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return createPullRequestTask("write_plan", activePullRequest, repoSyncRecord, {
|
|
54
|
+
reason: 'Pull request carries the "agent-in-progress" label and still needs the planning step.',
|
|
55
|
+
summary: `Write plan for PR #${activePullRequest.number}: ${activePullRequest.title}`,
|
|
56
|
+
contextBuilder: env.input.contextBuilder,
|
|
57
|
+
now: env.input.now
|
|
58
|
+
}, undefined, workstream.id);
|
|
59
|
+
}
|
|
60
|
+
function createWorktreeWritePlanTask(source, worktree, input, parentSource, workstreamId) {
|
|
61
|
+
const reason = worktree.packet
|
|
62
|
+
? `Implementation packet "${worktree.packet.packetId}" is in wave ${worktree.packet.waveIndex} and is ready for planning.`
|
|
63
|
+
: source.kind === "issue"
|
|
64
|
+
? "Product Spec is written; the first engineering step is to write the plan."
|
|
65
|
+
: 'Pull request carries the "agent-in-progress" label and still needs the planning step.';
|
|
66
|
+
const summary = worktree.packet
|
|
67
|
+
? `Write plan for packet "${worktree.packet.packetId}" in wave ${worktree.packet.waveIndex}`
|
|
68
|
+
: `Write plan for ${source.kind === "issue" ? "issue" : "PR"} #${source.number}: ${source.title}`;
|
|
69
|
+
return buildTaskRecord(`task:write_plan:worktree:${worktree.id}`, "write_plan", source, undefined, {
|
|
70
|
+
reason,
|
|
71
|
+
summary,
|
|
72
|
+
contextBuilder: input.contextBuilder,
|
|
73
|
+
now: input.now
|
|
74
|
+
}, input.contextBuilder.build({
|
|
75
|
+
taskKind: "write_plan",
|
|
76
|
+
repoIdentifier: source.repoSlug,
|
|
77
|
+
sourceKind: worktree.prUrl ? "pull_request" : source.kind,
|
|
78
|
+
sourceUrl: worktree.prUrl || source.url,
|
|
79
|
+
...(worktree.prUrl ? { parentUrl: parentSource?.url ?? source.url } : {}),
|
|
80
|
+
labels: source.labels,
|
|
81
|
+
localRepoPath: worktree.path,
|
|
82
|
+
branchName: worktree.branchName,
|
|
83
|
+
...(worktree.packet?.packetId ? { packetId: worktree.packet.packetId } : {}),
|
|
84
|
+
planDocumentPath: worktree.planDocumentPath,
|
|
85
|
+
...(worktree.productSpecDocumentPath ? { productSpecDocumentPath: worktree.productSpecDocumentPath } : {}),
|
|
86
|
+
logDocumentPath: worktree.logDocumentPath,
|
|
87
|
+
...(parentSource && parentSource.kind === "issue"
|
|
88
|
+
? {
|
|
89
|
+
issueTitle: parentSource.title,
|
|
90
|
+
issueBody: parentSource.body
|
|
91
|
+
}
|
|
92
|
+
: source.kind === "issue"
|
|
93
|
+
? {
|
|
94
|
+
issueTitle: source.title,
|
|
95
|
+
issueBody: source.body
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
...(worktree.triggerCommentUrl ? { commentUrl: worktree.triggerCommentUrl } : {}),
|
|
99
|
+
...(worktree.triggerCommentText ? { commentText: worktree.triggerCommentText } : {})
|
|
100
|
+
}), workstreamId);
|
|
101
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildTaskRecord } from "../builders.js";
|
|
2
|
+
export const writeProductSpecHandler = {
|
|
3
|
+
name: "write_product_spec",
|
|
4
|
+
derive(context, env) {
|
|
5
|
+
const task = deriveWriteProductSpecTask(context, env);
|
|
6
|
+
return task ? [task] : [];
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function deriveWriteProductSpecTask(context, env) {
|
|
10
|
+
const { workstream, issue, activeWorktree } = context;
|
|
11
|
+
if (!issue || workstream.origin.type !== "issue") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (workstream.phase !== "product_specification") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (!activeWorktree || activeWorktree.status !== "product_specification") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (activeWorktree.productSpecWrittenAt) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return createWorktreeProductSpecTask(issue, activeWorktree, {
|
|
24
|
+
contextBuilder: env.input.contextBuilder,
|
|
25
|
+
now: env.input.now
|
|
26
|
+
}, workstream.id);
|
|
27
|
+
}
|
|
28
|
+
function createWorktreeProductSpecTask(issue, worktree, input, workstreamId) {
|
|
29
|
+
const productSpecDocumentPath = worktree.productSpecDocumentPath;
|
|
30
|
+
return buildTaskRecord(`task:write_product_spec:worktree:${worktree.id}`, "write_product_spec", issue, undefined, {
|
|
31
|
+
reason: "Issue has Product approval; Product Manager must write the Product Spec before engineering planning.",
|
|
32
|
+
summary: `Write Product Spec for issue #${issue.number}: ${issue.title}`,
|
|
33
|
+
contextBuilder: input.contextBuilder,
|
|
34
|
+
now: input.now
|
|
35
|
+
}, input.contextBuilder.build({
|
|
36
|
+
taskKind: "write_product_spec",
|
|
37
|
+
repoIdentifier: issue.repoSlug,
|
|
38
|
+
sourceKind: "issue",
|
|
39
|
+
sourceUrl: issue.url,
|
|
40
|
+
labels: issue.labels,
|
|
41
|
+
localRepoPath: worktree.path,
|
|
42
|
+
branchName: worktree.branchName,
|
|
43
|
+
planDocumentPath: worktree.planDocumentPath,
|
|
44
|
+
...(productSpecDocumentPath ? { productSpecDocumentPath } : {}),
|
|
45
|
+
logDocumentPath: worktree.logDocumentPath,
|
|
46
|
+
issueTitle: issue.title,
|
|
47
|
+
issueBody: issue.body
|
|
48
|
+
}), workstreamId);
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function runBlockingHandler(handler, context, env, sink) {
|
|
2
|
+
const tasks = handler.derive(context, env, sink);
|
|
3
|
+
let shouldBlock = false;
|
|
4
|
+
for (const task of tasks) {
|
|
5
|
+
const mergedTask = sink.add(task);
|
|
6
|
+
if (handler.shouldBlockRemainingHandlers(mergedTask)) {
|
|
7
|
+
shouldBlock = true;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return shouldBlock;
|
|
11
|
+
}
|
|
12
|
+
export function runHandlers(handlers, context, env, sink) {
|
|
13
|
+
for (const handler of handlers) {
|
|
14
|
+
for (const task of handler.derive(context, env, sink)) {
|
|
15
|
+
sink.add(task);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|