froid-loop 0.11.1__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.
Files changed (116) hide show
  1. froid_loop/__init__.py +11 -0
  2. froid_loop/__main__.py +12 -0
  3. froid_loop/adapters/__init__.py +3 -0
  4. froid_loop/adapters/base.py +254 -0
  5. froid_loop/adapters/entrypoints.py +63 -0
  6. froid_loop/adapters/env_fault.py +290 -0
  7. froid_loop/adapters/generic.py +2013 -0
  8. froid_loop/adapters/mock.py +49 -0
  9. froid_loop/adapters/multiplexer.py +914 -0
  10. froid_loop/adapters/opencode_http.py +1687 -0
  11. froid_loop/adapters/profile.py +650 -0
  12. froid_loop/adapters/psmux_backend.py +1428 -0
  13. froid_loop/adapters/registry.py +322 -0
  14. froid_loop/adapters/tmux_backend.py +35 -0
  15. froid_loop/adapters/tmux_base.py +630 -0
  16. froid_loop/checks.py +187 -0
  17. froid_loop/cli.py +5041 -0
  18. froid_loop/data/__init__.py +0 -0
  19. froid_loop/data/froid_loop_hook.py +228 -0
  20. froid_loop/data/froid_loop_probe_hook.py +88 -0
  21. froid_loop/data/plugins/example/plugin.toml +21 -0
  22. froid_loop/data/plugins/tea/plugin.toml +184 -0
  23. froid_loop/data/plugins/tea/tea_plugin.py +258 -0
  24. froid_loop/data/plugins/unity/plugin.toml +140 -0
  25. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
  26. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
  27. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
  28. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
  29. froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
  30. froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
  31. froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
  32. froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
  33. froid_loop/data/plugins/unity/unity_facts.md +17 -0
  34. froid_loop/data/plugins/unity/unity_plugin.py +415 -0
  35. froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
  36. froid_loop/data/plugins/unity/unity_ready.py +230 -0
  37. froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
  38. froid_loop/data/plugins/unity/unity_setup.py +551 -0
  39. froid_loop/data/plugins/unity/unity_teardown.py +362 -0
  40. froid_loop/data/profiles/antigravity.toml +52 -0
  41. froid_loop/data/profiles/claude.toml +85 -0
  42. froid_loop/data/profiles/codex.toml +22 -0
  43. froid_loop/data/profiles/copilot.toml +52 -0
  44. froid_loop/data/profiles/gemini.toml +26 -0
  45. froid_loop/data/profiles/opencode.toml +54 -0
  46. froid_loop/data/settings/core.toml +458 -0
  47. froid_loop/data/skills/README.md +93 -0
  48. froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
  49. froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
  50. froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
  51. froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
  52. froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
  53. froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
  54. froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
  55. froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
  56. froid_loop/decisions.py +202 -0
  57. froid_loop/deferredwork.py +2282 -0
  58. froid_loop/devcontract.py +892 -0
  59. froid_loop/diagnostics.py +1104 -0
  60. froid_loop/documents.py +532 -0
  61. froid_loop/engine.py +7732 -0
  62. froid_loop/envvars.py +111 -0
  63. froid_loop/escalation.py +225 -0
  64. froid_loop/events.py +266 -0
  65. froid_loop/fences.py +103 -0
  66. froid_loop/froidconfig.py +226 -0
  67. froid_loop/frontmatter.py +526 -0
  68. froid_loop/gates.py +133 -0
  69. froid_loop/install.py +2936 -0
  70. froid_loop/journal.py +178 -0
  71. froid_loop/machine.py +148 -0
  72. froid_loop/model.py +898 -0
  73. froid_loop/operatoractions.py +474 -0
  74. froid_loop/platform_util.py +1490 -0
  75. froid_loop/plugins/__init__.py +64 -0
  76. froid_loop/plugins/bus.py +259 -0
  77. froid_loop/plugins/context.py +319 -0
  78. froid_loop/plugins/loader.py +145 -0
  79. froid_loop/plugins/manifest.py +279 -0
  80. froid_loop/plugins/model.py +296 -0
  81. froid_loop/plugins/registry.py +245 -0
  82. froid_loop/plugins/trust.py +75 -0
  83. froid_loop/policy.py +1569 -0
  84. froid_loop/probe.py +1044 -0
  85. froid_loop/process_host.py +408 -0
  86. froid_loop/recovery_flow.py +1561 -0
  87. froid_loop/resolve.py +283 -0
  88. froid_loop/runs.py +4715 -0
  89. froid_loop/runsetup.py +1293 -0
  90. froid_loop/sanitize.py +593 -0
  91. froid_loop/settings_schema.py +276 -0
  92. froid_loop/signals.py +160 -0
  93. froid_loop/sprintstatus.py +609 -0
  94. froid_loop/statemachine.py +57 -0
  95. froid_loop/stories.py +615 -0
  96. froid_loop/stories_engine.py +796 -0
  97. froid_loop/sweep.py +1892 -0
  98. froid_loop/tokens.py +196 -0
  99. froid_loop/tui/__init__.py +11 -0
  100. froid_loop/tui/app.py +1584 -0
  101. froid_loop/tui/data.py +840 -0
  102. froid_loop/tui/launch.py +1003 -0
  103. froid_loop/tui/screens/__init__.py +1 -0
  104. froid_loop/tui/screens/dashboard.py +1071 -0
  105. froid_loop/tui/screens/modals.py +943 -0
  106. froid_loop/tui/screens/settings_screen.py +477 -0
  107. froid_loop/tui/settings.py +135 -0
  108. froid_loop/tui/widgets.py +981 -0
  109. froid_loop/verify.py +4545 -0
  110. froid_loop/workspace.py +320 -0
  111. froid_loop/worktree_flow.py +2301 -0
  112. froid_loop-0.11.1.dist-info/METADATA +728 -0
  113. froid_loop-0.11.1.dist-info/RECORD +116 -0
  114. froid_loop-0.11.1.dist-info/WHEEL +4 -0
  115. froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
  116. froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
