@nidvue/teamwork 0.0.0-stage → 1.1.1

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 (70) hide show
  1. package/CHANGELOG.md +343 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +16 -0
  4. package/README.md +462 -2
  5. package/SECURITY.md +64 -0
  6. package/package.json +71 -4
  7. package/prompts/teamwork-pulse.md +12 -0
  8. package/prompts/teamwork-review.md +16 -0
  9. package/skills/teamwork/AGENTS.md +462 -0
  10. package/skills/teamwork/SKILL.md +795 -0
  11. package/skills/teamwork/references/Adapters.md +112 -0
  12. package/skills/teamwork/references/Audit.md +120 -0
  13. package/skills/teamwork/references/Concurrency.md +160 -0
  14. package/skills/teamwork/references/Dissent.md +85 -0
  15. package/skills/teamwork/references/Docs.md +74 -0
  16. package/skills/teamwork/references/Engineering.md +410 -0
  17. package/skills/teamwork/references/Evidence.md +140 -0
  18. package/skills/teamwork/references/Remotes.md +144 -0
  19. package/skills/teamwork/references/Roles.md +201 -0
  20. package/skills/teamwork/references/State.md +64 -0
  21. package/skills/teamwork/references/Testing.md +144 -0
  22. package/skills/teamwork/roles/architect.md +153 -0
  23. package/skills/teamwork/roles/ceo.md +218 -0
  24. package/skills/teamwork/roles/cfo.md +150 -0
  25. package/skills/teamwork/roles/coo.md +147 -0
  26. package/skills/teamwork/roles/cto.md +162 -0
  27. package/skills/teamwork/roles/data-engineer.md +120 -0
  28. package/skills/teamwork/roles/designer.md +349 -0
  29. package/skills/teamwork/roles/developer.md +166 -0
  30. package/skills/teamwork/roles/product-manager.md +151 -0
  31. package/skills/teamwork/roles/project-manager.md +159 -0
  32. package/skills/teamwork/roles/release-engineer.md +245 -0
  33. package/skills/teamwork/roles/reviewer.md +254 -0
  34. package/skills/teamwork/roles/security-auditor.md +157 -0
  35. package/skills/teamwork/roles/technical-writer.md +121 -0
  36. package/skills/teamwork/roles/tester.md +180 -0
  37. package/skills/teamwork/scripts/build_pi_package.py +200 -0
  38. package/skills/teamwork/scripts/privacy_scan.py +1185 -0
  39. package/skills/teamwork/scripts/sync_skill.py +108 -0
  40. package/skills/teamwork/scripts/teamwork.py +28 -0
  41. package/skills/teamwork/scripts/verify_release.py +502 -0
  42. package/skills/teamwork/src/teamwork/__init__.py +18 -0
  43. package/skills/teamwork/src/teamwork/__main__.py +10 -0
  44. package/skills/teamwork/src/teamwork/bundle.py +157 -0
  45. package/skills/teamwork/src/teamwork/cli.py +16 -0
  46. package/skills/teamwork/src/teamwork/config.py +396 -0
  47. package/skills/teamwork/src/teamwork/engine.py +10119 -0
  48. package/skills/teamwork/src/teamwork/gitutil.py +172 -0
  49. package/skills/teamwork/src/teamwork/imaging.py +124 -0
  50. package/skills/teamwork/src/teamwork/io.py +144 -0
  51. package/skills/teamwork/src/teamwork/layout.py +253 -0
  52. package/skills/teamwork/src/teamwork/names.py +191 -0
  53. package/skills/teamwork/src/teamwork/portable.py +79 -0
  54. package/skills/teamwork/src/teamwork/records.py +1220 -0
  55. package/skills/teamwork/src/teamwork/resources.py +120 -0
  56. package/skills/teamwork/src/teamwork/version.py +13 -0
  57. package/skills/teamwork/templates/AGENTS.md +460 -0
  58. package/skills/teamwork/templates/Docs/Architecture.md +40 -0
  59. package/skills/teamwork/templates/Docs/Archive.md +8 -0
  60. package/skills/teamwork/templates/Docs/Board.md +28 -0
  61. package/skills/teamwork/templates/Docs/Costs.md +30 -0
  62. package/skills/teamwork/templates/Docs/Decisions.md +26 -0
  63. package/skills/teamwork/templates/Docs/Design.md +106 -0
  64. package/skills/teamwork/templates/Docs/Plan.md +24 -0
  65. package/skills/teamwork/templates/Docs/Project.md +40 -0
  66. package/skills/teamwork/templates/Docs/Release.md +43 -0
  67. package/skills/teamwork/templates/Docs/Research.md +36 -0
  68. package/skills/teamwork/templates/Docs/Risks.md +18 -0
  69. package/skills/teamwork/templates/TeamMap.md +100 -0
  70. package/skills/teamwork-pi/SKILL.md +77 -0
