devflow-cli 8.1.0__py3-none-any.whl → 8.2.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/adapters/agents.py +72 -4
- devflow_cli/commands/adaptive.py +155 -5
- devflow_cli/core/activity_contracts.py +9 -0
- devflow_cli/core/result_template.py +74 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.2.0.dist-info}/METADATA +11 -1
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.2.0.dist-info}/RECORD +10 -9
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.2.0.dist-info}/WHEEL +0 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.2.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.1.0.dist-info → devflow_cli-8.2.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/adapters/agents.py
CHANGED
|
@@ -22,6 +22,24 @@ from devflow_cli.adapters.linear import environment_without_linear_key
|
|
|
22
22
|
PermissionMode = Literal["auto", "acceptEdits", "manual"]
|
|
23
23
|
SandboxMode = Literal["read-only", "workspace-write"]
|
|
24
24
|
|
|
25
|
+
# L'agent a termine sans erreur (code 0) mais sa sortie structuree manque ou n'est
|
|
26
|
+
# pas le JSON attendu. Claude porte le diagnostic dedie ; Codex et Cursor gardent
|
|
27
|
+
# leurs valeurs detaillees, traitees comme la meme famille par la reprise guidee.
|
|
28
|
+
STRUCTURED_OUTPUT_MISSING = "structured-output-missing"
|
|
29
|
+
MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS = frozenset({
|
|
30
|
+
STRUCTURED_OUTPUT_MISSING,
|
|
31
|
+
"final-output-empty",
|
|
32
|
+
"final-output-absent",
|
|
33
|
+
"final-output-invalid-json",
|
|
34
|
+
})
|
|
35
|
+
STRUCTURED_OUTPUT_MISSING_MESSAGE = (
|
|
36
|
+
"l'agent a termine sans erreur mais sa sortie structuree manque "
|
|
37
|
+
"ou n'est pas le JSON attendu"
|
|
38
|
+
)
|
|
39
|
+
# Impose le premier plan aux sous-agents de l'agent Claude d'une activite ; desactive
|
|
40
|
+
# aussi ses commandes shell en arriere-plan.
|
|
41
|
+
CLAUDE_FOREGROUND_ENV = "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS"
|
|
42
|
+
|
|
25
43
|
_SAFE_DIAGNOSTIC_MESSAGES = {
|
|
26
44
|
"final-output-empty": "--output-last-message est vide",
|
|
27
45
|
"final-output-absent": "--output-last-message est absent",
|
|
@@ -165,7 +183,7 @@ class AgentAdapter(ABC):
|
|
|
165
183
|
capture_output=True,
|
|
166
184
|
stdin=subprocess.DEVNULL,
|
|
167
185
|
shell=False,
|
|
168
|
-
env=
|
|
186
|
+
env=self.structured_environment(),
|
|
169
187
|
)
|
|
170
188
|
execution = AgentExecution(
|
|
171
189
|
result.returncode,
|
|
@@ -223,7 +241,9 @@ class AgentAdapter(ABC):
|
|
|
223
241
|
"Sortie structuree invalide "
|
|
224
242
|
f"({self.name}) [{diagnostic}]"
|
|
225
243
|
+ (
|
|
226
|
-
f" : {
|
|
244
|
+
f" : {self.missing_output_message(execution.stdout)}"
|
|
245
|
+
if diagnostic == STRUCTURED_OUTPUT_MISSING
|
|
246
|
+
else f" : {_SAFE_DIAGNOSTIC_MESSAGES[diagnostic]}"
|
|
227
247
|
if diagnostic in _SAFE_DIAGNOSTIC_MESSAGES
|
|
228
248
|
else ""
|
|
229
249
|
),
|
|
@@ -251,6 +271,27 @@ class AgentAdapter(ABC):
|
|
|
251
271
|
except FileNotFoundError:
|
|
252
272
|
return AgentExecution(127, stderr=f"Executable '{self.executable}' introuvable", duration_seconds=time.monotonic() - started)
|
|
253
273
|
|
|
274
|
+
def structured_environment(self) -> dict[str, str]:
|
|
275
|
+
"""Environnement de l'agent d'une activite : jamais la cle Linear."""
|
|
276
|
+
return environment_without_linear_key()
|
|
277
|
+
|
|
278
|
+
def subagent_counts(self, stdout: str) -> tuple[int | None, int | None]:
|
|
279
|
+
"""Sous-agents lances et lances en arriere-plan ; None si l'agent ne le dit pas."""
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
def missing_output_message(self, stdout: str) -> str:
|
|
283
|
+
"""Cause d'une sortie structuree absente : message fixe et entiers seulement."""
|
|
284
|
+
try:
|
|
285
|
+
spawned, background = self.subagent_counts(stdout)
|
|
286
|
+
except Exception: # noqa: BLE001 - format externe non contractuel
|
|
287
|
+
spawned, background = None, None
|
|
288
|
+
message = STRUCTURED_OUTPUT_MISSING_MESSAGE
|
|
289
|
+
if spawned is not None:
|
|
290
|
+
message += f" ; sous-agents lances : {spawned}"
|
|
291
|
+
if background is not None:
|
|
292
|
+
message += f", dont {background} en arriere-plan"
|
|
293
|
+
return message
|
|
294
|
+
|
|
254
295
|
def session_id(self, stdout: str, stderr: str) -> str | None:
|
|
255
296
|
"""Extrait un identifiant opaque de session, sans conserver la sortie."""
|
|
256
297
|
return _session_id(stdout, stderr)
|
|
@@ -326,8 +367,23 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
326
367
|
return envelope["structured_output"]
|
|
327
368
|
result = envelope.get("result")
|
|
328
369
|
if isinstance(result, str) and result.strip():
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
try:
|
|
371
|
+
return json.loads(_strip_json_fence(result))
|
|
372
|
+
except json.JSONDecodeError:
|
|
373
|
+
pass
|
|
374
|
+
raise StructuredOutputError(
|
|
375
|
+
STRUCTURED_OUTPUT_MISSING, "champ structured_output absent"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def structured_environment(self) -> dict[str, str]:
|
|
379
|
+
return {**environment_without_linear_key(), CLAUDE_FOREGROUND_ENV: "1"}
|
|
380
|
+
|
|
381
|
+
def subagent_counts(self, stdout: str) -> tuple[int | None, int | None]:
|
|
382
|
+
envelope = json.loads(stdout)
|
|
383
|
+
stats = envelope.get("subagent_stats") if isinstance(envelope, dict) else None
|
|
384
|
+
if not isinstance(stats, dict):
|
|
385
|
+
return None, None
|
|
386
|
+
return _count(stats.get("spawned")), _count(stats.get("started_in_background"))
|
|
331
387
|
|
|
332
388
|
def usage(self, stdout: str, session_id: str | None) -> AgentUsage | None:
|
|
333
389
|
"""Lit l'enveloppe --output-format json : usage cumule de tous les tours."""
|
|
@@ -515,6 +571,14 @@ def get_agent_adapter(name: str) -> AgentAdapter:
|
|
|
515
571
|
return adapter_type()
|
|
516
572
|
|
|
517
573
|
|
|
574
|
+
def describe_missing_output(agent: str, stdout: str) -> str:
|
|
575
|
+
"""Cause d'une sortie structuree absente, pour la reprise guidee."""
|
|
576
|
+
try:
|
|
577
|
+
return get_agent_adapter(agent).missing_output_message(stdout)
|
|
578
|
+
except ValueError:
|
|
579
|
+
return STRUCTURED_OUTPUT_MISSING_MESSAGE
|
|
580
|
+
|
|
581
|
+
|
|
518
582
|
def _portable_prompt(prompt: str) -> str:
|
|
519
583
|
"""Traduit une slash-command Claude en instruction portable pour les autres agents."""
|
|
520
584
|
match = re.fullmatch(r"/devflow\.([a-z0-9-]+)(?:\s+(.*))?", prompt.strip())
|
|
@@ -610,6 +674,10 @@ def _int(value: object) -> int:
|
|
|
610
674
|
return value if isinstance(value, int) and not isinstance(value, bool) else 0
|
|
611
675
|
|
|
612
676
|
|
|
677
|
+
def _count(value: object) -> int | None:
|
|
678
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
679
|
+
|
|
680
|
+
|
|
613
681
|
def _codex_home() -> Path:
|
|
614
682
|
return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
|
|
615
683
|
|
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -70,9 +70,20 @@ from devflow_cli.core.validation_approvals import (
|
|
|
70
70
|
)
|
|
71
71
|
from devflow_cli.core.contract_schemas import LegacySchemaError, diagnose_contract_file
|
|
72
72
|
from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
|
|
73
|
-
from devflow_cli.
|
|
73
|
+
from devflow_cli.core.result_template import (
|
|
74
|
+
remove_result_template,
|
|
75
|
+
write_result_template,
|
|
76
|
+
)
|
|
77
|
+
from devflow_cli.adapters.agents import (
|
|
78
|
+
MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS,
|
|
79
|
+
STRUCTURED_OUTPUT_MISSING,
|
|
80
|
+
StructuredOutputError,
|
|
81
|
+
describe_missing_output,
|
|
82
|
+
get_agent_adapter,
|
|
83
|
+
)
|
|
74
84
|
from devflow_cli.utils.console import console, fail, ok, warn
|
|
75
85
|
from devflow_cli.utils.paths import (
|
|
86
|
+
activity_attempt_file,
|
|
76
87
|
activity_raw_file,
|
|
77
88
|
activity_result_file,
|
|
78
89
|
project_root_for_feature,
|
|
@@ -476,6 +487,7 @@ def run_adaptive(
|
|
|
476
487
|
json_output=json_output,
|
|
477
488
|
)
|
|
478
489
|
if outcome.advanced:
|
|
490
|
+
remove_result_template(current_state.issueId, outcome.activity)
|
|
479
491
|
if json_output:
|
|
480
492
|
_print_execution_json(
|
|
481
493
|
current_state.issueId, outcome.activity, True, outcome.reason,
|
|
@@ -493,11 +505,22 @@ def run_adaptive(
|
|
|
493
505
|
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
494
506
|
safe_refresh_worklog(feature_dir)
|
|
495
507
|
exit_code = exit_code_for_run_reason(outcome.reason)
|
|
508
|
+
execution = attempt.get("execution")
|
|
509
|
+
recovery = (
|
|
510
|
+
_missing_output_recovery(
|
|
511
|
+
feature_dir, current_state.issueId, outcome.activity, agent,
|
|
512
|
+
execution.stdout,
|
|
513
|
+
)
|
|
514
|
+
if outcome.reason in {"agent-failed", "agent-failed-after-mutation"}
|
|
515
|
+
and execution is not None
|
|
516
|
+
and execution.final_output_diagnostic in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
517
|
+
else None
|
|
518
|
+
)
|
|
496
519
|
if json_output:
|
|
497
520
|
_print_execution_json(
|
|
498
521
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
499
522
|
outcome.detail, outcome.nextActivity, exit_code,
|
|
500
|
-
warnings=outcome.warnings,
|
|
523
|
+
warnings=outcome.warnings, recovery=recovery,
|
|
501
524
|
)
|
|
502
525
|
elif outcome.reason in BASE_REFUSALS:
|
|
503
526
|
fail(outcome.detail)
|
|
@@ -509,7 +532,8 @@ def run_adaptive(
|
|
|
509
532
|
f" Activite '{outcome.activity}' non validee : {outcome.reason}"
|
|
510
533
|
)
|
|
511
534
|
_print_run_recovery(
|
|
512
|
-
current_state.issueId, outcome.reason, outcome.activity, agent, feature_dir
|
|
535
|
+
current_state.issueId, outcome.reason, outcome.activity, agent, feature_dir,
|
|
536
|
+
recovery=recovery,
|
|
513
537
|
)
|
|
514
538
|
raise typer.Exit(int(exit_code))
|
|
515
539
|
|
|
@@ -611,8 +635,18 @@ def reconcile_adaptive(
|
|
|
611
635
|
fail("--reason ne peut etre utilise qu'avec --result")
|
|
612
636
|
raise typer.Exit(int(ExitCode.USAGE))
|
|
613
637
|
|
|
638
|
+
missing_output: dict[str, str] = {}
|
|
639
|
+
|
|
614
640
|
def recover(agent: str, raw_output: str) -> object:
|
|
615
|
-
|
|
641
|
+
try:
|
|
642
|
+
return get_agent_adapter(agent).recover_structured_payload(raw_output)
|
|
643
|
+
except (json.JSONDecodeError, StructuredOutputError) as exc:
|
|
644
|
+
if (
|
|
645
|
+
isinstance(exc, json.JSONDecodeError)
|
|
646
|
+
or exc.diagnostic in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
647
|
+
):
|
|
648
|
+
missing_output.update(agent=agent, raw=raw_output)
|
|
649
|
+
raise
|
|
616
650
|
|
|
617
651
|
outcome = reconcile_current_activity(
|
|
618
652
|
feature_dir,
|
|
@@ -628,9 +662,29 @@ def reconcile_adaptive(
|
|
|
628
662
|
)
|
|
629
663
|
_print_warnings(outcome.warnings)
|
|
630
664
|
_print_pending_approvals(feature_dir, state.issueId, outcome.activity)
|
|
665
|
+
remove_result_template(state.issueId, outcome.activity)
|
|
631
666
|
return
|
|
632
667
|
if outcome.reason != "agent-running": # worklog.md : fichier d'execution (B4)
|
|
633
668
|
safe_refresh_worklog(feature_dir)
|
|
669
|
+
if (
|
|
670
|
+
result is None
|
|
671
|
+
and outcome.reason == "invalid-result"
|
|
672
|
+
and missing_output
|
|
673
|
+
and _last_run_missed_structured_output(feature_dir, outcome.activity)
|
|
674
|
+
):
|
|
675
|
+
# Trace sans reponse structuree : message actionnable, pas l'erreur de lecture.
|
|
676
|
+
recovery = _missing_output_recovery(
|
|
677
|
+
feature_dir, state.issueId, outcome.activity,
|
|
678
|
+
missing_output["agent"], missing_output["raw"],
|
|
679
|
+
)
|
|
680
|
+
fail(
|
|
681
|
+
f"Reconciliation de '{outcome.activity}' refusee : {outcome.reason} "
|
|
682
|
+
f"({STRUCTURED_OUTPUT_MISSING})"
|
|
683
|
+
)
|
|
684
|
+
_print_warnings(outcome.warnings)
|
|
685
|
+
console.print(" Reprise guidee :")
|
|
686
|
+
_print_recovery_steps(recovery)
|
|
687
|
+
raise typer.Exit(int(exit_code_for_run_reason(outcome.reason)))
|
|
634
688
|
if outcome.reason in BASE_REFUSALS:
|
|
635
689
|
fail(outcome.detail)
|
|
636
690
|
else:
|
|
@@ -1372,8 +1426,99 @@ def _fallback_agents(agent: str) -> list[str]:
|
|
|
1372
1426
|
]
|
|
1373
1427
|
|
|
1374
1428
|
|
|
1429
|
+
def _last_run_missed_structured_output(feature_dir: Path, activity: str) -> bool:
|
|
1430
|
+
"""Vrai si la derniere execution de l'activite a fini sans sortie structuree."""
|
|
1431
|
+
try:
|
|
1432
|
+
audit = json.loads(
|
|
1433
|
+
(feature_dir / "agent-execution.json").read_text(encoding="utf-8")
|
|
1434
|
+
)
|
|
1435
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
1436
|
+
return False
|
|
1437
|
+
return (
|
|
1438
|
+
isinstance(audit, dict)
|
|
1439
|
+
and audit.get("activity") == activity
|
|
1440
|
+
and audit.get("finalOutputDiagnostic") in MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _missing_output_recovery(
|
|
1445
|
+
feature_dir: Path, issue_id: str, activity: str, agent: str, raw_output: str
|
|
1446
|
+
) -> dict:
|
|
1447
|
+
"""Reprise guidee d'une sortie structuree absente apres un agent termine sans erreur.
|
|
1448
|
+
|
|
1449
|
+
Le modele prerempli (contrat courant, fichiers observes depuis la base) est ecrit
|
|
1450
|
+
hors du depot ; ses champs a completer sont vides, donc refuses tels quels.
|
|
1451
|
+
"""
|
|
1452
|
+
issue = shlex.quote(issue_id)
|
|
1453
|
+
cause = describe_missing_output(agent, raw_output)
|
|
1454
|
+
recovery: dict = {
|
|
1455
|
+
"kind": STRUCTURED_OUTPUT_MISSING,
|
|
1456
|
+
"summary": f"Sortie structuree absente : {cause}.",
|
|
1457
|
+
"resultTemplate": None,
|
|
1458
|
+
"commands": [],
|
|
1459
|
+
}
|
|
1460
|
+
if activity not in MANUAL_RECONCILIATION_ACTIVITIES:
|
|
1461
|
+
recovery["commands"].append({
|
|
1462
|
+
"command": f"devflow adaptive run {issue} --agent {shlex.quote(agent)}",
|
|
1463
|
+
"purpose": "relancer l'agent : la correction manuelle est interdite pour cette activite",
|
|
1464
|
+
})
|
|
1465
|
+
return recovery
|
|
1466
|
+
try:
|
|
1467
|
+
attempt = json.loads(
|
|
1468
|
+
activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
|
|
1469
|
+
)
|
|
1470
|
+
contract = json.loads(
|
|
1471
|
+
(feature_dir / "activity-contract.json").read_text(encoding="utf-8")
|
|
1472
|
+
)
|
|
1473
|
+
if (
|
|
1474
|
+
contract["activity"] != activity
|
|
1475
|
+
or contract["contractId"] != attempt["contractId"]
|
|
1476
|
+
):
|
|
1477
|
+
raise ValueError("contrat et tentative incoherents")
|
|
1478
|
+
template = write_result_template(
|
|
1479
|
+
issue_id,
|
|
1480
|
+
activity,
|
|
1481
|
+
contract["contractId"],
|
|
1482
|
+
contract["expectedOutput"],
|
|
1483
|
+
sorted(attempt["observedFiles"]),
|
|
1484
|
+
)
|
|
1485
|
+
except (OSError, UnicodeDecodeError, KeyError, TypeError, ValueError):
|
|
1486
|
+
template = None
|
|
1487
|
+
if template is not None:
|
|
1488
|
+
recovery["resultTemplate"] = str(template)
|
|
1489
|
+
recovery["commands"].extend([
|
|
1490
|
+
{
|
|
1491
|
+
"command": "git status --short",
|
|
1492
|
+
"purpose": "inspecter les ecritures deja effectuees",
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
"command": (
|
|
1496
|
+
f"devflow adaptive reconcile {issue} --result "
|
|
1497
|
+
f"{shlex.quote(str(template)) if template else 'FICHIER.json'} --reason MOTIF"
|
|
1498
|
+
),
|
|
1499
|
+
"purpose": (
|
|
1500
|
+
"completer le modele prerempli (status, summary, checks, evidence) "
|
|
1501
|
+
"puis valider sans relancer l'agent"
|
|
1502
|
+
if template
|
|
1503
|
+
else "modele prerempli non ecrit : fournir le resultat du contrat"
|
|
1504
|
+
),
|
|
1505
|
+
},
|
|
1506
|
+
])
|
|
1507
|
+
return recovery
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def _print_recovery_steps(recovery: dict) -> None:
|
|
1511
|
+
# soft_wrap : la commande a copier reste sur une seule ligne.
|
|
1512
|
+
console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
|
|
1513
|
+
for step in recovery["commands"]:
|
|
1514
|
+
console.print(
|
|
1515
|
+
f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
|
|
1375
1519
|
def _print_run_recovery(
|
|
1376
|
-
issue_id: str, reason: str, activity: str, agent: str, feature_dir: Path
|
|
1520
|
+
issue_id: str, reason: str, activity: str, agent: str, feature_dir: Path,
|
|
1521
|
+
*, recovery: dict | None = None,
|
|
1377
1522
|
) -> None:
|
|
1378
1523
|
console.print(" Reprise guidee :")
|
|
1379
1524
|
console.print(f" devflow adaptive status {issue_id}")
|
|
@@ -1382,6 +1527,11 @@ def _print_run_recovery(
|
|
|
1382
1527
|
" Trace agent : "
|
|
1383
1528
|
f"{activity_raw_file(feature_dir, activity).name}"
|
|
1384
1529
|
)
|
|
1530
|
+
if recovery is not None:
|
|
1531
|
+
if reason == "agent-failed-after-mutation":
|
|
1532
|
+
console.print(" Ne relancez pas automatiquement l'agent : reconciliez d'abord les changements partiels.")
|
|
1533
|
+
_print_recovery_steps(recovery)
|
|
1534
|
+
return
|
|
1385
1535
|
if reason == "agent-unavailable":
|
|
1386
1536
|
console.print(" Agent indisponible : l'activite reste courante et rien n'est invalide.")
|
|
1387
1537
|
console.print(f" devflow check --ai {agent} --live # verifier le retour de l'agent")
|
|
@@ -43,6 +43,14 @@ DOCUMENT_ACTIVITIES = set(DOCUMENT_ARTIFACTS)
|
|
|
43
43
|
ActivityStatus = Literal["completed", "blocked"]
|
|
44
44
|
|
|
45
45
|
REMOVED_RESULT_FIELDS = ("omissionRecommendations", "reviewedOmissions")
|
|
46
|
+
# Commune a tous les contrats et a tous les agents : un sous-agent en arriere-plan
|
|
47
|
+
# force l'agent a rendre la main avant sa sortie structuree, qui ne s'envoie qu'une fois.
|
|
48
|
+
SUBAGENT_CONSTRAINT = (
|
|
49
|
+
"Si des sous-agents sont utilises, les lancer au premier plan, jamais en "
|
|
50
|
+
"arriere-plan, et attendre qu'ils soient tous termines avant la sortie structuree. "
|
|
51
|
+
"La sortie structuree du contrat est le dernier message de l'agent : ne rien "
|
|
52
|
+
"envoyer apres elle."
|
|
53
|
+
)
|
|
46
54
|
PROTECTED_VALIDATION_CONSTRAINT = (
|
|
47
55
|
"Les garde-fous sont actifs : inclure dans validationPlan au moins une validation "
|
|
48
56
|
"bloquante explicitement consacree a la securite, aux donnees, a l'authentification, "
|
|
@@ -244,6 +252,7 @@ def build_activity_contract(
|
|
|
244
252
|
"Signaler tout blocage au lieu de contourner une validation.",
|
|
245
253
|
"Dans checks, status vaut passed si et seulement si exitCode vaut 0. Une commande dont l'absence de resultat constitue le succes attendu doit etre declaree failed, ou remplacee par une commande qui renvoie 0 en cas de succes.",
|
|
246
254
|
"Donner un id stable a chaque objectif de check. Si un check echoue puis est corrige et relance pour le meme objectif, reutiliser cet id : seule la derniere tentative fait foi ; signaler dans remainingRisks toute instabilite encore pertinente.",
|
|
255
|
+
SUBAGENT_CONSTRAINT,
|
|
247
256
|
]
|
|
248
257
|
if activity in DOCUMENT_ACTIVITIES:
|
|
249
258
|
artifact_name = document_artifact_name(activity, reopened=reopened)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Modele de resultat prerempli pour ``adaptive reconcile --result``.
|
|
2
|
+
|
|
3
|
+
Ecrit hors du depot, dans ``~/.devflow/reconcile/`` (ou ``DEVFLOW_HOME``), quand un
|
|
4
|
+
agent a termine sans rendre sa sortie structuree. Les champs a completer sont
|
|
5
|
+
laisses vides : le modele brut est refuse par les validateurs de ``reconcile``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from devflow_cli.core.telemetry import telemetry_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def result_template_file(issue_id: str, activity: str) -> Path:
|
|
20
|
+
name = re.sub(r"[^A-Za-z0-9_-]", "_", f"{issue_id}-{activity}")
|
|
21
|
+
return telemetry_file().parent / "reconcile" / f"{name}-result.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def result_template_payload(
|
|
25
|
+
contract_id: str, expected_output: dict[str, Any], changed_files: list[str]
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
"""Champs requis du schema : faits connus de la CLI remplis, le reste vide."""
|
|
28
|
+
properties = expected_output.get("properties", {})
|
|
29
|
+
payload: dict[str, Any] = {}
|
|
30
|
+
for name in expected_output.get("required", []):
|
|
31
|
+
schema = properties.get(name, {})
|
|
32
|
+
if name == "contractId":
|
|
33
|
+
payload[name] = contract_id
|
|
34
|
+
elif name == "changedFiles":
|
|
35
|
+
payload[name] = changed_files
|
|
36
|
+
elif "const" in schema:
|
|
37
|
+
payload[name] = schema["const"]
|
|
38
|
+
elif schema.get("type") == "array":
|
|
39
|
+
payload[name] = []
|
|
40
|
+
elif schema.get("type") == "object":
|
|
41
|
+
payload[name] = {}
|
|
42
|
+
else:
|
|
43
|
+
payload[name] = ""
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_result_template(
|
|
48
|
+
issue_id: str,
|
|
49
|
+
activity: str,
|
|
50
|
+
contract_id: str,
|
|
51
|
+
expected_output: dict[str, Any],
|
|
52
|
+
changed_files: list[str],
|
|
53
|
+
) -> Path:
|
|
54
|
+
"""Ecrit le modele (dossier 0700, fichier 0600) et retourne son chemin."""
|
|
55
|
+
path = result_template_file(issue_id, activity)
|
|
56
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
57
|
+
path.parent.chmod(0o700)
|
|
58
|
+
content = json.dumps(
|
|
59
|
+
result_template_payload(contract_id, expected_output, changed_files),
|
|
60
|
+
indent=2,
|
|
61
|
+
ensure_ascii=False,
|
|
62
|
+
) + "\n"
|
|
63
|
+
path.unlink(missing_ok=True)
|
|
64
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
65
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
66
|
+
handle.write(content)
|
|
67
|
+
return path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove_result_template(issue_id: str, activity: str) -> None:
|
|
71
|
+
try:
|
|
72
|
+
result_template_file(issue_id, activity).unlink(missing_ok=True)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devflow-cli
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.2.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
|
|
@@ -102,6 +102,16 @@ qu'un agent travaille, les commandes humaines qui modifieraient l'état sont
|
|
|
102
102
|
refusées sans effet, et un fichier d'exécution modifié par l'agent est remis
|
|
103
103
|
dans son état d'avant l'agent.
|
|
104
104
|
|
|
105
|
+
L'agent Claude d'une activité est lancé avec
|
|
106
|
+
`CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` : il peut déléguer à des sous-agents,
|
|
107
|
+
mais au premier plan seulement. Effet de bord : il ne peut plus lancer de
|
|
108
|
+
commande shell en arrière-plan (serveur de test, par exemple). Tous les contrats
|
|
109
|
+
demandent en plus que la sortie structurée soit le dernier message de l'agent.
|
|
110
|
+
Si un agent termine sans erreur mais sans sortie structurée, `adaptive run`
|
|
111
|
+
nomme la cause (`structured-output-missing`) et propose
|
|
112
|
+
`devflow adaptive reconcile KS-123 --result MODELE --reason MOTIF` avec un
|
|
113
|
+
modèle de résultat prérempli, écrit dans `~/.devflow/reconcile/` et à compléter.
|
|
114
|
+
|
|
105
115
|
Chaque invocation de `adaptive run` traite une activité. La politique est dans
|
|
106
116
|
`assessment.json`; la progression et les preuves sont dans
|
|
107
117
|
`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=Y5UdblxgAc6mD_GvD_R7nfzo_uyAi09mJw3AJoB8Stk,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
|
-
devflow_cli/adapters/agents.py,sha256=
|
|
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=kYSpQWZ-xUTw1Kquv5fE2kpFjoVClqNl4Nd7pUj7FX8,62199
|
|
8
8
|
devflow_cli/commands/assess.py,sha256=lyj6TI5hXUWX55dY589fG4bKJSMztvMPxZ0HZjo0O-w,5230
|
|
9
9
|
devflow_cli/commands/check.py,sha256=hsfeOg9yDOTAIvNm6XhRmTIFgv7w9wC8zvrwm4ojfZs,6923
|
|
10
10
|
devflow_cli/commands/context.py,sha256=hwTitOHxIpJMx1X8R1jHHy9CpE4HVZpJKkyeOujxAio,6246
|
|
@@ -24,7 +24,7 @@ devflow_cli/commands/usage.py,sha256=ehH1AH5J-XfkebFx6qLBwpKN-MOoX2uUbq4LejBpNsk
|
|
|
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
26
|
devflow_cli/core/activity_base.py,sha256=haEVVgZAi3-iX75oo2-fK9GQZeFgN_xRw5U0Z0bdV5w,16343
|
|
27
|
-
devflow_cli/core/activity_contracts.py,sha256=
|
|
27
|
+
devflow_cli/core/activity_contracts.py,sha256=2-pjjxMyS7iER2eim118U5QSgOqwrP47X_HcfZ7jZ58,38717
|
|
28
28
|
devflow_cli/core/adaptive_runner.py,sha256=MLws62PW986dd00nA4m_3Y4BAR-ZmmYT3q8N6KiPU-Q,40375
|
|
29
29
|
devflow_cli/core/adaptive_workflow.py,sha256=F4uV5okQpFidZmneVZwW52XAO7gAmtuBAqBO9K9q7Uk,12173
|
|
30
30
|
devflow_cli/core/agent_failure.py,sha256=iX4q249lAHeVdD_ZF1aObmU-FX6sjfYP9dfLSnWGAD0,3771
|
|
@@ -42,6 +42,7 @@ devflow_cli/core/manifest.py,sha256=h058_m5-HJrYsNUdgfwnKts2riHiYPXUwMzPImHOs2g,
|
|
|
42
42
|
devflow_cli/core/operational.py,sha256=11iLTbyZF0ZjYTQ5HDmVCaw4bpbXpLtH2v9f1WQOrYg,1228
|
|
43
43
|
devflow_cli/core/redaction.py,sha256=o_1MDtCyvRLrk_dWlV6_7E6_mPVB1B5a0cCfTBj9j0Q,1973
|
|
44
44
|
devflow_cli/core/remediation.py,sha256=AI5fBs-F8xF3uaxgYJDNJHKxJ7V5obEtnfptMm351AU,8426
|
|
45
|
+
devflow_cli/core/result_template.py,sha256=XfrjUlSUDYJxE2nN8EJjHCXq1l06hn-wW1gDn1c1rBA,2493
|
|
45
46
|
devflow_cli/core/run_progress.py,sha256=hawaMo5sGcxvugexeryMWanFajdf46SY6b37O7dToZM,1855
|
|
46
47
|
devflow_cli/core/runtime_backup.py,sha256=dowz6a1vcJsqkl-m9wGhS4F6XGfsU2rwSNJvIZMJjhI,4345
|
|
47
48
|
devflow_cli/core/scanner.py,sha256=bcoC91Nc0Hw2P7aHpL-9ITxynLZhLTEKpOXo553a-18,4395
|
|
@@ -89,8 +90,8 @@ devflow_cli/resources/templates/contracts/api-template.md,sha256=rC5v7JBz71PznCn
|
|
|
89
90
|
devflow_cli/resources/templates/contracts/components-template.md,sha256=XIWfEXfC72sNmL5OA3V-7xIywkNDU2DBPLfyiagOWcs,484
|
|
90
91
|
devflow_cli/resources/templates/contracts/interfaces-template.md,sha256=kf8B7b6HsmN7jyRlLNU7RnZl0rTMlI5s97yVRyI-6yc,336
|
|
91
92
|
devflow_cli/resources/templates/contracts/services-template.md,sha256=cFafKCyLmq_a7aR7G0LXAaMO2z4Fuflq6iKdxfVg0vg,477
|
|
92
|
-
devflow_cli-8.
|
|
93
|
-
devflow_cli-8.
|
|
94
|
-
devflow_cli-8.
|
|
95
|
-
devflow_cli-8.
|
|
96
|
-
devflow_cli-8.
|
|
93
|
+
devflow_cli-8.2.0.dist-info/METADATA,sha256=9zPuMV6XtqesVYitXPdUdkg2KqmqP_zQ7LSRCa9jksQ,8470
|
|
94
|
+
devflow_cli-8.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
95
|
+
devflow_cli-8.2.0.dist-info/entry_points.txt,sha256=GVJ6bMvFpqyO5589iC4nFKeGTpGIW8aKn_QPPpSZ6kU,48
|
|
96
|
+
devflow_cli-8.2.0.dist-info/licenses/LICENSE,sha256=v79PvnmqOdKhQiOa_QcWxng4q8GuGscLOwi7ygUpwpo,1070
|
|
97
|
+
devflow_cli-8.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|