froid_loop/tokens.py ADDED
@@ -0,0 +1,196 @@
1
+ """Token accounting from coding-CLI transcript files.
2
+
3
+ One parser per CLI family, selected by the profile's `usage_parser` key. All
4
+ parsers are tolerant — unknown lines and shapes are skipped, never fatal; a
5
+ transcript that yields nothing reads as None (untracked), not zero.
6
+
7
+ - claude-jsonl: ~/.claude/projects/<munged-cwd>/<session-id>.jsonl;
8
+ assistant entries carry an API `usage` block, summed.
9
+ - codex-rollout: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl; `token_count`
10
+ event payloads carry CUMULATIVE totals, so the last one is
11
+ the session usage (sessions here are fresh per task).
12
+ - gemini-chat: ~/.gemini/tmp/<project>/chats/session-*.jsonl — a JSONL
13
+ patch stream: bare message entries (and messages inside
14
+ "$set" patches) carry a per-API-call `tokens` block
15
+ {input, output, cached, thoughts, tool, total}; the same
16
+ message id is re-emitted as it accretes content, so the
17
+ last occurrence per id wins, then unique messages are
18
+ summed. `input` includes the cached portion.
19
+ - copilot-events: ~/.copilot/session-state/<session>/events.jsonl; some
20
+ entries carry `data.modelMetrics.<model>.usage`
21
+ {inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
22
+ reasoningTokens} that is CUMULATIVE per model, so the last
23
+ entry bearing modelMetrics holds the session totals (summed
24
+ across models). reasoningTokens fold into output.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ from .model import TokenUsage
34
+
35
+
36
+ def read_usage(parser: str, transcript_path: Path) -> TokenUsage | None:
37
+ if parser == "claude-jsonl":
38
+ return tally(transcript_path)
39
+ if parser == "codex-rollout":
40
+ return tally_codex_rollout(transcript_path)
41
+ if parser == "gemini-chat":
42
+ return tally_gemini_chat(transcript_path)
43
+ if parser == "copilot-events":
44
+ return tally_copilot_events(transcript_path)
45
+ return None
46
+
47
+
48
+ def _jsonl_entries(transcript_path: Path):
49
+ if not transcript_path.is_file():
50
+ return
51
+ with transcript_path.open(encoding="utf-8") as f:
52
+ for line in f:
53
+ line = line.strip()
54
+ if not line:
55
+ continue
56
+ try:
57
+ entry = json.loads(line)
58
+ except json.JSONDecodeError:
59
+ continue
60
+ if isinstance(entry, dict):
61
+ yield entry
62
+
63
+
64
+ def _int(value: Any) -> int:
65
+ try:
66
+ return int(value or 0)
67
+ except (TypeError, ValueError):
68
+ return 0
69
+
70
+
71
+ # ------------------------------------------------------------- claude-jsonl
72
+
73
+
74
+ def _usage_block(entry: dict[str, Any]) -> dict[str, Any] | None:
75
+ message = entry.get("message")
76
+ if isinstance(message, dict) and isinstance(message.get("usage"), dict):
77
+ return message["usage"]
78
+ if isinstance(entry.get("usage"), dict):
79
+ return entry["usage"]
80
+ return None
81
+
82
+
83
+ def tally(transcript_path: Path) -> TokenUsage:
84
+ total = TokenUsage()
85
+ for entry in _jsonl_entries(transcript_path):
86
+ usage = _usage_block(entry)
87
+ if not usage:
88
+ continue
89
+ total.add(
90
+ TokenUsage(
91
+ input_tokens=_int(usage.get("input_tokens")),
92
+ output_tokens=_int(usage.get("output_tokens")),
93
+ cache_read_tokens=_int(usage.get("cache_read_input_tokens")),
94
+ cache_creation_tokens=_int(usage.get("cache_creation_input_tokens")),
95
+ )
96
+ )
97
+ return total
98
+
99
+
100
+ # ----------------------------------------------------------- codex-rollout
101
+
102
+
103
+ def tally_codex_rollout(transcript_path: Path) -> TokenUsage | None:
104
+ last: dict[str, Any] | None = None
105
+ for entry in _jsonl_entries(transcript_path):
106
+ payload = entry.get("payload")
107
+ if not isinstance(payload, dict) or payload.get("type") != "token_count":
108
+ continue
109
+ info = payload.get("info")
110
+ if not isinstance(info, dict):
111
+ continue
112
+ totals = info.get("total_token_usage")
113
+ last = totals if isinstance(totals, dict) else info
114
+ if last is None:
115
+ return None
116
+ cached = _int(last.get("cached_input_tokens"))
117
+ return TokenUsage(
118
+ # Codex's input_tokens includes the cached portion; split it out.
119
+ input_tokens=max(0, _int(last.get("input_tokens")) - cached),
120
+ output_tokens=_int(last.get("output_tokens")),
121
+ cache_read_tokens=cached,
122
+ )
123
+
124
+
125
+ # ------------------------------------------------------------- gemini-chat
126
+
127
+
128
+ def _gemini_messages(entry: dict[str, Any]):
129
+ if "id" in entry:
130
+ yield entry
131
+ set_patch = entry.get("$set")
132
+ if isinstance(set_patch, dict):
133
+ messages = set_patch.get("messages")
134
+ if isinstance(messages, list):
135
+ for message in messages:
136
+ if isinstance(message, dict):
137
+ yield message
138
+
139
+
140
+ def tally_gemini_chat(transcript_path: Path) -> TokenUsage | None:
141
+ by_id: dict[str, dict[str, Any]] = {}
142
+ for entry in _jsonl_entries(transcript_path):
143
+ for message in _gemini_messages(entry):
144
+ tokens = message.get("tokens")
145
+ message_id = message.get("id")
146
+ if isinstance(tokens, dict) and isinstance(message_id, str):
147
+ by_id[message_id] = tokens
148
+ if not by_id:
149
+ return None
150
+ total = TokenUsage()
151
+ for tokens in by_id.values():
152
+ cached = _int(tokens.get("cached"))
153
+ total.add(
154
+ TokenUsage(
155
+ input_tokens=max(0, _int(tokens.get("input")) - cached),
156
+ output_tokens=_int(tokens.get("output")) + _int(tokens.get("thoughts")),
157
+ cache_read_tokens=cached,
158
+ )
159
+ )
160
+ return total
161
+
162
+
163
+ # --------------------------------------------------------- copilot-events
164
+
165
+
166
+ def tally_copilot_events(transcript_path: Path) -> TokenUsage | None:
167
+ # data.modelMetrics.<model>.usage is cumulative per model, so the LAST entry
168
+ # carrying modelMetrics holds the session totals; sum across its models.
169
+ # (Schema verified against a single Copilot CLI 1.0.63 events.jsonl — revisit
170
+ # the cumulative/multi-model assumption if a newer build disagrees.)
171
+ last: dict[str, Any] | None = None
172
+ for entry in _jsonl_entries(transcript_path):
173
+ data = entry.get("data")
174
+ if not isinstance(data, dict):
175
+ continue
176
+ metrics = data.get("modelMetrics")
177
+ if isinstance(metrics, dict) and metrics:
178
+ last = metrics
179
+ if last is None:
180
+ return None
181
+ total = TokenUsage()
182
+ for model_metrics in last.values():
183
+ if not isinstance(model_metrics, dict):
184
+ continue
185
+ usage = model_metrics.get("usage")
186
+ if not isinstance(usage, dict):
187
+ continue
188
+ total.add(
189
+ TokenUsage(
190
+ input_tokens=_int(usage.get("inputTokens")),
191
+ output_tokens=_int(usage.get("outputTokens")) + _int(usage.get("reasoningTokens")),
192
+ cache_read_tokens=_int(usage.get("cacheReadTokens")),
193
+ cache_creation_tokens=_int(usage.get("cacheWriteTokens")),
194
+ )
195
+ )
196
+ return total
@@ -0,0 +1,11 @@
1
+ """froid-loop TUI (optional `froid-loop[tui]` extra).
2
+
3
+ Only `launch` is importable on a core-only install: `data` needs pyte and rich
4
+ (it never imports textual), and every other submodule may import textual. The
5
+ core-safe run inventory lives in `froid_loop.runs` — `data` re-exports it (#650).
6
+
7
+ It is the submodule the core CLI paths reach for (attach/park/sweep), and being
8
+ stdlib-only is what keeps those paths working extra-less. Every other
9
+ submodule is loaded lazily by the `tui` command, behind the guard that turns a
10
+ missing extra into an install hint.
11
+ """