@clipboard-health/groundcrew 4.52.12 → 4.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/crew.config.example.ts +9 -0
  2. package/dist/commands/cleanupWorkspace.js +1 -1
  3. package/dist/commands/dispatcher.d.ts +3 -0
  4. package/dist/commands/dispatcher.d.ts.map +1 -1
  5. package/dist/commands/dispatcher.js +15 -4
  6. package/dist/commands/eligibility.d.ts +59 -8
  7. package/dist/commands/eligibility.d.ts.map +1 -1
  8. package/dist/commands/eligibility.js +193 -8
  9. package/dist/commands/resumeWorkspace.d.ts.map +1 -1
  10. package/dist/commands/resumeWorkspace.js +3 -0
  11. package/dist/commands/reviewer.d.ts.map +1 -1
  12. package/dist/commands/reviewer.js +14 -0
  13. package/dist/commands/setupWorkspace.d.ts +4 -0
  14. package/dist/commands/setupWorkspace.d.ts.map +1 -1
  15. package/dist/commands/setupWorkspace.js +60 -18
  16. package/dist/commands/stackRetarget.d.ts +64 -0
  17. package/dist/commands/stackRetarget.d.ts.map +1 -0
  18. package/dist/commands/stackRetarget.js +627 -0
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +10 -0
  21. package/dist/commands/statusCollect.d.ts.map +1 -1
  22. package/dist/commands/statusCollect.js +3 -0
  23. package/dist/commands/teardownReporter.js +1 -1
  24. package/dist/lib/adapters/linear/factory.d.ts.map +1 -1
  25. package/dist/lib/adapters/linear/factory.js +2 -0
  26. package/dist/lib/adapters/linear/fetch.d.ts +4 -1
  27. package/dist/lib/adapters/linear/fetch.d.ts.map +1 -1
  28. package/dist/lib/adapters/linear/fetch.js +6 -2
  29. package/dist/lib/adapters/linear/parsing.d.ts +6 -1
  30. package/dist/lib/adapters/linear/parsing.d.ts.map +1 -1
  31. package/dist/lib/adapters/linear/parsing.js +5 -1
  32. package/dist/lib/config.d.ts +12 -1
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +8 -2
  35. package/dist/lib/githubStacks.d.ts +51 -0
  36. package/dist/lib/githubStacks.d.ts.map +1 -0
  37. package/dist/lib/githubStacks.js +116 -0
  38. package/dist/lib/launchCommand.d.ts +6 -0
  39. package/dist/lib/launchCommand.d.ts.map +1 -1
  40. package/dist/lib/launchCommand.js +4 -1
  41. package/dist/lib/pullRequests.d.ts +16 -0
  42. package/dist/lib/pullRequests.d.ts.map +1 -1
  43. package/dist/lib/pullRequests.js +31 -2
  44. package/dist/lib/runState.d.ts +53 -0
  45. package/dist/lib/runState.d.ts.map +1 -1
  46. package/dist/lib/runState.js +113 -24
  47. package/dist/lib/runStateCleanup.d.ts +8 -2
  48. package/dist/lib/runStateCleanup.d.ts.map +1 -1
  49. package/dist/lib/runStateCleanup.js +18 -2
  50. package/dist/lib/statusSnapshot.d.ts +6 -0
  51. package/dist/lib/statusSnapshot.d.ts.map +1 -1
  52. package/dist/lib/taskSource.d.ts +4 -0
  53. package/dist/lib/taskSource.d.ts.map +1 -1
  54. package/dist/lib/util.d.ts +6 -0
  55. package/dist/lib/util.d.ts.map +1 -1
  56. package/dist/lib/util.js +18 -0
  57. package/dist/lib/worktreeRunState.d.ts +19 -0
  58. package/dist/lib/worktreeRunState.d.ts.map +1 -1
  59. package/dist/lib/worktreeRunState.js +20 -1
  60. package/dist/lib/worktrees.d.ts +44 -0
  61. package/dist/lib/worktrees.d.ts.map +1 -1
  62. package/dist/lib/worktrees.js +95 -18
  63. package/docs/configuration.md +1 -0
  64. package/package.json +1 -1
