@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,191 @@
1
+ """Names, roles and commit identities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from .config import IDENTITY_DOMAIN
10
+
11
+ ROLE_DISPLAY = {
12
+ "ceo": "CEO",
13
+ "cto": "CTO",
14
+ "coo": "COO",
15
+ "cfo": "CFO",
16
+ "architect": "Architect",
17
+ "product-manager": "Product Manager",
18
+ "project-manager": "Project Manager",
19
+ "designer": "Designer",
20
+ "developer": "Developer",
21
+ "tester": "Tester",
22
+ "reviewer": "Reviewer",
23
+ "security-auditor": "Security Auditor",
24
+ "release-engineer": "Release Engineer",
25
+ "technical-writer": "Technical Writer",
26
+ "data-engineer": "Data Engineer",
27
+ }
28
+
29
+ ACRONYMS = {
30
+ "ceo": "CEO",
31
+ "cto": "CTO",
32
+ "coo": "COO",
33
+ "cfo": "CFO",
34
+ "pm": "PM",
35
+ "qa": "QA",
36
+ "ui": "UI",
37
+ "ux": "UX",
38
+ "api": "API",
39
+ }
40
+
41
+ ROLE_ALIASES = {
42
+ "dev": "developer",
43
+ "developer": "developer",
44
+ "qa": "tester",
45
+ "test": "tester",
46
+ "tester": "tester",
47
+ "design": "designer",
48
+ "designer": "designer",
49
+ "arch": "architect",
50
+ "architect": "architect",
51
+ "software-architect": "architect",
52
+ "pm": "project-manager",
53
+ "project-manager": "project-manager",
54
+ "product-manager": "product-manager",
55
+ "po": "product-manager",
56
+ "ceo": "ceo",
57
+ "cto": "cto",
58
+ "rel": "release-engineer",
59
+ "release-engineer": "release-engineer",
60
+ "devops": "release-engineer",
61
+ "sre": "release-engineer",
62
+ "writer": "technical-writer",
63
+ "tech-writer": "technical-writer",
64
+ "technical-writer": "technical-writer",
65
+ "docs": "technical-writer",
66
+ "data": "data-engineer",
67
+ "data-engineer": "data-engineer",
68
+ "dba": "data-engineer",
69
+ "coo": "coo",
70
+ "cfo": "cfo",
71
+ "reviewer": "reviewer",
72
+ "security-auditor": "security-auditor",
73
+ }
74
+
75
+
76
+ def _norm_key(value: str) -> str:
77
+ return re.sub(r"[^a-z0-9]", "", (value or "").lower())
78
+
79
+
80
+ _ALIAS_NORM = {_norm_key(k): v for k, v in ROLE_ALIASES.items()}
81
+
82
+
83
+ def pascal(name: str) -> str:
84
+ """developer-2 -> Developer-2 ; product-manager -> ProductManager."""
85
+ parts = [p for p in re.split(r"[-_\s]+", (name or "").strip()) if p]
86
+ out = []
87
+ for part in parts:
88
+ low = part.lower()
89
+ if low in ACRONYMS:
90
+ out.append(ACRONYMS[low])
91
+ elif part.isdigit():
92
+ out.append(part)
93
+ else:
94
+ out.append(part[:1].upper() + part[1:])
95
+ if len(out) > 1 and out[-1].isdigit():
96
+ return "-".join(out[:-1]) + "-" + out[-1]
97
+ return "".join(out) or "Team"
98
+
99
+
100
+ def safe_component(name: str) -> str:
101
+ """A single filesystem path component derived from a user-supplied name.
102
+
103
+ ``pascal`` is a display helper and deliberately preserves characters; a
104
+ name such as ``../../escape`` must never turn into a path traversal when it
105
+ becomes a directory or a slot filename. Normal names (``Developer-1``,
106
+ ``ProductManager``) are unchanged.
107
+ """
108
+ text = re.sub(r"[^A-Za-z0-9._-]+", "-", str(name or "")).strip(".-")
109
+ return text[:80] or "Team"
110
+
111
+
112
+ def slugify(text: str, limit: int = 40) -> str:
113
+ s = re.sub(r"[^a-z0-9]+", "-", (text or "").lower()).strip("-")
114
+ return s[:limit].rstrip("-") or "work"
115
+
116
+
117
+ def one_line(value, limit: int = 0) -> str:
118
+ """Collapse every newline/run of whitespace so a value cannot inject a
119
+ second markdown metadata line (and therefore a forged gate state)."""
120
+ text = " ".join(str(value or "").split())
121
+ return text[:limit].rstrip() if limit else text
122
+
123
+
124
+ def clip(text, limit: int = 600) -> str:
125
+ """Shorten free text for a machine view: ``head... (+N chars: --full)``.
126
+
127
+ The records keep the whole value (Reviews.md, Ledger.jsonl and Archive.md are
128
+ the trust boundary and are never rewritten here); a *listing* only needs
129
+ enough to triage from, and a 10 KB findings line is what makes
130
+ ``review --queue`` or ``archive`` expensive for an agent that has to read
131
+ it. The marker states how many characters were omitted and the flag that
132
+ shows them, so a clipped view can never hide evidence silently. Text of
133
+ ``limit`` characters or fewer is returned unchanged - nothing was omitted,
134
+ so there is no marker to add.
135
+ """
136
+ value = str(text or "")
137
+ if len(value) <= limit:
138
+ return value
139
+ omitted = len(value) - limit
140
+ return f"{value[:limit]}... (+{omitted} chars: --full)"
141
+
142
+
143
+ def body_text(value) -> str:
144
+ """Body text that cannot forge a new record header.
145
+
146
+ A body line beginning with ``##`` would otherwise be parsed as the next
147
+ task/memory/dispute entry and could hide or duplicate state. The parser
148
+ already stops reading metadata at the first blank line; this closes the
149
+ remaining header-injection path while keeping the body readable.
150
+ """
151
+ lines = str(value or "").strip().splitlines()
152
+ return "\n".join((" " + line) if re.match(r"^##\s", line) else line for line in lines)
153
+
154
+
155
+ def team_slug(project: Path, explicit: str = "") -> str:
156
+ raw = explicit or os.environ.get("TEAMWORK_TEAM") or project.name
157
+ return pascal(re.sub(r"[^A-Za-z0-9_-]+", "-", raw)) or "Team"
158
+
159
+
160
+ def resolve_role(name: str) -> str:
161
+ """Map any spelling to the canonical role slug.
162
+
163
+ Accepts instance names, PascalCase directory names and aliases:
164
+ Developer-2 -> developer, ProductManager -> product-manager, dev -> developer.
165
+ """
166
+ raw = (name or "").strip()
167
+ n = _norm_key(raw)
168
+ if n in _ALIAS_NORM:
169
+ return _ALIAS_NORM[n]
170
+ n2 = re.sub(r"\d+$", "", n)
171
+ if n2 in _ALIAS_NORM:
172
+ return _ALIAS_NORM[n2]
173
+ return re.sub(r"[^a-z0-9]+", "-", raw.lower()).strip("-") or raw.lower()
174
+
175
+
176
+ def role_identity(role: str) -> tuple:
177
+ """('Developer', 'developer@agents.nidvue.com') - role based, never instance based."""
178
+ slug = resolve_role(role)
179
+ name = ROLE_DISPLAY.get(slug) or pascal(slug).replace("-", " ")
180
+ email = re.sub(r"[^a-z0-9]", "", slug) + "@" + IDENTITY_DOMAIN
181
+ return name, email
182
+
183
+
184
+ def identity_string(role: str) -> str:
185
+ name, email = role_identity(role)
186
+ return f"{name} <{email}>"
187
+
188
+
189
+ def git_identity_args(role: str) -> list:
190
+ name, email = role_identity(role)
191
+ return ["-c", f"user.name={name}", "-c", f"user.email={email}"]
@@ -0,0 +1,79 @@
1
+ """Operating-system portability helpers.
2
+
3
+ Teamwork runs on Linux, macOS and Windows. Anything that behaves differently on
4
+ those platforms lives here so it can be unit-tested in isolation and audited as a
5
+ single seam (see ``tests/unit/test_portability.py``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+
13
+ IS_WINDOWS = os.name == "nt"
14
+ IS_POSIX = os.name == "posix"
15
+
16
+
17
+ def pid_alive(pid: int) -> bool:
18
+ """Return ``True`` only when process ``pid`` is still running.
19
+
20
+ Never signals or kills the process; this is a read-only liveness probe used
21
+ to decide whether a stale lock file may be reclaimed.
22
+
23
+ * POSIX: ``os.kill(pid, 0)`` is the standard probe.
24
+ * Windows: ``os.kill`` with signal ``0`` is not a portable probe, so the
25
+ process list is queried instead. If it cannot be queried, assume the
26
+ process is alive - refusing to steal a live lock is always the safe
27
+ default.
28
+ """
29
+ if pid <= 0:
30
+ return False
31
+ if IS_WINDOWS:
32
+ return _windows_pid_alive(pid)
33
+ try:
34
+ os.kill(pid, 0)
35
+ except ProcessLookupError:
36
+ return False
37
+ except PermissionError:
38
+ # The process exists but belongs to another user.
39
+ return True
40
+ except OSError:
41
+ return True
42
+ return True
43
+
44
+
45
+ def _windows_pid_alive(pid: int) -> bool: # pragma: no cover - Windows only
46
+ try:
47
+ import ctypes
48
+ from ctypes import wintypes
49
+ except ImportError:
50
+ return True
51
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
52
+ STILL_ACTIVE = 259
53
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
54
+ handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
55
+ if not handle:
56
+ # Access denied means the process exists but is protected.
57
+ return kernel32.GetLastError() == 5
58
+ try:
59
+ code = wintypes.DWORD()
60
+ if kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
61
+ return code.value == STILL_ACTIVE
62
+ return True
63
+ finally:
64
+ kernel32.CloseHandle(handle)
65
+
66
+
67
+ def newline_for(text_mode: bool = True) -> str:
68
+ """The newline convention Teamwork writes.
69
+
70
+ Generated markdown and JSON must be byte-identical on every platform, so the
71
+ engine always writes ``\\n`` explicitly instead of relying on the platform
72
+ default (which is ``\\r\\n`` on Windows).
73
+ """
74
+ return "\n" if text_mode else ""
75
+
76
+
77
+ def python_executable() -> str:
78
+ """Absolute path to the interpreter running us (safe to embed in docs)."""
79
+ return sys.executable or "python3"