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.
- froid_loop/__init__.py +11 -0
- froid_loop/__main__.py +12 -0
- froid_loop/adapters/__init__.py +3 -0
- froid_loop/adapters/base.py +254 -0
- froid_loop/adapters/entrypoints.py +63 -0
- froid_loop/adapters/env_fault.py +290 -0
- froid_loop/adapters/generic.py +2013 -0
- froid_loop/adapters/mock.py +49 -0
- froid_loop/adapters/multiplexer.py +914 -0
- froid_loop/adapters/opencode_http.py +1687 -0
- froid_loop/adapters/profile.py +650 -0
- froid_loop/adapters/psmux_backend.py +1428 -0
- froid_loop/adapters/registry.py +322 -0
- froid_loop/adapters/tmux_backend.py +35 -0
- froid_loop/adapters/tmux_base.py +630 -0
- froid_loop/checks.py +187 -0
- froid_loop/cli.py +5041 -0
- froid_loop/data/__init__.py +0 -0
- froid_loop/data/froid_loop_hook.py +228 -0
- froid_loop/data/froid_loop_probe_hook.py +88 -0
- froid_loop/data/plugins/example/plugin.toml +21 -0
- froid_loop/data/plugins/tea/plugin.toml +184 -0
- froid_loop/data/plugins/tea/tea_plugin.py +258 -0
- froid_loop/data/plugins/unity/plugin.toml +140 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
- froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
- froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
- froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
- froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
- froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
- froid_loop/data/plugins/unity/unity_facts.md +17 -0
- froid_loop/data/plugins/unity/unity_plugin.py +415 -0
- froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
- froid_loop/data/plugins/unity/unity_ready.py +230 -0
- froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
- froid_loop/data/plugins/unity/unity_setup.py +551 -0
- froid_loop/data/plugins/unity/unity_teardown.py +362 -0
- froid_loop/data/profiles/antigravity.toml +52 -0
- froid_loop/data/profiles/claude.toml +85 -0
- froid_loop/data/profiles/codex.toml +22 -0
- froid_loop/data/profiles/copilot.toml +52 -0
- froid_loop/data/profiles/gemini.toml +26 -0
- froid_loop/data/profiles/opencode.toml +54 -0
- froid_loop/data/settings/core.toml +458 -0
- froid_loop/data/skills/README.md +93 -0
- froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
- froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
- froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
- froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
- froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
- froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
- froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
- froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
- froid_loop/decisions.py +202 -0
- froid_loop/deferredwork.py +2282 -0
- froid_loop/devcontract.py +892 -0
- froid_loop/diagnostics.py +1104 -0
- froid_loop/documents.py +532 -0
- froid_loop/engine.py +7732 -0
- froid_loop/envvars.py +111 -0
- froid_loop/escalation.py +225 -0
- froid_loop/events.py +266 -0
- froid_loop/fences.py +103 -0
- froid_loop/froidconfig.py +226 -0
- froid_loop/frontmatter.py +526 -0
- froid_loop/gates.py +133 -0
- froid_loop/install.py +2936 -0
- froid_loop/journal.py +178 -0
- froid_loop/machine.py +148 -0
- froid_loop/model.py +898 -0
- froid_loop/operatoractions.py +474 -0
- froid_loop/platform_util.py +1490 -0
- froid_loop/plugins/__init__.py +64 -0
- froid_loop/plugins/bus.py +259 -0
- froid_loop/plugins/context.py +319 -0
- froid_loop/plugins/loader.py +145 -0
- froid_loop/plugins/manifest.py +279 -0
- froid_loop/plugins/model.py +296 -0
- froid_loop/plugins/registry.py +245 -0
- froid_loop/plugins/trust.py +75 -0
- froid_loop/policy.py +1569 -0
- froid_loop/probe.py +1044 -0
- froid_loop/process_host.py +408 -0
- froid_loop/recovery_flow.py +1561 -0
- froid_loop/resolve.py +283 -0
- froid_loop/runs.py +4715 -0
- froid_loop/runsetup.py +1293 -0
- froid_loop/sanitize.py +593 -0
- froid_loop/settings_schema.py +276 -0
- froid_loop/signals.py +160 -0
- froid_loop/sprintstatus.py +609 -0
- froid_loop/statemachine.py +57 -0
- froid_loop/stories.py +615 -0
- froid_loop/stories_engine.py +796 -0
- froid_loop/sweep.py +1892 -0
- froid_loop/tokens.py +196 -0
- froid_loop/tui/__init__.py +11 -0
- froid_loop/tui/app.py +1584 -0
- froid_loop/tui/data.py +840 -0
- froid_loop/tui/launch.py +1003 -0
- froid_loop/tui/screens/__init__.py +1 -0
- froid_loop/tui/screens/dashboard.py +1071 -0
- froid_loop/tui/screens/modals.py +943 -0
- froid_loop/tui/screens/settings_screen.py +477 -0
- froid_loop/tui/settings.py +135 -0
- froid_loop/tui/widgets.py +981 -0
- froid_loop/verify.py +4545 -0
- froid_loop/workspace.py +320 -0
- froid_loop/worktree_flow.py +2301 -0
- froid_loop-0.11.1.dist-info/METADATA +728 -0
- froid_loop-0.11.1.dist-info/RECORD +116 -0
- froid_loop-0.11.1.dist-info/WHEEL +4 -0
- froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
- froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
froid_loop/checks.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Findings — the structured form of what ``froid-loop validate`` reports.
|
|
2
|
+
|
|
3
|
+
``validate`` accumulated two lists of prose strings, printed one as `` ok:`` and
|
|
4
|
+
the other as ``FAIL:``. That made the *outcome* addressable (the exit code) but
|
|
5
|
+
not the *checks*: a script wanting to branch on "hooks aren't registered" had to
|
|
6
|
+
match remediation sentences, which are the part most likely to be reworded. A
|
|
7
|
+
:class:`Finding` pairs each printed line with a stable ``check`` id, so the id is
|
|
8
|
+
the matchable identity and the message stays free to change.
|
|
9
|
+
|
|
10
|
+
This lives in its own module rather than in ``cli`` or ``machine``:
|
|
11
|
+
|
|
12
|
+
- ``cli`` imports ``install``, and ``install`` must *return* Findings — putting
|
|
13
|
+
the type in ``cli`` would make that a cycle.
|
|
14
|
+
- ``machine`` is deliberately narrow: transport for the ``--json`` contract, zero
|
|
15
|
+
domain knowledge. Severity is a concept no other ``--json`` command has.
|
|
16
|
+
|
|
17
|
+
:data:`VALIDATE_CHECKS` is the registry every id must be in, enforced by an
|
|
18
|
+
assert in :meth:`ValidationReport.add`. That is what keeps "one printed line =
|
|
19
|
+
exactly one Finding" true: a new check site cannot ship without first naming
|
|
20
|
+
itself here. The assert only ever fires on a literal an author just wrote, never
|
|
21
|
+
on runtime data, so it is a lint with a stack trace rather than a validation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Strict-checked under #245 Stage 2, with one rule relaxed for this file only:
|
|
25
|
+
# `ValidationReport.findings` uses the idiomatic `field(default_factory=list)`,
|
|
26
|
+
# which pyright can only infer as `list[Unknown]` (it does not fold the declared
|
|
27
|
+
# `list[Finding]` annotation back into the bare `list` factory) though the field
|
|
28
|
+
# is correctly typed. Every other strict rule stays on.
|
|
29
|
+
# pyright: reportUnknownVariableType=false
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import sys
|
|
34
|
+
from collections.abc import Mapping
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Literal
|
|
37
|
+
|
|
38
|
+
Severity = Literal["ok", "warning", "problem"]
|
|
39
|
+
|
|
40
|
+
# The id names the *gate*, not the outcome — so the same id carries the ok and
|
|
41
|
+
# the problem for one check ("adapter.binary" is both "codex found" and "codex
|
|
42
|
+
# not found on PATH"). Ids are split only where the two outcomes are genuinely
|
|
43
|
+
# different findings with different detail (skills.base vs base-missing vs
|
|
44
|
+
# base-incomplete). Scheme: <area>.<check>.
|
|
45
|
+
VALIDATE_CHECKS: frozenset[str] = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"froid-config",
|
|
48
|
+
"policy",
|
|
49
|
+
"policy.model-qualified",
|
|
50
|
+
"policy.isolation-repo-root",
|
|
51
|
+
"adapter.profile",
|
|
52
|
+
"adapter.binary",
|
|
53
|
+
"adapter.binary-unrunnable",
|
|
54
|
+
"adapter.hookless",
|
|
55
|
+
"adapter.httpx",
|
|
56
|
+
"adapter.kind",
|
|
57
|
+
"adapter.external",
|
|
58
|
+
"adapter.external-profile",
|
|
59
|
+
"queue.sprint-status",
|
|
60
|
+
"queue.sprint-status-unknown-keys",
|
|
61
|
+
"queue.stories-manifest",
|
|
62
|
+
"git.worktree-clean",
|
|
63
|
+
"git.probe",
|
|
64
|
+
"git.render-tracked",
|
|
65
|
+
"git.version",
|
|
66
|
+
"hooks.config-parse",
|
|
67
|
+
"hooks.registered",
|
|
68
|
+
"hooks.relay-present",
|
|
69
|
+
"hooks.relay-stale",
|
|
70
|
+
"mux.backend",
|
|
71
|
+
"mux.preflight",
|
|
72
|
+
"mux.backends-detected",
|
|
73
|
+
"mux.selection",
|
|
74
|
+
"mux.external-backend",
|
|
75
|
+
"host.process",
|
|
76
|
+
"host.win32-on-wsl-path",
|
|
77
|
+
"notify.desktop-unavailable",
|
|
78
|
+
"skills.base",
|
|
79
|
+
"skills.base-missing",
|
|
80
|
+
"skills.base-incomplete",
|
|
81
|
+
"skills.base-shim",
|
|
82
|
+
"skills.customize-legacy",
|
|
83
|
+
"skills.dev-renderer",
|
|
84
|
+
"skills.dev-renderer-config",
|
|
85
|
+
"skills.dev-renderer-sources",
|
|
86
|
+
"skills.review-layer-missing",
|
|
87
|
+
"skills.review-layer-unresolved",
|
|
88
|
+
"skills.review-layers-empty",
|
|
89
|
+
"skills.customize-unreadable",
|
|
90
|
+
"skills.stories-dispatch",
|
|
91
|
+
"skills.stories-dispatch-missing",
|
|
92
|
+
"skills.stories-dispatch-stale",
|
|
93
|
+
"operator.registry-stale",
|
|
94
|
+
"operator.actions-malformed",
|
|
95
|
+
"operator.confirm-interrupted",
|
|
96
|
+
"operator.park-record-missing",
|
|
97
|
+
"deferred.closes-unknown",
|
|
98
|
+
"deferred.closes-malformed",
|
|
99
|
+
"deferred.closes-entry-unreadable",
|
|
100
|
+
"deferred.hard-gate",
|
|
101
|
+
"deferred.hard-gate-unstructured",
|
|
102
|
+
"deferred.ledger-unreadable",
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Finding:
|
|
109
|
+
"""One check's outcome: a stable ``check`` id, a severity, the human line.
|
|
110
|
+
|
|
111
|
+
``message`` is the exact prose the text mode prints (minus the severity
|
|
112
|
+
prefix) and is **not** contracted — several are a bare ``str(e)`` from the
|
|
113
|
+
config/policy/profile/sprint-status exceptions, so the wording moves with
|
|
114
|
+
those modules. ``detail`` carries what the check knew before it flattened
|
|
115
|
+
itself into that sentence, keyed however suits the check.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
check: str
|
|
119
|
+
severity: Severity
|
|
120
|
+
message: str
|
|
121
|
+
detail: Mapping[str, object] | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ValidationReport:
|
|
126
|
+
"""The findings of one ``validate`` pass, in emission order.
|
|
127
|
+
|
|
128
|
+
Emission order is the order the gates ran, and it is preserved across
|
|
129
|
+
severities — :meth:`render` filters, it never re-sorts, so the text output is
|
|
130
|
+
byte-identical to the two-list form it replaced (all stdout lines in append
|
|
131
|
+
order, then all stderr lines in append order).
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
findings: list[Finding] = field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
def add(
|
|
137
|
+
self,
|
|
138
|
+
check: str,
|
|
139
|
+
severity: Severity,
|
|
140
|
+
message: str,
|
|
141
|
+
detail: Mapping[str, object] | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
assert check in VALIDATE_CHECKS, f"unregistered check id: {check!r}"
|
|
144
|
+
self.findings.append(Finding(check, severity, message, detail))
|
|
145
|
+
|
|
146
|
+
def ok(self, check: str, message: str, detail: Mapping[str, object] | None = None) -> None:
|
|
147
|
+
self.add(check, "ok", message, detail)
|
|
148
|
+
|
|
149
|
+
def warn(self, check: str, message: str, detail: Mapping[str, object] | None = None) -> None:
|
|
150
|
+
self.add(check, "warning", message, detail)
|
|
151
|
+
|
|
152
|
+
def fail(self, check: str, message: str, detail: Mapping[str, object] | None = None) -> None:
|
|
153
|
+
self.add(check, "problem", message, detail)
|
|
154
|
+
|
|
155
|
+
def extend(self, findings: list[Finding]) -> None:
|
|
156
|
+
for finding in findings:
|
|
157
|
+
self.add(finding.check, finding.severity, finding.message, finding.detail)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def passed(self) -> bool:
|
|
161
|
+
"""True when nothing failed. Warnings do not clear it and do not set it —
|
|
162
|
+
this is exactly the validate exit code (0 when True, 1 when False)."""
|
|
163
|
+
return not any(f.severity == "problem" for f in self.findings)
|
|
164
|
+
|
|
165
|
+
def counts(self) -> dict[str, int]:
|
|
166
|
+
return {
|
|
167
|
+
severity: sum(1 for f in self.findings if f.severity == severity)
|
|
168
|
+
for severity in ("ok", "warning", "problem")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def render(self) -> None:
|
|
172
|
+
"""Print the human-readable form: notes to stdout, then problems to stderr.
|
|
173
|
+
|
|
174
|
+
The doubled space in the warning line (`` ok: warning: ...``) is the
|
|
175
|
+
shipped output, not a typo to tidy: the warning sites stored
|
|
176
|
+
``" warning: " + msg`` into the same list the `` ok: `` printer walked.
|
|
177
|
+
Tidying it would be a silent text change — the validate tests
|
|
178
|
+
substring-match on a lowercased stream and would not notice.
|
|
179
|
+
"""
|
|
180
|
+
for finding in self.findings:
|
|
181
|
+
if finding.severity == "ok":
|
|
182
|
+
print(f" ok: {finding.message}")
|
|
183
|
+
elif finding.severity == "warning":
|
|
184
|
+
print(f" ok: warning: {finding.message}")
|
|
185
|
+
for finding in self.findings:
|
|
186
|
+
if finding.severity == "problem":
|
|
187
|
+
print(f"FAIL: {finding.message}", file=sys.stderr)
|