devflow-cli 2.0.0__py3-none-any.whl → 2.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """devflow CLI — Spec-Driven Development workflow."""
2
2
 
3
- VERSION = "1.0.0"
3
+ VERSION = "2.3.0"
4
4
 
5
5
  STEPS = [
6
6
  "constitution", # Pré-requis projet (1/projet). Auto-complétée si .specify/memory/constitution.md existe.
@@ -20,6 +20,8 @@ STEPS = [
20
20
 
21
21
  OPTIONAL_STEPS = {"clarify", "research", "contracts", "docs"}
22
22
 
23
+ REVIEW_GATES = {"review-spec", "review-tasks", "review-impl"}
24
+
23
25
  STEP_ARTIFACTS = {
24
26
  "spec": "spec.md",
25
27
  "clarify": "clarify-log.md",
@@ -51,6 +53,17 @@ STEP_LINEAR_MAPPING = {
51
53
 
52
54
  SUPPORTED_AGENTS = ["claude-code", "cursor"]
53
55
 
56
+ # Mapping speckit → devflow pour la migration
57
+ SPECKIT_STEP_MAP = {
58
+ "specify": "spec",
59
+ "clarify": "clarify",
60
+ "plan": "plan",
61
+ "tasks": "tasks",
62
+ "implement": "implement",
63
+ }
64
+
65
+ SPECKIT_IGNORED_STEPS = {"checklist", "analyze"}
66
+
54
67
  ARTIFACTS = [
55
68
  "spec.md",
56
69
  "clarify-log.md",
@@ -65,3 +78,11 @@ ARTIFACTS = [
65
78
  "analysis.md",
66
79
  "checklist.md",
67
80
  ]
81
+
82
+ # --- Pipeline constants coherence checks ---
83
+ _steps_set = set(STEPS)
84
+ assert OPTIONAL_STEPS <= _steps_set, f"OPTIONAL_STEPS not subset of STEPS: {OPTIONAL_STEPS - _steps_set}"
85
+ assert REVIEW_GATES <= _steps_set, f"REVIEW_GATES not subset of STEPS: {REVIEW_GATES - _steps_set}"
86
+ assert set(STEP_ARTIFACTS.keys()) <= _steps_set, f"STEP_ARTIFACTS has unknown keys: {set(STEP_ARTIFACTS.keys()) - _steps_set}"
87
+ assert set(STEP_LINEAR_MAPPING.keys()) <= _steps_set, f"STEP_LINEAR_MAPPING has unknown keys: {set(STEP_LINEAR_MAPPING.keys()) - _steps_set}"
88
+ assert set(SPECKIT_STEP_MAP.values()) <= _steps_set, f"SPECKIT_STEP_MAP maps to unknown steps: {set(SPECKIT_STEP_MAP.values()) - _steps_set}"
devflow_cli/cli.py CHANGED
@@ -5,16 +5,18 @@ from __future__ import annotations
5
5
  import typer
6
6
 
7
7
  from devflow_cli import VERSION
8
+ from devflow_cli.utils.logging import setup_logging
8
9
  from devflow_cli.commands.init_cmd import init
9
10
  from devflow_cli.commands.check import check
10
11
  from devflow_cli.commands.status import status
11
12
  from devflow_cli.commands.feature import feature
12
13
  from devflow_cli.commands.context import context
13
14
  from devflow_cli.commands.export_cmd import export
14
- from devflow_cli.commands.regen import regen
15
+ from devflow_cli.commands.stale import stale
15
16
  from devflow_cli.commands.migrate_cmd import migrate
16
17
  from devflow_cli.commands.upgrade_cmd import upgrade
17
18
  from devflow_cli.commands.rollback import rollback
19
+ from devflow_cli.commands.migrate_speckit import migrate_speckit
18
20
  from devflow_cli.commands import extension
19
21
 
20
22
  app = typer.Typer(
@@ -33,8 +35,10 @@ def version_callback(value: bool) -> None:
33
35
  @app.callback()
34
36
  def main(
35
37
  version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True, help="Affiche la version"),
38
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Active les logs dans stderr"),
36
39
  ) -> None:
37
40
  """devflow — Spec-Driven Development workflow CLI."""
41
+ setup_logging(verbose=verbose)
38
42
 
39
43
 
40
44
  app.command()(init)
@@ -43,10 +47,11 @@ app.command()(status)
43
47
  app.command()(feature)
44
48
  app.command()(context)
45
49
  app.command(name="export")(export)
46
- app.command()(regen)
50
+ app.command()(stale)
47
51
  app.command()(migrate)
48
52
  app.command()(upgrade)
49
53
  app.command()(rollback)
54
+ app.command(name="migrate-speckit")(migrate_speckit)
50
55
  app.add_typer(extension.app, name="extension")
51
56
 
52
57
 
@@ -8,6 +8,9 @@ from pathlib import Path
8
8
 
9
9
  import typer
10
10
  from devflow_cli.utils.console import console, ok, warn, fail
11
+ from devflow_cli.utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
11
14
  from devflow_cli.utils.paths import get_claude_dir
12
15
 
13
16
 
@@ -133,7 +136,7 @@ def check() -> None:
133
136
  linear_configured = True
134
137
  break
135
138
  except OSError:
136
- pass
139
+ logger.warning("Failed to read config %s", cfg_file, exc_info=True)
137
140
  if linear_configured:
138
141
  ok("Linear MCP -- configure")
139
142
  ok_count += 1
@@ -9,6 +9,9 @@ from typing import Optional
9
9
  import typer
10
10
 
11
11
  from devflow_cli.core.git import branch_exists, create_branch, checkout_branch, GitNotFoundError
12
+ from devflow_cli.utils.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
12
15
  from devflow_cli.core.state import create_state
13
16
  from devflow_cli.utils.console import console, ok, info
14
17
  from devflow_cli.utils.paths import get_specs_root, next_feature_number
@@ -28,6 +31,7 @@ def _ensure_git() -> None:
28
31
  from devflow_cli.core.git import ensure_git_available
29
32
  ensure_git_available()
30
33
  except GitNotFoundError as e:
34
+ logger.error("Git not found: %s", e, exc_info=True)
31
35
  console.print(f"[red]Erreur : {e}[/red]")
32
36
  raise typer.Exit(1)
33
37
 
@@ -8,6 +8,10 @@ from typing import Literal, Optional
8
8
 
9
9
  import typer
10
10
 
11
+ from devflow_cli.utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
11
15
  from devflow_cli import SUPPORTED_AGENTS
12
16
  from devflow_cli.core.installer import install_devflow
13
17
  from devflow_cli.core.git import is_git_repo
@@ -110,6 +114,7 @@ def init(
110
114
  save_manifest(get_claude_dir(), manifest_data)
111
115
  ok("Manifeste d'installation cree")
112
116
  except Exception:
117
+ logger.error("Failed to create manifest", exc_info=True)
113
118
  warn("Impossible de creer le manifeste d'installation")
114
119
 
115
120
  console.print()
@@ -8,6 +8,9 @@ from pathlib import Path
8
8
  import typer
9
9
 
10
10
  from devflow_cli.core.state import read_state
11
+ from devflow_cli.utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
11
14
  from devflow_cli.utils.console import console, ok, info, warn, fail, header
12
15
  from devflow_cli.utils.paths import get_specs_root, next_feature_number
13
16
  from devflow_cli.utils.strings import clean_short_name
@@ -68,6 +71,7 @@ def migrate(
68
71
  default=default_name,
69
72
  )
70
73
  except (KeyboardInterrupt, typer.Abort):
74
+ logger.warning("Migration interrupted for %s", issue_id)
71
75
  console.print(f" [yellow]Ignore (annule)[/yellow]")
72
76
  skipped += 1
73
77
  continue
@@ -0,0 +1,227 @@
1
+ """devflow migrate-speckit — Migration des projets speckit vers devflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from devflow_cli.utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ from devflow_cli.core.state import (
16
+ is_speckit_state, convert_speckit_state, write_state,
17
+ )
18
+ from devflow_cli.core.manifest import compute_file_hash
19
+ from devflow_cli.utils.console import console, ok, info, warn, header
20
+ from devflow_cli.utils.paths import get_devflow_root, get_claude_dir, get_specs_root
21
+
22
+
23
+ def migrate_speckit(
24
+ dry_run: bool = typer.Option(False, "--dry-run", help="Affiche le plan de migration sans modifier de fichiers"),
25
+ clean_speckit: bool = typer.Option(False, "--clean-speckit", help="Supprime les commandes speckit et templates non personnalises"),
26
+ ) -> None:
27
+ """Migration des projets speckit vers le workflow devflow."""
28
+ specs_root = get_specs_root()
29
+
30
+ if not specs_root.is_dir():
31
+ info("Rien a migrer : .specify/specs/ n'existe pas.")
32
+ return
33
+
34
+ # Scanner les features
35
+ features = sorted(
36
+ d for d in specs_root.iterdir()
37
+ if d.is_dir() and (d / "state.json").is_file()
38
+ )
39
+
40
+ if not features:
41
+ info("Aucune feature speckit detectee.")
42
+ if clean_speckit:
43
+ _clean_speckit(dry_run)
44
+ else:
45
+ _suggest_clean_speckit()
46
+ return
47
+
48
+ # Filtrer : seulement les speckit states
49
+ speckit_features = []
50
+ devflow_features = []
51
+ error_features = []
52
+
53
+ for feat_dir in features:
54
+ state_file = feat_dir / "state.json"
55
+ if is_speckit_state(state_file):
56
+ speckit_features.append(feat_dir)
57
+ else:
58
+ # Verifier si c'est un devflow valide ou corrompu
59
+ try:
60
+ data = json.loads(state_file.read_text(encoding="utf-8"))
61
+ if "reviewIterations" in data:
62
+ devflow_features.append(feat_dir)
63
+ else:
64
+ error_features.append((feat_dir, "Format non reconnu"))
65
+ except (json.JSONDecodeError, OSError) as e:
66
+ logger.warning("Failed to read state %s", state_file, exc_info=True)
67
+ error_features.append((feat_dir, str(e)))
68
+
69
+ if not speckit_features and not error_features:
70
+ info("Aucune feature speckit detectee.")
71
+ if devflow_features:
72
+ info(f" {len(devflow_features)} feature(s) deja au format devflow.")
73
+ if clean_speckit:
74
+ _clean_speckit(dry_run)
75
+ else:
76
+ _suggest_clean_speckit()
77
+ return
78
+
79
+ header(f"devflow migrate-speckit — {len(speckit_features)} feature(s) speckit")
80
+ console.print()
81
+
82
+ if dry_run:
83
+ console.print("[dim]Mode dry-run : aucun fichier ne sera modifie.[/dim]")
84
+ console.print()
85
+
86
+ migrated = 0
87
+
88
+ for feat_dir in speckit_features:
89
+ state_file = feat_dir / "state.json"
90
+ try:
91
+ raw_data = json.loads(state_file.read_text(encoding="utf-8"))
92
+ except (json.JSONDecodeError, OSError) as e:
93
+ logger.warning("Failed to read speckit state %s", state_file, exc_info=True)
94
+ warn(f" {feat_dir.name} : erreur lecture — {e}")
95
+ error_features.append((feat_dir, str(e)))
96
+ continue
97
+
98
+ issue_id = raw_data.get("issueId", feat_dir.name)
99
+ from_step = raw_data.get("currentStep", "?")
100
+ converted = convert_speckit_state(raw_data)
101
+ to_step = converted.currentStep
102
+
103
+ if dry_run:
104
+ console.print(f" [bold]{feat_dir.name}[/bold] ({issue_id})")
105
+ console.print(f" Etape : {from_step} → {to_step}")
106
+ console.print(f" Champs ajoutes : reviewIterations, mode, skippedSteps")
107
+ else:
108
+ # Backup
109
+ shutil.copy2(state_file, state_file.with_suffix(".json.bak"))
110
+ # Ecrire le state converti
111
+ write_state(state_file, converted)
112
+ ok(f" {feat_dir.name} ({issue_id}) : {from_step} → {to_step}")
113
+
114
+ migrated += 1
115
+
116
+ # Constitution
117
+ if not dry_run:
118
+ _ensure_constitution()
119
+
120
+ # Rapport
121
+ console.print()
122
+ console.rule("Recapitulatif")
123
+ console.print(f" Migrees : {migrated}")
124
+ if devflow_features:
125
+ console.print(f" Ignorees : {len(devflow_features)} (deja devflow)")
126
+ if error_features:
127
+ console.print(f" Erreurs : {len(error_features)}")
128
+ for feat_dir, msg in error_features:
129
+ warn(f" {feat_dir.name} : {msg}")
130
+
131
+ if dry_run:
132
+ console.print()
133
+ console.print("[dim]Mode dry-run : aucun fichier modifie.[/dim]")
134
+
135
+ # Nettoyage speckit
136
+ if clean_speckit:
137
+ _clean_speckit(dry_run)
138
+ elif not dry_run:
139
+ _suggest_clean_speckit()
140
+
141
+
142
+ def _ensure_constitution() -> None:
143
+ """Cree une constitution starter si absente."""
144
+ const_dest = Path.cwd() / ".specify" / "memory" / "constitution.md"
145
+ if const_dest.is_file():
146
+ return
147
+
148
+ try:
149
+ const_src = get_devflow_root() / "templates" / "constitution-template.md"
150
+ if const_src.is_file():
151
+ const_dest.parent.mkdir(parents=True, exist_ok=True)
152
+ shutil.copy2(const_src, const_dest)
153
+ ok("Constitution starter creee dans .specify/memory/constitution.md")
154
+ info(" Pensez a la personnaliser pour votre projet.")
155
+ else:
156
+ warn("Template constitution non trouve — creation ignoree.")
157
+ except Exception:
158
+ logger.error("Failed to create constitution starter", exc_info=True)
159
+ warn("Impossible de creer la constitution starter.")
160
+
161
+
162
+ def _clean_speckit(dry_run: bool) -> None:
163
+ """Supprime les commandes speckit et templates non personnalises."""
164
+ console.print()
165
+ header("Nettoyage speckit")
166
+ console.print()
167
+
168
+ # 1. Commandes speckit dans ~/.claude/commands/
169
+ claude_dir = get_claude_dir()
170
+ cmd_dir = claude_dir / "commands"
171
+ removed_cmds = 0
172
+ if cmd_dir.is_dir():
173
+ for f in sorted(cmd_dir.glob("speckit.*.md")):
174
+ if dry_run:
175
+ console.print(f" [dim](dry-run) Supprimerait {f.name}[/dim]")
176
+ else:
177
+ f.unlink()
178
+ console.print(f" Supprime : {f.name}")
179
+ removed_cmds += 1
180
+
181
+ # 2. Templates dans .specify/templates/
182
+ tpl_dir = Path.cwd() / ".specify" / "templates"
183
+ removed_tpls = 0
184
+ preserved_tpls = 0
185
+ if tpl_dir.is_dir():
186
+ try:
187
+ devflow_tpl_dir = get_devflow_root() / "templates"
188
+ except Exception:
189
+ logger.error("Failed to resolve devflow template dir", exc_info=True)
190
+ devflow_tpl_dir = None
191
+
192
+ for f in sorted(tpl_dir.glob("*")):
193
+ if not f.is_file():
194
+ continue
195
+ # Comparer avec le template devflow source
196
+ if devflow_tpl_dir and (devflow_tpl_dir / f.name).is_file():
197
+ src_hash = compute_file_hash(devflow_tpl_dir / f.name)
198
+ installed_hash = compute_file_hash(f)
199
+ if src_hash == installed_hash:
200
+ if dry_run:
201
+ console.print(f" [dim](dry-run) Supprimerait {f.name}[/dim]")
202
+ else:
203
+ f.unlink()
204
+ console.print(f" Supprime : {f.name}")
205
+ removed_tpls += 1
206
+ else:
207
+ warn(f" Conserve (personnalise) : {f.name}")
208
+ preserved_tpls += 1
209
+ else:
210
+ # Pas de reference → conserver par securite
211
+ warn(f" Conserve (pas de reference) : {f.name}")
212
+ preserved_tpls += 1
213
+
214
+ console.print()
215
+ console.print(f" Commandes supprimees : {removed_cmds}")
216
+ console.print(f" Templates supprimes : {removed_tpls}")
217
+ if preserved_tpls:
218
+ console.print(f" Templates conserves : {preserved_tpls}")
219
+
220
+
221
+ def _suggest_clean_speckit() -> None:
222
+ """Suggere --clean-speckit si des commandes speckit sont detectees."""
223
+ claude_dir = get_claude_dir()
224
+ cmd_dir = claude_dir / "commands"
225
+ if cmd_dir.is_dir() and list(cmd_dir.glob("speckit.*.md")):
226
+ info("Des commandes speckit sont encore presentes dans ~/.claude/commands/.")
227
+ info("Utilisez --clean-speckit pour les supprimer.")
@@ -1,4 +1,4 @@
1
- """devflow regen — Detection et regeneration en cascade des artefacts desuets."""
1
+ """devflow stale — Detection des artefacts desuets."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -14,11 +14,11 @@ from devflow_cli.utils.console import console, ok, info, header
14
14
  from devflow_cli.utils.paths import resolve_feature_dir
15
15
 
16
16
 
17
- def regen(
18
- only: Optional[str] = typer.Option(None, "--only", help="Regenerer uniquement cet artefact (plan, tasks)"),
17
+ def stale(
18
+ only: Optional[str] = typer.Option(None, "--only", help="Filtrer uniquement cet artefact (plan, tasks)"),
19
19
  json_output: bool = typer.Option(False, "--json", help="Sortie JSON pour integration Claude Code"),
20
20
  ) -> None:
21
- """Detecte et propose la regeneration des artefacts desuets."""
21
+ """Detecte les artefacts desuets et affiche les commandes de regeneration."""
22
22
  branch = current_branch()
23
23
  feature_dir = resolve_feature_dir(branch) if branch else None
24
24
  if feature_dir is None:
@@ -62,7 +62,7 @@ def regen(
62
62
  print(json.dumps(output, indent=2))
63
63
  return
64
64
 
65
- header(f"devflow regen — {feature_dir.name}")
65
+ header(f"devflow stale — {feature_dir.name}")
66
66
  console.print()
67
67
 
68
68
  if not stale_list:
@@ -80,7 +80,7 @@ def regen(
80
80
  console.print(f" {i}. {cmd}")
81
81
 
82
82
  console.print()
83
- if typer.confirm("Regenerer ?", default=False):
83
+ if typer.confirm("Voir les commandes de regeneration ?", default=False):
84
84
  console.print()
85
85
  info("Lancez les commandes suivantes dans l'ordre :")
86
86
  for s in stale_list:
@@ -6,8 +6,11 @@ import json
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
 
9
+ from devflow_cli.utils.logging import get_logger
9
10
  from devflow_cli.utils.paths import get_devflow_root, get_claude_dir
10
11
 
12
+ logger = get_logger(__name__)
13
+
11
14
 
12
15
  @dataclass
13
16
  class ExtensionInclude:
@@ -40,6 +43,7 @@ def load_catalog(path: Path, source: str = "principal") -> Catalog | None:
40
43
  try:
41
44
  data = json.loads(path.read_text(encoding="utf-8"))
42
45
  except (json.JSONDecodeError, OSError) as e:
46
+ logger.warning("Failed to load catalog %s", path, exc_info=True)
43
47
  from devflow_cli.utils.console import warn
44
48
  warn(f"Catalogue corrompu ou illisible : {path} ({type(e).__name__}: {e})")
45
49
  return None
devflow_cli/core/git.py CHANGED
@@ -5,6 +5,10 @@ from __future__ import annotations
5
5
  import subprocess
6
6
  from pathlib import Path
7
7
 
8
+ from devflow_cli.utils.logging import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
8
12
 
9
13
  class GitNotFoundError(Exception):
10
14
  """Levee quand git n'est pas installe ou pas dans le PATH."""
@@ -30,6 +34,7 @@ def ensure_git_available() -> None:
30
34
  )
31
35
  _git_checked = True
32
36
  except FileNotFoundError:
37
+ logger.error("git not found in PATH", exc_info=True)
33
38
  raise GitNotFoundError() from None
34
39
 
35
40
 
@@ -43,6 +48,7 @@ def _run(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProces
43
48
  args, capture_output=True, text=True, cwd=cwd, timeout=_GIT_TIMEOUT,
44
49
  )
45
50
  except subprocess.TimeoutExpired:
51
+ logger.warning("git command timed out: %s", args, exc_info=True)
46
52
  return subprocess.CompletedProcess(
47
53
  args, returncode=1, stdout="", stderr="git command timed out",
48
54
  )
devflow_cli/core/hooks.py CHANGED
@@ -7,6 +7,10 @@ from pathlib import Path
7
7
 
8
8
  import yaml
9
9
 
10
+ from devflow_cli.utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
10
14
 
11
15
  VALID_HOOK_POINTS = {"after_tasks", "before_implement", "after_implement"}
12
16
  VALID_HOOK_TYPES = {"shell", "claude", "agent"}
@@ -34,6 +38,7 @@ def validate_hooks_file(hooks_file: Path) -> ValidationResult:
34
38
  try:
35
39
  data = yaml.safe_load(hooks_file.read_text(encoding="utf-8"))
36
40
  except yaml.YAMLError as e:
41
+ logger.warning("Failed to parse hooks YAML %s", hooks_file, exc_info=True)
37
42
  result.errors.append(f"Erreur de syntaxe YAML : {e}")
38
43
  return result
39
44
 
@@ -9,6 +9,9 @@ from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
 
11
11
  from devflow_cli import VERSION
12
+ from devflow_cli.utils.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
12
15
 
13
16
 
14
17
  @dataclass
@@ -82,6 +85,7 @@ def load_manifest(claude_dir: Path) -> dict | None:
82
85
  try:
83
86
  return json.loads(manifest_file.read_text(encoding="utf-8"))
84
87
  except (json.JSONDecodeError, OSError) as e:
88
+ logger.warning("Failed to read manifest %s", manifest_file, exc_info=True)
85
89
  from devflow_cli.utils.console import warn
86
90
  warn(f"Manifeste corrompu ou illisible : {manifest_file} ({type(e).__name__}: {e})")
87
91
  return None
devflow_cli/core/state.py CHANGED
@@ -7,7 +7,10 @@ from dataclasses import dataclass, field, asdict
7
7
  from datetime import datetime, timezone
8
8
  from pathlib import Path
9
9
 
10
- from devflow_cli import STEPS, OPTIONAL_STEPS, STEP_ARTIFACTS
10
+ from devflow_cli import STEPS, OPTIONAL_STEPS, STEP_ARTIFACTS, SPECKIT_STEP_MAP, SPECKIT_IGNORED_STEPS, REVIEW_GATES
11
+ from devflow_cli.utils.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
11
14
 
12
15
 
13
16
  @dataclass
@@ -82,7 +85,8 @@ def read_state(state_file: Path) -> FeatureState | None:
82
85
  if k in FeatureState.__dataclass_fields__
83
86
  })
84
87
  return _validate_state(state)
85
- except (json.JSONDecodeError, TypeError, KeyError, OSError):
88
+ except (json.JSONDecodeError, TypeError, KeyError, OSError, UnicodeDecodeError):
89
+ logger.warning("Failed to read state %s", state_file, exc_info=True)
86
90
  return None
87
91
 
88
92
 
@@ -205,6 +209,10 @@ def advance_step(
205
209
  else:
206
210
  state.stepTimestamps[completed_step]["completedAt"] = now
207
211
 
212
+ # Incrementer reviewIterations pour les gates de review
213
+ if completed_step in REVIEW_GATES:
214
+ state.reviewIterations[completed_step] = state.reviewIterations.get(completed_step, 0) + 1
215
+
208
216
  # Trouver la prochaine etape non-skippee
209
217
  next_idx = idx + 1
210
218
  while next_idx < len(STEPS) and STEPS[next_idx] in state.skippedSteps:
@@ -262,6 +270,75 @@ def rollback_step(state: FeatureState) -> FeatureState | None:
262
270
  return state
263
271
 
264
272
 
273
+ def is_speckit_state(state_file: Path) -> bool:
274
+ """Detecte si un state.json est au format speckit (pas devflow).
275
+
276
+ Lit le JSON brut et verifie l'absence du champ 'reviewIterations'.
277
+ """
278
+ try:
279
+ data = json.loads(state_file.read_text(encoding="utf-8"))
280
+ return isinstance(data, dict) and "reviewIterations" not in data
281
+ except (json.JSONDecodeError, OSError):
282
+ logger.warning("Failed to detect speckit state %s", state_file, exc_info=True)
283
+ return False
284
+
285
+
286
+ def convert_speckit_state(raw_data: dict) -> FeatureState:
287
+ """Convertit un state.json speckit brut en FeatureState devflow.
288
+
289
+ - Mappe les etapes via SPECKIT_STEP_MAP (specify→spec, etc.)
290
+ - Filtre les etapes ignorees (checklist, analyze)
291
+ - Injecte les etapes devflow intermediaires comme auto-validees
292
+ si l'utilisateur les a deja depassees
293
+ """
294
+ issue_id = raw_data.get("issueId", "")
295
+
296
+ # Mapper currentStep
297
+ raw_current = raw_data.get("currentStep", "spec")
298
+ current_step = SPECKIT_STEP_MAP.get(raw_current, raw_current)
299
+
300
+ # Mapper completedSteps : convertir et filtrer
301
+ raw_completed = raw_data.get("completedSteps", [])
302
+ mapped_completed: list[str] = []
303
+ for step in raw_completed:
304
+ if step in SPECKIT_IGNORED_STEPS:
305
+ continue
306
+ mapped = SPECKIT_STEP_MAP.get(step, step)
307
+ if mapped in STEPS and mapped not in mapped_completed:
308
+ mapped_completed.append(mapped)
309
+
310
+ # Injecter les etapes devflow intermediaires auto-validees
311
+ # Pour chaque etape devflow, si elle precede une etape deja completee
312
+ # et n'est pas encore dans completed, l'injecter
313
+ review_iterations: dict[str, int] = {}
314
+ if current_step in STEPS:
315
+ current_idx = STEPS.index(current_step)
316
+ final_completed: list[str] = []
317
+ for i, step in enumerate(STEPS):
318
+ if step == current_step:
319
+ break
320
+ if step in mapped_completed:
321
+ final_completed.append(step)
322
+ elif i < current_idx:
323
+ # Etape intermediaire manquante → auto-valider
324
+ final_completed.append(step)
325
+ if step in ("review-spec", "review-tasks", "review-impl"):
326
+ review_iterations[step] = 0
327
+ mapped_completed = final_completed
328
+
329
+ return FeatureState(
330
+ issueId=issue_id,
331
+ currentStep=current_step,
332
+ completedSteps=mapped_completed,
333
+ stepTimestamps={},
334
+ reviewIterations=review_iterations,
335
+ mode="full",
336
+ skippedSteps=[],
337
+ createdAt=raw_data.get("createdAt", ""),
338
+ updatedAt=raw_data.get("updatedAt", ""),
339
+ )
340
+
341
+
265
342
  def validate_and_advance(
266
343
  state: FeatureState,
267
344
  completed_step: str,
@@ -6,6 +6,10 @@ import re
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
8
 
9
+ from devflow_cli.utils.logging import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
9
13
 
10
14
  @dataclass
11
15
  class ValidationIssue:
@@ -85,6 +89,7 @@ def validate_artifact(step: str, artifact_path: Path) -> ValidationResult:
85
89
  try:
86
90
  content = artifact_path.read_text(encoding="utf-8")
87
91
  except (UnicodeDecodeError, OSError) as exc:
92
+ logger.warning("Failed to read artifact %s", artifact_path, exc_info=True)
88
93
  issues.append(ValidationIssue(
89
94
  type="encoding_error",
90
95
  message=f"Impossible de lire {artifact_path.name}: {exc}",
@@ -159,6 +164,7 @@ def _validate_contracts(
159
164
  try:
160
165
  content = artifact_path.read_text(encoding="utf-8")
161
166
  except (UnicodeDecodeError, OSError) as exc:
167
+ logger.warning("Failed to read artifact %s", artifact_path, exc_info=True)
162
168
  issues.append(ValidationIssue(
163
169
  type="encoding_error",
164
170
  message=f"Impossible de lire {artifact_path.name}: {exc}",
@@ -0,0 +1,62 @@
1
+ """Configuration du logging pour la CLI devflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from logging.handlers import RotatingFileHandler
8
+ from pathlib import Path
9
+
10
+ _LOG_DIR = ".devflow"
11
+ _LOG_FILE = "devflow.log"
12
+ _MAX_BYTES = 1_048_576 # 1 Mo
13
+ _BACKUP_COUNT = 1
14
+ _FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
15
+
16
+ _configured = False
17
+
18
+
19
+ def setup_logging(verbose: bool = False) -> None:
20
+ """Configure le logging une seule fois.
21
+
22
+ - Fichier : .devflow/devflow.log (RotatingFileHandler, 1 Mo, 1 backup)
23
+ - Console : stderr uniquement si verbose=True
24
+ - Fallback : NullHandler si le fichier ne peut pas etre cree
25
+ """
26
+ global _configured
27
+ if _configured:
28
+ return
29
+ _configured = True
30
+
31
+ root = logging.getLogger("devflow_cli")
32
+ root.setLevel(logging.DEBUG)
33
+
34
+ formatter = logging.Formatter(_FORMAT)
35
+
36
+ # File handler avec rotation
37
+ try:
38
+ log_dir = Path(_LOG_DIR)
39
+ log_dir.mkdir(parents=True, exist_ok=True)
40
+ file_handler = RotatingFileHandler(
41
+ log_dir / _LOG_FILE,
42
+ maxBytes=_MAX_BYTES,
43
+ backupCount=_BACKUP_COUNT,
44
+ encoding="utf-8",
45
+ )
46
+ file_handler.setLevel(logging.DEBUG)
47
+ file_handler.setFormatter(formatter)
48
+ root.addHandler(file_handler)
49
+ except OSError:
50
+ root.addHandler(logging.NullHandler())
51
+
52
+ # Console handler (stderr) si verbose
53
+ if verbose:
54
+ console_handler = logging.StreamHandler(sys.stderr)
55
+ console_handler.setLevel(logging.DEBUG)
56
+ console_handler.setFormatter(formatter)
57
+ root.addHandler(console_handler)
58
+
59
+
60
+ def get_logger(name: str) -> logging.Logger:
61
+ """Retourne un logger enfant de devflow_cli."""
62
+ return logging.getLogger(name)
@@ -7,6 +7,10 @@ import re
7
7
  import subprocess
8
8
  from pathlib import Path
9
9
 
10
+ from devflow_cli.utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
10
14
 
11
15
  def get_devflow_root() -> Path:
12
16
  """Racine du projet devflow (templates, commands, agents, extensions)."""
@@ -116,7 +120,7 @@ def next_feature_number(project_dir: Path | None = None) -> int:
116
120
  if num > highest:
117
121
  highest = num
118
122
  except (subprocess.TimeoutExpired, FileNotFoundError):
119
- pass
123
+ logger.warning("Failed to scan git branches for feature numbers", exc_info=True)
120
124
 
121
125
  return highest + 1
122
126
 
@@ -132,5 +136,5 @@ def next_feature_number(project_dir: Path | None = None) -> int:
132
136
  fcntl.flock(f, fcntl.LOCK_EX)
133
137
  return _scan()
134
138
  except ImportError:
135
- # Windows : pas de fcntl, on execute sans lock
139
+ logger.warning("fcntl not available (Windows) running without file lock")
136
140
  return _scan()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devflow-cli
3
- Version: 2.0.0
3
+ Version: 2.3.0
4
4
  Summary: Spec-Driven Development workflow CLI
5
5
  Project-URL: Homepage, https://github.com/sopequenoteck/devflow
6
6
  Project-URL: Repository, https://github.com/sopequenoteck/devflow
@@ -117,7 +117,7 @@ devflow status
117
117
  Consultez la [documentation complète](docs/index.md) pour :
118
118
 
119
119
  - [Pipeline détaillé](docs/pipeline.md) — les 13 étapes expliquées
120
- - [Commandes CLI](docs/cli-reference.md) — référence des 11 commandes terminal
120
+ - [Commandes CLI](docs/cli-reference.md) — référence des 12 commandes terminal
121
121
  - [Commandes slash](docs/commands-reference.md) — référence des 23 commandes Claude Code
122
122
  - [Guides par type de projet](docs/index.md#guides-par-type-de-projet) — brownfield, greenfield, microservices
123
123
 
@@ -0,0 +1,35 @@
1
+ devflow_cli/__init__.py,sha256=mraf4w1I_7HJ4qRRyQ62xTVfVDHUSCTqmxuYeP4Px4M,2394
2
+ devflow_cli/cli.py,sha256=4FdmMntfA7Uxpm45XUjx5LjOS6l_BpaukwLTHGGRRps,1759
3
+ devflow_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ devflow_cli/commands/check.py,sha256=84eXY1Oc2OXk-oNvDKS-FVXN954ykDJ6hnSHb30C3j0,4313
5
+ devflow_cli/commands/context.py,sha256=hwTitOHxIpJMx1X8R1jHHy9CpE4HVZpJKkyeOujxAio,6246
6
+ devflow_cli/commands/export_cmd.py,sha256=Mdvoh8oyainm0ulaaLd3pcx9m2s-Go--fFBeLS6i9Ts,4871
7
+ devflow_cli/commands/extension.py,sha256=gL33YS8EuBTADmqKGDqw_0gJXFwccwg1uJmCNtvexhE,4373
8
+ devflow_cli/commands/feature.py,sha256=GP_tZ7B3jMKb-cxXY3RC6yiv8mj5ZLyhHsXfIS1Ffhg,4238
9
+ devflow_cli/commands/init_cmd.py,sha256=5hKBYKFCklTvVVQxF5Qx4gjqIlUQqxePY4nWct3HYJc,4659
10
+ devflow_cli/commands/migrate_cmd.py,sha256=Kxz_Ml78UvDzjL1-4Gp3SJG_DrVDWGn9Ie2oc0in1N4,3423
11
+ devflow_cli/commands/migrate_speckit.py,sha256=zs-6aCeLLePRj9mSxfr_VrMYs84JrEHUYIgheIERHVk,8284
12
+ devflow_cli/commands/rollback.py,sha256=B-Qnmem_sAwmSwyFdzpzglbeX4uLd0t4ohsLYyHngl4,1789
13
+ devflow_cli/commands/stale.py,sha256=xInmH5QywgxHCeAbpumwvUKqnk-XnY8FcURkXXPGic0,3208
14
+ devflow_cli/commands/status.py,sha256=nPuudZPqCFWE2JqiCFOMKlN_CM_uDwD-gq9MbHBhg20,4775
15
+ devflow_cli/commands/upgrade_cmd.py,sha256=xLOoaLXpyyJO4UuWLHjkXSGSX2IcL65-PQnoZAmwW9M,5029
16
+ devflow_cli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ devflow_cli/core/catalog.py,sha256=nLPc78JkbISaSxJnHlWiMi_lvepaumbfwcjgLuVy7Zc,3102
18
+ devflow_cli/core/git.py,sha256=o7Vk_n8b3-cAQa3CR1FblTZUvMDez7Ik4D9lrYI0njY,2561
19
+ devflow_cli/core/hooks.py,sha256=KhG6BECui7IeoAERJAI6EWnRThqmSdeJCOxuk_xowe8,3189
20
+ devflow_cli/core/installer.py,sha256=XGuugkWAV7McTnpdNZ2JX4NtEAE8CI73mfrYnKlb-JQ,4329
21
+ devflow_cli/core/manifest.py,sha256=J7g2SzhXy-Vi4w9-rEPmARSZXo8KTOxfd2pEc9jNUME,5933
22
+ devflow_cli/core/scanner.py,sha256=bcoC91Nc0Hw2P7aHpL-9ITxynLZhLTEKpOXo553a-18,4395
23
+ devflow_cli/core/staleness.py,sha256=ASe-rlYN17AtpI_S4QlidaHwbtv7E7L4fhRGYtyTlPE,5586
24
+ devflow_cli/core/state.py,sha256=tkMScnTSMp8nQm_AElDJPqV_RtcXWpq29CJYfdNpBXI,12515
25
+ devflow_cli/core/validators.py,sha256=rW8JCveYjf8KtOR2Q7v2mdvhYfB0qQnC3FBuWYLmytE,6445
26
+ devflow_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ devflow_cli/utils/console.py,sha256=t7cpyeEluWj4v_O_KFSI59iqXK4GiSXPuxlsr8q5zoQ,771
28
+ devflow_cli/utils/logging.py,sha256=I7U9_IVvYAFnUAWi7xs5F_WGR0uSz6DmRvZ_B3xLFbs,1769
29
+ devflow_cli/utils/paths.py,sha256=C_0Jmiz4LRoelyFYWA42ehGTg6R2UnBxllBo5_LZ1HI,4400
30
+ devflow_cli/utils/strings.py,sha256=bfFXCwBDnFxshgZOYfy6mtysnjiaWO1wFnqBf7HLg4o,351
31
+ devflow_cli-2.3.0.dist-info/METADATA,sha256=1GDvGXx3RXvmsxOq-Ldc-9vgwS13lAHrxQaBU1x7jt4,4896
32
+ devflow_cli-2.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
33
+ devflow_cli-2.3.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
34
+ devflow_cli-2.3.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
35
+ devflow_cli-2.3.0.dist-info/RECORD,,
@@ -1,33 +0,0 @@
1
- devflow_cli/__init__.py,sha256=4C2mALrQiTmJMZx99CT2fViXvKO5_IFzMStcTLumeeg,1400
2
- devflow_cli/cli.py,sha256=UxuYGV0TgsnoVV_-vp2Ce1sUTkc61Viyr13X_2HEP-o,1458
3
- devflow_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- devflow_cli/commands/check.py,sha256=sb5zkSXXwl91o8yn1-TRL5wH89MWX-kY2CAHYYjMxq4,4170
5
- devflow_cli/commands/context.py,sha256=hwTitOHxIpJMx1X8R1jHHy9CpE4HVZpJKkyeOujxAio,6246
6
- devflow_cli/commands/export_cmd.py,sha256=Mdvoh8oyainm0ulaaLd3pcx9m2s-Go--fFBeLS6i9Ts,4871
7
- devflow_cli/commands/extension.py,sha256=gL33YS8EuBTADmqKGDqw_0gJXFwccwg1uJmCNtvexhE,4373
8
- devflow_cli/commands/feature.py,sha256=EceMCxYEif4P6Ir-1XZml4txLH9iqm3jEAeS6dJ8LpA,4098
9
- devflow_cli/commands/init_cmd.py,sha256=LLUJx_Kdl-YCEuy1corvaYe65pZ6R5-VnVBal6wZSa4,4513
10
- devflow_cli/commands/migrate_cmd.py,sha256=y2_yWyBabMgDZ0e2CsX2nYtb33yQRyr4zcrj858Ieoo,3274
11
- devflow_cli/commands/regen.py,sha256=UyB7_0oBxDs_CqaAQ6yE2Yoz4IcpUr2bqkIEmm0e1iM,3198
12
- devflow_cli/commands/rollback.py,sha256=B-Qnmem_sAwmSwyFdzpzglbeX4uLd0t4ohsLYyHngl4,1789
13
- devflow_cli/commands/status.py,sha256=nPuudZPqCFWE2JqiCFOMKlN_CM_uDwD-gq9MbHBhg20,4775
14
- devflow_cli/commands/upgrade_cmd.py,sha256=xLOoaLXpyyJO4UuWLHjkXSGSX2IcL65-PQnoZAmwW9M,5029
15
- devflow_cli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- devflow_cli/core/catalog.py,sha256=oMBZRigOtpAQceu3slV8UpuKSZpkxQcr4xtjZXyFHYY,2949
17
- devflow_cli/core/git.py,sha256=WL8tU3f5KcNyF5aL669Gzy73OIvuC_CCGZdt8TzWvXc,2346
18
- devflow_cli/core/hooks.py,sha256=EHIlpwmDYHshaDijiLCEoPDbnV9T7jDttOCnpRRx0HU,3025
19
- devflow_cli/core/installer.py,sha256=XGuugkWAV7McTnpdNZ2JX4NtEAE8CI73mfrYnKlb-JQ,4329
20
- devflow_cli/core/manifest.py,sha256=5hu2w6PPGo4raGbgKzx6qpAtt653SrzpRkbWngHM9qw,5770
21
- devflow_cli/core/scanner.py,sha256=bcoC91Nc0Hw2P7aHpL-9ITxynLZhLTEKpOXo553a-18,4395
22
- devflow_cli/core/staleness.py,sha256=ASe-rlYN17AtpI_S4QlidaHwbtv7E7L4fhRGYtyTlPE,5586
23
- devflow_cli/core/state.py,sha256=M7pldLzXekgQWwPXFYg62CQ-q_4C9Es1lza74qAmCGM,9433
24
- devflow_cli/core/validators.py,sha256=3PtqC-R2kqNbh4HK_l_Yl8RylQsFdtCXhopt0yT065k,6194
25
- devflow_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- devflow_cli/utils/console.py,sha256=t7cpyeEluWj4v_O_KFSI59iqXK4GiSXPuxlsr8q5zoQ,771
27
- devflow_cli/utils/paths.py,sha256=9J2Qhq2Ot6p7HQwRAfdK3V5bc5sme6FnnnAg9dhXcP4,4212
28
- devflow_cli/utils/strings.py,sha256=bfFXCwBDnFxshgZOYfy6mtysnjiaWO1wFnqBf7HLg4o,351
29
- devflow_cli-2.0.0.dist-info/METADATA,sha256=1nrITovXCXduwSNOfg0dRgHecrAbZoCO6fiWoe8tyeA,4896
30
- devflow_cli-2.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
31
- devflow_cli-2.0.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
32
- devflow_cli-2.0.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
33
- devflow_cli-2.0.0.dist-info/RECORD,,