speccycle 1.0.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.
- speccycle/__init__.py +79 -0
- speccycle/approvals.py +156 -0
- speccycle/auth.py +678 -0
- speccycle/cli.py +678 -0
- speccycle/config_api.py +325 -0
- speccycle/console.py +42 -0
- speccycle/context.py +499 -0
- speccycle/data/en/agents/behavior.md +37 -0
- speccycle/data/en/agents/blueprint.md +42 -0
- speccycle/data/en/agents/breakdown.md +30 -0
- speccycle/data/en/agents/build.md +36 -0
- speccycle/data/en/agents/discovery.md +53 -0
- speccycle/data/en/agents/foundation.md +22 -0
- speccycle/data/en/agents/intent.md +34 -0
- speccycle/data/en/agents/learning.md +28 -0
- speccycle/data/en/agents/mentor.md +14 -0
- speccycle/data/en/agents/orchestrator.md +12 -0
- speccycle/data/en/agents/quality.md +35 -0
- speccycle/data/en/checkpoints/ready-for-behavior.md +1 -0
- speccycle/data/en/checkpoints/ready-for-blueprint.md +3 -0
- speccycle/data/en/checkpoints/ready-for-breakdown.md +2 -0
- speccycle/data/en/checkpoints/ready-for-build.md +2 -0
- speccycle/data/en/checkpoints/ready-for-intent.md +4 -0
- speccycle/data/en/checkpoints/ready-for-quality.md +2 -0
- speccycle/data/en/checkpoints/ready-for-release.md +6 -0
- speccycle/data/en/commands/behavior.md +17 -0
- speccycle/data/en/commands/blueprint.md +17 -0
- speccycle/data/en/commands/breakdown.md +17 -0
- speccycle/data/en/commands/build.md +17 -0
- speccycle/data/en/commands/discovery.md +17 -0
- speccycle/data/en/commands/foundation.md +16 -0
- speccycle/data/en/commands/intent.md +17 -0
- speccycle/data/en/commands/learning.md +16 -0
- speccycle/data/en/commands/quality.md +17 -0
- speccycle/data/en/commands/scycle.index.md +18 -0
- speccycle/data/en/commands/scycle.mentor.md +16 -0
- speccycle/data/en/commands/scycle.new.md +14 -0
- speccycle/data/en/templates/behaviors-template.md +9 -0
- speccycle/data/en/templates/blueprint-template.md +15 -0
- speccycle/data/en/templates/breakdown-template.md +9 -0
- speccycle/data/en/templates/build-log-template.md +5 -0
- speccycle/data/en/templates/discovery-template.md +15 -0
- speccycle/data/en/templates/feature-template.feature +7 -0
- speccycle/data/en/templates/foundation-template.md +38 -0
- speccycle/data/en/templates/increment-template.md +7 -0
- speccycle/data/en/templates/intent-template.md +10 -0
- speccycle/data/en/templates/learnings-template.md +7 -0
- speccycle/data/en/templates/quality-report-template.md +9 -0
- speccycle/data/en/workflows/0-foundation.md +9 -0
- speccycle/data/en/workflows/1-discovery.md +9 -0
- speccycle/data/en/workflows/2-intent.md +8 -0
- speccycle/data/en/workflows/3-behavior.md +7 -0
- speccycle/data/en/workflows/4-blueprint.md +7 -0
- speccycle/data/en/workflows/5-breakdown.md +8 -0
- speccycle/data/en/workflows/6-build.md +9 -0
- speccycle/data/en/workflows/7-quality.md +10 -0
- speccycle/data/en/workflows/8-learning.md +7 -0
- speccycle/data/pt/agents/behavior.md +42 -0
- speccycle/data/pt/agents/blueprint.md +53 -0
- speccycle/data/pt/agents/breakdown.md +42 -0
- speccycle/data/pt/agents/build.md +48 -0
- speccycle/data/pt/agents/discovery.md +58 -0
- speccycle/data/pt/agents/foundation.md +44 -0
- speccycle/data/pt/agents/intent.md +45 -0
- speccycle/data/pt/agents/learning.md +33 -0
- speccycle/data/pt/agents/mentor.md +23 -0
- speccycle/data/pt/agents/orchestrator.md +20 -0
- speccycle/data/pt/agents/quality.md +42 -0
- speccycle/data/pt/checkpoints/ready-for-behavior.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-blueprint.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-breakdown.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-build.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-intent.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-quality.md +4 -0
- speccycle/data/pt/checkpoints/ready-for-release.md +6 -0
- speccycle/data/pt/commands/behavior.md +18 -0
- speccycle/data/pt/commands/blueprint.md +18 -0
- speccycle/data/pt/commands/breakdown.md +18 -0
- speccycle/data/pt/commands/build.md +23 -0
- speccycle/data/pt/commands/discovery.md +18 -0
- speccycle/data/pt/commands/foundation.md +15 -0
- speccycle/data/pt/commands/intent.md +18 -0
- speccycle/data/pt/commands/learning.md +18 -0
- speccycle/data/pt/commands/quality.md +18 -0
- speccycle/data/pt/commands/scycle.index.md +51 -0
- speccycle/data/pt/commands/scycle.mentor.md +17 -0
- speccycle/data/pt/commands/scycle.new.md +14 -0
- speccycle/data/pt/templates/behaviors-template.md +9 -0
- speccycle/data/pt/templates/blueprint-template.md +20 -0
- speccycle/data/pt/templates/breakdown-template.md +6 -0
- speccycle/data/pt/templates/build-log-template.md +4 -0
- speccycle/data/pt/templates/discovery-template.md +17 -0
- speccycle/data/pt/templates/feature-template.feature +7 -0
- speccycle/data/pt/templates/foundation-template.md +38 -0
- speccycle/data/pt/templates/increment-template.md +9 -0
- speccycle/data/pt/templates/intent-template.md +15 -0
- speccycle/data/pt/templates/learnings-template.md +9 -0
- speccycle/data/pt/templates/quality-report-template.md +12 -0
- speccycle/data/pt/workflows/0-foundation.md +9 -0
- speccycle/data/pt/workflows/1-discovery.md +8 -0
- speccycle/data/pt/workflows/2-intent.md +6 -0
- speccycle/data/pt/workflows/3-behavior.md +7 -0
- speccycle/data/pt/workflows/4-blueprint.md +8 -0
- speccycle/data/pt/workflows/5-breakdown.md +6 -0
- speccycle/data/pt/workflows/6-build.md +6 -0
- speccycle/data/pt/workflows/7-quality.md +8 -0
- speccycle/data/pt/workflows/8-learning.md +6 -0
- speccycle/design_cache.py +182 -0
- speccycle/env_check.py +73 -0
- speccycle/gates.py +755 -0
- speccycle/i18n.py +908 -0
- speccycle/i18n_web.py +484 -0
- speccycle/orchestrator/__init__.py +8 -0
- speccycle/orchestrator/antigravity_runtime.py +378 -0
- speccycle/orchestrator/claude_runtime.py +222 -0
- speccycle/orchestrator/github.py +327 -0
- speccycle/orchestrator/runtime.py +191 -0
- speccycle/paths.py +17 -0
- speccycle/progress.py +203 -0
- speccycle/project_config.py +44 -0
- speccycle/scaffold.py +2314 -0
- speccycle/server.py +1190 -0
- speccycle/sessions.py +481 -0
- speccycle/telemetry.py +957 -0
- speccycle/web/assets/brand/favicon.svg +14 -0
- speccycle/web/assets/brand/logo-dark.svg +23 -0
- speccycle/web/assets/brand/logo-light.svg +23 -0
- speccycle/web/assets/brand/mark-mono.svg +14 -0
- speccycle/web/assets/brand/mark.svg +14 -0
- speccycle/web/assets/brand/spec-cycle-logo.png +0 -0
- speccycle/web/assets/brand/tokens.css +27 -0
- speccycle/web/index.html +4954 -0
- speccycle-1.0.0.dist-info/METADATA +436 -0
- speccycle-1.0.0.dist-info/RECORD +138 -0
- speccycle-1.0.0.dist-info/WHEEL +5 -0
- speccycle-1.0.0.dist-info/entry_points.txt +2 -0
- speccycle-1.0.0.dist-info/licenses/LICENSE +100 -0
- speccycle-1.0.0.dist-info/top_level.txt +1 -0
speccycle/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
2
|
+
|
|
3
|
+
from speccycle.i18n import _
|
|
4
|
+
|
|
5
|
+
PHASES = [
|
|
6
|
+
{
|
|
7
|
+
"id": "discovery",
|
|
8
|
+
"n": 1,
|
|
9
|
+
"name": "Discovery",
|
|
10
|
+
"agent": "Discovery Agent",
|
|
11
|
+
"artifact": "discovery.md",
|
|
12
|
+
"command": "/scycle:discovery",
|
|
13
|
+
"desc": _("phase_discovery_desc")
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "intent",
|
|
17
|
+
"n": 2,
|
|
18
|
+
"name": "Intent",
|
|
19
|
+
"agent": "Intent Agent",
|
|
20
|
+
"artifact": "intent.md",
|
|
21
|
+
"command": "/scycle:intent",
|
|
22
|
+
"desc": _("phase_intent_desc")
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "behavior",
|
|
26
|
+
"n": 3,
|
|
27
|
+
"name": "Behavior",
|
|
28
|
+
"agent": "Behavior Agent",
|
|
29
|
+
"artifact": "behaviors.md",
|
|
30
|
+
"command": "/scycle:behavior",
|
|
31
|
+
"desc": _("phase_behavior_desc")
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "blueprint",
|
|
35
|
+
"n": 4,
|
|
36
|
+
"name": "Blueprint",
|
|
37
|
+
"agent": "Blueprint Agent",
|
|
38
|
+
"artifact": "blueprint.md",
|
|
39
|
+
"command": "/scycle:blueprint",
|
|
40
|
+
"desc": _("phase_blueprint_desc")
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "breakdown",
|
|
44
|
+
"n": 5,
|
|
45
|
+
"name": "Breakdown",
|
|
46
|
+
"agent": "Breakdown Agent",
|
|
47
|
+
"artifact": "breakdown.md",
|
|
48
|
+
"command": "/scycle:breakdown",
|
|
49
|
+
"desc": _("phase_breakdown_desc")
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "build",
|
|
53
|
+
"n": 6,
|
|
54
|
+
"name": "Build",
|
|
55
|
+
"agent": "Build Agent",
|
|
56
|
+
"artifact": "build-log.md",
|
|
57
|
+
"command": "/scycle:build",
|
|
58
|
+
"desc": _("phase_build_desc")
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "quality",
|
|
62
|
+
"n": 7,
|
|
63
|
+
"name": "Quality",
|
|
64
|
+
"agent": "Quality Agent",
|
|
65
|
+
"artifact": "quality-report.md",
|
|
66
|
+
"command": "/scycle:quality",
|
|
67
|
+
"desc": _("phase_quality_desc")
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "learning",
|
|
71
|
+
"n": 8,
|
|
72
|
+
"name": "Learning",
|
|
73
|
+
"agent": "Learning Agent",
|
|
74
|
+
"artifact": "learnings.md",
|
|
75
|
+
"command": "/scycle:learn",
|
|
76
|
+
"desc": _("phase_learning_desc")
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
|
speccycle/approvals.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Persistência do ``approvals.json`` da volta, segura entre processos.
|
|
2
|
+
|
|
3
|
+
Quem escreve nesse arquivo: o ``approve`` da CLI, o dashboard (aprovar gate,
|
|
4
|
+
iniciar fase), o ``prompt-gate`` ao reiniciar o Build e a sessão de Quality
|
|
5
|
+
reprovada. São processos distintos, às vezes no mesmo segundo. Toda escrita
|
|
6
|
+
passa por :func:`update_approvals`, que:
|
|
7
|
+
|
|
8
|
+
1. toma um lock do sistema operacional (``msvcrt``/``fcntl``), liberado
|
|
9
|
+
sozinho se o processo morrer — sem arquivo de lock órfão;
|
|
10
|
+
2. relê o arquivo **dentro** do lock, para nunca regravar uma leitura antiga;
|
|
11
|
+
3. grava num temporário e troca com ``os.replace``, para nenhum leitor ver um
|
|
12
|
+
arquivo pela metade;
|
|
13
|
+
4. recusa-se a sobrescrever um arquivo que não parseia — melhor falhar com
|
|
14
|
+
mensagem do que apagar aprovações.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
APPROVALS_FILE = "approvals.json"
|
|
24
|
+
LOCK_FILE = "approvals.lock"
|
|
25
|
+
LOCK_TIMEOUT = 15.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApprovalsLocked(TimeoutError):
|
|
29
|
+
"""Outro processo segurou o approvals.json por mais tempo que o limite."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ApprovalsCorrupt(ValueError):
|
|
33
|
+
"""O approvals.json existe mas não é JSON válido; não vai ser sobrescrito."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def approvals_path(cycle_path):
|
|
37
|
+
return os.path.join(cycle_path, APPROVALS_FILE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _lock_path(cycle_path):
|
|
41
|
+
# cycles/NNN-slug -> <raiz>/.speccycle/approvals.lock
|
|
42
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(cycle_path)))
|
|
43
|
+
speccycle_dir = os.path.join(project_root, ".speccycle")
|
|
44
|
+
if not os.path.isdir(speccycle_dir):
|
|
45
|
+
return os.path.join(os.path.abspath(cycle_path), "." + LOCK_FILE)
|
|
46
|
+
return os.path.join(speccycle_dir, LOCK_FILE)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
import msvcrt
|
|
51
|
+
|
|
52
|
+
def _try_lock(fd):
|
|
53
|
+
try:
|
|
54
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
55
|
+
return True
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def _unlock(fd):
|
|
60
|
+
try:
|
|
61
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
import fcntl
|
|
66
|
+
|
|
67
|
+
def _try_lock(fd):
|
|
68
|
+
try:
|
|
69
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
70
|
+
return True
|
|
71
|
+
except OSError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def _unlock(fd):
|
|
75
|
+
try:
|
|
76
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@contextlib.contextmanager
|
|
82
|
+
def locked(cycle_path, timeout=LOCK_TIMEOUT):
|
|
83
|
+
"""Lock exclusivo entre processos para o approvals.json desta volta."""
|
|
84
|
+
lock_path = _lock_path(cycle_path)
|
|
85
|
+
os.makedirs(os.path.dirname(lock_path), exist_ok=True)
|
|
86
|
+
fd = os.open(lock_path, os.O_RDWR | os.O_CREAT)
|
|
87
|
+
try:
|
|
88
|
+
deadline = time.monotonic() + timeout
|
|
89
|
+
while not _try_lock(fd):
|
|
90
|
+
if time.monotonic() > deadline:
|
|
91
|
+
raise ApprovalsLocked(
|
|
92
|
+
f"approvals.json ocupado por outro processo há mais de {timeout:.0f}s ({lock_path})"
|
|
93
|
+
)
|
|
94
|
+
time.sleep(0.02)
|
|
95
|
+
try:
|
|
96
|
+
yield
|
|
97
|
+
finally:
|
|
98
|
+
_unlock(fd)
|
|
99
|
+
finally:
|
|
100
|
+
os.close(fd)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _load(path, strict):
|
|
104
|
+
if not os.path.isfile(path):
|
|
105
|
+
return {}
|
|
106
|
+
try:
|
|
107
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
108
|
+
data = json.load(f)
|
|
109
|
+
except ValueError as e:
|
|
110
|
+
if strict:
|
|
111
|
+
raise ApprovalsCorrupt(
|
|
112
|
+
f"{path} não é JSON válido ({e}); corrija ou remova o arquivo"
|
|
113
|
+
) from e
|
|
114
|
+
return {}
|
|
115
|
+
except OSError:
|
|
116
|
+
return {}
|
|
117
|
+
return data if isinstance(data, dict) else {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def read_approvals(cycle_path):
|
|
121
|
+
"""Leitura sem lock: um arquivo inexistente ou ilegível vale ``{}``."""
|
|
122
|
+
return _load(approvals_path(cycle_path), strict=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _atomic_write(path, data):
|
|
126
|
+
tmp = f"{path}.{os.getpid()}.tmp"
|
|
127
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
128
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
129
|
+
f.flush()
|
|
130
|
+
os.fsync(f.fileno())
|
|
131
|
+
# No Windows, os.replace falha enquanto um leitor tiver o arquivo aberto;
|
|
132
|
+
# leituras são curtas, então basta insistir por alguns instantes.
|
|
133
|
+
for attempt in range(50):
|
|
134
|
+
try:
|
|
135
|
+
os.replace(tmp, path)
|
|
136
|
+
return
|
|
137
|
+
except PermissionError:
|
|
138
|
+
if attempt == 49:
|
|
139
|
+
raise
|
|
140
|
+
time.sleep(0.02)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def update_approvals(cycle_path, mutate, timeout=LOCK_TIMEOUT):
|
|
144
|
+
"""Aplica ``mutate(approvals)`` ao arquivo atual, sob lock e de forma atômica.
|
|
145
|
+
|
|
146
|
+
``mutate`` recebe o dict recém-lido e o altera no lugar (ou devolve um
|
|
147
|
+
novo dict). Devolve o conteúdo gravado.
|
|
148
|
+
"""
|
|
149
|
+
path = approvals_path(cycle_path)
|
|
150
|
+
with locked(cycle_path, timeout=timeout):
|
|
151
|
+
current = _load(path, strict=True)
|
|
152
|
+
result = mutate(current)
|
|
153
|
+
if not isinstance(result, dict):
|
|
154
|
+
result = current
|
|
155
|
+
_atomic_write(path, result)
|
|
156
|
+
return result
|