troupe 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.
- troupe/__init__.py +3 -0
- troupe/casting/__init__.py +1 -0
- troupe/casting/names.json +126 -0
- troupe/casting/registry.py +70 -0
- troupe/casting/roles.py +135 -0
- troupe/charters/__init__.py +1 -0
- troupe/charters/compiler.py +47 -0
- troupe/cli.py +51 -0
- troupe/commands/__init__.py +1 -0
- troupe/commands/doctor.py +249 -0
- troupe/commands/init.py +64 -0
- troupe/commands/upgrade.py +38 -0
- troupe/commands/watch.py +145 -0
- troupe/governance/__init__.py +1 -0
- troupe/governance/wiring.py +91 -0
- troupe/reeve/__init__.py +26 -0
- troupe/reeve/context.py +105 -0
- troupe/reeve/cycle.py +123 -0
- troupe/reeve/poller.py +96 -0
- troupe/reeve/runner.py +179 -0
- troupe/reeve/state.py +124 -0
- troupe/scaffold.py +226 -0
- troupe/templates/agent.md +28 -0
- troupe/templates/charter.md +24 -0
- troupe/templates/decisions.md +13 -0
- troupe/templates/directives.md +7 -0
- troupe/templates/history.md +10 -0
- troupe/templates/hooks/troupe_decision_log.py +96 -0
- troupe/templates/hooks/troupe_file_guard.py +84 -0
- troupe/templates/hooks/troupe_idle_nudge.py +95 -0
- troupe/templates/hooks/troupe_pii_scrub.py +109 -0
- troupe/templates/hooks/troupe_review_gate.py +86 -0
- troupe/templates/hooks/troupe_session_context.py +120 -0
- troupe/templates/policy.json +31 -0
- troupe/templates/team.md +15 -0
- troupe/upgrade.py +103 -0
- troupe-0.1.0.dist-info/METADATA +167 -0
- troupe-0.1.0.dist-info/RECORD +41 -0
- troupe-0.1.0.dist-info/WHEEL +4 -0
- troupe-0.1.0.dist-info/entry_points.txt +2 -0
- troupe-0.1.0.dist-info/licenses/LICENSE +21 -0
troupe/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Casting — the persistent naming system that gives troupe members their identities."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"theme": "crafts",
|
|
3
|
+
"comment": "English occupational surnames. Each entry's craft gloss doubles as the charter epigraph; affinities are role ids the name maps to naturally.",
|
|
4
|
+
"names": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Wright",
|
|
7
|
+
"craft": "A wright is a master builder — the one who decides how the thing goes together before anyone cuts wood.",
|
|
8
|
+
"affinities": ["lead", "architect"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "Mason",
|
|
12
|
+
"craft": "A mason works in stone: foundations first, load-bearing walls before ornament.",
|
|
13
|
+
"affinities": ["backend"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Webster",
|
|
17
|
+
"craft": "A webster is a weaver — of cloth then, of webs now. The surface people actually touch.",
|
|
18
|
+
"affinities": ["frontend"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Sawyer",
|
|
22
|
+
"craft": "A sawyer cuts things open for a living. If it splits along a hidden flaw, better now than later.",
|
|
23
|
+
"affinities": ["tester", "qa"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Ward",
|
|
27
|
+
"craft": "A ward is a watchman — assumes every knock at the gate is hostile until proven otherwise.",
|
|
28
|
+
"affinities": ["security"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "Piper",
|
|
32
|
+
"craft": "A piper keeps the pipes flowing. If it ships, it ships through Piper's lines.",
|
|
33
|
+
"affinities": ["devops", "ci", "platform"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "Page",
|
|
37
|
+
"craft": "A page keeps the written record close at hand. If the page is wrong, the product is wrong.",
|
|
38
|
+
"affinities": ["docs", "writer"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Shepherd",
|
|
42
|
+
"craft": "A shepherd watches the flock and notices the one that wandered — long before it bleats.",
|
|
43
|
+
"affinities": ["monitor", "sre", "observability"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Potter",
|
|
47
|
+
"craft": "A potter shapes raw material into vessels that hold exactly what they're meant to hold.",
|
|
48
|
+
"affinities": ["data", "database"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Glover",
|
|
52
|
+
"craft": "A glover does fine handwork — fit and finish, measured to the hand that will wear it.",
|
|
53
|
+
"affinities": ["design", "ux"]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "Fletcher",
|
|
57
|
+
"craft": "A fletcher makes arrows fly true. Precision is the whole job.",
|
|
58
|
+
"affinities": ["tester", "qa"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "Cooper",
|
|
62
|
+
"craft": "A cooper builds barrels — containers that hold under pressure and don't leak.",
|
|
63
|
+
"affinities": ["backend", "api"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "Turner",
|
|
67
|
+
"craft": "A turner works the lathe: same piece, round and round, a little truer every pass.",
|
|
68
|
+
"affinities": ["backend", "refactoring"]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "Miller",
|
|
72
|
+
"craft": "A miller grinds grain into something usable — raw input in, refined output out.",
|
|
73
|
+
"affinities": ["data", "analytics"]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "Smith",
|
|
77
|
+
"craft": "A smith forges whatever the village needs. The original generalist.",
|
|
78
|
+
"affinities": ["backend", "generalist"]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "Harper",
|
|
82
|
+
"craft": "A harper makes the hall feel right. Tone, rhythm, and knowing when not to play.",
|
|
83
|
+
"affinities": ["frontend", "design"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "Tucker",
|
|
87
|
+
"craft": "A tucker finishes cloth — the last pass that makes rough weave presentable.",
|
|
88
|
+
"affinities": ["frontend", "polish"]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "Walker",
|
|
92
|
+
"craft": "A walker treads new cloth for hours to prove it holds. Endurance testing, literally.",
|
|
93
|
+
"affinities": ["tester", "performance"]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "Chandler",
|
|
97
|
+
"craft": "A chandler makes candles — sheds light where there wasn't any.",
|
|
98
|
+
"affinities": ["monitor", "observability", "docs"]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "Porter",
|
|
102
|
+
"craft": "A porter carries the load from where it is to where it needs to be, reliably.",
|
|
103
|
+
"affinities": ["devops", "release"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "Carter",
|
|
107
|
+
"craft": "A carter hauls freight between places that need each other — pipelines and payloads.",
|
|
108
|
+
"affinities": ["data", "integration"]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "Marshall",
|
|
112
|
+
"craft": "A marshall keeps order when everyone's moving at once.",
|
|
113
|
+
"affinities": ["lead", "security", "pm"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "Steward",
|
|
117
|
+
"craft": "A steward runs the estate: priorities, provisions, and who does what by when.",
|
|
118
|
+
"affinities": ["pm", "lead"]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "Baker",
|
|
122
|
+
"craft": "A baker follows the recipe exactly — because everyone eats the result.",
|
|
123
|
+
"affinities": ["generalist"]
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Name-pool allocation: deterministic, affinity-first, never reuses a name."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from importlib.resources import files
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CastExhaustedError(Exception):
|
|
11
|
+
"""Raised when the name pool has no unused names left."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class PoolEntry:
|
|
16
|
+
name: str
|
|
17
|
+
craft: str
|
|
18
|
+
affinities: tuple[str, ...]
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def slug(self) -> str:
|
|
22
|
+
return self.name.lower()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class CastMember:
|
|
27
|
+
entry: PoolEntry
|
|
28
|
+
role: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return self.entry.name
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def slug(self) -> str:
|
|
36
|
+
return self.entry.slug
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_pool() -> list[PoolEntry]:
|
|
40
|
+
raw = json.loads(files("troupe.casting").joinpath("names.json").read_text(encoding="utf-8"))
|
|
41
|
+
return [
|
|
42
|
+
PoolEntry(name=e["name"], craft=e["craft"], affinities=tuple(e["affinities"]))
|
|
43
|
+
for e in raw["names"]
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def allocate(roles: list[str], taken: set[str]) -> list[CastMember]:
|
|
48
|
+
"""Assign one pool name per requested role.
|
|
49
|
+
|
|
50
|
+
Allocation is deterministic: for each role (in request order), the first
|
|
51
|
+
unused pool entry whose affinities include the role wins; if none match,
|
|
52
|
+
the first unused entry of any affinity is used. `taken` holds slugs of
|
|
53
|
+
names already assigned in this project (never reallocated, even for
|
|
54
|
+
retired members).
|
|
55
|
+
"""
|
|
56
|
+
pool = load_pool()
|
|
57
|
+
used = {t.lower() for t in taken}
|
|
58
|
+
cast: list[CastMember] = []
|
|
59
|
+
for role in roles:
|
|
60
|
+
pick = next((e for e in pool if role in e.affinities and e.slug not in used), None)
|
|
61
|
+
if pick is None:
|
|
62
|
+
pick = next((e for e in pool if e.slug not in used), None)
|
|
63
|
+
if pick is None:
|
|
64
|
+
raise CastExhaustedError(
|
|
65
|
+
f"Name pool exhausted: {len(pool)} names, all assigned. "
|
|
66
|
+
f"Cannot cast a member for role '{role}'."
|
|
67
|
+
)
|
|
68
|
+
used.add(pick.slug)
|
|
69
|
+
cast.append(CastMember(entry=pick, role=role))
|
|
70
|
+
return cast
|
troupe/casting/roles.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Role catalog: what each role owns and how its charter is seeded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Role:
|
|
10
|
+
id: str
|
|
11
|
+
title: str
|
|
12
|
+
expertise: str
|
|
13
|
+
ownership: tuple[str, ...]
|
|
14
|
+
use_hint: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROLE_CATALOG: dict[str, Role] = {
|
|
18
|
+
r.id: r
|
|
19
|
+
for r in (
|
|
20
|
+
Role(
|
|
21
|
+
id="lead",
|
|
22
|
+
title="Lead",
|
|
23
|
+
expertise="Architecture, technical decisions, code review, scope control",
|
|
24
|
+
ownership=(
|
|
25
|
+
"Architectural direction and cross-cutting design decisions",
|
|
26
|
+
"Code review and quality gates",
|
|
27
|
+
"Keeping scope honest — saying no is part of the job",
|
|
28
|
+
),
|
|
29
|
+
use_hint="design decisions, reviews, and anything that spans more than one area",
|
|
30
|
+
),
|
|
31
|
+
Role(
|
|
32
|
+
id="backend",
|
|
33
|
+
title="Backend",
|
|
34
|
+
expertise="APIs, services, data models, business logic",
|
|
35
|
+
ownership=(
|
|
36
|
+
"Server-side code: endpoints, services, background jobs",
|
|
37
|
+
"Data models and their migrations",
|
|
38
|
+
"Contracts between the backend and everyone else",
|
|
39
|
+
),
|
|
40
|
+
use_hint="API, service, and data-layer work",
|
|
41
|
+
),
|
|
42
|
+
Role(
|
|
43
|
+
id="frontend",
|
|
44
|
+
title="Frontend",
|
|
45
|
+
expertise="UI components, styling, client-side state, accessibility",
|
|
46
|
+
ownership=(
|
|
47
|
+
"UI components and their styling",
|
|
48
|
+
"Client-side state and data fetching",
|
|
49
|
+
"Accessibility and responsive behavior",
|
|
50
|
+
),
|
|
51
|
+
use_hint="UI, styling, and client-side work",
|
|
52
|
+
),
|
|
53
|
+
Role(
|
|
54
|
+
id="tester",
|
|
55
|
+
title="Tester",
|
|
56
|
+
expertise="Test plans, regression coverage, edge cases, breaking things on purpose",
|
|
57
|
+
ownership=(
|
|
58
|
+
"Test plans and test code",
|
|
59
|
+
"Regression coverage for fixed bugs",
|
|
60
|
+
"Hunting the edge cases nobody wants to think about",
|
|
61
|
+
),
|
|
62
|
+
use_hint="writing tests, reviewing coverage, and probing for failure modes",
|
|
63
|
+
),
|
|
64
|
+
Role(
|
|
65
|
+
id="security",
|
|
66
|
+
title="Security",
|
|
67
|
+
expertise="AuthN/authZ, secrets handling, input validation, dependency risk",
|
|
68
|
+
ownership=(
|
|
69
|
+
"Authentication and authorization flows",
|
|
70
|
+
"Secrets handling and credential hygiene",
|
|
71
|
+
"Input validation and dependency risk",
|
|
72
|
+
),
|
|
73
|
+
use_hint="security review of auth, secrets, and untrusted input",
|
|
74
|
+
),
|
|
75
|
+
Role(
|
|
76
|
+
id="devops",
|
|
77
|
+
title="DevOps",
|
|
78
|
+
expertise="CI/CD pipelines, builds, releases, infrastructure",
|
|
79
|
+
ownership=(
|
|
80
|
+
"CI/CD pipelines and build configuration",
|
|
81
|
+
"Release and deployment mechanics",
|
|
82
|
+
"Infrastructure as code",
|
|
83
|
+
),
|
|
84
|
+
use_hint="pipeline, build, release, and infrastructure work",
|
|
85
|
+
),
|
|
86
|
+
Role(
|
|
87
|
+
id="docs",
|
|
88
|
+
title="Docs",
|
|
89
|
+
expertise="READMEs, API documentation, changelogs, developer guides",
|
|
90
|
+
ownership=(
|
|
91
|
+
"README and developer guides",
|
|
92
|
+
"API documentation",
|
|
93
|
+
"Changelogs and release notes",
|
|
94
|
+
),
|
|
95
|
+
use_hint="writing and reviewing documentation",
|
|
96
|
+
),
|
|
97
|
+
Role(
|
|
98
|
+
id="data",
|
|
99
|
+
title="Data",
|
|
100
|
+
expertise="Schemas, queries, pipelines, analytics",
|
|
101
|
+
ownership=(
|
|
102
|
+
"Database schemas and query performance",
|
|
103
|
+
"Data pipelines and transformations",
|
|
104
|
+
"Analytics and reporting correctness",
|
|
105
|
+
),
|
|
106
|
+
use_hint="schema, query, and data-pipeline work",
|
|
107
|
+
),
|
|
108
|
+
Role(
|
|
109
|
+
id="design",
|
|
110
|
+
title="Design",
|
|
111
|
+
expertise="Interaction design, visual consistency, UX review",
|
|
112
|
+
ownership=(
|
|
113
|
+
"Interaction patterns and visual consistency",
|
|
114
|
+
"UX review of user-facing changes",
|
|
115
|
+
"Design tokens and component guidelines",
|
|
116
|
+
),
|
|
117
|
+
use_hint="UX and visual-design review",
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_role(role_id: str) -> Role:
|
|
124
|
+
"""Look up a catalog role, or synthesize a generic one for unknown ids."""
|
|
125
|
+
known = ROLE_CATALOG.get(role_id)
|
|
126
|
+
if known is not None:
|
|
127
|
+
return known
|
|
128
|
+
title = role_id.replace("-", " ").replace("_", " ").title()
|
|
129
|
+
return Role(
|
|
130
|
+
id=role_id,
|
|
131
|
+
title=title,
|
|
132
|
+
expertise=f"{title} work for this project",
|
|
133
|
+
ownership=(f"{title} tasks and their quality",),
|
|
134
|
+
use_hint=f"{title.lower()} work",
|
|
135
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Charters — rendering cast-member charters and compiling them to Claude Code agent definitions."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Render cast-member files from package templates.
|
|
2
|
+
|
|
3
|
+
Three artifacts per cast member:
|
|
4
|
+
charter.md — human-editable role definition, lives in .troupe/agents/{slug}/
|
|
5
|
+
history.md — accumulated knowledge, lives next to the charter
|
|
6
|
+
{slug}.md — compiled Claude Code subagent definition, lives in .claude/agents/;
|
|
7
|
+
works as an Agent Teams teammate type and as a plain subagent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.resources import files
|
|
13
|
+
from string import Template
|
|
14
|
+
|
|
15
|
+
from troupe.casting.registry import CastMember
|
|
16
|
+
from troupe.casting.roles import resolve_role
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _template(name: str) -> Template:
|
|
20
|
+
text = files("troupe.templates").joinpath(name).read_text(encoding="utf-8")
|
|
21
|
+
return Template(text)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _member_context(member: CastMember, created_at: str) -> dict[str, str]:
|
|
25
|
+
role = resolve_role(member.role)
|
|
26
|
+
return {
|
|
27
|
+
"name": member.name,
|
|
28
|
+
"slug": member.slug,
|
|
29
|
+
"craft": member.entry.craft,
|
|
30
|
+
"role_title": role.title,
|
|
31
|
+
"expertise": role.expertise,
|
|
32
|
+
"use_hint": role.use_hint,
|
|
33
|
+
"ownership_bullets": "\n".join(f"- {item}" for item in role.ownership),
|
|
34
|
+
"created_at": created_at,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_charter(member: CastMember, created_at: str) -> str:
|
|
39
|
+
return _template("charter.md").substitute(_member_context(member, created_at))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_history(member: CastMember, created_at: str) -> str:
|
|
43
|
+
return _template("history.md").substitute(_member_context(member, created_at))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_agent_definition(member: CastMember, created_at: str) -> str:
|
|
47
|
+
return _template("agent.md").substitute(_member_context(member, created_at))
|
troupe/cli.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Troupe CLI entry point.
|
|
2
|
+
|
|
3
|
+
Subcommands (init, doctor, upgrade, watch) register on `app` as they are
|
|
4
|
+
implemented in `troupe.commands`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from troupe import __version__
|
|
12
|
+
from troupe.commands.doctor import doctor
|
|
13
|
+
from troupe.commands.init import init
|
|
14
|
+
from troupe.commands.upgrade import upgrade
|
|
15
|
+
from troupe.commands.watch import watch
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="troupe",
|
|
19
|
+
help="A persistent, governed AI team for Claude Code.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
add_completion=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
app.command()(init)
|
|
25
|
+
app.command()(doctor)
|
|
26
|
+
app.command()(upgrade)
|
|
27
|
+
app.command()(watch)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _version_callback(value: bool) -> None:
|
|
31
|
+
if value:
|
|
32
|
+
typer.echo(f"troupe {__version__}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.callback()
|
|
37
|
+
def _root(
|
|
38
|
+
version: bool = typer.Option(
|
|
39
|
+
False,
|
|
40
|
+
"--version",
|
|
41
|
+
"-V",
|
|
42
|
+
callback=_version_callback,
|
|
43
|
+
is_eager=True,
|
|
44
|
+
help="Show the troupe version and exit.",
|
|
45
|
+
),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""A persistent, governed AI team for Claude Code."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI subcommand implementations."""
|