devflow-cli 7.1.1__py3-none-any.whl → 8.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 +9 -12
- devflow_cli/adapters/agents.py +4 -0
- devflow_cli/adapters/linear.py +163 -3
- devflow_cli/commands/adaptive.py +63 -20
- devflow_cli/commands/check.py +11 -2
- devflow_cli/commands/hooks_cmd.py +4 -0
- devflow_cli/core/activity_base.py +408 -0
- devflow_cli/core/activity_contracts.py +14 -20
- devflow_cli/core/adaptive_runner.py +195 -171
- devflow_cli/core/adaptive_workflow.py +29 -3
- devflow_cli/core/automatic_evidence.py +10 -2
- devflow_cli/core/contract_schemas.py +35 -9
- devflow_cli/core/feedback.py +15 -2
- devflow_cli/core/git.py +8 -1
- devflow_cli/core/hooks.py +2 -0
- devflow_cli/core/remediation.py +21 -137
- devflow_cli/core/validation_approvals.py +2 -2
- devflow_cli/core/verification.py +2 -0
- devflow_cli/core/worklog.py +20 -0
- devflow_cli/core/worktree_snapshot.py +26 -14
- devflow_cli/schemas/{workflow-state-v5.schema.json → workflow-state-v6.schema.json} +28 -5
- devflow_cli/utils/paths.py +2 -0
- {devflow_cli-7.1.1.dist-info → devflow_cli-8.0.0.dist-info}/METADATA +24 -1
- {devflow_cli-7.1.1.dist-info → devflow_cli-8.0.0.dist-info}/RECORD +27 -26
- {devflow_cli-7.1.1.dist-info → devflow_cli-8.0.0.dist-info}/WHEEL +0 -0
- {devflow_cli-7.1.1.dist-info → devflow_cli-8.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-7.1.1.dist-info → devflow_cli-8.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""devflow CLI — workflow de développement adaptatif."""
|
|
2
2
|
|
|
3
|
-
VERSION = "
|
|
3
|
+
VERSION = "8.0.0"
|
|
4
4
|
|
|
5
5
|
SUPPORTED_AGENTS = ["claude-code", "codex", "cursor"]
|
|
6
6
|
|
|
7
7
|
ACTIVITY_LINEAR_MAPPING = {
|
|
8
8
|
"assess": "Backlog",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"spec": "
|
|
12
|
-
"contracts": "
|
|
13
|
-
"plan": "
|
|
14
|
-
"tasks": "
|
|
15
|
-
"preflight-evidence": "
|
|
9
|
+
"risk-assessment": "Design",
|
|
10
|
+
"prepare": "Design",
|
|
11
|
+
"spec": "Design",
|
|
12
|
+
"contracts": "Design",
|
|
13
|
+
"plan": "Design",
|
|
14
|
+
"tasks": "Design",
|
|
15
|
+
"preflight-evidence": "Design",
|
|
16
16
|
"implement": "In Progress",
|
|
17
17
|
"verify": "Review",
|
|
18
18
|
"review": "Review",
|
|
@@ -22,10 +22,7 @@ ACTIVITY_LINEAR_MAPPING = {
|
|
|
22
22
|
|
|
23
23
|
LINEAR_STATUS_ORDER = [
|
|
24
24
|
"Backlog",
|
|
25
|
-
"
|
|
26
|
-
"Research",
|
|
27
|
-
"Plan",
|
|
28
|
-
"Tasks",
|
|
25
|
+
"Design",
|
|
29
26
|
"In Progress",
|
|
30
27
|
"Review",
|
|
31
28
|
"Done",
|
devflow_cli/adapters/agents.py
CHANGED
|
@@ -16,6 +16,8 @@ from typing import Literal
|
|
|
16
16
|
from jsonschema.exceptions import SchemaError, ValidationError
|
|
17
17
|
from jsonschema.validators import validator_for
|
|
18
18
|
|
|
19
|
+
from devflow_cli.adapters.linear import environment_without_linear_key
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
PermissionMode = Literal["auto", "acceptEdits", "manual"]
|
|
21
23
|
SandboxMode = Literal["read-only", "workspace-write"]
|
|
@@ -123,6 +125,7 @@ class AgentAdapter(ABC):
|
|
|
123
125
|
capture_output=capture_output,
|
|
124
126
|
stdin=subprocess.DEVNULL,
|
|
125
127
|
shell=False,
|
|
128
|
+
env=environment_without_linear_key(),
|
|
126
129
|
)
|
|
127
130
|
return AgentExecution(
|
|
128
131
|
result.returncode,
|
|
@@ -162,6 +165,7 @@ class AgentAdapter(ABC):
|
|
|
162
165
|
capture_output=True,
|
|
163
166
|
stdin=subprocess.DEVNULL,
|
|
164
167
|
shell=False,
|
|
168
|
+
env=environment_without_linear_key(),
|
|
165
169
|
)
|
|
166
170
|
execution = AgentExecution(
|
|
167
171
|
result.returncode,
|
devflow_cli/adapters/linear.py
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
"""Politique de synchronisation Linear
|
|
1
|
+
"""Politique de synchronisation Linear et transport GraphQL."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
5
9
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Literal, Protocol
|
|
10
|
+
from typing import Literal, Protocol, TextIO
|
|
7
11
|
|
|
8
|
-
from devflow_cli import ACTIVITY_LINEAR_MAPPING, LINEAR_STATUS_ORDER
|
|
12
|
+
from devflow_cli import ACTIVITY_LINEAR_MAPPING, LINEAR_STATUS_ORDER, VERSION
|
|
13
|
+
from devflow_cli.core.run_progress import emit_progress
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
LINEAR_API_KEY_ENV = "LINEAR_API_KEY"
|
|
17
|
+
LINEAR_API_URL = "https://api.linear.app/graphql"
|
|
18
|
+
LINEAR_TIMEOUT_SECONDS = 5
|
|
19
|
+
|
|
20
|
+
_ISSUE_QUERY = (
|
|
21
|
+
"query($id: String!) { issue(id: $id) { id state { name } "
|
|
22
|
+
"team { states { nodes { id name } } } } }"
|
|
23
|
+
)
|
|
24
|
+
_UPDATE_MUTATION = (
|
|
25
|
+
"mutation($id: String!, $stateId: String!) { "
|
|
26
|
+
"issueUpdate(id: $id, input: { stateId: $stateId }) { success } }"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_missing_key_warned = False
|
|
9
30
|
|
|
10
31
|
|
|
11
32
|
LinearAction = Literal["update", "keep", "skip"]
|
|
@@ -81,3 +102,142 @@ def sync_linear_status(
|
|
|
81
102
|
if decision.should_update and decision.target_status is not None:
|
|
82
103
|
adapter.update_status(issue_id, decision.target_status)
|
|
83
104
|
return decision
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def environment_without_linear_key() -> dict[str, str]:
|
|
108
|
+
"""Environnement transmis aux processus lancés par la CLI, sans la clé Linear."""
|
|
109
|
+
return {key: value for key, value in os.environ.items() if key != LINEAR_API_KEY_ENV}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
113
|
+
"""Refuse de suivre une redirection : l'en-tête Authorization ne doit pas la suivre."""
|
|
114
|
+
|
|
115
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LinearSyncError(Exception):
|
|
120
|
+
"""Échec de synchronisation ; le libellé ne contient aucune donnée externe."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, label: str) -> None:
|
|
123
|
+
super().__init__(label)
|
|
124
|
+
self.label = label
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LinearGraphQLAdapter:
|
|
128
|
+
"""Transport par l'API GraphQL de Linear, en bibliothèque standard."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, api_key: str, timeout: float = LINEAR_TIMEOUT_SECONDS) -> None:
|
|
131
|
+
self._api_key = api_key
|
|
132
|
+
self._timeout = timeout
|
|
133
|
+
self._issue_uuid: str | None = None
|
|
134
|
+
self._state_ids: dict[str, str] = {}
|
|
135
|
+
|
|
136
|
+
def get_status(self, issue_id: str) -> str | None:
|
|
137
|
+
data = self._request(_ISSUE_QUERY, {"id": issue_id})
|
|
138
|
+
issue = data.get("issue")
|
|
139
|
+
if issue is None:
|
|
140
|
+
raise LinearSyncError("issue inconnue de Linear")
|
|
141
|
+
try:
|
|
142
|
+
self._issue_uuid = str(issue["id"])
|
|
143
|
+
self._state_ids = {
|
|
144
|
+
str(node["name"]): str(node["id"])
|
|
145
|
+
for node in issue["team"]["states"]["nodes"]
|
|
146
|
+
}
|
|
147
|
+
return str(issue["state"]["name"])
|
|
148
|
+
except (KeyError, TypeError):
|
|
149
|
+
raise LinearSyncError("reponse Linear invalide") from None
|
|
150
|
+
|
|
151
|
+
def update_status(self, issue_id: str, status: str) -> None:
|
|
152
|
+
state_id = self._state_ids.get(status)
|
|
153
|
+
if self._issue_uuid is None or state_id is None:
|
|
154
|
+
raise LinearSyncError(f"statut '{status}' absent de l'equipe Linear")
|
|
155
|
+
data = self._request(
|
|
156
|
+
_UPDATE_MUTATION, {"id": self._issue_uuid, "stateId": state_id}
|
|
157
|
+
)
|
|
158
|
+
result = data.get("issueUpdate")
|
|
159
|
+
if not isinstance(result, dict) or result.get("success") is not True:
|
|
160
|
+
raise LinearSyncError("mise a jour refusee par Linear")
|
|
161
|
+
|
|
162
|
+
def _request(self, query: str, variables: dict[str, str]) -> dict:
|
|
163
|
+
request = urllib.request.Request(
|
|
164
|
+
LINEAR_API_URL,
|
|
165
|
+
data=json.dumps({"query": query, "variables": variables}).encode("utf-8"),
|
|
166
|
+
headers={
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"Authorization": self._api_key,
|
|
169
|
+
"User-Agent": f"devflow-cli/{VERSION}",
|
|
170
|
+
},
|
|
171
|
+
method="POST",
|
|
172
|
+
)
|
|
173
|
+
opener = urllib.request.build_opener(_NoRedirectHandler())
|
|
174
|
+
try:
|
|
175
|
+
with opener.open(request, timeout=self._timeout) as response:
|
|
176
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
177
|
+
except urllib.error.HTTPError as exc:
|
|
178
|
+
raise LinearSyncError(f"erreur HTTP {exc.code}") from None
|
|
179
|
+
except TimeoutError:
|
|
180
|
+
raise LinearSyncError("delai depasse") from None
|
|
181
|
+
except urllib.error.URLError as exc:
|
|
182
|
+
if isinstance(exc.reason, TimeoutError):
|
|
183
|
+
raise LinearSyncError("delai depasse") from None
|
|
184
|
+
raise LinearSyncError("erreur reseau") from None
|
|
185
|
+
except OSError:
|
|
186
|
+
raise LinearSyncError("erreur reseau") from None
|
|
187
|
+
except ValueError:
|
|
188
|
+
raise LinearSyncError("reponse Linear invalide") from None
|
|
189
|
+
if not isinstance(payload, dict):
|
|
190
|
+
raise LinearSyncError("reponse Linear invalide")
|
|
191
|
+
if payload.get("errors"):
|
|
192
|
+
raise LinearSyncError("erreur GraphQL")
|
|
193
|
+
data = payload.get("data")
|
|
194
|
+
if not isinstance(data, dict):
|
|
195
|
+
raise LinearSyncError("reponse Linear invalide")
|
|
196
|
+
return data
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def sync_activity_status(
|
|
200
|
+
issue_id: str, activity: str, stream: TextIO
|
|
201
|
+
) -> LinearDecision | None:
|
|
202
|
+
"""Synchronise le statut au démarrage d'une activité, sans jamais bloquer."""
|
|
203
|
+
global _missing_key_warned
|
|
204
|
+
api_key = os.environ.get(LINEAR_API_KEY_ENV, "").strip()
|
|
205
|
+
if not api_key:
|
|
206
|
+
if not _missing_key_warned:
|
|
207
|
+
_missing_key_warned = True
|
|
208
|
+
emit_progress(
|
|
209
|
+
f"Linear : avertissement, {LINEAR_API_KEY_ENV} absente ; "
|
|
210
|
+
"statuts non synchronises",
|
|
211
|
+
stream,
|
|
212
|
+
)
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
decision = sync_linear_status(LinearGraphQLAdapter(api_key), issue_id, activity)
|
|
216
|
+
except LinearSyncError as exc:
|
|
217
|
+
label = exc.label
|
|
218
|
+
except Exception:
|
|
219
|
+
label = "erreur inattendue"
|
|
220
|
+
else:
|
|
221
|
+
emit_progress(_describe_decision(issue_id, decision), stream)
|
|
222
|
+
return decision
|
|
223
|
+
emit_progress(
|
|
224
|
+
f"Linear : avertissement, {issue_id} non synchronisee ({label})", stream
|
|
225
|
+
)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _describe_decision(issue_id: str, decision: LinearDecision) -> str:
|
|
230
|
+
if decision.action == "update":
|
|
231
|
+
return (
|
|
232
|
+
f"Linear : {issue_id} {decision.current_status} -> "
|
|
233
|
+
f"{decision.target_status} (activite {decision.activity})"
|
|
234
|
+
)
|
|
235
|
+
if decision.action == "keep":
|
|
236
|
+
return (
|
|
237
|
+
f"Linear : {issue_id} reste en {decision.current_status} "
|
|
238
|
+
f"(activite {decision.activity}, {decision.reason})"
|
|
239
|
+
)
|
|
240
|
+
return (
|
|
241
|
+
f"Linear : {issue_id} ignoree, statut {decision.current_status or 'inconnu'} "
|
|
242
|
+
f"({decision.reason})"
|
|
243
|
+
)
|
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -14,6 +14,7 @@ from typing import Literal, Optional
|
|
|
14
14
|
import typer
|
|
15
15
|
|
|
16
16
|
from devflow_cli import VERSION
|
|
17
|
+
from devflow_cli.adapters.linear import sync_activity_status
|
|
17
18
|
from devflow_cli.core.adaptive_workflow import (
|
|
18
19
|
AdaptiveWorkflowState,
|
|
19
20
|
complete_activity,
|
|
@@ -24,6 +25,10 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
24
25
|
write_adaptive_state,
|
|
25
26
|
)
|
|
26
27
|
from devflow_cli.core.assessment import read_assessment
|
|
28
|
+
from devflow_cli.core.activity_base import (
|
|
29
|
+
enter_current_activity,
|
|
30
|
+
files_since_activity_base,
|
|
31
|
+
)
|
|
27
32
|
from devflow_cli.core.activity_contracts import (
|
|
28
33
|
build_activity_contract,
|
|
29
34
|
NEED_STATEMENT,
|
|
@@ -50,7 +55,6 @@ from devflow_cli.core.worklog import refresh_worklog, safe_refresh_worklog
|
|
|
50
55
|
from devflow_cli.core.telemetry import append_record, telemetry_enabled, usage_payload
|
|
51
56
|
from devflow_cli.core.waivers import create_validation_waiver
|
|
52
57
|
from devflow_cli.core.remediation import (
|
|
53
|
-
carried_changed_files,
|
|
54
58
|
discard_attempt,
|
|
55
59
|
is_reopened_activity,
|
|
56
60
|
reopen_activity,
|
|
@@ -76,6 +80,9 @@ app = typer.Typer(help="Machine d'etat des parcours adaptatifs")
|
|
|
76
80
|
|
|
77
81
|
LEGACY_SCHEMA_EXIT_CODE = 1
|
|
78
82
|
|
|
83
|
+
# Refus avant tout agent : base inutilisable (branche changee, historique reecrit).
|
|
84
|
+
BASE_REFUSALS = frozenset({"base-history-rewritten", "base-branch-changed"})
|
|
85
|
+
|
|
79
86
|
RESULT_VALIDATED_ACTIVITIES = frozenset({
|
|
80
87
|
"prepare",
|
|
81
88
|
"risk-assessment",
|
|
@@ -166,7 +173,10 @@ def start(feature: str = typer.Argument(..., help="ID ou dossier de la feature")
|
|
|
166
173
|
assessment = _assessment(feature_dir)
|
|
167
174
|
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
168
175
|
state = create_adaptive_workflow(assessment.to_dict())
|
|
169
|
-
|
|
176
|
+
enter_current_activity(
|
|
177
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
178
|
+
)
|
|
179
|
+
except (OSError, ValueError) as exc:
|
|
170
180
|
fail(f"Impossible de demarrer le workflow : {exc}")
|
|
171
181
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
172
182
|
write_adaptive_state(state_file, state)
|
|
@@ -220,12 +230,17 @@ def complete(
|
|
|
220
230
|
validate_preflight_evidence(feature_dir / "preflight-evidence.md")
|
|
221
231
|
ensure_fresh_devflow_evidence(feature_dir, state, activity)
|
|
222
232
|
complete_activity(state, activity)
|
|
223
|
-
|
|
233
|
+
enter_current_activity(
|
|
234
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
235
|
+
)
|
|
236
|
+
except (OSError, ValueError) as exc:
|
|
224
237
|
fail(str(exc))
|
|
225
238
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
226
239
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
227
240
|
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
228
241
|
safe_refresh_worklog(feature_dir)
|
|
242
|
+
if activity == "done":
|
|
243
|
+
sync_activity_status(state.issueId, "done", run_progress.progress_stream(False))
|
|
229
244
|
if state.status == "completed":
|
|
230
245
|
ok(f"Workflow {state.profile} termine.")
|
|
231
246
|
_print_completion_summary(feature_dir, state)
|
|
@@ -281,11 +296,15 @@ def contract(
|
|
|
281
296
|
state,
|
|
282
297
|
assessment.to_dict(),
|
|
283
298
|
feature_path,
|
|
284
|
-
carried_changed_files(feature_dir, state.currentActivity),
|
|
285
299
|
reopened=is_reopened_activity(feature_dir, state.currentActivity),
|
|
286
300
|
preflight_evidence=(feature_dir / "preflight-evidence.md").is_file(),
|
|
301
|
+
files_since_base=(
|
|
302
|
+
files_since_activity_base(feature_dir, state)
|
|
303
|
+
if state.activityBase is not None
|
|
304
|
+
else ()
|
|
305
|
+
),
|
|
287
306
|
)
|
|
288
|
-
except ValueError as exc:
|
|
307
|
+
except (OSError, ValueError) as exc:
|
|
289
308
|
fail(f"Impossible de generer le contrat : {exc}")
|
|
290
309
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
291
310
|
output_file = feature_dir / "activity-contract.json"
|
|
@@ -388,6 +407,9 @@ def run_adaptive(
|
|
|
388
407
|
f"agent {agent}, delai {timeout} s",
|
|
389
408
|
stream,
|
|
390
409
|
)
|
|
410
|
+
sync_activity_status(
|
|
411
|
+
current_state.issueId, activity_contract.activity, stream
|
|
412
|
+
)
|
|
391
413
|
with run_progress.heartbeat(
|
|
392
414
|
activity_contract.activity,
|
|
393
415
|
agent,
|
|
@@ -451,12 +473,14 @@ def run_adaptive(
|
|
|
451
473
|
_print_execution_json(
|
|
452
474
|
current_state.issueId, outcome.activity, True, outcome.reason,
|
|
453
475
|
outcome.detail, outcome.nextActivity, ExitCode.OK,
|
|
476
|
+
warnings=outcome.warnings,
|
|
454
477
|
)
|
|
455
478
|
else:
|
|
456
479
|
ok(
|
|
457
480
|
f"Activite '{outcome.activity}' validee. "
|
|
458
481
|
f"Prochaine : {outcome.nextActivity}"
|
|
459
482
|
)
|
|
483
|
+
_print_warnings(outcome.warnings)
|
|
460
484
|
return
|
|
461
485
|
safe_refresh_worklog(feature_dir)
|
|
462
486
|
exit_code = exit_code_for_run_reason(outcome.reason)
|
|
@@ -464,9 +488,14 @@ def run_adaptive(
|
|
|
464
488
|
_print_execution_json(
|
|
465
489
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
466
490
|
outcome.detail, outcome.nextActivity, exit_code,
|
|
491
|
+
warnings=outcome.warnings,
|
|
467
492
|
)
|
|
493
|
+
elif outcome.reason in BASE_REFUSALS:
|
|
494
|
+
fail(outcome.detail)
|
|
495
|
+
console.print(f" Suite : devflow adaptive status {current_state.issueId}")
|
|
468
496
|
else:
|
|
469
497
|
fail(outcome.detail or outcome.reason)
|
|
498
|
+
_print_warnings(outcome.warnings)
|
|
470
499
|
console.print(
|
|
471
500
|
f" Activite '{outcome.activity}' non validee : {outcome.reason}"
|
|
472
501
|
)
|
|
@@ -585,12 +614,17 @@ def reconcile_adaptive(
|
|
|
585
614
|
f"Activite '{outcome.activity}' reconciliee. "
|
|
586
615
|
f"Prochaine : {outcome.nextActivity}"
|
|
587
616
|
)
|
|
617
|
+
_print_warnings(outcome.warnings)
|
|
588
618
|
return
|
|
589
619
|
safe_refresh_worklog(feature_dir)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
620
|
+
if outcome.reason in BASE_REFUSALS:
|
|
621
|
+
fail(outcome.detail)
|
|
622
|
+
else:
|
|
623
|
+
fail(
|
|
624
|
+
f"Reconciliation de '{outcome.activity}' refusee : {outcome.reason}"
|
|
625
|
+
+ (f" ({outcome.detail})" if outcome.detail else "")
|
|
626
|
+
)
|
|
627
|
+
_print_warnings(outcome.warnings)
|
|
594
628
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
595
629
|
if outcome.reason == "reconcile-refused":
|
|
596
630
|
console.print(
|
|
@@ -765,6 +799,9 @@ def reopen(
|
|
|
765
799
|
entry = reopen_activity(
|
|
766
800
|
feature_dir, state, activity, reason, author=author
|
|
767
801
|
)
|
|
802
|
+
enter_current_activity(
|
|
803
|
+
state, project_root_for_feature(feature_dir), feature_dir
|
|
804
|
+
)
|
|
768
805
|
except (OSError, ValueError) as exc:
|
|
769
806
|
fail(str(exc))
|
|
770
807
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
@@ -800,19 +837,24 @@ def discard_attempt_command(
|
|
|
800
837
|
)
|
|
801
838
|
if entry["discardedFiles"]:
|
|
802
839
|
warn(
|
|
803
|
-
"
|
|
804
|
-
|
|
840
|
+
"Fichiers modifies depuis le debut de l'activite "
|
|
841
|
+
f"'{entry['activity']}', comptes dans le lot a la relance ; remise en "
|
|
842
|
+
"etat a la charge du superviseur : " + ", ".join(entry["discardedFiles"])
|
|
805
843
|
)
|
|
844
|
+
if entry.get("observationError"):
|
|
845
|
+
warn(f"Fichiers depuis la base non calcules : {entry['observationError']}")
|
|
806
846
|
|
|
807
847
|
|
|
808
848
|
def _execute_local_verification(
|
|
809
849
|
feature_dir: Path, project_root: Path, issue_id: str, *,
|
|
810
850
|
refresh: bool = False, json_output: bool = False,
|
|
811
851
|
) -> None:
|
|
852
|
+
stream = run_progress.progress_stream(json_output)
|
|
812
853
|
run_progress.emit_progress(
|
|
813
|
-
"Activite 'verify' demarree : verification locale",
|
|
814
|
-
run_progress.progress_stream(json_output),
|
|
854
|
+
"Activite 'verify' demarree : verification locale", stream
|
|
815
855
|
)
|
|
856
|
+
if not refresh:
|
|
857
|
+
sync_activity_status(issue_id, "verify", stream)
|
|
816
858
|
outcome = run_automatic_verification(feature_dir, project_root, refresh=refresh)
|
|
817
859
|
if json_output:
|
|
818
860
|
_print_execution_json(
|
|
@@ -843,6 +885,7 @@ def _execute_local_verification(
|
|
|
843
885
|
def _print_execution_json(
|
|
844
886
|
issue_id: str, activity: str | None, passed: bool, reason: str,
|
|
845
887
|
detail: str, next_activity: str | None, exit_code: ExitCode,
|
|
888
|
+
*, warnings: tuple[str, ...] = (),
|
|
846
889
|
) -> None:
|
|
847
890
|
typer.echo(json.dumps({
|
|
848
891
|
"schemaVersion": 1,
|
|
@@ -853,9 +896,16 @@ def _print_execution_json(
|
|
|
853
896
|
"detail": detail,
|
|
854
897
|
"nextActivity": next_activity,
|
|
855
898
|
"exitCode": int(exit_code),
|
|
899
|
+
"warnings": list(warnings),
|
|
856
900
|
}, ensure_ascii=False))
|
|
857
901
|
|
|
858
902
|
|
|
903
|
+
def _print_warnings(warnings: tuple[str, ...]) -> None:
|
|
904
|
+
"""Ecart de declaration : avertissement seul, sans effet sur le code de sortie."""
|
|
905
|
+
for message in warnings:
|
|
906
|
+
warn(message)
|
|
907
|
+
|
|
908
|
+
|
|
859
909
|
def _utc_now() -> str:
|
|
860
910
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
861
911
|
|
|
@@ -1147,13 +1197,6 @@ def _print_run_recovery(
|
|
|
1147
1197
|
console.print(" Ne relancez pas automatiquement l'agent : reconciliez d'abord les changements partiels.")
|
|
1148
1198
|
console.print(f" devflow adaptive reconcile {issue_id}")
|
|
1149
1199
|
return
|
|
1150
|
-
if reason == "stale-attempt":
|
|
1151
|
-
console.print(" git status --short # inspecter les changements de la tentative")
|
|
1152
|
-
console.print(
|
|
1153
|
-
" Restaurez les fichiers de la tentative, ou abandonnez-la explicitement :"
|
|
1154
|
-
)
|
|
1155
|
-
console.print(f" devflow adaptive discard-attempt {issue_id} --reason MOTIF")
|
|
1156
|
-
return
|
|
1157
1200
|
if reason == "inputs-changed-during-activity":
|
|
1158
1201
|
console.print(
|
|
1159
1202
|
" Les entrees du besoin ont change pendant l'execution. "
|
devflow_cli/commands/check.py
CHANGED
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
from devflow_cli.adapters.agents import get_agent_adapter
|
|
11
|
+
from devflow_cli.adapters.linear import environment_without_linear_key
|
|
11
12
|
from devflow_cli.core.agent_failure import diagnose_agent_failure
|
|
12
13
|
from devflow_cli.utils.console import console, ok, warn, fail
|
|
13
14
|
from devflow_cli.utils.logging import get_logger
|
|
@@ -78,7 +79,12 @@ def check(
|
|
|
78
79
|
# git
|
|
79
80
|
git_path = _cmd_exists("git")
|
|
80
81
|
if git_path:
|
|
81
|
-
result = subprocess.run(
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
["git", "--version"],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
env=environment_without_linear_key(),
|
|
87
|
+
)
|
|
82
88
|
version = result.stdout.strip().replace("git version ", "")
|
|
83
89
|
ok(f"git -- {git_path} (version {version})")
|
|
84
90
|
ok_count += 1
|
|
@@ -90,7 +96,10 @@ def check(
|
|
|
90
96
|
codex_path = _cmd_exists("codex")
|
|
91
97
|
if codex_path:
|
|
92
98
|
result = subprocess.run(
|
|
93
|
-
["codex", "--version"],
|
|
99
|
+
["codex", "--version"],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
env=environment_without_linear_key(),
|
|
94
103
|
)
|
|
95
104
|
version = (result.stdout or result.stderr).strip()
|
|
96
105
|
if result.returncode == 0:
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
+
from devflow_cli.adapters.linear import environment_without_linear_key
|
|
11
12
|
from devflow_cli.core.git import is_git_repo
|
|
12
13
|
from devflow_cli.core.hooks import (
|
|
13
14
|
DEFAULT_HOOK_TIMEOUT,
|
|
@@ -28,6 +29,7 @@ def _configure_hooks_path(repo: Path) -> None:
|
|
|
28
29
|
["git", "-C", str(repo), "config", "core.hooksPath", HOOKS_DIR_NAME],
|
|
29
30
|
check=True,
|
|
30
31
|
capture_output=True,
|
|
32
|
+
env=environment_without_linear_key(),
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
|
|
@@ -36,6 +38,7 @@ def _current_hooks_path(repo: Path) -> str | None:
|
|
|
36
38
|
["git", "-C", str(repo), "config", "--get", "core.hooksPath"],
|
|
37
39
|
capture_output=True,
|
|
38
40
|
text=True,
|
|
41
|
+
env=environment_without_linear_key(),
|
|
39
42
|
)
|
|
40
43
|
if result.returncode != 0:
|
|
41
44
|
return None
|
|
@@ -46,6 +49,7 @@ def _unset_hooks_path(repo: Path) -> None:
|
|
|
46
49
|
subprocess.run(
|
|
47
50
|
["git", "-C", str(repo), "config", "--unset", "core.hooksPath"],
|
|
48
51
|
capture_output=True,
|
|
52
|
+
env=environment_without_linear_key(),
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
|