@@ -4,7 +4,7 @@ import { inferAgentCommandName, workerEnvironmentForTask } from "../lib/launchCo
4
4
  import { createBoard } from "../lib/board.js";
5
5
  import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
6
6
  import { resolveRepositoryPreparationCommands } from "../lib/repositoryHooks.js";
7
- import { recordRunState } from "../lib/runState.js";
7
+ import { readRunState, recordRunState } from "../lib/runState.js";
8
8
  import { seedLaunchWorkspaceTrust } from "../lib/seedLaunchWorkspaceTrust.js";
9
9
  import { sourceSupportsMarkDone } from "../lib/sourceCapabilities.js";
10
10
  import { removeStagedPrompt, stageBuildSecretsForLaunch, stagePromptFromTemplate, stageWorkspaceLaunchCommand, } from "../lib/stagedLaunch.js";
@@ -42,25 +42,16 @@ export async function setupWorkspace(config, options, runOptions = {}) {
42
42
  purpose: "runs",
43
43
  ...(signal === undefined ? {} : { signal }),
44
44
  });
45
+ // Captured before preflightProvisioningGate stamps its own "provisioning"
46
+ // row (which carries options.baseBranch forward) — otherwise the reattach
47
+ // guard inside worktrees.create() would compare the just-requested
48
+ // baseBranch against itself and never fire. See
49
+ // StackedBaseBranchMismatchError's doc comment in worktrees.ts.
50
+ const recordedBaseBranch = readRunState(config, task)?.baseBranch;
45
51
  await preflightProvisioningGate({ config, options, signal });
46
- const spec = { repository, task };
47
- const createdPromise = signal === undefined ? worktrees.create(config, spec) : worktrees.create(config, spec, signal);
52
+ const createdPromise = beginWorktreeCreate({ config, options, signal, recordedBaseBranch });
48
53
  const readinessPromise = startLaunchReadiness(ensureReady);
49
- let created;
50
- try {
51
- created = await createdPromise;
52
- }
53
- catch (error) {
54
- // Roll the pre-flight `provisioning` row forward; the outer catch only
55
- // fires post-create and the dispatcher just logs and moves on.
56
- recordFailedToLaunch({
57
- config,
58
- options,
59
- paths: worktrees.predictedEntry(config, repository, task),
60
- error,
61
- });
62
- throw error;
63
- }
54
+ const created = await createdPromise;
64
55
  const { branchName, dir: worktreeDir } = created;
65
56
  const launchDir = resolveLaunchDir(config, repository, worktreeDir);
66
57
  const worktreeName = `${repository}-${task}`;
@@ -130,6 +121,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
130
121
  workerEnvironment: workerEnvironmentForTask({
131
122
  taskId: completionTaskId,
132
123
  markDoneSupported: completionMarkDoneSupported,
124
+ remote: config.git.remote,
125
+ ...(options.baseBranch === undefined ? {} : { baseBranch: options.baseBranch }),
133
126
  }),
134
127
  taskSourceWritePaths,
135
128
  safehouse: config.local.safehouse,
@@ -161,6 +154,8 @@ export async function setupWorkspace(config, options, runOptions = {}) {
161
154
  title: taskDetails.title,
162
155
  completionTaskId,
163
156
  ...(taskDetails.url === undefined ? {} : { url: taskDetails.url }),
157
+ ...(options.baseBranch === undefined ? {} : { baseBranch: options.baseBranch }),
158
+ ...(options.parentTask === undefined ? {} : { parentTask: options.parentTask }),
164
159
  });
165
160
  log(`${okMark()} "${task}" launched (${agent}) worktree ${worktreeName}`);
166
161
  debug(` Worktree: ${launchDir}`);
@@ -176,6 +171,34 @@ export async function setupWorkspace(config, options, runOptions = {}) {
176
171
  throw error;
177
172
  }
178
173
  }
