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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """devflow CLI — workflow de développement adaptatif."""
2
2
 
3
- VERSION = "8.1.0"
3
+ VERSION = "8.3.0"
4
4
 
5
5
  SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
6
6
 
@@ -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=environment_without_linear_key(),
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" : {_SAFE_DIAGNOSTIC_MESSAGES[diagnostic]}"
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
- return json.loads(_strip_json_fence(result))
330
- raise ValueError("champ structured_output absent")
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
 
@@ -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.adapters.agents import get_agent_adapter
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
- return get_agent_adapter(agent).recover_structured_payload(raw_output)
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 outcome.reason == "reconcile-refused":
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 else _verify_recovery(feature_dir, issue_id, outcome)
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 : deux voies,
931
- le code (reopen implement) ou la validation (reopen de l'activite qui l'a inscrite).
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(stale_devflow_evidence(feature_dir, state))
1198
- stale.update(stale_review_evidence(feature_dir, state))
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")
@@ -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
- console.print(f" Suite : devflow adaptive start {assessment.issueId}")
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 ValueError("preuve de pre-vol desuete apres : " + ", ".join(sorted(stale_inputs)))
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
- attempt = json.loads(
341
- activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
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 enter_current_activity, parse_recorded_result
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
- if not commands:
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(project_root, changed_files)
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
- return AutomaticVerificationOutcome(True, "passed", nextActivity=next_activity)
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
@@ -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
- raise ValueError(f"validation ambigue dans les preuves : {command_id}")
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 {
@@ -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.1.0
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, preuve
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 deux voies de réouverture
94
- (`implement` si le code est en cause, `prepare` ou `plan` si la validation l'est).
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=PUspBocDYmXBT3YAsO8WxkWncuo5lyBpxQRxMyBY1mY,925
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=lq3OjcaT3Nt9-WV9Po4Dj1fag4getb7DEt9Y3Zp1js4,26683
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=xfvjQOlBrUjZvf8NLLlaohyeUT54LhjdDCptMr_s9w8,56390
8
- devflow_cli/commands/assess.py,sha256=lyj6TI5hXUWX55dY589fG4bKJSMztvMPxZ0HZjo0O-w,5230
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=nbPj9dkCIG7HXrT1iwZ1m3oaUZY-V65bfC62ALuYAus,38212
28
- devflow_cli/core/adaptive_runner.py,sha256=MLws62PW986dd00nA4m_3Y4BAR-ZmmYT3q8N6KiPU-Q,40375
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=y1f6L3tsJpV57I54zcTySetho1ASHklu5mUQF1PaRDY,40600
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=xpfSiC4K51ZVlBOZTRNJ5ftS05-bsLyMW-W9CdQHqH4,11595
55
- devflow_cli/core/worklog.py,sha256=ECmGJL63eVwfKQl94-r-Zd56ubhApuufAqDq1h5xcAU,13854
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.1.0.dist-info/METADATA,sha256=anR27a2QLBDXONlC1oDcaDdTlxzx12qoqEmCBOxCouk,7809
93
- devflow_cli-8.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
94
- devflow_cli-8.1.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
95
- devflow_cli-8.1.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
96
- devflow_cli-8.1.0.dist-info/RECORD,,
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,,