devflow-cli 8.1.0__py3-none-any.whl → 8.3.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/adapters/agents.py +72 -4
- devflow_cli/commands/adaptive.py +213 -13
- devflow_cli/commands/assess.py +11 -1
- devflow_cli/core/activity_contracts.py +17 -2
- devflow_cli/core/adaptive_runner.py +32 -3
- devflow_cli/core/automatic_evidence.py +57 -9
- devflow_cli/core/result_template.py +74 -0
- devflow_cli/core/waivers.py +42 -1
- devflow_cli/core/worklog.py +10 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.3.0.dist-info}/METADATA +18 -4
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.3.0.dist-info}/RECORD +15 -14
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.3.0.dist-info}/WHEEL +0 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.3.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.3.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/adapters/agents.py
CHANGED
|
@@ -22,6 +22,24 @@ from devflow_cli.adapters.linear import environment_without_linear_key
|
|
|
22
22
|
PermissionMode = Literal["auto", "acceptEdits", "manual"]
|
|
23
23
|
SandboxMode = Literal["read-only", "workspace-write"]
|
|
24
24
|
|
|
25
|
+
# 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.
|
|
28
|
+
STRUCTURED_OUTPUT_MISSING = "structured-output-missing"
|
|
29
|
+
MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS = frozenset({
|
|
30
|
+
STRUCTURED_OUTPUT_MISSING,
|
|
31
|
+
"final-output-empty",
|
|
32
|
+
"final-output-absent",
|
|
33
|
+
"final-output-invalid-json",
|
|
34
|
+
})
|
|
35
|
+
STRUCTURED_OUTPUT_MISSING_MESSAGE = (
|
|
36
|
+
"l'agent a termine sans erreur mais sa sortie structuree manque "
|
|
37
|
+
"ou n'est pas le JSON attendu"
|
|
38
|
+
)
|
|
39
|
+
# Impose le premier plan aux sous-agents de l'agent Claude d'une activite ; desactive
|
|
40
|
+
# aussi ses commandes shell en arriere-plan.
|
|
41
|
+
CLAUDE_FOREGROUND_ENV = "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS"
|
|
42
|
+
|
|
25
43
|
_SAFE_DIAGNOSTIC_MESSAGES = {
|
|
26
44
|
"final-output-empty": "--output-last-message est vide",
|
|
27
45
|
"final-output-absent": "--output-last-message est absent",
|
|
@@ -165,7 +183,7 @@ class AgentAdapter(ABC):
|
|
|
165
183
|
capture_output=True,
|
|
166
184
|
stdin=subprocess.DEVNULL,
|
|
167
185
|
shell=False,
|
|
168
|
-
env=
|
|
186
|
+
env=self.structured_environment(),
|
|
169
187
|
)
|
|
170
188
|
execution = AgentExecution(
|
|
171
189
|
result.returncode,
|
|
@@ -223,7 +241,9 @@ class AgentAdapter(ABC):
|
|
|
223
241
|
"Sortie structuree invalide "
|
|
224
242
|
f"({self.name}) [{diagnostic}]"
|
|
225
243
|
+ (
|
|
226
|
-
f" : {
|
|
244
|
+
f" : {self.missing_output_message(execution.stdout)}"
|
|
245
|
+
if diagnostic == STRUCTURED_OUTPUT_MISSING
|
|
246
|
+
else f" : {_SAFE_DIAGNOSTIC_MESSAGES[diagnostic]}"
|
|
227
247
|
if diagnostic in _SAFE_DIAGNOSTIC_MESSAGES
|
|
228
248
|
else ""
|
|
229
249
|
),
|
|
@@ -251,6 +271,27 @@ class AgentAdapter(ABC):
|
|
|
251
271
|
except FileNotFoundError:
|
|
252
272
|
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable", duration_seconds=time.monotonic() - started)
|
|
253
273
|
|
|
274
|
+
def structured_environment(self) -> dict[str, str]:
|
|
275
|
+
"""Environnement de l'agent d'une activite : jamais la cle Linear."""
|
|
276
|
+
return environment_without_linear_key()
|
|
277
|
+
|
|
278
|
+
def subagent_counts(self, stdout: str) -> tuple[int | None, int | None]:
|
|
279
|
+
"""Sous-agents lances et lances en arriere-plan ; None si l'agent ne le dit pas."""
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
def missing_output_message(self, stdout: str) -> str:
|
|
283
|
+
"""Cause d'une sortie structuree absente : message fixe et entiers seulement."""
|
|
284
|
+
try:
|
|
285
|
+
spawned, background = self.subagent_counts(stdout)
|
|
286
|
+
except Exception: # noqa: BLE001 - format externe non contractuel
|
|
287
|
+
spawned, background = None, None
|
|
288
|
+
message = STRUCTURED_OUTPUT_MISSING_MESSAGE
|
|
289
|
+
if spawned is not None:
|
|
290
|
+
message += f" ; sous-agents lances : {spawned}"
|
|
291
|
+
if background is not None:
|
|
292
|
+
message += f", dont {background} en arriere-plan"
|
|
293
|
+
return message
|
|
294
|
+
|
|
254
295
|
def session_id(self, stdout: str, stderr: str) -> str | None:
|
|
255
296
|
"""Extrait un identifiant opaque de session, sans conserver la sortie."""
|
|
256
297
|
return _session_id(stdout, stderr)
|
|
@@ -326,8 +367,23 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
326
367
|
return envelope["structured_output"]
|
|
327
368
|
result = envelope.get("result")
|
|
328
369
|
if isinstance(result, str) and result.strip():
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
try:
|
|
371
|
+
return json.loads(_strip_json_fence(result))
|
|
372
|
+
except json.JSONDecodeError:
|
|
373
|
+
pass
|
|
374
|
+
raise StructuredOutputError(
|
|
375
|
+
STRUCTURED_OUTPUT_MISSING, "champ structured_output absent"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def structured_environment(self) -> dict[str, str]:
|
|
379
|
+
return {**environment_without_linear_key(), CLAUDE_FOREGROUND_ENV: "1"}
|
|
380
|
+
|
|
381
|
+
def subagent_counts(self, stdout: str) -> tuple[int | None, int | None]:
|
|
382
|
+
envelope = json.loads(stdout)
|
|
383
|
+
stats = envelope.get("subagent_stats") if isinstance(envelope, dict) else None
|
|
384
|
+
if not isinstance(stats, dict):
|
|
385
|
+
return None, None
|
|
386
|
+
return _count(stats.get("spawned")), _count(stats.get("started_in_background"))
|
|
331
387
|
|
|
332
388
|
def usage(self, stdout: str, session_id: str | None) -> AgentUsage | None:
|
|
333
389
|
"""Lit l'enveloppe --output-format json : usage cumule de tous les tours."""
|
|
@@ -515,6 +571,14 @@ def get_agent_adapter(name: str) -> AgentAdapter:
|
|
|
515
571
|
return adapter_type()
|
|
516
572
|
|
|
517
573
|
|
|
574
|
+
def describe_missing_output(agent: str, stdout: str) -> str:
|
|
575
|
+
"""Cause d'une sortie structuree absente, pour la reprise guidee."""
|
|
576
|
+
try:
|
|
577
|
+
return get_agent_adapter(agent).missing_output_message(stdout)
|
|
578
|
+
except ValueError:
|
|
579
|
+
return STRUCTURED_OUTPUT_MISSING_MESSAGE
|
|
580
|
+
|
|
581
|
+
|
|
518
582
|
def _portable_prompt(prompt: str) -> str:
|
|
519
583
|
"""Traduit une slash-command Claude en instruction portable pour les autres agents."""
|
|
520
584
|
match = re.fullmatch(r"/devflow\.([a-z0-9-]+)(?:\s+(.*))?", prompt.strip())
|
|
@@ -610,6 +674,10 @@ def _int(value: object) -> int:
|
|
|
610
674
|
return value if isinstance(value, int) and not isinstance(value, bool) else 0
|
|
611
675
|
|
|
612
676
|
|
|
677
|
+
def _count(value: object) -> int | None:
|
|
678
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
679
|
+
|
|
680
|
+
|
|
613
681
|
def _codex_home() -> Path:
|
|
614
682
|
return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
|
|
615
683
|
|
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -70,9 +70,20 @@ from devflow_cli.core.validation_approvals import (
|
|
|
70
70
|
)
|
|
71
71
|
from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
|
|
72
72
|
from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
|
|
73
|
-
from devflow_cli.
|
|
73
|
+
from devflow_cli.core.result_template import (
|
|
74
|
+
remove_result_template,
|
|
75
|
+
write_result_template,
|
|
76
|
+
)
|
|
77
|
+
from devflow_cli.adapters.agents import (
|
|
78
|
+
MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS,
|
|
79
|
+
STRUCTURED_OUTPUT_MISSING,
|
|
80
|
+
StructuredOutputError,
|
|
81
|
+
describe_missing_output,
|
|
82
|
+
get_agent_adapter,
|
|
83
|
+
)
|
|
74
84
|
from devflow_cli.utils.console import console, fail, ok, warn
|
|
75
85
|
from devflow_cli.utils.paths import (
|
|
86
|
+
activity_attempt_file,
|
|
76
87
|
activity_raw_file,
|
|
77
88
|
activity_result_file,
|
|
78
89
|
project_root_for_feature,
|
|
@@ -476,6 +487,7 @@ def run_adaptive(
|
|
|
476
487
|
json_output=json_output,
|
|
477
488
|
)
|
|
478
489
|
if outcome.advanced:
|
|
490
|
+
remove_result_template(current_state.issueId, outcome.activity)
|
|
479
491
|
if json_output:
|
|
480
492
|
_print_execution_json(
|
|
481
493
|
current_state.issueId, outcome.activity, True, outcome.reason,
|
|
@@ -493,11 +505,25 @@ def run_adaptive(
|
|
|
493
505
|
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
494
506
|
safe_refresh_worklog(feature_dir)
|
|
495
507
|
exit_code = exit_code_for_run_reason(outcome.reason)
|
|
508
|
+
execution = attempt.get("execution")
|
|
509
|
+
recovery = (
|
|
510
|
+
_missing_output_recovery(
|
|
511
|
+
feature_dir, current_state.issueId, outcome.activity, agent,
|
|
512
|
+
execution.stdout,
|
|
513
|
+
mutated=outcome.reason == "agent-failed-after-mutation",
|
|
514
|
+
)
|
|
515
|
+
if outcome.reason in {"agent-failed", "agent-failed-after-mutation"}
|
|
516
|
+
and execution is not None
|
|
517
|
+
and execution.final_output_diagnostic in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
518
|
+
else None
|
|
519
|
+
)
|
|
520
|
+
if outcome.reason == "preflight-evidence-stale":
|
|
521
|
+
recovery = _stale_preflight_recovery(current_state)
|
|
496
522
|
if json_output:
|
|
497
523
|
_print_execution_json(
|
|
498
524
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
499
525
|
outcome.detail, outcome.nextActivity, exit_code,
|
|
500
|
-
warnings=outcome.warnings,
|
|
526
|
+
warnings=outcome.warnings, recovery=recovery,
|
|
501
527
|
)
|
|
502
528
|
elif outcome.reason in BASE_REFUSALS:
|
|
503
529
|
fail(outcome.detail)
|
|
@@ -509,7 +535,8 @@ def run_adaptive(
|
|
|
509
535
|
f" Activite '{outcome.activity}' non validee : {outcome.reason}"
|
|
510
536
|
)
|
|
511
537
|
_print_run_recovery(
|
|
512
|
-
current_state.issueId, outcome.reason, outcome.activity, agent, feature_dir
|
|
538
|
+
current_state.issueId, outcome.reason, outcome.activity, agent, feature_dir,
|
|
539
|
+
recovery=recovery,
|
|
513
540
|
)
|
|
514
541
|
raise typer.Exit(int(exit_code))
|
|
515
542
|
|
|
@@ -611,8 +638,18 @@ def reconcile_adaptive(
|
|
|
611
638
|
fail("--reason ne peut etre utilise qu'avec --result")
|
|
612
639
|
raise typer.Exit(int(ExitCode.USAGE))
|
|
613
640
|
|
|
641
|
+
missing_output: dict[str, str] = {}
|
|
642
|
+
|
|
614
643
|
def recover(agent: str, raw_output: str) -> object:
|
|
615
|
-
|
|
644
|
+
try:
|
|
645
|
+
return get_agent_adapter(agent).recover_structured_payload(raw_output)
|
|
646
|
+
except (json.JSONDecodeError, StructuredOutputError) as exc:
|
|
647
|
+
if (
|
|
648
|
+
isinstance(exc, json.JSONDecodeError)
|
|
649
|
+
or exc.diagnostic in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
650
|
+
):
|
|
651
|
+
missing_output.update(agent=agent, raw=raw_output)
|
|
652
|
+
raise
|
|
616
653
|
|
|
617
654
|
outcome = reconcile_current_activity(
|
|
618
655
|
feature_dir,
|
|
@@ -628,9 +665,29 @@ def reconcile_adaptive(
|
|
|
628
665
|
)
|
|
629
666
|
_print_warnings(outcome.warnings)
|
|
630
667
|
_print_pending_approvals(feature_dir, state.issueId, outcome.activity)
|
|
668
|
+
remove_result_template(state.issueId, outcome.activity)
|
|
631
669
|
return
|
|
632
670
|
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
633
671
|
safe_refresh_worklog(feature_dir)
|
|
672
|
+
if (
|
|
673
|
+
result is None
|
|
674
|
+
and outcome.reason == "invalid-result"
|
|
675
|
+
and missing_output
|
|
676
|
+
and _last_run_missed_structured_output(feature_dir, outcome.activity)
|
|
677
|
+
):
|
|
678
|
+
# Trace sans reponse structuree : message actionnable, pas l'erreur de lecture.
|
|
679
|
+
recovery = _missing_output_recovery(
|
|
680
|
+
feature_dir, state.issueId, outcome.activity,
|
|
681
|
+
missing_output["agent"], missing_output["raw"],
|
|
682
|
+
)
|
|
683
|
+
fail(
|
|
684
|
+
f"Reconciliation de '{outcome.activity}' refusee : {outcome.reason} "
|
|
685
|
+
f"({STRUCTURED_OUTPUT_MISSING})"
|
|
686
|
+
)
|
|
687
|
+
_print_warnings(outcome.warnings)
|
|
688
|
+
console.print(" Reprise guidee :")
|
|
689
|
+
_print_recovery_steps(recovery)
|
|
690
|
+
raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
|
|
634
691
|
if outcome.reason in BASE_REFUSALS:
|
|
635
692
|
fail(outcome.detail)
|
|
636
693
|
else:
|
|
@@ -643,7 +700,10 @@ def reconcile_adaptive(
|
|
|
643
700
|
console.print(" Suite : attendez la fin de l'activite en cours, puis relancez.")
|
|
644
701
|
raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
|
|
645
702
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
646
|
-
if
|
|
703
|
+
if (
|
|
704
|
+
outcome.reason == "reconcile-refused"
|
|
705
|
+
and activity_attempt_file(feature_dir, outcome.activity).is_file()
|
|
706
|
+
):
|
|
647
707
|
console.print(
|
|
648
708
|
f" Abandon audite : devflow adaptive discard-attempt {state.issueId} --reason MOTIF"
|
|
649
709
|
)
|
|
@@ -883,7 +943,8 @@ def _execute_local_verification(
|
|
|
883
943
|
sync_activity_status(issue_id, "verify", stream)
|
|
884
944
|
outcome = run_automatic_verification(feature_dir, project_root, refresh=refresh)
|
|
885
945
|
recovery = (
|
|
886
|
-
None if outcome.passed
|
|
946
|
+
None if outcome.passed
|
|
947
|
+
else _verify_recovery(feature_dir, issue_id, outcome, refresh=refresh)
|
|
887
948
|
)
|
|
888
949
|
if json_output:
|
|
889
950
|
_print_execution_json(
|
|
@@ -900,6 +961,8 @@ def _execute_local_verification(
|
|
|
900
961
|
ok(f"Preuves de verify rafraichies. Activite courante : {outcome.nextActivity}")
|
|
901
962
|
else:
|
|
902
963
|
ok(f"Preuves generees et validees. Prochaine : {outcome.nextActivity}")
|
|
964
|
+
if outcome.detail:
|
|
965
|
+
console.print(f" Motif : {outcome.detail}", markup=False, soft_wrap=True)
|
|
903
966
|
return
|
|
904
967
|
fail(outcome.detail or outcome.reason)
|
|
905
968
|
console.print(f" Verification automatique refusee : {outcome.reason}")
|
|
@@ -922,17 +985,20 @@ PLANNED_PREFIXES = {"PREP-": "prepare", "PLAN-": "plan"}
|
|
|
922
985
|
|
|
923
986
|
|
|
924
987
|
def _verify_recovery(
|
|
925
|
-
feature_dir: Path, issue_id: str, outcome: AutomaticVerificationOutcome
|
|
988
|
+
feature_dir: Path, issue_id: str, outcome: AutomaticVerificationOutcome,
|
|
989
|
+
*, refresh: bool = False,
|
|
926
990
|
) -> dict:
|
|
927
991
|
"""Reprise guidee d'un refus de verify, par motif, sans heuristique (B2).
|
|
928
992
|
|
|
929
993
|
Approbation manquante : chaque approve-command puis verify. Executable introuvable
|
|
930
|
-
(code 127) : corriger l'environnement puis verify. Echec d'un controle :
|
|
931
|
-
le code (reopen implement)
|
|
994
|
+
(code 127) : corriger l'environnement puis verify. Echec d'un controle : trois voies,
|
|
995
|
+
le code (reopen implement), la validation (reopen de l'activite qui l'a inscrite) ou
|
|
996
|
+
un echec passager (relancer verify). Apres un verify --refresh refuse, toutes les
|
|
997
|
+
commandes verify portent --refresh.
|
|
932
998
|
"""
|
|
933
999
|
issue = shlex.quote(issue_id)
|
|
934
1000
|
verify_command = {
|
|
935
|
-
"command": f"devflow adaptive verify {issue}",
|
|
1001
|
+
"command": f"devflow adaptive verify {issue}" + (" --refresh" if refresh else ""),
|
|
936
1002
|
"purpose": "relancer la verification",
|
|
937
1003
|
}
|
|
938
1004
|
state = read_adaptive_state(feature_dir / "workflow-state.json")
|
|
@@ -1010,6 +1076,13 @@ def _verify_recovery(
|
|
|
1010
1076
|
"command": f"devflow adaptive reopen {issue} {origin} --reason {reason_text}",
|
|
1011
1077
|
"purpose": f"voie 2 : la validation {names}, inscrite par {origin}, est en cause",
|
|
1012
1078
|
})
|
|
1079
|
+
commands.append({
|
|
1080
|
+
"command": verify_command["command"],
|
|
1081
|
+
"purpose": (
|
|
1082
|
+
"voie 3 : echec passager (reseau, conteneur, service externe), "
|
|
1083
|
+
"relancer la verification"
|
|
1084
|
+
),
|
|
1085
|
+
})
|
|
1013
1086
|
return {
|
|
1014
1087
|
"kind": "check-failed",
|
|
1015
1088
|
"summary": (
|
|
@@ -1194,8 +1267,10 @@ def _operational_status(
|
|
|
1194
1267
|
risks = _remaining_risks(feature_dir)
|
|
1195
1268
|
if not isinstance(blockers, list):
|
|
1196
1269
|
blockers = []
|
|
1197
|
-
stale = set(
|
|
1198
|
-
|
|
1270
|
+
stale: set[str] = set()
|
|
1271
|
+
if state.status != "completed": # aucune gate ne consulte la peremption apres done
|
|
1272
|
+
stale.update(stale_devflow_evidence(feature_dir, state))
|
|
1273
|
+
stale.update(stale_review_evidence(feature_dir, state))
|
|
1199
1274
|
evidence = [
|
|
1200
1275
|
{
|
|
1201
1276
|
"id": evidence_id,
|
|
@@ -1372,8 +1447,128 @@ def _fallback_agents(agent: str) -> list[str]:
|
|
|
1372
1447
|
]
|
|
1373
1448
|
|
|
1374
1449
|
|
|
1450
|
+
def _last_run_missed_structured_output(feature_dir: Path, activity: str) -> bool:
|
|
1451
|
+
"""Vrai si la derniere execution de l'activite a fini sans sortie structuree."""
|
|
1452
|
+
try:
|
|
1453
|
+
audit = json.loads(
|
|
1454
|
+
(feature_dir / "agent-execution.json").read_text(encoding="utf-8")
|
|
1455
|
+
)
|
|
1456
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
1457
|
+
return False
|
|
1458
|
+
return (
|
|
1459
|
+
isinstance(audit, dict)
|
|
1460
|
+
and audit.get("activity") == activity
|
|
1461
|
+
and audit.get("finalOutputDiagnostic") in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def _missing_output_recovery(
|
|
1466
|
+
feature_dir: Path, issue_id: str, activity: str, agent: str, raw_output: str,
|
|
1467
|
+
*, mutated: bool | None = None,
|
|
1468
|
+
) -> dict:
|
|
1469
|
+
"""Reprise guidee d'une sortie structuree absente apres un agent termine sans erreur.
|
|
1470
|
+
|
|
1471
|
+
Le modele prerempli (contrat courant, fichiers observes depuis la base) est ecrit
|
|
1472
|
+
hors du depot ; ses champs a completer sont vides, donc refuses tels quels.
|
|
1473
|
+
"""
|
|
1474
|
+
issue = shlex.quote(issue_id)
|
|
1475
|
+
cause = describe_missing_output(agent, raw_output)
|
|
1476
|
+
recovery: dict = {
|
|
1477
|
+
"kind": STRUCTURED_OUTPUT_MISSING,
|
|
1478
|
+
"summary": f"Sortie structuree absente : {cause}.",
|
|
1479
|
+
"resultTemplate": None,
|
|
1480
|
+
"commands": [],
|
|
1481
|
+
}
|
|
1482
|
+
if activity not in MANUAL_RECONCILIATION_ACTIVITIES:
|
|
1483
|
+
recovery["commands"].append({
|
|
1484
|
+
"command": f"devflow adaptive run {issue} --agent {shlex.quote(agent)}",
|
|
1485
|
+
"purpose": "relancer l'agent : la correction manuelle est interdite pour cette activite",
|
|
1486
|
+
})
|
|
1487
|
+
return recovery
|
|
1488
|
+
try:
|
|
1489
|
+
attempt = json.loads(
|
|
1490
|
+
activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
|
|
1491
|
+
)
|
|
1492
|
+
if mutated is None: # reconcile : le motif vient de la fiche de tentative
|
|
1493
|
+
mutated = attempt["reason"] == "agent-failed-after-mutation"
|
|
1494
|
+
contract = json.loads(
|
|
1495
|
+
(feature_dir / "activity-contract.json").read_text(encoding="utf-8")
|
|
1496
|
+
)
|
|
1497
|
+
if (
|
|
1498
|
+
contract["activity"] != activity
|
|
1499
|
+
or contract["contractId"] != attempt["contractId"]
|
|
1500
|
+
):
|
|
1501
|
+
raise ValueError("contrat et tentative incoherents")
|
|
1502
|
+
template = write_result_template(
|
|
1503
|
+
issue_id,
|
|
1504
|
+
activity,
|
|
1505
|
+
contract["contractId"],
|
|
1506
|
+
contract["expectedOutput"],
|
|
1507
|
+
sorted(attempt["observedFiles"]),
|
|
1508
|
+
)
|
|
1509
|
+
except (OSError, UnicodeDecodeError, KeyError, TypeError, ValueError):
|
|
1510
|
+
template = None
|
|
1511
|
+
if template is not None:
|
|
1512
|
+
recovery["resultTemplate"] = str(template)
|
|
1513
|
+
recovery["commands"].extend([
|
|
1514
|
+
{
|
|
1515
|
+
"command": "git status --short",
|
|
1516
|
+
"purpose": "inspecter les ecritures deja effectuees",
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
"command": (
|
|
1520
|
+
f"devflow adaptive reconcile {issue} --result "
|
|
1521
|
+
f"{shlex.quote(str(template)) if template else 'FICHIER.json'} --reason MOTIF"
|
|
1522
|
+
),
|
|
1523
|
+
"purpose": (
|
|
1524
|
+
"completer le modele prerempli (status, summary, checks, evidence) "
|
|
1525
|
+
"puis valider sans relancer l'agent"
|
|
1526
|
+
if template
|
|
1527
|
+
else "modele prerempli non ecrit : fournir le resultat du contrat"
|
|
1528
|
+
),
|
|
1529
|
+
},
|
|
1530
|
+
])
|
|
1531
|
+
if not mutated:
|
|
1532
|
+
# Apres des ecritures, la relance contredirait « reconciliez d'abord ».
|
|
1533
|
+
recovery["commands"].append({
|
|
1534
|
+
"command": f"devflow adaptive run {issue} --agent {shlex.quote(agent)}",
|
|
1535
|
+
"purpose": "relancer l'agent : il n'a rien ecrit, la tentative est remplacee",
|
|
1536
|
+
})
|
|
1537
|
+
return recovery
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _stale_preflight_recovery(state: AdaptiveWorkflowState) -> dict:
|
|
1541
|
+
"""Reprise guidee d'une preuve de pre-vol perimee au lancement d'implement."""
|
|
1542
|
+
issue = shlex.quote(state.issueId)
|
|
1543
|
+
if _reopenable(state, "preflight-evidence"):
|
|
1544
|
+
command = {
|
|
1545
|
+
"command": f"devflow adaptive reopen {issue} preflight-evidence --reason MOTIF",
|
|
1546
|
+
"purpose": "refaire la preuve de pre-vol sur les entrees actuelles",
|
|
1547
|
+
}
|
|
1548
|
+
else:
|
|
1549
|
+
command = {
|
|
1550
|
+
"command": f"devflow adaptive status {issue}",
|
|
1551
|
+
"purpose": "consulter l'etat du parcours",
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
"kind": "preflight-evidence-stale",
|
|
1555
|
+
"summary": "Preuve de pre-vol perimee : la refaire avant implement.",
|
|
1556
|
+
"commands": [command],
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
def _print_recovery_steps(recovery: dict) -> None:
|
|
1561
|
+
# soft_wrap : la commande a copier reste sur une seule ligne.
|
|
1562
|
+
console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
|
|
1563
|
+
for step in recovery["commands"]:
|
|
1564
|
+
console.print(
|
|
1565
|
+
f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
|
|
1375
1569
|
def _print_run_recovery(
|
|
1376
|
-
issue_id: str, reason: str, activity: str, agent: str, feature_dir: Path
|
|
1570
|
+
issue_id: str, reason: str, activity: str, agent: str, feature_dir: Path,
|
|
1571
|
+
*, recovery: dict | None = None,
|
|
1377
1572
|
) -> None:
|
|
1378
1573
|
console.print(" Reprise guidee :")
|
|
1379
1574
|
console.print(f" devflow adaptive status {issue_id}")
|
|
@@ -1382,6 +1577,11 @@ def _print_run_recovery(
|
|
|
1382
1577
|
" Trace agent : "
|
|
1383
1578
|
f"{activity_raw_file(feature_dir, activity).name}"
|
|
1384
1579
|
)
|
|
1580
|
+
if recovery is not None:
|
|
1581
|
+
if reason == "agent-failed-after-mutation":
|
|
1582
|
+
console.print(" Ne relancez pas automatiquement l'agent : reconciliez d'abord les changements partiels.")
|
|
1583
|
+
_print_recovery_steps(recovery)
|
|
1584
|
+
return
|
|
1385
1585
|
if reason == "agent-unavailable":
|
|
1386
1586
|
console.print(" Agent indisponible : l'activite reste courante et rien n'est invalide.")
|
|
1387
1587
|
console.print(f" devflow check --ai {agent} --live # verifier le retour de l'agent")
|
devflow_cli/commands/assess.py
CHANGED
|
@@ -16,6 +16,7 @@ from devflow_cli.core.assessment import (
|
|
|
16
16
|
write_assessment,
|
|
17
17
|
)
|
|
18
18
|
from devflow_cli.core.adaptive_workflow import workflow_lock
|
|
19
|
+
from devflow_cli.core.activity_contracts import NEED_STATEMENT, validate_need_statement
|
|
19
20
|
from devflow_cli.core.contract_schemas import check_schema_version
|
|
20
21
|
from devflow_cli.utils.console import console, fail, ok
|
|
21
22
|
from devflow_cli.utils.paths import resolve_feature_dir
|
|
@@ -138,4 +139,13 @@ def assess(
|
|
|
138
139
|
console.print(f" Activites : {' -> '.join(assessment.activities)}")
|
|
139
140
|
console.print(f" Preuves : {', '.join(assessment.requiredEvidence)}")
|
|
140
141
|
console.print(f" Rapport : {output_file}")
|
|
141
|
-
|
|
142
|
+
try:
|
|
143
|
+
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
144
|
+
except ValueError:
|
|
145
|
+
console.print(
|
|
146
|
+
f" Suite : rediger {NEED_STATEMENT} ({feature_dir / NEED_STATEMENT}), "
|
|
147
|
+
f"puis devflow adaptive start {assessment.issueId}",
|
|
148
|
+
markup=False, soft_wrap=True,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
console.print(f" Suite : devflow adaptive start {assessment.issueId}")
|
|
@@ -43,10 +43,20 @@ DOCUMENT_ACTIVITIES = set(DOCUMENT_ARTIFACTS)
|
|
|
43
43
|
ActivityStatus = Literal["completed", "blocked"]
|
|
44
44
|
|
|
45
45
|
REMOVED_RESULT_FIELDS = ("omissionRecommendations", "reviewedOmissions")
|
|
46
|
+
# Commune a tous les contrats et a tous les agents : un sous-agent en arriere-plan
|
|
47
|
+
# force l'agent a rendre la main avant sa sortie structuree, qui ne s'envoie qu'une fois.
|
|
48
|
+
SUBAGENT_CONSTRAINT = (
|
|
49
|
+
"Si des sous-agents sont utilises, les lancer au premier plan, jamais en "
|
|
50
|
+
"arriere-plan, et attendre qu'ils soient tous termines avant la sortie structuree. "
|
|
51
|
+
"La sortie structuree du contrat est le dernier message de l'agent : ne rien "
|
|
52
|
+
"envoyer apres elle."
|
|
53
|
+
)
|
|
46
54
|
PROTECTED_VALIDATION_CONSTRAINT = (
|
|
47
55
|
"Les garde-fous sont actifs : inclure dans validationPlan au moins une validation "
|
|
48
56
|
"bloquante explicitement consacree a la securite, aux donnees, a l'authentification, "
|
|
49
|
-
"a une migration ou au rollback, dont id ou purpose contient l'un de ces termes."
|
|
57
|
+
"a une migration ou au rollback, dont id ou purpose contient l'un de ces termes. "
|
|
58
|
+
"Toute validation protegee est bloquante et doit reussir en code 0 : une recherche "
|
|
59
|
+
"dont l'absence de correspondance est le resultat attendu ne s'exprime pas ainsi."
|
|
50
60
|
)
|
|
51
61
|
|
|
52
62
|
PREFLIGHT_HEADINGS = (
|
|
@@ -170,6 +180,10 @@ def validate_need_statement(path: Path) -> None:
|
|
|
170
180
|
raise ValueError(f"{NEED_STATEMENT} est vide : decrivez le besoin avant de demarrer")
|
|
171
181
|
|
|
172
182
|
|
|
183
|
+
class PreflightEvidenceStaleError(ValueError):
|
|
184
|
+
"""Preuve de pre-vol anterieure a une entree : motif de refus dedie."""
|
|
185
|
+
|
|
186
|
+
|
|
173
187
|
def validate_preflight_evidence(path: Path, *, newer_than: list[Path] | None = None) -> None:
|
|
174
188
|
"""Validate the V4 preflight artifact without exporting its contents."""
|
|
175
189
|
try:
|
|
@@ -197,7 +211,7 @@ def validate_preflight_evidence(path: Path, *, newer_than: list[Path] | None = N
|
|
|
197
211
|
artifact_mtime = path.stat().st_mtime_ns
|
|
198
212
|
stale_inputs = [item.name for item in newer_than if item.is_file() and item.stat().st_mtime_ns > artifact_mtime]
|
|
199
213
|
if stale_inputs:
|
|
200
|
-
raise
|
|
214
|
+
raise PreflightEvidenceStaleError("preuve de pre-vol desuete apres : " + ", ".join(sorted(stale_inputs)))
|
|
201
215
|
|
|
202
216
|
|
|
203
217
|
def build_activity_contract(
|
|
@@ -244,6 +258,7 @@ def build_activity_contract(
|
|
|
244
258
|
"Signaler tout blocage au lieu de contourner une validation.",
|
|
245
259
|
"Dans checks, status vaut passed si et seulement si exitCode vaut 0. Une commande dont l'absence de resultat constitue le succes attendu doit etre declaree failed, ou remplacee par une commande qui renvoie 0 en cas de succes.",
|
|
246
260
|
"Donner un id stable a chaque objectif de check. Si un check echoue puis est corrige et relance pour le meme objectif, reutiliser cet id : seule la derniere tentative fait foi ; signaler dans remainingRisks toute instabilite encore pertinente.",
|
|
261
|
+
SUBAGENT_CONSTRAINT,
|
|
247
262
|
]
|
|
248
263
|
if activity in DOCUMENT_ACTIVITIES:
|
|
249
264
|
artifact_name = document_artifact_name(activity, reopened=reopened)
|
|
@@ -6,6 +6,7 @@ import hashlib
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
+
import shlex
|
|
9
10
|
import tempfile
|
|
10
11
|
from contextlib import ExitStack
|
|
11
12
|
from dataclasses import dataclass, replace
|
|
@@ -18,6 +19,7 @@ from devflow_cli.core.activity_contracts import (
|
|
|
18
19
|
ActivityResult,
|
|
19
20
|
DOCUMENT_ARTIFACTS,
|
|
20
21
|
NEED_STATEMENT,
|
|
22
|
+
PreflightEvidenceStaleError,
|
|
21
23
|
build_activity_contract,
|
|
22
24
|
contract_is_reopened_plan,
|
|
23
25
|
document_artifact_name,
|
|
@@ -155,6 +157,13 @@ def run_current_activity(
|
|
|
155
157
|
return AdaptiveRunOutcome(
|
|
156
158
|
exc.reason, state.currentActivity, state.currentActivity, str(exc)
|
|
157
159
|
)
|
|
160
|
+
except PreflightEvidenceStaleError as exc:
|
|
161
|
+
return AdaptiveRunOutcome(
|
|
162
|
+
"preflight-evidence-stale",
|
|
163
|
+
state.currentActivity,
|
|
164
|
+
state.currentActivity,
|
|
165
|
+
str(exc),
|
|
166
|
+
)
|
|
158
167
|
except (OSError, ValueError) as exc:
|
|
159
168
|
return AdaptiveRunOutcome(
|
|
160
169
|
"unsupported-activity", state.currentActivity, state.currentActivity, str(exc)
|
|
@@ -337,9 +346,18 @@ def reconcile_current_activity(
|
|
|
337
346
|
raise ValueError("le resultat corrige doit etre un objet JSON")
|
|
338
347
|
if not correction_reason.strip():
|
|
339
348
|
raise ValueError("un motif est obligatoire pour corriger le resultat")
|
|
340
|
-
|
|
341
|
-
activity_attempt_file(feature_dir, activity).read_text(
|
|
342
|
-
|
|
349
|
+
try:
|
|
350
|
+
attempt_text = activity_attempt_file(feature_dir, activity).read_text(
|
|
351
|
+
encoding="utf-8"
|
|
352
|
+
)
|
|
353
|
+
except FileNotFoundError:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
f"aucune tentative echouee a reconcilier pour '{activity}' : "
|
|
356
|
+
"reconcile ne s'applique qu'a une tentative agent echouee ; apres "
|
|
357
|
+
"un resultat blocked, lever le blocage puis relancer "
|
|
358
|
+
f"devflow adaptive run {shlex.quote(state.issueId)}"
|
|
359
|
+
) from None
|
|
360
|
+
attempt = json.loads(attempt_text)
|
|
343
361
|
if not _valid_attempt(attempt):
|
|
344
362
|
raise ValueError("fiche de tentative invalide")
|
|
345
363
|
agent = attempt["agent"]
|
|
@@ -733,6 +751,17 @@ def _validated_result(
|
|
|
733
751
|
raise ValueError(
|
|
734
752
|
f"validation {item.id}, commande : {command_label} — {exc}"
|
|
735
753
|
) from exc
|
|
754
|
+
if contract.guardrails:
|
|
755
|
+
for item in result.validationPlan:
|
|
756
|
+
if is_protected_validation(item.id, item.purpose) and not item.blocking:
|
|
757
|
+
raise ValueError(
|
|
758
|
+
f"validation {item.id} : un controle protege (securite, "
|
|
759
|
+
"donnees, authentification, migration, rollback) est toujours "
|
|
760
|
+
"bloquant et doit reussir en code 0 ; declarer blocking a true. "
|
|
761
|
+
"Une recherche dont l'absence de correspondance est le resultat "
|
|
762
|
+
"attendu ne s'exprime pas ainsi : utiliser une commande qui "
|
|
763
|
+
"renvoie 0 en cas de succes."
|
|
764
|
+
)
|
|
736
765
|
if contract.guardrails and not any(
|
|
737
766
|
item.blocking and is_protected_validation(item.id, item.purpose)
|
|
738
767
|
for item in result.validationPlan
|
|
@@ -22,7 +22,13 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
22
22
|
workflow_lock,
|
|
23
23
|
write_adaptive_state,
|
|
24
24
|
)
|
|
25
|
-
from devflow_cli.core.activity_base import
|
|
25
|
+
from devflow_cli.core.activity_base import (
|
|
26
|
+
ActivityBase,
|
|
27
|
+
BaseObservationError,
|
|
28
|
+
enter_current_activity,
|
|
29
|
+
files_since_base,
|
|
30
|
+
parse_recorded_result,
|
|
31
|
+
)
|
|
26
32
|
from devflow_cli.core.activity_contracts import (
|
|
27
33
|
DOCUMENT_ARTIFACTS,
|
|
28
34
|
activity_contract_id,
|
|
@@ -297,6 +303,38 @@ DOCUMENT_INSENSITIVE_EVIDENCE_IDS = frozenset({"test-suite", "requirements-cover
|
|
|
297
303
|
NON_DOCUMENT_FINGERPRINT_PREFIX = "non-document-v1:"
|
|
298
304
|
|
|
299
305
|
|
|
306
|
+
def is_document_path(path: str) -> bool:
|
|
307
|
+
"""Vrai pour un README ou un fichier Markdown du dossier docs."""
|
|
308
|
+
return Path(path).name.casefold() == "readme.md" or (
|
|
309
|
+
path.startswith("docs/") and path.casefold().endswith(".md")
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _observed_document_batch(
|
|
314
|
+
state: AdaptiveWorkflowState, feature_dir: Path, project_root: Path
|
|
315
|
+
) -> list[str] | None:
|
|
316
|
+
"""Fichiers observes depuis la base d'implement si tous sont documentaires.
|
|
317
|
+
|
|
318
|
+
None quand le lot n'est pas eligible : base inobservable, lot vide, fichier
|
|
319
|
+
non documentaire ou lien symbolique. L'observation decide, pas l'agent.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
base = ActivityBase.from_dict(
|
|
323
|
+
state.implementBase,
|
|
324
|
+
feature_path=feature_dir.resolve().relative_to(project_root.resolve()).as_posix(),
|
|
325
|
+
)
|
|
326
|
+
observed = sorted(
|
|
327
|
+
files_since_base(project_root, feature_dir, base, issue_id=state.issueId)
|
|
328
|
+
)
|
|
329
|
+
except (BaseObservationError, ValueError, OSError):
|
|
330
|
+
return None
|
|
331
|
+
if not observed or not all(is_document_path(path) for path in observed):
|
|
332
|
+
return None
|
|
333
|
+
if any((project_root / path).is_symlink() for path in observed):
|
|
334
|
+
return None
|
|
335
|
+
return observed
|
|
336
|
+
|
|
337
|
+
|
|
300
338
|
def _non_document_fingerprint(snapshot: WorktreeSnapshot, excluded_dir: str) -> str:
|
|
301
339
|
"""Empreinte des sources, sans README ni Markdown du dossier docs.
|
|
302
340
|
|
|
@@ -306,10 +344,7 @@ def _non_document_fingerprint(snapshot: WorktreeSnapshot, excluded_dir: str) ->
|
|
|
306
344
|
source_files = {
|
|
307
345
|
path: fingerprint
|
|
308
346
|
for path, fingerprint in snapshot.files.items()
|
|
309
|
-
if not (
|
|
310
|
-
Path(path).name.casefold() == "readme.md"
|
|
311
|
-
or (path.startswith("docs/") and path.casefold().endswith(".md"))
|
|
312
|
-
)
|
|
347
|
+
if not is_document_path(path)
|
|
313
348
|
}
|
|
314
349
|
return NON_DOCUMENT_FINGERPRINT_PREFIX + worktree_fingerprint(
|
|
315
350
|
replace(snapshot, files=source_files), excluded_dir
|
|
@@ -562,7 +597,10 @@ def run_automatic_verification(
|
|
|
562
597
|
# par run_verifications avant leur execution locale par devflow.
|
|
563
598
|
commands = regression_commands
|
|
564
599
|
reuse_targeted_as_regression = True
|
|
565
|
-
|
|
600
|
+
document_batch: list[str] | None = None
|
|
601
|
+
if not commands and state.profile == "quick" and not guardrails_active(state):
|
|
602
|
+
document_batch = _observed_document_batch(state, feature_dir, project_root)
|
|
603
|
+
if not commands and document_batch is None:
|
|
566
604
|
return AutomaticVerificationOutcome(
|
|
567
605
|
False,
|
|
568
606
|
"no-safe-validation",
|
|
@@ -661,7 +699,7 @@ def run_automatic_verification(
|
|
|
661
699
|
break
|
|
662
700
|
return AutomaticVerificationOutcome(False, "validation-policy-refused", detail)
|
|
663
701
|
completed_at = _now_iso()
|
|
664
|
-
checks_passed = bool(results) and all(
|
|
702
|
+
checks_passed = (bool(results) or document_batch is not None) and all(
|
|
665
703
|
result.succeeded or not result.blocking for result in results
|
|
666
704
|
)
|
|
667
705
|
regression_passed = all(
|
|
@@ -673,7 +711,9 @@ def run_automatic_verification(
|
|
|
673
711
|
and all(result.succeeded for result in security_results)
|
|
674
712
|
and not security_findings
|
|
675
713
|
)
|
|
676
|
-
diff_payload = _inspect_diff(
|
|
714
|
+
diff_payload = _inspect_diff(
|
|
715
|
+
project_root, document_batch if document_batch is not None else changed_files
|
|
716
|
+
)
|
|
677
717
|
final_snapshot = snapshot_worktree(project_root)
|
|
678
718
|
try:
|
|
679
719
|
test_mutations = changed_paths(before, final_snapshot)
|
|
@@ -883,7 +923,15 @@ def run_automatic_verification(
|
|
|
883
923
|
write_adaptive_state(state_file, latest)
|
|
884
924
|
next_activity = latest.currentActivity
|
|
885
925
|
safe_refresh_worklog(feature_dir)
|
|
886
|
-
|
|
926
|
+
detail = (
|
|
927
|
+
"lot documentaire, aucun controle executable : verification sur "
|
|
928
|
+
"inspection du diff (" + ", ".join(document_batch) + ")"
|
|
929
|
+
if document_batch is not None
|
|
930
|
+
else ""
|
|
931
|
+
)
|
|
932
|
+
return AutomaticVerificationOutcome(
|
|
933
|
+
True, "passed", detail, nextActivity=next_activity
|
|
934
|
+
)
|
|
887
935
|
|
|
888
936
|
|
|
889
937
|
def _failures(results: list[VerificationResult]) -> tuple[VerificationFailure, ...]:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Modele de resultat prerempli pour ``adaptive reconcile --result``.
|
|
2
|
+
|
|
3
|
+
Ecrit hors du depot, dans ``~/.devflow/reconcile/`` (ou ``DEVFLOW_HOME``), quand un
|
|
4
|
+
agent a termine sans rendre sa sortie structuree. Les champs a completer sont
|
|
5
|
+
laisses vides : le modele brut est refuse par les validateurs de ``reconcile``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from devflow_cli.core.telemetry import telemetry_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def result_template_file(issue_id: str, activity: str) -> Path:
|
|
20
|
+
name = re.sub(r"[^A-Za-z0-9_-]", "_", f"{issue_id}-{activity}")
|
|
21
|
+
return telemetry_file().parent / "reconcile" / f"{name}-result.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def result_template_payload(
|
|
25
|
+
contract_id: str, expected_output: dict[str, Any], changed_files: list[str]
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
"""Champs requis du schema : faits connus de la CLI remplis, le reste vide."""
|
|
28
|
+
properties = expected_output.get("properties", {})
|
|
29
|
+
payload: dict[str, Any] = {}
|
|
30
|
+
for name in expected_output.get("required", []):
|
|
31
|
+
schema = properties.get(name, {})
|
|
32
|
+
if name == "contractId":
|
|
33
|
+
payload[name] = contract_id
|
|
34
|
+
elif name == "changedFiles":
|
|
35
|
+
payload[name] = changed_files
|
|
36
|
+
elif "const" in schema:
|
|
37
|
+
payload[name] = schema["const"]
|
|
38
|
+
elif schema.get("type") == "array":
|
|
39
|
+
payload[name] = []
|
|
40
|
+
elif schema.get("type") == "object":
|
|
41
|
+
payload[name] = {}
|
|
42
|
+
else:
|
|
43
|
+
payload[name] = ""
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_result_template(
|
|
48
|
+
issue_id: str,
|
|
49
|
+
activity: str,
|
|
50
|
+
contract_id: str,
|
|
51
|
+
expected_output: dict[str, Any],
|
|
52
|
+
changed_files: list[str],
|
|
53
|
+
) -> Path:
|
|
54
|
+
"""Ecrit le modele (dossier 0700, fichier 0600) et retourne son chemin."""
|
|
55
|
+
path = result_template_file(issue_id, activity)
|
|
56
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
57
|
+
path.parent.chmod(0o700)
|
|
58
|
+
content = json.dumps(
|
|
59
|
+
result_template_payload(contract_id, expected_output, changed_files),
|
|
60
|
+
indent=2,
|
|
61
|
+
ensure_ascii=False,
|
|
62
|
+
) + "\n"
|
|
63
|
+
path.unlink(missing_ok=True)
|
|
64
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
65
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
66
|
+
handle.write(content)
|
|
67
|
+
return path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove_result_template(issue_id: str, activity: str) -> None:
|
|
71
|
+
try:
|
|
72
|
+
result_template_file(issue_id, activity).unlink(missing_ok=True)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
devflow_cli/core/waivers.py
CHANGED
|
@@ -203,10 +203,51 @@ def _find_failed_command(
|
|
|
203
203
|
if not matches:
|
|
204
204
|
raise ValueError(f"aucun echec execute trouve pour {command_id}")
|
|
205
205
|
if len(matches) > 1:
|
|
206
|
-
|
|
206
|
+
return _select_duplicated_failure(command_id, matches)
|
|
207
207
|
return matches[0]
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
_TIE_BREAK_ORDER = {"targeted-tests": 0, "regression-check": 1}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _select_duplicated_failure(
|
|
214
|
+
command_id: str, matches: list[tuple[dict[str, Any], dict[str, Any]]]
|
|
215
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
216
|
+
"""Retient la preuve la plus récente si toutes les occurrences sont cohérentes."""
|
|
217
|
+
names = sorted(str(payload.get("id")) for payload, _ in matches)
|
|
218
|
+
protected = [name for name in names if name in PROTECTED_EVIDENCE]
|
|
219
|
+
if protected:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"validation {command_id} presente dans une preuve protegee "
|
|
222
|
+
f"non derogable : {', '.join(protected)}"
|
|
223
|
+
)
|
|
224
|
+
reference = matches[0][1]
|
|
225
|
+
return_code = reference.get("returncode")
|
|
226
|
+
consistent = isinstance(return_code, int) and return_code != 0
|
|
227
|
+
for _, command in matches:
|
|
228
|
+
consistent = consistent and (
|
|
229
|
+
command.get("command") == reference.get("command")
|
|
230
|
+
and command.get("workingDirectory", ".") == reference.get("workingDirectory", ".")
|
|
231
|
+
and command.get("blocking") is True
|
|
232
|
+
and command.get("succeeded") is False
|
|
233
|
+
and command.get("timedOut") is not True
|
|
234
|
+
and command.get("returncode") == return_code
|
|
235
|
+
)
|
|
236
|
+
if not consistent:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"validation {command_id} presente dans plusieurs preuves "
|
|
239
|
+
f"({', '.join(names)}) avec des commandes, dossiers ou resultats differents"
|
|
240
|
+
)
|
|
241
|
+
ordered = sorted(
|
|
242
|
+
matches,
|
|
243
|
+
key=lambda match: (
|
|
244
|
+
_TIE_BREAK_ORDER.get(str(match[0].get("id")), len(_TIE_BREAK_ORDER)),
|
|
245
|
+
str(match[0].get("id")),
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
return max(ordered, key=lambda match: str(match[0].get("completedAt") or ""))
|
|
249
|
+
|
|
250
|
+
|
|
210
251
|
def _read_registry(path: Path, issue_id: str) -> dict[str, Any]:
|
|
211
252
|
if not path.is_file():
|
|
212
253
|
return {
|
devflow_cli/core/worklog.py
CHANGED
|
@@ -288,6 +288,16 @@ def _verification_lines(
|
|
|
288
288
|
f"`{waiver.get('id', 'inconnue')}` sur `{waiver.get('commandId', 'inconnu')}` "
|
|
289
289
|
f"par {waiver.get('author', 'inconnu')} : {waiver.get('reason', 'sans justification')}"
|
|
290
290
|
)
|
|
291
|
+
if (
|
|
292
|
+
targeted
|
|
293
|
+
and targeted.get("id") == "targeted-check"
|
|
294
|
+
and targeted.get("outcome") == "passed"
|
|
295
|
+
and commands == []
|
|
296
|
+
):
|
|
297
|
+
lines.append(
|
|
298
|
+
" - Lot documentaire, aucun contrôle exécutable : "
|
|
299
|
+
"vérification sur inspection du diff."
|
|
300
|
+
)
|
|
291
301
|
return lines or ["- Vérification non exécutée."]
|
|
292
302
|
|
|
293
303
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devflow-cli
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.3.0
|
|
4
4
|
Summary: Spec-Driven Development workflow CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/sopequenoteck/devflow
|
|
6
6
|
Project-URL: Repository, https://github.com/sopequenoteck/devflow
|
|
@@ -45,7 +45,8 @@ Le profil fixe la route documentaire :
|
|
|
45
45
|
|
|
46
46
|
Un bloc de garde-fous s'y ajoute quand `--data-security` est déclaré ou que
|
|
47
47
|
`--risk high` : `risk-assessment` après l'évaluation, validation bloquante de
|
|
48
|
-
sécurité ou de données dans le plan de validation
|
|
48
|
+
sécurité ou de données dans le plan de validation (une validation protégée non
|
|
49
|
+
bloquante est refusée dès `prepare` ou `plan`), preuve
|
|
49
50
|
`security-or-data-check` produite par `verify`, revue `independent-review`,
|
|
50
51
|
puis `rollback-check` avec `rollback-plan`. Un bloc actif impose au minimum le
|
|
51
52
|
profil `standard`, ne se désactive par aucune option et interdit les
|
|
@@ -90,8 +91,11 @@ devflow adaptive status KS-123
|
|
|
90
91
|
distinguent explicitement blocage métier, contrat invalide, panne agent,
|
|
91
92
|
validation échouée et conflit concurrent ; chaque refus affiche la commande de
|
|
92
93
|
reprise recommandée. Après un refus de `verify`, cette reprise dépend du motif :
|
|
93
|
-
approbations à donner, environnement à corriger, ou
|
|
94
|
-
(`implement` si le code est en cause, `prepare`
|
|
94
|
+
approbations à donner, environnement à corriger, ou trois voies après un
|
|
95
|
+
contrôle échoué (rouvrir `implement` si le code est en cause, rouvrir `prepare`
|
|
96
|
+
ou `plan` si la validation l'est, relancer `verify` après un échec passager).
|
|
97
|
+
En `quick`, un lot purement documentaire (`README.md`, Markdown sous `docs/`)
|
|
98
|
+
sans contrôle détecté franchit `verify` sur l'inspection du diff.
|
|
95
99
|
|
|
96
100
|
Un plan de `prepare` ou de `plan` refusé par les validateurs se corrige sans
|
|
97
101
|
relancer l'agent : `devflow adaptive reconcile KS-123 --result FICHIER.json
|
|
@@ -102,6 +106,16 @@ qu'un agent travaille, les commandes humaines qui modifieraient l'état sont
|
|
|
102
106
|
refusées sans effet, et un fichier d'exécution modifié par l'agent est remis
|
|
103
107
|
dans son état d'avant l'agent.
|
|
104
108
|
|
|
109
|
+
L'agent Claude d'une activité est lancé avec
|
|
110
|
+
`CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` : il peut déléguer à des sous-agents,
|
|
111
|
+
mais au premier plan seulement. Effet de bord : il ne peut plus lancer de
|
|
112
|
+
commande shell en arrière-plan (serveur de test, par exemple). Tous les contrats
|
|
113
|
+
demandent en plus que la sortie structurée soit le dernier message de l'agent.
|
|
114
|
+
Si un agent termine sans erreur mais sans sortie structurée, `adaptive run`
|
|
115
|
+
nomme la cause (`structured-output-missing`) et propose
|
|
116
|
+
`devflow adaptive reconcile KS-123 --result MODELE --reason MOTIF` avec un
|
|
117
|
+
modèle de résultat prérempli, écrit dans `~/.devflow/reconcile/` et à compléter.
|
|
118
|
+
|
|
105
119
|
Chaque invocation de `adaptive run` traite une activité. La politique est dans
|
|
106
120
|
`assessment.json`; la progression et les preuves sont dans
|
|
107
121
|
`workflow-state.json`. Les résultats agent sont validés par schéma et les
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
devflow_cli/__init__.py,sha256=
|
|
1
|
+
devflow_cli/__init__.py,sha256=IpmjnS3nun7PP7DJ0y0oLHgWCL8HVul9hsCpjbAVK4s,925
|
|
2
2
|
devflow_cli/cli.py,sha256=0_SmhJX_k7OQfp-jNV_jq9EOgWY4rTjStGIoc9pH6Ec,2686
|
|
3
3
|
devflow_cli/adapters/__init__.py,sha256=RkjHPU2Lp4hSVIq2iOmyTrv2zYU9NuiIF9wEMWzLdMI,377
|
|
4
|
-
devflow_cli/adapters/agents.py,sha256=
|
|
4
|
+
devflow_cli/adapters/agents.py,sha256=01ZQb96koWMKH-mma3AtttdEjNEbu3mM89vFetD9iR8,29689
|
|
5
5
|
devflow_cli/adapters/linear.py,sha256=HE1YzjBxwoj2jJ_3bmh9Pn_7Mt5u3EOLk891vMXBEj4,8700
|
|
6
6
|
devflow_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
devflow_cli/commands/adaptive.py,sha256=
|
|
8
|
-
devflow_cli/commands/assess.py,sha256=
|
|
7
|
+
devflow_cli/commands/adaptive.py,sha256=OdESwOyoMgySOyyLkAEk3oIkVPXS-bli_SvJe5VeHGU,64445
|
|
8
|
+
devflow_cli/commands/assess.py,sha256=V-E3ft41ljCrqp2WLjR1Dqw-JtswcqsK2nD1-euNbNE,5655
|
|
9
9
|
devflow_cli/commands/check.py,sha256=hsfeOg9yDOTAIvNm6XhRmTIFgv7w9wC8zvrwm4ojfZs,6923
|
|
10
10
|
devflow_cli/commands/context.py,sha256=hwTitOHxIpJMx1X8R1jHHy9CpE4HVZpJKkyeOujxAio,6246
|
|
11
11
|
devflow_cli/commands/docs_sync.py,sha256=Xc0mulpNTjDyeUDumKL7VNG0eumAf3016wEM_dqXpSU,1021
|
|
@@ -24,13 +24,13 @@ devflow_cli/commands/usage.py,sha256=ehH1AH5J-XfkebFx6qLBwpKN-MOoX2uUbq4LejBpNsk
|
|
|
24
24
|
devflow_cli/commands/verify.py,sha256=GBFCfsN-xWyROCcngOANz-3ftNVeNZ4Y_JeWFo2muwE,4139
|
|
25
25
|
devflow_cli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
devflow_cli/core/activity_base.py,sha256=haEVVgZAi3-iX75oo2-fK9GQZeFgN_xRw5U0Z0bdV5w,16343
|
|
27
|
-
devflow_cli/core/activity_contracts.py,sha256=
|
|
28
|
-
devflow_cli/core/adaptive_runner.py,sha256=
|
|
27
|
+
devflow_cli/core/activity_contracts.py,sha256=n9arBw-SWEFqTlCDNed6lV4WolkZKAvmQg1qcfk57ss,39036
|
|
28
|
+
devflow_cli/core/adaptive_runner.py,sha256=GRL4w9FsIPplmypPx1KUnel_byc9PVe7jkTq6Q5Uy0c,41879
|
|
29
29
|
devflow_cli/core/adaptive_workflow.py,sha256=F4uV5okQpFidZmneVZwW52XAO7gAmtuBAqBO9K9q7Uk,12173
|
|
30
30
|
devflow_cli/core/agent_failure.py,sha256=iX4q249lAHeVdD_ZF1aObmU-FX6sjfYP9dfLSnWGAD0,3771
|
|
31
31
|
devflow_cli/core/agent_run_marker.py,sha256=QW3ScA2ZpqfQdUo5ANTbenDVyY2iHpB_wTaN6VUKlzc,3451
|
|
32
32
|
devflow_cli/core/assessment.py,sha256=ACtJbXq1qZjVfeERjy8Qg6-ioLfSMUBVQm2l-mtGwgI,11522
|
|
33
|
-
devflow_cli/core/automatic_evidence.py,sha256=
|
|
33
|
+
devflow_cli/core/automatic_evidence.py,sha256=Ntsy2eQKLRl_hUD27V9NRBLavyhLxxkEcbPySKmd4W4,42337
|
|
34
34
|
devflow_cli/core/catalog.py,sha256=nLPc78JkbISaSxJnHlWiMi_lvepaumbfwcjgLuVy7Zc,3102
|
|
35
35
|
devflow_cli/core/contract_schemas.py,sha256=-7SlJCwQ31OjPfwA-tP4ctgbp9hxQAVwNvvOV09Hoho,6313
|
|
36
36
|
devflow_cli/core/docgen.py,sha256=47JoVDWetkcctgHwJZC67AVebOKgl10VB9hZ3Xr63mo,3908
|
|
@@ -42,6 +42,7 @@ devflow_cli/core/manifest.py,sha256=h058_m5-HJrYsNUdgfwnKts2riHiYPXUwMzPImHOs2g,
|
|
|
42
42
|
devflow_cli/core/operational.py,sha256=11iLTbyZF0ZjYTQ5HDmVCaw4bpbXpLtH2v9f1WQOrYg,1228
|
|
43
43
|
devflow_cli/core/redaction.py,sha256=o_1MDtCyvRLrk_dWlV6_7E6_mPVB1B5a0cCfTBj9j0Q,1973
|
|
44
44
|
devflow_cli/core/remediation.py,sha256=AI5fBs-F8xF3uaxgYJDNJHKxJ7V5obEtnfptMm351AU,8426
|
|
45
|
+
devflow_cli/core/result_template.py,sha256=XfrjUlSUDYJxE2nN8EJjHCXq1l06hn-wW1gDn1c1rBA,2493
|
|
45
46
|
devflow_cli/core/run_progress.py,sha256=hawaMo5sGcxvugexeryMWanFajdf46SY6b37O7dToZM,1855
|
|
46
47
|
devflow_cli/core/runtime_backup.py,sha256=dowz6a1vcJsqkl-m9wGhS4F6XGfsU2rwSNJvIZMJjhI,4345
|
|
47
48
|
devflow_cli/core/scanner.py,sha256=bcoC91Nc0Hw2P7aHpL-9ITxynLZhLTEKpOXo553a-18,4395
|
|
@@ -51,8 +52,8 @@ devflow_cli/core/traceability.py,sha256=jfNE9MZzGkmaPer9Wa6Y18yA9Y3sqdeR-nzH9h_T
|
|
|
51
52
|
devflow_cli/core/validation_approvals.py,sha256=3AHOpnW3BtY3BQrRZr38K7VNYRvbxGNhmQRYAufyhSA,9279
|
|
52
53
|
devflow_cli/core/validators.py,sha256=fREjEx2fBW36q3vB6WJNqoGUM9ejYlQUXie5A9lNuPs,7440
|
|
53
54
|
devflow_cli/core/verification.py,sha256=rvbdAdLyrZYb20oauti7D8nV5TDFBIf_8-hqLahfRag,23576
|
|
54
|
-
devflow_cli/core/waivers.py,sha256=
|
|
55
|
-
devflow_cli/core/worklog.py,sha256=
|
|
55
|
+
devflow_cli/core/waivers.py,sha256=9XXzMmoWCn-2Dc3486WhZwtGJQLQPXjP0qAaLuL_3qU,13299
|
|
56
|
+
devflow_cli/core/worklog.py,sha256=GkaXCsx22jntcWuC75OystUoqb5AlzBbXXLYH581Mwg,14164
|
|
56
57
|
devflow_cli/core/worktree_snapshot.py,sha256=Q2_KTkhJTGvdM6Ay4RK9OHkyPEMrHQRTb-f-Eptek-8,5255
|
|
57
58
|
devflow_cli/schemas/activity-contract-v4.schema.json,sha256=IeduWDiJ5IV6EL7FDZWnHBjzyUt1uwig2rB-SMbcpOU,2006
|
|
58
59
|
devflow_cli/schemas/activity-result-v1.schema.json,sha256=xQuyD5fwHGIdeoJoDJxJn26qBGJX74lD6Dc5gi5CLQo,2772
|
|
@@ -89,8 +90,8 @@ devflow_cli/resources/templates/contracts/api-template.md,sha256=rC5v7JBz71PznCn
|
|
|
89
90
|
devflow_cli/resources/templates/contracts/components-template.md,sha256=XIWfEXfC72sNmL5OA3V-7xIywkNDU2DBPLfyiagOWcs,484
|
|
90
91
|
devflow_cli/resources/templates/contracts/interfaces-template.md,sha256=kf8B7b6HsmN7jyRlLNU7RnZl0rTMlI5s97yVRyI-6yc,336
|
|
91
92
|
devflow_cli/resources/templates/contracts/services-template.md,sha256=cFafKCyLmq_a7aR7G0LXAaMO2z4Fuflq6iKdxfVg0vg,477
|
|
92
|
-
devflow_cli-8.
|
|
93
|
-
devflow_cli-8.
|
|
94
|
-
devflow_cli-8.
|
|
95
|
-
devflow_cli-8.
|
|
96
|
-
devflow_cli-8.
|
|
93
|
+
devflow_cli-8.3.0.dist-info/METADATA,sha256=8ck7BHBw2l01PS43k3pKc2d79l719w4YcK6RjUESOp0,8772
|
|
94
|
+
devflow_cli-8.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
95
|
+
devflow_cli-8.3.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
|
|
96
|
+
devflow_cli-8.3.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
|
|
97
|
+
devflow_cli-8.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|