@clipboard-health/groundcrew 4.52.12 → 4.53.1
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/README.md +1 -1
- package/crew.config.example.ts +9 -0
- package/dist/commands/cleanupWorkspace.js +1 -1
- package/dist/commands/dispatcher.d.ts +3 -0
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +15 -4
- package/dist/commands/eligibility.d.ts +59 -8
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +193 -8
- package/dist/commands/resumeWorkspace.d.ts.map +1 -1
- package/dist/commands/resumeWorkspace.js +3 -0
- package/dist/commands/reviewer.d.ts.map +1 -1
- package/dist/commands/reviewer.js +14 -0
- package/dist/commands/setupWorkspace.d.ts +4 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +60 -18
- package/dist/commands/stackRetarget.d.ts +64 -0
- package/dist/commands/stackRetarget.d.ts.map +1 -0
- package/dist/commands/stackRetarget.js +627 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +10 -0
- package/dist/commands/statusCollect.d.ts.map +1 -1
- package/dist/commands/statusCollect.js +3 -0
- package/dist/commands/teardownReporter.js +1 -1
- package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
- package/dist/lib/adapters/linear/factory.js +2 -0
- package/dist/lib/adapters/linear/fetch.d.ts +4 -1
- package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
- package/dist/lib/adapters/linear/fetch.js +6 -2
- package/dist/lib/adapters/linear/parsing.d.ts +6 -1
- package/dist/lib/adapters/linear/parsing.d.ts.map +1 -1
- package/dist/lib/adapters/linear/parsing.js +5 -1
- package/dist/lib/config.d.ts +12 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -2
- package/dist/lib/githubStacks.d.ts +51 -0
- package/dist/lib/githubStacks.d.ts.map +1 -0
- package/dist/lib/githubStacks.js +116 -0
- package/dist/lib/launchCommand.d.ts +6 -0
- package/dist/lib/launchCommand.d.ts.map +1 -1
- package/dist/lib/launchCommand.js +4 -1
- package/dist/lib/pullRequests.d.ts +16 -0
- package/dist/lib/pullRequests.d.ts.map +1 -1
- package/dist/lib/pullRequests.js +31 -2
- package/dist/lib/runState.d.ts +53 -0
- package/dist/lib/runState.d.ts.map +1 -1
- package/dist/lib/runState.js +113 -24
- package/dist/lib/runStateCleanup.d.ts +8 -2
- package/dist/lib/runStateCleanup.d.ts.map +1 -1
- package/dist/lib/runStateCleanup.js +18 -2
- package/dist/lib/statusSnapshot.d.ts +6 -0
- package/dist/lib/statusSnapshot.d.ts.map +1 -1
- package/dist/lib/taskSource.d.ts +4 -0
- package/dist/lib/taskSource.d.ts.map +1 -1
- package/dist/lib/util.d.ts +6 -0
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +18 -0
- package/dist/lib/worktreeRunState.d.ts +19 -0
- package/dist/lib/worktreeRunState.d.ts.map +1 -1
- package/dist/lib/worktreeRunState.js +20 -1
- package/dist/lib/worktrees.d.ts +44 -0
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +95 -18
- package/docs/configuration.md +1 -0
- package/docs/development.md +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Groundcrew watches assigned tasks, creates isolated worktrees, launches agent CL
|
|
|
32
32
|
|
|
33
33
|
`crew doctor` checks all of these, so you can install as you go.
|
|
34
34
|
|
|
35
|
-
- **Node 24.
|
|
35
|
+
- **Node 24.22.1:** [nvm](https://github.com/nvm-sh/nvm): `nvm install 24.22.1`.
|
|
36
36
|
- **git:** e.g., `brew install git`, `apt install git`.
|
|
37
37
|
- **A terminal multiplexer:** [tmux](https://github.com/tmux/tmux/wiki/Installing) (cross-platform), [cmux](https://cmux.com/) (macOS), or [zellij](https://zellij.dev/).
|
|
38
38
|
- **An agent CLI:** [Claude Code](https://code.claude.com/docs/en/quickstart), [Codex](https://developers.openai.com/codex/quickstart?setup=cli), the [Cursor CLI](https://docs.cursor.com/en/cli/overview) (`cursor-agent`, for the `cursor` and `cursor-grok` presets), and/or [Pi](https://pi.dev/).
|
package/crew.config.example.ts
CHANGED
|
@@ -149,6 +149,15 @@ export default {
|
|
|
149
149
|
//
|
|
150
150
|
// git: { remote: "origin", defaultBranch: "main" },
|
|
151
151
|
//
|
|
152
|
+
// Stacked PRs: when a Todo task is blocked only by unresolved same-repo
|
|
153
|
+
// blockers that form a single chain, dispatch it as a child stacked on
|
|
154
|
+
// the chain tip's branch instead of waiting for the blockers to merge.
|
|
155
|
+
// Off by default.
|
|
156
|
+
// Config is cached per process, so a running `crew run --watch` needs a
|
|
157
|
+
// restart before it picks this up. Opt an individual task out with the
|
|
158
|
+
// `groundcrew-no-stack` Linear label.
|
|
159
|
+
// git: { stacking: true },
|
|
160
|
+
//
|
|
152
161
|
// orchestrator: {
|
|
153
162
|
// maximumInProgress: 4,
|
|
154
163
|
// pollIntervalMilliseconds: 120_000,
|
|
@@ -54,7 +54,7 @@ function isWorkspaceInUse(probe, task) {
|
|
|
54
54
|
}
|
|
55
55
|
async function teardownEntries(config, entries, force) {
|
|
56
56
|
const result = await worktrees.teardown(config, entries, { force });
|
|
57
|
-
recordCleanedUpRuns(config, result.removed);
|
|
57
|
+
await recordCleanedUpRuns(config, result.removed);
|
|
58
58
|
logTeardown(result);
|
|
59
59
|
if (result.failures.length > 0) {
|
|
60
60
|
throw result.failures[0]?.error;
|
|
@@ -11,9 +11,12 @@ import type { ResolvedConfig } from "../lib/config.ts";
|
|
|
11
11
|
import { type BoardState, type Issue } from "../lib/taskSource.ts";
|
|
12
12
|
import type { UsageByAgent } from "../lib/usage.ts";
|
|
13
13
|
import type { WorktreeEntry } from "../lib/worktrees.ts";
|
|
14
|
+
import { type EligibilityDeps } from "./eligibility.ts";
|
|
14
15
|
interface DispatcherDeps {
|
|
15
16
|
config: ResolvedConfig;
|
|
16
17
|
board: Board;
|
|
18
|
+
/** Injection point for the stacking decision's run-state read and git probe; defaults to the real implementations. */
|
|
19
|
+
eligibilityDeps?: EligibilityDeps;
|
|
17
20
|
}
|
|
18
21
|
export interface Dispatcher {
|
|
19
22
|
runOnce: (arguments_: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,KAAK,EAEX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAML,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAC;AAG1B,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,sHAAsH;IACtH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,UAAU,EAAE;QACpB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,+FAA+F;QAC/F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB;;;;WAIG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAuCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAwPjE;AA8BD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAQrE"}
|
|
@@ -12,8 +12,13 @@ import { sourceSupportsMarkDone } from "../lib/sourceCapabilities.js";
|
|
|
12
12
|
import { isGroundcrewIssue, naturalIdFromCanonical, } from "../lib/taskSource.js";
|
|
13
13
|
import { errorMessage, failMark, log, logEvent, styleWarning } from "../lib/util.js";
|
|
14
14
|
import { workspaces } from "../lib/workspaces.js";
|
|
15
|
-
import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, } from "./eligibility.js";
|
|
15
|
+
import { classifyBlockers, classifyEligibility, classifyUsageExhaustion, defaultEligibilityDeps, } from "./eligibility.js";
|
|
16
16
|
import { setupWorkspace } from "./setupWorkspace.js";
|
|
17
|
+
function logStackDispatch(input) {
|
|
18
|
+
const logContext = { flow: "stack-dispatch", ...input };
|
|
19
|
+
log("Dispatching stacked task on its parent's branch");
|
|
20
|
+
logEvent("dispatch", { ...logContext, outcome: "stacking" });
|
|
21
|
+
}
|
|
17
22
|
function logSkip(verdict) {
|
|
18
23
|
log(verdict.message);
|
|
19
24
|
logEvent("dispatch", {
|
|
@@ -36,7 +41,7 @@ function logMissingRepositorySkip(issue, agent, knownRepositories) {
|
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
43
|
export function createDispatcher(deps) {
|
|
39
|
-
const { config, board } = deps;
|
|
44
|
+
const { config, board, eligibilityDeps = defaultEligibilityDeps } = deps;
|
|
40
45
|
const rawSources = sourcesFromConfig(config);
|
|
41
46
|
function buildExhaustedSet(usage) {
|
|
42
47
|
const exhausted = new Set();
|
|
@@ -47,7 +52,7 @@ export function createDispatcher(deps) {
|
|
|
47
52
|
return exhausted;
|
|
48
53
|
}
|
|
49
54
|
async function startEligibleIssue(start, dryRun, signal) {
|
|
50
|
-
const { issue, recovery } = start;
|
|
55
|
+
const { issue, recovery, baseBranch, parentTask } = start;
|
|
51
56
|
const taskId = naturalIdFromCanonical(issue.id);
|
|
52
57
|
if (start.resolvedFromAny) {
|
|
53
58
|
log(`Resolved agent-any for ${taskId} → ${issue.agent}`);
|
|
@@ -71,6 +76,9 @@ export function createDispatcher(deps) {
|
|
|
71
76
|
log(`Worktree and workspace already exist for ${taskId}; resuming with markInProgress`);
|
|
72
77
|
}
|
|
73
78
|
else {
|
|
79
|
+
if (baseBranch !== undefined && parentTask !== undefined) {
|
|
80
|
+
logStackDispatch({ task: taskId, parentTask, baseBranch });
|
|
81
|
+
}
|
|
74
82
|
const setupOptions = {
|
|
75
83
|
repository: issue.repository,
|
|
76
84
|
task: taskId,
|
|
@@ -83,6 +91,8 @@ export function createDispatcher(deps) {
|
|
|
83
91
|
...(issue.worktreePreparation === undefined
|
|
84
92
|
? {}
|
|
85
93
|
: { worktreePreparation: issue.worktreePreparation }),
|
|
94
|
+
...(baseBranch === undefined ? {} : { baseBranch }),
|
|
95
|
+
...(parentTask === undefined ? {} : { parentTask }),
|
|
86
96
|
details: {
|
|
87
97
|
title: issue.title,
|
|
88
98
|
description: issue.description,
|
|
@@ -159,7 +169,7 @@ export function createDispatcher(deps) {
|
|
|
159
169
|
}
|
|
160
170
|
// Run the blocker pre-pass first so an all-blocked board short-circuits
|
|
161
171
|
// before the codexbar HTTP call and the cmux/tmux shell-out fire.
|
|
162
|
-
const { unblocked, skips: blockerSkips } = classifyBlockers(todo);
|
|
172
|
+
const { unblocked, stackDecisions, skips: blockerSkips, } = await classifyBlockers(todo, config, eligibilityDeps);
|
|
163
173
|
for (const skip of blockerSkips) {
|
|
164
174
|
logSkip(skip);
|
|
165
175
|
}
|
|
@@ -207,6 +217,7 @@ export function createDispatcher(deps) {
|
|
|
207
217
|
const verdicts = classifyEligibility({
|
|
208
218
|
config,
|
|
209
219
|
unblocked: dispatchableUnblocked,
|
|
220
|
+
stackDecisions,
|
|
210
221
|
worktreeEntries,
|
|
211
222
|
workspaceProbe,
|
|
212
223
|
usage: fetchedUsage,
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure eligibility classifier — takes the per-iteration board snapshot plus
|
|
3
3
|
* derived state (worktrees, live workspaces, usage, slot count) and returns
|
|
4
|
-
* a verdict per Todo task. No logging, no Linear calls, no shell-outs
|
|
4
|
+
* a verdict per Todo task. No logging, no Linear calls, no shell-outs of its
|
|
5
|
+
* own: the one exception is the stacking decision in `classifyBlockers`,
|
|
6
|
+
* which reads the blocker's run state and probes its branch's availability.
|
|
7
|
+
* Both effects are routed through the injectable `EligibilityDeps` so
|
|
8
|
+
* callers can fake them the way the rest of the codebase fakes git.
|
|
9
|
+
* `probeParentBranch` also logs at WARN on a persistent `ls-remote` failure —
|
|
10
|
+
* the one logging exception, kept because a silent auth failure would
|
|
11
|
+
* otherwise hide forever behind the optimistic "unpushed, retry" verdict.
|
|
5
12
|
*
|
|
6
13
|
* The Dispatcher consumes the verdict list to drive logging and side
|
|
7
14
|
* effects.
|
|
8
15
|
*/
|
|
9
16
|
import { type ResolvedConfig } from "../lib/config.ts";
|
|
17
|
+
import { type RunState } from "../lib/runState.ts";
|
|
10
18
|
import { type GroundcrewIssue } from "../lib/taskSource.ts";
|
|
11
19
|
import type { UsageByAgent } from "../lib/usage.ts";
|
|
12
20
|
import type { WorkspaceProbe } from "../lib/workspaces.ts";
|
|
13
|
-
import type
|
|
14
|
-
type SkipReason = "blocked" | "blockers_paginated" | "agent_any_capacity" | "agent_exhausted" | "workspace_list_unavailable" | "workspace_missing";
|
|
21
|
+
import { type WorktreeEntry } from "../lib/worktrees.ts";
|
|
22
|
+
type SkipReason = "blocked" | "blockers_paginated" | "agent_any_capacity" | "agent_exhausted" | "workspace_list_unavailable" | "workspace_missing" | "stack_multiple_blockers" | "stack_parent_unknown" | "stack_parent_unpushed" | "stack_cross_repo_blocker" | "stack_provisioned_repo" | "stack_opted_out";
|
|
15
23
|
export interface StartVerdict {
|
|
16
24
|
kind: "start";
|
|
17
25
|
issue: GroundcrewIssue;
|
|
18
26
|
recovery: boolean;
|
|
19
27
|
/** Set when the verdict resolved an `agent-any` label to a concrete agent. */
|
|
20
28
|
resolvedFromAny: boolean;
|
|
29
|
+
/** Parent branch this task's worktree and PR should be based on, when stacked. */
|
|
30
|
+
baseBranch?: string;
|
|
31
|
+
/** Natural (unprefixed) id of the blocker task this task is stacked on. */
|
|
32
|
+
parentTask?: string;
|
|
21
33
|
}
|
|
22
34
|
export interface SkipVerdict {
|
|
23
35
|
kind: "skip";
|
|
@@ -26,7 +38,7 @@ export interface SkipVerdict {
|
|
|
26
38
|
message: string;
|
|
27
39
|
/** Stable kebab-case enum surfaced as `logEvent.reason`. */
|
|
28
40
|
eventReason: SkipReason;
|
|
29
|
-
/** Set for `blocked` and `
|
|
41
|
+
/** Set for `blocked`, `blockers_paginated`, and every `stack_*` reason. */
|
|
30
42
|
blockers?: string[];
|
|
31
43
|
/**
|
|
32
44
|
* Set when the skip event should carry the resolved agent (i.e. the
|
|
@@ -37,6 +49,39 @@ export interface SkipVerdict {
|
|
|
37
49
|
agent?: string;
|
|
38
50
|
}
|
|
39
51
|
type Verdict = StartVerdict | SkipVerdict;
|
|
52
|
+
/** A stacking decision for a single blocked issue: where its branch and PR should be based. */
|
|
53
|
+
interface StackDecision {
|
|
54
|
+
baseBranch: string;
|
|
55
|
+
parentTask: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Result of probing a parent branch's availability for stacking (spec
|
|
59
|
+
* section 8's error table):
|
|
60
|
+
*
|
|
61
|
+
* - `"pushed"`: `git ls-remote` finds the branch on the remote.
|
|
62
|
+
* - `"unpushed"`: the branch may still show up — either `ls-remote` came
|
|
63
|
+
* back empty but a local branch of the same name exists (the parent's
|
|
64
|
+
* agent hasn't pushed yet), or the `ls-remote` probe itself failed
|
|
65
|
+
* (network/auth). Both retry next tick under the same optimistic reason.
|
|
66
|
+
* - `"unknown"`: the branch is missing both locally and on the remote, so it
|
|
67
|
+
* will never appear (deleted, or the parent's run-state file is stale).
|
|
68
|
+
*/
|
|
69
|
+
type BranchAvailability = "pushed" | "unpushed" | "unknown";
|
|
70
|
+
/**
|
|
71
|
+
* Side-effecting boundary for the stacking decision — the only I/O
|
|
72
|
+
* `classifyBlockers` performs. Production uses the real run-state reader and
|
|
73
|
+
* a `git ls-remote`/local-branch probe; tests substitute fakes instead of
|
|
74
|
+
* mocking modules.
|
|
75
|
+
*/
|
|
76
|
+
export interface EligibilityDeps {
|
|
77
|
+
readParentRunState: (config: ResolvedConfig, task: string) => RunState | undefined;
|
|
78
|
+
probeParentBranch: (arguments_: {
|
|
79
|
+
repoDir: string;
|
|
80
|
+
remote: string;
|
|
81
|
+
branch: string;
|
|
82
|
+
}) => Promise<BranchAvailability>;
|
|
83
|
+
}
|
|
84
|
+
export declare const defaultEligibilityDeps: EligibilityDeps;
|
|
40
85
|
export type AgentUsageExhaustion = {
|
|
41
86
|
kind: "unavailable";
|
|
42
87
|
agent: string;
|
|
@@ -63,6 +108,8 @@ export interface ClassifyArguments {
|
|
|
63
108
|
* when every Todo is blocked.
|
|
64
109
|
*/
|
|
65
110
|
unblocked: readonly GroundcrewIssue[];
|
|
111
|
+
/** Stacking decisions from `classifyBlockers`, keyed by `issue.id`. Defaults to empty (no stacking). */
|
|
112
|
+
stackDecisions?: ReadonlyMap<string, StackDecision>;
|
|
66
113
|
worktreeEntries: readonly WorktreeEntry[];
|
|
67
114
|
workspaceProbe: WorkspaceProbe;
|
|
68
115
|
usage: UsageByAgent;
|
|
@@ -72,8 +119,10 @@ export interface ClassifyArguments {
|
|
|
72
119
|
slots: number;
|
|
73
120
|
dryRun: boolean;
|
|
74
121
|
}
|
|
75
|
-
interface BlockerClassification {
|
|
122
|
+
export interface BlockerClassification {
|
|
76
123
|
unblocked: GroundcrewIssue[];
|
|
124
|
+
/** Stacking decisions for issues in `unblocked` that stack on a parent, keyed by `issue.id`. */
|
|
125
|
+
stackDecisions: ReadonlyMap<string, StackDecision>;
|
|
77
126
|
skips: SkipVerdict[];
|
|
78
127
|
}
|
|
79
128
|
/**
|
|
@@ -87,12 +136,14 @@ interface BlockerClassification {
|
|
|
87
136
|
export declare function pickBestAgent(config: ResolvedConfig, usage: UsageByAgent, exhausted: Set<string>): string | undefined;
|
|
88
137
|
export declare function classifyUsageExhaustion(config: ResolvedConfig, usage: UsageByAgent): AgentUsageExhaustion[];
|
|
89
138
|
/**
|
|
90
|
-
* Cheap pre-pass — partitions Todo into unblocked issues
|
|
139
|
+
* Cheap pre-pass — partitions Todo into unblocked issues (including those
|
|
140
|
+
* stacking on a single unresolved blocker, per `stackDecisions`) and blocker
|
|
91
141
|
* skip verdicts. Runs before the dispatcher fetches usage or probes the
|
|
92
142
|
* workspace adapter, so a board where every Todo is blocked short-circuits
|
|
93
|
-
* without paying for either.
|
|
143
|
+
* without paying for either. `config.git.stacking` off reproduces today's
|
|
144
|
+
* behavior exactly and performs no run-state reads or git calls.
|
|
94
145
|
*/
|
|
95
|
-
export declare function classifyBlockers(todo: readonly GroundcrewIssue[]): BlockerClassification
|
|
146
|
+
export declare function classifyBlockers(todo: readonly GroundcrewIssue[], config: ResolvedConfig, deps?: EligibilityDeps): Promise<BlockerClassification>;
|
|
96
147
|
/**
|
|
97
148
|
* Eligibility verdicts for already-unblocked Todo issues — handles
|
|
98
149
|
* agent-any resolution, session exhaustion, worktree/workspace recovery,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,184 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure eligibility classifier — takes the per-iteration board snapshot plus
|
|
3
3
|
* derived state (worktrees, live workspaces, usage, slot count) and returns
|
|
4
|
-
* a verdict per Todo task. No logging, no Linear calls, no shell-outs
|
|
4
|
+
* a verdict per Todo task. No logging, no Linear calls, no shell-outs of its
|
|
5
|
+
* own: the one exception is the stacking decision in `classifyBlockers`,
|
|
6
|
+
* which reads the blocker's run state and probes its branch's availability.
|
|
7
|
+
* Both effects are routed through the injectable `EligibilityDeps` so
|
|
8
|
+
* callers can fake them the way the rest of the codebase fakes git.
|
|
9
|
+
* `probeParentBranch` also logs at WARN on a persistent `ls-remote` failure —
|
|
10
|
+
* the one logging exception, kept because a silent auth failure would
|
|
11
|
+
* otherwise hide forever behind the optimistic "unpushed, retry" verdict.
|
|
5
12
|
*
|
|
6
13
|
* The Dispatcher consumes the verdict list to drive logging and side
|
|
7
14
|
* effects.
|
|
8
15
|
*/
|
|
16
|
+
import { runCommandAsync } from "../lib/commandRunner.js";
|
|
9
17
|
import { AGENT_ANY } from "../lib/config.js";
|
|
18
|
+
import { readRunState } from "../lib/runState.js";
|
|
10
19
|
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
20
|
+
import { errorMessage, log, memoizeAsync } from "../lib/util.js";
|
|
21
|
+
import { localBranchExists, resolveRepoDir } from "../lib/worktrees.js";
|
|
11
22
|
const PERCENT_FRACTION_DIVISOR = 100;
|
|
12
23
|
const DAYS_PER_WEEK = 7;
|
|
13
24
|
const MINUTES_PER_DAY = 24 * 60;
|
|
14
25
|
const MINUTES_PER_WEEK = DAYS_PER_WEEK * MINUTES_PER_DAY;
|
|
26
|
+
async function isBranchOnRemote(arguments_) {
|
|
27
|
+
try {
|
|
28
|
+
const output = await runCommandAsync("git", [
|
|
29
|
+
"-C",
|
|
30
|
+
arguments_.repoDir,
|
|
31
|
+
"ls-remote",
|
|
32
|
+
"--heads",
|
|
33
|
+
arguments_.remote,
|
|
34
|
+
`refs/heads/${arguments_.branch}`,
|
|
35
|
+
]);
|
|
36
|
+
return output.length > 0;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Network/auth failures are common and expected (retried next tick), but
|
|
40
|
+
// silent forever would hide a persistent auth failure. WARN once per
|
|
41
|
+
// branch per tick — probeParentBranch is memoized per branch within a
|
|
42
|
+
// single classifyBlockers call, so this can't spam.
|
|
43
|
+
log(`Stack parent branch probe failed for ${arguments_.branch}; treating as unpushed and retrying next tick: ${errorMessage(error)}`);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function probeParentBranch(arguments_) {
|
|
48
|
+
const onRemote = await isBranchOnRemote(arguments_);
|
|
49
|
+
if (onRemote === true) {
|
|
50
|
+
return "pushed";
|
|
51
|
+
}
|
|
52
|
+
if (onRemote === undefined) {
|
|
53
|
+
return "unpushed";
|
|
54
|
+
}
|
|
55
|
+
return (await localBranchExists(arguments_.repoDir, arguments_.branch)) ? "unpushed" : "unknown";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Memoizes `probeParentBranch` by branch for the lifetime of a single
|
|
59
|
+
* `classifyBlockers` call, so N siblings blocked by the same parent do one
|
|
60
|
+
* `git ls-remote` probe instead of N. Wrapping happens fresh per call — the
|
|
61
|
+
* cache must never survive past one dispatcher tick, or a since-pushed
|
|
62
|
+
* branch could keep reading as unpushed.
|
|
63
|
+
*/
|
|
64
|
+
function memoizedProbeParentBranch(probeParentBranchDep) {
|
|
65
|
+
const cache = new Map();
|
|
66
|
+
return async (arguments_) => await memoizeAsync(cache, arguments_.branch, async () => await probeParentBranchDep(arguments_));
|
|
67
|
+
}
|
|
68
|
+
export const defaultEligibilityDeps = {
|
|
69
|
+
readParentRunState: readRunState,
|
|
70
|
+
probeParentBranch,
|
|
71
|
+
};
|
|
15
72
|
function blockerSummary(blocker) {
|
|
16
73
|
return `${blocker.id}:${blocker.status}`;
|
|
17
74
|
}
|
|
18
|
-
function
|
|
75
|
+
function stackSkip(issue, blockers, eventReason, message) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "skip",
|
|
78
|
+
issue,
|
|
79
|
+
message,
|
|
80
|
+
eventReason,
|
|
81
|
+
blockers: blockers.map(blockerSummary),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function normalizedTaskId(task) {
|
|
85
|
+
return task.toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
function readBlockerRunStates(blockers, config, deps) {
|
|
88
|
+
return new Map(blockers.map((blocker) => {
|
|
89
|
+
const task = naturalIdFromCanonical(blocker.id);
|
|
90
|
+
return [normalizedTaskId(task), deps.readParentRunState(config, task)];
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every task reachable from `start` by following `parentTask` links, read
|
|
95
|
+
* lazily so intermediate ancestors that are not themselves blockers (already
|
|
96
|
+
* merged, or never blocking this issue) still connect the chain. The visited
|
|
97
|
+
* set is the cycle guard.
|
|
98
|
+
*/
|
|
99
|
+
function stackAncestorsOf(start, runStates, config, deps) {
|
|
100
|
+
const ancestors = new Set();
|
|
101
|
+
let current = runStates.get(start);
|
|
102
|
+
while (current?.parentTask !== undefined) {
|
|
103
|
+
const parent = normalizedTaskId(current.parentTask);
|
|
104
|
+
if (ancestors.has(parent) || parent === start) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
ancestors.add(parent);
|
|
108
|
+
current = runStates.has(parent)
|
|
109
|
+
? runStates.get(parent)
|
|
110
|
+
: deps.readParentRunState(config, current.parentTask);
|
|
111
|
+
}
|
|
112
|
+
return ancestors;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* With several unresolved blockers, the child can still stack when they form
|
|
116
|
+
* a chain: exactly one blocker (the tip) has every other blocker among its
|
|
117
|
+
* stack ancestors, so its branch already carries all of their work. Siblings
|
|
118
|
+
* off the default branch have no single base and yield `undefined`.
|
|
119
|
+
*/
|
|
120
|
+
function chainTipOf(blockers, runStates, config, deps) {
|
|
121
|
+
const candidates = blockers.map((blocker) => ({
|
|
122
|
+
blocker,
|
|
123
|
+
task: normalizedTaskId(naturalIdFromCanonical(blocker.id)),
|
|
124
|
+
}));
|
|
125
|
+
const tips = candidates.filter(({ task }) => {
|
|
126
|
+
const ancestors = stackAncestorsOf(task, runStates, config, deps);
|
|
127
|
+
return candidates.every((other) => other.task === task || ancestors.has(other.task));
|
|
128
|
+
});
|
|
129
|
+
const [tip, ...rest] = tips;
|
|
130
|
+
return rest.length === 0 ? tip?.blocker : undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The stacking check from the stacked-PRs design (spec section 1). Cheap,
|
|
134
|
+
* dependency-free checks (provisioner, opt-out label) run before the
|
|
135
|
+
* run-state reads, which run before the `git ls-remote` probe, so a task that
|
|
136
|
+
* fails early never pays for the network call. Multiple unresolved blockers
|
|
137
|
+
* stack only when they form a chain (see `chainTipOf`).
|
|
138
|
+
*/
|
|
139
|
+
async function stackDecisionFor(issue, unresolved, config, deps) {
|
|
140
|
+
const repositoryEntry = config.workspace.repositories.find((entry) => entry.name === issue.repository);
|
|
141
|
+
if (repositoryEntry?.provision !== undefined) {
|
|
142
|
+
return stackSkip(issue, unresolved, "stack_provisioned_repo", `Skipping ${issue.id}: stacking is refused for scripted-provisioner repositories`);
|
|
143
|
+
}
|
|
144
|
+
if (issue.stacking === "opted-out") {
|
|
145
|
+
return stackSkip(issue, unresolved, "stack_opted_out", `Skipping ${issue.id}: opted out of stacking via the groundcrew-no-stack label`);
|
|
146
|
+
}
|
|
147
|
+
const runStates = readBlockerRunStates(unresolved, config, deps);
|
|
148
|
+
const foreign = unresolved.filter((blocker) => {
|
|
149
|
+
const repository = runStates.get(normalizedTaskId(naturalIdFromCanonical(blocker.id)))?.repository;
|
|
150
|
+
return repository !== undefined && repository !== issue.repository;
|
|
151
|
+
});
|
|
152
|
+
if (foreign.length > 0) {
|
|
153
|
+
return stackSkip(issue, unresolved, "stack_cross_repo_blocker", `Skipping ${issue.id}: blocked by ${foreign.map(blockerSummary).join(", ")} in another repository; a branch cannot stack across repositories`);
|
|
154
|
+
}
|
|
155
|
+
const [singleBlocker] = unresolved;
|
|
156
|
+
const blocker = unresolved.length === 1 ? singleBlocker : chainTipOf(unresolved, runStates, config, deps);
|
|
157
|
+
if (blocker === undefined) {
|
|
158
|
+
return stackSkip(issue, unresolved, "stack_multiple_blockers", `Skipping ${issue.id}: blocked by ${unresolved.map(blockerSummary).join(", ")}, which do not form a single stack`);
|
|
159
|
+
}
|
|
160
|
+
return await stackOntoBlocker(issue, blocker, runStates, config, deps);
|
|
161
|
+
}
|
|
162
|
+
async function stackOntoBlocker(issue, blocker, runStates, config, deps) {
|
|
163
|
+
const parentTask = naturalIdFromCanonical(blocker.id);
|
|
164
|
+
const parentRunState = runStates.get(normalizedTaskId(parentTask));
|
|
165
|
+
if (parentRunState === undefined) {
|
|
166
|
+
return stackSkip(issue, [blocker], "stack_parent_unknown", `Skipping ${issue.id}: blocker ${parentTask} has no run state in repository ${issue.repository}`);
|
|
167
|
+
}
|
|
168
|
+
const availability = await deps.probeParentBranch({
|
|
169
|
+
repoDir: resolveRepoDir(config, issue.repository),
|
|
170
|
+
remote: config.git.remote,
|
|
171
|
+
branch: parentRunState.branchName,
|
|
172
|
+
});
|
|
173
|
+
if (availability === "unknown") {
|
|
174
|
+
return stackSkip(issue, [blocker], "stack_parent_unknown", `Skipping ${issue.id}: blocker ${parentTask}'s branch ${parentRunState.branchName} is missing locally and on the remote`);
|
|
175
|
+
}
|
|
176
|
+
if (availability === "unpushed") {
|
|
177
|
+
return stackSkip(issue, [blocker], "stack_parent_unpushed", `Skipping ${issue.id}: blocker ${parentTask}'s branch isn't pushed yet`);
|
|
178
|
+
}
|
|
179
|
+
return { kind: "stack", issue, baseBranch: parentRunState.branchName, parentTask };
|
|
180
|
+
}
|
|
181
|
+
async function blockerVerdictFor(issue, config, deps) {
|
|
19
182
|
if (issue.hasMoreBlockers) {
|
|
20
183
|
const blockers = issue.blockers.map(blockerSummary);
|
|
21
184
|
return {
|
|
@@ -30,6 +193,9 @@ function blockerVerdictFor(issue) {
|
|
|
30
193
|
if (unresolved.length === 0) {
|
|
31
194
|
return undefined;
|
|
32
195
|
}
|
|
196
|
+
if (config.git.stacking === true) {
|
|
197
|
+
return await stackDecisionFor(issue, unresolved, config, deps);
|
|
198
|
+
}
|
|
33
199
|
const blockers = unresolved.map(blockerSummary);
|
|
34
200
|
return {
|
|
35
201
|
kind: "skip",
|
|
@@ -145,24 +311,39 @@ function classifyRecovery(arguments_) {
|
|
|
145
311
|
return { kind: "go", recovery: true };
|
|
146
312
|
}
|
|
147
313
|
/**
|
|
148
|
-
* Cheap pre-pass — partitions Todo into unblocked issues
|
|
314
|
+
* Cheap pre-pass — partitions Todo into unblocked issues (including those
|
|
315
|
+
* stacking on a single unresolved blocker, per `stackDecisions`) and blocker
|
|
149
316
|
* skip verdicts. Runs before the dispatcher fetches usage or probes the
|
|
150
317
|
* workspace adapter, so a board where every Todo is blocked short-circuits
|
|
151
|
-
* without paying for either.
|
|
318
|
+
* without paying for either. `config.git.stacking` off reproduces today's
|
|
319
|
+
* behavior exactly and performs no run-state reads or git calls.
|
|
152
320
|
*/
|
|
153
|
-
export function classifyBlockers(todo) {
|
|
321
|
+
export async function classifyBlockers(todo, config, deps = defaultEligibilityDeps) {
|
|
154
322
|
const unblocked = [];
|
|
155
323
|
const skips = [];
|
|
324
|
+
const stackDecisions = new Map();
|
|
325
|
+
const memoizedDeps = {
|
|
326
|
+
...deps,
|
|
327
|
+
probeParentBranch: memoizedProbeParentBranch(deps.probeParentBranch),
|
|
328
|
+
};
|
|
156
329
|
for (const issue of todo) {
|
|
157
|
-
|
|
330
|
+
// oxlint-disable-next-line no-await-in-loop -- one blocker check at a time mirrors the dispatcher's own "one workspace at a time" git serialization
|
|
331
|
+
const verdict = await blockerVerdictFor(issue, config, memoizedDeps);
|
|
158
332
|
if (verdict === undefined) {
|
|
159
333
|
unblocked.push(issue);
|
|
160
334
|
}
|
|
335
|
+
else if (verdict.kind === "stack") {
|
|
336
|
+
unblocked.push(issue);
|
|
337
|
+
stackDecisions.set(issue.id, {
|
|
338
|
+
baseBranch: verdict.baseBranch,
|
|
339
|
+
parentTask: verdict.parentTask,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
161
342
|
else {
|
|
162
343
|
skips.push(verdict);
|
|
163
344
|
}
|
|
164
345
|
}
|
|
165
|
-
return { unblocked, skips };
|
|
346
|
+
return { unblocked, stackDecisions, skips };
|
|
166
347
|
}
|
|
167
348
|
/**
|
|
168
349
|
* Eligibility verdicts for already-unblocked Todo issues — handles
|
|
@@ -171,7 +352,7 @@ export function classifyBlockers(todo) {
|
|
|
171
352
|
* the snapshots in.
|
|
172
353
|
*/
|
|
173
354
|
export function classifyEligibility(arguments_) {
|
|
174
|
-
const { config, unblocked, worktreeEntries, workspaceProbe, usage, exhausted, slots, dryRun } = arguments_;
|
|
355
|
+
const { config, unblocked, stackDecisions = new Map(), worktreeEntries, workspaceProbe, usage, exhausted, slots, dryRun, } = arguments_;
|
|
175
356
|
const verdicts = [];
|
|
176
357
|
let started = 0;
|
|
177
358
|
for (const original of unblocked) {
|
|
@@ -217,11 +398,15 @@ export function classifyEligibility(arguments_) {
|
|
|
217
398
|
verdicts.push({ ...recovery, agent: resolved.agent });
|
|
218
399
|
continue;
|
|
219
400
|
}
|
|
401
|
+
const stackDecision = stackDecisions.get(resolved.id);
|
|
220
402
|
verdicts.push({
|
|
221
403
|
kind: "start",
|
|
222
404
|
issue: resolved,
|
|
223
405
|
recovery: recovery.recovery,
|
|
224
406
|
resolvedFromAny,
|
|
407
|
+
...(stackDecision === undefined
|
|
408
|
+
? {}
|
|
409
|
+
: { baseBranch: stackDecision.baseBranch, parentTask: stackDecision.parentTask }),
|
|
225
410
|
});
|
|
226
411
|
started += 1;
|
|
227
412
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwBzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"resumeWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/resumeWorkspace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAwBzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAuMD,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAmIf;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE"}
|
|
@@ -90,6 +90,7 @@ async function contextFromState(config, task, state, worktree) {
|
|
|
90
90
|
}),
|
|
91
91
|
...(state.reason === undefined ? {} : { reason: state.reason }),
|
|
92
92
|
resumeCount: state.resumeCount,
|
|
93
|
+
...(state.baseBranch === undefined ? {} : { baseBranch: state.baseBranch }),
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
function taskUsesLinearSource(arguments_) {
|
|
@@ -222,6 +223,8 @@ export async function resumeWorkspace(config, options) {
|
|
|
222
223
|
workerEnvironment: workerEnvironmentForTask({
|
|
223
224
|
taskId: context.completionTaskId,
|
|
224
225
|
markDoneSupported: context.completionMarkDoneSupported,
|
|
226
|
+
remote: config.git.remote,
|
|
227
|
+
...(context.baseBranch === undefined ? {} : { baseBranch: context.baseBranch }),
|
|
225
228
|
}),
|
|
226
229
|
taskSourceWritePaths,
|
|
227
230
|
safehouse: config.local.safehouse,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,EAAE,KAAK,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,sEAAsE;AACtE,UAAU,eAAe;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AA+CD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CA+J3D"}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { naturalIdFromCanonical, } from "../lib/taskSource.js";
|
|
28
28
|
import { debug, errorMessage, log, logEvent } from "../lib/util.js";
|
|
29
29
|
import { effectiveBranchName } from "../lib/worktreeRunState.js";
|
|
30
|
+
import { createStackRetarget } from "./stackRetarget.js";
|
|
30
31
|
import { reapWorktrees } from "./teardownReporter.js";
|
|
31
32
|
// Maps a worktree's PRs to the transition its task should make. A merged PR
|
|
32
33
|
// means the work landed → done; an open PR on an in-progress task means it's
|
|
@@ -58,8 +59,21 @@ function matchingWorktreeEntries(arguments_) {
|
|
|
58
59
|
}
|
|
59
60
|
export function createReviewer(deps) {
|
|
60
61
|
const { board, config, findPullRequests } = deps;
|
|
62
|
+
const stackRetarget = createStackRetarget({ findPullRequests });
|
|
61
63
|
async function runOnce(arguments_) {
|
|
62
64
|
const { state, worktreeEntries, dryRun, signal } = arguments_;
|
|
65
|
+
// Independent of the in-progress/in-review candidate scan below: a
|
|
66
|
+
// stacked task's baseBranch correction must not wait on there being an
|
|
67
|
+
// ordinary transition to make this tick.
|
|
68
|
+
if (config !== undefined) {
|
|
69
|
+
await stackRetarget.runOnce({
|
|
70
|
+
config,
|
|
71
|
+
state,
|
|
72
|
+
worktreeEntries,
|
|
73
|
+
dryRun,
|
|
74
|
+
...(signal === undefined ? {} : { signal }),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
63
77
|
const candidates = state.issues.filter((issue) => issue.status === "in-progress" || issue.status === "in-review");
|
|
64
78
|
if (candidates.length === 0) {
|
|
65
79
|
return;
|
|
@@ -16,6 +16,10 @@ export interface SetupWorkspaceOptions {
|
|
|
16
16
|
agent: string;
|
|
17
17
|
/** Set to `skip` to bypass configured prepareWorktree hooks. */
|
|
18
18
|
worktreePreparation?: WorktreePreparation;
|
|
19
|
+
/** Parent branch this task's worktree and PR are based on, when stacked. */
|
|
20
|
+
baseBranch?: string;
|
|
21
|
+
/** Canonical id of the blocker task this task is stacked on. */
|
|
22
|
+
parentTask?: string;
|
|
19
23
|
details: TaskDetails;
|
|
20
24
|
}
|
|
21
25
|
export interface SetupWorkspaceRunOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAWxF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBnE,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAWxF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA0Jf;AA2RD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAyDf"}
|