devflow-cli 6.3.1__py3-none-any.whl → 7.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.
Files changed (33) hide show
  1. devflow_cli/__init__.py +1 -3
  2. devflow_cli/cli.py +15 -0
  3. devflow_cli/commands/adaptive.py +33 -35
  4. devflow_cli/commands/assess.py +26 -2
  5. devflow_cli/commands/status.py +47 -1
  6. devflow_cli/core/activity_contracts.py +91 -163
  7. devflow_cli/core/adaptive_runner.py +11 -24
  8. devflow_cli/core/adaptive_workflow.py +65 -101
  9. devflow_cli/core/assessment.py +152 -112
  10. devflow_cli/core/automatic_evidence.py +14 -16
  11. devflow_cli/core/contract_schemas.py +39 -15
  12. devflow_cli/core/docgen.py +32 -15
  13. devflow_cli/core/feedback.py +10 -1
  14. devflow_cli/core/manifest.py +18 -0
  15. devflow_cli/core/remediation.py +8 -11
  16. devflow_cli/core/staleness.py +1 -3
  17. devflow_cli/core/validation_approvals.py +1 -1
  18. devflow_cli/core/validators.py +1 -66
  19. devflow_cli/core/waivers.py +8 -1
  20. devflow_cli/core/worklog.py +0 -11
  21. devflow_cli/resources/templates/plan-template.md +0 -1
  22. devflow_cli/resources/templates/quickstart-template.md +0 -1
  23. devflow_cli/schemas/{activity-contract-v3.schema.json → activity-contract-v4.schema.json} +7 -6
  24. devflow_cli/schemas/activity-result-v1.schema.json +0 -10
  25. devflow_cli/schemas/{assessment-v3.schema.json → assessment-v4.schema.json} +19 -7
  26. devflow_cli/schemas/{workflow-state-v4.schema.json → workflow-state-v5.schema.json} +13 -23
  27. {devflow_cli-6.3.1.dist-info → devflow_cli-7.0.0.dist-info}/METADATA +34 -6
  28. {devflow_cli-6.3.1.dist-info → devflow_cli-7.0.0.dist-info}/RECORD +31 -33
  29. devflow_cli/core/omissions.py +0 -168
  30. devflow_cli/resources/templates/research-template.md +0 -51
  31. {devflow_cli-6.3.1.dist-info → devflow_cli-7.0.0.dist-info}/WHEEL +0 -0
  32. {devflow_cli-6.3.1.dist-info → devflow_cli-7.0.0.dist-info}/entry_points.txt +0 -0
  33. {devflow_cli-6.3.1.dist-info → devflow_cli-7.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """devflow CLI — workflow de développement adaptatif."""
2
2
 
3
- VERSION = "6.3.1"
3
+ VERSION = "7.0.0"
4
4
 
5
5
  SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
6
6
 
@@ -9,7 +9,6 @@ ACTIVITY_LINEAR_MAPPING = {
9
9
  "prepare": "Plan",
10
10
  "risk-assessment": "Research",
11
11
  "spec": "Spec",
12
- "research": "Research",
13
12
  "contracts": "Plan",
14
13
  "plan": "Plan",
15
14
  "tasks": "Tasks",
@@ -35,7 +34,6 @@ LINEAR_STATUS_ORDER = [
35
34
  ARTIFACTS = [
36
35
  "risk-assessment.md",
37
36
  "spec.md",
38
- "research.md",
39
37
  "contracts.md",
40
38
  "plan.md",
41
39
  "tasks.md",
devflow_cli/cli.py CHANGED
@@ -3,8 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import typer
6
+ from typer.core import TyperGroup
6
7
 
7
8
  from devflow_cli import VERSION
9
+ from devflow_cli.core.contract_schemas import LegacySchemaError
10
+ from devflow_cli.utils.console import fail
8
11
  from devflow_cli.utils.logging import setup_logging
9
12
  from devflow_cli.commands.init_cmd import init
10
13
  from devflow_cli.commands.check import check
@@ -25,10 +28,22 @@ from devflow_cli.commands.verify import verify
25
28
  from devflow_cli.commands.assess import assess
26
29
  from devflow_cli.commands.usage import usage
27
30
 
31
+ class DevflowGroup(TyperGroup):
32
+ """Affiche un refus actionnable, sans trace, pour un fichier de feature 6.x."""
33
+
34
+ def invoke(self, ctx):
35
+ try:
36
+ return super().invoke(ctx)
37
+ except LegacySchemaError as exc:
38
+ fail(str(exc))
39
+ raise typer.Exit(1) from exc
40
+
41
+
28
42
  app = typer.Typer(
29
43
  name="devflow",
30
44
  help="Spec-Driven Development workflow CLI",
31
45
  no_args_is_help=True,
46
+ cls=DevflowGroup,
32
47
  )
33
48
 
34
49
 
@@ -23,7 +23,6 @@ from devflow_cli.core.adaptive_workflow import (
23
23
  workflow_lock,
24
24
  write_adaptive_state,
25
25
  )
26
- from devflow_cli.core.omissions import omit_current_activity
27
26
  from devflow_cli.core.assessment import read_assessment
28
27
  from devflow_cli.core.activity_contracts import (
29
28
  build_activity_contract,
@@ -60,7 +59,7 @@ from devflow_cli.core.validation_approvals import (
60
59
  approve_plan_command,
61
60
  pending_plan_approval,
62
61
  )
63
- from devflow_cli.core.contract_schemas import diagnose_contract_file
62
+ from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
64
63
  from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
65
64
  from devflow_cli.adapters.agents import get_agent_adapter
66
65
  from devflow_cli.utils.console import console, fail, ok, warn
@@ -74,11 +73,12 @@ from devflow_cli.utils.paths import (
74
73
 
75
74
  app = typer.Typer(help="Machine d'etat des parcours adaptatifs")
76
75
 
76
+ LEGACY_SCHEMA_EXIT_CODE = 1
77
+
77
78
  RESULT_VALIDATED_ACTIVITIES = frozenset({
78
79
  "prepare",
79
80
  "risk-assessment",
80
81
  "spec",
81
- "research",
82
82
  "contracts",
83
83
  "plan",
84
84
  "tasks",
@@ -105,7 +105,17 @@ def _state(
105
105
  feature_dir: Path, *, json_output: bool = False, issue_id: str | None = None
106
106
  ):
107
107
  state_file = feature_dir / "workflow-state.json"
108
- state = read_adaptive_state(state_file)
108
+ try:
109
+ state = read_adaptive_state(state_file)
110
+ except LegacySchemaError as exc:
111
+ if json_output:
112
+ _print_execution_json(
113
+ issue_id or feature_dir.name, None, False, "legacy-schema", str(exc),
114
+ None, LEGACY_SCHEMA_EXIT_CODE,
115
+ )
116
+ else:
117
+ fail(str(exc))
118
+ raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
109
119
  if state is None:
110
120
  diagnostic = diagnose_contract_file("workflow-state", state_file)
111
121
  detail = (
@@ -125,7 +135,11 @@ def _state(
125
135
 
126
136
  def _assessment(feature_dir: Path):
127
137
  assessment_file = feature_dir / "assessment.json"
128
- assessment = read_assessment(assessment_file)
138
+ try:
139
+ assessment = read_assessment(assessment_file)
140
+ except LegacySchemaError as exc:
141
+ fail(str(exc))
142
+ raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
129
143
  if assessment is None:
130
144
  diagnostic = diagnose_contract_file("assessment", assessment_file)
131
145
  fail(
@@ -239,26 +253,6 @@ def evidence(
239
253
  warn("Source manuelle : cette preuve ne valide pas les gates du workflow.")
240
254
 
241
255
 
242
- @app.command("omit")
243
- def omit_adaptive(
244
- feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
245
- activity: str = typer.Argument(..., help="research ou contracts, activite courante"),
246
- by: str = typer.Option(..., "--by", help="Responsable de la decision"),
247
- reason: str = typer.Option(..., "--reason", help="Justification explicite"),
248
- ) -> None:
249
- """Omet une activite critical recommandee par risk-assessment, avec audit."""
250
- feature_dir = _feature(feature)
251
- try:
252
- _, next_activity = omit_current_activity(
253
- feature_dir, activity, by=by, reason=reason
254
- )
255
- except (OSError, ValueError) as exc:
256
- fail(f"Omission refusee : {exc}")
257
- raise typer.Exit(int(ExitCode.BLOCKED)) from exc
258
- safe_refresh_worklog(feature_dir)
259
- ok(f"Activite '{activity}' omise avec justification. Prochaine : {next_activity}")
260
-
261
-
262
256
  @app.command("contract")
263
257
  def contract(
264
258
  feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
@@ -344,7 +338,7 @@ def run_adaptive(
344
338
  )
345
339
  return
346
340
  if current_state.currentActivity not in {
347
- "prepare", "risk-assessment", "spec", "research", "contracts", "plan",
341
+ "prepare", "risk-assessment", "spec", "contracts", "plan",
348
342
  "tasks", "preflight-evidence", "implement", "review", "rollback-check",
349
343
  }:
350
344
  detail = f"L'activite '{current_state.currentActivity}' ne necessite pas d'agent."
@@ -603,9 +597,6 @@ def review_adaptive(
603
597
  risk: Optional[list[str]] = typer.Option(
604
598
  None, "--risk", help="Risque restant (option repetable)"
605
599
  ),
606
- ack_omission: Optional[list[str]] = typer.Option(
607
- None, "--ack-omission", help="Activite omise explicitement revue (repetable)"
608
- ),
609
600
  ) -> None:
610
601
  """Enregistre une revue humaine, validee comme la revue d'un agent."""
611
602
  feature_dir = _feature(feature)
@@ -624,7 +615,6 @@ def review_adaptive(
624
615
 
625
616
  result = record_human_review(
626
617
  feature_dir, feature_path, by, relative_report, outcome, list(risk or []),
627
- list(ack_omission or []),
628
618
  )
629
619
  if result.advanced:
630
620
  ok(
@@ -706,6 +696,9 @@ def approve_adaptive_command(
706
696
  approval = approve_plan_command(
707
697
  feature_dir, validation_id, reason, expected_command=item.command
708
698
  )
699
+ except LegacySchemaError as exc:
700
+ fail(str(exc))
701
+ raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
709
702
  except ValueError as exc:
710
703
  fail(f"Approbation refusee : {exc}")
711
704
  raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
@@ -731,6 +724,9 @@ def waive_validation(
731
724
  waiver = create_validation_waiver(
732
725
  feature_dir, validation_id, reason, author=author
733
726
  )
727
+ except LegacySchemaError as exc:
728
+ fail(str(exc))
729
+ raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
734
730
  except ValueError as exc:
735
731
  fail(str(exc))
736
732
  raise typer.Exit(int(ExitCode.BLOCKED)) from exc
@@ -917,6 +913,9 @@ def worklog(
917
913
  feature_dir = _feature(feature)
918
914
  try:
919
915
  output = refresh_worklog(feature_dir)
916
+ except LegacySchemaError as exc:
917
+ fail(str(exc))
918
+ raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
920
919
  except ValueError as exc:
921
920
  fail(str(exc))
922
921
  raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
@@ -959,7 +958,7 @@ def _operational_status(
959
958
  ),
960
959
  "currentActivity": state.currentActivity,
961
960
  "completedActivities": list(state.completedActivities),
962
- "omissions": dict(state.omissions),
961
+ "guardrails": state.guardrails,
963
962
  "progress": {
964
963
  "completed": len(state.completedActivities),
965
964
  "total": len(state.activities),
@@ -984,10 +983,9 @@ def _print_operational_status(payload: dict) -> None:
984
983
  console.print(f"Activite : [bold]{payload['currentActivity']}[/bold]")
985
984
  progress = payload["progress"]
986
985
  console.print(f"Progression : {progress['completed']}/{progress['total']}")
987
- if payload["omissions"]:
988
- console.print("Omissions justifiees :")
989
- for activity, entry in payload["omissions"].items():
990
- console.print(f" - {activity} : {entry['reason']} ({entry['by']})")
986
+ console.print(
987
+ f"Garde-fous : {'actifs' if payload['guardrails'] else 'inactifs'}"
988
+ )
991
989
  console.print("Preuves :")
992
990
  for item in payload["evidence"]:
993
991
  source = f" / {item['source']}" if item["source"] else ""
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import re
7
+ from pathlib import Path
6
8
  from typing import Literal
7
9
 
8
10
  import typer
@@ -14,10 +16,20 @@ from devflow_cli.core.assessment import (
14
16
  write_assessment,
15
17
  )
16
18
  from devflow_cli.core.adaptive_workflow import workflow_lock
19
+ from devflow_cli.core.contract_schemas import check_schema_version
17
20
  from devflow_cli.utils.console import console, fail, ok
18
21
  from devflow_cli.utils.paths import resolve_feature_dir
19
22
 
20
23
 
24
+ def _refuse_unsupported_version(path: Path) -> None:
25
+ """Refuse d'ecraser un assessment.json d'une autre version de devflow."""
26
+ try:
27
+ payload = json.loads(path.read_text(encoding="utf-8"))
28
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
29
+ return
30
+ check_schema_version("assessment", path, payload)
31
+
32
+
21
33
  def assess(
22
34
  feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
23
35
  scope: Literal["local", "multiple", "transversal"] = typer.Option(
@@ -36,7 +48,7 @@ def assess(
36
48
  False, "--data-security", help="Le changement touche aux donnees ou a la securite"
37
49
  ),
38
50
  summary: str = typer.Option("", help="Resume court de la demande"),
39
- profile: Literal["quick", "standard", "deep", "critical"] | None = typer.Option(
51
+ profile: Literal["quick", "standard", "deep"] | None = typer.Option(
40
52
  None, help="Force un profil au lieu de la recommandation"
41
53
  ),
42
54
  issue_id: str | None = typer.Option(
@@ -52,7 +64,13 @@ def assess(
52
64
  fail(f"Feature '{feature}' introuvable.")
53
65
  raise typer.Exit(1)
54
66
 
55
- existing_assessment = read_assessment(feature_dir / "assessment.json")
67
+ assessment_file = feature_dir / "assessment.json"
68
+ try:
69
+ _refuse_unsupported_version(assessment_file)
70
+ existing_assessment = read_assessment(assessment_file)
71
+ except ValueError as exc:
72
+ fail(str(exc))
73
+ raise typer.Exit(1) from exc
56
74
  canonical_issue_id = issue_id or (
57
75
  existing_assessment.issueId if existing_assessment is not None else None
58
76
  )
@@ -109,6 +127,12 @@ def assess(
109
127
  if assessment.selectedProfile != assessment.recommendedProfile:
110
128
  console.print(f" Recommande : {assessment.recommendedProfile}")
111
129
  console.print(f" Confiance : {assessment.confidence}")
130
+ if assessment.guardrails.active:
131
+ console.print(
132
+ f" Garde-fous : actifs ({', '.join(assessment.guardrails.reasons)})"
133
+ )
134
+ else:
135
+ console.print(" Garde-fous : inactifs")
112
136
  for reason in assessment.reasons:
113
137
  console.print(f" - {reason}")
114
138
  console.print(f" Activites : {' -> '.join(assessment.activities)}")
@@ -10,21 +10,48 @@ import typer
10
10
  from devflow_cli import ARTIFACTS
11
11
  from devflow_cli.core.adaptive_workflow import read_adaptive_state
12
12
  from devflow_cli.core.assessment import read_assessment
13
+ from devflow_cli.core.contract_schemas import LegacySchemaError
14
+ from devflow_cli.core.manifest import stale_commands
13
15
  from devflow_cli.core.staleness import get_staleness
14
16
  from devflow_cli.utils.console import console, make_table, warn
15
- from devflow_cli.utils.paths import iter_feature_dirs, resolve_feature_dir
17
+ from devflow_cli.utils.paths import (
18
+ get_claude_dir,
19
+ get_devflow_root,
20
+ iter_feature_dirs,
21
+ resolve_feature_dir,
22
+ )
16
23
 
17
24
 
18
25
  def status(
19
26
  issue_id: Optional[str] = typer.Argument(None, help="ID de la feature (optionnel)"),
20
27
  ) -> None:
21
28
  """Lecture rapide du statut d'une feature ou liste toutes les features."""
29
+ _warn_stale_commands()
22
30
  if issue_id is None:
23
31
  _list_features()
24
32
  else:
25
33
  _show_feature(issue_id)
26
34
 
27
35
 
36
+ def _warn_stale_commands() -> None:
37
+ """Signale les commandes /devflow installees d'une autre version, sans les modifier."""
38
+ try:
39
+ stale = stale_commands(get_devflow_root(), get_claude_dir())
40
+ except (OSError, AttributeError, ValueError) as exc:
41
+ warn(
42
+ "Commandes /devflow installees non verifiables "
43
+ f"({type(exc).__name__}). Lancer : devflow upgrade --dry-run"
44
+ )
45
+ return
46
+ if not stale:
47
+ return
48
+ paths = ", ".join(s.path for s in stale)
49
+ warn(
50
+ f"Commandes /devflow installees differentes de la CLI : {paths}. "
51
+ "Lancer : devflow upgrade"
52
+ )
53
+
54
+
28
55
  def _list_features() -> None:
29
56
  """Liste les espaces de feature et leur progression adaptative."""
30
57
  table = make_table(
@@ -40,6 +67,25 @@ def _list_features() -> None:
40
67
  workflow_file = feature_dir / "workflow-state.json"
41
68
  assessment_file = feature_dir / "assessment.json"
42
69
 
70
+ try:
71
+ legacy = None
72
+ if workflow_file.exists():
73
+ read_adaptive_state(workflow_file)
74
+ if assessment_file.exists():
75
+ read_assessment(assessment_file)
76
+ except LegacySchemaError as exc:
77
+ legacy = exc
78
+ if legacy is not None:
79
+ warn(str(legacy))
80
+ table.add_row(
81
+ feature_dir.name,
82
+ "-",
83
+ "[yellow]schema 6.x[/yellow]",
84
+ "-",
85
+ location,
86
+ )
87
+ continue
88
+
43
89
  if workflow_file.exists():
44
90
  state = read_adaptive_state(workflow_file)
45
91
  if state is None: