devflow-cli 2.8.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.
- devflow_cli/__init__.py +20 -86
- devflow_cli/adapters/agents.py +181 -1
- devflow_cli/adapters/linear.py +18 -14
- devflow_cli/cli.py +4 -10
- devflow_cli/commands/adaptive.py +594 -0
- devflow_cli/commands/assess.py +117 -0
- devflow_cli/commands/check.py +45 -16
- devflow_cli/commands/docs_sync.py +2 -2
- devflow_cli/commands/export_cmd.py +15 -73
- devflow_cli/commands/feature.py +32 -47
- devflow_cli/commands/init_cmd.py +28 -12
- devflow_cli/commands/linear.py +2 -2
- devflow_cli/commands/migrate_cmd.py +170 -75
- devflow_cli/commands/stale.py +9 -9
- devflow_cli/commands/status.py +124 -61
- devflow_cli/commands/upgrade_cmd.py +9 -0
- devflow_cli/core/activity_contracts.py +656 -0
- devflow_cli/core/adaptive_runner.py +343 -0
- devflow_cli/core/adaptive_workflow.py +289 -0
- devflow_cli/core/assessment.py +273 -0
- devflow_cli/core/automatic_evidence.py +719 -0
- devflow_cli/core/contract_schemas.py +110 -0
- devflow_cli/core/docgen.py +29 -32
- devflow_cli/core/installer.py +62 -4
- devflow_cli/core/operational.py +37 -0
- devflow_cli/core/redaction.py +68 -0
- devflow_cli/core/remediation.py +158 -0
- devflow_cli/core/staleness.py +22 -35
- devflow_cli/core/validators.py +24 -0
- devflow_cli/core/verification.py +52 -7
- devflow_cli/core/waivers.py +285 -0
- devflow_cli/core/worklog.py +304 -0
- devflow_cli/core/worktree_snapshot.py +91 -0
- devflow_cli/resources/commands/devflow.context.md +57 -0
- devflow_cli/resources/commands/devflow.extension.md +63 -0
- devflow_cli/resources/commands/devflow.init.md +50 -0
- devflow_cli/resources/commands/devflow.md +25 -0
- devflow_cli/resources/commands/devflow.regen.md +34 -0
- devflow_cli/resources/commands/devflow.status.md +11 -0
- devflow_cli/resources/extensions/EXTENSION-API-REFERENCE.md +140 -0
- devflow_cli/resources/extensions/catalog.community.json +5 -0
- devflow_cli/resources/extensions/catalog.json +19 -0
- devflow_cli/resources/extensions/template/example-command.md +29 -0
- devflow_cli/resources/extensions/template/example-extension.json +18 -0
- devflow_cli/resources/templates/agent-context-template.md +29 -0
- devflow_cli/resources/templates/constitution-template.md +85 -0
- devflow_cli/resources/templates/contracts/README-template.md +24 -0
- devflow_cli/resources/templates/contracts/api-template.md +38 -0
- devflow_cli/resources/templates/contracts/components-template.md +28 -0
- devflow_cli/resources/templates/contracts/interfaces-template.md +20 -0
- devflow_cli/resources/templates/contracts/services-template.md +22 -0
- devflow_cli/resources/templates/contracts-template.md +105 -0
- devflow_cli/resources/templates/data-model-template.md +55 -0
- devflow_cli/resources/templates/devflow-hooks-template.yml +38 -0
- devflow_cli/resources/templates/extension-manifest-template.json +9 -0
- devflow_cli/resources/templates/plan-template.md +98 -0
- devflow_cli/resources/templates/quickstart-template.md +65 -0
- devflow_cli/resources/templates/research-template.md +51 -0
- devflow_cli/resources/templates/spec-template.md +79 -0
- devflow_cli/resources/templates/tasks-template.md +105 -0
- devflow_cli/schemas/activity-contract-v1.schema.json +76 -0
- devflow_cli/schemas/activity-result-v1.schema.json +85 -0
- devflow_cli/schemas/assessment-v1.schema.json +60 -0
- devflow_cli/schemas/workflow-state-v2.schema.json +66 -0
- devflow_cli/utils/paths.py +23 -12
- devflow_cli-3.0.0.dist-info/METADATA +113 -0
- devflow_cli-3.0.0.dist-info/RECORD +88 -0
- devflow_cli/commands/migrate_speckit.py +0 -344
- devflow_cli/commands/review.py +0 -54
- devflow_cli/commands/rollback.py +0 -53
- devflow_cli/commands/run.py +0 -123
- devflow_cli/commands/step.py +0 -118
- devflow_cli/core/orchestrator.py +0 -170
- devflow_cli/core/state.py +0 -576
- devflow_cli/core/state_store.py +0 -45
- devflow_cli/schemas/state.schema.json +0 -39
- devflow_cli-2.8.0.dist-info/METADATA +0 -149
- devflow_cli-2.8.0.dist-info/RECORD +0 -52
- {devflow_cli-2.8.0.dist-info → devflow_cli-3.0.0.dist-info}/WHEEL +0 -0
- {devflow_cli-2.8.0.dist-info → devflow_cli-3.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-2.8.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 —
|
|
1
|
+
"""devflow CLI — workflow de développement adaptatif."""
|
|
2
2
|
|
|
3
|
-
VERSION = "
|
|
4
|
-
STATE_SCHEMA_VERSION = 3
|
|
3
|
+
VERSION = "3.0.0"
|
|
5
4
|
|
|
6
|
-
|
|
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"}
|
|
5
|
+
SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
|
|
31
6
|
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
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
|
-
}
|
|
45
|
-
|
|
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
|
-
"
|
|
57
|
-
"
|
|
17
|
+
"verify": "Review",
|
|
18
|
+
"review": "Review",
|
|
19
|
+
"rollback-check": "Review",
|
|
58
20
|
"done": "Done",
|
|
59
21
|
}
|
|
60
22
|
|
|
61
|
-
|
|
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
|
-
"
|
|
30
|
+
"Review",
|
|
72
31
|
"Done",
|
|
73
32
|
]
|
|
74
33
|
|
|
75
|
-
SUPPORTED_AGENTS = ["claude-code", "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
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
41
|
+
"worklog.md",
|
|
42
|
+
"activity-contract.json",
|
|
43
|
+
"activity-result.json",
|
|
44
|
+
"verification.json",
|
|
101
45
|
]
|
|
102
46
|
|
|
103
|
-
|
|
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)
|
devflow_cli/adapters/agents.py
CHANGED
|
@@ -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,8 +10,12 @@ 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"]
|
|
18
|
+
SandboxMode = Literal["read-only", "workspace-write"]
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
@dataclass(frozen=True)
|
|
@@ -34,6 +39,16 @@ class AgentAdapter(ABC):
|
|
|
34
39
|
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
35
40
|
raise ValueError(f"L'adaptateur {self.name} ne supporte pas les agents nommés")
|
|
36
41
|
|
|
42
|
+
def build_structured_command(
|
|
43
|
+
self,
|
|
44
|
+
prompt: str,
|
|
45
|
+
permission_mode: PermissionMode,
|
|
46
|
+
schema_file: Path,
|
|
47
|
+
output_file: Path,
|
|
48
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
49
|
+
) -> list[str]:
|
|
50
|
+
raise ValueError(f"L'adaptateur {self.name} ne supporte pas les sorties structurees")
|
|
51
|
+
|
|
37
52
|
def execute(
|
|
38
53
|
self,
|
|
39
54
|
prompt: str,
|
|
@@ -68,6 +83,66 @@ class AgentAdapter(ABC):
|
|
|
68
83
|
except FileNotFoundError:
|
|
69
84
|
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable")
|
|
70
85
|
|
|
86
|
+
def execute_structured(
|
|
87
|
+
self,
|
|
88
|
+
prompt: str,
|
|
89
|
+
cwd: Path,
|
|
90
|
+
timeout: int,
|
|
91
|
+
schema_file: Path,
|
|
92
|
+
output_file: Path,
|
|
93
|
+
permission_mode: PermissionMode = "auto",
|
|
94
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
95
|
+
) -> AgentExecution:
|
|
96
|
+
command = self.build_structured_command(
|
|
97
|
+
prompt, permission_mode, schema_file, output_file, sandbox_mode
|
|
98
|
+
)
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
command,
|
|
102
|
+
cwd=cwd,
|
|
103
|
+
timeout=timeout,
|
|
104
|
+
text=True,
|
|
105
|
+
capture_output=True,
|
|
106
|
+
shell=False,
|
|
107
|
+
)
|
|
108
|
+
execution = AgentExecution(
|
|
109
|
+
result.returncode,
|
|
110
|
+
result.stdout or "",
|
|
111
|
+
result.stderr or "",
|
|
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
|
|
131
|
+
except subprocess.TimeoutExpired as exc:
|
|
132
|
+
return AgentExecution(
|
|
133
|
+
124,
|
|
134
|
+
_text(exc.stdout),
|
|
135
|
+
_text(exc.stderr),
|
|
136
|
+
timed_out=True,
|
|
137
|
+
)
|
|
138
|
+
except FileNotFoundError:
|
|
139
|
+
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable")
|
|
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
|
+
|
|
71
146
|
|
|
72
147
|
class ClaudeCodeAdapter(AgentAdapter):
|
|
73
148
|
name = "claude-code"
|
|
@@ -85,18 +160,76 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
85
160
|
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
86
161
|
return [self.executable, "--print", "--agent", agent_name, prompt]
|
|
87
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
|
+
|
|
88
197
|
|
|
89
198
|
class CodexAdapter(AgentAdapter):
|
|
90
199
|
name = "codex"
|
|
91
200
|
executable = "codex"
|
|
92
201
|
|
|
93
202
|
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
94
|
-
command = [self.executable, "exec"
|
|
203
|
+
command = [self.executable, "exec"]
|
|
95
204
|
if permission_mode in {"auto", "acceptEdits"}:
|
|
96
205
|
command.append("--approve-for-me")
|
|
206
|
+
else:
|
|
207
|
+
command.extend(["--sandbox", "workspace-write"])
|
|
97
208
|
command.append(_portable_prompt(prompt))
|
|
98
209
|
return command
|
|
99
210
|
|
|
211
|
+
def build_structured_command(
|
|
212
|
+
self,
|
|
213
|
+
prompt: str,
|
|
214
|
+
permission_mode: PermissionMode,
|
|
215
|
+
schema_file: Path,
|
|
216
|
+
output_file: Path,
|
|
217
|
+
sandbox_mode: SandboxMode = "workspace-write",
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
command = [self.executable, "exec"]
|
|
220
|
+
if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
|
|
221
|
+
command.append("--approve-for-me")
|
|
222
|
+
else:
|
|
223
|
+
command.extend(["--sandbox", sandbox_mode])
|
|
224
|
+
command.extend([
|
|
225
|
+
"--output-schema",
|
|
226
|
+
str(schema_file),
|
|
227
|
+
"--output-last-message",
|
|
228
|
+
str(output_file),
|
|
229
|
+
])
|
|
230
|
+
command.append(prompt)
|
|
231
|
+
return command
|
|
232
|
+
|
|
100
233
|
|
|
101
234
|
class CursorAdapter(AgentAdapter):
|
|
102
235
|
name = "cursor"
|
|
@@ -109,6 +242,40 @@ class CursorAdapter(AgentAdapter):
|
|
|
109
242
|
command.append(_portable_prompt(prompt))
|
|
110
243
|
return command
|
|
111
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
|
+
|
|
112
279
|
|
|
113
280
|
_AGENT_ADAPTERS = {
|
|
114
281
|
"claude": ClaudeCodeAdapter,
|
|
@@ -146,3 +313,16 @@ def _text(value: str | bytes | None) -> str:
|
|
|
146
313
|
if value is None:
|
|
147
314
|
return ""
|
|
148
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)
|
devflow_cli/adapters/linear.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
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 =
|
|
47
|
+
target = ACTIVITY_LINEAR_MAPPING.get(activity)
|
|
48
48
|
if target is None:
|
|
49
|
-
return LinearDecision(
|
|
50
|
-
|
|
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
|
-
|
|
55
|
+
activity,
|
|
54
56
|
current_status,
|
|
55
57
|
target,
|
|
56
58
|
"Statut Linear courant absent de l'ordre canonique",
|
|
57
59
|
)
|
|
58
|
-
current_index =
|
|
59
|
-
target_index =
|
|
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
|
-
|
|
65
|
+
activity,
|
|
64
66
|
current_status,
|
|
65
67
|
target,
|
|
66
68
|
"Statut courant identique ou plus avancé",
|
|
67
69
|
)
|
|
68
|
-
return LinearDecision(
|
|
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,
|
|
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(
|
|
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,17 +15,14 @@ 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
|
|
24
|
-
from devflow_cli.commands import step
|
|
25
|
-
from devflow_cli.commands.run import run
|
|
21
|
+
from devflow_cli.commands import adaptive
|
|
26
22
|
from devflow_cli.commands.docs_sync import docs_sync
|
|
27
23
|
from devflow_cli.commands.traceability import traceability
|
|
28
24
|
from devflow_cli.commands.verify import verify
|
|
25
|
+
from devflow_cli.commands.assess import assess
|
|
29
26
|
|
|
30
27
|
app = typer.Typer(
|
|
31
28
|
name="devflow",
|
|
@@ -58,17 +55,14 @@ app.command(name="export")(export)
|
|
|
58
55
|
app.command()(stale)
|
|
59
56
|
app.command()(migrate)
|
|
60
57
|
app.command()(upgrade)
|
|
61
|
-
app.command()(rollback)
|
|
62
|
-
app.command(name="migrate-speckit")(migrate_speckit)
|
|
63
58
|
app.add_typer(extension.app, name="extension")
|
|
64
59
|
app.add_typer(hooks_cmd.app, name="hooks")
|
|
65
60
|
app.add_typer(linear.app, name="linear")
|
|
66
|
-
app.add_typer(
|
|
67
|
-
app.add_typer(review.app, name="review")
|
|
68
|
-
app.command()(run)
|
|
61
|
+
app.add_typer(adaptive.app, name="adaptive")
|
|
69
62
|
app.command(name="docs-sync")(docs_sync)
|
|
70
63
|
app.command()(traceability)
|
|
71
64
|
app.command()(verify)
|
|
65
|
+
app.command()(assess)
|
|
72
66
|
|
|
73
67
|
|
|
74
68
|
if __name__ == "__main__":
|