dotbrain 0.3.4__py3-none-any.whl
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.
- dotbrain/__init__.py +7 -0
- dotbrain/_cli_reference.py +115 -0
- dotbrain/adopter_repos.py +573 -0
- dotbrain/beads.py +511 -0
- dotbrain/bootstrap.py +199 -0
- dotbrain/brainspaces.py +238 -0
- dotbrain/cli.py +660 -0
- dotbrain/config.py +428 -0
- dotbrain/doctor.py +279 -0
- dotbrain/hooks.py +84 -0
- dotbrain/migrate.py +304 -0
- dotbrain/paths.py +139 -0
- dotbrain/resource_loader.py +47 -0
- dotbrain/resources/__init__.py +1 -0
- dotbrain/resources/agents/claude/implementer.md +47 -0
- dotbrain/resources/agents/claude/investigator.md +35 -0
- dotbrain/resources/agents/claude/reviewer.md +38 -0
- dotbrain/resources/agents/claude/verifier.md +35 -0
- dotbrain/resources/agents/codex/implementer.toml +26 -0
- dotbrain/resources/agents/codex/investigator.toml +21 -0
- dotbrain/resources/agents/codex/reviewer.toml +27 -0
- dotbrain/resources/agents/codex/verifier.toml +21 -0
- dotbrain/resources/config.yaml +20 -0
- dotbrain/resources/core.yaml +18 -0
- dotbrain/resources/templates/brain/AGENTS.md +9 -0
- dotbrain/resources/templates/brain/DOTBRAIN.md +105 -0
- dotbrain/resources/templates/brain/adr/README.md +8 -0
- dotbrain/resources/templates/brain/designs/README.md +28 -0
- dotbrain/resources/templates/brain/docs/README.md +9 -0
- dotbrain/resources/templates/brain/project.yaml +27 -0
- dotbrain/resources/templates/gitignore +17 -0
- dotbrain/skills.py +264 -0
- dotbrain/subagents.py +252 -0
- dotbrain/updater.py +105 -0
- dotbrain/workflows.py +529 -0
- dotbrain-0.3.4.dist-info/METADATA +21 -0
- dotbrain-0.3.4.dist-info/RECORD +40 -0
- dotbrain-0.3.4.dist-info/WHEEL +4 -0
- dotbrain-0.3.4.dist-info/entry_points.txt +2 -0
- dotbrain-0.3.4.dist-info/licenses/LICENSE +21 -0
dotbrain/workflows.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""User-facing workflows that compose the concept modules (stage 5).
|
|
2
|
+
|
|
3
|
+
These are the bodies behind ``dotbrain wire``, ``wire --all``, ``unwire``, and ``unwire --all``:
|
|
4
|
+
cross-concept orchestration that stitches together ``adopter_repos`` (repo links),
|
|
5
|
+
``brainspaces`` (Brain/workspace preparation, offboarding), ``beads`` (tracker init), ``skills``
|
|
6
|
+
(skill manifest). ``cli.py`` stays a thin Typer parsing/rendering
|
|
7
|
+
layer over these.
|
|
8
|
+
|
|
9
|
+
The subprocess seams take an injected ``run`` callable so tests record argv instead of invoking
|
|
10
|
+
``bd``/``git``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from dotbrain import adopter_repos, beads, bootstrap, config, brainspaces, paths, skills, subagents
|
|
21
|
+
from dotbrain.adopter_repos import UnwireResult, repo_for_brainspace, unwire_repo
|
|
22
|
+
from dotbrain.beads import BootstrapResult
|
|
23
|
+
from dotbrain.brainspaces import offboard_brainspace
|
|
24
|
+
|
|
25
|
+
# A subprocess seam: same shape as ``subprocess.run`` but easy to fake in tests.
|
|
26
|
+
Runner = Callable[..., "subprocess.CompletedProcess[str]"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _default_run(
|
|
30
|
+
argv: Sequence[str], *, cwd: Path | None = None, env: dict | None = None, check: bool = True
|
|
31
|
+
) -> "subprocess.CompletedProcess[str]":
|
|
32
|
+
# stdin=DEVNULL is load-bearing: bd auto-enables non-interactive mode on a non-TTY stdin,
|
|
33
|
+
# so destructive steps (e.g. bd init --reinit-local) skip their confirmation prompt instead
|
|
34
|
+
# of blocking forever on terminal input while capture_output swallows the prompt text.
|
|
35
|
+
return subprocess.run(
|
|
36
|
+
list(argv), cwd=cwd, env=env, check=check,
|
|
37
|
+
capture_output=True, encoding="utf-8", stdin=subprocess.DEVNULL,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class WireResult:
|
|
43
|
+
brainspace: Path
|
|
44
|
+
repo: Path | None = None
|
|
45
|
+
project: str = ""
|
|
46
|
+
logs: list[str] = field(default_factory=list)
|
|
47
|
+
warnings: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class RefreshResult:
|
|
52
|
+
logs: list[str] = field(default_factory=list)
|
|
53
|
+
warnings: list[str] = field(default_factory=list)
|
|
54
|
+
refreshed: list[str] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def project_workspace_dirs(
|
|
58
|
+
brainspace: Path,
|
|
59
|
+
repo: Path | None,
|
|
60
|
+
workspaces: Sequence[str],
|
|
61
|
+
run: Runner = _default_run,
|
|
62
|
+
) -> tuple[dict[str, Path], list[str]]:
|
|
63
|
+
"""Resolve runtime workspace directories, materializing them in the adopter repo."""
|
|
64
|
+
directories: dict[str, Path] = {}
|
|
65
|
+
warnings: list[str] = []
|
|
66
|
+
for workspace in workspaces:
|
|
67
|
+
if repo is not None:
|
|
68
|
+
warning = adopter_repos.materialize_workspace(repo, brainspace, workspace, run)
|
|
69
|
+
if warning:
|
|
70
|
+
warnings.append(warning)
|
|
71
|
+
continue
|
|
72
|
+
directories[workspace] = repo / workspace
|
|
73
|
+
else:
|
|
74
|
+
directories[workspace] = brainspace / workspace
|
|
75
|
+
return directories, warnings
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reconcile_project_asset_excludes(
|
|
79
|
+
repo: Path | None,
|
|
80
|
+
*results: skills.LinkResult,
|
|
81
|
+
run: Runner = _default_run,
|
|
82
|
+
) -> None:
|
|
83
|
+
if repo is None:
|
|
84
|
+
return
|
|
85
|
+
prefixes = (".claude/", ".codex/")
|
|
86
|
+
linked = [entry for result in results for entry in result.linked if entry.startswith(prefixes)]
|
|
87
|
+
pruned = [entry for result in results for entry in result.pruned if entry.startswith(prefixes)]
|
|
88
|
+
adopter_repos.reconcile_link_excludes(repo, linked=linked, pruned=pruned, run=run)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _link_project_assets(
|
|
92
|
+
dotbrain_home: Path,
|
|
93
|
+
brainspace: Path,
|
|
94
|
+
repo: Path | None,
|
|
95
|
+
workspaces: Sequence[str],
|
|
96
|
+
run: Runner = _default_run,
|
|
97
|
+
) -> tuple[skills.LinkResult, skills.LinkResult, list[str]]:
|
|
98
|
+
workspace_dirs, warnings = project_workspace_dirs(brainspace, repo, workspaces, run)
|
|
99
|
+
active_workspaces = tuple(workspace_dirs)
|
|
100
|
+
skill_result = skills.link_project(
|
|
101
|
+
dotbrain_home,
|
|
102
|
+
brainspace,
|
|
103
|
+
active_workspaces,
|
|
104
|
+
skills.project_link_set(config.load_project_skills(dotbrain_home, brainspace.name)),
|
|
105
|
+
workspace_dirs=workspace_dirs,
|
|
106
|
+
)
|
|
107
|
+
subagent_result = subagents.link_project_subagents(
|
|
108
|
+
dotbrain_home,
|
|
109
|
+
brainspace,
|
|
110
|
+
active_workspaces,
|
|
111
|
+
subagents.project_link_set(config.load_project_subagents(dotbrain_home, brainspace.name)),
|
|
112
|
+
workspace_dirs=workspace_dirs,
|
|
113
|
+
)
|
|
114
|
+
_reconcile_project_asset_excludes(repo, skill_result, subagent_result, run=run)
|
|
115
|
+
return skill_result, subagent_result, warnings
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --------------------------------------------------------------------------- wire one
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def wire_project(
|
|
122
|
+
*,
|
|
123
|
+
dotbrain_home: Path,
|
|
124
|
+
repo: Path | None = None,
|
|
125
|
+
project: str | None = None,
|
|
126
|
+
no_repo: bool = False,
|
|
127
|
+
run_beads: bool = True,
|
|
128
|
+
remote: str = "",
|
|
129
|
+
server_host: str = "",
|
|
130
|
+
server_port: str = "3307",
|
|
131
|
+
server_user: str = "beads",
|
|
132
|
+
database: str = "",
|
|
133
|
+
home: Path | None = None,
|
|
134
|
+
run: Runner = _default_run,
|
|
135
|
+
) -> WireResult:
|
|
136
|
+
"""Create/repair a Brainspace and wire an adopter repo. Mirrors wire-project.sh's main()."""
|
|
137
|
+
dotbrain_home = Path(dotbrain_home).resolve()
|
|
138
|
+
bootstrap.ensure_root_gitignore(dotbrain_home)
|
|
139
|
+
if not (dotbrain_home / ".git").exists():
|
|
140
|
+
raise RuntimeError(f"{dotbrain_home} is not a dotbrain git checkout")
|
|
141
|
+
|
|
142
|
+
resolved_repo: Path | None
|
|
143
|
+
if no_repo:
|
|
144
|
+
if not project:
|
|
145
|
+
raise ValueError("--no-repo requires --name <project>")
|
|
146
|
+
resolved_repo = None
|
|
147
|
+
else:
|
|
148
|
+
resolved_repo = adopter_repos.repo_root(repo, run)
|
|
149
|
+
project = project or resolved_repo.name
|
|
150
|
+
|
|
151
|
+
if resolved_repo is not None:
|
|
152
|
+
adopter_repos.ensure_not_wired_to_foreign_dotbrain(resolved_repo, dotbrain_home)
|
|
153
|
+
|
|
154
|
+
brainspace = paths.brainspace(dotbrain_home, project)
|
|
155
|
+
rel = paths.data_dir(dotbrain_home).name
|
|
156
|
+
archive = paths.data_dir(dotbrain_home) / ".archive" / project
|
|
157
|
+
unarchived = False
|
|
158
|
+
if archive.is_dir() and not brainspace.is_dir():
|
|
159
|
+
run(["git", "-C", str(dotbrain_home), "mv",
|
|
160
|
+
f"{rel}/.archive/{project}", f"{rel}/{project}"])
|
|
161
|
+
unarchived = True
|
|
162
|
+
brainspace.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
if no_repo:
|
|
164
|
+
(brainspace / ".repo").write_text(
|
|
165
|
+
"(brain-only)\n", encoding="utf-8", newline="\n"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
assert resolved_repo is not None
|
|
169
|
+
(brainspace / ".repo").write_text(
|
|
170
|
+
f"{adopter_repos.abbrev_home(resolved_repo, home)}\n",
|
|
171
|
+
encoding="utf-8",
|
|
172
|
+
newline="\n",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
result = WireResult(brainspace=brainspace, repo=resolved_repo, project=project)
|
|
176
|
+
if unarchived:
|
|
177
|
+
result.logs.append(f"unarchived {project} from {rel}/.archive/{project}")
|
|
178
|
+
|
|
179
|
+
brainspaces.seed_brain(brainspace, dotbrain_home)
|
|
180
|
+
config.migrate_legacy_skill_manifest(dotbrain_home, project)
|
|
181
|
+
active_workspaces = brainspaces.active_agent_workspaces(brainspace, dotbrain_home)
|
|
182
|
+
result.warnings += brainspaces.seed_agent_workspaces(brainspace, dotbrain_home, home)
|
|
183
|
+
skill_result, subagent_result, workspace_warnings = _link_project_assets(
|
|
184
|
+
dotbrain_home, brainspace, resolved_repo, active_workspaces, run
|
|
185
|
+
)
|
|
186
|
+
result.warnings += workspace_warnings + skill_result.warnings + subagent_result.warnings
|
|
187
|
+
result.logs += [f"pruned skill {entry}" for entry in skill_result.pruned]
|
|
188
|
+
result.logs += [f"pruned subagent {entry}" for entry in subagent_result.pruned]
|
|
189
|
+
beads_log = beads.init_beads(
|
|
190
|
+
brainspace, project, dotbrain_home,
|
|
191
|
+
run_beads=run_beads, remote=remote,
|
|
192
|
+
server_host=server_host, server_port=server_port,
|
|
193
|
+
server_user=server_user, database=database, run=run,
|
|
194
|
+
)
|
|
195
|
+
if beads_log:
|
|
196
|
+
result.logs.append(beads_log)
|
|
197
|
+
if run_beads:
|
|
198
|
+
# a deviating backend must be durable in project.yaml or hydration cannot
|
|
199
|
+
# reproduce it on a fresh clone
|
|
200
|
+
record_log = config.record_project_beads(
|
|
201
|
+
dotbrain_home, project,
|
|
202
|
+
config.ProjectBeads(
|
|
203
|
+
mode="server" if server_host else "embedded",
|
|
204
|
+
remote=remote,
|
|
205
|
+
database=database,
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
if record_log:
|
|
209
|
+
result.logs.append(record_log)
|
|
210
|
+
result.logs.append(
|
|
211
|
+
f"Brainspace ready at {rel}/{project} (not committed); "
|
|
212
|
+
f"review and commit (suggested: feat(brain): wire {project})"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if no_repo:
|
|
216
|
+
result.logs.append(f"created brain-only Brainspace: {brainspace}")
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
assert resolved_repo is not None
|
|
220
|
+
result.warnings += adopter_repos.wire_repo(
|
|
221
|
+
resolved_repo,
|
|
222
|
+
brainspace,
|
|
223
|
+
dotbrain_home,
|
|
224
|
+
run,
|
|
225
|
+
workspace_links=(),
|
|
226
|
+
)
|
|
227
|
+
if paths.INJECT_ADOPTER_POINTER:
|
|
228
|
+
result.warnings += adopter_repos.ensure_agent_context_pointer(resolved_repo)
|
|
229
|
+
expected_links = (".brain",)
|
|
230
|
+
if (brainspace / ".beads").exists():
|
|
231
|
+
expected_links += (".beads",)
|
|
232
|
+
result.warnings += adopter_repos.verify_wiring(
|
|
233
|
+
resolved_repo, run, expected_links=expected_links
|
|
234
|
+
)
|
|
235
|
+
result.logs.append(f"wired {project}")
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# --------------------------------------------------------------------------- wire all
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _repair_adopter_repo_links(
|
|
243
|
+
brainspace: Path, repo: Path, dotbrain_home: Path, run: Runner, *, skip_beads_link: bool = False
|
|
244
|
+
) -> tuple[list[str], list[str]]:
|
|
245
|
+
git_marker = repo / ".git"
|
|
246
|
+
if not (git_marker.is_dir() or git_marker.is_file()):
|
|
247
|
+
return [], [f"{repo} is not a git repo; skipping"]
|
|
248
|
+
|
|
249
|
+
warnings = adopter_repos.wire_repo(
|
|
250
|
+
repo,
|
|
251
|
+
brainspace,
|
|
252
|
+
dotbrain_home,
|
|
253
|
+
run,
|
|
254
|
+
skip_beads_link=skip_beads_link,
|
|
255
|
+
workspace_links=(),
|
|
256
|
+
)
|
|
257
|
+
return [f"wired {brainspace.name} -> {repo}"], warnings
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def wire_all_projects(
|
|
261
|
+
dotbrain_home: Path,
|
|
262
|
+
repo_base: Path | None = None,
|
|
263
|
+
home: Path | None = None,
|
|
264
|
+
run: Runner = _default_run,
|
|
265
|
+
) -> BootstrapResult:
|
|
266
|
+
"""Re-seed brains from templates and repair agent workspace links for every
|
|
267
|
+
wired Brainspace. Dotbrain-owned files (DOTBRAIN.md, README.md) are always
|
|
268
|
+
overwritten so template changes propagate.
|
|
269
|
+
|
|
270
|
+
Mirrors bootstrap.sh wire_brainspaces.
|
|
271
|
+
"""
|
|
272
|
+
dotbrain_home = Path(dotbrain_home).resolve()
|
|
273
|
+
bootstrap.ensure_root_gitignore(dotbrain_home)
|
|
274
|
+
rb = repo_base or (Path.home() / "repos" / "projects")
|
|
275
|
+
result = BootstrapResult()
|
|
276
|
+
cfg = config.load_config(dotbrain_home)
|
|
277
|
+
|
|
278
|
+
for brainspace in paths.brainspaces(dotbrain_home):
|
|
279
|
+
brainspaces.seed_brain(brainspace, dotbrain_home)
|
|
280
|
+
# prepare declared agent workspaces before linking their assets
|
|
281
|
+
result.warnings += brainspaces.seed_agent_workspaces(brainspace, dotbrain_home, home)
|
|
282
|
+
|
|
283
|
+
repo = adopter_repos.repo_for_brainspace(brainspace, dotbrain_home, rb, home)
|
|
284
|
+
if repo is None:
|
|
285
|
+
if not brainspaces.is_brain_only(brainspace):
|
|
286
|
+
result.warnings.append(
|
|
287
|
+
f"no repo found for Brainspace {brainspace.name}; "
|
|
288
|
+
f"add {brainspace}/.repo or create {rb}/{brainspace.name}"
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
active_workspaces = brainspaces.active_agent_workspaces(brainspace, dotbrain_home)
|
|
293
|
+
skill_result, subagent_result, workspace_warnings = _link_project_assets(
|
|
294
|
+
dotbrain_home, brainspace, repo, active_workspaces, run
|
|
295
|
+
)
|
|
296
|
+
result.warnings += workspace_warnings + skill_result.warnings + subagent_result.warnings
|
|
297
|
+
result.logs += [f"pruned skill {entry}" for entry in skill_result.pruned]
|
|
298
|
+
result.logs += [f"pruned subagent {entry}" for entry in subagent_result.pruned]
|
|
299
|
+
|
|
300
|
+
logs, warnings = _repair_adopter_repo_links(
|
|
301
|
+
brainspace, repo, dotbrain_home, run,
|
|
302
|
+
# a declared no-beads project never has a .beads dir; not a warning
|
|
303
|
+
skip_beads_link=config.load_project_config(dotbrain_home, brainspace.name).mode == "none",
|
|
304
|
+
)
|
|
305
|
+
result.logs += logs
|
|
306
|
+
result.warnings += warnings
|
|
307
|
+
if logs:
|
|
308
|
+
result.wired.append(str(repo))
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------- refresh
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _brainspaces_for_refresh(dotbrain_home: Path, projects: Sequence[str] | None) -> list[Path]:
|
|
317
|
+
brainspaces = paths.brainspaces(dotbrain_home)
|
|
318
|
+
if projects is None:
|
|
319
|
+
return brainspaces
|
|
320
|
+
|
|
321
|
+
by_name = {brainspace.name: brainspace for brainspace in brainspaces}
|
|
322
|
+
selected: list[Path] = []
|
|
323
|
+
missing: list[str] = []
|
|
324
|
+
for project in projects:
|
|
325
|
+
brainspace = by_name.get(project)
|
|
326
|
+
if brainspace is None:
|
|
327
|
+
missing.append(project)
|
|
328
|
+
else:
|
|
329
|
+
selected.append(brainspace)
|
|
330
|
+
if missing:
|
|
331
|
+
raise ValueError(f"no Brainspace: {', '.join(missing)}")
|
|
332
|
+
return selected
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def refresh_projects(
|
|
336
|
+
dotbrain_home: Path,
|
|
337
|
+
*,
|
|
338
|
+
projects: Sequence[str] | None = None,
|
|
339
|
+
repo_base: Path | None = None,
|
|
340
|
+
home: Path | None = None,
|
|
341
|
+
workspaces: Sequence[str] = (".claude", ".codex"),
|
|
342
|
+
run: Runner = _default_run,
|
|
343
|
+
) -> RefreshResult:
|
|
344
|
+
"""Refresh project Brainspaces without creating or offboarding projects.
|
|
345
|
+
|
|
346
|
+
Composes the existing concept owners: Brain/workspace seeding, adopter-repo link repair,
|
|
347
|
+
beads load/hydration, and project skill linking.
|
|
348
|
+
"""
|
|
349
|
+
dotbrain_home = Path(dotbrain_home).resolve()
|
|
350
|
+
if not (dotbrain_home / ".git").exists():
|
|
351
|
+
raise RuntimeError(f"{dotbrain_home} is not a dotbrain git checkout")
|
|
352
|
+
|
|
353
|
+
result = RefreshResult()
|
|
354
|
+
brainspace_paths = _brainspaces_for_refresh(dotbrain_home, projects)
|
|
355
|
+
cfg = config.load_config(dotbrain_home)
|
|
356
|
+
rb = repo_base or (Path.home() / "repos" / "projects")
|
|
357
|
+
|
|
358
|
+
for brainspace in brainspace_paths:
|
|
359
|
+
brainspaces.seed_brain(brainspace, dotbrain_home)
|
|
360
|
+
result.warnings += brainspaces.seed_agent_workspaces(brainspace, dotbrain_home, home)
|
|
361
|
+
migrate_log = config.migrate_legacy_skill_manifest(dotbrain_home, brainspace.name)
|
|
362
|
+
if migrate_log:
|
|
363
|
+
result.logs.append(migrate_log)
|
|
364
|
+
declared_workspaces = brainspaces.active_agent_workspaces(brainspace, dotbrain_home)
|
|
365
|
+
active_workspaces = tuple(ws for ws in workspaces if ws in declared_workspaces)
|
|
366
|
+
repo = repo_for_brainspace(brainspace, dotbrain_home, rb, home)
|
|
367
|
+
link_result, subagent_result, workspace_warnings = _link_project_assets(
|
|
368
|
+
dotbrain_home, brainspace, repo, active_workspaces, run
|
|
369
|
+
)
|
|
370
|
+
result.warnings += workspace_warnings
|
|
371
|
+
result.logs += [f"pruned skill {entry}" for entry in link_result.pruned]
|
|
372
|
+
result.logs += [f"stashed collision {path}" for path in link_result.stashed]
|
|
373
|
+
result.warnings += link_result.warnings
|
|
374
|
+
result.logs += [f"pruned subagent {entry}" for entry in subagent_result.pruned]
|
|
375
|
+
result.logs += [f"stashed subagent collision {path}" for path in subagent_result.stashed]
|
|
376
|
+
result.warnings += subagent_result.warnings
|
|
377
|
+
result.logs.append(
|
|
378
|
+
f"project: linked {len(subagent_result.linked)} subagent file(s) into {brainspace.name}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if repo is None:
|
|
382
|
+
if not brainspaces.is_brain_only(brainspace):
|
|
383
|
+
result.warnings.append(
|
|
384
|
+
f"no repo found for Brainspace {brainspace.name}; "
|
|
385
|
+
f"add {brainspace}/.repo or create {rb}/{brainspace.name}"
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
logs, warnings = _repair_adopter_repo_links(
|
|
389
|
+
brainspace,
|
|
390
|
+
repo,
|
|
391
|
+
dotbrain_home,
|
|
392
|
+
run,
|
|
393
|
+
skip_beads_link=config.load_project_config(dotbrain_home, brainspace.name).mode == "none",
|
|
394
|
+
)
|
|
395
|
+
result.logs += logs
|
|
396
|
+
result.warnings += warnings
|
|
397
|
+
|
|
398
|
+
result.refreshed.append(brainspace.name)
|
|
399
|
+
result.logs.append(f"refreshed {brainspace.name} ({len(link_result.linked)} skills linked)")
|
|
400
|
+
|
|
401
|
+
beads_result = beads.pull_beads_for_all(
|
|
402
|
+
dotbrain_home,
|
|
403
|
+
run=run,
|
|
404
|
+
projects=[brainspace.name for brainspace in brainspace_paths],
|
|
405
|
+
)
|
|
406
|
+
result.logs += beads_result.logs
|
|
407
|
+
result.warnings += beads_result.warnings
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def refresh_project(
|
|
412
|
+
dotbrain_home: Path,
|
|
413
|
+
project: str,
|
|
414
|
+
*,
|
|
415
|
+
repo_base: Path | None = None,
|
|
416
|
+
home: Path | None = None,
|
|
417
|
+
workspaces: Sequence[str] = (".claude", ".codex"),
|
|
418
|
+
run: Runner = _default_run,
|
|
419
|
+
) -> RefreshResult:
|
|
420
|
+
"""Refresh one project Brainspace without creating or offboarding it."""
|
|
421
|
+
return refresh_projects(
|
|
422
|
+
dotbrain_home,
|
|
423
|
+
projects=[project],
|
|
424
|
+
repo_base=repo_base,
|
|
425
|
+
home=home,
|
|
426
|
+
workspaces=workspaces,
|
|
427
|
+
run=run,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _resolve_project(repo: Path, project: str | None) -> str:
|
|
432
|
+
"""Infer project name from the .brain symlink target; fall back to repo dir name."""
|
|
433
|
+
brain = repo / ".brain"
|
|
434
|
+
if not project and brain.is_symlink():
|
|
435
|
+
project = brain.resolve().parent.name
|
|
436
|
+
return project or repo.name
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _repo_root(repo: Path | None, run: Runner) -> Path:
|
|
440
|
+
cwd = repo or Path.cwd()
|
|
441
|
+
result = run(["git", "-C", str(cwd), "rev-parse", "--show-toplevel"])
|
|
442
|
+
return Path(result.stdout.strip())
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def unwire_project(
|
|
446
|
+
*,
|
|
447
|
+
dotbrain_home: Path,
|
|
448
|
+
repo: Path | None = None,
|
|
449
|
+
project: str | None = None,
|
|
450
|
+
no_repo: bool = False,
|
|
451
|
+
offboard: str = "keep",
|
|
452
|
+
dry_run: bool = False,
|
|
453
|
+
run: Runner = _default_run,
|
|
454
|
+
) -> UnwireResult:
|
|
455
|
+
"""Disconnect an adopter repo from its dotbrain Brainspace and offboard the Brainspace.
|
|
456
|
+
|
|
457
|
+
Dropping a server-backend project's remote beads database is a separate, explicit step
|
|
458
|
+
(``beads.drop_remote_beads_database`` / the ``beads drop-db`` command).
|
|
459
|
+
"""
|
|
460
|
+
dotbrain_home = Path(dotbrain_home).resolve()
|
|
461
|
+
if not (dotbrain_home / ".git").exists():
|
|
462
|
+
raise RuntimeError(f"{dotbrain_home} is not a dotbrain git checkout")
|
|
463
|
+
|
|
464
|
+
if no_repo:
|
|
465
|
+
if not project:
|
|
466
|
+
raise ValueError("--no-repo requires --name")
|
|
467
|
+
resolved_repo = None
|
|
468
|
+
result = UnwireResult(project=project, repo=None)
|
|
469
|
+
else:
|
|
470
|
+
resolved_repo = _repo_root(repo, run)
|
|
471
|
+
project = _resolve_project(resolved_repo, project)
|
|
472
|
+
result = unwire_repo(
|
|
473
|
+
resolved_repo, dry_run=dry_run, dotbrain_home=dotbrain_home, run=run
|
|
474
|
+
)
|
|
475
|
+
result.project = project
|
|
476
|
+
result.logs += offboard_brainspace(dotbrain_home, project, offboard, dry_run=dry_run, run=run)
|
|
477
|
+
if offboard in {"archive", "delete"} and not dry_run:
|
|
478
|
+
# the Brainspace is gone from projects/; a leftover entry would make
|
|
479
|
+
# bootstrap try to hydrate a non-existent project
|
|
480
|
+
removed = config.remove_project_beads(dotbrain_home, project)
|
|
481
|
+
if removed:
|
|
482
|
+
result.logs.append(removed)
|
|
483
|
+
result.logs.append(
|
|
484
|
+
f"unwired {project} (repo: {resolved_repo})\n"
|
|
485
|
+
"repo edits are not committed; review and commit if wanted"
|
|
486
|
+
)
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def unwire_all_projects(
|
|
491
|
+
dotbrain_home: Path,
|
|
492
|
+
offboard: str = "keep",
|
|
493
|
+
dry_run: bool = False,
|
|
494
|
+
run: Runner = _default_run,
|
|
495
|
+
) -> list[UnwireResult]:
|
|
496
|
+
"""Unwire every project Brainspace. Continues on per-project failure.
|
|
497
|
+
|
|
498
|
+
``offboard`` defaults to ``keep`` (Brainspaces are not destroyed in batch
|
|
499
|
+
mode). Mutually exclusive with ``--archive``/``--delete`` — use per-project
|
|
500
|
+
``unwire`` for destructive offboard.
|
|
501
|
+
"""
|
|
502
|
+
dotbrain_home = Path(dotbrain_home).resolve()
|
|
503
|
+
results: list[UnwireResult] = []
|
|
504
|
+
for brainspace in paths.brainspaces(dotbrain_home):
|
|
505
|
+
project = brainspace.name
|
|
506
|
+
try:
|
|
507
|
+
repo = repo_for_brainspace(brainspace, dotbrain_home)
|
|
508
|
+
if repo and repo.is_dir():
|
|
509
|
+
result = unwire_repo(repo, dry_run=dry_run, dotbrain_home=dotbrain_home, run=run)
|
|
510
|
+
else:
|
|
511
|
+
result = UnwireResult(project=project, repo=None)
|
|
512
|
+
result.project = project
|
|
513
|
+
result.logs += offboard_brainspace(
|
|
514
|
+
dotbrain_home, project, offboard, dry_run=dry_run, run=run,
|
|
515
|
+
)
|
|
516
|
+
if offboard in {"archive", "delete"} and not dry_run:
|
|
517
|
+
removed = config.remove_project_beads(dotbrain_home, project)
|
|
518
|
+
if removed:
|
|
519
|
+
result.logs.append(removed)
|
|
520
|
+
result.logs.append(
|
|
521
|
+
f"unwired {project} (repo: {repo})"
|
|
522
|
+
)
|
|
523
|
+
results.append(result)
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
results.append(UnwireResult(
|
|
526
|
+
project=project,
|
|
527
|
+
logs=[f"error unwiring {project}: {exc}"],
|
|
528
|
+
))
|
|
529
|
+
return results
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dotbrain
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Wires project Brainspaces and skills into coding agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/arminzou/dotbrain
|
|
6
|
+
Project-URL: Repository, https://github.com/arminzou/dotbrain
|
|
7
|
+
Project-URL: Issues, https://github.com/arminzou/dotbrain/issues
|
|
8
|
+
Author: Armin Zou
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
dotbrain/__init__.py,sha256=HN1nRGcd7iLuIMOOXxlpWVRL8w1lfi176MNiI_5fpsQ,202
|
|
2
|
+
dotbrain/_cli_reference.py,sha256=-uwqJ96WTbh4U7ptSwSA6JM2kD27G-p9q3GZc71h09c,4619
|
|
3
|
+
dotbrain/adopter_repos.py,sha256=nFF4Ke1z_hcUA-L_UgjWXFtXAEd3t2haPnkLMeNO_J4,21080
|
|
4
|
+
dotbrain/beads.py,sha256=wteGT6Op7MQtshdz60-T5vu5-Zn-21PEYakLPbigtds,20103
|
|
5
|
+
dotbrain/bootstrap.py,sha256=srBotcoROGcxFWsdYZBsegBAxutVdO01r033V1XHuvM,7609
|
|
6
|
+
dotbrain/brainspaces.py,sha256=yqEhAZEBHnn5JkRw3RWjJDlnr-yQvFASl6z5kwxiPTk,9257
|
|
7
|
+
dotbrain/cli.py,sha256=QUJKud1JPgnOzYidgywcmv4uYo8JojlO6fEW-4i5tgI,29863
|
|
8
|
+
dotbrain/config.py,sha256=5ZLFKMwIru8VrSRAMMWWV8X_aPfJzRvu2ktygWOXeKU,15600
|
|
9
|
+
dotbrain/doctor.py,sha256=KBGuPCPn5ternVQcFSr7_hav7nrPWM2UHCcMNGoHSgc,10892
|
|
10
|
+
dotbrain/hooks.py,sha256=-RttCq5QbgnaK6RKzo8nkIq98i2tAHTf-x8POyMDVKY,3108
|
|
11
|
+
dotbrain/migrate.py,sha256=lSVuKPbnvL_0xiclKTAlAy8Q_9SxTPU7jHKRdqyTWS4,11953
|
|
12
|
+
dotbrain/paths.py,sha256=H9oddr2S3jiVlAnAhb-xqJ9bWXmPD3F1j6Fgb_8yPJU,5318
|
|
13
|
+
dotbrain/resource_loader.py,sha256=vz88YCw-e3Thq880x2SV2VnzqAbitoli4Suw7SidKLw,1428
|
|
14
|
+
dotbrain/skills.py,sha256=_C4HjRoJkKKSYUTLL-hmESIf4QGX5tfVbKTdgg0YBbU,8147
|
|
15
|
+
dotbrain/subagents.py,sha256=43p6JEezGZgC4JGgn3sZYQxm8IIz9s2nPH2SuKIa45g,8513
|
|
16
|
+
dotbrain/updater.py,sha256=0S3HcJEyE2C4UHezEFThoAJbsIDGPjreaBJC_J4dnHU,3576
|
|
17
|
+
dotbrain/workflows.py,sha256=r68EtJxsK1R_H1fT4AXQKz4EYCQp_-1L6iqqj3cMHqE,20374
|
|
18
|
+
dotbrain/resources/__init__.py,sha256=5wg3Qaq5N7goIs7n4wB1Omzs1bW9Ui9YlMbUTLO4_Iw,41
|
|
19
|
+
dotbrain/resources/config.yaml,sha256=jHtMw_jxaMIMWSZvyYXWlFj1Vz2mYc7p9UNW3VuilmA,995
|
|
20
|
+
dotbrain/resources/core.yaml,sha256=hP1_4nJWptFgP4VTFOkv5c2AhROBDJApndl2TsSHjq8,658
|
|
21
|
+
dotbrain/resources/agents/claude/implementer.md,sha256=WCBdZn3hGhbdNMWCArArCvAu8HRRkVEbL8NHewlUIlM,2433
|
|
22
|
+
dotbrain/resources/agents/claude/investigator.md,sha256=piBUF4tQwAmGBhUjwlqAUgvDKQmh-XOin-vpgJwCGB4,1468
|
|
23
|
+
dotbrain/resources/agents/claude/reviewer.md,sha256=j3czU-ykZeTGALjtJS1XaoGzP3bY6ltN9OSmRdxqQgk,1848
|
|
24
|
+
dotbrain/resources/agents/claude/verifier.md,sha256=PaNt5tzs2hPXBR0fmCV6Z0GHlWmNObsBvtFzncYV0cY,1583
|
|
25
|
+
dotbrain/resources/agents/codex/implementer.toml,sha256=4axdxDDojpiRMPxrRW4uo28gMbfizrB0pvZ0Gd-K-lA,1345
|
|
26
|
+
dotbrain/resources/agents/codex/investigator.toml,sha256=rMBZWHKR_RbYWEpn-96LmaiIcgexNPt88JrIe5dZyUo,977
|
|
27
|
+
dotbrain/resources/agents/codex/reviewer.toml,sha256=4Dxr6xKO3O5jLTTx9FMGhQhFoYJlhVykksvJtz8AdkE,1545
|
|
28
|
+
dotbrain/resources/agents/codex/verifier.toml,sha256=1SqQ-GX4WbScQ81nUoX8Nsi2voG18NmRnwwlfkQWncE,1028
|
|
29
|
+
dotbrain/resources/templates/gitignore,sha256=fKOqYIasov03QWzLnmLEH7K98ldvVQ850TLf6wgPRdI,477
|
|
30
|
+
dotbrain/resources/templates/brain/AGENTS.md,sha256=coraxjZ4NtPEk3mhJY3Bw59dd0MT-OJt_Qyf4e05bZc,828
|
|
31
|
+
dotbrain/resources/templates/brain/DOTBRAIN.md,sha256=X40SGuKLD7w7PkO2dbnP3zNlyaYzANlFQmNWAmC3-9E,6860
|
|
32
|
+
dotbrain/resources/templates/brain/project.yaml,sha256=9e2Z_fVxYeQ0SBQ7OhqxPQRfxZcAxI1bAva_xSh9JEY,931
|
|
33
|
+
dotbrain/resources/templates/brain/adr/README.md,sha256=6D3TlAlcKu3qdYsmYvRH2mKM_vFRhLqLB_tga4-Zj1Q,285
|
|
34
|
+
dotbrain/resources/templates/brain/designs/README.md,sha256=mQDEkw_NHgd_PlvpJbvtxO4bab_bZgedbXm2jrcMJEc,1571
|
|
35
|
+
dotbrain/resources/templates/brain/docs/README.md,sha256=D2GcjZEMv6HLm6mLeuNj8vKYW_fzRRfHtSqvoZm4CjA,361
|
|
36
|
+
dotbrain-0.3.4.dist-info/METADATA,sha256=5GW8h-MR4KyV4d9XXHWyAD1HRusME1XaY1vOWfutkeg,832
|
|
37
|
+
dotbrain-0.3.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
38
|
+
dotbrain-0.3.4.dist-info/entry_points.txt,sha256=xFOB2wF9P8j5XZzg1JmVevJMZArPxLNjfrsRXU20AXM,46
|
|
39
|
+
dotbrain-0.3.4.dist-info/licenses/LICENSE,sha256=dNK42nSumWv628_jEFn1wbwoHlwaeHQXJKlsvewyGbc,1066
|
|
40
|
+
dotbrain-0.3.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armin Zou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|