devflow-cli 8.0.0__py3-none-any.whl → 8.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """devflow CLI — workflow de développement adaptatif."""
2
2
 
3
- VERSION = "8.0.0"
3
+ VERSION = "8.1.0"
4
4
 
5
5
  SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
6
6
 
@@ -38,14 +38,17 @@ from devflow_cli.core.activity_contracts import (
38
38
  write_activity_contract,
39
39
  )
40
40
  from devflow_cli.core.adaptive_runner import (
41
+ MANUAL_RECONCILIATION_ACTIVITIES,
41
42
  AgentActivityExecution,
42
43
  reconcile_current_activity,
43
44
  record_human_review,
44
45
  run_current_activity,
45
46
  )
47
+ from devflow_cli.core.agent_run_marker import AgentRunningError, ensure_no_agent_running
46
48
  from devflow_cli.core import run_progress
47
49
  from devflow_cli.core.worktree_snapshot import changed_paths, snapshot_worktree
48
50
  from devflow_cli.core.automatic_evidence import (
51
+ AutomaticVerificationOutcome,
49
52
  ensure_fresh_devflow_evidence,
50
53
  run_automatic_verification,
51
54
  stale_devflow_evidence,
@@ -62,6 +65,7 @@ from devflow_cli.core.remediation import (
62
65
  from devflow_cli.core.redaction import redact_command
63
66
  from devflow_cli.core.validation_approvals import (
64
67
  approve_plan_command,
68
+ pending_approvals,
65
69
  pending_plan_approval,
66
70
  )
67
71
  from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
@@ -215,6 +219,7 @@ def complete(
215
219
  """Complete l'activite courante si ses gates sont satisfaites."""
216
220
  feature_dir = _feature(feature)
217
221
  with workflow_lock(feature_dir):
222
+ refuse_if_agent_running(feature_dir)
218
223
  state = _state(feature_dir)
219
224
  activity = activity or state.currentActivity
220
225
  try:
@@ -257,6 +262,7 @@ def evidence(
257
262
  """Enregistre une preuve manuelle informative, sans ouvrir les gates."""
258
263
  feature_dir = _feature(feature)
259
264
  with workflow_lock(feature_dir):
265
+ refuse_if_agent_running(feature_dir)
260
266
  state = _state(feature_dir)
261
267
  try:
262
268
  record_evidence(state, evidence_id, report or "")
@@ -288,6 +294,7 @@ def contract(
288
294
  raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
289
295
 
290
296
  with workflow_lock(feature_dir):
297
+ refuse_if_agent_running(feature_dir)
291
298
  state = _state(feature_dir)
292
299
  try:
293
300
  assessment = _assessment(feature_dir)
@@ -463,7 +470,7 @@ def run_adaptive(
463
470
  agent,
464
471
  )
465
472
 
466
- outcome = run_current_activity(feature_dir, feature_path, execute)
473
+ outcome = run_current_activity(feature_dir, feature_path, execute, agent=agent)
467
474
  _record_telemetry(
468
475
  run_id, attempt, agent, project_root, outcome.reason,
469
476
  json_output=json_output,
@@ -481,8 +488,10 @@ def run_adaptive(
481
488
  f"Prochaine : {outcome.nextActivity}"
482
489
  )
483
490
  _print_warnings(outcome.warnings)
491
+ _print_pending_approvals(feature_dir, current_state.issueId, outcome.activity)
484
492
  return
485
- safe_refresh_worklog(feature_dir)
493
+ if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
494
+ safe_refresh_worklog(feature_dir)
486
495
  exit_code = exit_code_for_run_reason(outcome.reason)
487
496
  if json_output:
488
497
  _print_execution_json(
@@ -569,7 +578,10 @@ def reconcile_adaptive(
569
578
  result: Optional[Path] = typer.Option(
570
579
  None,
571
580
  "--result",
572
- help="Payload JSON corrige a revalider quand la trace est inexploitable",
581
+ help=(
582
+ "Payload JSON corrige, rejoue par les validateurs de run, quand la trace "
583
+ "est inexploitable ou que sa reponse est refusee par les validateurs"
584
+ ),
573
585
  ),
574
586
  reason: Optional[str] = typer.Option(
575
587
  None,
@@ -615,8 +627,10 @@ def reconcile_adaptive(
615
627
  f"Prochaine : {outcome.nextActivity}"
616
628
  )
617
629
  _print_warnings(outcome.warnings)
630
+ _print_pending_approvals(feature_dir, state.issueId, outcome.activity)
618
631
  return
619
- safe_refresh_worklog(feature_dir)
632
+ if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
633
+ safe_refresh_worklog(feature_dir)
620
634
  if outcome.reason in BASE_REFUSALS:
621
635
  fail(outcome.detail)
622
636
  else:
@@ -625,6 +639,9 @@ def reconcile_adaptive(
625
639
  + (f" ({outcome.detail})" if outcome.detail else "")
626
640
  )
627
641
  _print_warnings(outcome.warnings)
642
+ if outcome.reason == "agent-running":
643
+ console.print(" Suite : attendez la fin de l'activite en cours, puis relancez.")
644
+ raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
628
645
  console.print(f" Suite : devflow adaptive status {state.issueId}")
629
646
  if outcome.reason == "reconcile-refused":
630
647
  console.print(
@@ -650,6 +667,7 @@ def review_adaptive(
650
667
  project_root = project_root_for_feature(feature_dir)
651
668
  feature_path = feature_dir.relative_to(project_root).as_posix()
652
669
  with workflow_lock(feature_dir):
670
+ refuse_if_agent_running(feature_dir)
653
671
  state = _state(feature_dir)
654
672
  try:
655
673
  report_file = report.resolve()
@@ -693,6 +711,10 @@ def verify_adaptive(
693
711
  """Genere localement les preuves requises par le profil actif."""
694
712
  feature_dir = _feature(feature, json_output=json_output)
695
713
  state = _state(feature_dir, json_output=json_output, issue_id=feature)
714
+ with workflow_lock(feature_dir):
715
+ refuse_if_agent_running(
716
+ feature_dir, json_issue_id=state.issueId if json_output else None
717
+ )
696
718
  if not refresh and state.currentActivity != "verify":
697
719
  detail = (
698
720
  "Activite courante attendue pour verify : verify "
@@ -726,6 +748,7 @@ def approve_adaptive_command(
726
748
  feature_dir = _feature(feature)
727
749
  try:
728
750
  with workflow_lock(feature_dir):
751
+ refuse_if_agent_running(feature_dir)
729
752
  item = pending_plan_approval(feature_dir, validation_id)
730
753
  console.print(f"Validation : {item.id}")
731
754
  console.print(f"Commande : {shlex.join(redact_command(item.command))}")
@@ -767,6 +790,7 @@ def waive_validation(
767
790
  """Rend non bloquant un controle planifie echoue, avec audit durable."""
768
791
  feature_dir = _feature(feature)
769
792
  with workflow_lock(feature_dir):
793
+ refuse_if_agent_running(feature_dir)
770
794
  try:
771
795
  waiver = create_validation_waiver(
772
796
  feature_dir, validation_id, reason, author=author
@@ -794,6 +818,7 @@ def reopen(
794
818
  """Revient a une activite anterieure sans effacer l'historique."""
795
819
  feature_dir = _feature(feature)
796
820
  with workflow_lock(feature_dir):
821
+ refuse_if_agent_running(feature_dir)
797
822
  state = _state(feature_dir)
798
823
  try:
799
824
  entry = reopen_activity(
@@ -823,6 +848,7 @@ def discard_attempt_command(
823
848
  """Abandonne la tentative de l'activite courante, avec archive et audit."""
824
849
  feature_dir = _feature(feature)
825
850
  with workflow_lock(feature_dir):
851
+ refuse_if_agent_running(feature_dir)
826
852
  state = _state(feature_dir)
827
853
  try:
828
854
  entry = discard_attempt(feature_dir, state, reason, author=author)
@@ -856,11 +882,15 @@ def _execute_local_verification(
856
882
  if not refresh:
857
883
  sync_activity_status(issue_id, "verify", stream)
858
884
  outcome = run_automatic_verification(feature_dir, project_root, refresh=refresh)
885
+ recovery = (
886
+ None if outcome.passed else _verify_recovery(feature_dir, issue_id, outcome)
887
+ )
859
888
  if json_output:
860
889
  _print_execution_json(
861
890
  issue_id, "verify", outcome.passed, outcome.reason, outcome.detail,
862
891
  outcome.nextActivity,
863
892
  ExitCode.OK if outcome.passed else ExitCode.VERIFICATION_FAILURE,
893
+ recovery=recovery,
864
894
  )
865
895
  if not outcome.passed:
866
896
  raise typer.Exit(int(ExitCode.VERIFICATION_FAILURE))
@@ -874,20 +904,182 @@ def _execute_local_verification(
874
904
  fail(outcome.detail or outcome.reason)
875
905
  console.print(f" Verification automatique refusee : {outcome.reason}")
876
906
  console.print(" Reprise guidee :")
877
- console.print(f" devflow adaptive status {issue_id}")
878
- console.print(
879
- f" devflow adaptive reopen {issue_id} implement --reason "
880
- f'"correction apres {outcome.reason}"'
881
- )
907
+ console.print(f" {recovery['summary']}")
908
+ for step in recovery["commands"]:
909
+ console.print(f" {step['command']} # {step['purpose']}", markup=False)
882
910
  raise typer.Exit(int(ExitCode.VERIFICATION_FAILURE))
883
911
 
884
912
 
913
+ # Motifs de refus de verify dus a l'echec d'un controle execute (B2).
914
+ CHECK_FAILURE_REASONS = frozenset({
915
+ "targeted-check-failed",
916
+ "targeted-tests-failed",
917
+ "test-suite-failed",
918
+ "regression-check-failed",
919
+ "security-or-data-check-failed",
920
+ })
921
+ PLANNED_PREFIXES = {"PREP-": "prepare", "PLAN-": "plan"}
922
+
923
+
924
+ def _verify_recovery(
925
+ feature_dir: Path, issue_id: str, outcome: AutomaticVerificationOutcome
926
+ ) -> dict:
927
+ """Reprise guidee d'un refus de verify, par motif, sans heuristique (B2).
928
+
929
+ Approbation manquante : chaque approve-command puis verify. Executable introuvable
930
+ (code 127) : corriger l'environnement puis verify. Echec d'un controle : deux voies,
931
+ le code (reopen implement) ou la validation (reopen de l'activite qui l'a inscrite).
932
+ """
933
+ issue = shlex.quote(issue_id)
934
+ verify_command = {
935
+ "command": f"devflow adaptive verify {issue}",
936
+ "purpose": "relancer la verification",
937
+ }
938
+ state = read_adaptive_state(feature_dir / "workflow-state.json")
939
+ plan_activity = (
940
+ "prepare" if state is not None and state.profile == "standard" else "plan"
941
+ )
942
+ if outcome.pendingApprovals:
943
+ return {
944
+ "kind": "approval-pending",
945
+ "summary": "Approuver chaque validation en attente, puis relancer verify.",
946
+ "validations": [
947
+ {"id": item, "origin": plan_activity, "returncode": None}
948
+ for item in outcome.pendingApprovals
949
+ ],
950
+ "commands": [
951
+ *(
952
+ {
953
+ "command": (
954
+ f"devflow adaptive approve-command {issue} "
955
+ f"{shlex.quote(item)} --reason MOTIF"
956
+ ),
957
+ "purpose": f"approuver la commande exacte de {item}",
958
+ }
959
+ for item in outcome.pendingApprovals
960
+ ),
961
+ verify_command,
962
+ ],
963
+ }
964
+ status_command = {
965
+ "command": f"devflow adaptive status {issue}",
966
+ "purpose": "consulter l'etat du parcours",
967
+ }
968
+ if outcome.reason not in CHECK_FAILURE_REASONS:
969
+ return {
970
+ "kind": "status",
971
+ "summary": f"Refus {outcome.reason} : consulter l'etat avant toute reprise.",
972
+ "validations": [],
973
+ "commands": [status_command],
974
+ }
975
+ validations = []
976
+ for failure in outcome.failures:
977
+ validation_id, origin = failure.id, None
978
+ for prefix, activity in PLANNED_PREFIXES.items():
979
+ if failure.id.startswith(prefix):
980
+ validation_id, origin = failure.id.removeprefix(prefix), activity
981
+ if origin is not None and not _reopenable(state, origin):
982
+ origin = None
983
+ validations.append({
984
+ "id": validation_id,
985
+ "origin": origin,
986
+ "returncode": failure.returncode,
987
+ })
988
+ if validations and all(item["returncode"] == 127 for item in validations):
989
+ names = ", ".join(item["id"] for item in validations)
990
+ return {
991
+ "kind": "environment",
992
+ "summary": (
993
+ f"Executable introuvable (code 127) pour {names} : corriger "
994
+ "l'environnement, puis relancer verify."
995
+ ),
996
+ "validations": validations,
997
+ "commands": [verify_command],
998
+ }
999
+ reason_text = shlex.quote(f"correction apres {outcome.reason}")
1000
+ commands = [
1001
+ status_command,
1002
+ {
1003
+ "command": f"devflow adaptive reopen {issue} implement --reason {reason_text}",
1004
+ "purpose": "voie 1 : le code est en cause",
1005
+ },
1006
+ ]
1007
+ for origin in sorted({item["origin"] for item in validations if item["origin"]}):
1008
+ names = ", ".join(item["id"] for item in validations if item["origin"] == origin)
1009
+ commands.append({
1010
+ "command": f"devflow adaptive reopen {issue} {origin} --reason {reason_text}",
1011
+ "purpose": f"voie 2 : la validation {names}, inscrite par {origin}, est en cause",
1012
+ })
1013
+ return {
1014
+ "kind": "check-failed",
1015
+ "summary": (
1016
+ "Controle echoue"
1017
+ + (f" : {', '.join(item['id'] for item in validations)}" if validations else "")
1018
+ + ". Choisir la voie selon la cause ; aucune n'est choisie automatiquement."
1019
+ ),
1020
+ "validations": validations,
1021
+ "commands": commands,
1022
+ }
1023
+
1024
+
1025
+ def _reopenable(state: AdaptiveWorkflowState | None, activity: str) -> bool:
1026
+ return (
1027
+ state is not None
1028
+ and activity in state.activities
1029
+ and activity in state.completedActivities
1030
+ and state.activities.index(activity) < state.activities.index(state.currentActivity)
1031
+ )
1032
+
1033
+
1034
+ def refuse_if_agent_running(feature_dir: Path, *, json_issue_id: str | None = None) -> None:
1035
+ """Refuse sans effet une commande humaine pendant l'execution d'un agent (B4).
1036
+
1037
+ A appeler avant toute ecriture dans le dossier de la feature. Avec ``json_issue_id``,
1038
+ le refus est rendu sous la forme JSON des commandes d'execution.
1039
+ """
1040
+ try:
1041
+ warning = ensure_no_agent_running(feature_dir)
1042
+ except AgentRunningError as exc:
1043
+ if json_issue_id is not None:
1044
+ _print_execution_json(
1045
+ json_issue_id, None, False, "agent-running", str(exc), None,
1046
+ ExitCode.CONCURRENT_CHANGE,
1047
+ )
1048
+ else:
1049
+ fail(str(exc))
1050
+ raise typer.Exit(int(ExitCode.CONCURRENT_CHANGE)) from exc
1051
+ if warning:
1052
+ warn(warning)
1053
+
1054
+
1055
+ def _print_pending_approvals(feature_dir: Path, issue_id: str, activity: str) -> None:
1056
+ """Liste en une fois les approbations attendues a la fin de prepare ou plan."""
1057
+ if activity not in {"prepare", "plan"}:
1058
+ return
1059
+ try:
1060
+ pending = pending_approvals(feature_dir)
1061
+ except ValueError:
1062
+ return
1063
+ if not pending:
1064
+ return
1065
+ warn(
1066
+ "Approbations attendues avant verify : "
1067
+ + ", ".join(item.id for item in pending)
1068
+ )
1069
+ for item in pending:
1070
+ console.print(
1071
+ f" devflow adaptive approve-command {shlex.quote(issue_id)} "
1072
+ f"{shlex.quote(item.id)} --reason MOTIF",
1073
+ markup=False,
1074
+ )
1075
+
1076
+
885
1077
  def _print_execution_json(
886
1078
  issue_id: str, activity: str | None, passed: bool, reason: str,
887
1079
  detail: str, next_activity: str | None, exit_code: ExitCode,
888
- *, warnings: tuple[str, ...] = (),
1080
+ *, warnings: tuple[str, ...] = (), recovery: dict | None = None,
889
1081
  ) -> None:
890
- typer.echo(json.dumps({
1082
+ payload = {
891
1083
  "schemaVersion": 1,
892
1084
  "issueId": issue_id,
893
1085
  "activity": activity,
@@ -897,7 +1089,10 @@ def _print_execution_json(
897
1089
  "nextActivity": next_activity,
898
1090
  "exitCode": int(exit_code),
899
1091
  "warnings": list(warnings),
900
- }, ensure_ascii=False))
1092
+ }
1093
+ if recovery is not None:
1094
+ payload["recovery"] = recovery
1095
+ typer.echo(json.dumps(payload, ensure_ascii=False))
901
1096
 
902
1097
 
903
1098
  def _print_warnings(warnings: tuple[str, ...]) -> None:
@@ -978,6 +1173,8 @@ def worklog(
978
1173
  ) -> None:
979
1174
  """Regenere le journal compact depuis les artefacts du workflow."""
980
1175
  feature_dir = _feature(feature)
1176
+ with workflow_lock(feature_dir):
1177
+ refuse_if_agent_running(feature_dir)
981
1178
  try:
982
1179
  output = refresh_worklog(feature_dir)
983
1180
  except LegacySchemaError as exc:
@@ -1204,6 +1401,14 @@ def _print_run_recovery(
1204
1401
  )
1205
1402
  console.print(f" devflow adaptive run {issue_id} --agent {agent}")
1206
1403
  return
1404
+ if reason == "agent-running":
1405
+ console.print(" Un agent execute deja cette activite : attendez sa fin.")
1406
+ return
1407
+ if reason == "invalid-result" and activity in MANUAL_RECONCILIATION_ACTIVITIES:
1408
+ console.print(
1409
+ f" devflow adaptive reconcile {issue_id} --result FICHIER.json --reason MOTIF"
1410
+ " # corriger la reponse refusee sans relancer l'agent"
1411
+ )
1207
1412
  if reason == "agent-failed":
1208
1413
  console.print(f" devflow adaptive reconcile {issue_id} # si la trace contient une reponse finale")
1209
1414
  console.print(f" devflow check --ai {agent}")
@@ -13,7 +13,8 @@ from devflow_cli.core.verification import (
13
13
  parse_validation_commands,
14
14
  run_verifications,
15
15
  )
16
- from devflow_cli.core.adaptive_workflow import read_adaptive_state
16
+ from devflow_cli.commands.adaptive import refuse_if_agent_running
17
+ from devflow_cli.core.adaptive_workflow import read_adaptive_state, workflow_lock
17
18
  from devflow_cli.core.automatic_evidence import run_automatic_verification
18
19
  from devflow_cli.utils.console import console, fail, ok, warn
19
20
  from devflow_cli.utils.paths import project_root_for_feature, resolve_feature_dir
@@ -44,6 +45,8 @@ def verify(
44
45
  "ISSUE_ID VAL-ID --reason MOTIF."
45
46
  )
46
47
  raise typer.Exit(1)
48
+ with workflow_lock(feature_dir):
49
+ refuse_if_agent_running(feature_dir)
47
50
  outcome = run_automatic_verification(
48
51
  feature_dir, project_root_for_feature(feature_dir)
49
52
  )
@@ -44,6 +44,7 @@ RUNTIME_FILE_NAMES = frozenset({
44
44
  "workflow-state.json", ".workflow-state.lock", "activity-contract.json",
45
45
  "agent-execution.json", "worklog.md", "reopen-log.json", "attempt-discard-log.json",
46
46
  "reconcile-log.json", "validation-approvals.json", "waivers.json", "verification.json",
47
+ ".agent-run.lock",
47
48
  })
48
49
  RUNTIME_FILE_SUFFIXES = ("-result.json", "-attempt.json", ".raw.txt")
49
50
  RUNTIME_DIRECTORIES = ("attempts", "evidence")