@clipboard-health/groundcrew 3.1.2 → 3.1.4
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 +47 -17
- package/crew.config.example.ts +25 -10
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/commands/cleaner.d.ts.map +1 -1
- package/dist/commands/cleaner.js +6 -4
- package/dist/commands/cleanupWorkspace.d.ts.map +1 -1
- package/dist/commands/cleanupWorkspace.js +2 -0
- package/dist/commands/dispatcher.d.ts.map +1 -1
- package/dist/commands/dispatcher.js +6 -6
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +2 -2
- package/dist/commands/interruptWorkspace.d.ts +8 -0
- package/dist/commands/interruptWorkspace.d.ts.map +1 -0
- package/dist/commands/interruptWorkspace.js +108 -0
- package/dist/commands/orchestrator.d.ts +4 -2
- package/dist/commands/orchestrator.d.ts.map +1 -1
- package/dist/commands/orchestrator.js +6 -105
- package/dist/commands/resumeWorkspace.d.ts +7 -0
- package/dist/commands/resumeWorkspace.d.ts.map +1 -0
- package/dist/commands/resumeWorkspace.js +163 -0
- package/dist/commands/setupWorkspace.d.ts.map +1 -1
- package/dist/commands/setupWorkspace.js +78 -79
- package/dist/commands/ticketDoctor.d.ts +18 -3
- package/dist/commands/ticketDoctor.d.ts.map +1 -1
- package/dist/commands/ticketDoctor.js +105 -11
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/lib/agentLaunch.d.ts +29 -0
- package/dist/lib/agentLaunch.d.ts.map +1 -0
- package/dist/lib/agentLaunch.js +53 -0
- package/dist/lib/boardSource.d.ts +41 -5
- package/dist/lib/boardSource.d.ts.map +1 -1
- package/dist/lib/boardSource.js +211 -70
- package/dist/lib/config.d.ts +59 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +130 -22
- package/dist/lib/linearIssueStatus.d.ts +3 -1
- package/dist/lib/linearIssueStatus.d.ts.map +1 -1
- package/dist/lib/linearIssueStatus.js +0 -0
- package/dist/lib/runState.d.ts +46 -0
- package/dist/lib/runState.d.ts.map +1 -0
- package/dist/lib/runState.js +137 -0
- package/dist/lib/runStateCleanup.d.ts +4 -0
- package/dist/lib/runStateCleanup.d.ts.map +1 -0
- package/dist/lib/runStateCleanup.js +12 -0
- package/dist/lib/stagedLaunch.d.ts +32 -0
- package/dist/lib/stagedLaunch.d.ts.map +1 -0
- package/dist/lib/stagedLaunch.js +58 -0
- package/dist/lib/util.d.ts +0 -1
- package/dist/lib/util.d.ts.map +1 -1
- package/dist/lib/util.js +0 -4
- package/dist/lib/workspaces.d.ts +19 -1
- package/dist/lib/workspaces.d.ts.map +1 -1
- package/dist/lib/workspaces.js +29 -9
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +12 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Installs the `crew` binary. `@clipboard-health/clearance` is pulled in transitiv
|
|
|
71
71
|
|
|
72
72
|
Or drop `crew.config.ts` at the root of any repo you run `crew` from — `crew` discovers it via cosmiconfig project-walk. Any of `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, or `.config/crewrc{,.json}` work.
|
|
73
73
|
|
|
74
|
-
Set `linear.projectSlug` (paste the trailing slug of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Defaults cover everything else.
|
|
74
|
+
Set `linear.projects[].projectSlug` (paste the trailing slug of your Linear project URL, e.g. `ai-strategy-5152195762f3`), `workspace.projectDir`, and `workspace.knownRepositories`. Defaults cover everything else. To watch multiple projects from one `crew` instance, add more entries to `linear.projects`; they all share the same `orchestrator.maximumInProgress` budget.
|
|
75
75
|
|
|
76
76
|
Then clone each repo before the first `crew run` — groundcrew creates per-ticket worktrees from these clones, it does not auto-clone:
|
|
77
77
|
|
|
@@ -163,11 +163,11 @@ Watch `${XDG_CACHE_HOME:-$HOME/.cache}/clearance/clearance.log` for `DENY` lines
|
|
|
163
163
|
|
|
164
164
|
Three keys are required; everything else has a default.
|
|
165
165
|
|
|
166
|
-
| Key
|
|
167
|
-
|
|
|
168
|
-
| `linear.projectSlug`
|
|
169
|
-
| `workspace.projectDir`
|
|
170
|
-
| `workspace.knownRepositories`
|
|
166
|
+
| Key | What |
|
|
167
|
+
| ------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| `linear.projects[].projectSlug` | Trailing slug of each Linear project URL to watch (e.g. `ai-strategy-5152195762f3`). One or more entries. |
|
|
169
|
+
| `workspace.projectDir` | Parent dir for cloned repos and sibling ticket worktrees. |
|
|
170
|
+
| `workspace.knownRepositories` | Repos searched for in ticket descriptions to infer where work belongs. |
|
|
171
171
|
|
|
172
172
|
`crew` resolves config as: `GROUNDCREW_CONFIG` if set → project-walk from cwd (cosmiconfig: `crew.config.{ts,mjs,js,json}`, `.crewrc{,.json,.ts}`, `.config/crew.config.{ts,json}`, `.config/crewrc{,.json}`) → `${XDG_CONFIG_HOME:-$HOME/.config}/groundcrew/crew.config.ts` (also accepts legacy `config.ts` for one release). The branch prefix (`<prefix>-<TICKET>`) is derived from `os.userInfo().username` — not configurable.
|
|
173
173
|
|
|
@@ -197,16 +197,17 @@ This keeps package defaults portable while letting your private config reference
|
|
|
197
197
|
|
|
198
198
|
| Key | Default | What it does |
|
|
199
199
|
| --------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
200
|
-
| `linear.
|
|
201
|
-
| `linear.
|
|
202
|
-
| `linear.statuses.
|
|
203
|
-
| `linear.statuses.
|
|
204
|
-
| `linear.statuses.
|
|
200
|
+
| `linear.projects` | **required** | Non-empty array of Linear projects to watch. One `crew` instance dispatches across every entry under a shared `maximumInProgress` budget. |
|
|
201
|
+
| `linear.projects[].projectSlug` | **required** | Linear project URL slug (e.g. `ai-strategy-5152195762f3`). The trailing 12-char hex `slugId` is what's matched against Linear's API; the leading name keeps `crew.config.ts` self-documenting and the lookup survives project renames. |
|
|
202
|
+
| `linear.projects[].statuses.todo` | `"Todo"` | Status name picked up for new work in this project. Per-project so multi-team setups with divergent state names can coexist. |
|
|
203
|
+
| `linear.projects[].statuses.inProgress` | `"In Progress"` | Status set after a workspace is provisioned; counts toward the shared `maximumInProgress`. |
|
|
204
|
+
| `linear.projects[].statuses.done` | `"Done"` | Status that triggers worktree cleanup for this project. |
|
|
205
|
+
| `linear.projects[].statuses.terminal` | `["Done"]` | Additional status names treated as terminal for cleanup and blocker checks. The project's `done` status is always included. |
|
|
205
206
|
| `git.remote` | `"origin"` | Remote used for `fetch` and as the worktree base ref. |
|
|
206
207
|
| `git.defaultBranch` | `"main"` | Branch fetched from `git.remote` and used as the worktree base. |
|
|
207
208
|
| `workspace.projectDir` | **required** | Parent dir for cloned repos and sibling ticket worktrees. |
|
|
208
209
|
| `workspace.knownRepositories` | **required** | Repos searched for in ticket descriptions to infer where work belongs. A ticket labeled for groundcrew (`agent-*`) fails fast when no known repo appears; unlabeled tickets are ignored. |
|
|
209
|
-
| `orchestrator.maximumInProgress` | `4` | Cap on tickets in `linear.
|
|
210
|
+
| `orchestrator.maximumInProgress` | `4` | Cap on in-progress tickets at once, shared across every project in `linear.projects`. |
|
|
210
211
|
| `orchestrator.pollIntervalMilliseconds` | `120_000` | Poll interval in `--watch` mode. |
|
|
211
212
|
| `orchestrator.sessionLimitPercentage` | `85` | Number in `(0, 100]`. A model whose codexbar session window exceeds this percentage is skipped that tick. |
|
|
212
213
|
| `models.default` | `"claude"` | Tiebreak for `agent-any` resolution and fallback for explicit but unknown `agent-*` labels. Also used by `crew run --ticket <TICKET>` for unlabeled tickets. `crew run` without `--ticket` ignores unlabeled tickets and does not apply this default. Must exist in `models.definitions`. |
|
|
@@ -316,14 +317,16 @@ crew run # one-shot dispatch
|
|
|
316
317
|
crew run --watch # poll forever
|
|
317
318
|
crew run --ticket <TICKET> # provision one ticket and exit
|
|
318
319
|
crew setup repos [--dry-run] [<repo>...]
|
|
320
|
+
crew interrupt <TICKET> [--reason <text>] # stop the live workspace, keep the worktree
|
|
321
|
+
crew resume <TICKET> # reopen an existing ticket worktree
|
|
319
322
|
crew cleanup <TICKET> # tear down every worktree carrying this ticket
|
|
320
323
|
```
|
|
321
324
|
|
|
322
|
-
`crew doctor --ticket <TICKET>` covers the full per-ticket lifecycle: pre-dispatch eligibility (Todo status, `agent-*` label, model resolution, repository mention, local clone, blockers, model session usage, in-progress capacity) **and** post-dispatch local-state recovery (host worktree, workspace pane, local branch, remote branch, open PR). Verdict precedence
|
|
325
|
+
`crew doctor --ticket <TICKET>` covers the full per-ticket lifecycle: pre-dispatch eligibility (Todo status, `agent-*` label, model resolution, repository mention, local clone, blockers, model session usage, in-progress capacity) **and** post-dispatch local-state recovery (recorded run state, host worktree, workspace pane, local branch, remote branch, open PR). Verdict precedence starts with PR outcomes (`pr-open` > `pr-merged`). Recorded failed launches report before ordinary local recovery, interrupted runs report concrete recoverable git work first when it exists and otherwise report `interrupted`, and ordinary post-dispatch cases report `in-flight` before `recoverable`. If none of those apply, doctor falls through to `unresolvable` > `ineligible` > `would-dispatch` > `lost`. Exits 0 on `would-dispatch`, `pr-open`, or `pr-merged`; any other verdict exits 1. `--watch` and `--ticket` are mutually exclusive. To inspect codexbar session windows directly, run `codexbar usage`.
|
|
323
326
|
|
|
324
327
|
### `crew doctor --ticket <ticket>`
|
|
325
328
|
|
|
326
|
-
Diagnose where a ticket is in its lifecycle and what to do next. Runs the same resolution and eligibility chain as the dispatcher, plus probes
|
|
329
|
+
Diagnose where a ticket is in its lifecycle and what to do next. Runs the same resolution and eligibility chain as the dispatcher, plus probes recorded run state, host worktree, workspace pane, local branch, remote branch, and PR; prints a single verdict with a copy-pasteable recovery step when one applies.
|
|
327
330
|
|
|
328
331
|
Flags:
|
|
329
332
|
|
|
@@ -344,6 +347,13 @@ Resolution
|
|
|
344
347
|
Eligibility
|
|
345
348
|
(skipped — post-dispatch — pre-dispatch checks are irrelevant)
|
|
346
349
|
|
|
350
|
+
Run state
|
|
351
|
+
[ok] Local run state (running)
|
|
352
|
+
[ok] Recorded model (claude)
|
|
353
|
+
[ok] Recorded worktree (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
|
|
354
|
+
[ok] Recorded branch (paul-hrd-442)
|
|
355
|
+
[ok] Resume count (0)
|
|
356
|
+
|
|
347
357
|
Worktree
|
|
348
358
|
[ok] Host worktree exists (/Users/paul/dev/groundcrew-workspaces/herds-social/herds-hrd-442)
|
|
349
359
|
[--] Working tree clean (0 modified, 1 untracked)
|
|
@@ -374,11 +384,31 @@ The verdict on the last line maps to a recovery action:
|
|
|
374
384
|
| `pr-merged` | Done. |
|
|
375
385
|
| `in-flight` | The ticket is still being worked on; the verdict line names the workspace pane to attach to. |
|
|
376
386
|
| `recoverable` | Run the printed `nextStep` exactly. |
|
|
387
|
+
| `interrupted` | Resume the preserved worktree with `crew resume <ticket>` or inspect it by hand. |
|
|
388
|
+
| `failed-launch` | Fix the launch failure, then run `crew resume <ticket>` or `crew cleanup <ticket>`. |
|
|
377
389
|
| `would-dispatch` | Pre-dispatch checks pass; the orchestrator will pick the ticket up on its next tick. |
|
|
378
390
|
| `ineligible` | A resolution or eligibility check failed; the reason after the colon names the failing check. |
|
|
379
391
|
| `unresolvable` | The Linear ticket couldn't be fetched; the reason after the colon names the error. |
|
|
380
392
|
| `lost` | No trace exists. Re-dispatch via `crew run --ticket <ticket>`. |
|
|
381
393
|
|
|
394
|
+
### `crew interrupt <ticket>`
|
|
395
|
+
|
|
396
|
+
Stop a live workspace pane while preserving the ticket worktree and branch. This is the manual pause button for cases where you need terminal capacity back, want to stop an agent that is going in the wrong direction, or need to inspect the diff before letting another agent continue.
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
crew interrupt HRD-442 --reason "wrong implementation direction"
|
|
400
|
+
crew doctor --ticket HRD-442
|
|
401
|
+
crew resume HRD-442
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The command closes the cmux/tmux workspace when it exists, records local run state under the groundcrew state directory, and never tears down the worktree. If the workspace was already gone but the worktree is still present, interrupt records that fact so doctor can point at the preserved branch instead of reporting a mystery ticket.
|
|
405
|
+
|
|
406
|
+
### `crew resume <ticket>`
|
|
407
|
+
|
|
408
|
+
Reopen an existing ticket worktree with a continuation prompt. Resume never creates a new worktree; if none exists, it fails and leaves re-dispatch to `crew run --ticket <ticket>`.
|
|
409
|
+
|
|
410
|
+
The resume prompt tells the agent to inspect current git status and diff before editing, includes the previous interrupt reason when recorded, and reuses the recorded model, repository, branch, runner, sandbox, and workspace backend. When no run-state file exists but a worktree does, resume falls back to Linear resolution for the model and ticket context.
|
|
411
|
+
|
|
382
412
|
## Troubleshooting
|
|
383
413
|
|
|
384
414
|
First stop for "labeled but not on the board": `crew doctor --ticket <ticket>` lists every check the dispatcher runs and flags the failing one.
|
|
@@ -414,7 +444,7 @@ When a wrapped agent command fails (e.g. `safehouse-clearance` not found, `npm i
|
|
|
414
444
|
<details>
|
|
415
445
|
<summary>Status names matter</summary>
|
|
416
446
|
|
|
417
|
-
If your team uses `Started` instead of `In Progress`, set `linear.statuses.inProgress = "Started"
|
|
447
|
+
If your team uses `Started` instead of `In Progress`, set `linear.projects[].statuses.inProgress = "Started"` on that project's entry. Status overrides are per-project so divergent team workflows coexist.
|
|
418
448
|
|
|
419
449
|
</details>
|
|
420
450
|
|
|
@@ -433,9 +463,9 @@ Groundcrew sets a ticket to `inProgress` when it provisions a workspace and neve
|
|
|
433
463
|
</details>
|
|
434
464
|
|
|
435
465
|
<details>
|
|
436
|
-
<summary>
|
|
466
|
+
<summary>Cross-team projects need a consistent `inProgress` name per project</summary>
|
|
437
467
|
|
|
438
|
-
Cross-team projects work — the orchestrator caches the in-progress state ID per team — but every team in
|
|
468
|
+
Cross-team projects work — the orchestrator caches the in-progress state ID per `(team, statusName)` pair — but every team in a given project must use the same status name for that project's `statuses.inProgress`. If you watch two projects that share a Linear team but configure different `inProgress` names, each project's lookup is independent.
|
|
439
469
|
|
|
440
470
|
</details>
|
|
441
471
|
|
package/crew.config.example.ts
CHANGED
|
@@ -3,16 +3,30 @@ import type { Config } from "./src/lib/config.js";
|
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
linear: {
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
//
|
|
6
|
+
// One or more Linear projects to watch. A single `crew` process
|
|
7
|
+
// dispatches across all configured projects under a shared
|
|
8
|
+
// `orchestrator.maximumInProgress` budget.
|
|
9
|
+
//
|
|
10
|
+
// Each entry's `projectSlug` is the trailing segment of your Linear
|
|
11
|
+
// project URL — copy it verbatim, e.g. "ai-strategy-5152195762f3"
|
|
12
|
+
// from "https://linear.app/<workspace>/project/ai-strategy-5152195762f3".
|
|
13
|
+
// The 12-char hex tail is the canonical ID groundcrew uses, so the
|
|
14
|
+
// orchestrator stays resilient across project renames and across
|
|
15
|
+
// same-name projects in different teams. The leading name segment
|
|
16
|
+
// keeps the file self-documenting at a glance.
|
|
17
|
+
//
|
|
18
|
+
// `statuses` is per-project so multi-team setups with divergent
|
|
19
|
+
// workflow state names (e.g. "Todo" vs "To Do", "Shipped" vs
|
|
20
|
+
// "Done") can coexist. Each field falls back to its default when
|
|
21
|
+
// omitted: { todo: "Todo", inProgress: "In Progress",
|
|
22
|
+
// done: "Done", terminal: ["Done"] }.
|
|
23
|
+
projects: [
|
|
24
|
+
{ projectSlug: "your-project-name-0123456789ab" },
|
|
25
|
+
// {
|
|
26
|
+
// projectSlug: "platform-aaaaaaaaaaaa",
|
|
27
|
+
// statuses: { inProgress: "Doing", done: "Released", terminal: ["Released", "Won't Do"] },
|
|
28
|
+
// },
|
|
29
|
+
],
|
|
16
30
|
},
|
|
17
31
|
workspace: {
|
|
18
32
|
// Parent directory under which groundcrew clones repositories and
|
|
@@ -29,6 +43,7 @@ export default {
|
|
|
29
43
|
// git: { remote: "origin", defaultBranch: "main" },
|
|
30
44
|
//
|
|
31
45
|
// orchestrator: {
|
|
46
|
+
// // Shared across all watched projects in linear.projects.
|
|
32
47
|
// maximumInProgress: 4,
|
|
33
48
|
// pollIntervalMilliseconds: 120_000,
|
|
34
49
|
// sessionLimitPercentage: 85,
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA6JA,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvD"}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { cleanupWorkspaceCli } from "./commands/cleanupWorkspace.js";
|
|
3
3
|
import { doctor } from "./commands/doctor.js";
|
|
4
|
+
import { interruptWorkspaceCli } from "./commands/interruptWorkspace.js";
|
|
4
5
|
import { orchestrate } from "./commands/orchestrator.js";
|
|
6
|
+
import { resumeWorkspaceCli } from "./commands/resumeWorkspace.js";
|
|
5
7
|
import { setupReposCli } from "./commands/setupRepos.js";
|
|
6
8
|
import { setupWorkspaceCli } from "./commands/setupWorkspace.js";
|
|
7
9
|
import { errorMessage, readTicketArgument, writeError, writeOutput } from "./lib/util.js";
|
|
@@ -90,6 +92,16 @@ const SUBCOMMANDS = {
|
|
|
90
92
|
usage: "[--force] <ticket>",
|
|
91
93
|
invoke: cleanupWorkspaceCli,
|
|
92
94
|
},
|
|
95
|
+
interrupt: {
|
|
96
|
+
summary: "Stop a live ticket workspace while preserving its worktree",
|
|
97
|
+
usage: "<ticket> [--reason <text>]",
|
|
98
|
+
invoke: interruptWorkspaceCli,
|
|
99
|
+
},
|
|
100
|
+
resume: {
|
|
101
|
+
summary: "Reopen an existing ticket worktree with a continuation prompt",
|
|
102
|
+
usage: "<ticket>",
|
|
103
|
+
invoke: resumeWorkspaceCli,
|
|
104
|
+
},
|
|
93
105
|
setup: {
|
|
94
106
|
summary: "Project-level setup commands (currently: repos)",
|
|
95
107
|
usage: "repos [--dry-run] [<repo>...]",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../src/commands/cleaner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"cleaner.d.ts","sourceRoot":"","sources":["../../src/commands/cleaner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,UAAU,EAA4B,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,qBAAqB,CAAC;AAGpE,UAAU,WAAW;IACnB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,UAAU,CAAC;QAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;QAC1C,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAsDxD"}
|
package/dist/commands/cleaner.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* tickets that have reached a terminal status. One per `orchestrate()`
|
|
4
4
|
* invocation; stateless across iterations. Mirrors `Dispatcher`.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { isTerminalStatusForIssue } from "../lib/boardSource.js";
|
|
7
|
+
import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
|
|
7
8
|
import { log, logEvent } from "../lib/util.js";
|
|
8
9
|
import { worktrees } from "../lib/worktrees.js";
|
|
9
10
|
import { logTeardown, recordTeardownEvents } from "./teardownReporter.js";
|
|
@@ -11,10 +12,10 @@ export function createCleaner(deps) {
|
|
|
11
12
|
const { config } = deps;
|
|
12
13
|
async function runOnce(arguments_) {
|
|
13
14
|
const { state, worktreeEntries, dryRun, signal } = arguments_;
|
|
14
|
-
// Only act on tickets in
|
|
15
|
-
// like a Linear ticket from another project, leave it alone.
|
|
15
|
+
// Only act on tickets in configured projects — if the dir name happens to
|
|
16
|
+
// look like a Linear ticket from another project, leave it alone.
|
|
16
17
|
const terminalTickets = new Set(state.issues
|
|
17
|
-
.filter((issue) =>
|
|
18
|
+
.filter((issue) => isTerminalStatusForIssue(issue, config))
|
|
18
19
|
.map((issue) => issue.id));
|
|
19
20
|
if (terminalTickets.size === 0) {
|
|
20
21
|
return;
|
|
@@ -41,6 +42,7 @@ export function createCleaner(deps) {
|
|
|
41
42
|
const result = signal === undefined
|
|
42
43
|
? await worktrees.teardown(config, stale)
|
|
43
44
|
: await worktrees.teardown(config, stale, { signal });
|
|
45
|
+
recordCleanedUpRuns(config, result.removed);
|
|
44
46
|
logTeardown(result);
|
|
45
47
|
recordTeardownEvents(result);
|
|
46
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"cleanupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/cleanupWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../lib/config.js";
|
|
2
|
+
import { recordCleanedUpRuns } from "../lib/runStateCleanup.js";
|
|
2
3
|
import { log } from "../lib/util.js";
|
|
3
4
|
import { worktrees } from "../lib/worktrees.js";
|
|
4
5
|
import { logTeardown } from "./teardownReporter.js";
|
|
@@ -29,6 +30,7 @@ export async function cleanupWorkspace(config, options) {
|
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
const result = await worktrees.teardown(config, entries, { force });
|
|
33
|
+
recordCleanedUpRuns(config, result.removed);
|
|
32
34
|
logTeardown(result);
|
|
33
35
|
if (result.failures.length > 0) {
|
|
34
36
|
throw result.failures[0]?.error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/commands/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAWzD,UAAU,cAAc;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,UAAU,EAAE;QAClB,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;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAuLjE"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Pure verdict logic lives in `eligibility.ts`; this module is responsible
|
|
7
7
|
* for telemetry, Linear writes, and side-effecting setupWorkspace calls.
|
|
8
8
|
*/
|
|
9
|
-
import { isGroundcrewIssue } from "../lib/boardSource.js";
|
|
9
|
+
import { isGroundcrewIssue, projectFor, } from "../lib/boardSource.js";
|
|
10
10
|
import { createLinearIssueStatusUpdater } from "../lib/linearIssueStatus.js";
|
|
11
11
|
import { errorMessage, log, logEvent } from "../lib/util.js";
|
|
12
12
|
import { workspaces } from "../lib/workspaces.js";
|
|
@@ -87,17 +87,17 @@ export function createDispatcher(deps) {
|
|
|
87
87
|
async function runOnce(arguments_) {
|
|
88
88
|
const { state, worktreeEntries, usage, dryRun, signal } = arguments_;
|
|
89
89
|
issueStatusUpdater.resetMissingInProgressCache();
|
|
90
|
-
const activeCount = state.issues.filter((issue) => issue.status === config.
|
|
90
|
+
const activeCount = state.issues.filter((issue) => issue.status === projectFor(issue, config).statuses.inProgress).length;
|
|
91
91
|
const slots = config.orchestrator.maximumInProgress - activeCount;
|
|
92
92
|
// Narrow Todo to tickets that opted in via an `agent-*` label.
|
|
93
93
|
// Unlabeled tickets are not groundcrew's concern even when in Todo.
|
|
94
|
-
const todo = state.issues.filter((issue) => issue.status === config.
|
|
94
|
+
const todo = state.issues.filter((issue) => issue.status === projectFor(issue, config).statuses.todo && isGroundcrewIssue(issue));
|
|
95
95
|
if (slots <= 0) {
|
|
96
96
|
log(`At capacity (${activeCount}/${config.orchestrator.maximumInProgress}), no new work to start`);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
if (todo.length === 0) {
|
|
100
|
-
log(`No
|
|
100
|
+
log(`No Todo tickets to pick up`);
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
// Run the blocker pre-pass first so an all-blocked board short-circuits
|
|
@@ -107,7 +107,7 @@ export function createDispatcher(deps) {
|
|
|
107
107
|
logSkip(skip);
|
|
108
108
|
}
|
|
109
109
|
if (unblocked.length === 0) {
|
|
110
|
-
log(`No eligible
|
|
110
|
+
log(`No eligible Todo tickets after blocker filtering`);
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
// usage() is an HTTP call; workspaces.probe shells tmux/cmux. Kick off
|
|
@@ -146,7 +146,7 @@ export function createDispatcher(deps) {
|
|
|
146
146
|
logSkip(skip);
|
|
147
147
|
}
|
|
148
148
|
if (starts.length === 0) {
|
|
149
|
-
log(`No eligible
|
|
149
|
+
log(`No eligible Todo tickets after eligibility filtering`);
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
log(`${slots} slot(s) available, starting ${starts.length} ticket(s): ${starts.map(({ issue }) => `${issue.id}(${issue.model})`).join(", ")}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../src/commands/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOzD,KAAK,UAAU,GACX,SAAS,GACT,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,4BAA4B,GAC5B,mBAAmB,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,WAAW,EAAE,UAAU,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,OAAO,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IACtC,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,oDAAoD;IACpD,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,UAAU,qBAAqB;IAC7B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAqCD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,MAAM,GAAG,SAAS,CAepB;AAaD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,GAClB,oBAAoB,EAAE,CAmCxB;AA4CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,qBAAqB,CAYvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,EAAE,CAgE5E"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The Dispatcher consumes the verdict list to drive logging and side
|
|
7
7
|
* effects.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { isTerminalStatusForBlocker, } from "../lib/boardSource.js";
|
|
10
10
|
import { AGENT_ANY_MODEL } from "../lib/config.js";
|
|
11
11
|
const PERCENT_FRACTION_DIVISOR = 100;
|
|
12
12
|
const DAYS_PER_WEEK = 7;
|
|
@@ -26,7 +26,7 @@ function blockerVerdictFor(issue, config) {
|
|
|
26
26
|
blockers,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
const unresolved = issue.blockers.filter((blocker) =>
|
|
29
|
+
const unresolved = issue.blockers.filter((blocker) => !isTerminalStatusForBlocker(blocker, config));
|
|
30
30
|
if (unresolved.length === 0) {
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "../lib/config.ts";
|
|
2
|
+
export interface InterruptWorkspaceOptions {
|
|
3
|
+
ticket: string;
|
|
4
|
+
reason?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function interruptWorkspace(config: ResolvedConfig, options: InterruptWorkspaceOptions): Promise<void>;
|
|
7
|
+
export declare function interruptWorkspaceCli(argv: string[]): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=interruptWorkspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interruptWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/interruptWorkspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMnE,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuGD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { loadConfig } from "../lib/config.js";
|
|
2
|
+
import { readRunState, recordRunState } from "../lib/runState.js";
|
|
3
|
+
import { errorMessage, log } from "../lib/util.js";
|
|
4
|
+
import { workspaces } from "../lib/workspaces.js";
|
|
5
|
+
import { worktrees } from "../lib/worktrees.js";
|
|
6
|
+
function parseArguments(argv) {
|
|
7
|
+
let reason;
|
|
8
|
+
const positionals = [];
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
const argument = argv[index];
|
|
11
|
+
/* v8 ignore next @preserve -- loop bounds ensure argv[index] exists; guard satisfies noUncheckedIndexedAccess */
|
|
12
|
+
if (argument === undefined) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (argument === "--reason") {
|
|
16
|
+
const value = argv[index + 1];
|
|
17
|
+
if (value === undefined || value.length === 0 || value.startsWith("-")) {
|
|
18
|
+
throw new Error("crew interrupt --reason: reason text is required");
|
|
19
|
+
}
|
|
20
|
+
reason = value;
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (argument.startsWith("-")) {
|
|
25
|
+
throw new Error(`Unknown option: ${argument}\nUsage: crew interrupt <ticket> [--reason <text>]`);
|
|
26
|
+
}
|
|
27
|
+
positionals.push(argument);
|
|
28
|
+
}
|
|
29
|
+
const [ticket, ...extras] = positionals;
|
|
30
|
+
if (ticket === undefined || ticket.length === 0 || extras.length > 0) {
|
|
31
|
+
throw new Error("Usage: crew interrupt <ticket> [--reason <text>]");
|
|
32
|
+
}
|
|
33
|
+
return { ticket: ticket.toLowerCase(), ...(reason === undefined ? {} : { reason }) };
|
|
34
|
+
}
|
|
35
|
+
function sourceFromState(state) {
|
|
36
|
+
return {
|
|
37
|
+
ticket: state.ticket,
|
|
38
|
+
repository: state.repository,
|
|
39
|
+
model: state.model,
|
|
40
|
+
worktreeDir: state.worktreeDir,
|
|
41
|
+
branchName: state.branchName,
|
|
42
|
+
workspaceName: state.workspaceName,
|
|
43
|
+
resumeCount: state.resumeCount,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function sourceFromWorktree(config, ticket, entry) {
|
|
47
|
+
return {
|
|
48
|
+
ticket,
|
|
49
|
+
repository: entry.repository,
|
|
50
|
+
model: config.models.default,
|
|
51
|
+
worktreeDir: entry.dir,
|
|
52
|
+
branchName: entry.branchName,
|
|
53
|
+
workspaceName: ticket,
|
|
54
|
+
resumeCount: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function resolveInterruptSource(arguments_) {
|
|
58
|
+
if (arguments_.state !== undefined) {
|
|
59
|
+
return sourceFromState(arguments_.state);
|
|
60
|
+
}
|
|
61
|
+
if (arguments_.entry !== undefined) {
|
|
62
|
+
return sourceFromWorktree(arguments_.config, arguments_.ticket, arguments_.entry);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`No run state or worktree found for ${arguments_.ticket}; nothing to interrupt.`);
|
|
65
|
+
}
|
|
66
|
+
function interruptDetail(result) {
|
|
67
|
+
if (result.kind === "missing") {
|
|
68
|
+
return "workspace missing";
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function failOnUnavailable(result) {
|
|
73
|
+
if (result.kind !== "unavailable") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const detail = result.error === undefined ? "workspace adapter unavailable" : errorMessage(result.error);
|
|
77
|
+
throw new Error(`Could not interrupt workspace: ${detail}`);
|
|
78
|
+
}
|
|
79
|
+
export async function interruptWorkspace(config, options) {
|
|
80
|
+
const ticket = options.ticket.toLowerCase();
|
|
81
|
+
const state = readRunState(config, ticket);
|
|
82
|
+
const [entry] = worktrees.findByTicket(config, ticket);
|
|
83
|
+
const source = resolveInterruptSource({ config, ticket, state, entry });
|
|
84
|
+
const result = await workspaces.interrupt(config, source.workspaceName);
|
|
85
|
+
failOnUnavailable(result);
|
|
86
|
+
const detail = interruptDetail(result);
|
|
87
|
+
recordRunState({
|
|
88
|
+
config,
|
|
89
|
+
state: {
|
|
90
|
+
ticket,
|
|
91
|
+
repository: source.repository,
|
|
92
|
+
model: source.model,
|
|
93
|
+
worktreeDir: source.worktreeDir,
|
|
94
|
+
branchName: source.branchName,
|
|
95
|
+
workspaceName: source.workspaceName,
|
|
96
|
+
state: "interrupted",
|
|
97
|
+
resumeCount: source.resumeCount,
|
|
98
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
99
|
+
...(detail === undefined ? {} : { detail }),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
log(`Interrupted ${ticket}; worktree preserved at ${source.worktreeDir}`);
|
|
103
|
+
log(`Next: crew doctor --ticket ${ticket}`);
|
|
104
|
+
}
|
|
105
|
+
export async function interruptWorkspaceCli(argv) {
|
|
106
|
+
const config = await loadConfig();
|
|
107
|
+
await interruptWorkspace(config, parseArguments(argv));
|
|
108
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* groundcrew orchestrator — polls
|
|
3
|
-
*
|
|
2
|
+
* groundcrew orchestrator — polls Linear projects and spins up workspace +
|
|
3
|
+
* git-worktree pairs for ready tickets. Each tick fetches the board, runs
|
|
4
|
+
* the cleaner, and runs the dispatcher; logging from those modules is the
|
|
5
|
+
* orchestrator's user-facing output.
|
|
4
6
|
*/
|
|
5
7
|
export interface OrchestratorOptions {
|
|
6
8
|
watch: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/commands/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2DH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7E"}
|