devflow-cli 9.0.0__py3-none-any.whl → 10.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 (65) hide show
  1. devflow_cli/__init__.py +1 -3
  2. devflow_cli/adapters/__init__.py +0 -4
  3. devflow_cli/adapters/agents.py +6 -272
  4. devflow_cli/adapters/linear.py +0 -10
  5. devflow_cli/cli.py +0 -18
  6. devflow_cli/commands/adaptive.py +26 -264
  7. devflow_cli/commands/check.py +3 -76
  8. devflow_cli/commands/feature.py +22 -0
  9. devflow_cli/commands/feedback.py +1 -24
  10. devflow_cli/commands/init_cmd.py +25 -65
  11. devflow_cli/commands/upgrade_cmd.py +14 -4
  12. devflow_cli/core/adaptive_runner.py +11 -80
  13. devflow_cli/core/adaptive_workflow.py +0 -7
  14. devflow_cli/core/automatic_evidence.py +1 -1
  15. devflow_cli/core/feedback.py +0 -145
  16. devflow_cli/core/git.py +0 -6
  17. devflow_cli/core/installer.py +50 -63
  18. devflow_cli/core/manifest.py +2 -13
  19. devflow_cli/core/staleness.py +0 -22
  20. devflow_cli/core/telemetry.py +1 -91
  21. devflow_cli/core/verification.py +0 -88
  22. devflow_cli/core/waivers.py +0 -189
  23. devflow_cli/core/worklog.py +27 -6
  24. devflow_cli/resources/commands/devflow.md +4 -4
  25. {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/METADATA +8 -17
  26. devflow_cli-10.0.0.dist-info/RECORD +62 -0
  27. devflow_cli/commands/context.py +0 -178
  28. devflow_cli/commands/export_cmd.py +0 -91
  29. devflow_cli/commands/extension.py +0 -139
  30. devflow_cli/commands/hooks_cmd.py +0 -197
  31. devflow_cli/commands/linear.py +0 -30
  32. devflow_cli/commands/stale.py +0 -93
  33. devflow_cli/commands/traceability.py +0 -47
  34. devflow_cli/commands/usage.py +0 -92
  35. devflow_cli/commands/verify.py +0 -111
  36. devflow_cli/core/catalog.py +0 -105
  37. devflow_cli/core/hooks.py +0 -237
  38. devflow_cli/core/scanner.py +0 -140
  39. devflow_cli/resources/commands/devflow.context.md +0 -57
  40. devflow_cli/resources/commands/devflow.extension.md +0 -63
  41. devflow_cli/resources/commands/devflow.init.md +0 -50
  42. devflow_cli/resources/commands/devflow.regen.md +0 -34
  43. devflow_cli/resources/extensions/EXTENSION-API-REFERENCE.md +0 -140
  44. devflow_cli/resources/extensions/catalog.community.json +0 -5
  45. devflow_cli/resources/extensions/catalog.json +0 -19
  46. devflow_cli/resources/extensions/template/example-command.md +0 -29
  47. devflow_cli/resources/extensions/template/example-extension.json +0 -18
  48. devflow_cli/resources/templates/agent-context-template.md +0 -29
  49. devflow_cli/resources/templates/contracts/README-template.md +0 -24
  50. devflow_cli/resources/templates/contracts/api-template.md +0 -38
  51. devflow_cli/resources/templates/contracts/components-template.md +0 -28
  52. devflow_cli/resources/templates/contracts/interfaces-template.md +0 -20
  53. devflow_cli/resources/templates/contracts/services-template.md +0 -22
  54. devflow_cli/resources/templates/contracts-template.md +0 -105
  55. devflow_cli/resources/templates/data-model-template.md +0 -55
  56. devflow_cli/resources/templates/devflow-hooks-template.yml +0 -38
  57. devflow_cli/resources/templates/extension-manifest-template.json +0 -9
  58. devflow_cli/resources/templates/plan-template.md +0 -97
  59. devflow_cli/resources/templates/quickstart-template.md +0 -64
  60. devflow_cli/resources/templates/spec-template.md +0 -79
  61. devflow_cli/resources/templates/tasks-template.md +0 -105
  62. devflow_cli-9.0.0.dist-info/RECORD +0 -97
  63. {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/WHEEL +0 -0
  64. {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/entry_points.txt +0 -0
  65. {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py CHANGED
@@ -1,8 +1,6 @@
1
1
  """devflow CLI — workflow de développement adaptatif."""
2
2
 
3
- VERSION = "9.0.0"
4
-
5
- SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
3
+ VERSION = "10.0.0"
6
4
 
7
5
  ACTIVITY_LINEAR_MAPPING = {
8
6
  "assess": "Backlog",
@@ -4,8 +4,6 @@ from devflow_cli.adapters.agents import (
4
4
  AgentAdapter,
5
5
  AgentExecution,
6
6
  ClaudeCodeAdapter,
7
- CodexAdapter,
8
- CursorAdapter,
9
7
  get_agent_adapter,
10
8
  )
11
9
 
@@ -13,7 +11,5 @@ __all__ = [
13
11
  "AgentAdapter",
14
12
  "AgentExecution",
15
13
  "ClaudeCodeAdapter",
16
- "CodexAdapter",
17
- "CursorAdapter",
18
14
  "get_agent_adapter",
19
15
  ]
@@ -3,13 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import os
7
6
  import subprocess
8
7
  import re
9
8
  import time
10
9
  from abc import ABC, abstractmethod
11
10
  from dataclasses import dataclass, replace
12
- from datetime import datetime, timedelta
13
11
  from pathlib import Path
14
12
  from typing import Literal
15
13
 
@@ -23,8 +21,9 @@ PermissionMode = Literal["auto", "acceptEdits", "manual"]
23
21
  SandboxMode = Literal["read-only", "workspace-write"]
24
22
 
25
23
  # L'agent a termine sans erreur (code 0) mais sa sortie structuree manque ou n'est
26
- # pas le JSON attendu. Claude porte le diagnostic dedie ; Codex et Cursor gardent
27
- # leurs valeurs detaillees, traitees comme la meme famille par la reprise guidee.
24
+ # pas le JSON attendu. Claude porte le diagnostic dedie ; les autres valeurs restent
25
+ # lisibles dans un agent-execution.json ecrit par une version anterieure, et sont
26
+ # traitees comme la meme famille par la reprise guidee.
28
27
  STRUCTURED_OUTPUT_MISSING = "structured-output-missing"
29
28
  MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS = frozenset({
30
29
  STRUCTURED_OUTPUT_MISSING,
@@ -40,13 +39,6 @@ STRUCTURED_OUTPUT_MISSING_MESSAGE = (
40
39
  # aussi ses commandes shell en arriere-plan.
41
40
  CLAUDE_FOREGROUND_ENV = "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS"
42
41
 
43
- _SAFE_DIAGNOSTIC_MESSAGES = {
44
- "final-output-empty": "--output-last-message est vide",
45
- "final-output-absent": "--output-last-message est absent",
46
- "agent-turn-failed": "le tour agent a echoue avant la reponse finale",
47
- }
48
-
49
-
50
42
  @dataclass(frozen=True)
51
43
  class QuotaWindow:
52
44
  """Evolution d'une fenetre de quota observee pendant une session agent."""
@@ -111,9 +103,6 @@ class AgentAdapter(ABC):
111
103
  def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
112
104
  """Construit une commande sans passer par un shell."""
113
105
 
114
- def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
115
- raise ValueError(f"L'adaptateur {self.name} ne supporte pas les agents nommés")
116
-
117
106
  def build_structured_command(
118
107
  self,
119
108
  prompt: str,
@@ -243,8 +232,6 @@ class AgentAdapter(ABC):
243
232
  + (
244
233
  f" : {self.missing_output_message(execution.stdout)}"
245
234
  if diagnostic == STRUCTURED_OUTPUT_MISSING
246
- else f" : {_SAFE_DIAGNOSTIC_MESSAGES[diagnostic]}"
247
- if diagnostic in _SAFE_DIAGNOSTIC_MESSAGES
248
235
  else ""
249
236
  ),
250
237
  duration_seconds=execution.duration_seconds,
@@ -330,9 +317,6 @@ class ClaudeCodeAdapter(AgentAdapter):
330
317
  prompt,
331
318
  ]
332
319
 
333
- def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
334
- return [self.executable, "--print", "--agent", agent_name, prompt]
335
-
336
320
  def build_structured_command(
337
321
  self,
338
322
  prompt: str,
@@ -413,153 +397,13 @@ class ClaudeCodeAdapter(AgentAdapter):
413
397
  )
414
398
 
415
399
 
416
- class CodexAdapter(AgentAdapter):
417
- name = "codex"
418
- executable = "codex"
419
-
420
- def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
421
- command = [self.executable, "exec"]
422
- if permission_mode in {"auto", "acceptEdits"}:
423
- command.append("--approve-for-me")
424
- else:
425
- command.extend(["--sandbox", "workspace-write"])
426
- command.append(_portable_prompt(prompt))
427
- return command
428
-
429
- def build_structured_command(
430
- self,
431
- prompt: str,
432
- permission_mode: PermissionMode,
433
- schema_file: Path,
434
- output_file: Path,
435
- sandbox_mode: SandboxMode = "workspace-write",
436
- ) -> list[str]:
437
- command = [self.executable, "exec"]
438
- if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
439
- command.append("--approve-for-me")
440
- else:
441
- command.extend(["--sandbox", sandbox_mode])
442
- command.extend([
443
- "--json",
444
- "--output-schema",
445
- str(schema_file),
446
- "--output-last-message",
447
- str(output_file),
448
- ])
449
- command.append(prompt)
450
- return command
451
-
452
- def session_id(self, stdout: str, stderr: str) -> str | None:
453
- """Prefere l'identifiant de fil annonce par le flux d'evenements."""
454
- for event in json_line_events(stdout):
455
- if event.get("type") == "thread.started" and isinstance(
456
- event.get("thread_id"), str
457
- ):
458
- return event["thread_id"]
459
- return _session_id(stdout, stderr)
460
-
461
- def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
462
- """Lit la reponse finale Codex, sinon la recupere depuis le flux d'evenements."""
463
- if output_file is not None and output_file.is_file():
464
- payload = output_file.read_text(encoding="utf-8")
465
- if payload.strip():
466
- return json.loads(payload)
467
- diagnostic = "final-output-empty"
468
- message = "Codex a termine sans reponse finale structuree"
469
- else:
470
- diagnostic = "final-output-absent"
471
- message = "Codex n'a pas produit le fichier de reponse finale structuree"
472
- events = json_line_events(stdout)
473
- if any(event.get("type") == "turn.failed" for event in events):
474
- raise StructuredOutputError(
475
- "agent-turn-failed",
476
- "Codex a interrompu son tour avant la reponse finale",
477
- )
478
- final_message = _codex_final_message(events)
479
- if final_message is not None:
480
- return json.loads(_strip_json_fence(final_message))
481
- raise StructuredOutputError(diagnostic, message)
482
-
483
- def usage(self, stdout: str, session_id: str | None) -> AgentUsage | None:
484
- """Combine le flux --json et, au mieux, le journal de session Codex.
485
-
486
- Le flux ne donne que les tokens ; le modele, l'effort, le nombre de
487
- requetes et les quotas ne figurent que dans le journal de session, dont
488
- le format est interne a Codex.
489
- """
490
- totals = [
491
- event["usage"]
492
- for event in json_line_events(stdout)
493
- if event.get("type") == "turn.completed" and isinstance(event.get("usage"), dict)
494
- ]
495
- session = _codex_session_facts(session_id)
496
- if not totals and session is None:
497
- return None
498
- raw_input = sum(_int(item.get("input_tokens")) for item in totals)
499
- cached = sum(_int(item.get("cached_input_tokens")) for item in totals)
500
- usage = AgentUsage(
501
- inputTokens=max(raw_input - cached, 0),
502
- cacheReadTokens=cached,
503
- cacheWriteTokens=sum(_int(item.get("cache_write_input_tokens")) for item in totals),
504
- outputTokens=sum(_int(item.get("output_tokens")) for item in totals),
505
- reasoningTokens=sum(_int(item.get("reasoning_output_tokens")) for item in totals),
506
- )
507
- return replace(usage, **session) if session else usage
508
-
509
-
510
- class CursorAdapter(AgentAdapter):
511
- name = "cursor"
512
- executable = "cursor-agent"
513
-
514
- def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
515
- command = [self.executable, "--print", "--output-format", "text"]
516
- if permission_mode in {"auto", "acceptEdits"}:
517
- command.append("--force")
518
- command.append(_portable_prompt(prompt))
519
- return command
520
-
521
- def build_structured_command(
522
- self,
523
- prompt: str,
524
- permission_mode: PermissionMode,
525
- schema_file: Path,
526
- output_file: Path,
527
- sandbox_mode: SandboxMode = "workspace-write",
528
- ) -> list[str]:
529
- del output_file
530
- schema = schema_file.read_text(encoding="utf-8")
531
- structured_prompt = (
532
- f"{prompt}\n\n"
533
- "Retourne comme reponse finale uniquement un objet JSON valide "
534
- "respectant exactement ce JSON Schema, sans bloc Markdown :\n"
535
- f"{schema}"
536
- )
537
- command = [self.executable, "--print", "--output-format", "json"]
538
- if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
539
- command.append("--force")
540
- command.append(structured_prompt)
541
- return command
542
-
543
- def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
544
- del output_file
545
- envelope = json.loads(stdout)
546
- if not isinstance(envelope, dict):
547
- raise ValueError("enveloppe JSON Cursor absente")
548
- if envelope.get("is_error") is True or envelope.get("subtype") not in {None, "success"}:
549
- raise ValueError("execution Cursor terminee en erreur")
550
- result = envelope.get("result")
551
- if not isinstance(result, str):
552
- raise ValueError("champ result Cursor absent")
553
- return json.loads(_strip_json_fence(result))
554
-
555
-
556
400
  _AGENT_ADAPTERS = {
557
401
  "claude": ClaudeCodeAdapter,
558
402
  "claude-code": ClaudeCodeAdapter,
559
- "codex": CodexAdapter,
560
- "cursor": CursorAdapter,
561
- "cursor-agent": CursorAdapter,
562
403
  }
404
+ # Nom ecrit dans les enregistrements (fiche de tentative, agent-execution.json, telemetrie).
405
+ AGENT_NAME = "claude"
406
+ SUPPORTED_AGENT_NAMES = frozenset(_AGENT_ADAPTERS)
563
407
 
564
408
 
565
409
  def get_agent_adapter(name: str) -> AgentAdapter:
@@ -579,20 +423,6 @@ def describe_missing_output(agent: str, stdout: str) -> str:
579
423
  return STRUCTURED_OUTPUT_MISSING_MESSAGE
580
424
 
581
425
 
582
- def _portable_prompt(prompt: str) -> str:
583
- """Traduit une slash-command Claude en instruction portable pour les autres agents."""
584
- match = re.fullmatch(r"/devflow\.([a-z0-9-]+)(?:\s+(.*))?", prompt.strip())
585
- if not match:
586
- return prompt
587
- command_name, arguments = match.groups()
588
- arguments = arguments or ""
589
- return (
590
- f"Exécute strictement le workflow décrit dans "
591
- f"commands/devflow.{command_name}.md avec $ARGUMENTS = {arguments!r}. "
592
- "Travaille dans le dépôt courant et respecte toutes les validations de ce fichier."
593
- )
594
-
595
-
596
426
  def _claude_compatible_schema(schema_file: Path) -> str:
597
427
  """Adapte l'en-tete Draft 2020-12 au format accepte par Claude Code."""
598
428
  raw_schema = schema_file.read_text(encoding="utf-8")
@@ -646,17 +476,6 @@ def json_line_events(stdout: str) -> list[dict]:
646
476
  return events
647
477
 
648
478
 
649
- def _codex_final_message(events: list[dict]) -> str | None:
650
- """Retourne le dernier message d'agent complet du flux d'evenements."""
651
- for event in reversed(events):
652
- item = event.get("item")
653
- if event.get("type") != "item.completed" or not isinstance(item, dict):
654
- continue
655
- if item.get("type") == "agent_message" and isinstance(item.get("text"), str):
656
- return item["text"]
657
- return None
658
-
659
-
660
479
  def _strip_json_fence(value: str) -> str:
661
480
  text = value.strip()
662
481
  match = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
@@ -676,88 +495,3 @@ def _int(value: object) -> int:
676
495
 
677
496
  def _count(value: object) -> int | None:
678
497
  return value if isinstance(value, int) and not isinstance(value, bool) else None
679
-
680
-
681
- def _codex_home() -> Path:
682
- return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
683
-
684
-
685
- def _codex_session_file(session_id: str | None) -> Path | None:
686
- """Retrouve le journal d'une session Codex recente a partir de son identifiant."""
687
- if not session_id or not re.fullmatch(r"[A-Za-z0-9-]{8,}", session_id):
688
- return None
689
- sessions = _codex_home() / "sessions"
690
- today = datetime.now()
691
- for offset in range(3):
692
- day = today - timedelta(days=offset)
693
- directory = sessions / f"{day:%Y}" / f"{day:%m}" / f"{day:%d}"
694
- matches = sorted(directory.glob(f"rollout-*{session_id}.jsonl"))
695
- if matches:
696
- return matches[-1]
697
- return None
698
-
699
-
700
- def _codex_session_facts(session_id: str | None) -> dict | None:
701
- """Extrait modele, effort, requetes et quotas du journal de session Codex."""
702
- path = _codex_session_file(session_id)
703
- if path is None:
704
- return None
705
- models: list[str] = []
706
- effort: str | None = None
707
- totals: set[int] = set()
708
- first_context: int | None = None
709
- limits: list[dict] = []
710
- with path.open(encoding="utf-8", errors="replace") as handle:
711
- for event in json_line_events(handle.read()):
712
- payload = event.get("payload")
713
- if not isinstance(payload, dict):
714
- continue
715
- if event.get("type") == "turn_context":
716
- model = payload.get("model")
717
- if isinstance(model, str) and model not in models:
718
- models.append(model)
719
- if isinstance(payload.get("effort"), str):
720
- effort = payload["effort"]
721
- elif event.get("type") == "event_msg" and payload.get("type") == "token_count":
722
- info = payload.get("info")
723
- if isinstance(info, dict) and isinstance(info.get("total_token_usage"), dict):
724
- marker = _int(info["total_token_usage"].get("total_tokens"))
725
- if marker not in totals:
726
- totals.add(marker)
727
- last = info.get("last_token_usage")
728
- if first_context is None and isinstance(last, dict):
729
- first_context = _int(last.get("input_tokens")) or None
730
- if isinstance(payload.get("rate_limits"), dict):
731
- limits.append(payload["rate_limits"])
732
- return {
733
- "models": tuple(models),
734
- "effort": effort,
735
- "turns": len(totals) or None,
736
- "firstContextTokens": first_context,
737
- "quota": _quota_windows(limits),
738
- }
739
-
740
-
741
- def _quota_windows(limits: list[dict]) -> tuple[QuotaWindow, ...]:
742
- """Compare le premier et le dernier releve de chaque fenetre de quota."""
743
- windows: list[QuotaWindow] = []
744
- if not limits:
745
- return ()
746
- for key in ("primary", "secondary"):
747
- first, last = limits[0].get(key), limits[-1].get(key)
748
- if not isinstance(first, dict) or not isinstance(last, dict):
749
- continue
750
- start, end = first.get("used_percent"), last.get("used_percent")
751
- if not isinstance(start, (int, float)) or not isinstance(end, (int, float)):
752
- continue
753
- minutes = last.get("window_minutes")
754
- same_window = first.get("resets_at") == last.get("resets_at")
755
- windows.append(
756
- QuotaWindow(
757
- windowMinutes=minutes if isinstance(minutes, int) else None,
758
- usedPercentStart=float(start),
759
- usedPercentEnd=float(end),
760
- deltaPoints=float(end) - float(start) if same_window else None,
761
- )
762
- )
763
- return tuple(windows)
@@ -52,16 +52,6 @@ class LinearDecision:
52
52
  def should_update(self) -> bool:
53
53
  return self.action == "update"
54
54
 
55
- def to_dict(self) -> dict[str, str | bool | None]:
56
- return {
57
- "action": self.action,
58
- "activity": self.activity,
59
- "currentStatus": self.current_status,
60
- "targetStatus": self.target_status,
61
- "shouldUpdate": self.should_update,
62
- "reason": self.reason,
63
- }
64
-
65
55
 
66
56
  def decide_linear_transition(activity: str, current_status: str | None) -> LinearDecision:
67
57
  """Applique le mapping et la règle de non-régression canonique."""
devflow_cli/cli.py CHANGED
@@ -13,20 +13,11 @@ from devflow_cli.commands.init_cmd import init
13
13
  from devflow_cli.commands.check import check
14
14
  from devflow_cli.commands.status import status
15
15
  from devflow_cli.commands.feature import feature
16
- from devflow_cli.commands.context import context
17
- from devflow_cli.commands.export_cmd import export
18
- from devflow_cli.commands.stale import stale
19
16
  from devflow_cli.commands.upgrade_cmd import upgrade
20
- from devflow_cli.commands import extension
21
- from devflow_cli.commands import hooks_cmd
22
- from devflow_cli.commands import linear
23
17
  from devflow_cli.commands import adaptive
24
18
  from devflow_cli.commands import feedback
25
19
  from devflow_cli.commands.docs_sync import docs_sync
26
- from devflow_cli.commands.traceability import traceability
27
- from devflow_cli.commands.verify import verify
28
20
  from devflow_cli.commands.assess import assess
29
- from devflow_cli.commands.usage import usage
30
21
 
31
22
  class DevflowGroup(TyperGroup):
32
23
  """Affiche un refus actionnable, sans trace, pour un fichier de feature 6.x."""
@@ -66,20 +57,11 @@ app.command()(init)
66
57
  app.command()(check)
67
58
  app.command()(status)
68
59
  app.command()(feature)
69
- app.command()(context)
70
- app.command(name="export")(export)
71
- app.command()(stale)
72
60
  app.command()(upgrade)
73
- app.add_typer(extension.app, name="extension")
74
- app.add_typer(hooks_cmd.app, name="hooks")
75
- app.add_typer(linear.app, name="linear")
76
61
  app.add_typer(adaptive.app, name="adaptive")
77
62
  app.add_typer(feedback.app, name="feedback")
78
63
  app.command(name="docs-sync")(docs_sync)
79
- app.command()(traceability)
80
- app.command()(verify)
81
64
  app.command()(assess)
82
- app.command()(usage)
83
65
 
84
66
 
85
67
  if __name__ == "__main__":