174
+ /**
175
+ * Starts the task's worktree create call, synchronously, so it kicks off
176
+ * before the concurrent launch-readiness check (some tests assert
177
+ * `worktrees.create` is invoked ahead of the safehouse clearance probe).
178
+ * Rolls the pre-flight "provisioning" run state row forward into
179
+ * "failed-to-launch" on failure — the outer catch in `setupWorkspace` only
180
+ * fires post-create, and the dispatcher just logs and moves on otherwise.
181
+ */
182
+ async function beginWorktreeCreate(arguments_) {
183
+ const { config, options, signal, recordedBaseBranch } = arguments_;
184
+ const { task, repository } = options;
185
+ const spec = {
186
+ repository,
187
+ task,
188
+ ...(options.baseBranch === undefined ? {} : { baseBranch: options.baseBranch }),
189
+ ...(recordedBaseBranch === undefined ? {} : { recordedBaseBranch }),
190
+ };
191
+ const createdPromise = signal === undefined ? worktrees.create(config, spec) : worktrees.create(config, spec, signal);
192
+ return await createdPromise.catch((error) => {
193
+ recordFailedToLaunch({
194
+ config,
195
+ options,
196
+ paths: worktrees.predictedEntry(config, repository, task),
197
+ error,
198
+ });
199
+ throw error;
200
+ });
201
+ }
179
202
  function resolveTaskPreparationCommands(arguments_) {
180
203
  if (!arguments_.shouldPrepareWorktree) {
181
204
  return {
@@ -220,6 +243,8 @@ async function preflightProvisioningGate(arguments_) {
220
243
  title: options.details.title,
221
244
  completionTaskId: options.completionTaskId ?? task,
222
245
  ...(options.details.url === undefined ? {} : { url: options.details.url }),
246
+ ...(options.baseBranch === undefined ? {} : { baseBranch: options.baseBranch }),
247
+ ...(options.parentTask === undefined ? {} : { parentTask: options.parentTask }),
223
248
  });
224
249
  }
225
250
  function recordFailedToLaunch(arguments_) {
@@ -238,6 +263,12 @@ function recordFailedToLaunch(arguments_) {
238
263
  title: options.details.title,
239
264
  completionTaskId: options.completionTaskId ?? task,
240
265
  ...(options.details.url === undefined ? {} : { url: options.details.url }),
266
+ // A failed dispatch never got a worktree, so any baseBranch/parentTask
267
+ // recorded moments earlier by preflightProvisioningGate's "provisioning"
268
+ // row must not survive into this terminal state — otherwise the parent's
269
+ // branch would look permanently referenced (worktreeRunState.ts's
270
+ // isReferencedAsStackParent) even though no child ever came to exist.
271
+ clearStackingFields: true,
241
272
  });
242
273
  }
243
274
  async function startLaunchReadiness(ensureReady) {
@@ -284,6 +315,12 @@ function renderWorkspaceContinuationInstruction(accessHint) {
284
315
  }
285
316
  return `Include this workspace continuation note in the output: Workspace attach: \`${accessHint.command}\`.`;
286
317
  }
