devflow-cli 8.2.0__py3-none-any.whl → 8.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devflow_cli/__init__.py +1 -1
- devflow_cli/commands/adaptive.py +237 -11
- devflow_cli/commands/assess.py +11 -1
- devflow_cli/core/activity_base.py +16 -4
- devflow_cli/core/activity_contracts.py +8 -2
- devflow_cli/core/adaptive_runner.py +232 -28
- devflow_cli/core/automatic_evidence.py +57 -9
- devflow_cli/core/waivers.py +42 -1
- devflow_cli/core/worklog.py +10 -0
- {devflow_cli-8.2.0.dist-info → devflow_cli-8.4.1.dist-info}/METADATA +24 -4
- {devflow_cli-8.2.0.dist-info → devflow_cli-8.4.1.dist-info}/RECORD +14 -14
- {devflow_cli-8.2.0.dist-info → devflow_cli-8.4.1.dist-info}/WHEEL +0 -0
- {devflow_cli-8.2.0.dist-info → devflow_cli-8.4.1.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.2.0.dist-info → devflow_cli-8.4.1.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -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,
|
|
@@ -510,12 +513,17 @@ def run_adaptive(
|
|
|
510
513
|
_missing_output_recovery(
|
|
511
514
|
feature_dir, current_state.issueId, outcome.activity, agent,
|
|
512
515
|
execution.stdout,
|
|
516
|
+
mutated=outcome.reason == "agent-failed-after-mutation",
|
|
513
517
|
)
|
|
514
518
|
if outcome.reason in {"agent-failed", "agent-failed-after-mutation"}
|
|
515
519
|
and execution is not None
|
|
516
520
|
and execution.final_output_diagnostic in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
517
521
|
else None
|
|
518
522
|
)
|
|
523
|
+
if outcome.reason == "preflight-evidence-stale":
|
|
524
|
+
recovery = _stale_preflight_recovery(current_state)
|
|
525
|
+
if outcome.reason == "artifact-files-mismatch":
|
|
526
|
+
recovery = _artifact_mismatch_recovery(current_state, outcome, agent)
|
|
519
527
|
if json_output:
|
|
520
528
|
_print_execution_json(
|
|
521
529
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
@@ -697,7 +705,17 @@ def reconcile_adaptive(
|
|
|
697
705
|
console.print(" Suite : attendez la fin de l'activite en cours, puis relancez.")
|
|
698
706
|
raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
|
|
699
707
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
700
|
-
if outcome.reason == "
|
|
708
|
+
if outcome.reason == "artifact-files-mismatch":
|
|
709
|
+
console.print(" Reprise guidee :")
|
|
710
|
+
_print_recovery_steps(
|
|
711
|
+
_artifact_mismatch_recovery(
|
|
712
|
+
state, outcome, _attempt_agent(feature_dir, outcome.activity)
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
if (
|
|
716
|
+
outcome.reason == "reconcile-refused"
|
|
717
|
+
and activity_attempt_file(feature_dir, outcome.activity).is_file()
|
|
718
|
+
):
|
|
701
719
|
console.print(
|
|
702
720
|
f" Abandon audite : devflow adaptive discard-attempt {state.issueId} --reason MOTIF"
|
|
703
721
|
)
|
|
@@ -915,16 +933,45 @@ def discard_attempt_command(
|
|
|
915
933
|
f"Tentative '{entry['activity']}' abandonnee (audit #{entry['sequence']}, "
|
|
916
934
|
f"archive : {entry['archivePath']})."
|
|
917
935
|
)
|
|
918
|
-
|
|
936
|
+
discarded = list(entry["discardedFiles"])
|
|
937
|
+
inputs = (
|
|
938
|
+
[
|
|
939
|
+
path for path in discarded
|
|
940
|
+
if _is_need_input(path, feature_path_for(
|
|
941
|
+
project_root_for_feature(feature_dir), feature_dir
|
|
942
|
+
))
|
|
943
|
+
]
|
|
944
|
+
if entry["activity"] in DOCUMENT_ARTIFACTS
|
|
945
|
+
else []
|
|
946
|
+
)
|
|
947
|
+
others = [path for path in discarded if path not in inputs]
|
|
948
|
+
if inputs:
|
|
949
|
+
warn(
|
|
950
|
+
"Entrees du besoin modifiees depuis le debut de l'activite "
|
|
951
|
+
f"'{entry['activity']}' : " + ", ".join(inputs) + " ; elles ne sont plus "
|
|
952
|
+
"imputees a un agent apres cet abandon et seront acceptees comme amendees "
|
|
953
|
+
"a la relance : a verifier"
|
|
954
|
+
)
|
|
955
|
+
if others:
|
|
919
956
|
warn(
|
|
920
957
|
"Fichiers modifies depuis le debut de l'activite "
|
|
921
958
|
f"'{entry['activity']}', comptes dans le lot a la relance ; remise en "
|
|
922
|
-
"etat a la charge du superviseur : " + ", ".join(
|
|
959
|
+
"etat a la charge du superviseur : " + ", ".join(others)
|
|
923
960
|
)
|
|
924
961
|
if entry.get("observationError"):
|
|
925
962
|
warn(f"Fichiers depuis la base non calcules : {entry['observationError']}")
|
|
926
963
|
|
|
927
964
|
|
|
965
|
+
def _is_need_input(path: str, feature_path: str | None) -> bool:
|
|
966
|
+
"""Vrai pour need.md ou issue-*.md directement sous le dossier de la feature."""
|
|
967
|
+
if feature_path is None:
|
|
968
|
+
return False
|
|
969
|
+
parent, _, name = path.rpartition("/")
|
|
970
|
+
return parent == feature_path and (
|
|
971
|
+
name == NEED_STATEMENT or (name.startswith("issue-") and name.endswith(".md"))
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
|
|
928
975
|
def _execute_local_verification(
|
|
929
976
|
feature_dir: Path, project_root: Path, issue_id: str, *,
|
|
930
977
|
refresh: bool = False, json_output: bool = False,
|
|
@@ -937,7 +984,8 @@ def _execute_local_verification(
|
|
|
937
984
|
sync_activity_status(issue_id, "verify", stream)
|
|
938
985
|
outcome = run_automatic_verification(feature_dir, project_root, refresh=refresh)
|
|
939
986
|
recovery = (
|
|
940
|
-
None if outcome.passed
|
|
987
|
+
None if outcome.passed
|
|
988
|
+
else _verify_recovery(feature_dir, issue_id, outcome, refresh=refresh)
|
|
941
989
|
)
|
|
942
990
|
if json_output:
|
|
943
991
|
_print_execution_json(
|
|
@@ -954,6 +1002,8 @@ def _execute_local_verification(
|
|
|
954
1002
|
ok(f"Preuves de verify rafraichies. Activite courante : {outcome.nextActivity}")
|
|
955
1003
|
else:
|
|
956
1004
|
ok(f"Preuves generees et validees. Prochaine : {outcome.nextActivity}")
|
|
1005
|
+
if outcome.detail:
|
|
1006
|
+
console.print(f" Motif : {outcome.detail}", markup=False, soft_wrap=True)
|
|
957
1007
|
return
|
|
958
1008
|
fail(outcome.detail or outcome.reason)
|
|
959
1009
|
console.print(f" Verification automatique refusee : {outcome.reason}")
|
|
@@ -976,17 +1026,20 @@ PLANNED_PREFIXES = {"PREP-": "prepare", "PLAN-": "plan"}
|
|
|
976
1026
|
|
|
977
1027
|
|
|
978
1028
|
def _verify_recovery(
|
|
979
|
-
feature_dir: Path, issue_id: str, outcome: AutomaticVerificationOutcome
|
|
1029
|
+
feature_dir: Path, issue_id: str, outcome: AutomaticVerificationOutcome,
|
|
1030
|
+
*, refresh: bool = False,
|
|
980
1031
|
) -> dict:
|
|
981
1032
|
"""Reprise guidee d'un refus de verify, par motif, sans heuristique (B2).
|
|
982
1033
|
|
|
983
1034
|
Approbation manquante : chaque approve-command puis verify. Executable introuvable
|
|
984
|
-
(code 127) : corriger l'environnement puis verify. Echec d'un controle :
|
|
985
|
-
le code (reopen implement)
|
|
1035
|
+
(code 127) : corriger l'environnement puis verify. Echec d'un controle : trois voies,
|
|
1036
|
+
le code (reopen implement), la validation (reopen de l'activite qui l'a inscrite) ou
|
|
1037
|
+
un echec passager (relancer verify). Apres un verify --refresh refuse, toutes les
|
|
1038
|
+
commandes verify portent --refresh.
|
|
986
1039
|
"""
|
|
987
1040
|
issue = shlex.quote(issue_id)
|
|
988
1041
|
verify_command = {
|
|
989
|
-
"command": f"devflow adaptive verify {issue}",
|
|
1042
|
+
"command": f"devflow adaptive verify {issue}" + (" --refresh" if refresh else ""),
|
|
990
1043
|
"purpose": "relancer la verification",
|
|
991
1044
|
}
|
|
992
1045
|
state = read_adaptive_state(feature_dir / "workflow-state.json")
|
|
@@ -1064,6 +1117,13 @@ def _verify_recovery(
|
|
|
1064
1117
|
"command": f"devflow adaptive reopen {issue} {origin} --reason {reason_text}",
|
|
1065
1118
|
"purpose": f"voie 2 : la validation {names}, inscrite par {origin}, est en cause",
|
|
1066
1119
|
})
|
|
1120
|
+
commands.append({
|
|
1121
|
+
"command": verify_command["command"],
|
|
1122
|
+
"purpose": (
|
|
1123
|
+
"voie 3 : echec passager (reseau, conteneur, service externe), "
|
|
1124
|
+
"relancer la verification"
|
|
1125
|
+
),
|
|
1126
|
+
})
|
|
1067
1127
|
return {
|
|
1068
1128
|
"kind": "check-failed",
|
|
1069
1129
|
"summary": (
|
|
@@ -1248,8 +1308,10 @@ def _operational_status(
|
|
|
1248
1308
|
risks = _remaining_risks(feature_dir)
|
|
1249
1309
|
if not isinstance(blockers, list):
|
|
1250
1310
|
blockers = []
|
|
1251
|
-
stale = set(
|
|
1252
|
-
|
|
1311
|
+
stale: set[str] = set()
|
|
1312
|
+
if state.status != "completed": # aucune gate ne consulte la peremption apres done
|
|
1313
|
+
stale.update(stale_devflow_evidence(feature_dir, state))
|
|
1314
|
+
stale.update(stale_review_evidence(feature_dir, state))
|
|
1253
1315
|
evidence = [
|
|
1254
1316
|
{
|
|
1255
1317
|
"id": evidence_id,
|
|
@@ -1442,7 +1504,8 @@ def _last_run_missed_structured_output(feature_dir: Path, activity: str) -> bool
|
|
|
1442
1504
|
|
|
1443
1505
|
|
|
1444
1506
|
def _missing_output_recovery(
|
|
1445
|
-
feature_dir: Path, issue_id: str, activity: str, agent: str, raw_output: str
|
|
1507
|
+
feature_dir: Path, issue_id: str, activity: str, agent: str, raw_output: str,
|
|
1508
|
+
*, mutated: bool | None = None,
|
|
1446
1509
|
) -> dict:
|
|
1447
1510
|
"""Reprise guidee d'une sortie structuree absente apres un agent termine sans erreur.
|
|
1448
1511
|
|
|
@@ -1467,6 +1530,8 @@ def _missing_output_recovery(
|
|
|
1467
1530
|
attempt = json.loads(
|
|
1468
1531
|
activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
|
|
1469
1532
|
)
|
|
1533
|
+
if mutated is None: # reconcile : le motif vient de la fiche de tentative
|
|
1534
|
+
mutated = attempt["reason"] == "agent-failed-after-mutation"
|
|
1470
1535
|
contract = json.loads(
|
|
1471
1536
|
(feature_dir / "activity-contract.json").read_text(encoding="utf-8")
|
|
1472
1537
|
)
|
|
@@ -1504,12 +1569,155 @@ def _missing_output_recovery(
|
|
|
1504
1569
|
),
|
|
1505
1570
|
},
|
|
1506
1571
|
])
|
|
1572
|
+
if not mutated:
|
|
1573
|
+
# Apres des ecritures, la relance contredirait « reconciliez d'abord ».
|
|
1574
|
+
recovery["commands"].append({
|
|
1575
|
+
"command": f"devflow adaptive run {issue} --agent {shlex.quote(agent)}",
|
|
1576
|
+
"purpose": "relancer l'agent : il n'a rien ecrit, la tentative est remplacee",
|
|
1577
|
+
})
|
|
1507
1578
|
return recovery
|
|
1508
1579
|
|
|
1509
1580
|
|
|
1581
|
+
def _stale_preflight_recovery(state: AdaptiveWorkflowState) -> dict:
|
|
1582
|
+
"""Reprise guidee d'une preuve de pre-vol perimee au lancement d'implement."""
|
|
1583
|
+
issue = shlex.quote(state.issueId)
|
|
1584
|
+
if _reopenable(state, "preflight-evidence"):
|
|
1585
|
+
command = {
|
|
1586
|
+
"command": f"devflow adaptive reopen {issue} preflight-evidence --reason MOTIF",
|
|
1587
|
+
"purpose": "refaire la preuve de pre-vol sur les entrees actuelles",
|
|
1588
|
+
}
|
|
1589
|
+
else:
|
|
1590
|
+
command = {
|
|
1591
|
+
"command": f"devflow adaptive status {issue}",
|
|
1592
|
+
"purpose": "consulter l'etat du parcours",
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
"kind": "preflight-evidence-stale",
|
|
1596
|
+
"summary": "Preuve de pre-vol perimee : la refaire avant implement.",
|
|
1597
|
+
"commands": [command],
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
ARTIFACT_FILE_CAUSES = {
|
|
1602
|
+
"during-agent": "modifie pendant l'agent",
|
|
1603
|
+
"before-launch": "modifie avant le lancement de l'agent",
|
|
1604
|
+
"agent-input": "entree du besoin modifiee par un agent de cette activite",
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
def _artifact_mismatch_recovery(
|
|
1609
|
+
state: AdaptiveWorkflowState, outcome: AdaptiveRunOutcome, agent: str
|
|
1610
|
+
) -> dict:
|
|
1611
|
+
"""Reprise guidee d'un refus d'artefact, par cause observee de chaque fichier en trop."""
|
|
1612
|
+
issue = shlex.quote(state.issueId)
|
|
1613
|
+
files = [
|
|
1614
|
+
{"path": path, "cause": cause, "label": ARTIFACT_FILE_CAUSES[cause]}
|
|
1615
|
+
for path, cause in outcome.extraFiles
|
|
1616
|
+
]
|
|
1617
|
+
causes = {cause for _, cause in outcome.extraFiles}
|
|
1618
|
+
if not files:
|
|
1619
|
+
return {
|
|
1620
|
+
"kind": "artifact-files-mismatch",
|
|
1621
|
+
"summary": (
|
|
1622
|
+
"Artefact attendu absent ou inchange depuis la base, sans fichier en "
|
|
1623
|
+
"trop : l'agent doit l'ecrire."
|
|
1624
|
+
),
|
|
1625
|
+
"files": files,
|
|
1626
|
+
"commands": [{
|
|
1627
|
+
"command": f"devflow adaptive run {issue}"
|
|
1628
|
+
+ (f" --agent {shlex.quote(agent)}" if agent else ""),
|
|
1629
|
+
"purpose": "relancer l'agent pour qu'il ecrive l'artefact",
|
|
1630
|
+
}],
|
|
1631
|
+
}
|
|
1632
|
+
if causes == {"agent-input"}:
|
|
1633
|
+
# Refus avant l'agent : ni reconcile ni reopen ne leve l'imputation.
|
|
1634
|
+
run = f"devflow adaptive run {issue}" + (
|
|
1635
|
+
f" --agent {shlex.quote(agent)}" if agent else ""
|
|
1636
|
+
)
|
|
1637
|
+
return {
|
|
1638
|
+
"kind": "artifact-files-mismatch",
|
|
1639
|
+
"summary": (
|
|
1640
|
+
"Entree du besoin modifiee par un agent de cette activite et pas remise "
|
|
1641
|
+
"en etat : l'agent n'a pas ete lance. Deux sorties : remettre l'entree "
|
|
1642
|
+
"dans son etat de base, ou abandonner la tentative (l'entree modifiee "
|
|
1643
|
+
"est gardee, a verifier)."
|
|
1644
|
+
),
|
|
1645
|
+
"files": files,
|
|
1646
|
+
"commands": [
|
|
1647
|
+
{
|
|
1648
|
+
"command": run,
|
|
1649
|
+
"purpose": "apres remise en etat de l'entree dans son etat de base",
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1653
|
+
"purpose": (
|
|
1654
|
+
"sinon, garder l'entree modifiee : abandon audite de la "
|
|
1655
|
+
"tentative, puis devflow adaptive run"
|
|
1656
|
+
),
|
|
1657
|
+
},
|
|
1658
|
+
],
|
|
1659
|
+
}
|
|
1660
|
+
commands = [{
|
|
1661
|
+
"command": f"devflow adaptive reconcile {issue}",
|
|
1662
|
+
"purpose": (
|
|
1663
|
+
"une fois chaque fichier en trop retire ou remis dans son etat de base : "
|
|
1664
|
+
"valider l'artefact sans relancer l'agent"
|
|
1665
|
+
),
|
|
1666
|
+
}]
|
|
1667
|
+
index = state.activities.index(state.currentActivity)
|
|
1668
|
+
previous = state.activities[index - 1] if index > 0 else ""
|
|
1669
|
+
if (
|
|
1670
|
+
"before-launch" in causes
|
|
1671
|
+
and "agent-input" not in causes
|
|
1672
|
+
and previous not in {"", "assess"}
|
|
1673
|
+
and _reopenable(state, previous)
|
|
1674
|
+
):
|
|
1675
|
+
commands.append({
|
|
1676
|
+
"command": f"devflow adaptive reopen {issue} {previous} --reason MOTIF",
|
|
1677
|
+
"purpose": (
|
|
1678
|
+
"sinon, garder le fichier modifie avant le lancement : rouvrir "
|
|
1679
|
+
"l'activite precedente pour repartir d'une base fraiche"
|
|
1680
|
+
),
|
|
1681
|
+
})
|
|
1682
|
+
if "agent-input" in causes:
|
|
1683
|
+
commands.append({
|
|
1684
|
+
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1685
|
+
"purpose": (
|
|
1686
|
+
"sinon, garder l'entree du besoin modifiee par l'agent : abandon "
|
|
1687
|
+
"audite de la tentative, puis devflow adaptive run"
|
|
1688
|
+
),
|
|
1689
|
+
})
|
|
1690
|
+
return {
|
|
1691
|
+
"kind": "artifact-files-mismatch",
|
|
1692
|
+
"summary": (
|
|
1693
|
+
"Fichiers en trop pour une activite documentaire : retirer ceux ecrits "
|
|
1694
|
+
"pendant l'agent, remettre dans leur etat de base ceux modifies avant son "
|
|
1695
|
+
"lancement. Relancer l'agent a l'identique donnerait le meme refus."
|
|
1696
|
+
),
|
|
1697
|
+
"files": files,
|
|
1698
|
+
"commands": commands,
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _attempt_agent(feature_dir: Path, activity: str) -> str:
|
|
1703
|
+
"""Agent de la fiche de tentative, ou '' si elle est illisible."""
|
|
1704
|
+
try:
|
|
1705
|
+
attempt = json.loads(
|
|
1706
|
+
activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
|
|
1707
|
+
)
|
|
1708
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
1709
|
+
return ""
|
|
1710
|
+
agent = attempt.get("agent") if isinstance(attempt, dict) else None
|
|
1711
|
+
return agent if isinstance(agent, str) else ""
|
|
1712
|
+
|
|
1713
|
+
|
|
1510
1714
|
def _print_recovery_steps(recovery: dict) -> None:
|
|
1511
1715
|
# soft_wrap : la commande a copier reste sur une seule ligne.
|
|
1512
1716
|
console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
|
|
1717
|
+
for item in recovery.get("files", []):
|
|
1718
|
+
console.print(
|
|
1719
|
+
f" {item['path']} : {item['label']}", markup=False, soft_wrap=True
|
|
1720
|
+
)
|
|
1513
1721
|
for step in recovery["commands"]:
|
|
1514
1722
|
console.print(
|
|
1515
1723
|
f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
|
|
@@ -1549,6 +1757,24 @@ def _print_run_recovery(
|
|
|
1549
1757
|
" Les entrees du besoin ont change pendant l'execution. "
|
|
1550
1758
|
"Verifiez-les avant une nouvelle tentative."
|
|
1551
1759
|
)
|
|
1760
|
+
if activity in DOCUMENT_ARTIFACTS:
|
|
1761
|
+
console.print(
|
|
1762
|
+
" Activite documentaire : la relance reste refusee tant que l'entree "
|
|
1763
|
+
"n'est pas remise dans son etat de base, ou la tentative abandonnee :",
|
|
1764
|
+
markup=False, soft_wrap=True,
|
|
1765
|
+
)
|
|
1766
|
+
issue, agent_arg = shlex.quote(issue_id), shlex.quote(agent)
|
|
1767
|
+
console.print(
|
|
1768
|
+
f" devflow adaptive discard-attempt {issue} --reason MOTIF"
|
|
1769
|
+
" # garder l'entree modifiee : abandon audite",
|
|
1770
|
+
markup=False, soft_wrap=True,
|
|
1771
|
+
)
|
|
1772
|
+
console.print(
|
|
1773
|
+
f" devflow adaptive run {issue} --agent {agent_arg}"
|
|
1774
|
+
" # apres remise en etat de l'entree, ou apres l'abandon",
|
|
1775
|
+
markup=False, soft_wrap=True,
|
|
1776
|
+
)
|
|
1777
|
+
return
|
|
1552
1778
|
console.print(f" devflow adaptive run {issue_id} --agent {agent}")
|
|
1553
1779
|
return
|
|
1554
1780
|
if reason == "agent-running":
|
devflow_cli/commands/assess.py
CHANGED
|
@@ -16,6 +16,7 @@ from devflow_cli.core.assessment import (
|
|
|
16
16
|
write_assessment,
|
|
17
17
|
)
|
|
18
18
|
from devflow_cli.core.adaptive_workflow import workflow_lock
|
|
19
|
+
from devflow_cli.core.activity_contracts import NEED_STATEMENT, validate_need_statement
|
|
19
20
|
from devflow_cli.core.contract_schemas import check_schema_version
|
|
20
21
|
from devflow_cli.utils.console import console, fail, ok
|
|
21
22
|
from devflow_cli.utils.paths import resolve_feature_dir
|
|
@@ -138,4 +139,13 @@ def assess(
|
|
|
138
139
|
console.print(f" Activites : {' -> '.join(assessment.activities)}")
|
|
139
140
|
console.print(f" Preuves : {', '.join(assessment.requiredEvidence)}")
|
|
140
141
|
console.print(f" Rapport : {output_file}")
|
|
141
|
-
|
|
142
|
+
try:
|
|
143
|
+
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
144
|
+
except ValueError:
|
|
145
|
+
console.print(
|
|
146
|
+
f" Suite : rediger {NEED_STATEMENT} ({feature_dir / NEED_STATEMENT}), "
|
|
147
|
+
f"puis devflow adaptive start {assessment.issueId}",
|
|
148
|
+
markup=False, soft_wrap=True,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
console.print(f" Suite : devflow adaptive start {assessment.issueId}")
|
|
@@ -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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -54,7 +54,9 @@ SUBAGENT_CONSTRAINT = (
|
|
|
54
54
|
PROTECTED_VALIDATION_CONSTRAINT = (
|
|
55
55
|
"Les garde-fous sont actifs : inclure dans validationPlan au moins une validation "
|
|
56
56
|
"bloquante explicitement consacree a la securite, aux donnees, a l'authentification, "
|
|
57
|
-
"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."
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
PREFLIGHT_HEADINGS = (
|
|
@@ -178,6 +180,10 @@ def validate_need_statement(path: Path) -> None:
|
|
|
178
180
|
raise ValueError(f"{NEED_STATEMENT} est vide : decrivez le besoin avant de demarrer")
|
|
179
181
|
|
|
180
182
|
|
|
183
|
+
class PreflightEvidenceStaleError(ValueError):
|
|
184
|
+
"""Preuve de pre-vol anterieure a une entree : motif de refus dedie."""
|
|
185
|
+
|
|
186
|
+
|
|
181
187
|
def validate_preflight_evidence(path: Path, *, newer_than: list[Path] | None = None) -> None:
|
|
182
188
|
"""Validate the V4 preflight artifact without exporting its contents."""
|
|
183
189
|
try:
|
|
@@ -205,7 +211,7 @@ def validate_preflight_evidence(path: Path, *, newer_than: list[Path] | None = N
|
|
|
205
211
|
artifact_mtime = path.stat().st_mtime_ns
|
|
206
212
|
stale_inputs = [item.name for item in newer_than if item.is_file() and item.stat().st_mtime_ns > artifact_mtime]
|
|
207
213
|
if stale_inputs:
|
|
208
|
-
raise
|
|
214
|
+
raise PreflightEvidenceStaleError("preuve de pre-vol desuete apres : " + ", ".join(sorted(stale_inputs)))
|
|
209
215
|
|
|
210
216
|
|
|
211
217
|
def build_activity_contract(
|
|
@@ -6,18 +6,20 @@ 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
|
|
12
13
|
from datetime import datetime, timezone
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import Callable
|
|
15
|
+
from typing import Callable, Iterable
|
|
15
16
|
|
|
16
17
|
from devflow_cli.core.activity_contracts import (
|
|
17
18
|
ActivityContract,
|
|
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,
|
|
@@ -49,6 +51,7 @@ from devflow_cli.core.automatic_evidence import (
|
|
|
49
51
|
review_worktree_fingerprint,
|
|
50
52
|
)
|
|
51
53
|
from devflow_cli.core.adaptive_workflow import (
|
|
54
|
+
AdaptiveWorkflowState,
|
|
52
55
|
complete_activity,
|
|
53
56
|
read_adaptive_state,
|
|
54
57
|
record_evidence,
|
|
@@ -90,6 +93,8 @@ class AdaptiveRunOutcome:
|
|
|
90
93
|
detail: str = ""
|
|
91
94
|
result: ActivityResult | None = None
|
|
92
95
|
warnings: tuple[str, ...] = ()
|
|
96
|
+
# artifact-files-mismatch : (chemin, cause observee) de chaque fichier en trop.
|
|
97
|
+
extraFiles: tuple[tuple[str, str], ...] = ()
|
|
93
98
|
|
|
94
99
|
@property
|
|
95
100
|
def advanced(self) -> bool:
|
|
@@ -142,6 +147,11 @@ def run_current_activity(
|
|
|
142
147
|
preflight_evidence=(feature_dir / "preflight-evidence.md").is_file(),
|
|
143
148
|
files_since_base=since_base,
|
|
144
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
|
+
)
|
|
145
155
|
if state.currentActivity == "implement":
|
|
146
156
|
upstream = [
|
|
147
157
|
feature_dir / Path(item).name
|
|
@@ -155,10 +165,29 @@ def run_current_activity(
|
|
|
155
165
|
return AdaptiveRunOutcome(
|
|
156
166
|
exc.reason, state.currentActivity, state.currentActivity, str(exc)
|
|
157
167
|
)
|
|
168
|
+
except PreflightEvidenceStaleError as exc:
|
|
169
|
+
return AdaptiveRunOutcome(
|
|
170
|
+
"preflight-evidence-stale",
|
|
171
|
+
state.currentActivity,
|
|
172
|
+
state.currentActivity,
|
|
173
|
+
str(exc),
|
|
174
|
+
)
|
|
158
175
|
except (OSError, ValueError) as exc:
|
|
159
176
|
return AdaptiveRunOutcome(
|
|
160
177
|
"unsupported-activity", state.currentActivity, state.currentActivity, str(exc)
|
|
161
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
|
+
)
|
|
162
191
|
raw_file = activity_raw_file(feature_dir, contract.activity)
|
|
163
192
|
guard = ExitStack()
|
|
164
193
|
try:
|
|
@@ -203,8 +232,19 @@ def run_current_activity(
|
|
|
203
232
|
restored = backup.restore() if runtime_mutated else []
|
|
204
233
|
guard.close()
|
|
205
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})
|
|
206
243
|
if runtime_mutated:
|
|
207
|
-
_record_failed_attempt(
|
|
244
|
+
_record_failed_attempt(
|
|
245
|
+
feature_dir, contract, execution, "runtime-file-mutated",
|
|
246
|
+
changed_inputs=attributed_inputs,
|
|
247
|
+
)
|
|
208
248
|
return AdaptiveRunOutcome(
|
|
209
249
|
"runtime-file-mutated",
|
|
210
250
|
contract.activity,
|
|
@@ -214,16 +254,10 @@ def run_current_activity(
|
|
|
214
254
|
+ " ; remis dans leur etat d'avant l'agent : "
|
|
215
255
|
+ (", ".join(restored) if restored else "aucun fichier a restaurer"),
|
|
216
256
|
)
|
|
217
|
-
current_inputs = _input_fingerprints(feature_dir)
|
|
218
|
-
changed_inputs = sorted(
|
|
219
|
-
name
|
|
220
|
-
for name in set(input_fingerprints) | set(current_inputs)
|
|
221
|
-
if input_fingerprints.get(name) != current_inputs.get(name)
|
|
222
|
-
)
|
|
223
257
|
if changed_inputs:
|
|
224
258
|
_record_failed_attempt(
|
|
225
259
|
feature_dir, contract, execution,
|
|
226
|
-
"inputs-changed-during-activity", changed_inputs=
|
|
260
|
+
"inputs-changed-during-activity", changed_inputs=attributed_inputs,
|
|
227
261
|
)
|
|
228
262
|
return AdaptiveRunOutcome(
|
|
229
263
|
"inputs-changed-during-activity", contract.activity, contract.activity,
|
|
@@ -243,7 +277,9 @@ def run_current_activity(
|
|
|
243
277
|
reason = "agent-failed"
|
|
244
278
|
if execution.observedChangedFiles:
|
|
245
279
|
detail += f"; mutations observees : {len(set(execution.observedChangedFiles))} fichier(s)"
|
|
246
|
-
_record_failed_attempt(
|
|
280
|
+
_record_failed_attempt(
|
|
281
|
+
feature_dir, contract, execution, reason, changed_inputs=attributed_inputs
|
|
282
|
+
)
|
|
247
283
|
return AdaptiveRunOutcome(reason, contract.activity, contract.activity, detail)
|
|
248
284
|
|
|
249
285
|
try:
|
|
@@ -252,7 +288,10 @@ def run_current_activity(
|
|
|
252
288
|
outcome = AdaptiveRunOutcome(
|
|
253
289
|
"invalid-result", contract.activity, contract.activity, str(exc)
|
|
254
290
|
)
|
|
255
|
-
_record_failed_attempt(
|
|
291
|
+
_record_failed_attempt(
|
|
292
|
+
feature_dir, contract, execution, outcome.reason,
|
|
293
|
+
changed_inputs=attributed_inputs,
|
|
294
|
+
)
|
|
256
295
|
return outcome
|
|
257
296
|
try:
|
|
258
297
|
observation = observe_activity(
|
|
@@ -273,9 +312,15 @@ def run_current_activity(
|
|
|
273
312
|
observation,
|
|
274
313
|
source="agent",
|
|
275
314
|
agent=execution.agent,
|
|
315
|
+
execution_files=execution.observedChangedFiles,
|
|
316
|
+
imputed_inputs=imputed_inputs,
|
|
317
|
+
closed_activities=_closed_activities(state),
|
|
276
318
|
)
|
|
277
319
|
if not outcome.advanced and outcome.reason not in {"blocked", "state-changed"}:
|
|
278
|
-
_record_failed_attempt(
|
|
320
|
+
_record_failed_attempt(
|
|
321
|
+
feature_dir, contract, execution, outcome.reason,
|
|
322
|
+
changed_inputs=attributed_inputs,
|
|
323
|
+
)
|
|
279
324
|
return outcome
|
|
280
325
|
|
|
281
326
|
|
|
@@ -337,15 +382,38 @@ def reconcile_current_activity(
|
|
|
337
382
|
raise ValueError("le resultat corrige doit etre un objet JSON")
|
|
338
383
|
if not correction_reason.strip():
|
|
339
384
|
raise ValueError("un motif est obligatoire pour corriger le resultat")
|
|
340
|
-
|
|
341
|
-
activity_attempt_file(feature_dir, activity).read_text(
|
|
342
|
-
|
|
385
|
+
try:
|
|
386
|
+
attempt_text = activity_attempt_file(feature_dir, activity).read_text(
|
|
387
|
+
encoding="utf-8"
|
|
388
|
+
)
|
|
389
|
+
except FileNotFoundError:
|
|
390
|
+
raise ValueError(
|
|
391
|
+
f"aucune tentative echouee a reconcilier pour '{activity}' : "
|
|
392
|
+
"reconcile ne s'applique qu'a une tentative agent echouee ; apres "
|
|
393
|
+
"un resultat blocked, lever le blocage puis relancer "
|
|
394
|
+
f"devflow adaptive run {shlex.quote(state.issueId)}"
|
|
395
|
+
) from None
|
|
396
|
+
attempt = json.loads(attempt_text)
|
|
343
397
|
if not _valid_attempt(attempt):
|
|
344
398
|
raise ValueError("fiche de tentative invalide")
|
|
345
399
|
agent = attempt["agent"]
|
|
346
400
|
attempt_reason = attempt["reason"]
|
|
347
401
|
observed_files = attempt["observedFiles"]
|
|
348
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
|
+
)
|
|
349
417
|
if attempt_reason == "inputs-changed-during-activity":
|
|
350
418
|
raise ValueError(
|
|
351
419
|
"les entrees ont change pendant la tentative ; inspectez-les "
|
|
@@ -365,10 +433,18 @@ def reconcile_current_activity(
|
|
|
365
433
|
)
|
|
366
434
|
if head != attempt["head"] or branch != attempt["branch"]:
|
|
367
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
|
+
)
|
|
368
443
|
drifted = sorted(
|
|
369
444
|
path
|
|
370
445
|
for path in set(observed_files) | set(since_base)
|
|
371
446
|
if observed_files.get(path) != since_base.get(path)
|
|
447
|
+
and (artifact is None or path == artifact or path in since_base)
|
|
372
448
|
)
|
|
373
449
|
if drifted:
|
|
374
450
|
raise ValueError(
|
|
@@ -449,6 +525,8 @@ def reconcile_current_activity(
|
|
|
449
525
|
observation,
|
|
450
526
|
source="reconciled",
|
|
451
527
|
agent=agent,
|
|
528
|
+
execution_files=execution_files,
|
|
529
|
+
closed_activities=_closed_activities(state),
|
|
452
530
|
)
|
|
453
531
|
if orphan_warning:
|
|
454
532
|
outcome = replace(outcome, warnings=(orphan_warning, *outcome.warnings))
|
|
@@ -528,12 +606,17 @@ def _apply_activity_result(
|
|
|
528
606
|
*,
|
|
529
607
|
source: str,
|
|
530
608
|
agent: str,
|
|
609
|
+
execution_files: Iterable[str] = (),
|
|
610
|
+
imputed_inputs: Iterable[str] = (),
|
|
611
|
+
closed_activities: Iterable[str] = (),
|
|
531
612
|
) -> AdaptiveRunOutcome:
|
|
532
613
|
"""Valide un resultat agent puis transitionne ; commun a run et reconcile.
|
|
533
614
|
|
|
534
615
|
L'observation fait foi : ``O`` (fichiers depuis la base) et ``E`` (fichiers
|
|
535
616
|
modifies pendant l'agent) fondent les refus ; la declaration de l'agent ne
|
|
536
|
-
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.
|
|
537
620
|
"""
|
|
538
621
|
state_file = feature_dir / "workflow-state.json"
|
|
539
622
|
observed = sorted(observation.sinceBase)
|
|
@@ -558,13 +641,35 @@ def _apply_activity_result(
|
|
|
558
641
|
"invalid-result", contract.activity, contract.activity, str(exc)
|
|
559
642
|
)
|
|
560
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]
|
|
561
659
|
declared = sorted(set(result.changedFiles))
|
|
562
|
-
|
|
563
|
-
warnings = (
|
|
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
|
+
)
|
|
564
666
|
|
|
565
|
-
def refuse(
|
|
667
|
+
def refuse(
|
|
668
|
+
reason: str, detail: str, extra_files: tuple[tuple[str, str], ...] = ()
|
|
669
|
+
) -> AdaptiveRunOutcome:
|
|
566
670
|
return AdaptiveRunOutcome(
|
|
567
|
-
reason, contract.activity, contract.activity, detail, result, warnings
|
|
671
|
+
reason, contract.activity, contract.activity, detail, result, warnings,
|
|
672
|
+
extra_files,
|
|
568
673
|
)
|
|
569
674
|
|
|
570
675
|
if contract.activity == "implement" and result.status == "completed" and not observed:
|
|
@@ -576,11 +681,22 @@ def _apply_activity_result(
|
|
|
576
681
|
contract.activity, reopened=contract_is_reopened_plan(contract)
|
|
577
682
|
)
|
|
578
683
|
expected = f"{feature_path}/{artifact_name}"
|
|
579
|
-
if any(path != expected for path in
|
|
580
|
-
result.status == "completed" and
|
|
684
|
+
if any(path != expected for path in counted) or (
|
|
685
|
+
result.status == "completed" and counted != [expected]
|
|
581
686
|
):
|
|
582
687
|
return refuse(
|
|
583
|
-
"artifact-files-mismatch",
|
|
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
|
+
),
|
|
584
700
|
)
|
|
585
701
|
if contract.activity in DOCUMENT_ARTIFACTS and result.status == "completed":
|
|
586
702
|
artifact_name = document_artifact_name(
|
|
@@ -624,7 +740,9 @@ def _apply_activity_result(
|
|
|
624
740
|
|
|
625
741
|
recorded = result.to_dict()
|
|
626
742
|
recorded["changedFiles"] = observed
|
|
627
|
-
recorded["observation"] = recorded_observation(
|
|
743
|
+
recorded["observation"] = recorded_observation(
|
|
744
|
+
observation, declared, exempted=amended, amendment_warning=amendment
|
|
745
|
+
)
|
|
628
746
|
_write_json_atomic(activity_result_file(feature_dir, contract.activity), recorded)
|
|
629
747
|
if result.status == "blocked":
|
|
630
748
|
safe_refresh_worklog(feature_dir)
|
|
@@ -733,6 +851,17 @@ def _validated_result(
|
|
|
733
851
|
raise ValueError(
|
|
734
852
|
f"validation {item.id}, commande : {command_label} — {exc}"
|
|
735
853
|
) from exc
|
|
854
|
+
if contract.guardrails:
|
|
855
|
+
for item in result.validationPlan:
|
|
856
|
+
if is_protected_validation(item.id, item.purpose) and not item.blocking:
|
|
857
|
+
raise ValueError(
|
|
858
|
+
f"validation {item.id} : un controle protege (securite, "
|
|
859
|
+
"donnees, authentification, migration, rollback) est toujours "
|
|
860
|
+
"bloquant et doit reussir en code 0 ; declarer blocking a true. "
|
|
861
|
+
"Une recherche dont l'absence de correspondance est le resultat "
|
|
862
|
+
"attendu ne s'exprime pas ainsi : utiliser une commande qui "
|
|
863
|
+
"renvoie 0 en cas de succes."
|
|
864
|
+
)
|
|
736
865
|
if contract.guardrails and not any(
|
|
737
866
|
item.blocking and is_protected_validation(item.id, item.purpose)
|
|
738
867
|
for item in result.validationPlan
|
|
@@ -903,15 +1032,22 @@ def _record_failed_attempt(
|
|
|
903
1032
|
|
|
904
1033
|
``executionFiles`` est la liste brute des fichiers modifies pendant
|
|
905
1034
|
l'agent, non filtree. ``observedFiles`` vaut les fichiers depuis la base de l'activite ; si
|
|
906
|
-
l'observation est impossible,
|
|
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).
|
|
907
1038
|
"""
|
|
908
1039
|
try:
|
|
909
1040
|
state = read_adaptive_state(feature_dir / "workflow-state.json")
|
|
910
|
-
if state is None:
|
|
911
|
-
return
|
|
912
|
-
head, branch, observed = observe_attempt(feature_dir, state)
|
|
913
1041
|
except (OSError, ValueError):
|
|
914
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 = "", "", {}
|
|
915
1051
|
if changed_inputs:
|
|
916
1052
|
input_paths = {f"{contract.featurePath}/{name}" for name in changed_inputs}
|
|
917
1053
|
observed = {
|
|
@@ -937,6 +1073,74 @@ def _record_failed_attempt(
|
|
|
937
1073
|
_write_json_atomic(activity_attempt_file(feature_dir, contract.activity), payload)
|
|
938
1074
|
|
|
939
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
|
+
|
|
940
1144
|
def _input_fingerprints(feature_dir: Path) -> dict[str, str]:
|
|
941
1145
|
"""Empreinte les entrees modifiables sans conserver leur contenu."""
|
|
942
1146
|
paths = [feature_dir / NEED_STATEMENT, *sorted(feature_dir.glob("issue-*.md"))]
|
|
@@ -22,7 +22,13 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
22
22
|
workflow_lock,
|
|
23
23
|
write_adaptive_state,
|
|
24
24
|
)
|
|
25
|
-
from devflow_cli.core.activity_base import
|
|
25
|
+
from devflow_cli.core.activity_base import (
|
|
26
|
+
ActivityBase,
|
|
27
|
+
BaseObservationError,
|
|
28
|
+
enter_current_activity,
|
|
29
|
+
files_since_base,
|
|
30
|
+
parse_recorded_result,
|
|
31
|
+
)
|
|
26
32
|
from devflow_cli.core.activity_contracts import (
|
|
27
33
|
DOCUMENT_ARTIFACTS,
|
|
28
34
|
activity_contract_id,
|
|
@@ -297,6 +303,38 @@ DOCUMENT_INSENSITIVE_EVIDENCE_IDS = frozenset({"test-suite", "requirements-cover
|
|
|
297
303
|
NON_DOCUMENT_FINGERPRINT_PREFIX = "non-document-v1:"
|
|
298
304
|
|
|
299
305
|
|
|
306
|
+
def is_document_path(path: str) -> bool:
|
|
307
|
+
"""Vrai pour un README ou un fichier Markdown du dossier docs."""
|
|
308
|
+
return Path(path).name.casefold() == "readme.md" or (
|
|
309
|
+
path.startswith("docs/") and path.casefold().endswith(".md")
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _observed_document_batch(
|
|
314
|
+
state: AdaptiveWorkflowState, feature_dir: Path, project_root: Path
|
|
315
|
+
) -> list[str] | None:
|
|
316
|
+
"""Fichiers observes depuis la base d'implement si tous sont documentaires.
|
|
317
|
+
|
|
318
|
+
None quand le lot n'est pas eligible : base inobservable, lot vide, fichier
|
|
319
|
+
non documentaire ou lien symbolique. L'observation decide, pas l'agent.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
base = ActivityBase.from_dict(
|
|
323
|
+
state.implementBase,
|
|
324
|
+
feature_path=feature_dir.resolve().relative_to(project_root.resolve()).as_posix(),
|
|
325
|
+
)
|
|
326
|
+
observed = sorted(
|
|
327
|
+
files_since_base(project_root, feature_dir, base, issue_id=state.issueId)
|
|
328
|
+
)
|
|
329
|
+
except (BaseObservationError, ValueError, OSError):
|
|
330
|
+
return None
|
|
331
|
+
if not observed or not all(is_document_path(path) for path in observed):
|
|
332
|
+
return None
|
|
333
|
+
if any((project_root / path).is_symlink() for path in observed):
|
|
334
|
+
return None
|
|
335
|
+
return observed
|
|
336
|
+
|
|
337
|
+
|
|
300
338
|
def _non_document_fingerprint(snapshot: WorktreeSnapshot, excluded_dir: str) -> str:
|
|
301
339
|
"""Empreinte des sources, sans README ni Markdown du dossier docs.
|
|
302
340
|
|
|
@@ -306,10 +344,7 @@ def _non_document_fingerprint(snapshot: WorktreeSnapshot, excluded_dir: str) ->
|
|
|
306
344
|
source_files = {
|
|
307
345
|
path: fingerprint
|
|
308
346
|
for path, fingerprint in snapshot.files.items()
|
|
309
|
-
if not (
|
|
310
|
-
Path(path).name.casefold() == "readme.md"
|
|
311
|
-
or (path.startswith("docs/") and path.casefold().endswith(".md"))
|
|
312
|
-
)
|
|
347
|
+
if not is_document_path(path)
|
|
313
348
|
}
|
|
314
349
|
return NON_DOCUMENT_FINGERPRINT_PREFIX + worktree_fingerprint(
|
|
315
350
|
replace(snapshot, files=source_files), excluded_dir
|
|
@@ -562,7 +597,10 @@ def run_automatic_verification(
|
|
|
562
597
|
# par run_verifications avant leur execution locale par devflow.
|
|
563
598
|
commands = regression_commands
|
|
564
599
|
reuse_targeted_as_regression = True
|
|
565
|
-
|
|
600
|
+
document_batch: list[str] | None = None
|
|
601
|
+
if not commands and state.profile == "quick" and not guardrails_active(state):
|
|
602
|
+
document_batch = _observed_document_batch(state, feature_dir, project_root)
|
|
603
|
+
if not commands and document_batch is None:
|
|
566
604
|
return AutomaticVerificationOutcome(
|
|
567
605
|
False,
|
|
568
606
|
"no-safe-validation",
|
|
@@ -661,7 +699,7 @@ def run_automatic_verification(
|
|
|
661
699
|
break
|
|
662
700
|
return AutomaticVerificationOutcome(False, "validation-policy-refused", detail)
|
|
663
701
|
completed_at = _now_iso()
|
|
664
|
-
checks_passed = bool(results) and all(
|
|
702
|
+
checks_passed = (bool(results) or document_batch is not None) and all(
|
|
665
703
|
result.succeeded or not result.blocking for result in results
|
|
666
704
|
)
|
|
667
705
|
regression_passed = all(
|
|
@@ -673,7 +711,9 @@ def run_automatic_verification(
|
|
|
673
711
|
and all(result.succeeded for result in security_results)
|
|
674
712
|
and not security_findings
|
|
675
713
|
)
|
|
676
|
-
diff_payload = _inspect_diff(
|
|
714
|
+
diff_payload = _inspect_diff(
|
|
715
|
+
project_root, document_batch if document_batch is not None else changed_files
|
|
716
|
+
)
|
|
677
717
|
final_snapshot = snapshot_worktree(project_root)
|
|
678
718
|
try:
|
|
679
719
|
test_mutations = changed_paths(before, final_snapshot)
|
|
@@ -883,7 +923,15 @@ def run_automatic_verification(
|
|
|
883
923
|
write_adaptive_state(state_file, latest)
|
|
884
924
|
next_activity = latest.currentActivity
|
|
885
925
|
safe_refresh_worklog(feature_dir)
|
|
886
|
-
|
|
926
|
+
detail = (
|
|
927
|
+
"lot documentaire, aucun controle executable : verification sur "
|
|
928
|
+
"inspection du diff (" + ", ".join(document_batch) + ")"
|
|
929
|
+
if document_batch is not None
|
|
930
|
+
else ""
|
|
931
|
+
)
|
|
932
|
+
return AutomaticVerificationOutcome(
|
|
933
|
+
True, "passed", detail, nextActivity=next_activity
|
|
934
|
+
)
|
|
887
935
|
|
|
888
936
|
|
|
889
937
|
def _failures(results: list[VerificationResult]) -> tuple[VerificationFailure, ...]:
|
devflow_cli/core/waivers.py
CHANGED
|
@@ -203,10 +203,51 @@ def _find_failed_command(
|
|
|
203
203
|
if not matches:
|
|
204
204
|
raise ValueError(f"aucun echec execute trouve pour {command_id}")
|
|
205
205
|
if len(matches) > 1:
|
|
206
|
-
|
|
206
|
+
return _select_duplicated_failure(command_id, matches)
|
|
207
207
|
return matches[0]
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
_TIE_BREAK_ORDER = {"targeted-tests": 0, "regression-check": 1}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _select_duplicated_failure(
|
|
214
|
+
command_id: str, matches: list[tuple[dict[str, Any], dict[str, Any]]]
|
|
215
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
216
|
+
"""Retient la preuve la plus récente si toutes les occurrences sont cohérentes."""
|
|
217
|
+
names = sorted(str(payload.get("id")) for payload, _ in matches)
|
|
218
|
+
protected = [name for name in names if name in PROTECTED_EVIDENCE]
|
|
219
|
+
if protected:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"validation {command_id} presente dans une preuve protegee "
|
|
222
|
+
f"non derogable : {', '.join(protected)}"
|
|
223
|
+
)
|
|
224
|
+
reference = matches[0][1]
|
|
225
|
+
return_code = reference.get("returncode")
|
|
226
|
+
consistent = isinstance(return_code, int) and return_code != 0
|
|
227
|
+
for _, command in matches:
|
|
228
|
+
consistent = consistent and (
|
|
229
|
+
command.get("command") == reference.get("command")
|
|
230
|
+
and command.get("workingDirectory", ".") == reference.get("workingDirectory", ".")
|
|
231
|
+
and command.get("blocking") is True
|
|
232
|
+
and command.get("succeeded") is False
|
|
233
|
+
and command.get("timedOut") is not True
|
|
234
|
+
and command.get("returncode") == return_code
|
|
235
|
+
)
|
|
236
|
+
if not consistent:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"validation {command_id} presente dans plusieurs preuves "
|
|
239
|
+
f"({', '.join(names)}) avec des commandes, dossiers ou resultats differents"
|
|
240
|
+
)
|
|
241
|
+
ordered = sorted(
|
|
242
|
+
matches,
|
|
243
|
+
key=lambda match: (
|
|
244
|
+
_TIE_BREAK_ORDER.get(str(match[0].get("id")), len(_TIE_BREAK_ORDER)),
|
|
245
|
+
str(match[0].get("id")),
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
return max(ordered, key=lambda match: str(match[0].get("completedAt") or ""))
|
|
249
|
+
|
|
250
|
+
|
|
210
251
|
def _read_registry(path: Path, issue_id: str) -> dict[str, Any]:
|
|
211
252
|
if not path.is_file():
|
|
212
253
|
return {
|
devflow_cli/core/worklog.py
CHANGED
|
@@ -288,6 +288,16 @@ def _verification_lines(
|
|
|
288
288
|
f"`{waiver.get('id', 'inconnue')}` sur `{waiver.get('commandId', 'inconnu')}` "
|
|
289
289
|
f"par {waiver.get('author', 'inconnu')} : {waiver.get('reason', 'sans justification')}"
|
|
290
290
|
)
|
|
291
|
+
if (
|
|
292
|
+
targeted
|
|
293
|
+
and targeted.get("id") == "targeted-check"
|
|
294
|
+
and targeted.get("outcome") == "passed"
|
|
295
|
+
and commands == []
|
|
296
|
+
):
|
|
297
|
+
lines.append(
|
|
298
|
+
" - Lot documentaire, aucun contrôle exécutable : "
|
|
299
|
+
"vérification sur inspection du diff."
|
|
300
|
+
)
|
|
291
301
|
return lines or ["- Vérification non exécutée."]
|
|
292
302
|
|
|
293
303
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devflow-cli
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.4.1
|
|
4
4
|
Summary: Spec-Driven Development workflow CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/sopequenoteck/devflow
|
|
6
6
|
Project-URL: Repository, https://github.com/sopequenoteck/devflow
|
|
@@ -45,7 +45,8 @@ Le profil fixe la route documentaire :
|
|
|
45
45
|
|
|
46
46
|
Un bloc de garde-fous s'y ajoute quand `--data-security` est déclaré ou que
|
|
47
47
|
`--risk high` : `risk-assessment` après l'évaluation, validation bloquante de
|
|
48
|
-
sécurité ou de données dans le plan de validation
|
|
48
|
+
sécurité ou de données dans le plan de validation (une validation protégée non
|
|
49
|
+
bloquante est refusée dès `prepare` ou `plan`), preuve
|
|
49
50
|
`security-or-data-check` produite par `verify`, revue `independent-review`,
|
|
50
51
|
puis `rollback-check` avec `rollback-plan`. Un bloc actif impose au minimum le
|
|
51
52
|
profil `standard`, ne se désactive par aucune option et interdit les
|
|
@@ -90,8 +91,11 @@ devflow adaptive status KS-123
|
|
|
90
91
|
distinguent explicitement blocage métier, contrat invalide, panne agent,
|
|
91
92
|
validation échouée et conflit concurrent ; chaque refus affiche la commande de
|
|
92
93
|
reprise recommandée. Après un refus de `verify`, cette reprise dépend du motif :
|
|
93
|
-
approbations à donner, environnement à corriger, ou
|
|
94
|
-
(`implement` si le code est en cause, `prepare`
|
|
94
|
+
approbations à donner, environnement à corriger, ou trois voies après un
|
|
95
|
+
contrôle échoué (rouvrir `implement` si le code est en cause, rouvrir `prepare`
|
|
96
|
+
ou `plan` si la validation l'est, relancer `verify` après un échec passager).
|
|
97
|
+
En `quick`, un lot purement documentaire (`README.md`, Markdown sous `docs/`)
|
|
98
|
+
sans contrôle détecté franchit `verify` sur l'inspection du diff.
|
|
95
99
|
|
|
96
100
|
Un plan de `prepare` ou de `plan` refusé par les validateurs se corrige sans
|
|
97
101
|
relancer l'agent : `devflow adaptive reconcile KS-123 --result FICHIER.json
|
|
@@ -112,6 +116,22 @@ nomme la cause (`structured-output-missing`) et propose
|
|
|
112
116
|
`devflow adaptive reconcile KS-123 --result MODELE --reason MOTIF` avec un
|
|
113
117
|
modèle de résultat prérempli, écrit dans `~/.devflow/reconcile/` et à compléter.
|
|
114
118
|
|
|
119
|
+
`need.md` peut être amendé en cours de parcours. Avant une activité
|
|
120
|
+
documentaire, l'amendement est accepté avec un avertissement qui nomme les
|
|
121
|
+
activités déjà closes, à rouvrir si leur résultat en dépend ; avant `prepare`,
|
|
122
|
+
il est accepté sans cet avertissement ; pendant l'exécution d'un agent, il est refusé ;
|
|
123
|
+
avant `implement`, il périme la preuve de pré-vol ; après `implement`, il
|
|
124
|
+
périme une revue déjà faite. Pour arbitrer après un blocage d'`implement` :
|
|
125
|
+
amender `need.md`, puis `devflow adaptive reopen KS-123 preflight-evidence
|
|
126
|
+
--reason MOTIF`, refaire le pré-vol et relancer `implement`. Un refus
|
|
127
|
+
d'artefact (`artifact-files-mismatch`) nomme chaque fichier en trop et sa
|
|
128
|
+
cause : retirer ou remettre en état le fichier, puis
|
|
129
|
+
`devflow adaptive reconcile KS-123` ; la relance à l'identique n'est plus
|
|
130
|
+
proposée. Pour une entrée du besoin modifiée par un agent, la relance est
|
|
131
|
+
refusée avant le lancement de l'agent : remettre l'entrée en état, ou
|
|
132
|
+
`devflow adaptive discard-attempt KS-123 --reason MOTIF`, puis
|
|
133
|
+
`devflow adaptive run KS-123`.
|
|
134
|
+
|
|
115
135
|
Chaque invocation de `adaptive run` traite une activité. La politique est dans
|
|
116
136
|
`assessment.json`; la progression et les preuves sont dans
|
|
117
137
|
`workflow-state.json`. Les résultats agent sont validés par schéma et les
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
devflow_cli/__init__.py,sha256=
|
|
1
|
+
devflow_cli/__init__.py,sha256=MQEclHX75ybzzUEyJ3iAVMLUfzOpBzaQ_E5gi0d-TUc,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=
|
|
8
|
-
devflow_cli/commands/assess.py,sha256=
|
|
7
|
+
devflow_cli/commands/adaptive.py,sha256=5D8K4aevxMlehsE65wnEn6ny6g_4A1Bes_OBVIUrbSM,71520
|
|
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
|
|
@@ -23,14 +23,14 @@ 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=
|
|
27
|
-
devflow_cli/core/activity_contracts.py,sha256=
|
|
28
|
-
devflow_cli/core/adaptive_runner.py,sha256=
|
|
26
|
+
devflow_cli/core/activity_base.py,sha256=YId_tUxiL6TYmnVpG2VYGkA31nPjIrskx6Dof27XR6E,16696
|
|
27
|
+
devflow_cli/core/activity_contracts.py,sha256=n9arBw-SWEFqTlCDNed6lV4WolkZKAvmQg1qcfk57ss,39036
|
|
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=
|
|
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
|
|
@@ -52,8 +52,8 @@ devflow_cli/core/traceability.py,sha256=jfNE9MZzGkmaPer9Wa6Y18yA9Y3sqdeR-nzH9h_T
|
|
|
52
52
|
devflow_cli/core/validation_approvals.py,sha256=3AHOpnW3BtY3BQrRZr38K7VNYRvbxGNhmQRYAufyhSA,9279
|
|
53
53
|
devflow_cli/core/validators.py,sha256=fREjEx2fBW36q3vB6WJNqoGUM9ejYlQUXie5A9lNuPs,7440
|
|
54
54
|
devflow_cli/core/verification.py,sha256=rvbdAdLyrZYb20oauti7D8nV5TDFBIf_8-hqLahfRag,23576
|
|
55
|
-
devflow_cli/core/waivers.py,sha256=
|
|
56
|
-
devflow_cli/core/worklog.py,sha256=
|
|
55
|
+
devflow_cli/core/waivers.py,sha256=9XXzMmoWCn-2Dc3486WhZwtGJQLQPXjP0qAaLuL_3qU,13299
|
|
56
|
+
devflow_cli/core/worklog.py,sha256=GkaXCsx22jntcWuC75OystUoqb5AlzBbXXLYH581Mwg,14164
|
|
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
|
|
@@ -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.
|
|
94
|
-
devflow_cli-8.
|
|
95
|
-
devflow_cli-8.
|
|
96
|
-
devflow_cli-8.
|
|
97
|
-
devflow_cli-8.
|
|
93
|
+
devflow_cli-8.4.1.dist-info/METADATA,sha256=Z1ZlFp1ND90t29jlf-ofmvs_JOnZUOk7KMJB8M-HmEA,9846
|
|
94
|
+
devflow_cli-8.4.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
95
|
+
devflow_cli-8.4.1.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
|
|
96
|
+
devflow_cli-8.4.1.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
|
|
97
|
+
devflow_cli-8.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|