devflow-cli 4.0.2__py3-none-any.whl → 6.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 +2 -2
- devflow_cli/adapters/agents.py +75 -17
- devflow_cli/cli.py +0 -2
- devflow_cli/commands/adaptive.py +523 -110
- devflow_cli/commands/assess.py +0 -1
- devflow_cli/commands/check.py +41 -0
- devflow_cli/commands/feature.py +25 -3
- devflow_cli/commands/status.py +3 -3
- devflow_cli/commands/upgrade_cmd.py +14 -8
- devflow_cli/commands/verify.py +30 -0
- devflow_cli/core/activity_contracts.py +218 -23
- devflow_cli/core/adaptive_runner.py +559 -41
- devflow_cli/core/adaptive_workflow.py +90 -50
- devflow_cli/core/agent_failure.py +113 -0
- devflow_cli/core/assessment.py +17 -19
- devflow_cli/core/automatic_evidence.py +252 -71
- devflow_cli/core/contract_schemas.py +21 -31
- devflow_cli/core/docgen.py +6 -5
- devflow_cli/core/manifest.py +9 -1
- devflow_cli/core/omissions.py +168 -0
- devflow_cli/core/operational.py +5 -1
- devflow_cli/core/remediation.py +202 -25
- devflow_cli/core/validation_approvals.py +220 -0
- devflow_cli/core/validators.py +59 -9
- devflow_cli/core/verification.py +95 -8
- devflow_cli/core/worklog.py +14 -3
- devflow_cli/core/worktree_snapshot.py +17 -0
- devflow_cli/resources/commands/devflow.md +8 -4
- devflow_cli/resources/templates/plan-template.md +4 -4
- devflow_cli/schemas/{activity-contract-v2.schema.json → activity-contract-v3.schema.json} +3 -3
- devflow_cli/schemas/activity-result-v1.schema.json +13 -1
- devflow_cli/schemas/{assessment-v2.schema.json → assessment-v3.schema.json} +3 -3
- devflow_cli/schemas/{workflow-state-v3.schema.json → workflow-state-v4.schema.json} +23 -4
- devflow_cli/utils/paths.py +17 -2
- {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/METADATA +8 -23
- {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/RECORD +39 -41
- {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/WHEEL +1 -1
- devflow_cli/commands/migrate_cmd.py +0 -200
- devflow_cli/core/v4_migration.py +0 -134
- devflow_cli/schemas/activity-contract-v1.schema.json +0 -76
- devflow_cli/schemas/assessment-v1.schema.json +0 -60
- devflow_cli/schemas/workflow-state-v2.schema.json +0 -66
- {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-4.0.2.dist-info → devflow_cli-6.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 = "
|
|
3
|
+
VERSION = "6.0.0"
|
|
4
4
|
|
|
5
5
|
SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
|
|
6
6
|
|
|
@@ -42,7 +42,7 @@ ARTIFACTS = [
|
|
|
42
42
|
"preflight-evidence.md",
|
|
43
43
|
"worklog.md",
|
|
44
44
|
"activity-contract.json",
|
|
45
|
-
"
|
|
45
|
+
"implement-result.json",
|
|
46
46
|
"verification.json",
|
|
47
47
|
]
|
|
48
48
|
|
devflow_cli/adapters/agents.py
CHANGED
|
@@ -21,6 +21,7 @@ SandboxMode = Literal["read-only", "workspace-write"]
|
|
|
21
21
|
_SAFE_DIAGNOSTIC_MESSAGES = {
|
|
22
22
|
"final-output-empty": "--output-last-message est vide",
|
|
23
23
|
"final-output-absent": "--output-last-message est absent",
|
|
24
|
+
"agent-turn-failed": "le tour agent a echoue avant la reponse finale",
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
|
|
@@ -85,6 +86,7 @@ class AgentAdapter(ABC):
|
|
|
85
86
|
timeout=timeout,
|
|
86
87
|
text=True,
|
|
87
88
|
capture_output=capture_output,
|
|
89
|
+
stdin=subprocess.DEVNULL,
|
|
88
90
|
shell=False,
|
|
89
91
|
)
|
|
90
92
|
return AgentExecution(
|
|
@@ -123,6 +125,7 @@ class AgentAdapter(ABC):
|
|
|
123
125
|
timeout=timeout,
|
|
124
126
|
text=True,
|
|
125
127
|
capture_output=True,
|
|
128
|
+
stdin=subprocess.DEVNULL,
|
|
126
129
|
shell=False,
|
|
127
130
|
)
|
|
128
131
|
execution = AgentExecution(
|
|
@@ -133,7 +136,7 @@ class AgentAdapter(ABC):
|
|
|
133
136
|
final_output_status=_output_status(output_file),
|
|
134
137
|
final_output_bytes=_output_size(output_file),
|
|
135
138
|
final_output_diagnostic="process-failed" if result.returncode else "validating",
|
|
136
|
-
session_id=
|
|
139
|
+
session_id=self.session_id(result.stdout or "", result.stderr or ""),
|
|
137
140
|
)
|
|
138
141
|
if execution.returncode != 0:
|
|
139
142
|
return execution
|
|
@@ -200,8 +203,16 @@ class AgentAdapter(ABC):
|
|
|
200
203
|
except FileNotFoundError:
|
|
201
204
|
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable", duration_seconds=time.monotonic() - started)
|
|
202
205
|
|
|
203
|
-
def
|
|
204
|
-
|
|
206
|
+
def session_id(self, stdout: str, stderr: str) -> str | None:
|
|
207
|
+
"""Extrait un identifiant opaque de session, sans conserver la sortie."""
|
|
208
|
+
return _session_id(stdout, stderr)
|
|
209
|
+
|
|
210
|
+
def recover_structured_payload(self, raw_output: str) -> object:
|
|
211
|
+
"""Extrait la reponse structuree d'une trace brute conservee apres un echec."""
|
|
212
|
+
return self._structured_payload(raw_output, None)
|
|
213
|
+
|
|
214
|
+
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
215
|
+
if output_file is not None and output_file.is_file():
|
|
205
216
|
return json.loads(output_file.read_text(encoding="utf-8"))
|
|
206
217
|
return json.loads(stdout)
|
|
207
218
|
|
|
@@ -245,16 +256,19 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
245
256
|
prompt,
|
|
246
257
|
]
|
|
247
258
|
|
|
248
|
-
def _structured_payload(self, stdout: str, output_file: Path) -> object:
|
|
259
|
+
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
249
260
|
del output_file
|
|
250
261
|
envelope = json.loads(stdout)
|
|
251
262
|
if not isinstance(envelope, dict):
|
|
252
263
|
raise ValueError("enveloppe JSON Claude absente")
|
|
253
264
|
if envelope.get("subtype") not in {None, "success"}:
|
|
254
265
|
raise ValueError(f"execution Claude terminee avec {envelope.get('subtype')}")
|
|
255
|
-
if "structured_output"
|
|
256
|
-
|
|
257
|
-
|
|
266
|
+
if "structured_output" in envelope:
|
|
267
|
+
return envelope["structured_output"]
|
|
268
|
+
result = envelope.get("result")
|
|
269
|
+
if isinstance(result, str) and result.strip():
|
|
270
|
+
return json.loads(_strip_json_fence(result))
|
|
271
|
+
raise ValueError("champ structured_output absent")
|
|
258
272
|
|
|
259
273
|
|
|
260
274
|
class CodexAdapter(AgentAdapter):
|
|
@@ -284,6 +298,7 @@ class CodexAdapter(AgentAdapter):
|
|
|
284
298
|
else:
|
|
285
299
|
command.extend(["--sandbox", sandbox_mode])
|
|
286
300
|
command.extend([
|
|
301
|
+
"--json",
|
|
287
302
|
"--output-schema",
|
|
288
303
|
str(schema_file),
|
|
289
304
|
"--output-last-message",
|
|
@@ -292,20 +307,36 @@ class CodexAdapter(AgentAdapter):
|
|
|
292
307
|
command.append(prompt)
|
|
293
308
|
return command
|
|
294
309
|
|
|
295
|
-
def
|
|
296
|
-
"""
|
|
297
|
-
|
|
310
|
+
def session_id(self, stdout: str, stderr: str) -> str | None:
|
|
311
|
+
"""Prefere l'identifiant de fil annonce par le flux d'evenements."""
|
|
312
|
+
for event in json_line_events(stdout):
|
|
313
|
+
if event.get("type") == "thread.started" and isinstance(
|
|
314
|
+
event.get("thread_id"), str
|
|
315
|
+
):
|
|
316
|
+
return event["thread_id"]
|
|
317
|
+
return _session_id(stdout, stderr)
|
|
318
|
+
|
|
319
|
+
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
320
|
+
"""Lit la reponse finale Codex, sinon la recupere depuis le flux d'evenements."""
|
|
321
|
+
if output_file is not None and output_file.is_file():
|
|
298
322
|
payload = output_file.read_text(encoding="utf-8")
|
|
299
323
|
if payload.strip():
|
|
300
324
|
return json.loads(payload)
|
|
325
|
+
diagnostic = "final-output-empty"
|
|
326
|
+
message = "Codex a termine sans reponse finale structuree"
|
|
327
|
+
else:
|
|
328
|
+
diagnostic = "final-output-absent"
|
|
329
|
+
message = "Codex n'a pas produit le fichier de reponse finale structuree"
|
|
330
|
+
events = json_line_events(stdout)
|
|
331
|
+
if any(event.get("type") == "turn.failed" for event in events):
|
|
301
332
|
raise StructuredOutputError(
|
|
302
|
-
"
|
|
303
|
-
"Codex a
|
|
333
|
+
"agent-turn-failed",
|
|
334
|
+
"Codex a interrompu son tour avant la reponse finale",
|
|
304
335
|
)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
336
|
+
final_message = _codex_final_message(events)
|
|
337
|
+
if final_message is not None:
|
|
338
|
+
return json.loads(_strip_json_fence(final_message))
|
|
339
|
+
raise StructuredOutputError(diagnostic, message)
|
|
309
340
|
|
|
310
341
|
|
|
311
342
|
class CursorAdapter(AgentAdapter):
|
|
@@ -341,7 +372,7 @@ class CursorAdapter(AgentAdapter):
|
|
|
341
372
|
command.append(structured_prompt)
|
|
342
373
|
return command
|
|
343
374
|
|
|
344
|
-
def _structured_payload(self, stdout: str, output_file: Path) -> object:
|
|
375
|
+
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
345
376
|
del output_file
|
|
346
377
|
envelope = json.loads(stdout)
|
|
347
378
|
if not isinstance(envelope, dict):
|
|
@@ -423,6 +454,33 @@ def _session_id(stdout: str, stderr: str) -> str | None:
|
|
|
423
454
|
return match.group(1) if match else None
|
|
424
455
|
|
|
425
456
|
|
|
457
|
+
def json_line_events(stdout: str) -> list[dict]:
|
|
458
|
+
"""Decode un flux JSONL d'agent en ignorant les lignes non JSON ou non objet."""
|
|
459
|
+
events: list[dict] = []
|
|
460
|
+
for line in stdout.splitlines():
|
|
461
|
+
line = line.strip()
|
|
462
|
+
if not line.startswith("{"):
|
|
463
|
+
continue
|
|
464
|
+
try:
|
|
465
|
+
event = json.loads(line)
|
|
466
|
+
except json.JSONDecodeError:
|
|
467
|
+
continue
|
|
468
|
+
if isinstance(event, dict):
|
|
469
|
+
events.append(event)
|
|
470
|
+
return events
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _codex_final_message(events: list[dict]) -> str | None:
|
|
474
|
+
"""Retourne le dernier message d'agent complet du flux d'evenements."""
|
|
475
|
+
for event in reversed(events):
|
|
476
|
+
item = event.get("item")
|
|
477
|
+
if event.get("type") != "item.completed" or not isinstance(item, dict):
|
|
478
|
+
continue
|
|
479
|
+
if item.get("type") == "agent_message" and isinstance(item.get("text"), str):
|
|
480
|
+
return item["text"]
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
|
|
426
484
|
def _strip_json_fence(value: str) -> str:
|
|
427
485
|
text = value.strip()
|
|
428
486
|
match = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
|
devflow_cli/cli.py
CHANGED
|
@@ -13,7 +13,6 @@ from devflow_cli.commands.feature import feature
|
|
|
13
13
|
from devflow_cli.commands.context import context
|
|
14
14
|
from devflow_cli.commands.export_cmd import export
|
|
15
15
|
from devflow_cli.commands.stale import stale
|
|
16
|
-
from devflow_cli.commands.migrate_cmd import migrate
|
|
17
16
|
from devflow_cli.commands.upgrade_cmd import upgrade
|
|
18
17
|
from devflow_cli.commands import extension
|
|
19
18
|
from devflow_cli.commands import hooks_cmd
|
|
@@ -53,7 +52,6 @@ app.command()(feature)
|
|
|
53
52
|
app.command()(context)
|
|
54
53
|
app.command(name="export")(export)
|
|
55
54
|
app.command()(stale)
|
|
56
|
-
app.command()(migrate)
|
|
57
55
|
app.command()(upgrade)
|
|
58
56
|
app.add_typer(extension.app, name="extension")
|
|
59
57
|
app.add_typer(hooks_cmd.app, name="hooks")
|