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 +1 -1
- devflow_cli/commands/adaptive.py +227 -31
- devflow_cli/core/activity_base.py +16 -4
- devflow_cli/core/activity_contracts.py +12 -5
- devflow_cli/core/adaptive_runner.py +200 -25
- devflow_cli/core/automatic_evidence.py +2 -1
- devflow_cli/core/contract_schemas.py +12 -4
- devflow_cli/core/feedback.py +1 -1
- devflow_cli/core/installer.py +1 -0
- devflow_cli/core/worklog.py +21 -9
- devflow_cli/resources/commands/devflow.md +10 -6
- devflow_cli/resources/templates/constitution-template.md +5 -5
- devflow_cli/resources/templates/plan-template.md +3 -3
- {devflow_cli-8.3.0.dist-info → devflow_cli-8.5.0.dist-info}/METADATA +19 -2
- {devflow_cli-8.3.0.dist-info → devflow_cli-8.5.0.dist-info}/RECORD +18 -18
- {devflow_cli-8.3.0.dist-info → devflow_cli-8.5.0.dist-info}/WHEEL +0 -0
- {devflow_cli-8.3.0.dist-info → devflow_cli-8.5.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.3.0.dist-info → devflow_cli-8.5.0.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,
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
@@ -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 ;
|
|
559
|
-
"
|
|
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(
|
|
864
|
+
raise ValueError(_EVIDENCE_SHAPE_MESSAGE)
|
|
858
865
|
if set(item) != {"id", "outcome", "report"}:
|
|
859
|
-
raise ValueError(
|
|
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(
|
|
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(
|
|
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=
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
581
|
-
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
|
+
)
|
|
582
666
|
|
|
583
|
-
def refuse(
|
|
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
|
|
598
|
-
result.status == "completed" and
|
|
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",
|
|
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(
|
|
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,
|
|
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("
|
|
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 == "
|
|
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
|
|
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
|
|
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
|
|
devflow_cli/core/feedback.py
CHANGED
|
@@ -442,7 +442,7 @@ def render_export(
|
|
|
442
442
|
treated: dict[str, dict[str, Any]],
|
|
443
443
|
home: Path,
|
|
444
444
|
) -> str:
|
|
445
|
-
"""Markdown
|
|
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.", ""]
|
devflow_cli/core/installer.py
CHANGED
devflow_cli/core/worklog.py
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
16
|
-
`devflow feature ISSUE_ID
|
|
17
|
-
|
|
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
|
-
|
|
23
|
+
6. Si elle est évaluée mais non démarrée, lancer
|
|
20
24
|
`devflow adaptive start ISSUE_ID`.
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
94
|
-
devflow_cli-8.
|
|
95
|
-
devflow_cli-8.
|
|
96
|
-
devflow_cli-8.
|
|
97
|
-
devflow_cli-8.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|