318
+ const STACKING_RUN_STATE_FIELDS = [
319
+ "baseBranch",
320
+ "parentTask",
321
+ "needsRebase",
322
+ "stackRegistered",
323
+ ];
287
324
  function recordRunStateBestEffort(arguments_) {
288
325
  try {
289
326
  recordRunState({
@@ -300,6 +337,11 @@ function recordRunStateBestEffort(arguments_) {
300
337
  completionTaskId: arguments_.completionTaskId,
301
338
  ...(arguments_.detail === undefined ? {} : { detail: arguments_.detail }),
302
339
  ...(arguments_.url === undefined ? {} : { url: arguments_.url }),
340
+ ...(arguments_.baseBranch === undefined ? {} : { baseBranch: arguments_.baseBranch }),
341
+ ...(arguments_.parentTask === undefined ? {} : { parentTask: arguments_.parentTask }),
342
+ ...(arguments_.clearStackingFields === true
343
+ ? { clearFields: STACKING_RUN_STATE_FIELDS }
344
+ : {}),
303
345
  },
304
346
  });
305
347
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-tick pass that keeps a stacked child's PR base and branch in sync with
3
+ * its parent, per the stacked-PRs design (spec section 5). Runs from within
4
+ * the reviewer, once per task whose run state carries `baseBranch`:
5
+ *
6
+ * - Parent still open, child PR based on something other than the parent →
7
+ * `gh pr edit --base <parent>`.
8
+ * - Parent merged (a PR on the parent's branch has state `merged` — the
9
+ * parent issue's canonical `done` status is never on its own proof of a
10
+ * merge: a ticket closed as duplicate, or completed via `crew task done`
11
+ * with an unmerged/closed PR, is `done` but not merged) and the child PR
12
+ * is still based on the parent (or GitHub already moved it to the default
13
+ * branch itself) → `gh pr edit --base <default>`, then rebase onto the
14
+ * default branch and force-push when the worktree is clean, or flag
15
+ * `needsRebase` when it isn't. A successful rebase also tries to reclaim
16
+ * the parent's now-unreferenced branch (spec section 6) — `worktrees.teardown`
17
+ * only checks this at the moment the parent's own worktree is torn down,
18
+ * which is usually before this rebase runs.
19
+ * - Parent not merged, and the parent issue is done (or its worktree is
20
+ * gone) — the parent will never merge, so retargeting onto or rebasing
21
+ * onto a branch that is about to vanish would be wrong. Leave the child
22
+ * untouched (`parent_done_unmerged`).
23
+ *
24
+ * A task with no `baseBranch` in run state, or no open PR yet, is left
25
+ * alone — there is nothing to correct. Every git/gh failure is caught,
26
+ * logged, and left for the next tick; one task's failure never blocks
27
+ * another's.
28
+ */
29
+ import type { ResolvedConfig } from "../lib/config.ts";
30
+ import { type StacksClient } from "../lib/githubStacks.ts";
31
+ import { type CountMergeCommits } from "../lib/pullRequests.ts";
32
+ import { type BoardState } from "../lib/taskSource.ts";
33
+ import { type WorktreeEntry } from "../lib/worktrees.ts";
34
+ import type { FindPullRequests } from "./reviewer.ts";
35
+ export type RunGitCommand = (arguments_: {
36
+ cwd: string;
37
+ args: readonly string[];
38
+ signal?: AbortSignal;
39
+ }) => Promise<string>;
40
+ export type RunGhCommand = (arguments_: {
41
+ cwd: string;
42
+ args: readonly string[];
43
+ signal?: AbortSignal;
44
+ }) => Promise<string>;
45
+ export interface StackRetargetDeps {
46
+ findPullRequests: FindPullRequests;
47
+ runGit?: RunGitCommand;
48
+ runGh?: RunGhCommand;
49
+ stacks?: StacksClient;
50
+ countMergeCommits?: CountMergeCommits;
51
+ }
52
+ interface StackRetargetArguments {
53
+ config: ResolvedConfig;
54
+ state: BoardState;
55
+ worktreeEntries: readonly WorktreeEntry[];
56
+ dryRun: boolean;
57
+ signal?: AbortSignal;
58
+ }
59
+ export interface StackRetarget {
60
+ runOnce: (arguments_: StackRetargetArguments) => Promise<void>;
61
+ }
62
+ export declare function createStackRetarget(deps: StackRetargetDeps): StackRetarget;
63
+ export {};
64
+ //# sourceMappingURL=stackRetarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stackRetarget.d.ts","sourceRoot":"","sources":["../../src/commands/stackRetarget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,iBAAiB,EAGvB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,EACL,KAAK,UAAU,EAGhB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AA8CtD,MAAM,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEtB,MAAM,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAQtB,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED,UAAU,sBAAsB;IAC9B,MAAM,EAAE,cAAc,CAAC;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,aAAa;IAC5B,OAAO,EAAE,CAAC,UAAU,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAgyBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CAuD1E"}