devflow-cli 7.2.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 +1 -1
- devflow_cli/commands/adaptive.py +269 -29
- devflow_cli/commands/verify.py +4 -1
- devflow_cli/core/activity_base.py +409 -0
- devflow_cli/core/activity_contracts.py +14 -20
- devflow_cli/core/adaptive_runner.py +389 -214
- devflow_cli/core/adaptive_workflow.py +29 -3
- devflow_cli/core/agent_run_marker.py +99 -0
- devflow_cli/core/automatic_evidence.py +95 -9
- devflow_cli/core/contract_schemas.py +35 -9
- devflow_cli/core/feedback.py +13 -2
- devflow_cli/core/operational.py +3 -1
- devflow_cli/core/remediation.py +21 -137
- devflow_cli/core/runtime_backup.py +119 -0
- devflow_cli/core/validation_approvals.py +22 -2
- devflow_cli/core/verification.py +98 -13
- devflow_cli/core/worklog.py +44 -0
- devflow_cli/core/worktree_snapshot.py +22 -14
- devflow_cli/schemas/{workflow-state-v5.schema.json → workflow-state-v6.schema.json} +28 -5
- {devflow_cli-7.2.0.dist-info → devflow_cli-8.1.0.dist-info}/METADATA +20 -2
- {devflow_cli-7.2.0.dist-info → devflow_cli-8.1.0.dist-info}/RECORD +24 -21
- {devflow_cli-7.2.0.dist-info → devflow_cli-8.1.0.dist-info}/WHEEL +0 -0
- {devflow_cli-7.2.0.dist-info → devflow_cli-8.1.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-7.2.0.dist-info → devflow_cli-8.1.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -25,6 +25,10 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
25
25
|
write_adaptive_state,
|
|
26
26
|
)
|
|
27
27
|
from devflow_cli.core.assessment import read_assessment
|
|
28
|
+
from devflow_cli.core.activity_base import (
|
|
29
|
+
enter_current_activity,
|
|
30
|
+
files_since_activity_base,
|
|
31
|
+
)
|
|
28
32
|
from devflow_cli.core.activity_contracts import (
|
|
29
33
|
build_activity_contract,
|
|
30
34
|
NEED_STATEMENT,
|
|
@@ -34,14 +38,17 @@ from devflow_cli.core.activity_contracts import (
|
|
|
34
38
|
write_activity_contract,
|
|
35
39
|
)
|
|
36
40
|
from devflow_cli.core.adaptive_runner import (
|
|
41
|
+
MANUAL_RECONCILIATION_ACTIVITIES,
|
|
37
42
|
AgentActivityExecution,
|
|
38
43
|
reconcile_current_activity,
|
|
39
44
|
record_human_review,
|
|
40
45
|
run_current_activity,
|
|
41
46
|
)
|
|
47
|
+
from devflow_cli.core.agent_run_marker import AgentRunningError, ensure_no_agent_running
|
|
42
48
|
from devflow_cli.core import run_progress
|
|
43
49
|
from devflow_cli.core.worktree_snapshot import changed_paths, snapshot_worktree
|
|
44
50
|
from devflow_cli.core.automatic_evidence import (
|
|
51
|
+
AutomaticVerificationOutcome,
|
|
45
52
|
ensure_fresh_devflow_evidence,
|
|
46
53
|
run_automatic_verification,
|
|
47
54
|
stale_devflow_evidence,
|
|
@@ -51,7 +58,6 @@ from devflow_cli.core.worklog import refresh_worklog, safe_refresh_worklog
|
|
|
51
58
|
from devflow_cli.core.telemetry import append_record, telemetry_enabled, usage_payload
|
|
52
59
|
from devflow_cli.core.waivers import create_validation_waiver
|
|
53
60
|
from devflow_cli.core.remediation import (
|
|
54
|
-
carried_changed_files,
|
|
55
61
|
discard_attempt,
|
|
56
62
|
is_reopened_activity,
|
|
57
63
|
reopen_activity,
|
|
@@ -59,6 +65,7 @@ from devflow_cli.core.remediation import (
|
|
|
59
65
|
from devflow_cli.core.redaction import redact_command
|
|
60
66
|
from devflow_cli.core.validation_approvals import (
|
|
61
67
|
approve_plan_command,
|
|
68
|
+
pending_approvals,
|
|
62
69
|
pending_plan_approval,
|
|
63
70
|
)
|
|
64
71
|
from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
|
|
@@ -77,6 +84,9 @@ app = typer.Typer(help="Machine d'etat des parcours adaptatifs")
|
|
|
77
84
|
|
|
78
85
|
LEGACY_SCHEMA_EXIT_CODE = 1
|
|
79
86
|
|
|
87
|
+
# Refus avant tout agent : base inutilisable (branche changee, historique reecrit).
|
|
88
|
+
BASE_REFUSALS = frozenset({"base-history-rewritten", "base-branch-changed"})
|
|
89
|
+
|
|
80
90
|
RESULT_VALIDATED_ACTIVITIES = frozenset({
|
|
81
91
|
"prepare",
|
|
82
92
|
"risk-assessment",
|
|
@@ -167,7 +177,10 @@ def start(feature: str = typer.Argument(..., help="ID ou dossier de la feature")
|
|
|
167
177
|
assessment = _assessment(feature_dir)
|
|
168
178
|
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
169
179
|
state = create_adaptive_workflow(assessment.to_dict())
|
|
170
|
-
|
|
180
|
+
enter_current_activity(
|
|
181
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
182
|
+
)
|
|
183
|
+
except (OSError, ValueError) as exc:
|
|
171
184
|
fail(f"Impossible de demarrer le workflow : {exc}")
|
|
172
185
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
173
186
|
write_adaptive_state(state_file, state)
|
|
@@ -206,6 +219,7 @@ def complete(
|
|
|
206
219
|
"""Complete l'activite courante si ses gates sont satisfaites."""
|
|
207
220
|
feature_dir = _feature(feature)
|
|
208
221
|
with workflow_lock(feature_dir):
|
|
222
|
+
refuse_if_agent_running(feature_dir)
|
|
209
223
|
state = _state(feature_dir)
|
|
210
224
|
activity = activity or state.currentActivity
|
|
211
225
|
try:
|
|
@@ -221,7 +235,10 @@ def complete(
|
|
|
221
235
|
validate_preflight_evidence(feature_dir / "preflight-evidence.md")
|
|
222
236
|
ensure_fresh_devflow_evidence(feature_dir, state, activity)
|
|
223
237
|
complete_activity(state, activity)
|
|
224
|
-
|
|
238
|
+
enter_current_activity(
|
|
239
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
240
|
+
)
|
|
241
|
+
except (OSError, ValueError) as exc:
|
|
225
242
|
fail(str(exc))
|
|
226
243
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
227
244
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
@@ -245,6 +262,7 @@ def evidence(
|
|
|
245
262
|
"""Enregistre une preuve manuelle informative, sans ouvrir les gates."""
|
|
246
263
|
feature_dir = _feature(feature)
|
|
247
264
|
with workflow_lock(feature_dir):
|
|
265
|
+
refuse_if_agent_running(feature_dir)
|
|
248
266
|
state = _state(feature_dir)
|
|
249
267
|
try:
|
|
250
268
|
record_evidence(state, evidence_id, report or "")
|
|
@@ -276,6 +294,7 @@ def contract(
|
|
|
276
294
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
277
295
|
|
|
278
296
|
with workflow_lock(feature_dir):
|
|
297
|
+
refuse_if_agent_running(feature_dir)
|
|
279
298
|
state = _state(feature_dir)
|
|
280
299
|
try:
|
|
281
300
|
assessment = _assessment(feature_dir)
|
|
@@ -284,11 +303,15 @@ def contract(
|
|
|
284
303
|
state,
|
|
285
304
|
assessment.to_dict(),
|
|
286
305
|
feature_path,
|
|
287
|
-
carried_changed_files(feature_dir, state.currentActivity),
|
|
288
306
|
reopened=is_reopened_activity(feature_dir, state.currentActivity),
|
|
289
307
|
preflight_evidence=(feature_dir / "preflight-evidence.md").is_file(),
|
|
308
|
+
files_since_base=(
|
|
309
|
+
files_since_activity_base(feature_dir, state)
|
|
310
|
+
if state.activityBase is not None
|
|
311
|
+
else ()
|
|
312
|
+
),
|
|
290
313
|
)
|
|
291
|
-
except ValueError as exc:
|
|
314
|
+
except (OSError, ValueError) as exc:
|
|
292
315
|
fail(f"Impossible de generer le contrat : {exc}")
|
|
293
316
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
294
317
|
output_file = feature_dir / "activity-contract.json"
|
|
@@ -447,7 +470,7 @@ def run_adaptive(
|
|
|
447
470
|
agent,
|
|
448
471
|
)
|
|
449
472
|
|
|
450
|
-
outcome = run_current_activity(feature_dir, feature_path, execute)
|
|
473
|
+
outcome = run_current_activity(feature_dir, feature_path, execute, agent=agent)
|
|
451
474
|
_record_telemetry(
|
|
452
475
|
run_id, attempt, agent, project_root, outcome.reason,
|
|
453
476
|
json_output=json_output,
|
|
@@ -457,22 +480,31 @@ def run_adaptive(
|
|
|
457
480
|
_print_execution_json(
|
|
458
481
|
current_state.issueId, outcome.activity, True, outcome.reason,
|
|
459
482
|
outcome.detail, outcome.nextActivity, ExitCode.OK,
|
|
483
|
+
warnings=outcome.warnings,
|
|
460
484
|
)
|
|
461
485
|
else:
|
|
462
486
|
ok(
|
|
463
487
|
f"Activite '{outcome.activity}' validee. "
|
|
464
488
|
f"Prochaine : {outcome.nextActivity}"
|
|
465
489
|
)
|
|
490
|
+
_print_warnings(outcome.warnings)
|
|
491
|
+
_print_pending_approvals(feature_dir, current_state.issueId, outcome.activity)
|
|
466
492
|
return
|
|
467
|
-
|
|
493
|
+
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
494
|
+
safe_refresh_worklog(feature_dir)
|
|
468
495
|
exit_code = exit_code_for_run_reason(outcome.reason)
|
|
469
496
|
if json_output:
|
|
470
497
|
_print_execution_json(
|
|
471
498
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
472
499
|
outcome.detail, outcome.nextActivity, exit_code,
|
|
500
|
+
warnings=outcome.warnings,
|
|
473
501
|
)
|
|
502
|
+
elif outcome.reason in BASE_REFUSALS:
|
|
503
|
+
fail(outcome.detail)
|
|
504
|
+
console.print(f" Suite : devflow adaptive status {current_state.issueId}")
|
|
474
505
|
else:
|
|
475
506
|
fail(outcome.detail or outcome.reason)
|
|
507
|
+
_print_warnings(outcome.warnings)
|
|
476
508
|
console.print(
|
|
477
509
|
f" Activite '{outcome.activity}' non validee : {outcome.reason}"
|
|
478
510
|
)
|
|
@@ -546,7 +578,10 @@ def reconcile_adaptive(
|
|
|
546
578
|
result: Optional[Path] = typer.Option(
|
|
547
579
|
None,
|
|
548
580
|
"--result",
|
|
549
|
-
help=
|
|
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
|
+
),
|
|
550
585
|
),
|
|
551
586
|
reason: Optional[str] = typer.Option(
|
|
552
587
|
None,
|
|
@@ -591,12 +626,22 @@ def reconcile_adaptive(
|
|
|
591
626
|
f"Activite '{outcome.activity}' reconciliee. "
|
|
592
627
|
f"Prochaine : {outcome.nextActivity}"
|
|
593
628
|
)
|
|
629
|
+
_print_warnings(outcome.warnings)
|
|
630
|
+
_print_pending_approvals(feature_dir, state.issueId, outcome.activity)
|
|
594
631
|
return
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
632
|
+
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
633
|
+
safe_refresh_worklog(feature_dir)
|
|
634
|
+
if outcome.reason in BASE_REFUSALS:
|
|
635
|
+
fail(outcome.detail)
|
|
636
|
+
else:
|
|
637
|
+
fail(
|
|
638
|
+
f"Reconciliation de '{outcome.activity}' refusee : {outcome.reason}"
|
|
639
|
+
+ (f" ({outcome.detail})" if outcome.detail else "")
|
|
640
|
+
)
|
|
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)))
|
|
600
645
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
601
646
|
if outcome.reason == "reconcile-refused":
|
|
602
647
|
console.print(
|
|
@@ -622,6 +667,7 @@ def review_adaptive(
|
|
|
622
667
|
project_root = project_root_for_feature(feature_dir)
|
|
623
668
|
feature_path = feature_dir.relative_to(project_root).as_posix()
|
|
624
669
|
with workflow_lock(feature_dir):
|
|
670
|
+
refuse_if_agent_running(feature_dir)
|
|
625
671
|
state = _state(feature_dir)
|
|
626
672
|
try:
|
|
627
673
|
report_file = report.resolve()
|
|
@@ -665,6 +711,10 @@ def verify_adaptive(
|
|
|
665
711
|
"""Genere localement les preuves requises par le profil actif."""
|
|
666
712
|
feature_dir = _feature(feature, json_output=json_output)
|
|
667
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
|
+
)
|
|
668
718
|
if not refresh and state.currentActivity != "verify":
|
|
669
719
|
detail = (
|
|
670
720
|
"Activite courante attendue pour verify : verify "
|
|
@@ -698,6 +748,7 @@ def approve_adaptive_command(
|
|
|
698
748
|
feature_dir = _feature(feature)
|
|
699
749
|
try:
|
|
700
750
|
with workflow_lock(feature_dir):
|
|
751
|
+
refuse_if_agent_running(feature_dir)
|
|
701
752
|
item = pending_plan_approval(feature_dir, validation_id)
|
|
702
753
|
console.print(f"Validation : {item.id}")
|
|
703
754
|
console.print(f"Commande : {shlex.join(redact_command(item.command))}")
|
|
@@ -739,6 +790,7 @@ def waive_validation(
|
|
|
739
790
|
"""Rend non bloquant un controle planifie echoue, avec audit durable."""
|
|
740
791
|
feature_dir = _feature(feature)
|
|
741
792
|
with workflow_lock(feature_dir):
|
|
793
|
+
refuse_if_agent_running(feature_dir)
|
|
742
794
|
try:
|
|
743
795
|
waiver = create_validation_waiver(
|
|
744
796
|
feature_dir, validation_id, reason, author=author
|
|
@@ -766,11 +818,15 @@ def reopen(
|
|
|
766
818
|
"""Revient a une activite anterieure sans effacer l'historique."""
|
|
767
819
|
feature_dir = _feature(feature)
|
|
768
820
|
with workflow_lock(feature_dir):
|
|
821
|
+
refuse_if_agent_running(feature_dir)
|
|
769
822
|
state = _state(feature_dir)
|
|
770
823
|
try:
|
|
771
824
|
entry = reopen_activity(
|
|
772
825
|
feature_dir, state, activity, reason, author=author
|
|
773
826
|
)
|
|
827
|
+
enter_current_activity(
|
|
828
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
829
|
+
)
|
|
774
830
|
except (OSError, ValueError) as exc:
|
|
775
831
|
fail(str(exc))
|
|
776
832
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
@@ -792,6 +848,7 @@ def discard_attempt_command(
|
|
|
792
848
|
"""Abandonne la tentative de l'activite courante, avec archive et audit."""
|
|
793
849
|
feature_dir = _feature(feature)
|
|
794
850
|
with workflow_lock(feature_dir):
|
|
851
|
+
refuse_if_agent_running(feature_dir)
|
|
795
852
|
state = _state(feature_dir)
|
|
796
853
|
try:
|
|
797
854
|
entry = discard_attempt(feature_dir, state, reason, author=author)
|
|
@@ -806,9 +863,12 @@ def discard_attempt_command(
|
|
|
806
863
|
)
|
|
807
864
|
if entry["discardedFiles"]:
|
|
808
865
|
warn(
|
|
809
|
-
"
|
|
810
|
-
|
|
866
|
+
"Fichiers modifies depuis le debut de l'activite "
|
|
867
|
+
f"'{entry['activity']}', comptes dans le lot a la relance ; remise en "
|
|
868
|
+
"etat a la charge du superviseur : " + ", ".join(entry["discardedFiles"])
|
|
811
869
|
)
|
|
870
|
+
if entry.get("observationError"):
|
|
871
|
+
warn(f"Fichiers depuis la base non calcules : {entry['observationError']}")
|
|
812
872
|
|
|
813
873
|
|
|
814
874
|
def _execute_local_verification(
|
|
@@ -822,11 +882,15 @@ def _execute_local_verification(
|
|
|
822
882
|
if not refresh:
|
|
823
883
|
sync_activity_status(issue_id, "verify", stream)
|
|
824
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
|
+
)
|
|
825
888
|
if json_output:
|
|
826
889
|
_print_execution_json(
|
|
827
890
|
issue_id, "verify", outcome.passed, outcome.reason, outcome.detail,
|
|
828
891
|
outcome.nextActivity,
|
|
829
892
|
ExitCode.OK if outcome.passed else ExitCode.VERIFICATION_FAILURE,
|
|
893
|
+
recovery=recovery,
|
|
830
894
|
)
|
|
831
895
|
if not outcome.passed:
|
|
832
896
|
raise typer.Exit(int(ExitCode.VERIFICATION_FAILURE))
|
|
@@ -840,19 +904,182 @@ def _execute_local_verification(
|
|
|
840
904
|
fail(outcome.detail or outcome.reason)
|
|
841
905
|
console.print(f" Verification automatique refusee : {outcome.reason}")
|
|
842
906
|
console.print(" Reprise guidee :")
|
|
843
|
-
console.print(f"
|
|
844
|
-
|
|
845
|
-
f"
|
|
846
|
-
f'"correction apres {outcome.reason}"'
|
|
847
|
-
)
|
|
907
|
+
console.print(f" {recovery['summary']}")
|
|
908
|
+
for step in recovery["commands"]:
|
|
909
|
+
console.print(f" {step['command']} # {step['purpose']}", markup=False)
|
|
848
910
|
raise typer.Exit(int(ExitCode.VERIFICATION_FAILURE))
|
|
849
911
|
|
|
850
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
|
+
|
|
851
1077
|
def _print_execution_json(
|
|
852
1078
|
issue_id: str, activity: str | None, passed: bool, reason: str,
|
|
853
1079
|
detail: str, next_activity: str | None, exit_code: ExitCode,
|
|
1080
|
+
*, warnings: tuple[str, ...] = (), recovery: dict | None = None,
|
|
854
1081
|
) -> None:
|
|
855
|
-
|
|
1082
|
+
payload = {
|
|
856
1083
|
"schemaVersion": 1,
|
|
857
1084
|
"issueId": issue_id,
|
|
858
1085
|
"activity": activity,
|
|
@@ -861,7 +1088,17 @@ def _print_execution_json(
|
|
|
861
1088
|
"detail": detail,
|
|
862
1089
|
"nextActivity": next_activity,
|
|
863
1090
|
"exitCode": int(exit_code),
|
|
864
|
-
|
|
1091
|
+
"warnings": list(warnings),
|
|
1092
|
+
}
|
|
1093
|
+
if recovery is not None:
|
|
1094
|
+
payload["recovery"] = recovery
|
|
1095
|
+
typer.echo(json.dumps(payload, ensure_ascii=False))
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _print_warnings(warnings: tuple[str, ...]) -> None:
|
|
1099
|
+
"""Ecart de declaration : avertissement seul, sans effet sur le code de sortie."""
|
|
1100
|
+
for message in warnings:
|
|
1101
|
+
warn(message)
|
|
865
1102
|
|
|
866
1103
|
|
|
867
1104
|
def _utc_now() -> str:
|
|
@@ -936,6 +1173,8 @@ def worklog(
|
|
|
936
1173
|
) -> None:
|
|
937
1174
|
"""Regenere le journal compact depuis les artefacts du workflow."""
|
|
938
1175
|
feature_dir = _feature(feature)
|
|
1176
|
+
with workflow_lock(feature_dir):
|
|
1177
|
+
refuse_if_agent_running(feature_dir)
|
|
939
1178
|
try:
|
|
940
1179
|
output = refresh_worklog(feature_dir)
|
|
941
1180
|
except LegacySchemaError as exc:
|
|
@@ -1155,13 +1394,6 @@ def _print_run_recovery(
|
|
|
1155
1394
|
console.print(" Ne relancez pas automatiquement l'agent : reconciliez d'abord les changements partiels.")
|
|
1156
1395
|
console.print(f" devflow adaptive reconcile {issue_id}")
|
|
1157
1396
|
return
|
|
1158
|
-
if reason == "stale-attempt":
|
|
1159
|
-
console.print(" git status --short # inspecter les changements de la tentative")
|
|
1160
|
-
console.print(
|
|
1161
|
-
" Restaurez les fichiers de la tentative, ou abandonnez-la explicitement :"
|
|
1162
|
-
)
|
|
1163
|
-
console.print(f" devflow adaptive discard-attempt {issue_id} --reason MOTIF")
|
|
1164
|
-
return
|
|
1165
1397
|
if reason == "inputs-changed-during-activity":
|
|
1166
1398
|
console.print(
|
|
1167
1399
|
" Les entrees du besoin ont change pendant l'execution. "
|
|
@@ -1169,6 +1401,14 @@ def _print_run_recovery(
|
|
|
1169
1401
|
)
|
|
1170
1402
|
console.print(f" devflow adaptive run {issue_id} --agent {agent}")
|
|
1171
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
|
+
)
|
|
1172
1412
|
if reason == "agent-failed":
|
|
1173
1413
|
console.print(f" devflow adaptive reconcile {issue_id} # si la trace contient une reponse finale")
|
|
1174
1414
|
console.print(f" devflow check --ai {agent}")
|
devflow_cli/commands/verify.py
CHANGED
|
@@ -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.
|
|
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
|
)
|