@clipboard-health/groundcrew 4.18.1 → 4.18.2

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.
@@ -1,21 +1,24 @@
1
1
  /**
2
- * Per-iteration scanner that advances in-progress tickets to in-review once
3
- * their worktree has an open (or merged) pull request. Sits between the cleaner
4
- * and the dispatcher in each `orchestrate()` tick. A successfully-applied
5
- * transition frees a dispatch slot (the slot math counts only in-progress)
6
- * while leaving the worktree intact for review, since the cleaner only tears
7
- * down `done` tickets.
2
+ * Per-iteration scanner that advances a ticket based on its worktree's pull
3
+ * request state. Sits between the cleaner and the dispatcher in each
4
+ * `orchestrate()` tick.
8
5
  *
9
- * The write-back lands in the ticket source, not the in-memory `BoardState`, so
10
- * the dispatcher in the SAME tick still sees the ticket as in-progress; the slot
11
- * frees on the NEXT tick's `board.fetch()`. That one-tick latency is deliberate
12
- * it keeps the reviewer from mutating shared `BoardState` mid-tick. One per
13
- * `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
6
+ * - An **open** PR on an **in-progress** ticket → `markInReview`: frees a
7
+ * dispatch slot (slot math counts only in-progress) while leaving the
8
+ * worktree intact for review, since the cleaner only tears down `done`.
9
+ * - A **merged** PR (on an in-progress or in-review ticket) → `markDone`:
10
+ * the work has landed, so the ticket is terminal and the cleaner tears the
11
+ * worktree down on a later tick. `merged` never routes to `in-review`.
14
12
  *
15
- * "Worktree has an open PR" is a v1 proxy for "the implementation is finished
16
- * and up for review". The detection + the open/merged condition live here in
17
- * the core reviewer (a push model) rather than inside any adapter, so a future
18
- * per-adapter `shouldAdvanceToReview` predicate is a clean additive change.
13
+ * Sources that don't implement `markDone` (e.g. Linear) return `unsupported`;
14
+ * the reviewer logs the skip and does nothing there is no in-review
15
+ * fallback. (Linear's own GitHub integration moves merged issues to Done,
16
+ * which groundcrew observes via `fetch()`.)
17
+ *
18
+ * The write-back lands in the ticket source, not the in-memory `BoardState`,
19
+ * so the dispatcher in the SAME tick still sees prior state; the slot frees on
20
+ * the NEXT tick's `board.fetch()`. That one-tick latency is deliberate. One
21
+ * per `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
19
22
  */
20
23
  import type { Board } from "../lib/board.ts";
21
24
  import type { PullRequestSummary } from "../lib/pullRequests.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAsC,MAAM,wBAAwB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;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;CACpC;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,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAsBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAqG3D"}
