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.
Files changed (40) hide show
  1. dotbrain/__init__.py +7 -0
  2. dotbrain/_cli_reference.py +115 -0
  3. dotbrain/adopter_repos.py +573 -0
  4. dotbrain/beads.py +511 -0
  5. dotbrain/bootstrap.py +199 -0
  6. dotbrain/brainspaces.py +238 -0
  7. dotbrain/cli.py +660 -0
  8. dotbrain/config.py +428 -0
  9. dotbrain/doctor.py +279 -0
  10. dotbrain/hooks.py +84 -0
  11. dotbrain/migrate.py +304 -0
  12. dotbrain/paths.py +139 -0
  13. dotbrain/resource_loader.py +47 -0
  14. dotbrain/resources/__init__.py +1 -0
  15. dotbrain/resources/agents/claude/implementer.md +47 -0
  16. dotbrain/resources/agents/claude/investigator.md +35 -0
  17. dotbrain/resources/agents/claude/reviewer.md +38 -0
  18. dotbrain/resources/agents/claude/verifier.md +35 -0
  19. dotbrain/resources/agents/codex/implementer.toml +26 -0
  20. dotbrain/resources/agents/codex/investigator.toml +21 -0
  21. dotbrain/resources/agents/codex/reviewer.toml +27 -0
  22. dotbrain/resources/agents/codex/verifier.toml +21 -0
  23. dotbrain/resources/config.yaml +20 -0
  24. dotbrain/resources/core.yaml +18 -0
  25. dotbrain/resources/templates/brain/AGENTS.md +9 -0
  26. dotbrain/resources/templates/brain/DOTBRAIN.md +105 -0
  27. dotbrain/resources/templates/brain/adr/README.md +8 -0
  28. dotbrain/resources/templates/brain/designs/README.md +28 -0
  29. dotbrain/resources/templates/brain/docs/README.md +9 -0
  30. dotbrain/resources/templates/brain/project.yaml +27 -0
  31. dotbrain/resources/templates/gitignore +17 -0
  32. dotbrain/skills.py +264 -0
  33. dotbrain/subagents.py +252 -0
  34. dotbrain/updater.py +105 -0
  35. dotbrain/workflows.py +529 -0
  36. dotbrain-0.3.4.dist-info/METADATA +21 -0
  37. dotbrain-0.3.4.dist-info/RECORD +40 -0
  38. dotbrain-0.3.4.dist-info/WHEEL +4 -0
  39. dotbrain-0.3.4.dist-info/entry_points.txt +2 -0
  40. 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dotbrain = dotbrain.cli:app
@@ -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.