devflow-cli 6.4.0__py3-none-any.whl → 7.0.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 -3
- devflow_cli/cli.py +15 -0
- devflow_cli/commands/adaptive.py +33 -35
- devflow_cli/commands/assess.py +26 -2
- devflow_cli/commands/status.py +20 -0
- devflow_cli/core/activity_contracts.py +91 -163
- devflow_cli/core/adaptive_runner.py +11 -24
- devflow_cli/core/adaptive_workflow.py +65 -101
- devflow_cli/core/assessment.py +152 -112
- devflow_cli/core/automatic_evidence.py +14 -16
- devflow_cli/core/contract_schemas.py +39 -15
- devflow_cli/core/docgen.py +32 -15
- devflow_cli/core/feedback.py +10 -1
- devflow_cli/core/remediation.py +8 -11
- devflow_cli/core/staleness.py +1 -3
- devflow_cli/core/validation_approvals.py +1 -1
- devflow_cli/core/validators.py +1 -66
- devflow_cli/core/waivers.py +8 -1
- devflow_cli/core/worklog.py +0 -11
- devflow_cli/resources/templates/plan-template.md +0 -1
- devflow_cli/resources/templates/quickstart-template.md +0 -1
- devflow_cli/schemas/{activity-contract-v3.schema.json → activity-contract-v4.schema.json} +7 -6
- devflow_cli/schemas/activity-result-v1.schema.json +0 -10
- devflow_cli/schemas/{assessment-v3.schema.json → assessment-v4.schema.json} +19 -7
- devflow_cli/schemas/{workflow-state-v4.schema.json → workflow-state-v5.schema.json} +13 -23
- {devflow_cli-6.4.0.dist-info → devflow_cli-7.0.0.dist-info}/METADATA +26 -5
- {devflow_cli-6.4.0.dist-info → devflow_cli-7.0.0.dist-info}/RECORD +30 -32
- devflow_cli/core/omissions.py +0 -168
- devflow_cli/resources/templates/research-template.md +0 -51
- {devflow_cli-6.4.0.dist-info → devflow_cli-7.0.0.dist-info}/WHEEL +0 -0
- {devflow_cli-6.4.0.dist-info → devflow_cli-7.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-6.4.0.dist-info → devflow_cli-7.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""devflow CLI — workflow de développement adaptatif."""
|
|
2
2
|
|
|
3
|
-
VERSION = "
|
|
3
|
+
VERSION = "7.0.0"
|
|
4
4
|
|
|
5
5
|
SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
|
|
6
6
|
|
|
@@ -9,7 +9,6 @@ ACTIVITY_LINEAR_MAPPING = {
|
|
|
9
9
|
"prepare": "Plan",
|
|
10
10
|
"risk-assessment": "Research",
|
|
11
11
|
"spec": "Spec",
|
|
12
|
-
"research": "Research",
|
|
13
12
|
"contracts": "Plan",
|
|
14
13
|
"plan": "Plan",
|
|
15
14
|
"tasks": "Tasks",
|
|
@@ -35,7 +34,6 @@ LINEAR_STATUS_ORDER = [
|
|
|
35
34
|
ARTIFACTS = [
|
|
36
35
|
"risk-assessment.md",
|
|
37
36
|
"spec.md",
|
|
38
|
-
"research.md",
|
|
39
37
|
"contracts.md",
|
|
40
38
|
"plan.md",
|
|
41
39
|
"tasks.md",
|
devflow_cli/cli.py
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
+
from typer.core import TyperGroup
|
|
6
7
|
|
|
7
8
|
from devflow_cli import VERSION
|
|
9
|
+
from devflow_cli.core.contract_schemas import LegacySchemaError
|
|
10
|
+
from devflow_cli.utils.console import fail
|
|
8
11
|
from devflow_cli.utils.logging import setup_logging
|
|
9
12
|
from devflow_cli.commands.init_cmd import init
|
|
10
13
|
from devflow_cli.commands.check import check
|
|
@@ -25,10 +28,22 @@ from devflow_cli.commands.verify import verify
|
|
|
25
28
|
from devflow_cli.commands.assess import assess
|
|
26
29
|
from devflow_cli.commands.usage import usage
|
|
27
30
|
|
|
31
|
+
class DevflowGroup(TyperGroup):
|
|
32
|
+
"""Affiche un refus actionnable, sans trace, pour un fichier de feature 6.x."""
|
|
33
|
+
|
|
34
|
+
def invoke(self, ctx):
|
|
35
|
+
try:
|
|
36
|
+
return super().invoke(ctx)
|
|
37
|
+
except LegacySchemaError as exc:
|
|
38
|
+
fail(str(exc))
|
|
39
|
+
raise typer.Exit(1) from exc
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
app = typer.Typer(
|
|
29
43
|
name="devflow",
|
|
30
44
|
help="Spec-Driven Development workflow CLI",
|
|
31
45
|
no_args_is_help=True,
|
|
46
|
+
cls=DevflowGroup,
|
|
32
47
|
)
|
|
33
48
|
|
|
34
49
|
|
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -23,7 +23,6 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
23
23
|
workflow_lock,
|
|
24
24
|
write_adaptive_state,
|
|
25
25
|
)
|
|
26
|
-
from devflow_cli.core.omissions import omit_current_activity
|
|
27
26
|
from devflow_cli.core.assessment import read_assessment
|
|
28
27
|
from devflow_cli.core.activity_contracts import (
|
|
29
28
|
build_activity_contract,
|
|
@@ -60,7 +59,7 @@ from devflow_cli.core.validation_approvals import (
|
|
|
60
59
|
approve_plan_command,
|
|
61
60
|
pending_plan_approval,
|
|
62
61
|
)
|
|
63
|
-
from devflow_cli.core.contract_schemas import diagnose_contract_file
|
|
62
|
+
from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
|
|
64
63
|
from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
|
|
65
64
|
from devflow_cli.adapters.agents import get_agent_adapter
|
|
66
65
|
from devflow_cli.utils.console import console, fail, ok, warn
|
|
@@ -74,11 +73,12 @@ from devflow_cli.utils.paths import (
|
|
|
74
73
|
|
|
75
74
|
app = typer.Typer(help="Machine d'etat des parcours adaptatifs")
|
|
76
75
|
|
|
76
|
+
LEGACY_SCHEMA_EXIT_CODE = 1
|
|
77
|
+
|
|
77
78
|
RESULT_VALIDATED_ACTIVITIES = frozenset({
|
|
78
79
|
"prepare",
|
|
79
80
|
"risk-assessment",
|
|
80
81
|
"spec",
|
|
81
|
-
"research",
|
|
82
82
|
"contracts",
|
|
83
83
|
"plan",
|
|
84
84
|
"tasks",
|
|
@@ -105,7 +105,17 @@ def _state(
|
|
|
105
105
|
feature_dir: Path, *, json_output: bool = False, issue_id: str | None = None
|
|
106
106
|
):
|
|
107
107
|
state_file = feature_dir / "workflow-state.json"
|
|
108
|
-
|
|
108
|
+
try:
|
|
109
|
+
state = read_adaptive_state(state_file)
|
|
110
|
+
except LegacySchemaError as exc:
|
|
111
|
+
if json_output:
|
|
112
|
+
_print_execution_json(
|
|
113
|
+
issue_id or feature_dir.name, None, False, "legacy-schema", str(exc),
|
|
114
|
+
None, LEGACY_SCHEMA_EXIT_CODE,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
fail(str(exc))
|
|
118
|
+
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
109
119
|
if state is None:
|
|
110
120
|
diagnostic = diagnose_contract_file("workflow-state", state_file)
|
|
111
121
|
detail = (
|
|
@@ -125,7 +135,11 @@ def _state(
|
|
|
125
135
|
|
|
126
136
|
def _assessment(feature_dir: Path):
|
|
127
137
|
assessment_file = feature_dir / "assessment.json"
|
|
128
|
-
|
|
138
|
+
try:
|
|
139
|
+
assessment = read_assessment(assessment_file)
|
|
140
|
+
except LegacySchemaError as exc:
|
|
141
|
+
fail(str(exc))
|
|
142
|
+
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
129
143
|
if assessment is None:
|
|
130
144
|
diagnostic = diagnose_contract_file("assessment", assessment_file)
|
|
131
145
|
fail(
|
|
@@ -239,26 +253,6 @@ def evidence(
|
|
|
239
253
|
warn("Source manuelle : cette preuve ne valide pas les gates du workflow.")
|
|
240
254
|
|
|
241
255
|
|
|
242
|
-
@app.command("omit")
|
|
243
|
-
def omit_adaptive(
|
|
244
|
-
feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
|
|
245
|
-
activity: str = typer.Argument(..., help="research ou contracts, activite courante"),
|
|
246
|
-
by: str = typer.Option(..., "--by", help="Responsable de la decision"),
|
|
247
|
-
reason: str = typer.Option(..., "--reason", help="Justification explicite"),
|
|
248
|
-
) -> None:
|
|
249
|
-
"""Omet une activite critical recommandee par risk-assessment, avec audit."""
|
|
250
|
-
feature_dir = _feature(feature)
|
|
251
|
-
try:
|
|
252
|
-
_, next_activity = omit_current_activity(
|
|
253
|
-
feature_dir, activity, by=by, reason=reason
|
|
254
|
-
)
|
|
255
|
-
except (OSError, ValueError) as exc:
|
|
256
|
-
fail(f"Omission refusee : {exc}")
|
|
257
|
-
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
258
|
-
safe_refresh_worklog(feature_dir)
|
|
259
|
-
ok(f"Activite '{activity}' omise avec justification. Prochaine : {next_activity}")
|
|
260
|
-
|
|
261
|
-
|
|
262
256
|
@app.command("contract")
|
|
263
257
|
def contract(
|
|
264
258
|
feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
|
|
@@ -344,7 +338,7 @@ def run_adaptive(
|
|
|
344
338
|
)
|
|
345
339
|
return
|
|
346
340
|
if current_state.currentActivity not in {
|
|
347
|
-
"prepare", "risk-assessment", "spec", "
|
|
341
|
+
"prepare", "risk-assessment", "spec", "contracts", "plan",
|
|
348
342
|
"tasks", "preflight-evidence", "implement", "review", "rollback-check",
|
|
349
343
|
}:
|
|
350
344
|
detail = f"L'activite '{current_state.currentActivity}' ne necessite pas d'agent."
|
|
@@ -603,9 +597,6 @@ def review_adaptive(
|
|
|
603
597
|
risk: Optional[list[str]] = typer.Option(
|
|
604
598
|
None, "--risk", help="Risque restant (option repetable)"
|
|
605
599
|
),
|
|
606
|
-
ack_omission: Optional[list[str]] = typer.Option(
|
|
607
|
-
None, "--ack-omission", help="Activite omise explicitement revue (repetable)"
|
|
608
|
-
),
|
|
609
600
|
) -> None:
|
|
610
601
|
"""Enregistre une revue humaine, validee comme la revue d'un agent."""
|
|
611
602
|
feature_dir = _feature(feature)
|
|
@@ -624,7 +615,6 @@ def review_adaptive(
|
|
|
624
615
|
|
|
625
616
|
result = record_human_review(
|
|
626
617
|
feature_dir, feature_path, by, relative_report, outcome, list(risk or []),
|
|
627
|
-
list(ack_omission or []),
|
|
628
618
|
)
|
|
629
619
|
if result.advanced:
|
|
630
620
|
ok(
|
|
@@ -706,6 +696,9 @@ def approve_adaptive_command(
|
|
|
706
696
|
approval = approve_plan_command(
|
|
707
697
|
feature_dir, validation_id, reason, expected_command=item.command
|
|
708
698
|
)
|
|
699
|
+
except LegacySchemaError as exc:
|
|
700
|
+
fail(str(exc))
|
|
701
|
+
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
709
702
|
except ValueError as exc:
|
|
710
703
|
fail(f"Approbation refusee : {exc}")
|
|
711
704
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
@@ -731,6 +724,9 @@ def waive_validation(
|
|
|
731
724
|
waiver = create_validation_waiver(
|
|
732
725
|
feature_dir, validation_id, reason, author=author
|
|
733
726
|
)
|
|
727
|
+
except LegacySchemaError as exc:
|
|
728
|
+
fail(str(exc))
|
|
729
|
+
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
734
730
|
except ValueError as exc:
|
|
735
731
|
fail(str(exc))
|
|
736
732
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
@@ -917,6 +913,9 @@ def worklog(
|
|
|
917
913
|
feature_dir = _feature(feature)
|
|
918
914
|
try:
|
|
919
915
|
output = refresh_worklog(feature_dir)
|
|
916
|
+
except LegacySchemaError as exc:
|
|
917
|
+
fail(str(exc))
|
|
918
|
+
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
920
919
|
except ValueError as exc:
|
|
921
920
|
fail(str(exc))
|
|
922
921
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
@@ -959,7 +958,7 @@ def _operational_status(
|
|
|
959
958
|
),
|
|
960
959
|
"currentActivity": state.currentActivity,
|
|
961
960
|
"completedActivities": list(state.completedActivities),
|
|
962
|
-
"
|
|
961
|
+
"guardrails": state.guardrails,
|
|
963
962
|
"progress": {
|
|
964
963
|
"completed": len(state.completedActivities),
|
|
965
964
|
"total": len(state.activities),
|
|
@@ -984,10 +983,9 @@ def _print_operational_status(payload: dict) -> None:
|
|
|
984
983
|
console.print(f"Activite : [bold]{payload['currentActivity']}[/bold]")
|
|
985
984
|
progress = payload["progress"]
|
|
986
985
|
console.print(f"Progression : {progress['completed']}/{progress['total']}")
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
console.print(f" - {activity} : {entry['reason']} ({entry['by']})")
|
|
986
|
+
console.print(
|
|
987
|
+
f"Garde-fous : {'actifs' if payload['guardrails'] else 'inactifs'}"
|
|
988
|
+
)
|
|
991
989
|
console.print("Preuves :")
|
|
992
990
|
for item in payload["evidence"]:
|
|
993
991
|
source = f" / {item['source']}" if item["source"] else ""
|
devflow_cli/commands/assess.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import re
|
|
7
|
+
from pathlib import Path
|
|
6
8
|
from typing import Literal
|
|
7
9
|
|
|
8
10
|
import typer
|
|
@@ -14,10 +16,20 @@ from devflow_cli.core.assessment import (
|
|
|
14
16
|
write_assessment,
|
|
15
17
|
)
|
|
16
18
|
from devflow_cli.core.adaptive_workflow import workflow_lock
|
|
19
|
+
from devflow_cli.core.contract_schemas import check_schema_version
|
|
17
20
|
from devflow_cli.utils.console import console, fail, ok
|
|
18
21
|
from devflow_cli.utils.paths import resolve_feature_dir
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def _refuse_unsupported_version(path: Path) -> None:
|
|
25
|
+
"""Refuse d'ecraser un assessment.json d'une autre version de devflow."""
|
|
26
|
+
try:
|
|
27
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
29
|
+
return
|
|
30
|
+
check_schema_version("assessment", path, payload)
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
def assess(
|
|
22
34
|
feature: str = typer.Argument(..., help="ID ou dossier de la feature"),
|
|
23
35
|
scope: Literal["local", "multiple", "transversal"] = typer.Option(
|
|
@@ -36,7 +48,7 @@ def assess(
|
|
|
36
48
|
False, "--data-security", help="Le changement touche aux donnees ou a la securite"
|
|
37
49
|
),
|
|
38
50
|
summary: str = typer.Option("", help="Resume court de la demande"),
|
|
39
|
-
profile: Literal["quick", "standard", "deep"
|
|
51
|
+
profile: Literal["quick", "standard", "deep"] | None = typer.Option(
|
|
40
52
|
None, help="Force un profil au lieu de la recommandation"
|
|
41
53
|
),
|
|
42
54
|
issue_id: str | None = typer.Option(
|
|
@@ -52,7 +64,13 @@ def assess(
|
|
|
52
64
|
fail(f"Feature '{feature}' introuvable.")
|
|
53
65
|
raise typer.Exit(1)
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
assessment_file = feature_dir / "assessment.json"
|
|
68
|
+
try:
|
|
69
|
+
_refuse_unsupported_version(assessment_file)
|
|
70
|
+
existing_assessment = read_assessment(assessment_file)
|
|
71
|
+
except ValueError as exc:
|
|
72
|
+
fail(str(exc))
|
|
73
|
+
raise typer.Exit(1) from exc
|
|
56
74
|
canonical_issue_id = issue_id or (
|
|
57
75
|
existing_assessment.issueId if existing_assessment is not None else None
|
|
58
76
|
)
|
|
@@ -109,6 +127,12 @@ def assess(
|
|
|
109
127
|
if assessment.selectedProfile != assessment.recommendedProfile:
|
|
110
128
|
console.print(f" Recommande : {assessment.recommendedProfile}")
|
|
111
129
|
console.print(f" Confiance : {assessment.confidence}")
|
|
130
|
+
if assessment.guardrails.active:
|
|
131
|
+
console.print(
|
|
132
|
+
f" Garde-fous : actifs ({', '.join(assessment.guardrails.reasons)})"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
console.print(" Garde-fous : inactifs")
|
|
112
136
|
for reason in assessment.reasons:
|
|
113
137
|
console.print(f" - {reason}")
|
|
114
138
|
console.print(f" Activites : {' -> '.join(assessment.activities)}")
|
devflow_cli/commands/status.py
CHANGED
|
@@ -10,6 +10,7 @@ import typer
|
|
|
10
10
|
from devflow_cli import ARTIFACTS
|
|
11
11
|
from devflow_cli.core.adaptive_workflow import read_adaptive_state
|
|
12
12
|
from devflow_cli.core.assessment import read_assessment
|
|
13
|
+
from devflow_cli.core.contract_schemas import LegacySchemaError
|
|
13
14
|
from devflow_cli.core.manifest import stale_commands
|
|
14
15
|
from devflow_cli.core.staleness import get_staleness
|
|
15
16
|
from devflow_cli.utils.console import console, make_table, warn
|
|
@@ -66,6 +67,25 @@ def _list_features() -> None:
|
|
|
66
67
|
workflow_file = feature_dir / "workflow-state.json"
|
|
67
68
|
assessment_file = feature_dir / "assessment.json"
|
|
68
69
|
|
|
70
|
+
try:
|
|
71
|
+
legacy = None
|
|
72
|
+
if workflow_file.exists():
|
|
73
|
+
read_adaptive_state(workflow_file)
|
|
74
|
+
if assessment_file.exists():
|
|
75
|
+
read_assessment(assessment_file)
|
|
76
|
+
except LegacySchemaError as exc:
|
|
77
|
+
legacy = exc
|
|
78
|
+
if legacy is not None:
|
|
79
|
+
warn(str(legacy))
|
|
80
|
+
table.add_row(
|
|
81
|
+
feature_dir.name,
|
|
82
|
+
"-",
|
|
83
|
+
"[yellow]schema 6.x[/yellow]",
|
|
84
|
+
"-",
|
|
85
|
+
location,
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
|
|
69
89
|
if workflow_file.exists():
|
|
70
90
|
state = read_adaptive_state(workflow_file)
|
|
71
91
|
if state is None:
|