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.
Files changed (44) hide show
  1. devflow_cli/__init__.py +2 -2
  2. devflow_cli/adapters/agents.py +75 -17
  3. devflow_cli/cli.py +0 -2
  4. devflow_cli/commands/adaptive.py +523 -110
  5. devflow_cli/commands/assess.py +0 -1
  6. devflow_cli/commands/check.py +41 -0
  7. devflow_cli/commands/feature.py +25 -3
  8. devflow_cli/commands/status.py +3 -3
  9. devflow_cli/commands/upgrade_cmd.py +14 -8
  10. devflow_cli/commands/verify.py +30 -0
  11. devflow_cli/core/activity_contracts.py +218 -23
  12. devflow_cli/core/adaptive_runner.py +559 -41
  13. devflow_cli/core/adaptive_workflow.py +90 -50
  14. devflow_cli/core/agent_failure.py +113 -0
  15. devflow_cli/core/assessment.py +17 -19
  16. devflow_cli/core/automatic_evidence.py +252 -71
  17. devflow_cli/core/contract_schemas.py +21 -31
  18. devflow_cli/core/docgen.py +6 -5
  19. devflow_cli/core/manifest.py +9 -1
  20. devflow_cli/core/omissions.py +168 -0
  21. devflow_cli/core/operational.py +5 -1
  22. devflow_cli/core/remediation.py +202 -25
  23. devflow_cli/core/validation_approvals.py +220 -0
  24. devflow_cli/core/validators.py +59 -9
  25. devflow_cli/core/verification.py +95 -8
  26. devflow_cli/core/worklog.py +14 -3
  27. devflow_cli/core/worktree_snapshot.py +17 -0
  28. devflow_cli/resources/commands/devflow.md +8 -4
  29. devflow_cli/resources/templates/plan-template.md +4 -4
  30. devflow_cli/schemas/{activity-contract-v2.schema.json → activity-contract-v3.schema.json} +3 -3
  31. devflow_cli/schemas/activity-result-v1.schema.json +13 -1
  32. devflow_cli/schemas/{assessment-v2.schema.json → assessment-v3.schema.json} +3 -3
  33. devflow_cli/schemas/{workflow-state-v3.schema.json → workflow-state-v4.schema.json} +23 -4
  34. devflow_cli/utils/paths.py +17 -2
  35. {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/METADATA +8 -23
  36. {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/RECORD +39 -41
  37. {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/WHEEL +1 -1
  38. devflow_cli/commands/migrate_cmd.py +0 -200
  39. devflow_cli/core/v4_migration.py +0 -134
  40. devflow_cli/schemas/activity-contract-v1.schema.json +0 -76
  41. devflow_cli/schemas/assessment-v1.schema.json +0 -60
  42. devflow_cli/schemas/workflow-state-v2.schema.json +0 -66
  43. {devflow_cli-4.0.2.dist-info → devflow_cli-6.0.0.dist-info}/entry_points.txt +0 -0
  44. {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 = "4.0.2"
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
- "activity-result.json",
45
+ "implement-result.json",
46
46
  "verification.json",
47
47
  ]
48
48
 
@@ -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=_session_id(result.stdout or "", result.stderr or ""),
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 _structured_payload(self, stdout: str, output_file: Path) -> object:
204
- if output_file.is_file():
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" not in envelope:
256
- raise ValueError("champ structured_output absent")
257
- return envelope["structured_output"]
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 _structured_payload(self, stdout: str, output_file: Path) -> object:
296
- """Lit la reponse finale Codex et distingue son absence d'un JSON invalide."""
297
- if output_file.is_file():
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
- "final-output-empty",
303
- "Codex a termine sans reponse finale structuree",
333
+ "agent-turn-failed",
334
+ "Codex a interrompu son tour avant la reponse finale",
304
335
  )
305
- raise StructuredOutputError(
306
- "final-output-absent",
307
- "Codex n'a pas produit le fichier de reponse finale structuree",
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")