1
+ {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;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;CACpC;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,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AA+CD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAwH3D"}
@@ -1,28 +1,47 @@
1
1
  /**
2
- * Per-iteration scanner that advances in-progress tickets to in-review once
3
- * their worktree has an open (or merged) pull request. Sits between the cleaner
4
- * and the dispatcher in each `orchestrate()` tick. A successfully-applied
5
- * transition frees a dispatch slot (the slot math counts only in-progress)
6
- * while leaving the worktree intact for review, since the cleaner only tears
7
- * down `done` tickets.
2
+ * Per-iteration scanner that advances a ticket based on its worktree's pull
3
+ * request state. Sits between the cleaner and the dispatcher in each
4
+ * `orchestrate()` tick.
8
5
  *
9
- * The write-back lands in the ticket source, not the in-memory `BoardState`, so
10
- * the dispatcher in the SAME tick still sees the ticket as in-progress; the slot
11
- * frees on the NEXT tick's `board.fetch()`. That one-tick latency is deliberate
12
- * it keeps the reviewer from mutating shared `BoardState` mid-tick. One per
13
- * `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
6
+ * - An **open** PR on an **in-progress** ticket → `markInReview`: frees a
7
+ * dispatch slot (slot math counts only in-progress) while leaving the
8
+ * worktree intact for review, since the cleaner only tears down `done`.
9
+ * - A **merged** PR (on an in-progress or in-review ticket) → `markDone`:
10
+ * the work has landed, so the ticket is terminal and the cleaner tears the
11
+ * worktree down on a later tick. `merged` never routes to `in-review`.
14
12
  *
15
- * "Worktree has an open PR" is a v1 proxy for "the implementation is finished
16
- * and up for review". The detection + the open/merged condition live here in
17
- * the core reviewer (a push model) rather than inside any adapter, so a future
18
- * per-adapter `shouldAdvanceToReview` predicate is a clean additive change.
13
+ * Sources that don't implement `markDone` (e.g. Linear) return `unsupported`;
14
+ * the reviewer logs the skip and does nothing there is no in-review
15
+ * fallback. (Linear's own GitHub integration moves merged issues to Done,
16
+ * which groundcrew observes via `fetch()`.)
17
+ *
18
+ * The write-back lands in the ticket source, not the in-memory `BoardState`,
19
+ * so the dispatcher in the SAME tick still sees prior state; the slot frees on
20
+ * the NEXT tick's `board.fetch()`. That one-tick latency is deliberate. One
21
+ * per `orchestrate()`; stateless across iterations. Mirrors `Cleaner`.
19
22
  */
20
- import { naturalIdFromCanonical } from "../lib/ticketSource.js";
23
+ import { naturalIdFromCanonical, } from "../lib/ticketSource.js";
21
24
  import { debug, errorMessage, log, logEvent } from "../lib/util.js";
22
- // A PR whose lifecycle means the work is up for (or past) review. `merged`
23
- // catches a PR that merged between ticks, so we still free the slot.
24
- function isReviewablePullRequest(pr) {
25
- return pr.state === "open" || pr.state === "merged";
25
+ // Maps a worktree's PRs to the transition its ticket should make. A merged PR
26
+ // means the work landed done; an open PR on an in-progress ticket means it's
27
+ // up for review. `merged` wins over `open`. An open PR on an already in-review
28
+ // ticket is a no-op (returns undefined). Closed-only PRs are ignored.
29
+ function intendedTransition(pullRequests, status) {
30
+ if (pullRequests.some((pr) => pr.state === "merged")) {
31
+ return "done";
32
+ }
33
+ if (status === "in-progress" && pullRequests.some((pr) => pr.state === "open")) {
34
+ return "in-review";
35
+ }
36
+ return undefined;
37
+ }
38
+ // The PR to name in logs for a transition: the merged one for `done`, the open
39
+ // one for `in-review`. Guaranteed to exist because intendedTransition only
40
+ // returns a transition when a PR of the matching state is present.
41
+ function pullRequestForTransition(pullRequests, transition) {
42
+ const state = transition === "done" ? "merged" : "open";
43
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- intendedTransition guarantees a PR of this state exists
44
+ return pullRequests.find((pr) => pr.state === state);
26
45
  }
27
46
  function matchingWorktreeEntries(arguments_) {
28
47
  const { issue, worktreeEntries, ticket } = arguments_;
@@ -35,12 +54,12 @@ export function createReviewer(deps) {
35
54
  const { board, findPullRequests } = deps;
36
55
  async function runOnce(arguments_) {
37
56
  const { state, worktreeEntries, dryRun, signal } = arguments_;
38
- const inProgress = state.issues.filter((issue) => issue.status === "in-progress");
39
- if (inProgress.length === 0) {
57
+ const candidates = state.issues.filter((issue) => issue.status === "in-progress" || issue.status === "in-review");
58
+ if (candidates.length === 0) {
40
59
  return;
41
60
  }
42
- for (const issue of inProgress) {
43
- // oxlint-disable-next-line no-await-in-loop -- at most maximumInProgress (1-5) issues per tick; sequential keeps gh load low.
61
+ for (const issue of candidates) {
62
+ // oxlint-disable-next-line no-await-in-loop -- few candidates per tick; sequential keeps gh load low.
44
63
  await advanceIfReviewable({
45
64
  issue,
46
65
  worktreeEntries,
@@ -49,9 +68,10 @@ export function createReviewer(deps) {
49
68
  });
50
69
  }
51
70
  }
52
- // Idempotent after an applied transition: once advanced, the issue leaves
53
- // `in-progress`, so it never reaches this scan again. Unsupported writebacks
54
- // are skipped without claiming success and may retry on later ticks.
71
+ // Idempotent after an applied transition: once advanced, the issue leaves the
72
+ // in-progress/in-review candidate set, so it never reaches this scan again.
73
+ // Unsupported writebacks are skipped without claiming success and may retry
74
+ // on later ticks.
55
75
  async function advanceIfReviewable(arguments_) {
56
76
  const { issue, worktreeEntries, dryRun, signal } = arguments_;
57
77
  const ticket = naturalIdFromCanonical(issue.id);
@@ -59,8 +79,8 @@ export function createReviewer(deps) {
59
79
  for (const entry of entries) {
60
80
  // The injected lookup is contracted never to reject (failures resolve to
61
81
  // []), but we still guard it so one bad lookup can never abort the tick
62
- // and starve the other in-progress issues. A failure means "can't tell
63
- // yet" → skip this worktree and retry next tick.
82
+ // and starve the other candidates. A failure means "can't tell yet" →
83
+ // skip this worktree and retry next tick.
64
84
  let pullRequests;
65
85
  try {
66
86
  // oxlint-disable-next-line no-await-in-loop -- a ticket almost always has one worktree; sequential lookups are fine.
@@ -74,47 +94,61 @@ export function createReviewer(deps) {
74
94
  debug(`PR lookup failed for ${ticket} (${entry.branchName}): ${errorMessage(error)}`);
75
95
  continue;
76
96
  }
77
- const reviewable = pullRequests.find(isReviewablePullRequest);
78
- if (reviewable === undefined) {
97
+ const transition = intendedTransition(pullRequests, issue.status);
98
+ if (transition === undefined) {
79
99
  continue;
80
100
  }
101
+ const pullRequest = pullRequestForTransition(pullRequests, transition);
81
102
  if (dryRun) {
82
- log(`[dry-run] Would advance ${ticket} to in-review (PR ${reviewable.url})`);
83
- logEvent("review", { outcome: "skipped", reason: "dry_run", ticket, pr: reviewable.url });
103
+ log(`[dry-run] Would advance ${ticket} to ${transition} (PR ${pullRequest.url})`);
104
+ logEvent("review", {
105
+ outcome: "skipped",
106
+ reason: "dry_run",
107
+ ticket,
108
+ pr: pullRequest.url,
109
+ to: transition,
110
+ });
84
111
  return;
85
112
  }
86
- // oxlint-disable-next-line no-await-in-loop -- single write-back then return; never iterates past the first reviewable worktree.
87
- await advance({ issue, ticket, pullRequest: reviewable });
113
+ // oxlint-disable-next-line no-await-in-loop -- single write-back then return; never iterates past the first actionable worktree.
114
+ await advance({ issue, ticket, pullRequest, transition });
88
115
  return;
89
116
  }
90
117
  }
91
118
  // A writeback failure (shell/Linear error) is logged and swallowed: the
92
- // ticket stays in-progress and is retried next tick, exactly like a failed
119
+ // ticket keeps its status and is retried next tick, exactly like a failed
93
120
  // lookup. We never let one ticket's writeback abort the others' reviews.
94
121
  async function advance(arguments_) {
95
- const { issue, ticket, pullRequest } = arguments_;
122
+ const { issue, ticket, pullRequest, transition } = arguments_;
96
123
  try {
97
- const result = await board.markInReview(issue);
124
+ const result = transition === "done" ? await board.markDone(issue) : await board.markInReview(issue);
98
125
  if (result.outcome === "unsupported") {
99
- log(`Skipped advancing ${ticket} to in-review: ${result.reason}`);
126
+ log(`Skipped advancing ${ticket} to ${transition}: ${result.reason}`);
100
127
  logEvent("review", {
101
128
  outcome: "skipped",
102
129
  reason: "unsupported",
103
130
  ticket,
131
+ to: transition,
104
132
  });
105
133
  return;
106
134
  }
107
- log(`Advanced ${ticket} to in-review (PR ${pullRequest.url})`);
135
+ log(`Advanced ${ticket} to ${transition} (PR ${pullRequest.url})`);
108
136
  logEvent("review", {
109
137
  outcome: "advanced",
110
138
  ticket,
111
139
  pr: pullRequest.url,
112
140
  state: pullRequest.state,
141
+ to: transition,
113
142
  });
114
143
  }
115
144
  catch (error) {
116
- log(`Failed to advance ${ticket} to in-review: ${errorMessage(error)}`);
117
- logEvent("review", { outcome: "failed", reason: "writeback_failed", ticket });
145
+ log(`Failed to advance ${ticket} to ${transition}: ${errorMessage(error)}`);
146
+ logEvent("review", {
147
+ outcome: "failed",
148
+ reason: "writeback_failed",
149
+ ticket,
150
+ to: transition,
151
+ });
118
152
  }
119
153
  }
120
154
  return { runOnce };
@@ -12,6 +12,7 @@
12
12
  * in MVP-2.
13
13
  * - `markInProgress` absent → silent no-op.
14
14
  * - `markInReview` absent → reports unsupported.
15
+ * - `markDone` absent → reports unsupported.
15
16
  * - `fetch` is required by the Zod schema.
16
17
  */
17
18
  import type { AdapterContext } from "../../adapterDefinition.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGL,KAAK,KAAK,IAAI,cAAc,EAE5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AA4BrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAuB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CAoGd"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGL,KAAK,KAAK,IAAI,cAAc,EAG5B,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AA+BrB,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAuB3F;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,cAAc,GACvB,YAAY,CA8Gd"}
@@ -12,6 +12,7 @@
12
12
  * in MVP-2.
13
13
  * - `markInProgress` absent → silent no-op.
14
14
  * - `markInReview` absent → reports unsupported.
15
+ * - `markDone` absent → reports unsupported.
15
16
  * - `fetch` is required by the Zod schema.
16
17
  */
17
18
  import { toCanonicalId, } from "../../ticketSource.js";
@@ -23,6 +24,7 @@ const DEFAULT_TIMEOUTS = {
23
24
  resolveOne: 10_000,
24
25
  markInProgress: 10_000,
25
26
  markInReview: 10_000,
27
+ markDone: 10_000,
26
28
  };
27
29
  function mergeTimeouts(overrides) {
28
30
  return {
@@ -31,6 +33,7 @@ function mergeTimeouts(overrides) {
31
33
  resolveOne: overrides?.resolveOne ?? DEFAULT_TIMEOUTS.resolveOne,
32
34
  markInProgress: overrides?.markInProgress ?? DEFAULT_TIMEOUTS.markInProgress,
33
35
  markInReview: overrides?.markInReview ?? DEFAULT_TIMEOUTS.markInReview,
36
+ markDone: overrides?.markDone ?? DEFAULT_TIMEOUTS.markDone,
34
37
  };
35
38
  }
36
39
  export function toCanonicalIssue(shellIssue, sourceName) {
@@ -149,5 +152,15 @@ export function createShellTicketSource(config, _context) {
149
152
  await invokeWriteback(config.commands.markInReview, timeouts.markInReview, issue);
150
153
  return { outcome: "applied" };
151
154
  },
155
+ async markDone(issue) {
156
+ if (config.commands.markDone === undefined) {
157
+ return {
158
+ outcome: "unsupported",
159
+ reason: `shell source "${sourceName}" has no commands.markDone configured`,
160
+ };
161
+ }
162
+ await invokeWriteback(config.commands.markDone, timeouts.markDone, issue);
163
+ return { outcome: "applied" };
164
+ },
152
165
  };
153
166
  }
@@ -89,6 +89,7 @@ export declare const shellAdapterConfigSchema: z.ZodObject<{
89
89
  resolveOne: z.ZodOptional<z.ZodString>;
90
90
  markInProgress: z.ZodOptional<z.ZodString>;
91
91
  markInReview: z.ZodOptional<z.ZodString>;
92
+ markDone: z.ZodOptional<z.ZodString>;
92
93
  }, z.core.$strip>;
93
94
  cwd: z.ZodOptional<z.ZodString>;
94
95
  timeouts: z.ZodOptional<z.ZodObject<{
@@ -97,6 +98,7 @@ export declare const shellAdapterConfigSchema: z.ZodObject<{
97
98
  resolveOne: z.ZodOptional<z.ZodNumber>;
98
99
  markInProgress: z.ZodOptional<z.ZodNumber>;
99
100
  markInReview: z.ZodOptional<z.ZodNumber>;
101
+ markDone: z.ZodOptional<z.ZodNumber>;
100
102
  }, z.core.$strip>>;
101
103
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
102
104
  }, z.core.$strip>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;iBA0BnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/shell/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAA4B,CAAC;AAEhE,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;iBA4BnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
@@ -47,6 +47,7 @@ export const shellAdapterConfigSchema = z.object({
47
47
  resolveOne: z.string().optional(),
48
48
  markInProgress: z.string().optional(),
49
49
  markInReview: z.string().optional(),
50
+ markDone: z.string().optional(),
50
51
  }),
51
52
  cwd: z.string().optional(),
52
53
  timeouts: z
@@ -59,6 +60,7 @@ export const shellAdapterConfigSchema = z.object({
59
60
  resolveOne: z.number().int().positive().optional(),
60
61
  markInProgress: z.number().int().positive().optional(),
61
62
  markInReview: z.number().int().positive().optional(),
63
+ markDone: z.number().int().positive().optional(),
62
64
  })
63
65
  .optional(),
64
66
  env: z.record(z.string(), z.string()).optional(),
@@ -4,7 +4,7 @@
4
4
  * goes through this; the moment we skip the wrapper we grow Linear assumptions
5
5
  * back into consumers.
6
6
  */
7
- import { type BoardState, type Issue, type MarkInReviewResult, type TicketSource } from "./ticketSource.ts";
7
+ import { type BoardState, type Issue, type MarkDoneResult, type MarkInReviewResult, type TicketSource } from "./ticketSource.ts";
8
8
  export interface Board {
9
9
  verify(): Promise<void>;
10
10
  fetch(): Promise<BoardState>;
@@ -21,6 +21,12 @@ export interface Board {
21
21
  * return `unsupported` (see `TicketSource.markInReview`).
22
22
  */
23
23
  markInReview(issue: Issue): Promise<MarkInReviewResult>;
24
+ /**
25
+ * Advances a ticket to done on the adapter whose `name` matches
26
+ * `issue.source`. Unknown source throws. Sources that don't implement the
27
+ * optional `markDone` return `unsupported` (see `TicketSource.markDone`).
28
+ */
29
+ markDone(issue: Issue): Promise<MarkDoneResult>;
24
30
  }
25
31
  export declare function createBoard(sources: readonly TicketSource[]): Board;
26
32
  //# sourceMappingURL=board.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/lib/board.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,kBAAkB,EAEvB,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IACpB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,oBAAoB,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IACrE,wFAAwF;IACxF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAqBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,KAAK,CAwGnE"}
1
+ {"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/lib/board.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAEvB,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IACpB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,oBAAoB,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IACrE,wFAAwF;IACxF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACjD;AAqBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,KAAK,CAmHnE"}
package/dist/lib/board.js CHANGED
@@ -111,6 +111,16 @@ export function createBoard(sources) {
111
111
  async markInReview(issue) {
112
112
  return await routeWriteback(byName, issue).markInReview(issue);
113
113
  },
114
+ async markDone(issue) {
115
+ const source = routeWriteback(byName, issue);
116
+ if (source.markDone === undefined) {
117
+ return {
118
+ outcome: "unsupported",
119
+ reason: `source "${source.name}" does not support markDone`,
120
+ };
121
+ }
122
+ return await source.markDone(issue);
123
+ },
114
124
  };
115
125
  }
116
126
  /**
@@ -15,6 +15,8 @@ export interface PullRequestSummary {
15
15
  /** Lowercased lifecycle: "open" | "merged" | "closed". */
16
16
  state: string;
17
17
  title: string;
18
+ /** PR head commit SHA used to ignore historical PRs from reused branch names. */
19
+ headRefOid: string;
18
20
  }
19
21
  interface LookupArgs {
20
22
  /** Worktree directory; `gh` resolves the GitHub repo from its git remote. */
@@ -1 +1 @@
1
- {"version":3,"file":"pullRequests.d.ts","sourceRoot":"","sources":["../../src/lib/pullRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AASD,UAAU,UAAU;IAClB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAgDD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAyBxC"}
1
+ {"version":3,"file":"pullRequests.d.ts","sourceRoot":"","sources":["../../src/lib/pullRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,UAAU,UAAU;IAClB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmDD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CA6BxC"}
@@ -37,6 +37,7 @@ function parsePullRequests(output) {
37
37
  number: entry.number,
38
38
  state: STATE_MAP[entry.state] ?? entry.state.toLowerCase(),
39
39
  title: entry.title,
40
+ headRefOid: entry.headRefOid,
40
41
  });
41
42
  }
42
43
  return summaries;
@@ -50,27 +51,32 @@ function isRawPullRequest(value) {
50
51
  return (typeof record["url"] === "string" &&
51
52
  typeof record["number"] === "number" &&
52
53
  typeof record["state"] === "string" &&
53
- typeof record["title"] === "string");
54
+ typeof record["title"] === "string" &&
55
+ typeof record["headRefOid"] === "string");
54
56
  }
55
57
  export async function findPullRequestsForBranch(arguments_) {
56
58
  const { cwd, branchName, signal } = arguments_;
59
+ const options = signal === undefined ? { cwd } : { cwd, signal };
57
60
  try {
58
- const output = await runCommandAsync("gh", [
59
- "pr",
60
- "list",
61
- "--head",
62
- branchName,
63
- "--state",
64
- "all",
65
- "--limit",
66
- String(GH_PR_LIST_LIMIT),
67
- "--json",
68
- "url,number,state,title",
69
- ], signal === undefined ? { cwd } : { cwd, signal });
70
- return parsePullRequests(output);
61
+ const [output, currentHeadOid] = await Promise.all([
62
+ runCommandAsync("gh", [
63
+ "pr",
64
+ "list",
65
+ "--head",
66
+ branchName,
67
+ "--state",
68
+ "all",
69
+ "--limit",
70
+ String(GH_PR_LIST_LIMIT),
71
+ "--json",
72
+ "url,number,state,title,headRefOid",
73
+ ], options),
74
+ runCommandAsync("git", ["rev-parse", "HEAD"], options),
75
+ ]);
76
+ return parsePullRequests(output).filter((pr) => pr.headRefOid === currentHeadOid);
71
77
  }
72
78
  catch {
73
- // gh not installed / not authenticated / non-GitHub remote / network
79
+ // gh/git not installed / not authenticated / non-GitHub remote / network
74
80
  // error / etc. All resolve to "no PR info available" for display.
75
81
  return [];
76
82
  }
@@ -116,6 +116,12 @@ export type MarkInReviewResult = {
116
116
  outcome: "unsupported";
117
117
  reason: string;
118
118
  };
119
+ export type MarkDoneResult = {
120
+ outcome: "applied";
121
+ } | {
122
+ outcome: "unsupported";
123
+ reason: string;
124
+ };
119
125
  export interface TicketSource {
120
126
  /** Stable identifier used as the id prefix and in log lines. Equal to the source's config `name`. */
121
127
  readonly name: string;
@@ -136,6 +142,16 @@ export interface TicketSource {
136
142
  * rather than pretending the transition happened.
137
143
  */
138
144
  markInReview(issue: Issue): Promise<MarkInReviewResult>;
145
+ /**
146
+ * Optional writeback: advance a ticket to done once its PR has merged.
147
+ * Sources without a native/configured done transition omit this method; the
148
+ * Board treats an absent method as `{ outcome: "unsupported" }` so the
149
+ * reviewer can log the skip without claiming a transition that never
150
+ * happened. Linear omits it on purpose: on merge, Linear's own GitHub
151
+ * integration moves the issue to Done, which groundcrew then observes via
152
+ * `fetch()` and the cleaner tears down.
153
+ */
154
+ markDone?(issue: Issue): Promise<MarkDoneResult>;
139
155
  /**
140
156
  * Optional: return parent tickets that were excluded from `fetch()` because
141
157
  * they have sub-issues. Board surfaces these so the dispatcher can log WHY
@@ -1 +1 @@
1
- {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ticketSource.d.ts","sourceRoot":"","sources":["../../src/lib/ticketSource.ts"],"names":[],"mappings":"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"}
@@ -16,8 +16,9 @@ export default {
16
16
  resolveOne: "~/.config/groundcrew/jira-resolve.sh ${id}",
17
17
  markInProgress: "jira issue move ${id} 'In Progress'",
18
18
  markInReview: "jira issue move ${id} 'In Review'",
19
+ markDone: "jira issue move ${id} 'Done'",
19
20
  },
20
- timeouts: { fetch: 60_000, markInReview: 15_000 },
21
+ timeouts: { fetch: 60_000, markInReview: 15_000, markDone: 15_000 },
21
22
  },
22
23
  ],
23
24
  };
@@ -26,12 +27,15 @@ export default {
26
27
  `commands.fetch` must print a JSON array of issues. `commands.resolveOne`, when
27
28
  set, must print one issue, print nothing for "not found", or exit `3` for "not
28
29
  found". `commands.markInProgress`, when set, receives the issue's `sourceRef` as
29
- JSON on stdin. `commands.markInReview`, when set, receives the same `sourceRef`
30
- and is run after groundcrew sees an open or merged PR on the ticket's worktree
31
- branch. If `commands.markInReview` is omitted, groundcrew treats in-review
32
- advancement as unsupported for that source and does not claim the transition
33
- succeeded. `${id}`, `${canonicalId}`, and `${name}` placeholders are shell-quoted
34
- before substitution.
30
+ JSON on stdin. `commands.markInReview`, when set, receives the same `sourceRef` and is run
31
+ after groundcrew sees an **open** PR on the ticket's worktree branch (in-progress
32
+ tickets only). If omitted, groundcrew treats in-review advancement as unsupported
33
+ for that source and does not claim the transition succeeded. `commands.markDone`,
34
+ when set, receives the same `sourceRef` and is run after groundcrew sees a
35
+ **merged** PR on the ticket's worktree branch (a merged PR never advances to
36
+ in-review). If omitted, groundcrew treats done advancement as unsupported and
37
+ leaves the ticket for the source's own integration to close out. `${id}`,
38
+ `${canonicalId}`, and `${name}` placeholders are shell-quoted before substitution.
35
39
 
36
40
  ```json
37
41
  [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.18.1",
3
+ "version": "4.18.2",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",