devflow-cli 2.3.0__py3-none-any.whl → 2.9.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.
- devflow_cli/__init__.py +27 -2
- devflow_cli/adapters/__init__.py +19 -0
- devflow_cli/adapters/agents.py +220 -0
- devflow_cli/adapters/linear.py +79 -0
- devflow_cli/cli.py +20 -0
- devflow_cli/commands/adaptive.py +357 -0
- devflow_cli/commands/assess.py +115 -0
- devflow_cli/commands/check.py +44 -1
- devflow_cli/commands/docs_sync.py +33 -0
- devflow_cli/commands/extension.py +3 -1
- devflow_cli/commands/feature.py +4 -4
- devflow_cli/commands/hooks_cmd.py +193 -0
- devflow_cli/commands/init_cmd.py +16 -10
- devflow_cli/commands/linear.py +30 -0
- devflow_cli/commands/migrate_speckit.py +139 -22
- devflow_cli/commands/review.py +54 -0
- devflow_cli/commands/run.py +123 -0
- devflow_cli/commands/status.py +24 -29
- devflow_cli/commands/step.py +118 -0
- devflow_cli/commands/traceability.py +47 -0
- devflow_cli/commands/verify.py +78 -0
- devflow_cli/core/activity_contracts.py +735 -0
- devflow_cli/core/adaptive_runner.py +332 -0
- devflow_cli/core/adaptive_workflow.py +303 -0
- devflow_cli/core/assessment.py +161 -0
- devflow_cli/core/automatic_evidence.py +717 -0
- devflow_cli/core/docgen.py +93 -0
- devflow_cli/core/hooks.py +143 -2
- devflow_cli/core/orchestrator.py +170 -0
- devflow_cli/core/remediation.py +155 -0
- devflow_cli/core/state.py +207 -6
- devflow_cli/core/state_store.py +45 -0
- devflow_cli/core/traceability.py +81 -0
- devflow_cli/core/validators.py +49 -5
- devflow_cli/core/verification.py +339 -0
- devflow_cli/core/waivers.py +285 -0
- devflow_cli/core/worklog.py +304 -0
- devflow_cli/core/worktree_snapshot.py +91 -0
- devflow_cli/schemas/state.schema.json +39 -0
- devflow_cli/utils/paths.py +78 -8
- devflow_cli-2.9.0.dist-info/METADATA +223 -0
- devflow_cli-2.9.0.dist-info/RECORD +63 -0
- {devflow_cli-2.3.0.dist-info → devflow_cli-2.9.0.dist-info}/WHEEL +1 -1
- devflow_cli-2.3.0.dist-info/METADATA +0 -141
- devflow_cli-2.3.0.dist-info/RECORD +0 -35
- {devflow_cli-2.3.0.dist-info → devflow_cli-2.9.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-2.3.0.dist-info → devflow_cli-2.9.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""devflow CLI — Spec-Driven Development workflow."""
|
|
2
2
|
|
|
3
|
-
VERSION = "2.
|
|
3
|
+
VERSION = "2.9.0"
|
|
4
|
+
STATE_SCHEMA_VERSION = 3
|
|
4
5
|
|
|
5
6
|
STEPS = [
|
|
6
7
|
"constitution", # Pré-requis projet (1/projet). Auto-complétée si .specify/memory/constitution.md existe.
|
|
@@ -20,6 +21,12 @@ STEPS = [
|
|
|
20
21
|
|
|
21
22
|
OPTIONAL_STEPS = {"clarify", "research", "contracts", "docs"}
|
|
22
23
|
|
|
24
|
+
# Le mode fast conserve les gates et les bornes du pipeline. Il compte donc
|
|
25
|
+
# 9 etapes effectives, dont 4 etapes productives principales.
|
|
26
|
+
FAST_PRODUCTIVE_STEPS = ("spec", "plan", "tasks", "implement")
|
|
27
|
+
FAST_STEP_COUNT = len(STEPS) - len(OPTIONAL_STEPS)
|
|
28
|
+
FULL_STEP_COUNT = len(STEPS)
|
|
29
|
+
|
|
23
30
|
REVIEW_GATES = {"review-spec", "review-tasks", "review-impl"}
|
|
24
31
|
|
|
25
32
|
STEP_ARTIFACTS = {
|
|
@@ -51,7 +58,21 @@ STEP_LINEAR_MAPPING = {
|
|
|
51
58
|
"done": "Done",
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
# Ordre de progression des statuts Linear (pour non-régression lors de la sync).
|
|
62
|
+
# Un statut ne doit jamais régresser : si l'index courant >= index cible, on ne met pas à jour.
|
|
63
|
+
STEP_LINEAR_ORDER = [
|
|
64
|
+
"Backlog",
|
|
65
|
+
"Spec",
|
|
66
|
+
"Research",
|
|
67
|
+
"Plan",
|
|
68
|
+
"Tasks",
|
|
69
|
+
"Review",
|
|
70
|
+
"In Progress",
|
|
71
|
+
"Doc",
|
|
72
|
+
"Done",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
|
|
55
76
|
|
|
56
77
|
# Mapping speckit → devflow pour la migration
|
|
57
78
|
SPECKIT_STEP_MAP = {
|
|
@@ -85,4 +106,8 @@ assert OPTIONAL_STEPS <= _steps_set, f"OPTIONAL_STEPS not subset of STEPS: {OPTI
|
|
|
85
106
|
assert REVIEW_GATES <= _steps_set, f"REVIEW_GATES not subset of STEPS: {REVIEW_GATES - _steps_set}"
|
|
86
107
|
assert set(STEP_ARTIFACTS.keys()) <= _steps_set, f"STEP_ARTIFACTS has unknown keys: {set(STEP_ARTIFACTS.keys()) - _steps_set}"
|
|
87
108
|
assert set(STEP_LINEAR_MAPPING.keys()) <= _steps_set, f"STEP_LINEAR_MAPPING has unknown keys: {set(STEP_LINEAR_MAPPING.keys()) - _steps_set}"
|
|
109
|
+
assert set(STEP_LINEAR_MAPPING.values()) <= set(STEP_LINEAR_ORDER), (
|
|
110
|
+
f"STEP_LINEAR_MAPPING values not in STEP_LINEAR_ORDER: "
|
|
111
|
+
f"{set(STEP_LINEAR_MAPPING.values()) - set(STEP_LINEAR_ORDER)}"
|
|
112
|
+
)
|
|
88
113
|
assert set(SPECKIT_STEP_MAP.values()) <= _steps_set, f"SPECKIT_STEP_MAP maps to unknown steps: {set(SPECKIT_STEP_MAP.values()) - _steps_set}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Adaptateurs des services externes utilisés par devflow."""
|
|
2
|
+
|
|
3
|
+
from devflow_cli.adapters.agents import (
|
|
4
|
+
AgentAdapter,
|
|
5
|
+
AgentExecution,
|
|
6
|
+
ClaudeCodeAdapter,
|
|
7
|
+
CodexAdapter,
|
|
8
|
+
CursorAdapter,
|
|
9
|
+
get_agent_adapter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AgentAdapter",
|
|
14
|
+
"AgentExecution",
|
|
15
|
+
"ClaudeCodeAdapter",
|
|
16
|
+
"CodexAdapter",
|
|
17
|
+
"CursorAdapter",
|
|
18
|
+
"get_agent_adapter",
|
|
19
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Adaptateurs d'exécution pour les agents IA en mode non interactif."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import re
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PermissionMode = Literal["auto", "acceptEdits", "manual"]
|
|
14
|
+
SandboxMode = Literal["read-only", "workspace-write"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AgentExecution:
|
|
19
|
+
returncode: int
|
|
20
|
+
stdout: str = ""
|
|
21
|
+
stderr: str = ""
|
|
22
|
+
timed_out: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentAdapter(ABC):
|
|
26
|
+
"""Contrat minimal d'un agent pilotable par le CLI."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
executable: str
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
33
|
+
"""Construit une commande sans passer par un shell."""
|
|
34
|
+
|
|
35
|
+
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
36
|
+
raise ValueError(f"L'adaptateur {self.name} ne supporte pas les agents nommés")
|
|
37
|
+
|
|
38
|
+
def build_structured_command(
|
|
39
|
+
self,
|
|
40
|
+
prompt: str,
|
|
41
|
+
permission_mode: PermissionMode,
|
|
42
|
+
schema_file: Path,
|
|
43
|
+
output_file: Path,
|
|
44
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
raise ValueError(f"L'adaptateur {self.name} ne supporte pas les sorties structurees")
|
|
47
|
+
|
|
48
|
+
def execute(
|
|
49
|
+
self,
|
|
50
|
+
prompt: str,
|
|
51
|
+
cwd: Path,
|
|
52
|
+
timeout: int,
|
|
53
|
+
permission_mode: PermissionMode = "auto",
|
|
54
|
+
*,
|
|
55
|
+
capture_output: bool = False,
|
|
56
|
+
) -> AgentExecution:
|
|
57
|
+
command = self.build_command(prompt, permission_mode)
|
|
58
|
+
try:
|
|
59
|
+
result = subprocess.run(
|
|
60
|
+
command,
|
|
61
|
+
cwd=cwd,
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
text=True,
|
|
64
|
+
capture_output=capture_output,
|
|
65
|
+
shell=False,
|
|
66
|
+
)
|
|
67
|
+
return AgentExecution(
|
|
68
|
+
result.returncode,
|
|
69
|
+
result.stdout or "",
|
|
70
|
+
result.stderr or "",
|
|
71
|
+
)
|
|
72
|
+
except subprocess.TimeoutExpired as exc:
|
|
73
|
+
return AgentExecution(
|
|
74
|
+
124,
|
|
75
|
+
_text(exc.stdout),
|
|
76
|
+
_text(exc.stderr),
|
|
77
|
+
timed_out=True,
|
|
78
|
+
)
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable")
|
|
81
|
+
|
|
82
|
+
def execute_structured(
|
|
83
|
+
self,
|
|
84
|
+
prompt: str,
|
|
85
|
+
cwd: Path,
|
|
86
|
+
timeout: int,
|
|
87
|
+
schema_file: Path,
|
|
88
|
+
output_file: Path,
|
|
89
|
+
permission_mode: PermissionMode = "auto",
|
|
90
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
91
|
+
) -> AgentExecution:
|
|
92
|
+
command = self.build_structured_command(
|
|
93
|
+
prompt, permission_mode, schema_file, output_file, sandbox_mode
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
command,
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
text=True,
|
|
101
|
+
capture_output=True,
|
|
102
|
+
shell=False,
|
|
103
|
+
)
|
|
104
|
+
return AgentExecution(
|
|
105
|
+
result.returncode,
|
|
106
|
+
result.stdout or "",
|
|
107
|
+
result.stderr or "",
|
|
108
|
+
)
|
|
109
|
+
except subprocess.TimeoutExpired as exc:
|
|
110
|
+
return AgentExecution(
|
|
111
|
+
124,
|
|
112
|
+
_text(exc.stdout),
|
|
113
|
+
_text(exc.stderr),
|
|
114
|
+
timed_out=True,
|
|
115
|
+
)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ClaudeCodeAdapter(AgentAdapter):
|
|
121
|
+
name = "claude-code"
|
|
122
|
+
executable = "claude"
|
|
123
|
+
|
|
124
|
+
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
125
|
+
return [
|
|
126
|
+
self.executable,
|
|
127
|
+
"--print",
|
|
128
|
+
"--permission-mode",
|
|
129
|
+
permission_mode,
|
|
130
|
+
prompt,
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
134
|
+
return [self.executable, "--print", "--agent", agent_name, prompt]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CodexAdapter(AgentAdapter):
|
|
138
|
+
name = "codex"
|
|
139
|
+
executable = "codex"
|
|
140
|
+
|
|
141
|
+
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
142
|
+
command = [self.executable, "exec"]
|
|
143
|
+
if permission_mode in {"auto", "acceptEdits"}:
|
|
144
|
+
command.append("--approve-for-me")
|
|
145
|
+
else:
|
|
146
|
+
command.extend(["--sandbox", "workspace-write"])
|
|
147
|
+
command.append(_portable_prompt(prompt))
|
|
148
|
+
return command
|
|
149
|
+
|
|
150
|
+
def build_structured_command(
|
|
151
|
+
self,
|
|
152
|
+
prompt: str,
|
|
153
|
+
permission_mode: PermissionMode,
|
|
154
|
+
schema_file: Path,
|
|
155
|
+
output_file: Path,
|
|
156
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
157
|
+
) -> list[str]:
|
|
158
|
+
command = [self.executable, "exec"]
|
|
159
|
+
if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
|
|
160
|
+
command.append("--approve-for-me")
|
|
161
|
+
else:
|
|
162
|
+
command.extend(["--sandbox", sandbox_mode])
|
|
163
|
+
command.extend([
|
|
164
|
+
"--output-schema",
|
|
165
|
+
str(schema_file),
|
|
166
|
+
"--output-last-message",
|
|
167
|
+
str(output_file),
|
|
168
|
+
])
|
|
169
|
+
command.append(prompt)
|
|
170
|
+
return command
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class CursorAdapter(AgentAdapter):
|
|
174
|
+
name = "cursor"
|
|
175
|
+
executable = "cursor-agent"
|
|
176
|
+
|
|
177
|
+
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
178
|
+
command = [self.executable, "--print", "--output-format", "text"]
|
|
179
|
+
if permission_mode in {"auto", "acceptEdits"}:
|
|
180
|
+
command.append("--force")
|
|
181
|
+
command.append(_portable_prompt(prompt))
|
|
182
|
+
return command
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_AGENT_ADAPTERS = {
|
|
186
|
+
"claude": ClaudeCodeAdapter,
|
|
187
|
+
"claude-code": ClaudeCodeAdapter,
|
|
188
|
+
"codex": CodexAdapter,
|
|
189
|
+
"cursor": CursorAdapter,
|
|
190
|
+
"cursor-agent": CursorAdapter,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_agent_adapter(name: str) -> AgentAdapter:
|
|
195
|
+
try:
|
|
196
|
+
adapter_type = _AGENT_ADAPTERS[name.casefold()]
|
|
197
|
+
except KeyError as exc:
|
|
198
|
+
supported = ", ".join(sorted({kind().name for kind in _AGENT_ADAPTERS.values()}))
|
|
199
|
+
raise ValueError(f"Agent inconnu '{name}'. Agents supportés: {supported}") from exc
|
|
200
|
+
return adapter_type()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _portable_prompt(prompt: str) -> str:
|
|
204
|
+
"""Traduit une slash-command Claude en instruction portable pour les autres agents."""
|
|
205
|
+
match = re.fullmatch(r"/devflow\.([a-z0-9-]+)(?:\s+(.*))?", prompt.strip())
|
|
206
|
+
if not match:
|
|
207
|
+
return prompt
|
|
208
|
+
command_name, arguments = match.groups()
|
|
209
|
+
arguments = arguments or ""
|
|
210
|
+
return (
|
|
211
|
+
f"Exécute strictement le workflow décrit dans "
|
|
212
|
+
f"commands/devflow.{command_name}.md avec $ARGUMENTS = {arguments!r}. "
|
|
213
|
+
"Travaille dans le dépôt courant et respecte toutes les validations de ce fichier."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _text(value: str | bytes | None) -> str:
|
|
218
|
+
if value is None:
|
|
219
|
+
return ""
|
|
220
|
+
return value.decode(errors="replace") if isinstance(value, bytes) else value
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Politique de synchronisation Linear, indépendante du transport MCP/API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Protocol
|
|
7
|
+
|
|
8
|
+
from devflow_cli import STEP_LINEAR_MAPPING, STEP_LINEAR_ORDER
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
LinearAction = Literal["update", "keep", "skip"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LinearAdapter(Protocol):
|
|
15
|
+
"""Transport minimal requis pour synchroniser le statut d'une issue."""
|
|
16
|
+
|
|
17
|
+
def get_status(self, issue_id: str) -> str | None: ...
|
|
18
|
+
|
|
19
|
+
def update_status(self, issue_id: str, status: str) -> None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class LinearDecision:
|
|
24
|
+
action: LinearAction
|
|
25
|
+
step: str
|
|
26
|
+
current_status: str | None
|
|
27
|
+
target_status: str | None
|
|
28
|
+
reason: str
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def should_update(self) -> bool:
|
|
32
|
+
return self.action == "update"
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, str | bool | None]:
|
|
35
|
+
return {
|
|
36
|
+
"action": self.action,
|
|
37
|
+
"step": self.step,
|
|
38
|
+
"currentStatus": self.current_status,
|
|
39
|
+
"targetStatus": self.target_status,
|
|
40
|
+
"shouldUpdate": self.should_update,
|
|
41
|
+
"reason": self.reason,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def decide_linear_transition(step: str, current_status: str | None) -> LinearDecision:
|
|
46
|
+
"""Applique le mapping et la règle de non-régression canonique."""
|
|
47
|
+
target = STEP_LINEAR_MAPPING.get(step)
|
|
48
|
+
if target is None:
|
|
49
|
+
return LinearDecision("skip", step, current_status, None, "Étape sans statut Linear")
|
|
50
|
+
if current_status not in STEP_LINEAR_ORDER:
|
|
51
|
+
return LinearDecision(
|
|
52
|
+
"skip",
|
|
53
|
+
step,
|
|
54
|
+
current_status,
|
|
55
|
+
target,
|
|
56
|
+
"Statut Linear courant absent de l'ordre canonique",
|
|
57
|
+
)
|
|
58
|
+
current_index = STEP_LINEAR_ORDER.index(current_status)
|
|
59
|
+
target_index = STEP_LINEAR_ORDER.index(target)
|
|
60
|
+
if current_index >= target_index:
|
|
61
|
+
return LinearDecision(
|
|
62
|
+
"keep",
|
|
63
|
+
step,
|
|
64
|
+
current_status,
|
|
65
|
+
target,
|
|
66
|
+
"Statut courant identique ou plus avancé",
|
|
67
|
+
)
|
|
68
|
+
return LinearDecision("update", step, current_status, target, "Progression autorisée")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sync_linear_status(
|
|
72
|
+
adapter: LinearAdapter, issue_id: str, step: str
|
|
73
|
+
) -> LinearDecision:
|
|
74
|
+
"""Synchronise via un transport injecté lorsque la politique l'autorise."""
|
|
75
|
+
current_status = adapter.get_status(issue_id)
|
|
76
|
+
decision = decide_linear_transition(step, current_status)
|
|
77
|
+
if decision.should_update and decision.target_status is not None:
|
|
78
|
+
adapter.update_status(issue_id, decision.target_status)
|
|
79
|
+
return decision
|
devflow_cli/cli.py
CHANGED
|
@@ -18,6 +18,16 @@ from devflow_cli.commands.upgrade_cmd import upgrade
|
|
|
18
18
|
from devflow_cli.commands.rollback import rollback
|
|
19
19
|
from devflow_cli.commands.migrate_speckit import migrate_speckit
|
|
20
20
|
from devflow_cli.commands import extension
|
|
21
|
+
from devflow_cli.commands import hooks_cmd
|
|
22
|
+
from devflow_cli.commands import linear
|
|
23
|
+
from devflow_cli.commands import review
|
|
24
|
+
from devflow_cli.commands import step
|
|
25
|
+
from devflow_cli.commands import adaptive
|
|
26
|
+
from devflow_cli.commands.run import run
|
|
27
|
+
from devflow_cli.commands.docs_sync import docs_sync
|
|
28
|
+
from devflow_cli.commands.traceability import traceability
|
|
29
|
+
from devflow_cli.commands.verify import verify
|
|
30
|
+
from devflow_cli.commands.assess import assess
|
|
21
31
|
|
|
22
32
|
app = typer.Typer(
|
|
23
33
|
name="devflow",
|
|
@@ -53,6 +63,16 @@ app.command()(upgrade)
|
|
|
53
63
|
app.command()(rollback)
|
|
54
64
|
app.command(name="migrate-speckit")(migrate_speckit)
|
|
55
65
|
app.add_typer(extension.app, name="extension")
|
|
66
|
+
app.add_typer(hooks_cmd.app, name="hooks")
|
|
67
|
+
app.add_typer(linear.app, name="linear")
|
|
68
|
+
app.add_typer(step.app, name="step")
|
|
69
|
+
app.add_typer(review.app, name="review")
|
|
70
|
+
app.add_typer(adaptive.app, name="adaptive")
|
|
71
|
+
app.command()(run)
|
|
72
|
+
app.command(name="docs-sync")(docs_sync)
|
|
73
|
+
app.command()(traceability)
|
|
74
|
+
app.command()(verify)
|
|
75
|
+
app.command()(assess)
|
|
56
76
|
|
|
57
77
|
|
|
58
78
|
if __name__ == "__main__":
|