paseo-bm-plugin 0.0.0-placeholder.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/client/agent-tree.ts +308 -0
- package/client/answer-state.ts +62 -0
- package/client/bead-chips.tsx +147 -0
- package/client/beads-header-button.ts +108 -0
- package/client/beads-model.ts +581 -0
- package/client/beads-screen.tsx +516 -0
- package/client/beads-tab.tsx +58 -0
- package/client/chat-card.tsx +636 -0
- package/client/chat-cards.ts +1038 -0
- package/client/dashboard-actions.tsx +255 -0
- package/client/dashboard-model.ts +947 -0
- package/client/dashboard-view.ts +215 -0
- package/client/dashboard.tsx +318 -0
- package/client/launch-manager.ts +323 -0
- package/client/launcher.tsx +516 -0
- package/client/markdown-view.tsx +112 -0
- package/client/markdown.ts +145 -0
- package/client/settings.tsx +104 -0
- package/client/setup-model.ts +552 -0
- package/client/setup-screen.tsx +913 -0
- package/client/slot.ts +47 -0
- package/client/tree.tsx +204 -0
- package/client/ui.tsx +262 -0
- package/client/waiting-pills-model.ts +156 -0
- package/client/waiting-pills.tsx +201 -0
- package/index.client.tsx +232 -0
- package/index.server.ts +168 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/roles/manager.md +181 -0
- package/roles/reviewer.md +160 -0
- package/roles/worker.md +407 -0
- package/server/agent-labels.ts +194 -0
- package/server/agent-role.ts +102 -0
- package/server/answer-marks.ts +120 -0
- package/server/bead-actions.ts +88 -0
- package/server/bead-work.ts +80 -0
- package/server/beads-store.ts +342 -0
- package/server/bm-report.ts +433 -0
- package/server/chat-peers.ts +65 -0
- package/server/chat-rpc.ts +122 -0
- package/server/chat-waiting.ts +182 -0
- package/server/collector.ts +629 -0
- package/server/config-writer.ts +222 -0
- package/server/cost.ts +88 -0
- package/server/dashboard-rpc.ts +662 -0
- package/server/fallback-detect.ts +183 -0
- package/server/fallback-handover.ts +365 -0
- package/server/fallback-manager.ts +170 -0
- package/server/fallback-reviewer.ts +198 -0
- package/server/fallback-rpc.ts +306 -0
- package/server/fallback-settings.ts +322 -0
- package/server/fallback-state.ts +518 -0
- package/server/fallback-switch.ts +191 -0
- package/server/fallback-wait.ts +188 -0
- package/server/format-check.ts +352 -0
- package/server/install-home.ts +187 -0
- package/server/live-timeline.ts +129 -0
- package/server/manager-instructions.ts +9 -0
- package/server/manager.ts +647 -0
- package/server/model-costs.ts +238 -0
- package/server/notice-queue.ts +315 -0
- package/server/notices.ts +81 -0
- package/server/paseo-cli.ts +115 -0
- package/server/provider-id.ts +12 -0
- package/server/review-budget.ts +208 -0
- package/server/reviewer-instructions.ts +9 -0
- package/server/role-choices.ts +161 -0
- package/server/role-extras.ts +270 -0
- package/server/role-hook.ts +347 -0
- package/server/role-mode.ts +397 -0
- package/server/role-settings-rpc.ts +325 -0
- package/server/roles.ts +96 -0
- package/server/settings-notices.ts +112 -0
- package/server/setup-rpc.ts +70 -0
- package/server/setup-skills.ts +121 -0
- package/server/setup-tools.ts +162 -0
- package/server/shell.ts +68 -0
- package/server/stop-propagation.ts +365 -0
- package/server/tools-check.ts +118 -0
- package/server/trace-store.ts +1137 -0
- package/server/traces.ts +1356 -0
- package/server/worker-instructions.ts +9 -0
- package/server/workflow-steps.ts +422 -0
- package/shared/bead-ids.ts +25 -0
- package/shared/bm-fallback.ts +91 -0
- package/shared/bm-format.ts +424 -0
- package/shared/bm-questions.ts +213 -0
- package/shared/bm-report.ts +433 -0
- package/shared/contracts.ts +1371 -0
- package/shared/fallback-patterns.ts +201 -0
- package/shared/fallback.ts +46 -0
- package/shared/new-request.ts +20 -0
- package/shared/order.ts +22 -0
- package/shared/prices.ts +65 -0
- package/shared/settings.ts +57 -0
- package/shared/sole-worker.ts +20 -0
- package/shared/version.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the agent skills the roles rely on are installed, as the Paseo
|
|
3
|
+
* daemon sees them (delta 20260916-setup-screen §3.3). Read-only: the plugin
|
|
4
|
+
* never installs or removes a skill.
|
|
5
|
+
*
|
|
6
|
+
* Same lists and rules as the CLI (`src/skills/detect.ts`) and `manager.md`:
|
|
7
|
+
* Claude Code counts only its own directory; Codex counts the shared
|
|
8
|
+
* `~/.agents/skills` or its own. The plugin payload cannot import `src/`, so
|
|
9
|
+
* the two short lists are repeated here and pinned by a test.
|
|
10
|
+
*
|
|
11
|
+
* Pi and OpenCode (delta 20260921 §4.2.6, F8) count only their own
|
|
12
|
+
* directory, `~/.pi/agent/skills` and `~/.config/opencode/skill` (singular),
|
|
13
|
+
* checked the same way as Claude's. No environment variable moves them in this
|
|
14
|
+
* release, and the CLI (`--skills-agents`, `doctor`) does not look at them.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
export const REQUIRED_SKILLS = [
|
|
21
|
+
"feature-workflow",
|
|
22
|
+
"reviewing-plan",
|
|
23
|
+
"converting-plan-to-beads",
|
|
24
|
+
"polishing-beads",
|
|
25
|
+
"implementing-beads",
|
|
26
|
+
] as const;
|
|
27
|
+
export const OPTIONAL_SKILLS = ["architecture-premise-audit", "authoring-workspace-protocol"] as const;
|
|
28
|
+
|
|
29
|
+
export type SkillState = "ok" | "missing" | "broken";
|
|
30
|
+
|
|
31
|
+
export interface SkillRow {
|
|
32
|
+
name: string;
|
|
33
|
+
required: boolean;
|
|
34
|
+
claude: SkillState;
|
|
35
|
+
codex: SkillState;
|
|
36
|
+
pi: SkillState;
|
|
37
|
+
opencode: SkillState;
|
|
38
|
+
/** Why a copy is broken, when one is. */
|
|
39
|
+
problem: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SkillsStatus {
|
|
43
|
+
checkedAt: string;
|
|
44
|
+
dirs: { shared: string; claude: string; codex: string; pi: string; opencode: string };
|
|
45
|
+
skills: SkillRow[];
|
|
46
|
+
/** Required skills not usable, per agent. */
|
|
47
|
+
missingRequired: { claude: number; codex: number; pi: number; opencode: number };
|
|
48
|
+
installCommand: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SkillDeps {
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
homedir?: () => string;
|
|
54
|
+
readFile?: (path: string) => string;
|
|
55
|
+
now?: () => Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function skillDirs(deps: SkillDeps = {}): SkillsStatus["dirs"] {
|
|
59
|
+
const env = deps.env ?? process.env;
|
|
60
|
+
const home = (deps.homedir ?? homedir)();
|
|
61
|
+
return {
|
|
62
|
+
shared: join(home, ".agents", "skills"),
|
|
63
|
+
claude: join(env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"), "skills"),
|
|
64
|
+
codex: join(env.CODEX_HOME ?? join(home, ".codex"), "skills"),
|
|
65
|
+
pi: join(home, ".pi", "agent", "skills"),
|
|
66
|
+
opencode: join(home, ".config", "opencode", "skill"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One copy of a skill: `ok`, `missing`, or `broken` with the reason. */
|
|
71
|
+
export function checkSkillAt(dir: string, name: string, read: (path: string) => string): { state: SkillState; problem: string | null } {
|
|
72
|
+
let text: string;
|
|
73
|
+
try {
|
|
74
|
+
text = read(join(dir, name, "SKILL.md"));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
77
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { state: "missing", problem: null };
|
|
78
|
+
return { state: "broken", problem: `${join(dir, name, "SKILL.md")} cannot be read (${code ?? "error"})` };
|
|
79
|
+
}
|
|
80
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
81
|
+
const declared = frontmatter === undefined ? null : /^name:\s*["']?([^"'\n]+?)["']?\s*$/m.exec(frontmatter)?.[1] ?? null;
|
|
82
|
+
if (declared === null) return { state: "broken", problem: `${name}/SKILL.md has no \`name:\` in its frontmatter` };
|
|
83
|
+
if (declared !== name) return { state: "broken", problem: `${name}/SKILL.md declares name \`${declared}\`` };
|
|
84
|
+
return { state: "ok", problem: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function best(...states: Array<{ state: SkillState; problem: string | null }>): { state: SkillState; problem: string | null } {
|
|
88
|
+
return states.find((entry) => entry.state === "ok") ?? states.find((entry) => entry.state === "broken") ?? states[0]!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function skillsStatus(deps: SkillDeps = {}): SkillsStatus {
|
|
92
|
+
const read = deps.readFile ?? ((path: string) => readFileSync(path, "utf8"));
|
|
93
|
+
const dirs = skillDirs(deps);
|
|
94
|
+
const rows: SkillRow[] = [...REQUIRED_SKILLS, ...OPTIONAL_SKILLS].map((name) => {
|
|
95
|
+
const claude = checkSkillAt(dirs.claude, name, read);
|
|
96
|
+
const codex = best(checkSkillAt(dirs.shared, name, read), checkSkillAt(dirs.codex, name, read));
|
|
97
|
+
const pi = checkSkillAt(dirs.pi, name, read);
|
|
98
|
+
const opencode = checkSkillAt(dirs.opencode, name, read);
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
required: (REQUIRED_SKILLS as readonly string[]).includes(name),
|
|
102
|
+
claude: claude.state,
|
|
103
|
+
codex: codex.state,
|
|
104
|
+
pi: pi.state,
|
|
105
|
+
opencode: opencode.state,
|
|
106
|
+
problem: claude.problem ?? codex.problem ?? pi.problem ?? opencode.problem,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
checkedAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
111
|
+
dirs,
|
|
112
|
+
skills: rows,
|
|
113
|
+
missingRequired: {
|
|
114
|
+
claude: rows.filter((row) => row.required && row.claude !== "ok").length,
|
|
115
|
+
codex: rows.filter((row) => row.required && row.codex !== "ok").length,
|
|
116
|
+
pi: rows.filter((row) => row.required && row.pi !== "ok").length,
|
|
117
|
+
opencode: rows.filter((row) => row.required && row.opencode !== "ok").length,
|
|
118
|
+
},
|
|
119
|
+
installCommand: `npx -y skills add cuongntr/agent-skills -g -a claude-code codex -s ${REQUIRED_SKILLS.join(" ")} -y`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The beads tools the Worker depends on — `br` and `bv` — and `bd` for
|
|
3
|
+
* information (delta 20260916-setup-screen §3.4).
|
|
4
|
+
*
|
|
5
|
+
* Finding and version-checking is read-only. Installing runs only the
|
|
6
|
+
* documented command for a tool that is missing, only when the user pressed
|
|
7
|
+
* Install and confirmed it on the Setup screen.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { delimiter, join } from "node:path";
|
|
13
|
+
import { DashboardError } from "../shared/contracts";
|
|
14
|
+
|
|
15
|
+
export type ToolId = "br" | "bv" | "bd";
|
|
16
|
+
|
|
17
|
+
export interface ToolInfo {
|
|
18
|
+
id: ToolId;
|
|
19
|
+
name: string;
|
|
20
|
+
purpose: string;
|
|
21
|
+
required: boolean;
|
|
22
|
+
path: string | null;
|
|
23
|
+
version: string | null;
|
|
24
|
+
latestKnown: string | null;
|
|
25
|
+
installCommand: string | null;
|
|
26
|
+
updateCommand: string | null;
|
|
27
|
+
homepage: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Newest releases seen on the projects' pages, and when (not fetched at runtime). */
|
|
31
|
+
export const LATEST_KNOWN = { br: "0.6.0", bv: "v0.25.0", checkedOn: "2026-09-16" } as const;
|
|
32
|
+
|
|
33
|
+
const BR_SCRIPT =
|
|
34
|
+
"curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/beads_rust/main/install.sh | bash -s -- --skip-skills";
|
|
35
|
+
/** Pinned to the commit the beads_viewer README asks installers to use. */
|
|
36
|
+
const BV_SCRIPT =
|
|
37
|
+
"curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/a43b8e85a39664381566abdfd85dc8fcbfdcb773/install.sh | bash";
|
|
38
|
+
|
|
39
|
+
const DESCRIPTIONS: Record<ToolId, { name: string; purpose: string; required: boolean; homepage: string }> = {
|
|
40
|
+
br: {
|
|
41
|
+
name: "br — beads_rust",
|
|
42
|
+
purpose: "The issue tracker the Worker uses to create, update and close beads.",
|
|
43
|
+
required: true,
|
|
44
|
+
homepage: "https://github.com/Dicklesworthstone/beads_rust",
|
|
45
|
+
},
|
|
46
|
+
bv: {
|
|
47
|
+
name: "bv — beads_viewer",
|
|
48
|
+
purpose: "Triage and dependency views over the same beads (agents use its --robot-* modes).",
|
|
49
|
+
required: true,
|
|
50
|
+
homepage: "https://github.com/Dicklesworthstone/beads_viewer",
|
|
51
|
+
},
|
|
52
|
+
bd: {
|
|
53
|
+
name: "bd — beads (Go)",
|
|
54
|
+
purpose: "The original beads CLI. Optional when br is installed.",
|
|
55
|
+
required: false,
|
|
56
|
+
homepage: "https://github.com/steveyegge/beads",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export interface ToolDeps {
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
homedir?: () => string;
|
|
63
|
+
isExecutable?: (path: string) => boolean;
|
|
64
|
+
run?: (file: string, args: string[], timeoutMs: number) => Promise<{ code: number; output: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isExecutable(path: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
if (!statSync(path).isFile()) return false;
|
|
70
|
+
accessSync(path, constants.X_OK);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function run(file: string, args: string[], timeoutMs: number): Promise<{ code: number; output: string }> {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
80
|
+
const code = error === null ? 0 : typeof (error as { code?: unknown }).code === "number" ? (error as { code: number }).code : 1;
|
|
81
|
+
resolve({ code, output: `${stdout}${stderr}` });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The daemon's PATH, then the places the documented installers use. */
|
|
87
|
+
export function searchDirs(deps: ToolDeps = {}): string[] {
|
|
88
|
+
const env = deps.env ?? process.env;
|
|
89
|
+
const home = (deps.homedir ?? homedir)();
|
|
90
|
+
const fromPath = (env.PATH ?? "").split(delimiter).filter((dir) => dir !== "");
|
|
91
|
+
const extra = [join(home, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin", join(home, ".cargo", "bin"), join(home, "go", "bin")];
|
|
92
|
+
return [...new Set([...fromPath, ...extra])];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function findTool(name: string, deps: ToolDeps = {}): string | null {
|
|
96
|
+
const check = deps.isExecutable ?? isExecutable;
|
|
97
|
+
for (const dir of searchDirs(deps)) {
|
|
98
|
+
const candidate = join(dir, name);
|
|
99
|
+
if (check(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** First line of `--version` output, without the tool name. */
|
|
105
|
+
export function versionOf(output: string): string | null {
|
|
106
|
+
const line = output.split("\n").map((part) => part.trim()).find((part) => part !== "");
|
|
107
|
+
if (line === undefined) return null;
|
|
108
|
+
return line.replace(/^(?:br|bv|bd)\s+(?:version\s+)?/i, "").trim() || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function commandsFor(id: ToolId, brewPath: string | null): { install: string | null; update: string | null } {
|
|
112
|
+
if (id === "bd") return { install: null, update: null };
|
|
113
|
+
if (brewPath !== null) {
|
|
114
|
+
return { install: `brew install dicklesworthstone/tap/${id}`, update: `brew upgrade dicklesworthstone/tap/${id}` };
|
|
115
|
+
}
|
|
116
|
+
const script = id === "br" ? BR_SCRIPT : BV_SCRIPT;
|
|
117
|
+
return { install: script, update: script };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function toolsStatus(deps: ToolDeps = {}): Promise<ToolInfo[]> {
|
|
121
|
+
const exec = deps.run ?? run;
|
|
122
|
+
const brew = findTool("brew", deps);
|
|
123
|
+
return Promise.all(
|
|
124
|
+
(["br", "bv", "bd"] as const).map(async (id): Promise<ToolInfo> => {
|
|
125
|
+
const path = findTool(id, deps);
|
|
126
|
+
// Only ever `--version`: a bare `bv` opens an interactive TUI.
|
|
127
|
+
const version = path === null ? null : versionOf((await exec(path, ["--version"], 5000)).output);
|
|
128
|
+
const commands = commandsFor(id, brew);
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
...DESCRIPTIONS[id],
|
|
132
|
+
path,
|
|
133
|
+
version,
|
|
134
|
+
latestKnown: id === "bd" ? null : LATEST_KNOWN[id],
|
|
135
|
+
installCommand: commands.install,
|
|
136
|
+
updateCommand: commands.update,
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const INSTALL_TIMEOUT_MS = 300_000;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Runs the documented install command for a missing tool. The user pressed
|
|
146
|
+
* Install and confirmed the exact command before this is called.
|
|
147
|
+
*/
|
|
148
|
+
export async function installTool(id: "br" | "bv", deps: ToolDeps = {}): Promise<{ command: string; code: number; tail: string[] }> {
|
|
149
|
+
if (findTool(id, deps) !== null) {
|
|
150
|
+
throw new DashboardError("E_TOOL_PRESENT", `${id} is already installed; update it with the command shown on the Setup screen`);
|
|
151
|
+
}
|
|
152
|
+
const command = commandsFor(id, findTool("brew", deps)).install!;
|
|
153
|
+
const exec = deps.run ?? run;
|
|
154
|
+
const env = deps.env ?? process.env;
|
|
155
|
+
// A login shell, so the installer sees the user's usual PATH (brew, curl).
|
|
156
|
+
const result = await exec(env.SHELL?.endsWith("zsh") ? "/bin/zsh" : "/bin/bash", ["-lc", command], INSTALL_TIMEOUT_MS);
|
|
157
|
+
const tail = result.output.split("\n").filter((line) => line.trim() !== "").slice(-40);
|
|
158
|
+
if (result.code !== 0) {
|
|
159
|
+
throw new DashboardError("E_TOOL_INSTALL_FAILED", `\`${command}\` exited with ${result.code}: ${tail.slice(-3).join(" | ")}`);
|
|
160
|
+
}
|
|
161
|
+
return { command, code: result.code, tail };
|
|
162
|
+
}
|
package/server/shell.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the shell commands agents ran. One place, because two modules asked
|
|
3
|
+
* the same question ("did this `br` command act on beads?") with two different
|
|
4
|
+
* rules and could disagree about the same line.
|
|
5
|
+
*
|
|
6
|
+
* All three rules below were found on real agent output (WP-214 acceptance).
|
|
7
|
+
*/
|
|
8
|
+
import { BEAD_ID } from "./bm-report";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Splits a shell line into the commands it runs, with quoted arguments blanked:
|
|
12
|
+
* `grep -iE 'makefile|pytest'` contains pipes and a runner's name inside the
|
|
13
|
+
* quotes, and must not look like two commands, one of them a test run.
|
|
14
|
+
*/
|
|
15
|
+
export function commandSegments(line: string): string[] {
|
|
16
|
+
return line
|
|
17
|
+
.replace(/'[^']*'|"[^"]*"/g, " ARG ")
|
|
18
|
+
.split(/&&|\|\||[;\n|]/)
|
|
19
|
+
.map((segment) => segment.trim().replace(/^[(\s]+/, ""))
|
|
20
|
+
.filter((segment) => segment !== "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type BrVerb = "create" | "update" | "close";
|
|
24
|
+
|
|
25
|
+
export interface BrAction {
|
|
26
|
+
verb: BrVerb;
|
|
27
|
+
ids: string[];
|
|
28
|
+
/** An update that moves its beads to `in_progress` (`--status in_progress` or `--claim`). */
|
|
29
|
+
startsWork: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startsWork(tokens: readonly string[]): boolean {
|
|
33
|
+
return tokens.some(
|
|
34
|
+
(token, index) =>
|
|
35
|
+
token === "--claim" ||
|
|
36
|
+
token === "--status=in_progress" ||
|
|
37
|
+
((token === "--status" || token === "-s") && tokens[index + 1] === "in_progress"),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `br create | update | close` commands in a shell line that actually act.
|
|
43
|
+
*
|
|
44
|
+
* - A `--help`, `-h` or `--dry-run` command only looked (a Worker ran
|
|
45
|
+
* `br create --help` to orient itself, and the beads step read as done).
|
|
46
|
+
* - Bead ids are positional arguments only, read before the first flag.
|
|
47
|
+
* Scanning the whole line read `-l "feature:format-date"` and the prose of
|
|
48
|
+
* `-r "… a single-line docstring …"` as ids: one request reported 6 closed
|
|
49
|
+
* beads instead of 1.
|
|
50
|
+
* - `br create` names no id at all: `br` mints it.
|
|
51
|
+
*/
|
|
52
|
+
export function brActions(line: string): BrAction[] {
|
|
53
|
+
const out: BrAction[] = [];
|
|
54
|
+
for (const segment of commandSegments(line)) {
|
|
55
|
+
const match = /^br\s+(create|update|close)\b(.*)$/i.exec(segment);
|
|
56
|
+
if (match === null) continue;
|
|
57
|
+
const tokens = match[2]!.trim().split(/\s+/).filter((token) => token !== "");
|
|
58
|
+
if (tokens.some((token) => token === "--help" || token === "-h" || token === "--dry-run")) continue;
|
|
59
|
+
const ids: string[] = [];
|
|
60
|
+
for (const token of tokens) {
|
|
61
|
+
if (token.startsWith("-")) break;
|
|
62
|
+
if (BEAD_ID.test(token)) ids.push(token);
|
|
63
|
+
}
|
|
64
|
+
const verb = match[1]!.toLowerCase() as BrVerb;
|
|
65
|
+
out.push({ verb, ids, startsWork: verb === "update" && startsWork(tokens) });
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|