@nidvue/teamwork 0.0.0-stage → 1.1.0

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 +329 -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,108 @@
1
+ #!/usr/bin/env python3
2
+ """Mirror this checkout's skill bundle into an installed skill directory.
3
+
4
+ Why: the repository root *is* the skill, but an agent host loads a **copy** from
5
+ its skills directory (``~/.pi/agent/skills/teamwork`` on pi, or whatever the host
6
+ names). That copy is what every agent actually reads, so the moment the source
7
+ tree changes the installed copy is stale: the host reads an old ``SKILL.md``, and
8
+ an engine file the tree needs (``config.py``, ``gitutil.py``) is simply absent,
9
+ so a shipped behaviour appears broken on the machine that ships it.
10
+
11
+ ``sync`` copies the bundle, deletes files the source no longer has (a renamed
12
+ module must not linger and shadow the new one), and reports every change.
13
+ ``--check`` writes nothing and exits ``1`` when the installed copy differs, which
14
+ is the shape a CI job or a pre-release gate needs. ``teamwork.py refresh`` uses
15
+ the same mirror and additionally fetches the remote first, so an installed skill
16
+ can follow ``main`` without a manual checkout.
17
+
18
+ Usage::
19
+
20
+ python3 scripts/sync_skill.py # sync into the pi user dir
21
+ python3 scripts/sync_skill.py --dest DIR # any host's skills dir
22
+ python3 scripts/sync_skill.py --check # verify only, non-zero on drift
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ ROOT = Path(__file__).resolve().parent.parent
32
+
33
+ # One source of truth for what a skill bundle contains: the engine module that
34
+ # also serves ``teamwork.py refresh`` and the pi package build.
35
+ sys.path.insert(0, str(ROOT / "src"))
36
+ from teamwork import bundle # noqa: E402
37
+
38
+ BUNDLE_FILES = bundle.BUNDLE_FILES
39
+ BUNDLE_TREES = bundle.BUNDLE_TREES
40
+ HOST_DEFAULTS = bundle.HOST_DEFAULTS
41
+ bundle_files = bundle.bundle_files
42
+ installed_files = bundle.installed_files
43
+ managed = bundle.managed
44
+ plan = bundle.plan
45
+
46
+
47
+ def sync(source: Path, dest: Path, *, dry_run: bool, quiet: bool = False) -> int:
48
+ """Mirror ``source`` into ``dest``; ``0`` when the copy is in sync afterwards."""
49
+ bundle.sync(source, dest, dry_run=dry_run, quiet=quiet)
50
+ return 0
51
+
52
+
53
+ def main(argv: list[str]) -> int:
54
+ parser = argparse.ArgumentParser(
55
+ prog="sync_skill.py",
56
+ description="mirror this checkout's skill bundle into an installed skills directory",
57
+ )
58
+ parser.add_argument(
59
+ "--dest",
60
+ default="",
61
+ help=f"skills directory to write ({', '.join(str(p) for p in HOST_DEFAULTS.values())})",
62
+ )
63
+ parser.add_argument(
64
+ "--host",
65
+ default="pi",
66
+ choices=sorted(HOST_DEFAULTS),
67
+ help="which host's default skills directory to use when --dest is omitted",
68
+ )
69
+ parser.add_argument(
70
+ "--source",
71
+ default="",
72
+ help="checkout to mirror (default: this script's repository root)",
73
+ )
74
+ parser.add_argument(
75
+ "--check",
76
+ action="store_true",
77
+ help="write nothing; exit 1 when the installed copy differs from the checkout",
78
+ )
79
+ args = parser.parse_args(argv)
80
+
81
+ source = Path(args.source).resolve() if args.source else ROOT
82
+ dest = bundle.resolve_dest(args.host, args.dest)
83
+ added, updated, removed = plan(source, dest)
84
+ if args.check:
85
+ if not dest.is_dir():
86
+ print(f"sync_skill: {dest} does not exist (nothing installed there)", file=sys.stderr)
87
+ return 1
88
+ if added or updated or removed:
89
+ print(
90
+ f"sync_skill: {dest} is out of sync with {source}: "
91
+ f"{len(added)} missing, {len(updated)} stale, {len(removed)} to delete",
92
+ file=sys.stderr,
93
+ )
94
+ for rel in added:
95
+ print(f" missing: {rel}", file=sys.stderr)
96
+ for rel in updated:
97
+ print(f" stale: {rel}", file=sys.stderr)
98
+ for rel in removed:
99
+ print(f" extra: {rel}", file=sys.stderr)
100
+ return 1
101
+ print(f"# {dest} is in sync with {source}")
102
+ return 0
103
+ bundle.sync(source, dest, dry_run=False)
104
+ return 0
105
+
106
+
107
+ if __name__ == "__main__":
108
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ """Compatibility shim: run the Teamwork CLI straight from a git checkout.
3
+
4
+ ``pip install .`` provides the ``teamwork`` console script and
5
+ ``python -m teamwork``; this file keeps ``python3 scripts/teamwork.py ...``
6
+ working with nothing but the standard library, which is how the skill and the
7
+ team constitutions reference the engine.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ _SRC = Path(__file__).resolve().parent.parent / "src"
16
+ if _SRC.is_dir():
17
+ # Drop this script's own directory first: it contains ``teamwork.py``, which
18
+ # would otherwise shadow the real ``teamwork`` package on import. Python puts
19
+ # the script directory on sys.path[0], so this is not optional.
20
+ _HERE = str(Path(__file__).resolve().parent)
21
+ while _HERE in sys.path:
22
+ sys.path.remove(_HERE)
23
+ sys.path.insert(0, str(_SRC))
24
+
25
+ from teamwork.cli import main # noqa: E402 (import after sys.path fix)
26
+
27
+ if __name__ == "__main__":
28
+ sys.exit(main())
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env python3
2
+ """Build, inspect and smoke-test the Teamwork release artefacts.
3
+
4
+ The single command that proves acceptance criterion AC-1 end to end:
5
+
6
+ 1. ``python -m build`` produces the sdist and the wheel in ``dist/``.
7
+ 2. The wheel is inspected with :mod:`zipfile`; the bundled skill assets
8
+ ``teamwork/data/SKILL.md`` and ``teamwork/data/templates/AGENTS.md`` must be
9
+ present, and the metadata must declare no runtime dependency.
10
+ 3. ``SHA256SUMS`` is written next to the artefacts.
11
+ 4. A throwaway virtual environment is created and the wheel installed into it
12
+ with ``--no-deps`` (the package has no dependencies by contract).
13
+ 5. The installed ``teamwork`` console script is executed:
14
+ ``teamwork --version``, ``teamwork selftest`` and a ``teamwork init`` inside a
15
+ temporary project directory.
16
+
17
+ Only the standard library is used at runtime; ``build`` is the one external
18
+ requirement (it is part of the ``dev`` extra).
19
+
20
+ Exit code 0 means the release was verified; any failure exits 1.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import hashlib
27
+ import os
28
+ import re
29
+ import subprocess
30
+ import sys
31
+ import tarfile
32
+ import tempfile
33
+ import venv
34
+ import zipfile
35
+ from collections.abc import Sequence
36
+ from pathlib import Path
37
+
38
+ REPO_ROOT = Path(__file__).resolve().parent.parent
39
+ DIST_DIR = REPO_ROOT / "dist"
40
+
41
+ #: Members that must exist inside the built wheel.
42
+ REQUIRED_WHEEL_MEMBERS = (
43
+ "teamwork/data/SKILL.md",
44
+ "teamwork/data/templates/AGENTS.md",
45
+ )
46
+
47
+
48
+ def log(message: str) -> None:
49
+ print(f"verify_release: {message}", flush=True)
50
+
51
+
52
+ def run(
53
+ command: Sequence[object],
54
+ cwd: Path = REPO_ROOT,
55
+ env: dict[str, str] | None = None,
56
+ capture: bool = False,
57
+ timeout: int = 900,
58
+ ) -> subprocess.CompletedProcess:
59
+ """Run a command, echoing it, and return the completed process."""
60
+ rendered = " ".join(str(part) for part in command)
61
+ log(f"run: {rendered}")
62
+ try:
63
+ result = subprocess.run(
64
+ [str(part) for part in command],
65
+ cwd=str(cwd),
66
+ env=env,
67
+ capture_output=capture,
68
+ text=True,
69
+ timeout=timeout,
70
+ )
71
+ except subprocess.TimeoutExpired as exc:
72
+ raise VerificationError(f"command timed out after {timeout}s: {rendered}") from exc
73
+ except OSError as exc:
74
+ raise VerificationError(f"could not run {rendered}: {exc}") from exc
75
+ if capture:
76
+ for stream in (result.stdout, result.stderr):
77
+ if stream:
78
+ print(stream, end="" if stream.endswith("\n") else "\n")
79
+ return result
80
+
81
+
82
+ def run_ok(
83
+ command: Sequence[object],
84
+ cwd: Path = REPO_ROOT,
85
+ env: dict[str, str] | None = None,
86
+ ) -> subprocess.CompletedProcess:
87
+ """Run a command and abort the whole verification when it fails."""
88
+ result = run(command, cwd=cwd, env=env, capture=True)
89
+ if result.returncode != 0:
90
+ raise VerificationError(
91
+ f"command failed with exit {result.returncode}: "
92
+ + " ".join(str(part) for part in command)
93
+ )
94
+ return result
95
+
96
+
97
+ class VerificationError(Exception):
98
+ """A release check failed; the message is user-facing."""
99
+
100
+
101
+ def build_artefacts() -> None:
102
+ """Run ``python -m build`` from the repository root."""
103
+ try:
104
+ import build # noqa: F401
105
+ except ImportError as exc: # pragma: no cover - environment-dependent
106
+ raise VerificationError(
107
+ "'build' is not installed; run `make dev` or `python -m pip install build`"
108
+ ) from exc
109
+ run_ok([sys.executable, "-m", "build"])
110
+
111
+
112
+ def project_version() -> str:
113
+ text = (REPO_ROOT / "src" / "teamwork" / "version.py").read_text(encoding="utf-8")
114
+ match = re.search(r'__version__\s*=\s*"([^"]+)"', text)
115
+ if not match:
116
+ raise VerificationError("cannot read __version__ from src/teamwork/version.py")
117
+ return match.group(1)
118
+
119
+
120
+ def find_artefacts() -> dict[str, Path]:
121
+ """Return the newest sdist and wheel *for this source tree's version*."""
122
+ version = project_version()
123
+ wheels = list(DIST_DIR.glob(f"teamwork_agents-{version}-*.whl"))
124
+ sdists = list(DIST_DIR.glob(f"teamwork_agents-{version}.tar.gz"))
125
+ if not wheels:
126
+ raise VerificationError(f"no {version} wheel found in dist/ after build")
127
+ if not sdists:
128
+ raise VerificationError(f"no {version} sdist found in dist/ after build")
129
+ newest = lambda path: path.stat().st_mtime # noqa: E731 - local key helper
130
+ return {"wheel": max(wheels, key=newest), "sdist": max(sdists, key=newest)}
131
+
132
+
133
+ def expected_bundle_members() -> list[str]:
134
+ """Every skill-bundle file the wheel must carry, derived from the checkout.
135
+
136
+ Comparing against the source directories (rather than a hand-written count)
137
+ means adding a role charter or a template can never silently stop being
138
+ shipped, and a rename cannot pass unnoticed.
139
+ """
140
+ members: list[str] = ["teamwork/data/SKILL.md"]
141
+ for folder in ("roles", "references", "templates"):
142
+ for path in sorted((REPO_ROOT / folder).rglob("*.md")):
143
+ members.append("teamwork/data/" + path.relative_to(REPO_ROOT).as_posix())
144
+ return members
145
+
146
+
147
+ def inspect_wheel(wheel: Path) -> None:
148
+ """Assert the payload and metadata contract of the wheel."""
149
+ expected = expected_bundle_members()
150
+ with zipfile.ZipFile(wheel) as archive:
151
+ names = set(archive.namelist())
152
+ for member in REQUIRED_WHEEL_MEMBERS:
153
+ if member not in names:
154
+ raise VerificationError(f"wheel is missing required member: {member}")
155
+ missing = [member for member in expected if member not in names]
156
+ if missing:
157
+ raise VerificationError(
158
+ f"wheel is missing {len(missing)} bundled skill file(s): "
159
+ + ", ".join(missing[:5])
160
+ + (" ..." if len(missing) > 5 else "")
161
+ )
162
+ if len(expected) < 20:
163
+ raise VerificationError(
164
+ f"only {len(expected)} bundle files found in the checkout; the "
165
+ "comparison would be vacuous (expected SKILL.md + roles + "
166
+ "references + templates)"
167
+ )
168
+ metadata_names = [n for n in names if n.endswith(".dist-info/METADATA")]
169
+ if not metadata_names:
170
+ raise VerificationError("wheel has no METADATA file")
171
+ metadata = archive.read(metadata_names[0]).decode("utf-8", "replace")
172
+ expected_version = project_version()
173
+ if "Name: teamwork-agents" not in metadata:
174
+ raise VerificationError("wheel metadata does not name teamwork-agents")
175
+ if f"Version: {expected_version}" not in metadata:
176
+ raise VerificationError(
177
+ f"wheel version does not match src/teamwork/version.py ({expected_version})"
178
+ )
179
+ # ``Requires-Dist`` is also emitted for optional extras; only an
180
+ # ungated requirement violates the zero-runtime-dependency contract.
181
+ runtime = [
182
+ line
183
+ for line in metadata.splitlines()
184
+ if line.startswith("Requires-Dist:") and "extra ==" not in line
185
+ ]
186
+ if runtime:
187
+ raise VerificationError(
188
+ "wheel declares runtime dependencies but the contract is zero: " + "; ".join(runtime)
189
+ )
190
+ log(f"wheel payload ok ({len(names)} entries, {len(expected)}/{len(expected)} bundle files)")
191
+ log("wheel metadata ok (no ungated Requires-Dist)")
192
+
193
+
194
+ def inspect_sdist(sdist: Path) -> None:
195
+ """Assert the sdist carries the same bundle and metadata contract."""
196
+ import tarfile
197
+
198
+ expected = set(expected_bundle_members())
199
+ expected_dirs = {name[len("teamwork/data/") :] for name in expected}
200
+ with tarfile.open(sdist, "r:gz") as archive:
201
+ names = archive.getnames()
202
+ roots = {name.split("/", 1)[0] for name in names if "/" in name}
203
+ if len(roots) != 1:
204
+ raise VerificationError(f"sdist has an unexpected root layout: {sorted(roots)[:3]}")
205
+ root = roots.pop()
206
+ present = {name[len(root) + 1 :] for name in names}
207
+ missing = sorted(expected_dirs - present)
208
+ if missing:
209
+ raise VerificationError(
210
+ f"sdist is missing {len(missing)} skill bundle file(s): " + ", ".join(missing[:5])
211
+ )
212
+ if not any(name.endswith("PKG-INFO") for name in names):
213
+ raise VerificationError("sdist has no PKG-INFO")
214
+ log(f"sdist payload ok ({len(names)} entries)")
215
+
216
+
217
+ def write_checksums(artefacts: dict[str, Path]) -> Path:
218
+ """Write ``SHA256SUMS`` next to the artefacts."""
219
+ manifest = DIST_DIR / "SHA256SUMS"
220
+ lines: list[str] = []
221
+ for path in sorted(artefacts.values()):
222
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
223
+ lines.append(f"{digest} {path.name}")
224
+ # ``Path.write_text(..., newline=...)`` is Python 3.10+ only; the project
225
+ # supports 3.9, so the file is opened directly (see ``engine.write_lf``).
226
+ with manifest.open("w", encoding="utf-8", newline="\n") as handle:
227
+ handle.write("\n".join(lines) + "\n")
228
+ log(f"wrote {manifest.relative_to(REPO_ROOT)}")
229
+ return manifest
230
+
231
+
232
+ def venv_paths(venv_dir: Path) -> tuple:
233
+ """Return ``(python, bin_dir)`` for a virtual environment."""
234
+ if os.name == "nt": # pragma: no cover - exercised on Windows CI
235
+ return venv_dir / "Scripts" / "python.exe", venv_dir / "Scripts"
236
+ return venv_dir / "bin" / "python", venv_dir / "bin"
237
+
238
+
239
+ def fresh_env() -> dict[str, str]:
240
+ """Environment without ambient Teamwork overrides.
241
+
242
+ ``TEAMWORK_ROOT`` redirects workspace commands unconditionally, so leaving
243
+ it set would make the smoke test mutate the operator's live team tree.
244
+ Every ``TEAMWORK_*`` variable is dropped; the smoke test sets only the ones
245
+ it needs. ``PYTHONPATH`` is dropped so the checkout cannot shadow the wheel.
246
+ """
247
+ env = dict(os.environ)
248
+ for key in list(env):
249
+ if key.startswith("TEAMWORK_"):
250
+ env.pop(key, None)
251
+ env.pop("PYTHONPATH", None)
252
+ return env
253
+
254
+
255
+ def smoke_test(wheel: Path, workdir: Path) -> None:
256
+ """Install the wheel in a clean venv and exercise the console script."""
257
+ venv_dir = workdir / "venv"
258
+ log(f"creating throwaway virtualenv at {venv_dir}")
259
+ venv.EnvBuilder(with_pip=True, clear=True).create(str(venv_dir))
260
+ python, bin_dir = venv_paths(venv_dir)
261
+ if not python.is_file():
262
+ raise VerificationError(f"virtualenv python not found: {python}")
263
+
264
+ env = fresh_env()
265
+ run_ok(
266
+ [python, "-m", "pip", "install", "--quiet", "--no-deps", wheel],
267
+ env=env,
268
+ )
269
+
270
+ console = bin_dir / ("teamwork.exe" if os.name == "nt" else "teamwork")
271
+ if not console.is_file():
272
+ raise VerificationError(f"console script not installed: {console}")
273
+
274
+ result = run_ok([console, "--version"], env=env)
275
+ expected = f"teamwork {project_version()}"
276
+ if result.stdout.strip() != expected:
277
+ raise VerificationError(
278
+ f"unexpected --version output: {result.stdout!r} (expected {expected!r})"
279
+ )
280
+
281
+ run_ok([console, "selftest"], env=env)
282
+
283
+ full_loop_smoke(console, workdir, env)
284
+
285
+ project = workdir / "init-project"
286
+ project.mkdir()
287
+ run_ok(
288
+ [console, "init", "--no-git", "--agents", "ceo,architect,developer-1,reviewer-1"],
289
+ cwd=project,
290
+ env=env,
291
+ )
292
+ teams = project / ".pi" / "Teamwork" / "Teams"
293
+ if not teams.is_dir() or not any(teams.iterdir()):
294
+ raise VerificationError(f"`teamwork init` created no workspace under {teams}")
295
+ log("installed-wheel smoke test ok (--version, selftest, init)")
296
+
297
+
298
+ def cli(console: Path, args: Sequence[str], cwd: Path, env: dict[str, str]) -> str:
299
+ """Run the *installed* console script and return its combined output."""
300
+ result = run([console, *args], cwd=cwd, env=env, capture=True)
301
+ if result.returncode != 0:
302
+ raise VerificationError(
303
+ f"installed `teamwork {' '.join(args)}` failed with exit "
304
+ f"{result.returncode}: {(result.stderr or result.stdout).strip()[:400]}"
305
+ )
306
+ return (result.stdout or "") + (result.stderr or "")
307
+
308
+
309
+ def full_loop_smoke(console: Path, workdir: Path, env: dict[str, str]) -> None:
310
+ """Drive the complete gate loop through the *installed wheel* (AC-10).
311
+
312
+ The acceptance suite proves the loop from the repository checkout. This
313
+ proves the same behaviour from a wheel installed into a clean virtualenv,
314
+ which is what a user actually runs.
315
+
316
+ The roster is deliberately tiny (CEO, one Developer, one Reviewer) so that
317
+ every staffed role owns a test layer and the stop condition is reachable.
318
+ """
319
+ project = workdir / "loop-project"
320
+ project.mkdir()
321
+ cli(console, ["init", "--no-git", "--agents", "ceo,developer-1,reviewer-1"], project, env)
322
+
323
+ cli(
324
+ console,
325
+ [
326
+ "criteria",
327
+ "add",
328
+ "--id",
329
+ "AC-1",
330
+ "--text",
331
+ "the release loop works",
332
+ "--owner",
333
+ "developer",
334
+ "--by",
335
+ "ceo",
336
+ ],
337
+ project,
338
+ env,
339
+ )
340
+ cli(
341
+ console,
342
+ [
343
+ "tests",
344
+ "require",
345
+ "--area",
346
+ "engine",
347
+ "--layers",
348
+ "unit,acceptance",
349
+ "--by",
350
+ "ceo",
351
+ "--reason",
352
+ "release smoke test",
353
+ ],
354
+ project,
355
+ env,
356
+ )
357
+
358
+ def add_and_run(layer: str, command: str, by: str) -> None:
359
+ out = cli(
360
+ console,
361
+ ["tests", "add", "--layer", layer, "--area", "engine", "--command", command],
362
+ project,
363
+ env,
364
+ )
365
+ match = re.search(r"(TS-\d+)", out)
366
+ if not match:
367
+ raise VerificationError(f"`tests add {layer}` printed no id: {out.strip()[:200]}")
368
+ cli(
369
+ console,
370
+ [
371
+ "tests",
372
+ "run",
373
+ match.group(1),
374
+ "--by",
375
+ by,
376
+ "--result",
377
+ "pass",
378
+ "--evidence",
379
+ f"{command} @wheel -> ok",
380
+ ],
381
+ project,
382
+ env,
383
+ )
384
+
385
+ add_and_run("goal", "teamwork criteria list", "CEO")
386
+ add_and_run("unit", "python -m pytest tests/unit", "Developer-1")
387
+ add_and_run("acceptance", "python -m pytest tests/acceptance", "Reviewer-1")
388
+
389
+ added = cli(
390
+ console,
391
+ ["task", "add", "developer", "ship the widget", "--priority", "P0"],
392
+ project,
393
+ env,
394
+ )
395
+ match = re.search(r"(T-\d+)", added)
396
+ if not match:
397
+ raise VerificationError(f"`task add` printed no id: {added.strip()[:200]}")
398
+ task_id = match.group(1)
399
+ cli(console, ["task", "set", "Developer-1", task_id, "--status", "selfcheck"], project, env)
400
+ cli(
401
+ console,
402
+ [
403
+ "selfcheck",
404
+ "developer",
405
+ task_id,
406
+ "--by",
407
+ "Developer-1",
408
+ "--note",
409
+ "re-read the diff and ran the unit layer",
410
+ "--test",
411
+ "python -m pytest tests/unit @wheel -> ok",
412
+ ],
413
+ project,
414
+ env,
415
+ )
416
+ cli(console, ["task", "set", "Developer-1", task_id, "--status", "review"], project, env)
417
+ cli(
418
+ console,
419
+ [
420
+ "review",
421
+ "developer",
422
+ task_id,
423
+ "--by",
424
+ "Reviewer-1",
425
+ "--result",
426
+ "pass",
427
+ "--evidence",
428
+ "python -m pytest tests/acceptance @wheel -> ok",
429
+ ],
430
+ project,
431
+ env,
432
+ )
433
+ cli(
434
+ console,
435
+ [
436
+ "criteria",
437
+ "verify",
438
+ "AC-1",
439
+ "--by",
440
+ "Reviewer-1",
441
+ "--evidence",
442
+ "python -m pytest tests/acceptance @wheel -> ok",
443
+ ],
444
+ project,
445
+ env,
446
+ )
447
+
448
+ stopped = run([console, "loop"], cwd=project, env=env, capture=True)
449
+ combined = (stopped.stdout or "") + (stopped.stderr or "")
450
+ if stopped.returncode != 0 or "> STOP" not in combined:
451
+ raise VerificationError(
452
+ "the installed wheel could not reach the stop condition: "
453
+ f"exit {stopped.returncode}; {combined.strip()[-400:]}"
454
+ )
455
+ log("installed-wheel full-loop smoke test ok (gate cleared, loop -> STOP)")
456
+
457
+
458
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
459
+ parser = argparse.ArgumentParser(
460
+ prog="verify_release.py",
461
+ description="Build, inspect and smoke-test the Teamwork release artefacts.",
462
+ )
463
+ parser.add_argument(
464
+ "--keep",
465
+ action="store_true",
466
+ help="keep the throwaway virtualenv and project for inspection",
467
+ )
468
+ return parser.parse_args(argv)
469
+
470
+
471
+ def main(argv: Sequence[str] | None = None) -> int:
472
+ args = parse_args(argv)
473
+ try:
474
+ build_artefacts()
475
+ artefacts = find_artefacts()
476
+ inspect_wheel(artefacts["wheel"])
477
+ inspect_sdist(artefacts["sdist"])
478
+
479
+ if args.keep:
480
+ workdir = Path(tempfile.mkdtemp(prefix="teamwork-verify-"))
481
+ try:
482
+ smoke_test(artefacts["wheel"], workdir)
483
+ finally:
484
+ log(f"kept scratch directory: {workdir}")
485
+ else:
486
+ with tempfile.TemporaryDirectory(prefix="teamwork-verify-") as tmp:
487
+ smoke_test(artefacts["wheel"], Path(tmp))
488
+ # Publish checksums only after every check passed: a failed run must not
489
+ # leave a manifest next to unverified artefacts.
490
+ write_checksums(artefacts)
491
+ except VerificationError as exc:
492
+ log(f"FAILED: {exc}")
493
+ return 1
494
+ except (OSError, ValueError, tarfile.TarError, zipfile.BadZipFile) as exc:
495
+ log(f"FAILED: {type(exc).__name__}: {exc}")
496
+ return 1
497
+ log("OK - release verified")
498
+ return 0
499
+
500
+
501
+ if __name__ == "__main__":
502
+ sys.exit(main())
@@ -0,0 +1,18 @@
1
+ """Nidvue Teamwork - an autonomous multi-agent company driven from one prompt.
2
+
3
+ The user talks only to the CEO; the CEO staffs the rest of the org and keeps the
4
+ lanes running until the goal is verified with evidence.
5
+
6
+ This package bundles both halves of the project:
7
+
8
+ * the workspace/engine (``teamwork.engine``) that every teammate runs, and
9
+ * the skill bundle (``SKILL.md``, ``roles/``, ``references/``, ``templates/``)
10
+ that describes how the team works, shipped as package data under
11
+ ``teamwork/data/`` when installed from a wheel.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .version import __version__
17
+
18
+ __all__ = ["__version__"]
@@ -0,0 +1,10 @@
1
+ """``python -m teamwork`` runs the same CLI as the installed console script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())