beacon-framework 0.1.0__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.
- beacon/__init__.py +3 -0
- beacon/__main__.py +4 -0
- beacon/cli.py +177 -0
- beacon/installer.py +92 -0
- beacon/integrations/__init__.py +36 -0
- beacon/integrations/claude_code.py +123 -0
- beacon/manifest.py +55 -0
- beacon/resources/beacon.md +9 -0
- beacon/resources/integrations/claude_code/CLAUDE.md.fragment +137 -0
- beacon/resources/integrations/claude_code/commands/design/diagram.md +313 -0
- beacon/resources/integrations/claude_code/commands/design/evaluate.md +156 -0
- beacon/resources/integrations/claude_code/commands/design/wardley.md +156 -0
- beacon/resources/integrations/claude_code/commands/git/feature.md +57 -0
- beacon/resources/integrations/claude_code/commands/git/pr.md +78 -0
- beacon/resources/integrations/claude_code/commands/git/release.md +107 -0
- beacon/resources/integrations/claude_code/commands/init.md +179 -0
- beacon/resources/project-management/ADRs/ADR-000-template.md +60 -0
- beacon/resources/project-management/ADRs/README.md +31 -0
- beacon/resources/project-management/Background/00-problem-statement.md +48 -0
- beacon/resources/project-management/Background/01-final-architecture-document.md +172 -0
- beacon/resources/project-management/Prompts/01-SEED.md +61 -0
- beacon/resources/project-management/Prompts/02-DESIGN.md +78 -0
- beacon/resources/project-management/Prompts/03-BUILD.md +70 -0
- beacon/resources/project-management/Prompts/04-SHIP.md +75 -0
- beacon/resources/project-management/Roadmap/README.md +80 -0
- beacon/resources/project-management/Roadmap/archive/.gitkeep +0 -0
- beacon/resources/project-management/Work/README.md +60 -0
- beacon/resources/project-management/Work/analysis/.gitkeep +0 -0
- beacon/resources/project-management/Work/planning/.gitkeep +0 -0
- beacon/resources/project-management/Work/sessions/.gitkeep +0 -0
- beacon/upgrader.py +31 -0
- beacon_framework-0.1.0.dist-info/METADATA +92 -0
- beacon_framework-0.1.0.dist-info/RECORD +36 -0
- beacon_framework-0.1.0.dist-info/WHEEL +4 -0
- beacon_framework-0.1.0.dist-info/entry_points.txt +2 -0
- beacon_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
beacon/__init__.py
ADDED
beacon/__main__.py
ADDED
beacon/cli.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""BEACON CLI — install, upgrade, validate, and manage AI integrations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from beacon import __version__, installer, integrations
|
|
11
|
+
from beacon.manifest import MANIFEST_RELPATH, Manifest
|
|
12
|
+
from beacon.upgrader import upgrade as upgrade_project
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="beacon",
|
|
16
|
+
help="BEACON Framework — lifecycle and discipline for artifact-driven AI-assisted delivery.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
integration_app = typer.Typer(
|
|
21
|
+
name="integration",
|
|
22
|
+
help="Manage AI tool integrations (Claude Code, ...).",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
app.add_typer(integration_app)
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_root(here: bool, path: Path | None) -> Path:
|
|
31
|
+
if here and path is not None:
|
|
32
|
+
raise typer.BadParameter("Pass either --here or PATH, not both.")
|
|
33
|
+
if here:
|
|
34
|
+
return Path.cwd()
|
|
35
|
+
if path is not None:
|
|
36
|
+
return path.resolve()
|
|
37
|
+
return Path.cwd()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def version() -> None:
|
|
42
|
+
"""Print the installed BEACON version."""
|
|
43
|
+
console.print(f"beacon {__version__}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command()
|
|
47
|
+
def init(
|
|
48
|
+
path: Path | None = typer.Argument(None, help="Project directory (default: cwd)."),
|
|
49
|
+
here: bool = typer.Option(False, "--here", help="Install into the current directory."),
|
|
50
|
+
ai: list[str] = typer.Option(
|
|
51
|
+
["claude"],
|
|
52
|
+
"--ai",
|
|
53
|
+
help="AI integration(s) to wire up.",
|
|
54
|
+
),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Install BEACON into a project. Idempotent: existing user files are preserved."""
|
|
57
|
+
root = _resolve_root(here, path)
|
|
58
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
existing = Manifest.load(root)
|
|
61
|
+
manifest = existing or Manifest.fresh()
|
|
62
|
+
|
|
63
|
+
framework_written, user_seeded_written = installer.install_core(root, overwrite_framework=True)
|
|
64
|
+
|
|
65
|
+
for name in ai:
|
|
66
|
+
try:
|
|
67
|
+
integrations.install(name, root)
|
|
68
|
+
except KeyError as e:
|
|
69
|
+
raise typer.BadParameter(str(e)) from e
|
|
70
|
+
|
|
71
|
+
manifest.beacon_version = __version__
|
|
72
|
+
manifest.ai = sorted(set(manifest.ai) | set(ai))
|
|
73
|
+
manifest.framework_files = sorted(set(manifest.framework_files) | set(framework_written))
|
|
74
|
+
manifest.user_seeded_files = sorted(set(manifest.user_seeded_files) | set(user_seeded_written))
|
|
75
|
+
manifest.save(root)
|
|
76
|
+
|
|
77
|
+
console.print(f"[green]✓[/green] BEACON {__version__} installed at [cyan]{root}[/cyan]")
|
|
78
|
+
console.print(f" framework files written: {len(framework_written)}")
|
|
79
|
+
console.print(f" user-seeded files written: {len(user_seeded_written)} (existing left intact)")
|
|
80
|
+
console.print(f" integrations: {', '.join(manifest.ai) or 'none'}")
|
|
81
|
+
console.print(f" manifest: [dim]{MANIFEST_RELPATH}[/dim]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def upgrade(
|
|
86
|
+
path: Path | None = typer.Argument(None, help="Project directory (default: cwd)."),
|
|
87
|
+
here: bool = typer.Option(False, "--here", help="Upgrade in the current directory."),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Refresh BEACON framework files. User-seeded content is left untouched."""
|
|
90
|
+
root = _resolve_root(here, path)
|
|
91
|
+
try:
|
|
92
|
+
manifest, touched = upgrade_project(root)
|
|
93
|
+
except FileNotFoundError as e:
|
|
94
|
+
raise typer.Exit(code=1) from typer.BadParameter(str(e))
|
|
95
|
+
console.print(f"[green]✓[/green] BEACON upgraded to {manifest.beacon_version}")
|
|
96
|
+
console.print(f" files refreshed: {len(touched)}")
|
|
97
|
+
console.print(f" integrations: {', '.join(manifest.ai) or 'none'}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command()
|
|
101
|
+
def check(
|
|
102
|
+
path: Path | None = typer.Argument(None, help="Project directory (default: cwd)."),
|
|
103
|
+
here: bool = typer.Option(False, "--here", help="Check the current directory."),
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Validate a BEACON install: manifest present and required files in place."""
|
|
106
|
+
root = _resolve_root(here, path)
|
|
107
|
+
manifest = Manifest.load(root)
|
|
108
|
+
if manifest is None:
|
|
109
|
+
console.print(
|
|
110
|
+
f"[red]✗[/red] No BEACON manifest at {root / MANIFEST_RELPATH}. "
|
|
111
|
+
f"Run `beacon init` first."
|
|
112
|
+
)
|
|
113
|
+
raise typer.Exit(code=1)
|
|
114
|
+
|
|
115
|
+
missing: list[str] = []
|
|
116
|
+
for rel in manifest.framework_files:
|
|
117
|
+
if not (root / rel).exists():
|
|
118
|
+
missing.append(rel)
|
|
119
|
+
|
|
120
|
+
if missing:
|
|
121
|
+
console.print(f"[red]✗[/red] {len(missing)} framework files missing:")
|
|
122
|
+
for rel in missing:
|
|
123
|
+
console.print(f" - {rel}")
|
|
124
|
+
console.print(" Run `beacon upgrade` to restore.")
|
|
125
|
+
raise typer.Exit(code=1)
|
|
126
|
+
|
|
127
|
+
console.print(
|
|
128
|
+
f"[green]✓[/green] BEACON {manifest.beacon_version} OK at [cyan]{root}[/cyan] "
|
|
129
|
+
f"(integrations: {', '.join(manifest.ai) or 'none'})"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@integration_app.command("list")
|
|
134
|
+
def integration_list() -> None:
|
|
135
|
+
"""List available AI integrations."""
|
|
136
|
+
for name in integrations.names():
|
|
137
|
+
console.print(f" - {name}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@integration_app.command("add")
|
|
141
|
+
def integration_add(
|
|
142
|
+
name: str,
|
|
143
|
+
path: Path | None = typer.Argument(None, help="Project directory (default: cwd)."),
|
|
144
|
+
here: bool = typer.Option(False, "--here"),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Add an AI integration to an existing BEACON install."""
|
|
147
|
+
root = _resolve_root(here, path)
|
|
148
|
+
manifest = Manifest.load(root)
|
|
149
|
+
if manifest is None:
|
|
150
|
+
raise typer.BadParameter("No BEACON manifest. Run `beacon init` first.")
|
|
151
|
+
try:
|
|
152
|
+
integrations.install(name, root)
|
|
153
|
+
except KeyError as e:
|
|
154
|
+
raise typer.BadParameter(str(e)) from e
|
|
155
|
+
manifest.ai = sorted(set(manifest.ai) | {name})
|
|
156
|
+
manifest.save(root)
|
|
157
|
+
console.print(f"[green]✓[/green] integration added: {name}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@integration_app.command("remove")
|
|
161
|
+
def integration_remove(
|
|
162
|
+
name: str,
|
|
163
|
+
path: Path | None = typer.Argument(None, help="Project directory (default: cwd)."),
|
|
164
|
+
here: bool = typer.Option(False, "--here"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Remove an AI integration; manifest is updated accordingly."""
|
|
167
|
+
root = _resolve_root(here, path)
|
|
168
|
+
manifest = Manifest.load(root)
|
|
169
|
+
if manifest is None:
|
|
170
|
+
raise typer.BadParameter("No BEACON manifest. Run `beacon init` first.")
|
|
171
|
+
try:
|
|
172
|
+
integrations.remove(name, root)
|
|
173
|
+
except KeyError as e:
|
|
174
|
+
raise typer.BadParameter(str(e)) from e
|
|
175
|
+
manifest.ai = sorted(set(manifest.ai) - {name})
|
|
176
|
+
manifest.save(root)
|
|
177
|
+
console.print(f"[green]✓[/green] integration removed: {name}")
|
beacon/installer.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""File installation logic for BEACON.
|
|
2
|
+
|
|
3
|
+
Two file categories with different upgrade semantics:
|
|
4
|
+
|
|
5
|
+
- **framework**: shipped by the package, always overwritten on ``beacon init``
|
|
6
|
+
and ``beacon upgrade``. Users should not edit these.
|
|
7
|
+
- **user_seeded**: shipped as a starting point. Written by ``init`` only if
|
|
8
|
+
absent; ``upgrade`` never touches them.
|
|
9
|
+
|
|
10
|
+
The classification is data, not code: see ``FRAMEWORK_RELPATHS`` and
|
|
11
|
+
``USER_SEEDED_RELPATHS`` below.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
from importlib import resources
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Paths are relative to the *target project root*, and mirror the layout under
|
|
21
|
+
# ``src/beacon/resources/``.
|
|
22
|
+
USER_SEEDED_RELPATHS: tuple[str, ...] = (
|
|
23
|
+
"beacon.md",
|
|
24
|
+
"project-management/Background/00-problem-statement.md",
|
|
25
|
+
"project-management/Background/01-final-architecture-document.md",
|
|
26
|
+
"project-management/Roadmap/README.md",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
FRAMEWORK_RELPATHS: tuple[str, ...] = (
|
|
30
|
+
"project-management/ADRs/README.md",
|
|
31
|
+
"project-management/ADRs/ADR-000-template.md",
|
|
32
|
+
"project-management/Prompts/01-SEED.md",
|
|
33
|
+
"project-management/Prompts/02-DESIGN.md",
|
|
34
|
+
"project-management/Prompts/03-BUILD.md",
|
|
35
|
+
"project-management/Prompts/04-SHIP.md",
|
|
36
|
+
"project-management/Work/README.md",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
GITKEEP_RELPATHS: tuple[str, ...] = (
|
|
40
|
+
"project-management/Roadmap/archive/.gitkeep",
|
|
41
|
+
"project-management/Work/analysis/.gitkeep",
|
|
42
|
+
"project-management/Work/planning/.gitkeep",
|
|
43
|
+
"project-management/Work/sessions/.gitkeep",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resource_root() -> Path:
|
|
48
|
+
"""Return the on-disk path to the packaged ``resources/`` directory."""
|
|
49
|
+
return Path(str(resources.files("beacon").joinpath("resources")))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _copy_one(src: Path, dst: Path) -> None:
|
|
53
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
shutil.copyfile(src, dst)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def install_core(project_root: Path, *, overwrite_framework: bool) -> tuple[list[str], list[str]]:
|
|
58
|
+
"""Install the BEACON core file set into ``project_root``.
|
|
59
|
+
|
|
60
|
+
``overwrite_framework=False`` is the ``init`` behaviour (seed only what's
|
|
61
|
+
missing). ``True`` is the ``upgrade`` behaviour: refresh framework files,
|
|
62
|
+
leave user_seeded alone.
|
|
63
|
+
"""
|
|
64
|
+
src_root = _resource_root()
|
|
65
|
+
framework_written: list[str] = []
|
|
66
|
+
user_seeded_written: list[str] = []
|
|
67
|
+
|
|
68
|
+
for rel in FRAMEWORK_RELPATHS:
|
|
69
|
+
src = src_root / rel
|
|
70
|
+
dst = project_root / rel
|
|
71
|
+
if dst.exists() and not overwrite_framework:
|
|
72
|
+
continue
|
|
73
|
+
_copy_one(src, dst)
|
|
74
|
+
framework_written.append(rel)
|
|
75
|
+
|
|
76
|
+
for rel in USER_SEEDED_RELPATHS:
|
|
77
|
+
src = src_root / rel
|
|
78
|
+
dst = project_root / rel
|
|
79
|
+
if dst.exists():
|
|
80
|
+
continue # never overwrite user-seeded files
|
|
81
|
+
_copy_one(src, dst)
|
|
82
|
+
user_seeded_written.append(rel)
|
|
83
|
+
|
|
84
|
+
for rel in GITKEEP_RELPATHS:
|
|
85
|
+
dst = project_root / rel
|
|
86
|
+
if dst.exists():
|
|
87
|
+
continue
|
|
88
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
dst.touch()
|
|
90
|
+
framework_written.append(rel)
|
|
91
|
+
|
|
92
|
+
return framework_written, user_seeded_written
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""AI tool integrations for BEACON.
|
|
2
|
+
|
|
3
|
+
The registry below is the single source of truth for which integrations exist.
|
|
4
|
+
Adding a new one (Copilot, Gemini, ...) is one entry plus one module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from beacon.integrations import claude_code
|
|
13
|
+
|
|
14
|
+
# Each entry exposes (install, remove) callables. Both take ``project_root`` and
|
|
15
|
+
# return the list of relative paths written or removed.
|
|
16
|
+
Integration = tuple[Callable[[Path], list[str]], Callable[[Path], list[str]]]
|
|
17
|
+
|
|
18
|
+
REGISTRY: dict[str, Integration] = {
|
|
19
|
+
"claude": (claude_code.install, claude_code.remove),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def names() -> list[str]:
|
|
24
|
+
return sorted(REGISTRY.keys())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def install(name: str, project_root: Path) -> list[str]:
|
|
28
|
+
if name not in REGISTRY:
|
|
29
|
+
raise KeyError(f"Unknown integration: {name}. Available: {names()}")
|
|
30
|
+
return REGISTRY[name][0](project_root)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove(name: str, project_root: Path) -> list[str]:
|
|
34
|
+
if name not in REGISTRY:
|
|
35
|
+
raise KeyError(f"Unknown integration: {name}. Available: {names()}")
|
|
36
|
+
return REGISTRY[name][1](project_root)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Claude Code integration.
|
|
2
|
+
|
|
3
|
+
Installs slash commands under ``.claude/commands/`` and a marker-delimited
|
|
4
|
+
fragment into ``.claude/CLAUDE.md``. The fragment is the only place where
|
|
5
|
+
BEACON's lifecycle and Spec Kit seams are described — by design.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
INTEGRATION_NAME = "claude"
|
|
15
|
+
RESOURCE_SUBDIR = "integrations/claude_code"
|
|
16
|
+
|
|
17
|
+
# Slash command files installed under ``.claude/commands/``. Always overwritten
|
|
18
|
+
# by ``install`` / ``upgrade`` — BEACON owns these.
|
|
19
|
+
COMMAND_RELPATHS: tuple[str, ...] = (
|
|
20
|
+
".claude/commands/init.md",
|
|
21
|
+
".claude/commands/git/feature.md",
|
|
22
|
+
".claude/commands/git/pr.md",
|
|
23
|
+
".claude/commands/git/release.md",
|
|
24
|
+
".claude/commands/design/diagram.md",
|
|
25
|
+
".claude/commands/design/evaluate.md",
|
|
26
|
+
".claude/commands/design/wardley.md",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
CLAUDE_MD = Path(".claude/CLAUDE.md")
|
|
30
|
+
START_MARKER = "<!-- BEACON START -->"
|
|
31
|
+
END_MARKER = "<!-- BEACON END -->"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resource_root() -> Path:
|
|
35
|
+
return Path(str(resources.files("beacon").joinpath("resources").joinpath(RESOURCE_SUBDIR)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _copy_command(rel: str, project_root: Path) -> None:
|
|
39
|
+
src = _resource_root() / "commands" / rel.removeprefix(".claude/commands/")
|
|
40
|
+
dst = project_root / rel
|
|
41
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
shutil.copyfile(src, dst)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_fragment() -> str:
|
|
46
|
+
return (_resource_root() / "CLAUDE.md.fragment").read_text()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _inject_fragment(project_root: Path) -> None:
|
|
50
|
+
"""Write or refresh the BEACON-managed block in ``.claude/CLAUDE.md``.
|
|
51
|
+
|
|
52
|
+
If the file does not exist, create it with just the fragment.
|
|
53
|
+
If markers are present, replace what's between them.
|
|
54
|
+
If markers are absent, append the fragment.
|
|
55
|
+
"""
|
|
56
|
+
path = project_root / CLAUDE_MD
|
|
57
|
+
fragment = _read_fragment().rstrip() + "\n"
|
|
58
|
+
|
|
59
|
+
if not path.exists():
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
path.write_text(fragment)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
content = path.read_text()
|
|
65
|
+
start = content.find(START_MARKER)
|
|
66
|
+
end = content.find(END_MARKER)
|
|
67
|
+
if start != -1 and end != -1 and end > start:
|
|
68
|
+
end_line = content.find("\n", end)
|
|
69
|
+
end_idx = end_line + 1 if end_line != -1 else len(content)
|
|
70
|
+
new = content[:start] + fragment + content[end_idx:]
|
|
71
|
+
path.write_text(new)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
sep = "" if content.endswith("\n") else "\n"
|
|
75
|
+
path.write_text(content + sep + "\n" + fragment)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _strip_fragment(project_root: Path) -> None:
|
|
79
|
+
path = project_root / CLAUDE_MD
|
|
80
|
+
if not path.exists():
|
|
81
|
+
return
|
|
82
|
+
content = path.read_text()
|
|
83
|
+
start = content.find(START_MARKER)
|
|
84
|
+
end = content.find(END_MARKER)
|
|
85
|
+
if start == -1 or end == -1 or end <= start:
|
|
86
|
+
return
|
|
87
|
+
end_line = content.find("\n", end)
|
|
88
|
+
end_idx = end_line + 1 if end_line != -1 else len(content)
|
|
89
|
+
cleaned = content[:start] + content[end_idx:]
|
|
90
|
+
# Collapse consecutive blank lines at the seam
|
|
91
|
+
while "\n\n\n" in cleaned:
|
|
92
|
+
cleaned = cleaned.replace("\n\n\n", "\n\n")
|
|
93
|
+
path.write_text(cleaned)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def install(project_root: Path) -> list[str]:
|
|
97
|
+
"""Install Claude Code integration. Returns relpaths written."""
|
|
98
|
+
written: list[str] = []
|
|
99
|
+
for rel in COMMAND_RELPATHS:
|
|
100
|
+
_copy_command(rel, project_root)
|
|
101
|
+
written.append(rel)
|
|
102
|
+
_inject_fragment(project_root)
|
|
103
|
+
written.append(str(CLAUDE_MD))
|
|
104
|
+
return written
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def remove(project_root: Path) -> list[str]:
|
|
108
|
+
"""Remove Claude Code integration. Returns relpaths removed/modified."""
|
|
109
|
+
removed: list[str] = []
|
|
110
|
+
for rel in COMMAND_RELPATHS:
|
|
111
|
+
p = project_root / rel
|
|
112
|
+
if p.exists():
|
|
113
|
+
p.unlink()
|
|
114
|
+
removed.append(rel)
|
|
115
|
+
# Clean empty parent dirs (best effort)
|
|
116
|
+
for parent in (p.parent, p.parent.parent):
|
|
117
|
+
try:
|
|
118
|
+
parent.rmdir()
|
|
119
|
+
except OSError:
|
|
120
|
+
break
|
|
121
|
+
_strip_fragment(project_root)
|
|
122
|
+
removed.append(str(CLAUDE_MD))
|
|
123
|
+
return removed
|
beacon/manifest.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""BEACON installation manifest — the atomicity contract.
|
|
2
|
+
|
|
3
|
+
A manifest lives at ``project-management/.beacon/init-options.json`` and records:
|
|
4
|
+
- the package version used to install,
|
|
5
|
+
- which AI integrations are wired up,
|
|
6
|
+
- which files BEACON owns (``framework`` — overwritten by upgrade) versus
|
|
7
|
+
which it merely seeded (``user_seeded`` — never overwritten).
|
|
8
|
+
|
|
9
|
+
The manifest is what makes ``beacon upgrade`` safe: it knows exactly which
|
|
10
|
+
files it may touch and which belong to the user.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import asdict, dataclass, field
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from beacon import __version__
|
|
21
|
+
|
|
22
|
+
MANIFEST_RELPATH = Path("project-management/.beacon/init-options.json")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Manifest:
|
|
27
|
+
beacon_version: str = __version__
|
|
28
|
+
installed_at: str = ""
|
|
29
|
+
ai: list[str] = field(default_factory=list)
|
|
30
|
+
framework_files: list[str] = field(default_factory=list)
|
|
31
|
+
user_seeded_files: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def fresh(cls) -> Manifest:
|
|
35
|
+
return cls(installed_at=datetime.now(timezone.utc).isoformat(timespec="seconds"))
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def load(cls, project_root: Path) -> Manifest | None:
|
|
39
|
+
path = project_root / MANIFEST_RELPATH
|
|
40
|
+
if not path.exists():
|
|
41
|
+
return None
|
|
42
|
+
data = json.loads(path.read_text())
|
|
43
|
+
return cls(
|
|
44
|
+
beacon_version=data.get("beacon_version", ""),
|
|
45
|
+
installed_at=data.get("installed_at", ""),
|
|
46
|
+
ai=list(data.get("ai", [])),
|
|
47
|
+
framework_files=list(data.get("framework_files", [])),
|
|
48
|
+
user_seeded_files=list(data.get("user_seeded_files", [])),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def save(self, project_root: Path) -> Path:
|
|
52
|
+
path = project_root / MANIFEST_RELPATH
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
path.write_text(json.dumps(asdict(self), indent=2, sort_keys=True) + "\n")
|
|
55
|
+
return path
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
> "Would I proudly sign my name to this?"
|
|
4
|
+
|
|
5
|
+
| Phase | Active Bullet | Branch | Last DEV deploy |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| SEED → **DESIGN** → BUILD → SHIP | `#— —` | `—` | `—` |
|
|
8
|
+
|
|
9
|
+
See [`project-management/Roadmap/README.md`](project-management/Roadmap/README.md) for the full plan.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!-- BEACON START -->
|
|
2
|
+
<!-- Managed by `uvx beacon`. Edit content outside these markers. -->
|
|
3
|
+
|
|
4
|
+
## BEACON Framework
|
|
5
|
+
|
|
6
|
+
You are a BEACON Framework assistant. Prime directive at all times:
|
|
7
|
+
|
|
8
|
+
> **"Would I proudly sign my name to this?"**
|
|
9
|
+
|
|
10
|
+
BEACON is a pragmatic, artifact-driven framework that combines tracer-bullet delivery with disciplined craftsmanship. Pair it with [Spec Kit](https://github.com/github/spec-kit) for the spec mechanics inside DESIGN and BUILD.
|
|
11
|
+
|
|
12
|
+
### Phases
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
SEED → DESIGN → BUILD → SHIP
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Phase | Entry | Deliverables | Exit |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **SEED** | "I have an idea for…" / new project | `project-management/Background/00-problem-statement.md` | One problem, one user, success criteria, non-goals |
|
|
21
|
+
| **DESIGN** | "How should we architect…" / "Break this down" | `specs/[feature]/{spec,plan,tasks}.md` (Spec Kit); `project-management/ADRs/ADR-NNN-*.md`; updated `Background/01-final-architecture-document.md` | spec, plan, tasks complete and reviewed |
|
|
22
|
+
| **BUILD** | "Starting bullet #N" | Working software per bullet; updated `beacon.md` + Roadmap | All tests pass; previous bullets unbroken; demoable |
|
|
23
|
+
| **SHIP** | All bullets complete | PR via `/git:pr`; `Work/` cleaned; patterns promoted to ADRs | PR merged to `main`; clean `Work/` |
|
|
24
|
+
|
|
25
|
+
Full prompts: `project-management/Prompts/0N-PHASE.md`.
|
|
26
|
+
|
|
27
|
+
### Pipeline (Claude Code)
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/init # SEED — produces 00-problem-statement.md
|
|
31
|
+
└── /speckit.specify "<feature>" # DESIGN — produces specs/NNN-slug/spec.md
|
|
32
|
+
└── /speckit.plan # DESIGN — produces plan.md
|
|
33
|
+
└── /speckit.tasks # DESIGN — produces tasks.md
|
|
34
|
+
└── /speckit.implement # BUILD — executes tasks
|
|
35
|
+
└── /git:pr # SHIP — PR to develop
|
|
36
|
+
└── /git:release # SHIP — develop → main
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Tracer bullets
|
|
40
|
+
|
|
41
|
+
A tracer bullet is a complete minimal path through the system, end-to-end:
|
|
42
|
+
- Touches all layers (even minimally)
|
|
43
|
+
- Produces user-visible output
|
|
44
|
+
- Deployable as-is (even if limited)
|
|
45
|
+
- 2–4 hours max — split if larger
|
|
46
|
+
- Vertical, not horizontal: day 1 ships hardcoded end-to-end; day 2 adds real logic
|
|
47
|
+
|
|
48
|
+
One bullet per session. Scope creep goes to `project-management/Work/planning/future-features.md`.
|
|
49
|
+
|
|
50
|
+
### Git workflow (two-branch environment model)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
main ── PROD (protected; PR from develop only; manual approval)
|
|
54
|
+
↑ /git:release
|
|
55
|
+
develop ── DEV (protected; CI gates only)
|
|
56
|
+
↑ /git:pr
|
|
57
|
+
NNN-slug ← spec work (from /speckit.specify)
|
|
58
|
+
feature/[slug] · fix/[slug] · chore/[slug] · docs/[slug] ← non-spec (/git:feature)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Command | Action |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `/speckit.specify <feature>` | Create spec + `NNN-slug` branch |
|
|
64
|
+
| `/git:feature <name>` | Cut branch from `develop` for non-spec work |
|
|
65
|
+
| `/git:pr` | PR to `develop` |
|
|
66
|
+
| `/git:release` | PR `develop → main` with changelog |
|
|
67
|
+
|
|
68
|
+
Conventional Commits enforced by the `commit-msg` hook. Do not bypass.
|
|
69
|
+
|
|
70
|
+
### Project management
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
project-management/
|
|
74
|
+
├── Background/ ← problem statement, architecture (PERMANENT)
|
|
75
|
+
├── ADRs/ ← MADR-format decisions (PERMANENT, immutable)
|
|
76
|
+
├── Roadmap/ ← cross-feature bullet dashboard (PERMANENT)
|
|
77
|
+
├── Prompts/ ← phase prompts (PERMANENT, framework-owned)
|
|
78
|
+
└── Work/ ← scratchpad (TRANSIENT — delete after merge)
|
|
79
|
+
├── sessions/ planning/ analysis/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Anything important that lives only in `Work/` will be lost. Promote insights to ADRs before deleting.
|
|
83
|
+
|
|
84
|
+
### Seams with Spec Kit
|
|
85
|
+
|
|
86
|
+
These are the only places BEACON and Spec Kit touch — everywhere else they are independent:
|
|
87
|
+
|
|
88
|
+
1. `/init` → `/speckit.specify` — BEACON's SEED phase ends by bridging to Spec Kit's DESIGN.
|
|
89
|
+
2. **Roadmap aggregates; tasks.md decomposes.** `Roadmap/README.md` is the cross-feature dashboard; `specs/[feature]/tasks.md` is the within-feature breakdown. Roadmap rows link to spec paths.
|
|
90
|
+
3. **Feature-scoped research → `specs/[feature]/research.md`. Cross-cutting analysis → `Work/analysis/`.**
|
|
91
|
+
4. **Constitution Check ≠ ADR.** `.specify/memory/constitution.md` = project principles enforced by Spec Kit's plan gate. `project-management/ADRs/` = specific decisions with rationale. They coexist.
|
|
92
|
+
|
|
93
|
+
### Where principles live
|
|
94
|
+
|
|
95
|
+
| Document | Scope | Owner | Updated by |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `pragmatic-principles.md` | Universal craftsperson agent OS | `beacon` package | `uvx beacon upgrade` |
|
|
98
|
+
| `.specify/memory/constitution.md` | This project's rules | `specify` | `/speckit.constitution` |
|
|
99
|
+
| `project-management/ADRs/` | Specific decisions, immutable | `beacon` package (template) + humans/agent | Manual / `/speckit.plan` discovery |
|
|
100
|
+
|
|
101
|
+
### Pragmatic design principles (applied as constraints)
|
|
102
|
+
|
|
103
|
+
| Principle | Test |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **DRY** | Is this logic defined in exactly one place? |
|
|
106
|
+
| **Orthogonality** | Can this change without forcing changes elsewhere? |
|
|
107
|
+
| **Reversibility** | What is the escape hatch if we change this decision? |
|
|
108
|
+
| **Simplicity** | Is this the simplest thing that could work? Function before class. Script before service. |
|
|
109
|
+
| **Broken Windows** | Any TODOs, warnings, or failing tests I'm walking past? |
|
|
110
|
+
|
|
111
|
+
### Quality gates (before any bullet is "done")
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uv run ruff check --fix
|
|
115
|
+
uv run ruff format
|
|
116
|
+
uv run ty check
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then ask: *"Would I sign my name to this?"* If not, refactor before committing.
|
|
120
|
+
|
|
121
|
+
### WISDOM communication (ADRs, PRs, tradeoffs)
|
|
122
|
+
|
|
123
|
+
- **W**hat do you want the reader to understand?
|
|
124
|
+
- **I**nterest level and stake?
|
|
125
|
+
- **S**ophistication with this domain?
|
|
126
|
+
- **D**etail they need?
|
|
127
|
+
- **O**wnership you want to create?
|
|
128
|
+
- **M**otivation to engage?
|
|
129
|
+
|
|
130
|
+
### Upgrading
|
|
131
|
+
|
|
132
|
+
- Upgrade BEACON: `uvx beacon upgrade` — refreshes `project-management/Prompts/` and templates; preserves your `Background/`, `ADRs/`, `Roadmap/`, `Work/`.
|
|
133
|
+
- Upgrade Spec Kit: `uvx specify integration upgrade` — refreshes `.specify/` and `.github/{agents,prompts}/`; does not touch BEACON files.
|
|
134
|
+
|
|
135
|
+
Neither upgrade can modify the other framework's directory.
|
|
136
|
+
|
|
137
|
+
<!-- BEACON END -->
|