devflow-cli 8.3.0__py3-none-any.whl → 8.5.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.3.0"
3
+ VERSION = "8.5.0"
4
4
 
5
5
  SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
6
6
 
@@ -27,9 +27,11 @@ from devflow_cli.core.adaptive_workflow import (
27
27
  from devflow_cli.core.assessment import read_assessment
28
28
  from devflow_cli.core.activity_base import (
29
29
  enter_current_activity,
30
+ feature_path_for,
30
31
  files_since_activity_base,
31
32
  )
32
33
  from devflow_cli.core.activity_contracts import (
34
+ DOCUMENT_ARTIFACTS,
33
35
  build_activity_contract,
34
36
  NEED_STATEMENT,
35
37
  render_contract_prompt,
@@ -39,6 +41,7 @@ from devflow_cli.core.activity_contracts import (
39
41
  )
40
42
  from devflow_cli.core.adaptive_runner import (
41
43
  MANUAL_RECONCILIATION_ACTIVITIES,
44
+ AdaptiveRunOutcome,
42
45
  AgentActivityExecution,
43
46
  reconcile_current_activity,
44
47
  record_human_review,
@@ -54,7 +57,11 @@ from devflow_cli.core.automatic_evidence import (
54
57
  stale_devflow_evidence,
55
58
  stale_review_evidence,
56
59
  )
57
- from devflow_cli.core.worklog import refresh_worklog, safe_refresh_worklog
60
+ from devflow_cli.core.worklog import (
61
+ latest_remaining_risks,
62
+ refresh_worklog,
63
+ safe_refresh_worklog,
64
+ )
58
65
  from devflow_cli.core.telemetry import append_record, telemetry_enabled, usage_payload
59
66
  from devflow_cli.core.waivers import create_validation_waiver
60
67
  from devflow_cli.core.remediation import (
@@ -519,6 +526,8 @@ def run_adaptive(
519
526
  )
520
527
  if outcome.reason == "preflight-evidence-stale":
521
528
  recovery = _stale_preflight_recovery(current_state)
529
+ if outcome.reason == "artifact-files-mismatch":
530
+ recovery = _artifact_mismatch_recovery(current_state, outcome, agent)
522
531
  if json_output:
523
532
  _print_execution_json(
524
533
  current_state.issueId, outcome.activity, False, outcome.reason,
@@ -700,6 +709,13 @@ def reconcile_adaptive(
700
709
  console.print(" Suite : attendez la fin de l'activite en cours, puis relancez.")
701
710
  raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
702
711
  console.print(f" Suite : devflow adaptive status {state.issueId}")
712
+ if outcome.reason == "artifact-files-mismatch":
713
+ console.print(" Reprise guidee :")
714
+ _print_recovery_steps(
715
+ _artifact_mismatch_recovery(
716
+ state, outcome, _attempt_agent(feature_dir, outcome.activity)
717
+ )
718
+ )
703
719
  if (
704
720
  outcome.reason == "reconcile-refused"
705
721
  and activity_attempt_file(feature_dir, outcome.activity).is_file()
@@ -743,8 +759,8 @@ def review_adaptive(
743
759
  )
744
760
  if result.advanced:
745
761
  ok(
746
- f"Revue humaine de {by.strip()} validee pour '{result.activity}'. "
747
- f"Prochaine : {result.nextActivity}"
762
+ f"Revue humaine de {by.strip()} validee pour '{result.activity}' : "
763
+ f"activite completee. Prochaine : {result.nextActivity}"
748
764
  )
749
765
  return
750
766
  safe_refresh_worklog(feature_dir)
@@ -916,21 +932,72 @@ def discard_attempt_command(
916
932
  fail(str(exc))
917
933
  console.print(f" Suite : devflow adaptive status {state.issueId}")
918
934
  raise typer.Exit(int(ExitCode.BLOCKED)) from exc
935
+ remove_result_template(state.issueId, entry["activity"])
919
936
  safe_refresh_worklog(feature_dir)
920
937
  ok(
921
938
  f"Tentative '{entry['activity']}' abandonnee (audit #{entry['sequence']}, "
922
939
  f"archive : {entry['archivePath']})."
923
940
  )
924
- if entry["discardedFiles"]:
941
+ discarded = list(entry["discardedFiles"])
942
+ feature_path = feature_path_for(project_root_for_feature(feature_dir), feature_dir)
943
+ imputed = _imputed_inputs_of_discarded_attempt(feature_dir, entry, feature_path)
944
+ inputs = (
945
+ [
946
+ path for path in discarded
947
+ if _is_need_input(path, feature_path) and path in imputed
948
+ ]
949
+ if entry["activity"] in DOCUMENT_ARTIFACTS
950
+ else []
951
+ )
952
+ others = [path for path in discarded if path not in inputs]
953
+ if inputs:
954
+ warn(
955
+ "Entrees du besoin modifiees depuis le debut de l'activite "
956
+ f"'{entry['activity']}' : " + ", ".join(inputs) + " ; elles ne sont plus "
957
+ "imputees a un agent apres cet abandon et seront acceptees comme amendees "
958
+ "a la relance : a verifier"
959
+ )
960
+ if others:
925
961
  warn(
926
962
  "Fichiers modifies depuis le debut de l'activite "
927
963
  f"'{entry['activity']}', comptes dans le lot a la relance ; remise en "
928
- "etat a la charge du superviseur : " + ", ".join(entry["discardedFiles"])
964
+ "etat a la charge du superviseur : " + ", ".join(others)
929
965
  )
930
966
  if entry.get("observationError"):
931
967
  warn(f"Fichiers depuis la base non calcules : {entry['observationError']}")
932
968
 
933
969
 
970
+ def _imputed_inputs_of_discarded_attempt(
971
+ feature_dir: Path, entry: dict, feature_path: str | None
972
+ ) -> set[str]:
973
+ """Entrees du besoin que la fiche abandonnee imputait a un agent (lecture seule)."""
974
+ if feature_path is None:
975
+ return set()
976
+ card = (
977
+ feature_dir
978
+ / entry["archivePath"]
979
+ / activity_attempt_file(feature_dir, entry["activity"]).name
980
+ )
981
+ try:
982
+ payload = json.loads(card.read_text(encoding="utf-8"))
983
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
984
+ return set()
985
+ names = payload.get("changedInputs") if isinstance(payload, dict) else None
986
+ if not isinstance(names, list):
987
+ return set()
988
+ return {f"{feature_path}/{name}" for name in names if isinstance(name, str)}
989
+
990
+
991
+ def _is_need_input(path: str, feature_path: str | None) -> bool:
992
+ """Vrai pour need.md ou issue-*.md directement sous le dossier de la feature."""
993
+ if feature_path is None:
994
+ return False
995
+ parent, _, name = path.rpartition("/")
996
+ return parent == feature_path and (
997
+ name == NEED_STATEMENT or (name.startswith("issue-") and name.endswith(".md"))
998
+ )
999
+
1000
+
934
1001
  def _execute_local_verification(
935
1002
  feature_dir: Path, project_root: Path, issue_id: str, *,
936
1003
  refresh: bool = False, json_output: bool = False,
@@ -1264,7 +1331,7 @@ def _operational_status(
1264
1331
  ) -> dict:
1265
1332
  result_file, result = _latest_activity_result(feature_dir, state.currentActivity)
1266
1333
  blockers = result.get("blockers", []) if isinstance(result, dict) else []
1267
- risks = _remaining_risks(feature_dir)
1334
+ risks = latest_remaining_risks(feature_dir, state.activities)
1268
1335
  if not isinstance(blockers, list):
1269
1336
  blockers = []
1270
1337
  stale: set[str] = set()
@@ -1415,28 +1482,6 @@ def _latest_activity_result(
1415
1482
  return None, {}
1416
1483
 
1417
1484
 
1418
- def _remaining_risks(feature_dir: Path) -> list[str]:
1419
- risks: list[str] = []
1420
- for path in sorted(feature_dir.glob("*-result.json")):
1421
- if not path.is_file():
1422
- continue
1423
- try:
1424
- payload = json.loads(path.read_text(encoding="utf-8"))
1425
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
1426
- continue
1427
- values = (
1428
- payload.get("remainingRisks", [])
1429
- if isinstance(payload, dict)
1430
- else []
1431
- )
1432
- if not isinstance(values, list):
1433
- continue
1434
- for value in values:
1435
- if isinstance(value, str) and value not in risks:
1436
- risks.append(value)
1437
- return risks
1438
-
1439
-
1440
1485
  def _fallback_agents(agent: str) -> list[str]:
1441
1486
  """Autres agents pris en charge, pour reprendre pendant une indisponibilite."""
1442
1487
  current = get_agent_adapter(agent).name
@@ -1557,21 +1602,154 @@ def _stale_preflight_recovery(state: AdaptiveWorkflowState) -> dict:
1557
1602
  }
1558
1603
 
1559
1604
 
1605
+ ARTIFACT_FILE_CAUSES = {
1606
+ "during-agent": "modifie pendant l'agent",
1607
+ "before-launch": "modifie avant le lancement de l'agent",
1608
+ "agent-input": "entree du besoin modifiee par un agent de cette activite",
1609
+ }
1610
+
1611
+
1612
+ def _artifact_mismatch_recovery(
1613
+ state: AdaptiveWorkflowState, outcome: AdaptiveRunOutcome, agent: str
1614
+ ) -> dict:
1615
+ """Reprise guidee d'un refus d'artefact, par cause observee de chaque fichier en trop."""
1616
+ issue = shlex.quote(state.issueId)
1617
+ files = [
1618
+ {"path": path, "cause": cause, "label": ARTIFACT_FILE_CAUSES[cause]}
1619
+ for path, cause in outcome.extraFiles
1620
+ ]
1621
+ causes = {cause for _, cause in outcome.extraFiles}
1622
+ if not files:
1623
+ return {
1624
+ "kind": "artifact-files-mismatch",
1625
+ "summary": (
1626
+ "Artefact attendu absent ou inchange depuis la base, sans fichier en "
1627
+ "trop : l'agent doit l'ecrire."
1628
+ ),
1629
+ "files": files,
1630
+ "commands": [{
1631
+ "command": f"devflow adaptive run {issue}"
1632
+ + (f" --agent {shlex.quote(agent)}" if agent else ""),
1633
+ "purpose": "relancer l'agent pour qu'il ecrive l'artefact",
1634
+ }],
1635
+ }
1636
+ if causes == {"agent-input"}:
1637
+ # Refus avant l'agent : ni reconcile ni reopen ne leve l'imputation.
1638
+ run = f"devflow adaptive run {issue}" + (
1639
+ f" --agent {shlex.quote(agent)}" if agent else ""
1640
+ )
1641
+ return {
1642
+ "kind": "artifact-files-mismatch",
1643
+ "summary": (
1644
+ "Entree du besoin modifiee par un agent de cette activite et pas remise "
1645
+ "en etat : l'agent n'a pas ete lance. Deux sorties : remettre l'entree "
1646
+ "dans son etat de base, ou abandonner la tentative (l'entree modifiee "
1647
+ "est gardee, a verifier)."
1648
+ ),
1649
+ "files": files,
1650
+ "commands": [
1651
+ {
1652
+ "command": run,
1653
+ "purpose": "apres remise en etat de l'entree dans son etat de base",
1654
+ },
1655
+ {
1656
+ "command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
1657
+ "purpose": (
1658
+ "sinon, garder l'entree modifiee : abandon audite de la "
1659
+ "tentative, puis devflow adaptive run"
1660
+ ),
1661
+ },
1662
+ ],
1663
+ }
1664
+ commands = [{
1665
+ "command": f"devflow adaptive reconcile {issue}",
1666
+ "purpose": (
1667
+ "une fois chaque fichier en trop retire ou remis dans son etat de base : "
1668
+ "valider l'artefact sans relancer l'agent"
1669
+ ),
1670
+ }]
1671
+ index = state.activities.index(state.currentActivity)
1672
+ previous = state.activities[index - 1] if index > 0 else ""
1673
+ if (
1674
+ "before-launch" in causes
1675
+ and "agent-input" not in causes
1676
+ and previous not in {"", "assess"}
1677
+ and _reopenable(state, previous)
1678
+ ):
1679
+ commands.append({
1680
+ "command": f"devflow adaptive reopen {issue} {previous} --reason MOTIF",
1681
+ "purpose": (
1682
+ "sinon, garder le fichier modifie avant le lancement : rouvrir "
1683
+ "l'activite precedente pour repartir d'une base fraiche"
1684
+ ),
1685
+ })
1686
+ if "agent-input" in causes:
1687
+ commands.append({
1688
+ "command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
1689
+ "purpose": (
1690
+ "sinon, garder l'entree du besoin modifiee par l'agent : abandon "
1691
+ "audite de la tentative, puis devflow adaptive run"
1692
+ ),
1693
+ })
1694
+ return {
1695
+ "kind": "artifact-files-mismatch",
1696
+ "summary": (
1697
+ "Fichiers en trop pour une activite documentaire : retirer ceux ecrits "
1698
+ "pendant l'agent, remettre dans leur etat de base ceux modifies avant son "
1699
+ "lancement. Relancer l'agent a l'identique donnerait le meme refus."
1700
+ ),
1701
+ "files": files,
1702
+ "commands": commands,
1703
+ }
1704
+
1705
+
1706
+ def _attempt_agent(feature_dir: Path, activity: str) -> str:
1707
+ """Agent de la fiche de tentative, ou '' si elle est illisible."""
1708
+ try:
1709
+ attempt = json.loads(
1710
+ activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
1711
+ )
1712
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
1713
+ return ""
1714
+ agent = attempt.get("agent") if isinstance(attempt, dict) else None
1715
+ return agent if isinstance(agent, str) else ""
1716
+
1717
+
1560
1718
  def _print_recovery_steps(recovery: dict) -> None:
1561
1719
  # soft_wrap : la commande a copier reste sur une seule ligne.
1562
1720
  console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
1721
+ for item in recovery.get("files", []):
1722
+ console.print(
1723
+ f" {item['path']} : {item['label']}", markup=False, soft_wrap=True
1724
+ )
1563
1725
  for step in recovery["commands"]:
1564
1726
  console.print(
1565
1727
  f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
1566
1728
  )
1567
1729
 
1568
1730
 
1731
+ # Noms de `run --agent` traduits en noms que `devflow check --ai` accepte.
1732
+ CHECK_AGENT_NAMES = {"claude": "claude-code", "claude-code": "claude-code", "codex": "codex"}
1733
+
1734
+
1735
+ def _print_check_command(agent: str, comment: str) -> None:
1736
+ """Propose `devflow check --ai` avec un nom que cette commande accepte."""
1737
+ name = CHECK_AGENT_NAMES.get(agent.casefold())
1738
+ if name is not None:
1739
+ console.print(f" devflow check --ai {name} --live{comment}")
1740
+
1741
+
1569
1742
  def _print_run_recovery(
1570
1743
  issue_id: str, reason: str, activity: str, agent: str, feature_dir: Path,
1571
1744
  *, recovery: dict | None = None,
1572
1745
  ) -> None:
1573
1746
  console.print(" Reprise guidee :")
1574
- console.print(f" devflow adaptive status {issue_id}")
1747
+ status_command = f"devflow adaptive status {issue_id}"
1748
+ status_commands = {status_command, f"devflow adaptive status {shlex.quote(issue_id)}"}
1749
+ if recovery is None or not any(
1750
+ step["command"] in status_commands for step in recovery["commands"]
1751
+ ):
1752
+ console.print(f" {status_command}")
1575
1753
  if reason in {"agent-failed", "agent-failed-after-mutation", "agent-unavailable"}:
1576
1754
  console.print(
1577
1755
  " Trace agent : "
@@ -1584,7 +1762,7 @@ def _print_run_recovery(
1584
1762
  return
1585
1763
  if reason == "agent-unavailable":
1586
1764
  console.print(" Agent indisponible : l'activite reste courante et rien n'est invalide.")
1587
- console.print(f" devflow check --ai {agent} --live # verifier le retour de l'agent")
1765
+ _print_check_command(agent, " # verifier le retour de l'agent")
1588
1766
  console.print(f" devflow adaptive reconcile {issue_id} # si l'agent avait deja produit son travail")
1589
1767
  for fallback in _fallback_agents(agent):
1590
1768
  console.print(f" devflow adaptive run {issue_id} --agent {fallback}")
@@ -1599,6 +1777,24 @@ def _print_run_recovery(
1599
1777
  " Les entrees du besoin ont change pendant l'execution. "
1600
1778
  "Verifiez-les avant une nouvelle tentative."
1601
1779
  )
1780
+ if activity in DOCUMENT_ARTIFACTS:
1781
+ console.print(
1782
+ " Activite documentaire : la relance reste refusee tant que l'entree "
1783
+ "n'est pas remise dans son etat de base, ou la tentative abandonnee :",
1784
+ markup=False, soft_wrap=True,
1785
+ )
1786
+ issue, agent_arg = shlex.quote(issue_id), shlex.quote(agent)
1787
+ console.print(
1788
+ f" devflow adaptive discard-attempt {issue} --reason MOTIF"
1789
+ " # garder l'entree modifiee : abandon audite",
1790
+ markup=False, soft_wrap=True,
1791
+ )
1792
+ console.print(
1793
+ f" devflow adaptive run {issue} --agent {agent_arg}"
1794
+ " # apres remise en etat de l'entree, ou apres l'abandon",
1795
+ markup=False, soft_wrap=True,
1796
+ )
1797
+ return
1602
1798
  console.print(f" devflow adaptive run {issue_id} --agent {agent}")
1603
1799
  return
1604
1800
  if reason == "agent-running":
@@ -1611,7 +1807,7 @@ def _print_run_recovery(
1611
1807
  )
1612
1808
  if reason == "agent-failed":
1613
1809
  console.print(f" devflow adaptive reconcile {issue_id} # si la trace contient une reponse finale")
1614
- console.print(f" devflow check --ai {agent}")
1810
+ _print_check_command(agent, " # verifier l'agent")
1615
1811
  elif reason == "state-changed":
1616
1812
  console.print(" Rechargez le statut avant toute nouvelle execution.")
1617
1813
  return
@@ -369,17 +369,29 @@ def declaration_warning(declared: list[str], observed: list[str]) -> str:
369
369
 
370
370
 
371
371
  def recorded_observation(
372
- observation: ActivityObservation, declared: list[str]
372
+ observation: ActivityObservation,
373
+ declared: list[str],
374
+ *,
375
+ exempted: Iterable[str] = (),
376
+ amendment_warning: str = "",
373
377
  ) -> dict[str, Any]:
374
- """Bloc ``observation`` ecrit par la CLI seule dans le resultat enregistre."""
375
- observed = sorted(observation.sinceBase)
378
+ """Bloc ``observation`` ecrit par la CLI seule dans le resultat enregistre.
379
+
380
+ ``exempted`` : entrees du besoin amendees avant l'agent, hors ecart de
381
+ declaration ; ``amendment_warning`` les nomme dans la cle ``warning``.
382
+ """
383
+ observed = sorted(set(observation.sinceBase) - set(exempted))
376
384
  block: dict[str, Any] = {
377
385
  "baseRecordedAt": observation.base.recordedAt,
378
386
  "baseHead": observation.base.head,
379
387
  "observedAt": observation.observedAt,
380
388
  "head": observation.head,
381
389
  }
382
- warning = declaration_warning(declared, observed)
390
+ warning = " ; ".join(
391
+ message
392
+ for message in (declaration_warning(declared, observed), amendment_warning)
393
+ if message
394
+ )
383
395
  if warning:
384
396
  block["declaredChangedFiles"] = list(declared)
385
397
  block["warning"] = warning
@@ -555,8 +555,8 @@ def parse_activity_result(payload: dict[str, Any], contract: ActivityContract) -
555
555
  for removed in REMOVED_RESULT_FIELDS:
556
556
  if removed in payload:
557
557
  raise ValueError(
558
- f"champ {removed} supprime en devflow 7.0 ; mettre a jour les consignes "
559
- "avec devflow upgrade"
558
+ f"champ {removed} supprime en devflow 7.0 ; relancer l'activite, qui "
559
+ "produit un nouveau contrat sans ce champ"
560
560
  )
561
561
  expected_fields = {
562
562
  "schemaVersion",
@@ -847,6 +847,13 @@ def _parse_checks(value: Any) -> list[ActivityCheck]:
847
847
  return checks
848
848
 
849
849
 
850
+ _EVIDENCE_SHAPE_MESSAGE = (
851
+ "preuve invalide : chaque element de evidence doit etre un objet aux cles "
852
+ "id (texte non vide, unique), outcome (passed ou failed) et report (texte non vide), "
853
+ "sans autre cle"
854
+ )
855
+
856
+
850
857
  def _parse_evidence(value: Any) -> list[ActivityEvidence]:
851
858
  if not isinstance(value, list):
852
859
  raise ValueError("evidence doit etre une liste")
@@ -854,9 +861,9 @@ def _parse_evidence(value: Any) -> list[ActivityEvidence]:
854
861
  seen: set[str] = set()
855
862
  for item in value:
856
863
  if not isinstance(item, dict):
857
- raise ValueError("preuve invalide")
864
+ raise ValueError(_EVIDENCE_SHAPE_MESSAGE)
858
865
  if set(item) != {"id", "outcome", "report"}:
859
- raise ValueError("preuve invalide")
866
+ raise ValueError(_EVIDENCE_SHAPE_MESSAGE)
860
867
  evidence_id, outcome, report = item.get("id"), item.get("outcome"), item.get("report")
861
868
  if (
862
869
  not isinstance(evidence_id, str)
@@ -866,7 +873,7 @@ def _parse_evidence(value: Any) -> list[ActivityEvidence]:
866
873
  or not isinstance(report, str)
867
874
  or not report.strip()
868
875
  ):
869
- raise ValueError("preuve invalide")
876
+ raise ValueError(_EVIDENCE_SHAPE_MESSAGE)
870
877
  seen.add(evidence_id)
871
878
  evidence.append(ActivityEvidence(evidence_id, outcome, report.strip()))
872
879
  return evidence
@@ -12,7 +12,7 @@ from contextlib import ExitStack
12
12
  from dataclasses import dataclass, replace
13
13
  from datetime import datetime, timezone
14
14
  from pathlib import Path
15
- from typing import Callable
15
+ from typing import Callable, Iterable
16
16
 
17
17
  from devflow_cli.core.activity_contracts import (
18
18
  ActivityContract,
@@ -51,6 +51,7 @@ from devflow_cli.core.automatic_evidence import (
51
51
  review_worktree_fingerprint,
52
52
  )
53
53
  from devflow_cli.core.adaptive_workflow import (
54
+ AdaptiveWorkflowState,
54
55
  complete_activity,
55
56
  read_adaptive_state,
56
57
  record_evidence,
@@ -92,6 +93,8 @@ class AdaptiveRunOutcome:
92
93
  detail: str = ""
93
94
  result: ActivityResult | None = None
94
95
  warnings: tuple[str, ...] = ()
96
+ # artifact-files-mismatch : (chemin, cause observee) de chaque fichier en trop.
97
+ extraFiles: tuple[tuple[str, str], ...] = ()
95
98
 
96
99
  @property
97
100
  def advanced(self) -> bool:
@@ -144,6 +147,11 @@ def run_current_activity(
144
147
  preflight_evidence=(feature_dir / "preflight-evidence.md").is_file(),
145
148
  files_since_base=since_base,
146
149
  )
150
+ imputed_inputs = (
151
+ _imputed_inputs(feature_dir, feature_path, contract.activity, since_base)
152
+ if contract.activity in DOCUMENT_ARTIFACTS
153
+ else []
154
+ )
147
155
  if state.currentActivity == "implement":
148
156
  upstream = [
149
157
  feature_dir / Path(item).name
@@ -168,6 +176,18 @@ def run_current_activity(
168
176
  return AdaptiveRunOutcome(
169
177
  "unsupported-activity", state.currentActivity, state.currentActivity, str(exc)
170
178
  )
179
+ if imputed_inputs:
180
+ # N5 : refus avant toute ecriture et avant l'agent ; la fiche reste intacte.
181
+ return AdaptiveRunOutcome(
182
+ "artifact-files-mismatch",
183
+ contract.activity,
184
+ contract.activity,
185
+ "entrees du besoin modifiees par un agent de cette activite et pas "
186
+ "remises en etat : " + ", ".join(imputed_inputs) + " ; agent non lance",
187
+ extraFiles=tuple(
188
+ (f"{feature_path}/{name}", "agent-input") for name in imputed_inputs
189
+ ),
190
+ )
171
191
  raw_file = activity_raw_file(feature_dir, contract.activity)
172
192
  guard = ExitStack()
173
193
  try:
@@ -212,8 +232,19 @@ def run_current_activity(
212
232
  restored = backup.restore() if runtime_mutated else []
213
233
  guard.close()
214
234
  _write_text_atomic(raw_file, execution.output)
235
+ current_inputs = _input_fingerprints(feature_dir)
236
+ changed_inputs = sorted(
237
+ name
238
+ for name in set(input_fingerprints) | set(current_inputs)
239
+ if input_fingerprints.get(name) != current_inputs.get(name)
240
+ )
241
+ # N5 : une entree modifiee par un agent reste imputee de fiche en fiche.
242
+ attributed_inputs = sorted({*changed_inputs, *imputed_inputs})
215
243
  if runtime_mutated:
216
- _record_failed_attempt(feature_dir, contract, execution, "runtime-file-mutated")
244
+ _record_failed_attempt(
245
+ feature_dir, contract, execution, "runtime-file-mutated",
246
+ changed_inputs=attributed_inputs,
247
+ )
217
248
  return AdaptiveRunOutcome(
218
249
  "runtime-file-mutated",
219
250
  contract.activity,
@@ -223,16 +254,10 @@ def run_current_activity(
223
254
  + " ; remis dans leur etat d'avant l'agent : "
224
255
  + (", ".join(restored) if restored else "aucun fichier a restaurer"),
225
256
  )
226
- current_inputs = _input_fingerprints(feature_dir)
227
- changed_inputs = sorted(
228
- name
229
- for name in set(input_fingerprints) | set(current_inputs)
230
- if input_fingerprints.get(name) != current_inputs.get(name)
231
- )
232
257
  if changed_inputs:
233
258
  _record_failed_attempt(
234
259
  feature_dir, contract, execution,
235
- "inputs-changed-during-activity", changed_inputs=changed_inputs,
260
+ "inputs-changed-during-activity", changed_inputs=attributed_inputs,
236
261
  )
237
262
  return AdaptiveRunOutcome(
238
263
  "inputs-changed-during-activity", contract.activity, contract.activity,
@@ -252,7 +277,9 @@ def run_current_activity(
252
277
  reason = "agent-failed"
253
278
  if execution.observedChangedFiles:
254
279
  detail += f"; mutations observees : {len(set(execution.observedChangedFiles))} fichier(s)"
255
- _record_failed_attempt(feature_dir, contract, execution, reason)
280
+ _record_failed_attempt(
281
+ feature_dir, contract, execution, reason, changed_inputs=attributed_inputs
282
+ )
256
283
  return AdaptiveRunOutcome(reason, contract.activity, contract.activity, detail)
257
284
 
258
285
  try:
@@ -261,7 +288,10 @@ def run_current_activity(
261
288
  outcome = AdaptiveRunOutcome(
262
289
  "invalid-result", contract.activity, contract.activity, str(exc)
263
290
  )
264
- _record_failed_attempt(feature_dir, contract, execution, outcome.reason)
291
+ _record_failed_attempt(
292
+ feature_dir, contract, execution, outcome.reason,
293
+ changed_inputs=attributed_inputs,
294
+ )
265
295
  return outcome
266
296
  try:
267
297
  observation = observe_activity(
@@ -282,9 +312,15 @@ def run_current_activity(
282
312
  observation,
283
313
  source="agent",
284
314
  agent=execution.agent,
315
+ execution_files=execution.observedChangedFiles,
316
+ imputed_inputs=imputed_inputs,
317
+ closed_activities=_closed_activities(state),
285
318
  )
286
319
  if not outcome.advanced and outcome.reason not in {"blocked", "state-changed"}:
287
- _record_failed_attempt(feature_dir, contract, execution, outcome.reason)
320
+ _record_failed_attempt(
321
+ feature_dir, contract, execution, outcome.reason,
322
+ changed_inputs=attributed_inputs,
323
+ )
288
324
  return outcome
289
325
 
290
326
 
@@ -364,6 +400,20 @@ def reconcile_current_activity(
364
400
  attempt_reason = attempt["reason"]
365
401
  observed_files = attempt["observedFiles"]
366
402
  execution_files = attempt["executionFiles"]
403
+ imputed_inputs = sorted(
404
+ name
405
+ for name in attempt.get("changedInputs", [])
406
+ if f"{feature_path}/{name}" in since_base
407
+ )
408
+ if imputed_inputs:
409
+ # N5 : reconcile ne blanchit pas une entree imputee a un agent.
410
+ raise ValueError(
411
+ "entrees du besoin modifiees par un agent de cette activite et pas "
412
+ "remises en etat : " + ", ".join(imputed_inputs) + " ; les remettre "
413
+ "dans leur etat de base, ou abandonner la tentative (devflow adaptive "
414
+ f"discard-attempt {shlex.quote(state.issueId)} --reason MOTIF), puis "
415
+ "relancez l'activite"
416
+ )
367
417
  if attempt_reason == "inputs-changed-during-activity":
368
418
  raise ValueError(
369
419
  "les entrees ont change pendant la tentative ; inspectez-les "
@@ -383,10 +433,18 @@ def reconcile_current_activity(
383
433
  )
384
434
  if head != attempt["head"] or branch != attempt["branch"]:
385
435
  raise ValueError("HEAD ou la branche a change depuis la tentative")
436
+ # N6 : pour une activite documentaire, un fichier de la fiche autre que
437
+ # l'artefact peut etre revenu a son etat de base (fichier en trop retire).
438
+ artifact = (
439
+ f"{feature_path}/{DOCUMENT_ARTIFACTS[activity]}"
440
+ if activity in DOCUMENT_ARTIFACTS
441
+ else None
442
+ )
386
443
  drifted = sorted(
387
444
  path
388
445
  for path in set(observed_files) | set(since_base)
389
446
  if observed_files.get(path) != since_base.get(path)
447
+ and (artifact is None or path == artifact or path in since_base)
390
448
  )
391
449
  if drifted:
392
450
  raise ValueError(
@@ -467,6 +525,8 @@ def reconcile_current_activity(
467
525
  observation,
468
526
  source="reconciled",
469
527
  agent=agent,
528
+ execution_files=execution_files,
529
+ closed_activities=_closed_activities(state),
470
530
  )
471
531
  if orphan_warning:
472
532
  outcome = replace(outcome, warnings=(orphan_warning, *outcome.warnings))
@@ -546,12 +606,17 @@ def _apply_activity_result(
546
606
  *,
547
607
  source: str,
548
608
  agent: str,
609
+ execution_files: Iterable[str] = (),
610
+ imputed_inputs: Iterable[str] = (),
611
+ closed_activities: Iterable[str] = (),
549
612
  ) -> AdaptiveRunOutcome:
550
613
  """Valide un resultat agent puis transitionne ; commun a run et reconcile.
551
614
 
552
615
  L'observation fait foi : ``O`` (fichiers depuis la base) et ``E`` (fichiers
553
616
  modifies pendant l'agent) fondent les refus ; la declaration de l'agent ne
554
- produit qu'un avertissement.
617
+ produit qu'un avertissement. ``execution_files`` est la liste brute des fichiers
618
+ modifies pendant l'agent, ``imputed_inputs`` les entrees du besoin deja imputees
619
+ a un agent de l'activite (N5) : ils fondent l'exemption N1.
555
620
  """
556
621
  state_file = feature_dir / "workflow-state.json"
557
622
  observed = sorted(observation.sinceBase)
@@ -576,13 +641,35 @@ def _apply_activity_result(
576
641
  "invalid-result", contract.activity, contract.activity, str(exc)
577
642
  )
578
643
 
644
+ executed = set(execution_files)
645
+ imputed_paths = {f"{feature_path}/{name}" for name in imputed_inputs}
646
+ # N1 : entrees du besoin amendees avant le lancement de l'agent, non comptees.
647
+ amended = (
648
+ [
649
+ path
650
+ for path in _input_paths(feature_dir, feature_path)
651
+ if path in observation.sinceBase
652
+ and path not in executed
653
+ and path not in imputed_paths
654
+ ]
655
+ if contract.activity in DOCUMENT_ARTIFACTS
656
+ else []
657
+ )
658
+ counted = [path for path in observed if path not in amended]
579
659
  declared = sorted(set(result.changedFiles))
580
- warning = declaration_warning(declared, observed)
581
- warnings = (warning,) if warning else ()
660
+ amendment = _amendment_warning(contract.issueId, amended, closed_activities)
661
+ warnings = tuple(
662
+ message
663
+ for message in (declaration_warning(declared, counted), amendment)
664
+ if message
665
+ )
582
666
 
583
- def refuse(reason: str, detail: str) -> AdaptiveRunOutcome:
667
+ def refuse(
668
+ reason: str, detail: str, extra_files: tuple[tuple[str, str], ...] = ()
669
+ ) -> AdaptiveRunOutcome:
584
670
  return AdaptiveRunOutcome(
585
- reason, contract.activity, contract.activity, detail, result, warnings
671
+ reason, contract.activity, contract.activity, detail, result, warnings,
672
+ extra_files,
586
673
  )
587
674
 
588
675
  if contract.activity == "implement" and result.status == "completed" and not observed:
@@ -594,11 +681,22 @@ def _apply_activity_result(
594
681
  contract.activity, reopened=contract_is_reopened_plan(contract)
595
682
  )
596
683
  expected = f"{feature_path}/{artifact_name}"
597
- if any(path != expected for path in observed) or (
598
- result.status == "completed" and observed != [expected]
684
+ if any(path != expected for path in counted) or (
685
+ result.status == "completed" and counted != [expected]
599
686
  ):
600
687
  return refuse(
601
- "artifact-files-mismatch", f"attendu={[expected]}, observes={observed}"
688
+ "artifact-files-mismatch",
689
+ f"attendu={[expected]}, observes={counted}",
690
+ tuple(
691
+ (
692
+ path,
693
+ "agent-input" if path in imputed_paths
694
+ else "during-agent" if path in executed
695
+ else "before-launch",
696
+ )
697
+ for path in counted
698
+ if path != expected
699
+ ),
602
700
  )
603
701
  if contract.activity in DOCUMENT_ARTIFACTS and result.status == "completed":
604
702
  artifact_name = document_artifact_name(
@@ -642,7 +740,9 @@ def _apply_activity_result(
642
740
 
643
741
  recorded = result.to_dict()
644
742
  recorded["changedFiles"] = observed
645
- recorded["observation"] = recorded_observation(observation, declared)
743
+ recorded["observation"] = recorded_observation(
744
+ observation, declared, exempted=amended, amendment_warning=amendment
745
+ )
646
746
  _write_json_atomic(activity_result_file(feature_dir, contract.activity), recorded)
647
747
  if result.status == "blocked":
648
748
  safe_refresh_worklog(feature_dir)
@@ -932,15 +1032,22 @@ def _record_failed_attempt(
932
1032
 
933
1033
  ``executionFiles`` est la liste brute des fichiers modifies pendant
934
1034
  l'agent, non filtree. ``observedFiles`` vaut les fichiers depuis la base de l'activite ; si
935
- l'observation est impossible, aucune fiche n'est ecrite.
1035
+ l'observation est impossible, une fiche minimale (``head`` et ``branch`` vides,
1036
+ ``observedFiles`` vide) n'est ecrite que si une entree du besoin est imputee ;
1037
+ sinon aucune fiche n'est ecrite (N7).
936
1038
  """
937
1039
  try:
938
1040
  state = read_adaptive_state(feature_dir / "workflow-state.json")
939
- if state is None:
940
- return
941
- head, branch, observed = observe_attempt(feature_dir, state)
942
1041
  except (OSError, ValueError):
943
1042
  return
1043
+ if state is None:
1044
+ return
1045
+ try:
1046
+ head, branch, observed = observe_attempt(feature_dir, state)
1047
+ except (OSError, ValueError):
1048
+ if not changed_inputs:
1049
+ return
1050
+ head, branch, observed = "", "", {}
944
1051
  if changed_inputs:
945
1052
  input_paths = {f"{contract.featurePath}/{name}" for name in changed_inputs}
946
1053
  observed = {
@@ -966,6 +1073,74 @@ def _record_failed_attempt(
966
1073
  _write_json_atomic(activity_attempt_file(feature_dir, contract.activity), payload)
967
1074
 
968
1075
 
1076
+ def _input_paths(feature_dir: Path, feature_path: str) -> list[str]:
1077
+ """Chemins des entrees du besoin qui sont des fichiers ordinaires lisibles.
1078
+
1079
+ Un lien symbolique ou une entree illisible n'en fait pas partie : il reste compte.
1080
+ """
1081
+ return [
1082
+ f"{feature_path}/{name}"
1083
+ for name, fingerprint in sorted(_input_fingerprints(feature_dir).items())
1084
+ if not fingerprint.startswith(("symlink:", "unreadable:"))
1085
+ ]
1086
+
1087
+
1088
+ def _imputed_inputs(
1089
+ feature_dir: Path, feature_path: str, activity: str, since_base: dict[str, str]
1090
+ ) -> list[str]:
1091
+ """Entrees que la fiche de tentative impute a un agent de l'activite (N5).
1092
+
1093
+ Seules comptent celles qui different encore de la base : une entree remise en
1094
+ etat n'est plus imputee.
1095
+ """
1096
+ try:
1097
+ attempt = json.loads(
1098
+ activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
1099
+ )
1100
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
1101
+ return []
1102
+ if not _valid_attempt(attempt) or attempt["activity"] != activity:
1103
+ return []
1104
+ return sorted(
1105
+ name
1106
+ for name in attempt.get("changedInputs", [])
1107
+ if f"{feature_path}/{name}" in since_base
1108
+ )
1109
+
1110
+
1111
+ def _closed_activities(state: AdaptiveWorkflowState) -> list[str]:
1112
+ """Activites closes qu'``adaptive reopen`` accepte comme cible."""
1113
+ return [
1114
+ activity
1115
+ for activity in state.completedActivities
1116
+ if activity not in {"assess", "done"}
1117
+ ]
1118
+
1119
+
1120
+ def _amendment_warning(
1121
+ issue_id: str, amended: list[str], closed_activities: Iterable[str]
1122
+ ) -> str:
1123
+ """Avertissement d'une entree du besoin amendee avant l'agent (N2), ou ''."""
1124
+ if not amended:
1125
+ return ""
1126
+ parts = [
1127
+ "entrees du besoin amendees avant le lancement de l'agent, non comptees "
1128
+ "contre l'artefact attendu : " + ", ".join(amended)
1129
+ ]
1130
+ closed = list(closed_activities)
1131
+ if closed:
1132
+ parts.append(
1133
+ "activites deja closes, produites sur l'ancien besoin : " + ", ".join(closed)
1134
+ )
1135
+ parts.append(
1136
+ "si l'amendement change leur resultat : devflow adaptive reopen "
1137
+ f"{shlex.quote(issue_id)} ACTIVITE --reason MOTIF"
1138
+ )
1139
+ else:
1140
+ parts.append("aucune activite close a rouvrir")
1141
+ return " ; ".join(parts)
1142
+
1143
+
969
1144
  def _input_fingerprints(feature_dir: Path) -> dict[str, str]:
970
1145
  """Empreinte les entrees modifiables sans conserver leur contenu."""
971
1146
  paths = [feature_dir / NEED_STATEMENT, *sorted(feature_dir.glob("issue-*.md"))]
@@ -545,7 +545,7 @@ def run_automatic_verification(
545
545
  preparation_payload, prepare_contract
546
546
  )
547
547
  if preparation.status != "completed":
548
- raise ValueError("prepare-result n'est pas complete")
548
+ raise ValueError(f"{plan_activity}-result n'est pas complete")
549
549
  planned_prefix = "PREP" if state.profile == "standard" else "PLAN"
550
550
  planned_commands = []
551
551
  for item in preparation.validationPlan:
@@ -1011,6 +1011,7 @@ def _inspect_diff(project_root: Path, changed_files: list[str]) -> dict:
1011
1011
  if missing:
1012
1012
  findings.append(
1013
1013
  "fichiers implementes absents du diff courant : " + ", ".join(missing)
1014
+ + " ; cause possible : lot commite entre implement et verify"
1014
1015
  )
1015
1016
 
1016
1017
  diff_result = run_verifications(
@@ -38,7 +38,13 @@ class LegacySchemaError(ValueError):
38
38
  ) -> None:
39
39
  self.path = path
40
40
  self.version = version
41
- if producer == "7.x":
41
+ if producer == "no-base":
42
+ message = (
43
+ f"{path} : schemaVersion {version} : l'etat n'a pas de base d'activite "
44
+ "valide (activityBase absente ou nulle) ; aucune base n'est reconstruite ; "
45
+ "recreez le parcours"
46
+ )
47
+ elif producer == "7.x":
42
48
  message = (
43
49
  f"{path} : schemaVersion {version} : parcours cree avec devflow 7.x ; "
44
50
  f"terminez-le avec devflow 7.x ({install}) ou recreez-le ; aucune migration"
@@ -46,8 +52,8 @@ class LegacySchemaError(ValueError):
46
52
  else:
47
53
  message = (
48
54
  f"{path} : schemaVersion {version} produit par devflow {producer}, non "
49
- f"lisible par devflow 8 ; terminer ce parcours avec devflow {producer} "
50
- f"({install})"
55
+ f"lisible par devflow 8 ; le terminer ou le consulter avec "
56
+ f"devflow {producer} ({install})"
51
57
  )
52
58
  super().__init__(message)
53
59
 
@@ -58,7 +64,9 @@ WORKFLOW_STATE_7X_VERSION = 5
58
64
 
59
65
 
60
66
  def legacy_workflow_state_error(path: Path, version: int) -> LegacySchemaError:
61
- """Refus de rupture d'un etat adaptatif cree avec devflow 7.x (sans base)."""
67
+ """Refus de rupture d'un etat adaptatif sans base (schema 5 de 7.x, ou schema 6 sans base)."""
68
+ if version != WORKFLOW_STATE_7X_VERSION:
69
+ return LegacySchemaError(path, version, producer="no-base")
62
70
  return LegacySchemaError(path, version, producer="7.x", install=LEGACY_7X_INSTALL)
63
71
 
64
72
 
@@ -442,7 +442,7 @@ def render_export(
442
442
  treated: dict[str, dict[str, Any]],
443
443
  home: Path,
444
444
  ) -> str:
445
- """Markdown au format de FEEDBACK.md ; chemins normalises, structure neutralisee."""
445
+ """Markdown de l'export (format de l'ancien FEEDBACK.md, fichier supprime) ; chemins normalises, structure neutralisee."""
446
446
  out = ["# Retours d'usage devflow — export", ""]
447
447
  if not entries:
448
448
  out += ["Aucun retour.", ""]
@@ -42,6 +42,7 @@ OBSOLETE_CLAUDE_RESOURCES = {
42
42
  "devflow.tasks.md",
43
43
  ),
44
44
  "agents": ("devflow-autofix.md", "devflow-review.md"),
45
+ "templates": ("research-template.md",),
45
46
  }
46
47
 
47
48
 
@@ -31,6 +31,7 @@ def refresh_worklog(feature_dir: Path) -> Path:
31
31
 
32
32
  result = _read_json(activity_result_file(feature_dir, "implement"), required=False)
33
33
  review = _read_json(activity_result_file(feature_dir, "review"), required=False)
34
+ risks = latest_remaining_risks(feature_dir, state.activities)
34
35
  rollback = _read_json(
35
36
  activity_result_file(feature_dir, "rollback-check"), required=False
36
37
  )
@@ -71,11 +72,28 @@ def refresh_worklog(feature_dir: Path) -> Path:
71
72
  execution,
72
73
  observation_warnings,
73
74
  approvals,
75
+ risks,
74
76
  )
75
77
  _write_atomic(worklog_file, managed + manual)
76
78
  return worklog_file
77
79
 
78
80
 
81
+ def latest_remaining_risks(feature_dir: Path, activities: list[str]) -> list[str]:
82
+ """Risques du resultat le plus recent de la route (dernier lisible), sans doublon."""
83
+ for activity in reversed(activities):
84
+ payload = _read_json(activity_result_file(feature_dir, activity), required=False)
85
+ if not payload:
86
+ continue
87
+ values = payload.get("remainingRisks", [])
88
+ risks: list[str] = []
89
+ if isinstance(values, list):
90
+ for value in values:
91
+ if isinstance(value, str) and value not in risks:
92
+ risks.append(value)
93
+ return risks
94
+ return []
95
+
96
+
79
97
  def safe_refresh_worklog(feature_dir: Path) -> Path | None:
80
98
  """Actualise la vue derivee sans compromettre une transition deja validee."""
81
99
  try:
@@ -99,6 +117,7 @@ def _render_managed(
99
117
  execution: dict[str, Any],
100
118
  observation_warnings: list[str] | None = None,
101
119
  approvals: list[str] | None = None,
120
+ risks: list[str] | None = None,
102
121
  ) -> str:
103
122
  issue_id = state["issueId"]
104
123
  summary = assessment.get("summary")
@@ -117,14 +136,7 @@ def _render_managed(
117
136
  checks.extend(_verification_lines(security, {}))
118
137
  checks.extend(_activity_evidence_lines(review))
119
138
  checks.extend(_activity_evidence_lines(rollback))
120
- risk_source = rollback or review or result
121
- risks = (
122
- risk_source.get("remainingRisks", [])
123
- if isinstance(risk_source, dict)
124
- else []
125
- )
126
- if not isinstance(risks, list) or not all(isinstance(item, str) for item in risks):
127
- risks = []
139
+ risks = risks or []
128
140
 
129
141
  lines = [
130
142
  f"# Worklog — {issue_id}",
@@ -343,7 +355,7 @@ def _manual_suffix(path: Path) -> str:
343
355
  def _read_json(path: Path, *, required: bool = True) -> dict[str, Any]:
344
356
  try:
345
357
  payload = json.loads(path.read_text(encoding="utf-8"))
346
- except (OSError, json.JSONDecodeError):
358
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
347
359
  if required:
348
360
  raise ValueError(f"artefact JSON invalide ou absent : {path.name}")
349
361
  return {}
@@ -12,13 +12,17 @@ interpréter de `state.json`.
12
12
  1. Lire `$ARGUMENTS` et extraire l'identifiant de feature, l'agent demandé
13
13
  (`claude` par défaut) et la cible `--until` (`done` par défaut).
14
14
  2. Exécuter `devflow status ISSUE_ID`.
15
- 3. Si la feature n'existe pas, demander à l'utilisateur de lancer
16
- `devflow feature ISSUE_ID`.
17
- 4. Si elle n'est pas évaluée, demander les signaux nécessaires puis lancer
15
+ 3. Si la feature n'existe pas, exécuter
16
+ `devflow feature ISSUE_ID --short-name NOM` (nom court tiré du titre de
17
+ l'issue) et annoncer la branche et le dossier créés.
18
+ 4. Si la feature n'est pas encore évaluée et que `need.md` n'existe pas dans
19
+ son dossier, le rédiger à partir de l'issue et de la demande. En cas de
20
+ doute sur le besoin, poser la question avant de l'écrire.
21
+ 5. Si elle n'est pas évaluée, demander les signaux nécessaires puis lancer
18
22
  `devflow assess ISSUE_ID` avec des options explicites.
19
- 5. Si elle est évaluée mais non démarrée, lancer
23
+ 6. Si elle est évaluée mais non démarrée, lancer
20
24
  `devflow adaptive start ISSUE_ID`.
21
- 6. Enchaîner les activités avec
25
+ 7. Enchaîner les activités avec
22
26
  `devflow adaptive run ISSUE_ID --agent AGENT --until CIBLE`.
23
27
  `--until` inclut l'activité ciblée.
24
28
  La CLI s'arrête au premier refus. Distinguer alors deux cas :
@@ -31,7 +35,7 @@ interpréter de `state.json`.
31
35
  CLI plante (traceback, commande inconnue), sortir du parcours : terminer
32
36
  la feature hors Devflow, clôturer par un `bilan` dont `--what` indique
33
37
  l'abandon et sa cause, et indiquer cette cause dans le compte rendu.
34
- 7. Afficher ensuite `devflow adaptive status ISSUE_ID`.
38
+ 8. Afficher ensuite `devflow adaptive status ISSUE_ID`.
35
39
 
36
40
  ## Retours d'usage
37
41
 
@@ -46,7 +46,7 @@
46
46
 
47
47
  ## Gates obligatoires
48
48
 
49
- Les gates sont vérifiées à chaque `/devflow.plan`. Une violation non justifiée est BLOQUANTE.
49
+ Les gates sont vérifiées avant l'implémentation. Une violation non justifiée est BLOQUANTE.
50
50
 
51
51
  ### Gate 1 — Simplicité
52
52
 
@@ -64,7 +64,7 @@ Les gates sont vérifiées à chaque `/devflow.plan`. Une violation non justifi
64
64
  | Conventions | Suivre celles du codebase existant |
65
65
  | Modèle de données | Représentation unique, pas de duplication |
66
66
 
67
- ### Gate 3 — Tests d'abord
67
+ ### Gate 3 — Tests
68
68
 
69
69
  | Critère | Règle |
70
70
  |---------|-------|
@@ -80,6 +80,6 @@ Les gates sont vérifiées à chaque `/devflow.plan`. Une violation non justifi
80
80
 
81
81
  ## Suivi des dérogations
82
82
 
83
- | Date | Article | Dérogation | Justification |
84
- |------|---------|------------|---------------|
85
- | | | | |
83
+ | Date | Article | Dérogation | Justification | Échéance |
84
+ |------|---------|------------|---------------|----------|
85
+ | | | | | |
@@ -14,14 +14,14 @@
14
14
  |------|--------|-------------|
15
15
  | Simplicité | | |
16
16
  | Patterns existants | | |
17
- | Tests d'abord | | |
17
+ | Tests | | |
18
18
 
19
19
  ### Dérogations
20
20
 
21
21
  <!-- Si une gate est violée, justifier ici. Sans justification = BLOQUANT -->
22
22
 
23
- | Article | Dérogation | Justification |
24
- |---------|------------|---------------|
23
+ | Article | Dérogation | Justification | Échéance |
24
+ |---------|------------|---------------|----------|
25
25
 
26
26
  ### Complexity Tracking
27
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devflow-cli
3
- Version: 8.3.0
3
+ Version: 8.5.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
@@ -65,7 +65,8 @@ devflow init --ai codex
65
65
  Après chaque mise à jour de la CLI, lancer `devflow upgrade` : il met à jour
66
66
  les commandes `/devflow` installées dans `~/.claude`, que l'installation de la
67
67
  CLI ne touche pas. `--refresh` évite qu'`uv` ignore une version tout juste
68
- publiée. Tant que les commandes installées diffèrent de la CLI,
68
+ publiée. Juste après une publication, l'installation peut échouer pendant une
69
+ à quatre minutes, le temps que l'index se propage : réessayer. Tant que les commandes installées diffèrent de la CLI,
69
70
  `devflow status` affiche un avertissement avec cette commande ; rien n'est
70
71
  modifié sans vous.
71
72
 
@@ -116,6 +117,22 @@ nomme la cause (`structured-output-missing`) et propose
116
117
  `devflow adaptive reconcile KS-123 --result MODELE --reason MOTIF` avec un
117
118
  modèle de résultat prérempli, écrit dans `~/.devflow/reconcile/` et à compléter.
118
119
 
120
+ `need.md` peut être amendé en cours de parcours. Avant une activité
121
+ documentaire, l'amendement est accepté avec un avertissement qui nomme les
122
+ activités déjà closes, à rouvrir si leur résultat en dépend ; avant `prepare`,
123
+ il est accepté sans cet avertissement ; pendant l'exécution d'un agent, il est refusé ;
124
+ avant `implement`, il périme la preuve de pré-vol ; après `implement`, il
125
+ périme une revue déjà faite. Pour arbitrer après un blocage d'`implement` :
126
+ amender `need.md`, puis `devflow adaptive reopen KS-123 preflight-evidence
127
+ --reason MOTIF`, refaire le pré-vol et relancer `implement`. Un refus
128
+ d'artefact (`artifact-files-mismatch`) nomme chaque fichier en trop et sa
129
+ cause : retirer ou remettre en état le fichier, puis
130
+ `devflow adaptive reconcile KS-123` ; la relance à l'identique n'est plus
131
+ proposée. Pour une entrée du besoin modifiée par un agent, la relance est
132
+ refusée avant le lancement de l'agent : remettre l'entrée en état, ou
133
+ `devflow adaptive discard-attempt KS-123 --reason MOTIF`, puis
134
+ `devflow adaptive run KS-123`.
135
+
119
136
  Chaque invocation de `adaptive run` traite une activité. La politique est dans
120
137
  `assessment.json`; la progression et les preuves sont dans
121
138
  `workflow-state.json`. Les résultats agent sont validés par schéma et les
@@ -1,10 +1,10 @@
1
- devflow_cli/__init__.py,sha256=IpmjnS3nun7PP7DJ0y0oLHgWCL8HVul9hsCpjbAVK4s,925
1
+ devflow_cli/__init__.py,sha256=gN1yusVBmKEDSmEtb_g_BPIHNRFR3EDm3BA7mMbPXtQ,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
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=OdESwOyoMgySOyyLkAEk3oIkVPXS-bli_SvJe5VeHGU,64445
7
+ devflow_cli/commands/adaptive.py,sha256=efDFrh9GOfnISzJwc5utP4KchhjyjxagytIwBgQ7N2Q,72537
8
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
@@ -23,21 +23,21 @@ devflow_cli/commands/upgrade_cmd.py,sha256=d5uBFUPzPsCJDLxhQBFaeHWJv3GWWLmPpkYHR
23
23
  devflow_cli/commands/usage.py,sha256=ehH1AH5J-XfkebFx6qLBwpKN-MOoX2uUbq4LejBpNsk,3148
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
- devflow_cli/core/activity_base.py,sha256=haEVVgZAi3-iX75oo2-fK9GQZeFgN_xRw5U0Z0bdV5w,16343
27
- devflow_cli/core/activity_contracts.py,sha256=n9arBw-SWEFqTlCDNed6lV4WolkZKAvmQg1qcfk57ss,39036
28
- devflow_cli/core/adaptive_runner.py,sha256=GRL4w9FsIPplmypPx1KUnel_byc9PVe7jkTq6Q5Uy0c,41879
26
+ devflow_cli/core/activity_base.py,sha256=YId_tUxiL6TYmnVpG2VYGkA31nPjIrskx6Dof27XR6E,16696
27
+ devflow_cli/core/activity_contracts.py,sha256=ui-9JYwtZYcjEVppgcD5YNW_O7gkxxCkfXlIevLrJ-c,39295
28
+ devflow_cli/core/adaptive_runner.py,sha256=BU3LZzM2f7dNvgKm_ul0CZYIs8z7WN1hM--TrF9o07s,48852
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=Ntsy2eQKLRl_hUD27V9NRBLavyhLxxkEcbPySKmd4W4,42337
33
+ devflow_cli/core/automatic_evidence.py,sha256=m84Pa8DL4tO-Jzcvv6BNCX7g4zPbczFruGlg5Y2lrUs,42420
34
34
  devflow_cli/core/catalog.py,sha256=nLPc78JkbISaSxJnHlWiMi_lvepaumbfwcjgLuVy7Zc,3102
35
- devflow_cli/core/contract_schemas.py,sha256=-7SlJCwQ31OjPfwA-tP4ctgbp9hxQAVwNvvOV09Hoho,6313
35
+ devflow_cli/core/contract_schemas.py,sha256=TOGXwpNmm_MEzDkxR1wYvcCqASv1j4C1vxT6RjDFMiM,6744
36
36
  devflow_cli/core/docgen.py,sha256=47JoVDWetkcctgHwJZC67AVebOKgl10VB9hZ3Xr63mo,3908
37
- devflow_cli/core/feedback.py,sha256=A4yz2HqD5umMqrY4YQaqugxZM2tnS7NkXOObo1Nbm4M,18927
37
+ devflow_cli/core/feedback.py,sha256=4LJk-stAz_1toVVK4kGlWs6yUIQEEjjXVLglDXc37JU,18965
38
38
  devflow_cli/core/git.py,sha256=gsDfT-hDNtMQFRUl1WzZSqltrdwZQ2BJBcCIh4TwDkA,2780
39
39
  devflow_cli/core/hooks.py,sha256=nFPaziFJgeZgAPKPxbTibrB_0MJAST3cAmg3gtcvDBQ,8067
40
- devflow_cli/core/installer.py,sha256=MfcGZKoOcQc2qviNnGShoD0P_MqOgRuVHX7YwCrG2vw,6259
40
+ devflow_cli/core/installer.py,sha256=fmfaJs5ApkhRmQi1B4vz52iAHBtqgeyBoAnE8CXAzJI,6303
41
41
  devflow_cli/core/manifest.py,sha256=h058_m5-HJrYsNUdgfwnKts2riHiYPXUwMzPImHOs2g,7023
42
42
  devflow_cli/core/operational.py,sha256=11iLTbyZF0ZjYTQ5HDmVCaw4bpbXpLtH2v9f1WQOrYg,1228
43
43
  devflow_cli/core/redaction.py,sha256=o_1MDtCyvRLrk_dWlV6_7E6_mPVB1B5a0cCfTBj9j0Q,1973
@@ -53,7 +53,7 @@ devflow_cli/core/validation_approvals.py,sha256=3AHOpnW3BtY3BQrRZr38K7VNYRvbxGNh
53
53
  devflow_cli/core/validators.py,sha256=fREjEx2fBW36q3vB6WJNqoGUM9ejYlQUXie5A9lNuPs,7440
54
54
  devflow_cli/core/verification.py,sha256=rvbdAdLyrZYb20oauti7D8nV5TDFBIf_8-hqLahfRag,23576
55
55
  devflow_cli/core/waivers.py,sha256=9XXzMmoWCn-2Dc3486WhZwtGJQLQPXjP0qAaLuL_3qU,13299
56
- devflow_cli/core/worklog.py,sha256=GkaXCsx22jntcWuC75OystUoqb5AlzBbXXLYH581Mwg,14164
56
+ devflow_cli/core/worklog.py,sha256=i4zgJIlS1OSIBMrhT1AKU97bh7IHAhNYbnr4eJVGv_Y,14692
57
57
  devflow_cli/core/worktree_snapshot.py,sha256=Q2_KTkhJTGvdM6Ay4RK9OHkyPEMrHQRTb-f-Eptek-8,5255
58
58
  devflow_cli/schemas/activity-contract-v4.schema.json,sha256=IeduWDiJ5IV6EL7FDZWnHBjzyUt1uwig2rB-SMbcpOU,2006
59
59
  devflow_cli/schemas/activity-result-v1.schema.json,sha256=xQuyD5fwHGIdeoJoDJxJn26qBGJX74lD6Dc5gi5CLQo,2772
@@ -67,7 +67,7 @@ devflow_cli/utils/strings.py,sha256=bfFXCwBDnFxshgZOYfy6mtysnjiaWO1wFnqBf7HLg4o,
67
67
  devflow_cli/resources/commands/devflow.context.md,sha256=SdvZ286cJKkgQIKYwnjd30v5zDeEXTFBhpEEsEOInoc,2048
68
68
  devflow_cli/resources/commands/devflow.extension.md,sha256=w7hoFfxRS0y-3rsvUmQDx1fvZhtE9KhANmRgFDwYVaA,2373
69
69
  devflow_cli/resources/commands/devflow.init.md,sha256=wplv3IiJrNkUoPXGZtOw9k6m3dzXBQDYAcPPN8d3EF8,2011
70
- devflow_cli/resources/commands/devflow.md,sha256=ojhLIwQvl0pS7Zxct6RS2ihpiT9f9-F1yH7jRTO47Ng,2689
70
+ devflow_cli/resources/commands/devflow.md,sha256=EZ1yo5Hx7lnHX8G1EB0U6V4wNJ4kzb8nSmXI2PneY7s,2984
71
71
  devflow_cli/resources/commands/devflow.regen.md,sha256=yjbMNAt6k0y6M9Y5HZDhCRgj_tb33Om6_-5OijmD5Bw,1169
72
72
  devflow_cli/resources/commands/devflow.status.md,sha256=tqjQ1IboU99ztG6C4nunBHGJCvMn3qDqx3AoF4SPjCk,338
73
73
  devflow_cli/resources/extensions/EXTENSION-API-REFERENCE.md,sha256=1wuAV-K8WI0BJ6SJ0HViieSHe3zazlfHemkKDT7bbKM,4478
@@ -76,12 +76,12 @@ devflow_cli/resources/extensions/catalog.json,sha256=O2Uqgkvudf6JXIMQFKEk8d1MuA1
76
76
  devflow_cli/resources/extensions/template/example-command.md,sha256=3qlpEOzbYQdL79iucaZ5wU5zDX2Q_pxz0Fg9jSF-B84,1020
77
77
  devflow_cli/resources/extensions/template/example-extension.json,sha256=Az9Ihq3ANlWmUzWZDfttaUCUmfesOFH4IbYq23CmWC4,416
78
78
  devflow_cli/resources/templates/agent-context-template.md,sha256=D5wd2hcxGrVR0sIE2JEDUPcsFHoZWJHf5m8LuD12J9Y,520
79
- devflow_cli/resources/templates/constitution-template.md,sha256=r2UkG3f9l-zlWJ4yt5aIXhuwj37zdF9VbmqPC9J1uXA,2086
79
+ devflow_cli/resources/templates/constitution-template.md,sha256=uSzNuwF-xIRx8cHt9gloynRjNgDGmnN5z2Xck6fCOsI,2102
80
80
  devflow_cli/resources/templates/contracts-template.md,sha256=dx7slFV_5mG6BJ-OfQ8rfRXSid-ZY_NKmVjllmN2fAA,1768
81
81
  devflow_cli/resources/templates/data-model-template.md,sha256=RliumtOY20L-Z0grx7tfvg9pbulGDKR6EN7k-7zKNUU,1015
82
82
  devflow_cli/resources/templates/devflow-hooks-template.yml,sha256=dGjsh4PLEkCjwNWWhMOI0F7Md-Edu3oYkHtmaPLlg3o,1205
83
83
  devflow_cli/resources/templates/extension-manifest-template.json,sha256=GVCE2ZFObN93dlMRyf7UUawpmy5GSu11Yj11UPfqTBc,226
84
- devflow_cli/resources/templates/plan-template.md,sha256=zAhlp5DVy8q3A5zexZVhId4WDbvmEgBgWUNctsWjGsk,2609
84
+ devflow_cli/resources/templates/plan-template.md,sha256=0mZ0hBCbP8mYdAjzDzrJY4bZDhAXvJNHynm_mKu6Vmk,2625
85
85
  devflow_cli/resources/templates/quickstart-template.md,sha256=LkB7FfO7ie0-wD5NltR7BGDudNVOW5_huDvha6NcOsg,1226
86
86
  devflow_cli/resources/templates/spec-template.md,sha256=XiUSkj-GkzhN-kHO4p5i1u1thbSCgK1x-CIAHSso3Go,1918
87
87
  devflow_cli/resources/templates/tasks-template.md,sha256=PnWRAeok9MGTj1F30UeYu0njru7RsAOdj16Cgyd45Ww,2525
@@ -90,8 +90,8 @@ devflow_cli/resources/templates/contracts/api-template.md,sha256=rC5v7JBz71PznCn
90
90
  devflow_cli/resources/templates/contracts/components-template.md,sha256=XIWfEXfC72sNmL5OA3V-7xIywkNDU2DBPLfyiagOWcs,484
91
91
  devflow_cli/resources/templates/contracts/interfaces-template.md,sha256=kf8B7b6HsmN7jyRlLNU7RnZl0rTMlI5s97yVRyI-6yc,336
92
92
  devflow_cli/resources/templates/contracts/services-template.md,sha256=cFafKCyLmq_a7aR7G0LXAaMO2z4Fuflq6iKdxfVg0vg,477
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,,
93
+ devflow_cli-8.5.0.dist-info/METADATA,sha256=pdYiU-6bFeQyZOYpdXA3GrR9Ha99afor2pZcjB9__oU,9982
94
+ devflow_cli-8.5.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
95
+ devflow_cli-8.5.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
96
+ devflow_cli-8.5.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
97
+ devflow_cli-8.5.0.dist-info/RECORD,,