@@ -0,0 +1,157 @@
1
+ """The skill bundle as a movable set of files, and the mirror that keeps a copy fresh.
2
+
3
+ The repository root *is* the skill, but an agent host loads a **copy** from its
4
+ skills directory, and a `pi`/pip install unpacks another copy. Those copies are
5
+ what an agent actually reads, so they must be replaceable byte for byte: a stale
6
+ copy is a live defect (a renamed engine module that lingers shadows the new one,
7
+ an old ``SKILL.md`` describes a command that no longer exists).
8
+
9
+ One manifest lives here - :data:`BUNDLE_FILES` and :data:`BUNDLE_TREES` - and
10
+ everything that copies the bundle uses it: the wheel build, the pi package build,
11
+ ``scripts/sync_skill.py`` and ``teamwork.py refresh``. Two copies of the list
12
+ would drift, and the drift would be invisible until a shipped file went missing.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import filecmp
18
+ import os
19
+ import shutil
20
+ from pathlib import Path
21
+
22
+ #: Files the skill needs at runtime, at the bundle root.
23
+ BUNDLE_FILES = ("SKILL.md", "AGENTS.md")
24
+
25
+ #: Trees the skill needs at runtime. Tests, docs and build output stay out.
26
+ BUNDLE_TREES = ("references", "roles", "templates", "scripts", "src")
27
+
28
+ #: Where a host scans skills from. Only a default - any directory works.
29
+ HOST_DEFAULTS = {
30
+ "pi": Path("~/.pi/agent/skills/teamwork"),
31
+ "agents": Path("~/.agents/skills/teamwork"),
32
+ }
33
+
34
+ #: Marker that identifies a directory as a skill bundle rather than a random tree.
35
+ MARKERS = ("SKILL.md", "roles", "references", "templates")
36
+
37
+ _SKIP_SUFFIXES = (".pyc", ".pyo")
38
+
39
+
40
+ def looks_like_bundle(path: Path) -> bool:
41
+ return path.is_dir() and any((path / marker).exists() for marker in MARKERS)
42
+
43
+
44
+ def _skip(path: Path) -> bool:
45
+ if "__pycache__" in path.parts or any(p.endswith(".egg-info") for p in path.parts):
46
+ return True
47
+ return path.suffix in _SKIP_SUFFIXES
48
+
49
+
50
+ def bundle_files(source: Path) -> list[Path]:
51
+ """Every file the bundle needs, relative to ``source``."""
52
+ found: list[Path] = [Path(name) for name in BUNDLE_FILES if (source / name).is_file()]
53
+ for tree in BUNDLE_TREES:
54
+ base = source / tree
55
+ if not base.is_dir():
56
+ continue
57
+ for path in sorted(base.rglob("*")):
58
+ if path.is_dir() or _skip(path.relative_to(source)):
59
+ continue
60
+ found.append(path.relative_to(source))
61
+ return found
62
+
63
+
64
+ def installed_files(dest: Path) -> list[Path]:
65
+ """Every file currently in ``dest``, relative to it."""
66
+ if not dest.is_dir():
67
+ return []
68
+ return [
69
+ path.relative_to(dest)
70
+ for path in sorted(dest.rglob("*"))
71
+ if path.is_file() and not _skip(path.relative_to(dest))
72
+ ]
73
+
74
+
75
+ def managed(rel: Path) -> bool:
76
+ """Is ``rel`` a path the mirror owns? Unmanaged files are never deleted.
77
+
78
+ Only the bundle trees are pruned. A file the host or the operator put in the
79
+ skills directory (a note, a README, a local override) is left alone: a sync
80
+ that deletes what it did not write is a data-loss bug, not a convenience.
81
+ """
82
+ return bool(rel.parts) and rel.parts[0] in BUNDLE_TREES
83
+
84
+
85
+ def plan(source: Path, dest: Path) -> tuple[list[Path], list[Path], list[Path]]:
86
+ """``(added, updated, removed)`` - what mirroring ``source`` into ``dest`` does."""
87
+ wanted = bundle_files(source)
88
+ have = set(installed_files(dest))
89
+ added, updated = [], []
90
+ for rel in wanted:
91
+ if rel not in have:
92
+ added.append(rel)
93
+ elif not filecmp.cmp(source / rel, dest / rel, shallow=False):
94
+ updated.append(rel)
95
+ removed = sorted(rel for rel in have - set(wanted) if managed(rel))
96
+ return added, updated, removed
97
+
98
+
99
+ def sync(source: Path, dest: Path, *, dry_run: bool, quiet: bool = False) -> int:
100
+ """Mirror ``source`` into ``dest``. Returns the number of files touched."""
101
+ added, updated, removed = plan(source, dest)
102
+ touched = len(added) + len(updated) + len(removed)
103
+ if not quiet:
104
+ for rel in added:
105
+ print(f" + {rel}")
106
+ for rel in updated:
107
+ print(f" ~ {rel}")
108
+ for rel in removed:
109
+ print(f" - {rel}")
110
+ if dry_run:
111
+ return touched
112
+ dest.mkdir(parents=True, exist_ok=True)
113
+ for rel in added + updated:
114
+ target = dest / rel
115
+ target.parent.mkdir(parents=True, exist_ok=True)
116
+ shutil.copy2(source / rel, target)
117
+ for rel in removed:
118
+ (dest / rel).unlink()
119
+ # A stale __pycache__ would keep importing a module the source dropped.
120
+ for path in sorted(dest.rglob("__pycache__"), reverse=True):
121
+ shutil.rmtree(path, ignore_errors=True)
122
+ for directory in sorted((p for p in dest.rglob("*") if p.is_dir()), reverse=True):
123
+ if not any(directory.iterdir()):
124
+ directory.rmdir()
125
+ if not quiet:
126
+ print(f"# {len(added)} added, {len(updated)} updated, {len(removed)} removed -> {dest}")
127
+ return touched
128
+
129
+
130
+ def resolve_dest(host: str = "pi", dest: str = "") -> Path:
131
+ """The skills directory to write: ``--dest`` first, else the host default."""
132
+ target = Path(dest).expanduser() if dest else HOST_DEFAULTS.get(host, HOST_DEFAULTS["pi"])
133
+ target = target.expanduser()
134
+ if not target.is_absolute():
135
+ target = (Path.cwd() / target).resolve()
136
+ return target
137
+
138
+
139
+ def is_checkout(path: Path) -> bool:
140
+ """Is ``path`` a git checkout of the skill (so it can be fetched and pulled)?"""
141
+ return (path / ".git").exists() and (path / "src" / "teamwork").is_dir()
142
+
143
+
144
+ def default_mirror_path(dest: Path) -> Path:
145
+ """Where a tool-managed mirror checkout of the skill lives for ``dest``.
146
+
147
+ Next to the skills directory, hidden, so it is obvious that no operator owns
148
+ it: ``refresh`` fetches into it and mirrors into ``dest``.
149
+ """
150
+ parent = dest.parent
151
+ if parent.name == "skills":
152
+ parent = parent.parent
153
+ return parent / ".teamwork-source"
154
+
155
+
156
+ def env_source() -> str:
157
+ return os.environ.get("TEAMWORK_SOURCE", "")
@@ -0,0 +1,16 @@
1
+ """Console entry point: ``teamwork`` and ``python -m teamwork``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .engine import main as _engine_main
8
+
9
+
10
+ def main(argv=None) -> int:
11
+ """Run the Teamwork CLI and return its exit code."""
12
+ return _engine_main(argv)
13
+
14
+
15
+ if __name__ == "__main__": # pragma: no cover
16
+ sys.exit(main())
@@ -0,0 +1,396 @@
1
+ """Workspace paths, record names, caps and shared constants.
2
+
3
+ Everything here is data: changing a constant is the supported way to tune the
4
+ workspace, and no module imports the command layer to read it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ from . import resources as _resources
12
+
13
+ SKILL_DIR = _resources.skill_root()
14
+
15
+ ROLES_DIR = _resources.asset("roles")
16
+
17
+ TEMPLATES_DIR = _resources.asset("templates")
18
+
19
+ DIR_TEAM_MAP_TPL = "TeamMap.md"
20
+
21
+ TEAM_ROOT_DIR = ".pi/Teamwork"
22
+
23
+ TEAMS_SUBDIR = "Teams"
24
+
25
+ DIR_GLOBAL = "Global"
26
+
27
+ DIR_ROLES = "Roles"
28
+
29
+ DIR_AGENTS = "Agents"
30
+
31
+ DIR_RUNTIME = "Runtime"
32
+
33
+ DIR_LOCKS = "Locks"
34
+
35
+ DIR_SLOTS = "Slots"
36
+
37
+ DIR_ARTIFACTS = "Artifacts"
38
+
39
+ DIR_REPORTS = "Reports"
40
+
41
+ DIR_SCRATCH = "Scratch"
42
+
43
+ DIR_DOCS = ("docs", "Team")
44
+
45
+ F_SHORT = "ShortTermMemory.md"
46
+
47
+ F_LONG = "LongTermMemory.md"
48
+
49
+ F_MEM_ARCHIVE = "ArchivedMemory.md"
50
+
51
+ F_TASKS = "TaskQueue.md"
52
+
53
+ F_ROLE = "Role.md"
54
+
55
+ F_START = "StartHere.md"
56
+
57
+ F_WORKLOG = "WorkLog.md"
58
+
59
+ F_INBOX = "Inbox.md"
60
+
61
+ F_GLOBAL = "GlobalMemory.md"
62
+
63
+ F_GLOBAL_ARCHIVE = "GlobalMemoryArchive.md"
64
+
65
+ F_DISPUTES = "Disputes.md"
66
+
67
+ F_REDLINES = "RedLines.md"
68
+
69
+ F_MAP = "TeamMap.md"
70
+
71
+ F_REVIEWS = "Reviews.md"
72
+
73
+ F_CRITERIA = "Criteria.md"
74
+ F_TESTS = "Tests.md"
75
+ F_TESTPLAN = "TestPlan.md"
76
+
77
+ DIR_WORKTREES = "Worktrees"
78
+
79
+ F_SETTINGS = "Settings.json"
80
+
81
+ IDENTITY_DOMAIN = os.environ.get("TEAMWORK_IDENTITY_DOMAIN", "agents.nidvue.com")
82
+
83
+ TEST_LAYERS = (
84
+ "goal",
85
+ "requirements",
86
+ "plan",
87
+ "tech",
88
+ "release",
89
+ "data",
90
+ "process",
91
+ "cost",
92
+ "system",
93
+ "ui",
94
+ "docs",
95
+ "unit",
96
+ "integration",
97
+ "acceptance",
98
+ "security",
99
+ )
100
+
101
+ LAYER_OWNER = {
102
+ "goal": "ceo",
103
+ "requirements": "product-manager",
104
+ "plan": "project-manager",
105
+ "tech": "cto",
106
+ "release": "release-engineer",
107
+ "data": "data-engineer",
108
+ "process": "coo",
109
+ "cost": "cfo",
110
+ "system": "architect",
111
+ "ui": "designer",
112
+ "docs": "technical-writer",
113
+ "unit": "developer",
114
+ "integration": "tester",
115
+ "acceptance": "reviewer",
116
+ "security": "security-auditor",
117
+ }
118
+
119
+ LAYER_WHAT = {
120
+ "goal": "black-box verification that the delivered result meets the user's objective",
121
+ "requirements": "requirement coverage and traceability: every requirement maps to a passing check",
122
+ "plan": "plan integrity: dependency graph, duplicate work, milestone exit criteria",
123
+ "tech": "technology validation: toolchain and dependency upgrades, feasibility spikes, version drills",
124
+ "release": "release tests: reproducible build, CI gates, packaging, upgrade and rollback",
125
+ "data": "data tests: schema and migration correctness, backfill, backup and restore drills",
126
+ "process": "process and automation tests: the documented workflow and tooling actually run",
127
+ "cost": "cost and budget tests: metering, cap enforcement, unit-economics assertions",
128
+ "system": "architecture-conformance tests (boundaries, dependency direction, contract, ADR)",
129
+ "ui": "UI use-case tests (flows, component states, accessibility)",
130
+ "docs": "documentation tests: every documented command and claim is executed as written",
131
+ "unit": "unit tests for the code the Developer wrote",
132
+ "integration": "integration / system / end-to-end tests",
133
+ "acceptance": "acceptance tests over every role's output",
134
+ "security": "security tests (auth, input handling, secrets, red lines)",
135
+ }
136
+
137
+ DEFAULT_REQUIRED_LAYERS = ("unit", "integration", "acceptance")
138
+
139
+ ALL_LAYERS_SENTINEL = "__all__"
140
+
141
+ LAYER_CELL = {
142
+ "goal": "goal",
143
+ "requirements": "req",
144
+ "plan": "plan",
145
+ "tech": "tech",
146
+ "release": "rel",
147
+ "data": "data",
148
+ "process": "proc",
149
+ "cost": "cost",
150
+ "system": "sys",
151
+ "ui": "ui",
152
+ "docs": "docs",
153
+ "unit": "unit",
154
+ "integration": "int",
155
+ "acceptance": "acc",
156
+ "security": "sec",
157
+ }
158
+
159
+ TEST_STATUSES = ("pending", "passing", "failing", "na")
160
+
161
+ F_LEDGER = "Ledger.jsonl"
162
+
163
+ F_HEAD = "LedgerHead.json"
164
+
165
+ F_MANIFEST = "AuditManifest.json"
166
+
167
+ F_AUDITREPORT = "AuditManifest.md"
168
+
169
+ F_SKILLS = "Skills.md"
170
+
171
+ SKILLS_DIR = (".agents", "skills")
172
+
173
+ DEFAULT_PROVIDER_LIMITS = {
174
+ "ollama-cloud": 64,
175
+ "deepseek": 2500,
176
+ "openai": 64,
177
+ "anthropic": 64,
178
+ "default": 32,
179
+ }
180
+
181
+ SLOT_TTL_MINUTES = int(os.environ.get("TEAMWORK_SLOT_TTL_MINUTES", "90"))
182
+
183
+ REVIEW_FAIL_LIMIT = int(os.environ.get("TEAMWORK_REVIEW_FAIL_LIMIT", "3"))
184
+
185
+ SHORT_CAP = int(os.environ.get("TEAMWORK_SHORT_CAP", "24"))
186
+
187
+ LONG_CAP = int(os.environ.get("TEAMWORK_LONG_CAP", "64"))
188
+
189
+ ARCHIVE_CAP = int(os.environ.get("TEAMWORK_ARCHIVE_CAP", "256"))
190
+
191
+ STALE_DAYS = int(os.environ.get("TEAMWORK_STALE_DAYS", "3"))
192
+
193
+ MIN_USES = int(os.environ.get("TEAMWORK_MIN_USES", "2"))
194
+
195
+ PROMOTE_MIN_IMPORTANCE = int(os.environ.get("TEAMWORK_PROMOTE_MIN_IMPORTANCE", "2"))
196
+
197
+ DOCS = {
198
+ "Project.md": "CEO",
199
+ "Plan.md": "ProjectManager",
200
+ "Board.md": "ProjectManager",
201
+ "Decisions.md": "Architect",
202
+ "Architecture.md": "Architect",
203
+ "Research.md": "ProductManager",
204
+ "Design.md": "Designer",
205
+ "Risks.md": "COO",
206
+ "Costs.md": "CFO",
207
+ "Release.md": "Release Engineer",
208
+ "Archive.md": "CEO",
209
+ }
210
+
211
+ CODE_DIRS = ("src", "lib", "cmd", "internal", "app", "packages", "crates", "rust")
212
+
213
+ TEST_HINTS = ("test", "tests", "spec", "specs", "e2e", "integration")
214
+
215
+ GITIGNORE_LINES = (
216
+ "# --- teamwork: local working state (never commit) ---",
217
+ ".pi/",
218
+ "# --- teamwork: scratch and editor noise ---",
219
+ "*.tmp",
220
+ "*.temp",
221
+ "*.bak",
222
+ "*.orig",
223
+ "*.rej",
224
+ "*.swp",
225
+ "*~",
226
+ ".DS_Store",
227
+ "Thumbs.db",
228
+ "# --- teamwork: secrets ---",
229
+ ".env",
230
+ ".env.local",
231
+ ".env.*.local",
232
+ "*.pem",
233
+ "*.key",
234
+ "# --- teamwork: caches / build output (keep them on disk, just uncommitted) ---",
235
+ "__pycache__/",
236
+ "*.py[cod]",
237
+ ".pytest_cache/",
238
+ ".mypy_cache/",
239
+ ".ruff_cache/",
240
+ "node_modules/",
241
+ "dist/",
242
+ "build/",
243
+ "target/",
244
+ ".venv/",
245
+ "venv/",
246
+ "coverage/",
247
+ ".coverage",
248
+ )
249
+
250
+ SCRATCH_DIRS = (DIR_SCRATCH,)
251
+
252
+ SCRATCH_PATTERNS = ("*.tmp", "*.temp", "*.bak", "*.orig", "*.rej", "*.log")
253
+
254
+ MAX_ROUNDS = int(os.environ.get("TEAMWORK_MAX_DISPUTE_ROUNDS", "3"))
255
+
256
+ TRIBUNAL = ("ceo", "cto", "cfo")
257
+
258
+ # Who each role reports to. One parent per role - a role with two managers answers
259
+ # to neither - and the CEO is the only lane that reaches the user. The parent is
260
+ # the role that owns the standard the child is judged by: technology work under the
261
+ # CTO, execution and planning under the COO, product and documentation under the
262
+ # Product Manager. Three roles stay with the CEO on purpose, because their job is
263
+ # to disagree with everyone else: the Reviewer passes and merges (it must not be
264
+ # managed by a lane whose work it reviews), the Security Auditor audits the whole
265
+ # team, CTO included, and the CFO holds the budget cap that binds every lane.
266
+ # `ROLE_PARENT` is the single source of truth; the docs, the charters, the
267
+ # AGENTS.md block and the generated StartHere brief are checked against it.
268
+ ROLE_PARENT = {
269
+ "ceo": None,
270
+ "cto": "ceo",
271
+ "coo": "ceo",
272
+ "cfo": "ceo",
273
+ "product-manager": "ceo",
274
+ "reviewer": "ceo",
275
+ "security-auditor": "ceo",
276
+ "architect": "cto",
277
+ "developer": "cto",
278
+ "tester": "cto",
279
+ "release-engineer": "cto",
280
+ "data-engineer": "cto",
281
+ "project-manager": "coo",
282
+ "designer": "product-manager",
283
+ "technical-writer": "product-manager",
284
+ }
285
+
286
+ # Reporting order, parents before children: the order the tree is rendered in and
287
+ # the order an escalation walks. Kept explicit so `map`, the docs and the tests
288
+ # agree without recomputing a graph from a plain dict.
289
+ ROLE_REPORT_ORDER = (
290
+ "ceo",
291
+ "cto",
292
+ "architect",
293
+ "developer",
294
+ "tester",
295
+ "release-engineer",
296
+ "data-engineer",
297
+ "coo",
298
+ "project-manager",
299
+ "product-manager",
300
+ "designer",
301
+ "technical-writer",
302
+ "reviewer",
303
+ "security-auditor",
304
+ "cfo",
305
+ )
306
+
307
+ TRIBUNAL_MIN_SIGNERS = 2
308
+
309
+ # Code-scanning findings are a release gate, not a nice-to-have: a red alert on
310
+ # the default branch means the shipped revision has a known defect. The Release
311
+ # Engineer owns the read and the routing because it already owns repository and CI
312
+ # integrity (`references/Remotes.md`); the fix lands with whoever owns the file.
313
+ # `SCAN_ROUTE` is the file-ownership map, most specific pattern first.
314
+ SCAN_OWNER_ROLE = "release-engineer"
315
+
316
+ SCAN_ROUTE = (
317
+ (".github/**", "release-engineer"),
318
+ ("scripts/privacy_scan.py", "security-auditor"),
319
+ ("scripts/**", "developer"),
320
+ ("src/**", "developer"),
321
+ ("tests/**", "developer"),
322
+ ("docs/guide/**", "technical-writer"),
323
+ ("docs/Team/Architecture.md", "architect"),
324
+ ("docs/Team/Release.md", "release-engineer"),
325
+ ("docs/Team/AuditReport.md", "security-auditor"),
326
+ ("docs/**", "technical-writer"),
327
+ ("templates/**", "ceo"),
328
+ ("roles/**", "ceo"),
329
+ ("references/**", "ceo"),
330
+ ("README.md", "designer"),
331
+ ("assets/**", "designer"),
332
+ )
333
+
334
+ # A scan alert at or above this severity blocks the release gate.
335
+ SCAN_BLOCKING = ("error", "critical", "high")
336
+
337
+ # Roster tiers, cheapest first. A role earns a seat only if it owns a deliverable,
338
+ # a test layer and a decision right that no other role holds; everything else rides
339
+ # as a lane. `slim` is the default because every extra role is another charter, queue
340
+ # and memory set loaded at startup, and the gates do not need it: `staffed_layers`
341
+ # already restricts the matrix to staffed roles, and `required_tribunal` stays at or
342
+ # above TRIBUNAL_MIN_SIGNERS with the Reviewer as the neutral seat.
343
+ ROSTER_TIERS = {
344
+ "slim": (
345
+ "ceo",
346
+ "architect",
347
+ "developer",
348
+ "tester",
349
+ "reviewer",
350
+ ),
351
+ "standard": (
352
+ "ceo",
353
+ "architect",
354
+ "product-manager",
355
+ "project-manager",
356
+ "developer",
357
+ "tester",
358
+ "reviewer",
359
+ "release-engineer",
360
+ ),
361
+ "full": (
362
+ "ceo",
363
+ "cto",
364
+ "coo",
365
+ "cfo",
366
+ "architect",
367
+ "product-manager",
368
+ "project-manager",
369
+ "designer",
370
+ "developer",
371
+ "tester",
372
+ "reviewer",
373
+ "security-auditor",
374
+ "release-engineer",
375
+ "technical-writer",
376
+ "data-engineer",
377
+ ),
378
+ }
379
+
380
+ DEFAULT_ROSTER_TIER = "slim"
381
+
382
+ # Roles the gates cannot do without, whichever tier is chosen.
383
+ ROSTER_REQUIRED = ("ceo", "developer", "tester", "reviewer")
384
+
385
+ # Instances per role for a tier: reviewers get two so one verifies and another merges.
386
+ ROSTER_INSTANCE_COUNT = {"reviewer": 2, "developer": 1, "tester": 1}
387
+
388
+ SETTINGS_NON_PROVIDER = ("PulseMinutes", "Criteria")
389
+
390
+ DEFAULT_BASE = "main"
391
+
392
+ REVIEW_STATUSES = ("open", "approved", "changes-requested", "merged", "closed")
393
+
394
+ PRIORITY_ORDER = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
395
+
396
+ STATUSES = ("todo", "doing", "selfcheck", "review", "blocked", "done", "dropped")