devflow-cli 2.9.0__py3-none-any.whl → 3.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 (77) hide show
  1. devflow_cli/__init__.py +20 -86
  2. devflow_cli/adapters/agents.py +109 -1
  3. devflow_cli/adapters/linear.py +18 -14
  4. devflow_cli/cli.py +0 -10
  5. devflow_cli/commands/adaptive.py +285 -48
  6. devflow_cli/commands/assess.py +39 -37
  7. devflow_cli/commands/check.py +1 -15
  8. devflow_cli/commands/docs_sync.py +2 -2
  9. devflow_cli/commands/export_cmd.py +15 -73
  10. devflow_cli/commands/feature.py +32 -47
  11. devflow_cli/commands/init_cmd.py +12 -2
  12. devflow_cli/commands/linear.py +2 -2
  13. devflow_cli/commands/migrate_cmd.py +170 -75
  14. devflow_cli/commands/stale.py +9 -9
  15. devflow_cli/commands/status.py +124 -61
  16. devflow_cli/commands/upgrade_cmd.py +9 -0
  17. devflow_cli/core/activity_contracts.py +16 -95
  18. devflow_cli/core/adaptive_runner.py +17 -6
  19. devflow_cli/core/adaptive_workflow.py +16 -30
  20. devflow_cli/core/assessment.py +112 -0
  21. devflow_cli/core/automatic_evidence.py +10 -8
  22. devflow_cli/core/contract_schemas.py +110 -0
  23. devflow_cli/core/docgen.py +29 -32
  24. devflow_cli/core/installer.py +62 -4
  25. devflow_cli/core/operational.py +37 -0
  26. devflow_cli/core/redaction.py +68 -0
  27. devflow_cli/core/remediation.py +6 -3
  28. devflow_cli/core/staleness.py +22 -35
  29. devflow_cli/core/verification.py +5 -3
  30. devflow_cli/resources/commands/devflow.context.md +57 -0
  31. devflow_cli/resources/commands/devflow.extension.md +63 -0
  32. devflow_cli/resources/commands/devflow.init.md +50 -0
  33. devflow_cli/resources/commands/devflow.md +25 -0
  34. devflow_cli/resources/commands/devflow.regen.md +34 -0
  35. devflow_cli/resources/commands/devflow.status.md +11 -0
  36. devflow_cli/resources/extensions/EXTENSION-API-REFERENCE.md +140 -0
  37. devflow_cli/resources/extensions/catalog.community.json +5 -0
  38. devflow_cli/resources/extensions/catalog.json +19 -0
  39. devflow_cli/resources/extensions/template/example-command.md +29 -0
  40. devflow_cli/resources/extensions/template/example-extension.json +18 -0
  41. devflow_cli/resources/templates/agent-context-template.md +29 -0
  42. devflow_cli/resources/templates/constitution-template.md +85 -0
  43. devflow_cli/resources/templates/contracts/README-template.md +24 -0
  44. devflow_cli/resources/templates/contracts/api-template.md +38 -0
  45. devflow_cli/resources/templates/contracts/components-template.md +28 -0
  46. devflow_cli/resources/templates/contracts/interfaces-template.md +20 -0
  47. devflow_cli/resources/templates/contracts/services-template.md +22 -0
  48. devflow_cli/resources/templates/contracts-template.md +105 -0
  49. devflow_cli/resources/templates/data-model-template.md +55 -0
  50. devflow_cli/resources/templates/devflow-hooks-template.yml +38 -0
  51. devflow_cli/resources/templates/extension-manifest-template.json +9 -0
  52. devflow_cli/resources/templates/plan-template.md +98 -0
  53. devflow_cli/resources/templates/quickstart-template.md +65 -0
  54. devflow_cli/resources/templates/research-template.md +51 -0
  55. devflow_cli/resources/templates/spec-template.md +79 -0
  56. devflow_cli/resources/templates/tasks-template.md +105 -0
  57. devflow_cli/schemas/activity-contract-v1.schema.json +76 -0
  58. devflow_cli/schemas/activity-result-v1.schema.json +85 -0
  59. devflow_cli/schemas/assessment-v1.schema.json +60 -0
  60. devflow_cli/schemas/workflow-state-v2.schema.json +66 -0
  61. devflow_cli/utils/paths.py +23 -12
  62. devflow_cli-3.0.0.dist-info/METADATA +113 -0
  63. devflow_cli-3.0.0.dist-info/RECORD +88 -0
  64. devflow_cli/commands/migrate_speckit.py +0 -344
  65. devflow_cli/commands/review.py +0 -54
  66. devflow_cli/commands/rollback.py +0 -53
  67. devflow_cli/commands/run.py +0 -123
  68. devflow_cli/commands/step.py +0 -118
  69. devflow_cli/core/orchestrator.py +0 -170
  70. devflow_cli/core/state.py +0 -576
  71. devflow_cli/core/state_store.py +0 -45
  72. devflow_cli/schemas/state.schema.json +0 -39
  73. devflow_cli-2.9.0.dist-info/METADATA +0 -223
  74. devflow_cli-2.9.0.dist-info/RECORD +0 -63
  75. {devflow_cli-2.9.0.dist-info → devflow_cli-3.0.0.dist-info}/WHEEL +0 -0
  76. {devflow_cli-2.9.0.dist-info → devflow_cli-3.0.0.dist-info}/entry_points.txt +0 -0
  77. {devflow_cli-2.9.0.dist-info → devflow_cli-3.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py CHANGED
@@ -1,113 +1,47 @@
1
- """devflow CLI — Spec-Driven Development workflow."""
1
+ """devflow CLI — workflow de développement adaptatif."""
2
2
 
3
- VERSION = "2.9.0"
4
- STATE_SCHEMA_VERSION = 3
3
+ VERSION = "3.0.0"
5
4
 
6
- STEPS = [
7
- "constitution", # Pré-requis projet (1/projet). Auto-complétée si .specify/memory/constitution.md existe.
8
- "spec",
9
- "clarify",
10
- "review-spec",
11
- "research",
12
- "plan",
13
- "contracts",
14
- "tasks",
15
- "review-tasks",
16
- "implement",
17
- "review-impl",
18
- "docs",
19
- "done",
20
- ]
21
-
22
- OPTIONAL_STEPS = {"clarify", "research", "contracts", "docs"}
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
-
30
- REVIEW_GATES = {"review-spec", "review-tasks", "review-impl"}
31
-
32
- STEP_ARTIFACTS = {
33
- "spec": "spec.md",
34
- "clarify": "clarify-log.md",
35
- "review-spec": "review-log.md",
36
- "research": "research.md",
37
- "plan": "plan.md",
38
- "contracts": "contracts",
39
- "tasks": "tasks.md",
40
- "review-tasks": "review-log.md",
41
- "implement": None,
42
- "review-impl": "review-log.md",
43
- "docs": "docs.md",
44
- }
5
+ SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
45
6
 
46
- STEP_LINEAR_MAPPING = {
7
+ ACTIVITY_LINEAR_MAPPING = {
8
+ "assess": "Backlog",
9
+ "prepare": "Plan",
10
+ "risk-assessment": "Research",
47
11
  "spec": "Spec",
48
- "clarify": "Spec",
49
- "review-spec": "Review",
50
12
  "research": "Research",
51
- "plan": "Plan",
52
13
  "contracts": "Plan",
14
+ "plan": "Plan",
53
15
  "tasks": "Tasks",
54
- "review-tasks": "Review",
55
16
  "implement": "In Progress",
56
- "review-impl": "Review",
57
- "docs": "Doc",
17
+ "verify": "Review",
18
+ "review": "Review",
19
+ "rollback-check": "Review",
58
20
  "done": "Done",
59
21
  }
60
22
 
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 = [
23
+ LINEAR_STATUS_ORDER = [
64
24
  "Backlog",
65
25
  "Spec",
66
26
  "Research",
67
27
  "Plan",
68
28
  "Tasks",
69
- "Review",
70
29
  "In Progress",
71
- "Doc",
30
+ "Review",
72
31
  "Done",
73
32
  ]
74
33
 
75
- SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
76
-
77
- # Mapping speckit → devflow pour la migration
78
- SPECKIT_STEP_MAP = {
79
- "specify": "spec",
80
- "clarify": "clarify",
81
- "plan": "plan",
82
- "tasks": "tasks",
83
- "implement": "implement",
84
- }
85
-
86
- SPECKIT_IGNORED_STEPS = {"checklist", "analyze"}
87
-
88
34
  ARTIFACTS = [
35
+ "risk-assessment.md",
89
36
  "spec.md",
90
- "clarify-log.md",
91
37
  "research.md",
38
+ "contracts.md",
92
39
  "plan.md",
93
- "contracts/",
94
- "data-model.md",
95
- "quickstart.md",
96
40
  "tasks.md",
97
- "docs.md",
98
- "review-log.md",
99
- "analysis.md",
100
- "checklist.md",
41
+ "worklog.md",
42
+ "activity-contract.json",
43
+ "activity-result.json",
44
+ "verification.json",
101
45
  ]
102
46
 
103
- # --- Pipeline constants coherence checks ---
104
- _steps_set = set(STEPS)
105
- assert OPTIONAL_STEPS <= _steps_set, f"OPTIONAL_STEPS not subset of STEPS: {OPTIONAL_STEPS - _steps_set}"
106
- assert REVIEW_GATES <= _steps_set, f"REVIEW_GATES not subset of STEPS: {REVIEW_GATES - _steps_set}"
107
- assert set(STEP_ARTIFACTS.keys()) <= _steps_set, f"STEP_ARTIFACTS has unknown keys: {set(STEP_ARTIFACTS.keys()) - _steps_set}"
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
- )
113
- assert set(SPECKIT_STEP_MAP.values()) <= _steps_set, f"SPECKIT_STEP_MAP maps to unknown steps: {set(SPECKIT_STEP_MAP.values()) - _steps_set}"
47
+ assert set(ACTIVITY_LINEAR_MAPPING.values()) <= set(LINEAR_STATUS_ORDER)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import subprocess
6
7
  import re
7
8
  from abc import ABC, abstractmethod
@@ -9,6 +10,9 @@ from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
  from typing import Literal
11
12
 
13
+ from jsonschema.exceptions import SchemaError, ValidationError
14
+ from jsonschema.validators import validator_for
15
+
12
16
 
13
17
  PermissionMode = Literal["auto", "acceptEdits", "manual"]
14
18
  SandboxMode = Literal["read-only", "workspace-write"]
@@ -101,11 +105,29 @@ class AgentAdapter(ABC):
101
105
  capture_output=True,
102
106
  shell=False,
103
107
  )
104
- return AgentExecution(
108
+ execution = AgentExecution(
105
109
  result.returncode,
106
110
  result.stdout or "",
107
111
  result.stderr or "",
108
112
  )
113
+ if execution.returncode != 0:
114
+ return execution
115
+ try:
116
+ payload = self._structured_payload(execution.stdout, output_file)
117
+ _validate_structured_payload(payload, schema_file)
118
+ output_file.write_text(
119
+ json.dumps(payload, ensure_ascii=False), encoding="utf-8"
120
+ )
121
+ except (
122
+ OSError, ValueError, json.JSONDecodeError, SchemaError, ValidationError
123
+ ) as exc:
124
+ output_file.unlink(missing_ok=True)
125
+ return AgentExecution(
126
+ 1,
127
+ execution.stdout,
128
+ f"Sortie structuree invalide ({self.name}) : {exc}",
129
+ )
130
+ return execution
109
131
  except subprocess.TimeoutExpired as exc:
110
132
  return AgentExecution(
111
133
  124,
@@ -116,6 +138,11 @@ class AgentAdapter(ABC):
116
138
  except FileNotFoundError:
117
139
  return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable")
118
140
 
141
+ def _structured_payload(self, stdout: str, output_file: Path) -> object:
142
+ if output_file.is_file():
143
+ return json.loads(output_file.read_text(encoding="utf-8"))
144
+ return json.loads(stdout)
145
+
119
146
 
120
147
  class ClaudeCodeAdapter(AgentAdapter):
121
148
  name = "claude-code"
@@ -133,6 +160,40 @@ class ClaudeCodeAdapter(AgentAdapter):
133
160
  def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
134
161
  return [self.executable, "--print", "--agent", agent_name, prompt]
135
162
 
163
+ def build_structured_command(
164
+ self,
165
+ prompt: str,
166
+ permission_mode: PermissionMode,
167
+ schema_file: Path,
168
+ output_file: Path,
169
+ sandbox_mode: SandboxMode = "workspace-write",
170
+ ) -> list[str]:
171
+ del output_file
172
+ schema = schema_file.read_text(encoding="utf-8")
173
+ effective_mode = "plan" if sandbox_mode == "read-only" else permission_mode
174
+ return [
175
+ self.executable,
176
+ "--print",
177
+ "--permission-mode",
178
+ effective_mode,
179
+ "--output-format",
180
+ "json",
181
+ "--json-schema",
182
+ schema,
183
+ prompt,
184
+ ]
185
+
186
+ def _structured_payload(self, stdout: str, output_file: Path) -> object:
187
+ del output_file
188
+ envelope = json.loads(stdout)
189
+ if not isinstance(envelope, dict):
190
+ raise ValueError("enveloppe JSON Claude absente")
191
+ if envelope.get("subtype") not in {None, "success"}:
192
+ raise ValueError(f"execution Claude terminee avec {envelope.get('subtype')}")
193
+ if "structured_output" not in envelope:
194
+ raise ValueError("champ structured_output absent")
195
+ return envelope["structured_output"]
196
+
136
197
 
137
198
  class CodexAdapter(AgentAdapter):
138
199
  name = "codex"
@@ -181,6 +242,40 @@ class CursorAdapter(AgentAdapter):
181
242
  command.append(_portable_prompt(prompt))
182
243
  return command
183
244
 
245
+ def build_structured_command(
246
+ self,
247
+ prompt: str,
248
+ permission_mode: PermissionMode,
249
+ schema_file: Path,
250
+ output_file: Path,
251
+ sandbox_mode: SandboxMode = "workspace-write",
252
+ ) -> list[str]:
253
+ del output_file
254
+ schema = schema_file.read_text(encoding="utf-8")
255
+ structured_prompt = (
256
+ f"{prompt}\n\n"
257
+ "Retourne comme reponse finale uniquement un objet JSON valide "
258
+ "respectant exactement ce JSON Schema, sans bloc Markdown :\n"
259
+ f"{schema}"
260
+ )
261
+ command = [self.executable, "--print", "--output-format", "json"]
262
+ if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
263
+ command.append("--force")
264
+ command.append(structured_prompt)
265
+ return command
266
+
267
+ def _structured_payload(self, stdout: str, output_file: Path) -> object:
268
+ del output_file
269
+ envelope = json.loads(stdout)
270
+ if not isinstance(envelope, dict):
271
+ raise ValueError("enveloppe JSON Cursor absente")
272
+ if envelope.get("is_error") is True or envelope.get("subtype") not in {None, "success"}:
273
+ raise ValueError("execution Cursor terminee en erreur")
274
+ result = envelope.get("result")
275
+ if not isinstance(result, str):
276
+ raise ValueError("champ result Cursor absent")
277
+ return json.loads(_strip_json_fence(result))
278
+
184
279
 
185
280
  _AGENT_ADAPTERS = {
186
281
  "claude": ClaudeCodeAdapter,
@@ -218,3 +313,16 @@ def _text(value: str | bytes | None) -> str:
218
313
  if value is None:
219
314
  return ""
220
315
  return value.decode(errors="replace") if isinstance(value, bytes) else value
316
+
317
+
318
+ def _strip_json_fence(value: str) -> str:
319
+ text = value.strip()
320
+ match = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
321
+ return match.group(1).strip() if match else text
322
+
323
+
324
+ def _validate_structured_payload(payload: object, schema_file: Path) -> None:
325
+ schema = json.loads(schema_file.read_text(encoding="utf-8"))
326
+ validator_type = validator_for(schema)
327
+ validator_type.check_schema(schema)
328
+ validator_type(schema).validate(payload)
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from typing import Literal, Protocol
7
7
 
8
- from devflow_cli import STEP_LINEAR_MAPPING, STEP_LINEAR_ORDER
8
+ from devflow_cli import ACTIVITY_LINEAR_MAPPING, LINEAR_STATUS_ORDER
9
9
 
10
10
 
11
11
  LinearAction = Literal["update", "keep", "skip"]
@@ -22,7 +22,7 @@ class LinearAdapter(Protocol):
22
22
  @dataclass(frozen=True)
23
23
  class LinearDecision:
24
24
  action: LinearAction
25
- step: str
25
+ activity: str
26
26
  current_status: str | None
27
27
  target_status: str | None
28
28
  reason: str
@@ -34,7 +34,7 @@ class LinearDecision:
34
34
  def to_dict(self) -> dict[str, str | bool | None]:
35
35
  return {
36
36
  "action": self.action,
37
- "step": self.step,
37
+ "activity": self.activity,
38
38
  "currentStatus": self.current_status,
39
39
  "targetStatus": self.target_status,
40
40
  "shouldUpdate": self.should_update,
@@ -42,38 +42,42 @@ class LinearDecision:
42
42
  }
43
43
 
44
44
 
45
- def decide_linear_transition(step: str, current_status: str | None) -> LinearDecision:
45
+ def decide_linear_transition(activity: str, current_status: str | None) -> LinearDecision:
46
46
  """Applique le mapping et la règle de non-régression canonique."""
47
- target = STEP_LINEAR_MAPPING.get(step)
47
+ target = ACTIVITY_LINEAR_MAPPING.get(activity)
48
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:
49
+ return LinearDecision(
50
+ "skip", activity, current_status, None, "Activité sans statut Linear"
51
+ )
52
+ if current_status not in LINEAR_STATUS_ORDER:
51
53
  return LinearDecision(
52
54
  "skip",
53
- step,
55
+ activity,
54
56
  current_status,
55
57
  target,
56
58
  "Statut Linear courant absent de l'ordre canonique",
57
59
  )
58
- current_index = STEP_LINEAR_ORDER.index(current_status)
59
- target_index = STEP_LINEAR_ORDER.index(target)
60
+ current_index = LINEAR_STATUS_ORDER.index(current_status)
61
+ target_index = LINEAR_STATUS_ORDER.index(target)
60
62
  if current_index >= target_index:
61
63
  return LinearDecision(
62
64
  "keep",
63
- step,
65
+ activity,
64
66
  current_status,
65
67
  target,
66
68
  "Statut courant identique ou plus avancé",
67
69
  )
68
- return LinearDecision("update", step, current_status, target, "Progression autorisée")
70
+ return LinearDecision(
71
+ "update", activity, current_status, target, "Progression autorisée"
72
+ )
69
73
 
70
74
 
71
75
  def sync_linear_status(
72
- adapter: LinearAdapter, issue_id: str, step: str
76
+ adapter: LinearAdapter, issue_id: str, activity: str
73
77
  ) -> LinearDecision:
74
78
  """Synchronise via un transport injecté lorsque la politique l'autorise."""
75
79
  current_status = adapter.get_status(issue_id)
76
- decision = decide_linear_transition(step, current_status)
80
+ decision = decide_linear_transition(activity, current_status)
77
81
  if decision.should_update and decision.target_status is not None:
78
82
  adapter.update_status(issue_id, decision.target_status)
79
83
  return decision
devflow_cli/cli.py CHANGED
@@ -15,15 +15,10 @@ from devflow_cli.commands.export_cmd import export
15
15
  from devflow_cli.commands.stale import stale
16
16
  from devflow_cli.commands.migrate_cmd import migrate
17
17
  from devflow_cli.commands.upgrade_cmd import upgrade
18
- from devflow_cli.commands.rollback import rollback
19
- from devflow_cli.commands.migrate_speckit import migrate_speckit
20
18
  from devflow_cli.commands import extension
21
19
  from devflow_cli.commands import hooks_cmd
22
20
  from devflow_cli.commands import linear
23
- from devflow_cli.commands import review
24
- from devflow_cli.commands import step
25
21
  from devflow_cli.commands import adaptive
26
- from devflow_cli.commands.run import run
27
22
  from devflow_cli.commands.docs_sync import docs_sync
28
23
  from devflow_cli.commands.traceability import traceability
29
24
  from devflow_cli.commands.verify import verify
@@ -60,15 +55,10 @@ app.command(name="export")(export)
60
55
  app.command()(stale)
61
56
  app.command()(migrate)
62
57
  app.command()(upgrade)
63
- app.command()(rollback)
64
- app.command(name="migrate-speckit")(migrate_speckit)
65
58
  app.add_typer(extension.app, name="extension")
66
59
  app.add_typer(hooks_cmd.app, name="hooks")
67
60
  app.add_typer(linear.app, name="linear")
68
- app.add_typer(step.app, name="step")
69
- app.add_typer(review.app, name="review")
70
61
  app.add_typer(adaptive.app, name="adaptive")
71
- app.command()(run)
72
62
  app.command(name="docs-sync")(docs_sync)
73
63
  app.command()(traceability)
74
64
  app.command()(verify)