@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.
- package/CHANGELOG.md +343 -0
- package/LICENSE +661 -0
- package/NOTICE +16 -0
- package/README.md +462 -2
- package/SECURITY.md +64 -0
- package/package.json +71 -4
- package/prompts/teamwork-pulse.md +12 -0
- package/prompts/teamwork-review.md +16 -0
- package/skills/teamwork/AGENTS.md +462 -0
- package/skills/teamwork/SKILL.md +795 -0
- package/skills/teamwork/references/Adapters.md +112 -0
- package/skills/teamwork/references/Audit.md +120 -0
- package/skills/teamwork/references/Concurrency.md +160 -0
- package/skills/teamwork/references/Dissent.md +85 -0
- package/skills/teamwork/references/Docs.md +74 -0
- package/skills/teamwork/references/Engineering.md +410 -0
- package/skills/teamwork/references/Evidence.md +140 -0
- package/skills/teamwork/references/Remotes.md +144 -0
- package/skills/teamwork/references/Roles.md +201 -0
- package/skills/teamwork/references/State.md +64 -0
- package/skills/teamwork/references/Testing.md +144 -0
- package/skills/teamwork/roles/architect.md +153 -0
- package/skills/teamwork/roles/ceo.md +218 -0
- package/skills/teamwork/roles/cfo.md +150 -0
- package/skills/teamwork/roles/coo.md +147 -0
- package/skills/teamwork/roles/cto.md +162 -0
- package/skills/teamwork/roles/data-engineer.md +120 -0
- package/skills/teamwork/roles/designer.md +349 -0
- package/skills/teamwork/roles/developer.md +166 -0
- package/skills/teamwork/roles/product-manager.md +151 -0
- package/skills/teamwork/roles/project-manager.md +159 -0
- package/skills/teamwork/roles/release-engineer.md +245 -0
- package/skills/teamwork/roles/reviewer.md +254 -0
- package/skills/teamwork/roles/security-auditor.md +157 -0
- package/skills/teamwork/roles/technical-writer.md +121 -0
- package/skills/teamwork/roles/tester.md +180 -0
- package/skills/teamwork/scripts/build_pi_package.py +200 -0
- package/skills/teamwork/scripts/privacy_scan.py +1185 -0
- package/skills/teamwork/scripts/sync_skill.py +108 -0
- package/skills/teamwork/scripts/teamwork.py +28 -0
- package/skills/teamwork/scripts/verify_release.py +502 -0
- package/skills/teamwork/src/teamwork/__init__.py +18 -0
- package/skills/teamwork/src/teamwork/__main__.py +10 -0
- package/skills/teamwork/src/teamwork/bundle.py +157 -0
- package/skills/teamwork/src/teamwork/cli.py +16 -0
- package/skills/teamwork/src/teamwork/config.py +396 -0
- package/skills/teamwork/src/teamwork/engine.py +10119 -0
- package/skills/teamwork/src/teamwork/gitutil.py +172 -0
- package/skills/teamwork/src/teamwork/imaging.py +124 -0
- package/skills/teamwork/src/teamwork/io.py +144 -0
- package/skills/teamwork/src/teamwork/layout.py +253 -0
- package/skills/teamwork/src/teamwork/names.py +191 -0
- package/skills/teamwork/src/teamwork/portable.py +79 -0
- package/skills/teamwork/src/teamwork/records.py +1220 -0
- package/skills/teamwork/src/teamwork/resources.py +120 -0
- package/skills/teamwork/src/teamwork/version.py +13 -0
- package/skills/teamwork/templates/AGENTS.md +460 -0
- package/skills/teamwork/templates/Docs/Architecture.md +40 -0
- package/skills/teamwork/templates/Docs/Archive.md +8 -0
- package/skills/teamwork/templates/Docs/Board.md +28 -0
- package/skills/teamwork/templates/Docs/Costs.md +30 -0
- package/skills/teamwork/templates/Docs/Decisions.md +26 -0
- package/skills/teamwork/templates/Docs/Design.md +106 -0
- package/skills/teamwork/templates/Docs/Plan.md +24 -0
- package/skills/teamwork/templates/Docs/Project.md +40 -0
- package/skills/teamwork/templates/Docs/Release.md +43 -0
- package/skills/teamwork/templates/Docs/Research.md +36 -0
- package/skills/teamwork/templates/Docs/Risks.md +18 -0
- package/skills/teamwork/templates/TeamMap.md +100 -0
- 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"
|