devflow-cli 9.0.0__py3-none-any.whl → 10.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/adapters/__init__.py +0 -4
- devflow_cli/adapters/agents.py +6 -272
- devflow_cli/adapters/linear.py +0 -10
- devflow_cli/cli.py +0 -18
- devflow_cli/commands/adaptive.py +26 -264
- devflow_cli/commands/check.py +3 -76
- devflow_cli/commands/feature.py +22 -0
- devflow_cli/commands/feedback.py +1 -24
- devflow_cli/commands/init_cmd.py +25 -65
- devflow_cli/commands/upgrade_cmd.py +14 -4
- devflow_cli/core/adaptive_runner.py +11 -80
- devflow_cli/core/adaptive_workflow.py +0 -7
- devflow_cli/core/automatic_evidence.py +1 -1
- devflow_cli/core/feedback.py +0 -145
- devflow_cli/core/git.py +0 -6
- devflow_cli/core/installer.py +50 -63
- devflow_cli/core/manifest.py +2 -13
- devflow_cli/core/staleness.py +0 -22
- devflow_cli/core/telemetry.py +1 -91
- devflow_cli/core/verification.py +0 -88
- devflow_cli/core/waivers.py +0 -189
- devflow_cli/core/worklog.py +27 -6
- devflow_cli/resources/commands/devflow.md +4 -4
- {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/METADATA +8 -17
- devflow_cli-10.0.0.dist-info/RECORD +62 -0
- devflow_cli/commands/context.py +0 -178
- devflow_cli/commands/export_cmd.py +0 -91
- devflow_cli/commands/extension.py +0 -139
- devflow_cli/commands/hooks_cmd.py +0 -197
- devflow_cli/commands/linear.py +0 -30
- devflow_cli/commands/stale.py +0 -93
- devflow_cli/commands/traceability.py +0 -47
- devflow_cli/commands/usage.py +0 -92
- devflow_cli/commands/verify.py +0 -111
- devflow_cli/core/catalog.py +0 -105
- devflow_cli/core/hooks.py +0 -237
- devflow_cli/core/scanner.py +0 -140
- devflow_cli/resources/commands/devflow.context.md +0 -57
- devflow_cli/resources/commands/devflow.extension.md +0 -63
- devflow_cli/resources/commands/devflow.init.md +0 -50
- devflow_cli/resources/commands/devflow.regen.md +0 -34
- devflow_cli/resources/extensions/EXTENSION-API-REFERENCE.md +0 -140
- devflow_cli/resources/extensions/catalog.community.json +0 -5
- devflow_cli/resources/extensions/catalog.json +0 -19
- devflow_cli/resources/extensions/template/example-command.md +0 -29
- devflow_cli/resources/extensions/template/example-extension.json +0 -18
- devflow_cli/resources/templates/agent-context-template.md +0 -29
- devflow_cli/resources/templates/contracts/README-template.md +0 -24
- devflow_cli/resources/templates/contracts/api-template.md +0 -38
- devflow_cli/resources/templates/contracts/components-template.md +0 -28
- devflow_cli/resources/templates/contracts/interfaces-template.md +0 -20
- devflow_cli/resources/templates/contracts/services-template.md +0 -22
- devflow_cli/resources/templates/contracts-template.md +0 -105
- devflow_cli/resources/templates/data-model-template.md +0 -55
- devflow_cli/resources/templates/devflow-hooks-template.yml +0 -38
- devflow_cli/resources/templates/extension-manifest-template.json +0 -9
- devflow_cli/resources/templates/plan-template.md +0 -97
- devflow_cli/resources/templates/quickstart-template.md +0 -64
- devflow_cli/resources/templates/spec-template.md +0 -79
- devflow_cli/resources/templates/tasks-template.md +0 -105
- devflow_cli-9.0.0.dist-info/RECORD +0 -97
- {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/WHEEL +0 -0
- {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-9.0.0.dist-info → devflow_cli-10.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/adapters/__init__.py
CHANGED
|
@@ -4,8 +4,6 @@ from devflow_cli.adapters.agents import (
|
|
|
4
4
|
AgentAdapter,
|
|
5
5
|
AgentExecution,
|
|
6
6
|
ClaudeCodeAdapter,
|
|
7
|
-
CodexAdapter,
|
|
8
|
-
CursorAdapter,
|
|
9
7
|
get_agent_adapter,
|
|
10
8
|
)
|
|
11
9
|
|
|
@@ -13,7 +11,5 @@ __all__ = [
|
|
|
13
11
|
"AgentAdapter",
|
|
14
12
|
"AgentExecution",
|
|
15
13
|
"ClaudeCodeAdapter",
|
|
16
|
-
"CodexAdapter",
|
|
17
|
-
"CursorAdapter",
|
|
18
14
|
"get_agent_adapter",
|
|
19
15
|
]
|
devflow_cli/adapters/agents.py
CHANGED
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
import subprocess
|
|
8
7
|
import re
|
|
9
8
|
import time
|
|
10
9
|
from abc import ABC, abstractmethod
|
|
11
10
|
from dataclasses import dataclass, replace
|
|
12
|
-
from datetime import datetime, timedelta
|
|
13
11
|
from pathlib import Path
|
|
14
12
|
from typing import Literal
|
|
15
13
|
|
|
@@ -23,8 +21,9 @@ PermissionMode = Literal["auto", "acceptEdits", "manual"]
|
|
|
23
21
|
SandboxMode = Literal["read-only", "workspace-write"]
|
|
24
22
|
|
|
25
23
|
# 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 ;
|
|
27
|
-
#
|
|
24
|
+
# pas le JSON attendu. Claude porte le diagnostic dedie ; les autres valeurs restent
|
|
25
|
+
# lisibles dans un agent-execution.json ecrit par une version anterieure, et sont
|
|
26
|
+
# traitees comme la meme famille par la reprise guidee.
|
|
28
27
|
STRUCTURED_OUTPUT_MISSING = "structured-output-missing"
|
|
29
28
|
MISSING_STRUCTURED_OUTPUT_DIAGNOSTICS = frozenset({
|
|
30
29
|
STRUCTURED_OUTPUT_MISSING,
|
|
@@ -40,13 +39,6 @@ STRUCTURED_OUTPUT_MISSING_MESSAGE = (
|
|
|
40
39
|
# aussi ses commandes shell en arriere-plan.
|
|
41
40
|
CLAUDE_FOREGROUND_ENV = "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS"
|
|
42
41
|
|
|
43
|
-
_SAFE_DIAGNOSTIC_MESSAGES = {
|
|
44
|
-
"final-output-empty": "--output-last-message est vide",
|
|
45
|
-
"final-output-absent": "--output-last-message est absent",
|
|
46
|
-
"agent-turn-failed": "le tour agent a echoue avant la reponse finale",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
42
|
@dataclass(frozen=True)
|
|
51
43
|
class QuotaWindow:
|
|
52
44
|
"""Evolution d'une fenetre de quota observee pendant une session agent."""
|
|
@@ -111,9 +103,6 @@ class AgentAdapter(ABC):
|
|
|
111
103
|
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
112
104
|
"""Construit une commande sans passer par un shell."""
|
|
113
105
|
|
|
114
|
-
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
115
|
-
raise ValueError(f"L'adaptateur {self.name} ne supporte pas les agents nommés")
|
|
116
|
-
|
|
117
106
|
def build_structured_command(
|
|
118
107
|
self,
|
|
119
108
|
prompt: str,
|
|
@@ -243,8 +232,6 @@ class AgentAdapter(ABC):
|
|
|
243
232
|
+ (
|
|
244
233
|
f" : {self.missing_output_message(execution.stdout)}"
|
|
245
234
|
if diagnostic == STRUCTURED_OUTPUT_MISSING
|
|
246
|
-
else f" : {_SAFE_DIAGNOSTIC_MESSAGES[diagnostic]}"
|
|
247
|
-
if diagnostic in _SAFE_DIAGNOSTIC_MESSAGES
|
|
248
235
|
else ""
|
|
249
236
|
),
|
|
250
237
|
duration_seconds=execution.duration_seconds,
|
|
@@ -330,9 +317,6 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
330
317
|
prompt,
|
|
331
318
|
]
|
|
332
319
|
|
|
333
|
-
def build_named_agent_command(self, agent_name: str, prompt: str) -> list[str]:
|
|
334
|
-
return [self.executable, "--print", "--agent", agent_name, prompt]
|
|
335
|
-
|
|
336
320
|
def build_structured_command(
|
|
337
321
|
self,
|
|
338
322
|
prompt: str,
|
|
@@ -413,153 +397,13 @@ class ClaudeCodeAdapter(AgentAdapter):
|
|
|
413
397
|
)
|
|
414
398
|
|
|
415
399
|
|
|
416
|
-
class CodexAdapter(AgentAdapter):
|
|
417
|
-
name = "codex"
|
|
418
|
-
executable = "codex"
|
|
419
|
-
|
|
420
|
-
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
421
|
-
command = [self.executable, "exec"]
|
|
422
|
-
if permission_mode in {"auto", "acceptEdits"}:
|
|
423
|
-
command.append("--approve-for-me")
|
|
424
|
-
else:
|
|
425
|
-
command.extend(["--sandbox", "workspace-write"])
|
|
426
|
-
command.append(_portable_prompt(prompt))
|
|
427
|
-
return command
|
|
428
|
-
|
|
429
|
-
def build_structured_command(
|
|
430
|
-
self,
|
|
431
|
-
prompt: str,
|
|
432
|
-
permission_mode: PermissionMode,
|
|
433
|
-
schema_file: Path,
|
|
434
|
-
output_file: Path,
|
|
435
|
-
sandbox_mode: SandboxMode = "workspace-write",
|
|
436
|
-
) -> list[str]:
|
|
437
|
-
command = [self.executable, "exec"]
|
|
438
|
-
if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
|
|
439
|
-
command.append("--approve-for-me")
|
|
440
|
-
else:
|
|
441
|
-
command.extend(["--sandbox", sandbox_mode])
|
|
442
|
-
command.extend([
|
|
443
|
-
"--json",
|
|
444
|
-
"--output-schema",
|
|
445
|
-
str(schema_file),
|
|
446
|
-
"--output-last-message",
|
|
447
|
-
str(output_file),
|
|
448
|
-
])
|
|
449
|
-
command.append(prompt)
|
|
450
|
-
return command
|
|
451
|
-
|
|
452
|
-
def session_id(self, stdout: str, stderr: str) -> str | None:
|
|
453
|
-
"""Prefere l'identifiant de fil annonce par le flux d'evenements."""
|
|
454
|
-
for event in json_line_events(stdout):
|
|
455
|
-
if event.get("type") == "thread.started" and isinstance(
|
|
456
|
-
event.get("thread_id"), str
|
|
457
|
-
):
|
|
458
|
-
return event["thread_id"]
|
|
459
|
-
return _session_id(stdout, stderr)
|
|
460
|
-
|
|
461
|
-
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
462
|
-
"""Lit la reponse finale Codex, sinon la recupere depuis le flux d'evenements."""
|
|
463
|
-
if output_file is not None and output_file.is_file():
|
|
464
|
-
payload = output_file.read_text(encoding="utf-8")
|
|
465
|
-
if payload.strip():
|
|
466
|
-
return json.loads(payload)
|
|
467
|
-
diagnostic = "final-output-empty"
|
|
468
|
-
message = "Codex a termine sans reponse finale structuree"
|
|
469
|
-
else:
|
|
470
|
-
diagnostic = "final-output-absent"
|
|
471
|
-
message = "Codex n'a pas produit le fichier de reponse finale structuree"
|
|
472
|
-
events = json_line_events(stdout)
|
|
473
|
-
if any(event.get("type") == "turn.failed" for event in events):
|
|
474
|
-
raise StructuredOutputError(
|
|
475
|
-
"agent-turn-failed",
|
|
476
|
-
"Codex a interrompu son tour avant la reponse finale",
|
|
477
|
-
)
|
|
478
|
-
final_message = _codex_final_message(events)
|
|
479
|
-
if final_message is not None:
|
|
480
|
-
return json.loads(_strip_json_fence(final_message))
|
|
481
|
-
raise StructuredOutputError(diagnostic, message)
|
|
482
|
-
|
|
483
|
-
def usage(self, stdout: str, session_id: str | None) -> AgentUsage | None:
|
|
484
|
-
"""Combine le flux --json et, au mieux, le journal de session Codex.
|
|
485
|
-
|
|
486
|
-
Le flux ne donne que les tokens ; le modele, l'effort, le nombre de
|
|
487
|
-
requetes et les quotas ne figurent que dans le journal de session, dont
|
|
488
|
-
le format est interne a Codex.
|
|
489
|
-
"""
|
|
490
|
-
totals = [
|
|
491
|
-
event["usage"]
|
|
492
|
-
for event in json_line_events(stdout)
|
|
493
|
-
if event.get("type") == "turn.completed" and isinstance(event.get("usage"), dict)
|
|
494
|
-
]
|
|
495
|
-
session = _codex_session_facts(session_id)
|
|
496
|
-
if not totals and session is None:
|
|
497
|
-
return None
|
|
498
|
-
raw_input = sum(_int(item.get("input_tokens")) for item in totals)
|
|
499
|
-
cached = sum(_int(item.get("cached_input_tokens")) for item in totals)
|
|
500
|
-
usage = AgentUsage(
|
|
501
|
-
inputTokens=max(raw_input - cached, 0),
|
|
502
|
-
cacheReadTokens=cached,
|
|
503
|
-
cacheWriteTokens=sum(_int(item.get("cache_write_input_tokens")) for item in totals),
|
|
504
|
-
outputTokens=sum(_int(item.get("output_tokens")) for item in totals),
|
|
505
|
-
reasoningTokens=sum(_int(item.get("reasoning_output_tokens")) for item in totals),
|
|
506
|
-
)
|
|
507
|
-
return replace(usage, **session) if session else usage
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
class CursorAdapter(AgentAdapter):
|
|
511
|
-
name = "cursor"
|
|
512
|
-
executable = "cursor-agent"
|
|
513
|
-
|
|
514
|
-
def build_command(self, prompt: str, permission_mode: PermissionMode) -> list[str]:
|
|
515
|
-
command = [self.executable, "--print", "--output-format", "text"]
|
|
516
|
-
if permission_mode in {"auto", "acceptEdits"}:
|
|
517
|
-
command.append("--force")
|
|
518
|
-
command.append(_portable_prompt(prompt))
|
|
519
|
-
return command
|
|
520
|
-
|
|
521
|
-
def build_structured_command(
|
|
522
|
-
self,
|
|
523
|
-
prompt: str,
|
|
524
|
-
permission_mode: PermissionMode,
|
|
525
|
-
schema_file: Path,
|
|
526
|
-
output_file: Path,
|
|
527
|
-
sandbox_mode: SandboxMode = "workspace-write",
|
|
528
|
-
) -> list[str]:
|
|
529
|
-
del output_file
|
|
530
|
-
schema = schema_file.read_text(encoding="utf-8")
|
|
531
|
-
structured_prompt = (
|
|
532
|
-
f"{prompt}\n\n"
|
|
533
|
-
"Retourne comme reponse finale uniquement un objet JSON valide "
|
|
534
|
-
"respectant exactement ce JSON Schema, sans bloc Markdown :\n"
|
|
535
|
-
f"{schema}"
|
|
536
|
-
)
|
|
537
|
-
command = [self.executable, "--print", "--output-format", "json"]
|
|
538
|
-
if permission_mode in {"auto", "acceptEdits"} and sandbox_mode == "workspace-write":
|
|
539
|
-
command.append("--force")
|
|
540
|
-
command.append(structured_prompt)
|
|
541
|
-
return command
|
|
542
|
-
|
|
543
|
-
def _structured_payload(self, stdout: str, output_file: Path | None) -> object:
|
|
544
|
-
del output_file
|
|
545
|
-
envelope = json.loads(stdout)
|
|
546
|
-
if not isinstance(envelope, dict):
|
|
547
|
-
raise ValueError("enveloppe JSON Cursor absente")
|
|
548
|
-
if envelope.get("is_error") is True or envelope.get("subtype") not in {None, "success"}:
|
|
549
|
-
raise ValueError("execution Cursor terminee en erreur")
|
|
550
|
-
result = envelope.get("result")
|
|
551
|
-
if not isinstance(result, str):
|
|
552
|
-
raise ValueError("champ result Cursor absent")
|
|
553
|
-
return json.loads(_strip_json_fence(result))
|
|
554
|
-
|
|
555
|
-
|
|
556
400
|
_AGENT_ADAPTERS = {
|
|
557
401
|
"claude": ClaudeCodeAdapter,
|
|
558
402
|
"claude-code": ClaudeCodeAdapter,
|
|
559
|
-
"codex": CodexAdapter,
|
|
560
|
-
"cursor": CursorAdapter,
|
|
561
|
-
"cursor-agent": CursorAdapter,
|
|
562
403
|
}
|
|
404
|
+
# Nom ecrit dans les enregistrements (fiche de tentative, agent-execution.json, telemetrie).
|
|
405
|
+
AGENT_NAME = "claude"
|
|
406
|
+
SUPPORTED_AGENT_NAMES = frozenset(_AGENT_ADAPTERS)
|
|
563
407
|
|
|
564
408
|
|
|
565
409
|
def get_agent_adapter(name: str) -> AgentAdapter:
|
|
@@ -579,20 +423,6 @@ def describe_missing_output(agent: str, stdout: str) -> str:
|
|
|
579
423
|
return STRUCTURED_OUTPUT_MISSING_MESSAGE
|
|
580
424
|
|
|
581
425
|
|
|
582
|
-
def _portable_prompt(prompt: str) -> str:
|
|
583
|
-
"""Traduit une slash-command Claude en instruction portable pour les autres agents."""
|
|
584
|
-
match = re.fullmatch(r"/devflow\.([a-z0-9-]+)(?:\s+(.*))?", prompt.strip())
|
|
585
|
-
if not match:
|
|
586
|
-
return prompt
|
|
587
|
-
command_name, arguments = match.groups()
|
|
588
|
-
arguments = arguments or ""
|
|
589
|
-
return (
|
|
590
|
-
f"Exécute strictement le workflow décrit dans "
|
|
591
|
-
f"commands/devflow.{command_name}.md avec $ARGUMENTS = {arguments!r}. "
|
|
592
|
-
"Travaille dans le dépôt courant et respecte toutes les validations de ce fichier."
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
|
|
596
426
|
def _claude_compatible_schema(schema_file: Path) -> str:
|
|
597
427
|
"""Adapte l'en-tete Draft 2020-12 au format accepte par Claude Code."""
|
|
598
428
|
raw_schema = schema_file.read_text(encoding="utf-8")
|
|
@@ -646,17 +476,6 @@ def json_line_events(stdout: str) -> list[dict]:
|
|
|
646
476
|
return events
|
|
647
477
|
|
|
648
478
|
|
|
649
|
-
def _codex_final_message(events: list[dict]) -> str | None:
|
|
650
|
-
"""Retourne le dernier message d'agent complet du flux d'evenements."""
|
|
651
|
-
for event in reversed(events):
|
|
652
|
-
item = event.get("item")
|
|
653
|
-
if event.get("type") != "item.completed" or not isinstance(item, dict):
|
|
654
|
-
continue
|
|
655
|
-
if item.get("type") == "agent_message" and isinstance(item.get("text"), str):
|
|
656
|
-
return item["text"]
|
|
657
|
-
return None
|
|
658
|
-
|
|
659
|
-
|
|
660
479
|
def _strip_json_fence(value: str) -> str:
|
|
661
480
|
text = value.strip()
|
|
662
481
|
match = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", text, re.IGNORECASE)
|
|
@@ -676,88 +495,3 @@ def _int(value: object) -> int:
|
|
|
676
495
|
|
|
677
496
|
def _count(value: object) -> int | None:
|
|
678
497
|
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _codex_home() -> Path:
|
|
682
|
-
return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
def _codex_session_file(session_id: str | None) -> Path | None:
|
|
686
|
-
"""Retrouve le journal d'une session Codex recente a partir de son identifiant."""
|
|
687
|
-
if not session_id or not re.fullmatch(r"[A-Za-z0-9-]{8,}", session_id):
|
|
688
|
-
return None
|
|
689
|
-
sessions = _codex_home() / "sessions"
|
|
690
|
-
today = datetime.now()
|
|
691
|
-
for offset in range(3):
|
|
692
|
-
day = today - timedelta(days=offset)
|
|
693
|
-
directory = sessions / f"{day:%Y}" / f"{day:%m}" / f"{day:%d}"
|
|
694
|
-
matches = sorted(directory.glob(f"rollout-*{session_id}.jsonl"))
|
|
695
|
-
if matches:
|
|
696
|
-
return matches[-1]
|
|
697
|
-
return None
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def _codex_session_facts(session_id: str | None) -> dict | None:
|
|
701
|
-
"""Extrait modele, effort, requetes et quotas du journal de session Codex."""
|
|
702
|
-
path = _codex_session_file(session_id)
|
|
703
|
-
if path is None:
|
|
704
|
-
return None
|
|
705
|
-
models: list[str] = []
|
|
706
|
-
effort: str | None = None
|
|
707
|
-
totals: set[int] = set()
|
|
708
|
-
first_context: int | None = None
|
|
709
|
-
limits: list[dict] = []
|
|
710
|
-
with path.open(encoding="utf-8", errors="replace") as handle:
|
|
711
|
-
for event in json_line_events(handle.read()):
|
|
712
|
-
payload = event.get("payload")
|
|
713
|
-
if not isinstance(payload, dict):
|
|
714
|
-
continue
|
|
715
|
-
if event.get("type") == "turn_context":
|
|
716
|
-
model = payload.get("model")
|
|
717
|
-
if isinstance(model, str) and model not in models:
|
|
718
|
-
models.append(model)
|
|
719
|
-
if isinstance(payload.get("effort"), str):
|
|
720
|
-
effort = payload["effort"]
|
|
721
|
-
elif event.get("type") == "event_msg" and payload.get("type") == "token_count":
|
|
722
|
-
info = payload.get("info")
|
|
723
|
-
if isinstance(info, dict) and isinstance(info.get("total_token_usage"), dict):
|
|
724
|
-
marker = _int(info["total_token_usage"].get("total_tokens"))
|
|
725
|
-
if marker not in totals:
|
|
726
|
-
totals.add(marker)
|
|
727
|
-
last = info.get("last_token_usage")
|
|
728
|
-
if first_context is None and isinstance(last, dict):
|
|
729
|
-
first_context = _int(last.get("input_tokens")) or None
|
|
730
|
-
if isinstance(payload.get("rate_limits"), dict):
|
|
731
|
-
limits.append(payload["rate_limits"])
|
|
732
|
-
return {
|
|
733
|
-
"models": tuple(models),
|
|
734
|
-
"effort": effort,
|
|
735
|
-
"turns": len(totals) or None,
|
|
736
|
-
"firstContextTokens": first_context,
|
|
737
|
-
"quota": _quota_windows(limits),
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
def _quota_windows(limits: list[dict]) -> tuple[QuotaWindow, ...]:
|
|
742
|
-
"""Compare le premier et le dernier releve de chaque fenetre de quota."""
|
|
743
|
-
windows: list[QuotaWindow] = []
|
|
744
|
-
if not limits:
|
|
745
|
-
return ()
|
|
746
|
-
for key in ("primary", "secondary"):
|
|
747
|
-
first, last = limits[0].get(key), limits[-1].get(key)
|
|
748
|
-
if not isinstance(first, dict) or not isinstance(last, dict):
|
|
749
|
-
continue
|
|
750
|
-
start, end = first.get("used_percent"), last.get("used_percent")
|
|
751
|
-
if not isinstance(start, (int, float)) or not isinstance(end, (int, float)):
|
|
752
|
-
continue
|
|
753
|
-
minutes = last.get("window_minutes")
|
|
754
|
-
same_window = first.get("resets_at") == last.get("resets_at")
|
|
755
|
-
windows.append(
|
|
756
|
-
QuotaWindow(
|
|
757
|
-
windowMinutes=minutes if isinstance(minutes, int) else None,
|
|
758
|
-
usedPercentStart=float(start),
|
|
759
|
-
usedPercentEnd=float(end),
|
|
760
|
-
deltaPoints=float(end) - float(start) if same_window else None,
|
|
761
|
-
)
|
|
762
|
-
)
|
|
763
|
-
return tuple(windows)
|
devflow_cli/adapters/linear.py
CHANGED
|
@@ -52,16 +52,6 @@ class LinearDecision:
|
|
|
52
52
|
def should_update(self) -> bool:
|
|
53
53
|
return self.action == "update"
|
|
54
54
|
|
|
55
|
-
def to_dict(self) -> dict[str, str | bool | None]:
|
|
56
|
-
return {
|
|
57
|
-
"action": self.action,
|
|
58
|
-
"activity": self.activity,
|
|
59
|
-
"currentStatus": self.current_status,
|
|
60
|
-
"targetStatus": self.target_status,
|
|
61
|
-
"shouldUpdate": self.should_update,
|
|
62
|
-
"reason": self.reason,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
55
|
|
|
66
56
|
def decide_linear_transition(activity: str, current_status: str | None) -> LinearDecision:
|
|
67
57
|
"""Applique le mapping et la règle de non-régression canonique."""
|
devflow_cli/cli.py
CHANGED
|
@@ -13,20 +13,11 @@ from devflow_cli.commands.init_cmd import init
|
|
|
13
13
|
from devflow_cli.commands.check import check
|
|
14
14
|
from devflow_cli.commands.status import status
|
|
15
15
|
from devflow_cli.commands.feature import feature
|
|
16
|
-
from devflow_cli.commands.context import context
|
|
17
|
-
from devflow_cli.commands.export_cmd import export
|
|
18
|
-
from devflow_cli.commands.stale import stale
|
|
19
16
|
from devflow_cli.commands.upgrade_cmd import upgrade
|
|
20
|
-
from devflow_cli.commands import extension
|
|
21
|
-
from devflow_cli.commands import hooks_cmd
|
|
22
|
-
from devflow_cli.commands import linear
|
|
23
17
|
from devflow_cli.commands import adaptive
|
|
24
18
|
from devflow_cli.commands import feedback
|
|
25
19
|
from devflow_cli.commands.docs_sync import docs_sync
|
|
26
|
-
from devflow_cli.commands.traceability import traceability
|
|
27
|
-
from devflow_cli.commands.verify import verify
|
|
28
20
|
from devflow_cli.commands.assess import assess
|
|
29
|
-
from devflow_cli.commands.usage import usage
|
|
30
21
|
|
|
31
22
|
class DevflowGroup(TyperGroup):
|
|
32
23
|
"""Affiche un refus actionnable, sans trace, pour un fichier de feature 6.x."""
|
|
@@ -66,20 +57,11 @@ app.command()(init)
|
|
|
66
57
|
app.command()(check)
|
|
67
58
|
app.command()(status)
|
|
68
59
|
app.command()(feature)
|
|
69
|
-
app.command()(context)
|
|
70
|
-
app.command(name="export")(export)
|
|
71
|
-
app.command()(stale)
|
|
72
60
|
app.command()(upgrade)
|
|
73
|
-
app.add_typer(extension.app, name="extension")
|
|
74
|
-
app.add_typer(hooks_cmd.app, name="hooks")
|
|
75
|
-
app.add_typer(linear.app, name="linear")
|
|
76
61
|
app.add_typer(adaptive.app, name="adaptive")
|
|
77
62
|
app.add_typer(feedback.app, name="feedback")
|
|
78
63
|
app.command(name="docs-sync")(docs_sync)
|
|
79
|
-
app.command()(traceability)
|
|
80
|
-
app.command()(verify)
|
|
81
64
|
app.command()(assess)
|
|
82
|
-
app.command()(usage)
|
|
83
65
|
|
|
84
66
|
|
|
85
67
|
if __name__ == "__main__":
|