@clipboard-health/groundcrew 4.26.0 → 4.26.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/dist/cli.js +2 -2
- package/dist/commands/dispatcher.d.ts +2 -2
- package/dist/commands/dispatcher.js +19 -19
- package/dist/commands/doctor.js +14 -14
- package/dist/commands/eligibility.d.ts +19 -19
- package/dist/commands/eligibility.d.ts.map +1 -1
- package/dist/commands/eligibility.js +21 -21
- package/dist/commands/init.d.ts +4 -4
- package/dist/commands/init.js +17 -17
- package/dist/commands/interruptWorkspace.js +3 -3
- package/dist/commands/orchestrator.js +2 -2
- package/dist/commands/resumeWorkspace.js +9 -9
- package/dist/commands/setupWorkspace.d.ts +1 -1
- package/dist/commands/setupWorkspace.js +14 -14
- package/dist/commands/status.js +4 -4
- package/dist/commands/task.js +8 -8
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/lib/adapterDefinition.d.ts +1 -1
- package/dist/lib/adapters/linear/create.js +1 -1
- package/dist/lib/adapters/linear/factory.js +2 -2
- package/dist/lib/adapters/linear/fetch.d.ts +6 -6
- package/dist/lib/adapters/linear/fetch.js +28 -28
- package/dist/lib/adapters/linear/parsing.d.ts +7 -7
- package/dist/lib/adapters/linear/parsing.d.ts.map +1 -1
- package/dist/lib/adapters/linear/parsing.js +14 -14
- package/dist/lib/adapters/shell/factory.js +1 -1
- package/dist/lib/adapters/shell/schema.d.ts +3 -3
- package/dist/lib/adapters/shell/schema.js +2 -2
- package/dist/lib/adapters/todo-txt/normalizer.js +3 -3
- package/dist/lib/adapters/todo-txt/source.d.ts.map +1 -1
- package/dist/lib/adapters/todo-txt/source.js +5 -7
- package/dist/lib/agentLaunch.d.ts +4 -4
- package/dist/lib/agentLaunch.js +7 -7
- package/dist/lib/cmuxAdapter.js +1 -1
- package/dist/lib/config.d.ts +22 -22
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +47 -44
- package/dist/lib/launchCommand.d.ts +4 -4
- package/dist/lib/launchCommand.js +4 -4
- package/dist/lib/runState.d.ts +2 -2
- package/dist/lib/runState.js +4 -4
- package/dist/lib/srtLaunch.d.ts +2 -2
- package/dist/lib/taskSource.d.ts +4 -4
- package/dist/lib/taskSource.d.ts.map +1 -1
- package/dist/lib/taskSource.js +1 -1
- package/dist/lib/testing/canonicalFixtures.js +2 -2
- package/dist/lib/usage.d.ts +7 -7
- package/dist/lib/usage.js +20 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { errorMessage, parseDryRunPositionals, readEnvironmentVariable, readTask
|
|
|
14
14
|
const REMOVED_SANDBOX_COMMAND_MESSAGE = [
|
|
15
15
|
"`crew sandbox` is no longer supported.",
|
|
16
16
|
"Groundcrew now launches agents inside existing sbx sandboxes but does not list, create, regenerate, authenticate, or remove them.",
|
|
17
|
-
"Use the manual `sbx` workflow in README.md#docker-sandboxes-sdx-setup, then keep `
|
|
17
|
+
"Use the manual `sbx` workflow in README.md#docker-sandboxes-sdx-setup, then keep `agents.definitions.<agent>.sandbox.agent` in crew.config.ts so launches can address the existing sandbox.",
|
|
18
18
|
].join("\n");
|
|
19
19
|
const requireFromCli = createRequire(import.meta.url);
|
|
20
20
|
const SETUP_REPOS_REMOVED_MESSAGE = [
|
|
@@ -112,7 +112,7 @@ async function doctorCli(argv) {
|
|
|
112
112
|
const SUBCOMMANDS = {
|
|
113
113
|
init: {
|
|
114
114
|
summary: "Create a crew.config.ts in the cwd (or --global into the XDG config dir)",
|
|
115
|
-
usage: "[--global | --local] [--force] [--dry-run] [--project-dir <dir>] [--repo <repo>]... [--runner <runner>] [--
|
|
115
|
+
usage: "[--global | --local] [--force] [--dry-run] [--project-dir <dir>] [--repo <repo>]... [--runner <runner>] [--agent <agent>]",
|
|
116
116
|
invoke: initConfigCli,
|
|
117
117
|
},
|
|
118
118
|
run: {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Board } from "../lib/board.ts";
|
|
10
10
|
import type { ResolvedConfig } from "../lib/config.ts";
|
|
11
11
|
import { type BoardState, type Issue } from "../lib/taskSource.ts";
|
|
12
|
-
import type {
|
|
12
|
+
import type { UsageByAgent } from "../lib/usage.ts";
|
|
13
13
|
import type { WorktreeEntry } from "../lib/worktrees.ts";
|
|
14
14
|
interface DispatcherDeps {
|
|
15
15
|
config: ResolvedConfig;
|
|
@@ -20,7 +20,7 @@ export interface Dispatcher {
|
|
|
20
20
|
state: BoardState;
|
|
21
21
|
worktreeEntries: readonly WorktreeEntry[];
|
|
22
22
|
/** Lazy so dispatcher can early-return on idle ticks without paying the codexbar shell-out. */
|
|
23
|
-
usage: (signal?: AbortSignal) => Promise<
|
|
23
|
+
usage: (signal?: AbortSignal) => Promise<UsageByAgent>;
|
|
24
24
|
dryRun: boolean;
|
|
25
25
|
signal?: AbortSignal;
|
|
26
26
|
/**
|
|
@@ -19,18 +19,18 @@ function logSkip(verdict) {
|
|
|
19
19
|
reason: verdict.eventReason,
|
|
20
20
|
task: naturalIdFromCanonical(verdict.issue.id),
|
|
21
21
|
blockers: verdict.blockers,
|
|
22
|
-
|
|
22
|
+
agent: verdict.agent,
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
function logMissingRepositorySkip(issue,
|
|
25
|
+
function logMissingRepositorySkip(issue, agent, knownRepositories) {
|
|
26
26
|
const task = naturalIdFromCanonical(issue.id);
|
|
27
|
-
log(styleWarning(`WARNING: ${issue.id} has agent "${
|
|
27
|
+
log(styleWarning(`WARNING: ${issue.id} has agent "${agent}" but no repository; skipping dispatch. Add --repo <repo> when creating the task, add repo:<repo> to the task line, or set defaultRepository on source "${issue.source}". Known repositories: ${formatKnownRepositories(knownRepositories)}`));
|
|
28
28
|
logEvent("dispatch", {
|
|
29
29
|
outcome: "skipped",
|
|
30
30
|
reason: "missing_repository",
|
|
31
31
|
task,
|
|
32
32
|
source: issue.source,
|
|
33
|
-
|
|
33
|
+
agent,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
export function createDispatcher(deps) {
|
|
@@ -38,7 +38,7 @@ export function createDispatcher(deps) {
|
|
|
38
38
|
function buildExhaustedSet(usage) {
|
|
39
39
|
const exhausted = new Set();
|
|
40
40
|
for (const exhaustion of classifyUsageExhaustion(config, usage)) {
|
|
41
|
-
exhausted.add(exhaustion.
|
|
41
|
+
exhausted.add(exhaustion.agent);
|
|
42
42
|
log(formatUsageExhaustion(exhaustion));
|
|
43
43
|
}
|
|
44
44
|
return exhausted;
|
|
@@ -47,17 +47,17 @@ export function createDispatcher(deps) {
|
|
|
47
47
|
const { issue, recovery } = start;
|
|
48
48
|
const taskId = naturalIdFromCanonical(issue.id);
|
|
49
49
|
if (start.resolvedFromAny) {
|
|
50
|
-
log(`Resolved agent-any for ${taskId} → ${issue.
|
|
50
|
+
log(`Resolved agent-any for ${taskId} → ${issue.agent}`);
|
|
51
51
|
}
|
|
52
52
|
if (dryRun) {
|
|
53
53
|
log(
|
|
54
54
|
/* v8 ignore next @preserve -- classifyTodo forces recovery=false in dry-run, so the resume branch can't fire here */
|
|
55
|
-
`[dry-run] Would ${recovery ? "resume" : "start"} ${taskId} in ${issue.repository} (${issue.
|
|
55
|
+
`[dry-run] Would ${recovery ? "resume" : "start"} ${taskId} in ${issue.repository} (${issue.agent})`);
|
|
56
56
|
logEvent("dispatch", {
|
|
57
57
|
outcome: "skipped",
|
|
58
58
|
reason: "dry_run",
|
|
59
59
|
task: taskId,
|
|
60
|
-
|
|
60
|
+
agent: issue.agent,
|
|
61
61
|
repository: issue.repository,
|
|
62
62
|
});
|
|
63
63
|
return;
|
|
@@ -70,7 +70,7 @@ export function createDispatcher(deps) {
|
|
|
70
70
|
const setupOptions = {
|
|
71
71
|
repository: issue.repository,
|
|
72
72
|
task: taskId,
|
|
73
|
-
|
|
73
|
+
agent: issue.agent,
|
|
74
74
|
details: {
|
|
75
75
|
title: issue.title,
|
|
76
76
|
description: issue.description,
|
|
@@ -85,7 +85,7 @@ export function createDispatcher(deps) {
|
|
|
85
85
|
logEvent("dispatch", {
|
|
86
86
|
outcome: recovery ? "resumed" : "started",
|
|
87
87
|
task: taskId,
|
|
88
|
-
|
|
88
|
+
agent: issue.agent,
|
|
89
89
|
repository: issue.repository,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
@@ -94,7 +94,7 @@ export function createDispatcher(deps) {
|
|
|
94
94
|
logEvent("dispatch", {
|
|
95
95
|
outcome: "failed",
|
|
96
96
|
task: taskId,
|
|
97
|
-
|
|
97
|
+
agent: issue.agent,
|
|
98
98
|
repository: issue.repository,
|
|
99
99
|
error: errorMessage(error),
|
|
100
100
|
});
|
|
@@ -122,9 +122,9 @@ export function createDispatcher(deps) {
|
|
|
122
122
|
const slots = config.orchestrator.maximumInProgress - activeCount;
|
|
123
123
|
const rawTodo = state.issues.filter((issue) => issue.status === "todo");
|
|
124
124
|
for (const issue of rawTodo) {
|
|
125
|
-
const {
|
|
126
|
-
if (
|
|
127
|
-
logMissingRepositorySkip(issue,
|
|
125
|
+
const { agent, repository } = issue;
|
|
126
|
+
if (agent !== undefined && repository === undefined) {
|
|
127
|
+
logMissingRepositorySkip(issue, agent, config.workspace.knownRepositories);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
// Narrow queued work to tasks that opted in with an agent and resolved a
|
|
@@ -212,10 +212,10 @@ export function createDispatcher(deps) {
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
const dispatchable = starts;
|
|
215
|
-
log(`Slots ${activeCount}/${config.orchestrator.maximumInProgress} used${formatActiveSlotList(active)}, starting ${dispatchable.length} task(s): ${dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}(${issue.
|
|
215
|
+
log(`Slots ${activeCount}/${config.orchestrator.maximumInProgress} used${formatActiveSlotList(active)}, starting ${dispatchable.length} task(s): ${dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}(${issue.agent})`).join(", ")}`);
|
|
216
216
|
logEvent("dispatch", {
|
|
217
217
|
outcome: "starting",
|
|
218
|
-
tasks: dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}:${issue.
|
|
218
|
+
tasks: dispatchable.map(({ issue }) => `${naturalIdFromCanonical(issue.id)}:${issue.agent}`),
|
|
219
219
|
});
|
|
220
220
|
for (const start of dispatchable) {
|
|
221
221
|
// oxlint-disable-next-line no-await-in-loop -- one workspace at a time avoids racing on git
|
|
@@ -233,9 +233,9 @@ function hasRecoverableCandidate(issues, worktreeEntries) {
|
|
|
233
233
|
function formatUsageExhaustion(exhaustion) {
|
|
234
234
|
if (exhaustion.kind === "session") {
|
|
235
235
|
const mins = exhaustion.resetMinutes ?? "?";
|
|
236
|
-
return `${exhaustion.
|
|
236
|
+
return `${exhaustion.agent} session at ${exhaustion.usedPercentage.toFixed(0)}% (> ${exhaustion.limitPercentage}%), resets in ${mins}m — skipping its tasks`;
|
|
237
237
|
}
|
|
238
|
-
return `${exhaustion.
|
|
238
|
+
return `${exhaustion.agent} weekly at ${exhaustion.usedPercentage.toFixed(1)}% (> ${exhaustion.allowedPercentage.toFixed(1)}% paced budget), resets in ${exhaustion.resetMinutes}m — skipping its tasks`;
|
|
239
239
|
}
|
|
240
240
|
/** Undefined priority sorts last. */
|
|
241
241
|
function prioritySortKey(priority) {
|
|
@@ -246,7 +246,7 @@ export function formatActiveSlotList(active) {
|
|
|
246
246
|
return "";
|
|
247
247
|
}
|
|
248
248
|
const entries = active
|
|
249
|
-
.map((issue) => `${naturalIdFromCanonical(issue.id)}(${issue.
|
|
249
|
+
.map((issue) => `${naturalIdFromCanonical(issue.id)}(${issue.agent ?? "?"})`)
|
|
250
250
|
.join(", ");
|
|
251
251
|
return ` [${entries}]`;
|
|
252
252
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,13 +8,13 @@ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
|
8
8
|
import { loadConfigWithSource, worktreeBaseDir, } from "../lib/config.js";
|
|
9
9
|
import { detectHostCapabilities, which } from "../lib/host.js";
|
|
10
10
|
import { resolveLocalRunner } from "../lib/localRunner.js";
|
|
11
|
-
import {
|
|
11
|
+
import { gatedAgents } from "../lib/usage.js";
|
|
12
12
|
import { errorMessage, writeOutput } from "../lib/util.js";
|
|
13
13
|
import { resolveWorkspaceKind } from "../lib/workspaces.js";
|
|
14
14
|
// Tokenization stops after this many non-flag tokens. Two is enough to
|
|
15
15
|
// catch wrapper + wrapped CLI commands like `safehouse claude --foo`.
|
|
16
16
|
const MAX_TOKENS_PER_CMD = 2;
|
|
17
|
-
const
|
|
17
|
+
const BUILT_IN_AGENT_NAMES = ["claude", "codex"];
|
|
18
18
|
const CONFIG_SOURCE_LABELS = {
|
|
19
19
|
env: "GROUNDCREW_CONFIG",
|
|
20
20
|
project: "project",
|
|
@@ -74,7 +74,7 @@ function checkDir(path, label) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Tokens worth checking against PATH from
|
|
77
|
+
* Tokens worth checking against PATH from an agent's `cmd`:
|
|
78
78
|
* the executable name (first non-flag token), and any subsequent
|
|
79
79
|
* non-flag, non-flag-value token until a flag is hit. Flag tokens are
|
|
80
80
|
* dropped along with the token immediately following them (treated as
|
|
@@ -111,9 +111,9 @@ function commandTokensToCheck(cmd) {
|
|
|
111
111
|
}
|
|
112
112
|
function gatherToolTargets(config) {
|
|
113
113
|
const all = new Map();
|
|
114
|
-
for (const [
|
|
114
|
+
for (const [agentName, definition] of Object.entries(config.agents.definitions)) {
|
|
115
115
|
for (const token of commandTokensToCheck(definition.cmd)) {
|
|
116
|
-
const hint =
|
|
116
|
+
const hint = agentCliHint(agentName, token);
|
|
117
117
|
if (!all.has(token) || all.get(token) === undefined) {
|
|
118
118
|
all.set(token, hint);
|
|
119
119
|
}
|
|
@@ -121,16 +121,16 @@ function gatherToolTargets(config) {
|
|
|
121
121
|
}
|
|
122
122
|
return [...all].map(([token, hint]) => (hint === undefined ? { token } : { token, hint }));
|
|
123
123
|
}
|
|
124
|
-
function
|
|
125
|
-
if (token !==
|
|
124
|
+
function agentCliHint(agentName, token) {
|
|
125
|
+
if (token !== agentName) {
|
|
126
126
|
return undefined;
|
|
127
127
|
}
|
|
128
|
-
if (!
|
|
128
|
+
if (!isBuiltInAgentName(agentName)) {
|
|
129
129
|
return undefined;
|
|
130
130
|
}
|
|
131
|
-
return `install ${token} or remove \`
|
|
131
|
+
return `install ${token} or remove \`agents.definitions.${agentName}\` from crew.config.ts`;
|
|
132
132
|
}
|
|
133
|
-
function
|
|
133
|
+
function isBuiltInAgentName(value) {
|
|
134
134
|
return value === "claude" || value === "codex";
|
|
135
135
|
}
|
|
136
136
|
function format(check) {
|
|
@@ -196,16 +196,16 @@ export async function doctor() {
|
|
|
196
196
|
const check = await checkCmd(token, required, required ? hint : "required for local runs");
|
|
197
197
|
checks.push(check);
|
|
198
198
|
}
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
199
|
+
const usageGatedAgents = gatedAgents(config);
|
|
200
|
+
if (usageGatedAgents.length > 0) {
|
|
201
201
|
const codexbarPath = await which("codexbar");
|
|
202
202
|
if (codexbarPath === undefined) {
|
|
203
|
-
const
|
|
203
|
+
const agentList = usageGatedAgents.map((name) => `\`${name}\``).join(", ");
|
|
204
204
|
checks.push({
|
|
205
205
|
name: "codexbar",
|
|
206
206
|
ok: false,
|
|
207
207
|
required: true,
|
|
208
|
-
hint: `required for usage gating on ${
|
|
208
|
+
hint: `required for usage gating on ${agentList} — install codexbar, or set \`agents.definitions.<name>.usage\` to disable gating`,
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
else {
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type ResolvedConfig } from "../lib/config.ts";
|
|
10
10
|
import { type GroundcrewIssue } from "../lib/taskSource.ts";
|
|
11
|
-
import type {
|
|
11
|
+
import type { UsageByAgent } from "../lib/usage.ts";
|
|
12
12
|
import type { WorkspaceProbe } from "../lib/workspaces.ts";
|
|
13
13
|
import type { WorktreeEntry } from "../lib/worktrees.ts";
|
|
14
|
-
type SkipReason = "blocked" | "blockers_paginated" | "agent_any_capacity" | "
|
|
14
|
+
type SkipReason = "blocked" | "blockers_paginated" | "agent_any_capacity" | "agent_exhausted" | "workspace_list_unavailable" | "workspace_missing";
|
|
15
15
|
export interface StartVerdict {
|
|
16
16
|
kind: "start";
|
|
17
17
|
issue: GroundcrewIssue;
|
|
18
18
|
recovery: boolean;
|
|
19
|
-
/** Set when the verdict resolved an `agent-any` label to a concrete
|
|
19
|
+
/** Set when the verdict resolved an `agent-any` label to a concrete agent. */
|
|
20
20
|
resolvedFromAny: boolean;
|
|
21
21
|
}
|
|
22
22
|
export interface SkipVerdict {
|
|
@@ -29,23 +29,23 @@ export interface SkipVerdict {
|
|
|
29
29
|
/** Set for `blocked` and `blockers_paginated`. */
|
|
30
30
|
blockers?: string[];
|
|
31
31
|
/**
|
|
32
|
-
* Set when the skip event should carry the resolved
|
|
33
|
-
* verdict knew which
|
|
34
|
-
* and `agent_any_capacity` where the
|
|
32
|
+
* Set when the skip event should carry the resolved agent (i.e. the
|
|
33
|
+
* verdict knew which agent would have run). Omitted for blocker skips
|
|
34
|
+
* and `agent_any_capacity` where the agent was either unresolved or
|
|
35
35
|
* irrelevant.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
agent?: string;
|
|
38
38
|
}
|
|
39
39
|
type Verdict = StartVerdict | SkipVerdict;
|
|
40
|
-
export type
|
|
40
|
+
export type AgentUsageExhaustion = {
|
|
41
41
|
kind: "session";
|
|
42
|
-
|
|
42
|
+
agent: string;
|
|
43
43
|
usedPercentage: number;
|
|
44
44
|
limitPercentage: number;
|
|
45
45
|
resetMinutes: number | null;
|
|
46
46
|
} | {
|
|
47
47
|
kind: "weekly";
|
|
48
|
-
|
|
48
|
+
agent: string;
|
|
49
49
|
usedPercentage: number;
|
|
50
50
|
allowedPercentage: number;
|
|
51
51
|
resetMinutes: number;
|
|
@@ -61,8 +61,8 @@ export interface ClassifyArguments {
|
|
|
61
61
|
unblocked: readonly GroundcrewIssue[];
|
|
62
62
|
worktreeEntries: readonly WorktreeEntry[];
|
|
63
63
|
workspaceProbe: WorkspaceProbe;
|
|
64
|
-
usage:
|
|
65
|
-
/**
|
|
64
|
+
usage: UsageByAgent;
|
|
65
|
+
/** Agents flagged over `sessionLimitPercentage`. */
|
|
66
66
|
exhausted: Set<string>;
|
|
67
67
|
/** Maximum number of `start` verdicts to produce. */
|
|
68
68
|
slots: number;
|
|
@@ -73,15 +73,15 @@ interface BlockerClassification {
|
|
|
73
73
|
skips: SkipVerdict[];
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Pick the configured
|
|
77
|
-
*
|
|
78
|
-
* Score is `usage[
|
|
79
|
-
* (maximum headroom), so when no usage data is available every
|
|
80
|
-
* ties at 0 and the default
|
|
76
|
+
* Pick the configured agent with the most available session capacity.
|
|
77
|
+
* Agents flagged exhausted (over `sessionLimitPercentage`) are excluded.
|
|
78
|
+
* Score is `usage[agent].session` with `null`/missing treated as 0
|
|
79
|
+
* (maximum headroom), so when no usage data is available every agent
|
|
80
|
+
* ties at 0 and the default agent wins the tiebreak — `agent-any` then
|
|
81
81
|
* falls back to the default predictably.
|
|
82
82
|
*/
|
|
83
|
-
export declare function
|
|
84
|
-
export declare function classifyUsageExhaustion(config: ResolvedConfig, usage:
|
|
83
|
+
export declare function pickBestAgent(config: ResolvedConfig, usage: UsageByAgent, exhausted: Set<string>): string | undefined;
|
|
84
|
+
export declare function classifyUsageExhaustion(config: ResolvedConfig, usage: UsageByAgent): AgentUsageExhaustion[];
|
|
85
85
|
/**
|
|
86
86
|
* Cheap pre-pass — partitions Todo into unblocked issues and blocker
|
|
87
87
|
* skip verdicts. Runs before the dispatcher fetches usage or probes the
|
|
@@ -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,EAAa,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAwC,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAClG,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;AAgCD;;;;;;;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;AA6CD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,eAAe,EAAE,GAAG,qBAAqB,CAYxF;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 { AGENT_ANY } from "../lib/config.js";
|
|
10
10
|
import { naturalIdFromCanonical } from "../lib/taskSource.js";
|
|
11
11
|
const PERCENT_FRACTION_DIVISOR = 100;
|
|
12
12
|
const DAYS_PER_WEEK = 7;
|
|
@@ -40,15 +40,15 @@ function blockerVerdictFor(issue) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* Pick the configured
|
|
44
|
-
*
|
|
45
|
-
* Score is `usage[
|
|
46
|
-
* (maximum headroom), so when no usage data is available every
|
|
47
|
-
* ties at 0 and the default
|
|
43
|
+
* Pick the configured agent with the most available session capacity.
|
|
44
|
+
* Agents flagged exhausted (over `sessionLimitPercentage`) are excluded.
|
|
45
|
+
* Score is `usage[agent].session` with `null`/missing treated as 0
|
|
46
|
+
* (maximum headroom), so when no usage data is available every agent
|
|
47
|
+
* ties at 0 and the default agent wins the tiebreak — `agent-any` then
|
|
48
48
|
* falls back to the default predictably.
|
|
49
49
|
*/
|
|
50
|
-
export function
|
|
51
|
-
const candidates = Object.keys(config.
|
|
50
|
+
export function pickBestAgent(config, usage, exhausted) {
|
|
51
|
+
const candidates = Object.keys(config.agents.definitions).filter((name) => !exhausted.has(name));
|
|
52
52
|
if (candidates.length === 0) {
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@ export function pickBestModel(config, usage, exhausted) {
|
|
|
57
57
|
if (candidate.score < best.score) {
|
|
58
58
|
return candidate;
|
|
59
59
|
}
|
|
60
|
-
if (candidate.score === best.score && candidate.name === config.
|
|
60
|
+
if (candidate.score === best.score && candidate.name === config.agents.default) {
|
|
61
61
|
return candidate;
|
|
62
62
|
}
|
|
63
63
|
return best;
|
|
@@ -72,11 +72,11 @@ function weeklyPacedBudgetPercentage(weekEndDuration) {
|
|
|
72
72
|
export function classifyUsageExhaustion(config, usage) {
|
|
73
73
|
const exhausted = [];
|
|
74
74
|
const sessionLimit = config.orchestrator.sessionLimitPercentage;
|
|
75
|
-
for (const [
|
|
75
|
+
for (const [agent, snapshot] of Object.entries(usage)) {
|
|
76
76
|
if (snapshot.session !== null && snapshot.session * PERCENT_FRACTION_DIVISOR > sessionLimit) {
|
|
77
77
|
exhausted.push({
|
|
78
78
|
kind: "session",
|
|
79
|
-
|
|
79
|
+
agent,
|
|
80
80
|
usedPercentage: snapshot.session * PERCENT_FRACTION_DIVISOR,
|
|
81
81
|
limitPercentage: sessionLimit,
|
|
82
82
|
resetMinutes: snapshot.sessionEndDuration,
|
|
@@ -93,7 +93,7 @@ export function classifyUsageExhaustion(config, usage) {
|
|
|
93
93
|
if (usedPercentage > allowedPercentage) {
|
|
94
94
|
exhausted.push({
|
|
95
95
|
kind: "weekly",
|
|
96
|
-
|
|
96
|
+
agent,
|
|
97
97
|
usedPercentage,
|
|
98
98
|
allowedPercentage,
|
|
99
99
|
resetMinutes: snapshot.weekEndDuration,
|
|
@@ -172,27 +172,27 @@ export function classifyEligibility(arguments_) {
|
|
|
172
172
|
}
|
|
173
173
|
let resolved = original;
|
|
174
174
|
let resolvedFromAny = false;
|
|
175
|
-
if (original.
|
|
176
|
-
const picked =
|
|
175
|
+
if (original.agent === AGENT_ANY) {
|
|
176
|
+
const picked = pickBestAgent(config, usage, exhausted);
|
|
177
177
|
if (picked === undefined) {
|
|
178
178
|
verdicts.push({
|
|
179
179
|
kind: "skip",
|
|
180
180
|
issue: original,
|
|
181
|
-
message: `Skipping ${original.id}: agent-any but no
|
|
181
|
+
message: `Skipping ${original.id}: agent-any but no agent has available capacity`,
|
|
182
182
|
eventReason: "agent_any_capacity",
|
|
183
183
|
});
|
|
184
184
|
continue;
|
|
185
185
|
}
|
|
186
|
-
resolved = { ...original,
|
|
186
|
+
resolved = { ...original, agent: picked };
|
|
187
187
|
resolvedFromAny = true;
|
|
188
188
|
}
|
|
189
|
-
if (exhausted.has(resolved.
|
|
189
|
+
if (exhausted.has(resolved.agent)) {
|
|
190
190
|
verdicts.push({
|
|
191
191
|
kind: "skip",
|
|
192
192
|
issue: resolved,
|
|
193
|
-
message: `Skipping ${resolved.id} (${resolved.
|
|
194
|
-
eventReason: "
|
|
195
|
-
|
|
193
|
+
message: `Skipping ${resolved.id} (${resolved.agent} session exhausted)`,
|
|
194
|
+
eventReason: "agent_exhausted",
|
|
195
|
+
agent: resolved.agent,
|
|
196
196
|
});
|
|
197
197
|
continue;
|
|
198
198
|
}
|
|
@@ -203,7 +203,7 @@ export function classifyEligibility(arguments_) {
|
|
|
203
203
|
dryRun,
|
|
204
204
|
});
|
|
205
205
|
if (recovery.kind === "skip") {
|
|
206
|
-
verdicts.push({ ...recovery,
|
|
206
|
+
verdicts.push({ ...recovery, agent: resolved.agent });
|
|
207
207
|
continue;
|
|
208
208
|
}
|
|
209
209
|
verdicts.push({
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* `cp` dance documented in the README.
|
|
6
6
|
*/
|
|
7
7
|
import { type LocalRunnerSetting } from "../lib/config.ts";
|
|
8
|
-
declare const
|
|
8
|
+
declare const INIT_AGENTS: readonly ["claude", "codex"];
|
|
9
9
|
type InitConfigScope = "global" | "local";
|
|
10
|
-
type
|
|
10
|
+
type InitAgent = (typeof INIT_AGENTS)[number];
|
|
11
11
|
interface InitConfigOptions {
|
|
12
12
|
/** Where to write the config. Defaults to "local" (cwd). */
|
|
13
13
|
scope?: InitConfigScope;
|
|
@@ -23,8 +23,8 @@ interface InitConfigOptions {
|
|
|
23
23
|
repositories?: string[];
|
|
24
24
|
/** Pre-fill local.runner in the generated config. */
|
|
25
25
|
runner?: LocalRunnerSetting;
|
|
26
|
-
/** Choose the single built-in
|
|
27
|
-
|
|
26
|
+
/** Choose the single built-in agent preset enabled by the generated config. */
|
|
27
|
+
agent?: InitAgent;
|
|
28
28
|
/** Override the source template path. */
|
|
29
29
|
examplePath?: string;
|
|
30
30
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -13,8 +13,8 @@ import { xdgConfigPath } from "../lib/xdg.js";
|
|
|
13
13
|
const CONFIG_FILE_NAME = "crew.config.ts";
|
|
14
14
|
const EXAMPLE_FILE_NAME = "crew.config.example.ts";
|
|
15
15
|
const DEFAULT_EXAMPLE_PROJECT_DIR = "~/dev/groundcrew";
|
|
16
|
-
const INIT_USAGE = "Usage: crew init [--global | --local] [--force] [--dry-run] [--project-dir <dir>] [--repo <owner/repo>]... [--runner <auto|safehouse|sdx|none>] [--
|
|
17
|
-
const
|
|
16
|
+
const INIT_USAGE = "Usage: crew init [--global | --local] [--force] [--dry-run] [--project-dir <dir>] [--repo <owner/repo>]... [--runner <auto|safehouse|sdx|none>] [--agent <claude|codex>]";
|
|
17
|
+
const INIT_AGENTS = ["claude", "codex"];
|
|
18
18
|
export function initConfig(options = {}) {
|
|
19
19
|
const scope = options.scope ?? "local";
|
|
20
20
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -51,7 +51,7 @@ function parseArguments(argv) {
|
|
|
51
51
|
let projectDir;
|
|
52
52
|
const repositories = [];
|
|
53
53
|
let runner;
|
|
54
|
-
let
|
|
54
|
+
let agent;
|
|
55
55
|
for (let index = 0; index < argv.length; index += 1) {
|
|
56
56
|
const argument = argv[index];
|
|
57
57
|
/* v8 ignore next 3 @preserve -- loop bounds keep argv[index] defined */
|
|
@@ -89,8 +89,8 @@ function parseArguments(argv) {
|
|
|
89
89
|
index += 1;
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
92
|
-
if (argument === "--
|
|
93
|
-
|
|
92
|
+
if (argument === "--agent") {
|
|
93
|
+
agent = parseAgent(readOptionValue(argv, index, argument));
|
|
94
94
|
index += 1;
|
|
95
95
|
continue;
|
|
96
96
|
}
|
|
@@ -108,8 +108,8 @@ function parseArguments(argv) {
|
|
|
108
108
|
if (runner !== undefined) {
|
|
109
109
|
parsed.runner = runner;
|
|
110
110
|
}
|
|
111
|
-
if (
|
|
112
|
-
parsed.
|
|
111
|
+
if (agent !== undefined) {
|
|
112
|
+
parsed.agent = agent;
|
|
113
113
|
}
|
|
114
114
|
return parsed;
|
|
115
115
|
}
|
|
@@ -137,16 +137,16 @@ function parseRunner(value) {
|
|
|
137
137
|
}
|
|
138
138
|
throw new Error(`crew init --runner must be one of ${LOCAL_RUNNER_SETTINGS.join(", ")}`);
|
|
139
139
|
}
|
|
140
|
-
function
|
|
141
|
-
if (
|
|
140
|
+
function parseAgent(value) {
|
|
141
|
+
if (isInitAgent(value)) {
|
|
142
142
|
return value;
|
|
143
143
|
}
|
|
144
|
-
throw new Error(`crew init --
|
|
144
|
+
throw new Error(`crew init --agent must be one of ${INIT_AGENTS.join(", ")}`);
|
|
145
145
|
}
|
|
146
146
|
function isLocalRunnerSetting(value) {
|
|
147
147
|
return value === "auto" || value === "safehouse" || value === "sdx" || value === "none";
|
|
148
148
|
}
|
|
149
|
-
function
|
|
149
|
+
function isInitAgent(value) {
|
|
150
150
|
return value === "claude" || value === "codex";
|
|
151
151
|
}
|
|
152
152
|
function tsString(value) {
|
|
@@ -163,15 +163,15 @@ function renderConfig(source, options) {
|
|
|
163
163
|
if (options.runner !== undefined) {
|
|
164
164
|
contents = replaceRequired(contents, ` // local: { runner: "auto" },`, ` local: { runner: ${tsString(options.runner)} },`, "--runner");
|
|
165
165
|
}
|
|
166
|
-
if (options.
|
|
167
|
-
contents = replaceRequired(contents, ` default: "claude",`, ` default: ${tsString(options.
|
|
168
|
-
contents = replaceRequired(contents, " claude: {},", ` ${options.
|
|
169
|
-
contents =
|
|
166
|
+
if (options.agent !== undefined) {
|
|
167
|
+
contents = replaceRequired(contents, ` default: "claude",`, ` default: ${tsString(options.agent)},`, "--agent");
|
|
168
|
+
contents = replaceRequired(contents, " claude: {},", ` ${options.agent}: {},`, "--agent");
|
|
169
|
+
contents = removeDuplicateAgentDefinitionLines(contents, options.agent);
|
|
170
170
|
}
|
|
171
171
|
return contents;
|
|
172
172
|
}
|
|
173
|
-
function
|
|
174
|
-
const linePattern = new RegExp(`^\\s*(?://\\s*)?${escapeRegExp(
|
|
173
|
+
function removeDuplicateAgentDefinitionLines(contents, agent) {
|
|
174
|
+
const linePattern = new RegExp(`^\\s*(?://\\s*)?${escapeRegExp(agent)}:\\s*\\{\\},\\s*$`);
|
|
175
175
|
let hasActiveEntry = false;
|
|
176
176
|
return contents
|
|
177
177
|
.split("\n")
|
|
@@ -36,7 +36,7 @@ function sourceFromState(state) {
|
|
|
36
36
|
return {
|
|
37
37
|
task: state.task,
|
|
38
38
|
repository: state.repository,
|
|
39
|
-
|
|
39
|
+
agent: state.agent,
|
|
40
40
|
worktreeDir: state.worktreeDir,
|
|
41
41
|
branchName: state.branchName,
|
|
42
42
|
workspaceName: state.workspaceName,
|
|
@@ -47,7 +47,7 @@ function sourceFromWorktree(config, task, entry) {
|
|
|
47
47
|
return {
|
|
48
48
|
task,
|
|
49
49
|
repository: entry.repository,
|
|
50
|
-
|
|
50
|
+
agent: config.agents.default,
|
|
51
51
|
worktreeDir: entry.dir,
|
|
52
52
|
branchName: entry.branchName,
|
|
53
53
|
workspaceName: task,
|
|
@@ -89,7 +89,7 @@ export async function interruptWorkspace(config, options) {
|
|
|
89
89
|
state: {
|
|
90
90
|
task,
|
|
91
91
|
repository: source.repository,
|
|
92
|
-
|
|
92
|
+
agent: source.agent,
|
|
93
93
|
worktreeDir: source.worktreeDir,
|
|
94
94
|
branchName: source.branchName,
|
|
95
95
|
workspaceName: source.workspaceName,
|
|
@@ -9,7 +9,7 @@ import { buildSources, sourcesFromConfig } from "../lib/buildSources.js";
|
|
|
9
9
|
import { loadConfigWithSource } from "../lib/config.js";
|
|
10
10
|
import { findPullRequestsForBranch } from "../lib/pullRequests.js";
|
|
11
11
|
import { RepositoryResolutionError } from "../lib/taskSource.js";
|
|
12
|
-
import {
|
|
12
|
+
import { getUsageByAgent } from "../lib/usage.js";
|
|
13
13
|
import { errorMessage, log, sleep, writeOutput } from "../lib/util.js";
|
|
14
14
|
import { worktrees } from "../lib/worktrees.js";
|
|
15
15
|
import { createCleaner } from "./cleaner.js";
|
|
@@ -55,7 +55,7 @@ class WatchLoopShutdownError extends Error {
|
|
|
55
55
|
}
|
|
56
56
|
async function fetchUsageOrEmpty(config, signal) {
|
|
57
57
|
try {
|
|
58
|
-
return await
|
|
58
|
+
return await getUsageByAgent(config, signal);
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
if (signal?.aborted === true) {
|