frontface-flashy 1.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.
flashy/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """flashy — front-facing conversion of the flashy execution skill.
2
+
3
+ Persistent execution discipline for long tasks: lock the objective,
4
+ expand it into milestones, track live state, detect drift and artificial
5
+ stopping points, checkpoint honestly on genuine limits, and gate
6
+ completion on a quality checklist. A draft is not a delivery.
7
+
8
+ No prerequisites. The mechanism is the whole product.
9
+ See README.md for the plain-language version. See CHANGELOG.md for what
10
+ was decoupled from the original skill.
11
+ """
12
+
13
+ from . import checkpoints, detectors, quality_gate, session, state_machine
14
+ from .checkpoints import (
15
+ Checkpoint, CheckpointError, make_resume_token, parse_resume_token,
16
+ )
17
+ from .detectors import (
18
+ ARTIFICIAL_STOP_PHRASES, PERMISSION_PHRASES, DRIFT_QUESTIONS,
19
+ DRIFT_CORRECTION, HIGH, MEDIUM, LOW, CONFIDENCE_LEVELS,
20
+ CONFIDENCE_GUIDANCE, DetectorError, detect_artificial_stop,
21
+ asks_permission_to_continue, should_pause, is_implementation_changing,
22
+ )
23
+ from .quality_gate import QualityGate, GateError, GATE_ITEMS
24
+ from .session import MissionSession, Milestone
25
+ from .state_machine import (
26
+ ExecutionStateMachine, TransitionError,
27
+ LOCKED, PLANNING, EXECUTING, VERIFYING, DONE, BLOCKED,
28
+ )
29
+
30
+ __version__ = "1.0.0"
31
+ __all__ = [
32
+ "checkpoints", "detectors", "quality_gate", "session", "state_machine",
33
+ "Checkpoint", "CheckpointError", "make_resume_token", "parse_resume_token",
34
+ "ARTIFICIAL_STOP_PHRASES", "PERMISSION_PHRASES", "DRIFT_QUESTIONS",
35
+ "DRIFT_CORRECTION", "HIGH", "MEDIUM", "LOW", "CONFIDENCE_LEVELS",
36
+ "CONFIDENCE_GUIDANCE", "DetectorError", "detect_artificial_stop",
37
+ "asks_permission_to_continue", "should_pause", "is_implementation_changing",
38
+ "QualityGate", "GateError", "GATE_ITEMS",
39
+ "MissionSession", "Milestone",
40
+ "ExecutionStateMachine", "TransitionError",
41
+ "LOCKED", "PLANNING", "EXECUTING", "VERIFYING", "DONE", "BLOCKED",
42
+ ]
flashy/checkpoints.py ADDED
@@ -0,0 +1,195 @@
1
+ """Checkpoints and resume tokens: honest "not done yet" state.
2
+
3
+ A checkpoint is emitted only on a genuine limit — a context/length limit,
4
+ a session or tool interruption, or a low-confidence blocker with no safe
5
+ default. Never on fatigue, never on "this section feels done," never as a
6
+ soft way to stop early.
7
+
8
+ The resume token is a short, copy-pasteable pointer, not a cryptographic
9
+ token: `finish:<slug>-m<N>` (package:slug-milestoneIndex). Quoting it on
10
+ resume must let the session reconstruct where things stood from the
11
+ checkpoint block alone.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import re
19
+ from dataclasses import dataclass, field
20
+
21
+ TOKEN_RE = re.compile(r"^finish:([a-z0-9][a-z0-9-]*)-m(\d+)$")
22
+
23
+
24
+ class CheckpointError(ValueError):
25
+ """A malformed checkpoint or resume token."""
26
+
27
+
28
+ def make_resume_token(slug: str, milestone_index: int) -> str:
29
+ """Build a resume token like `finish:debate-sim-m3`."""
30
+ slug = slug.strip().lower().replace(" ", "-")
31
+ if not slug or milestone_index < 0:
32
+ raise CheckpointError("Resume token needs a slug and a non-negative index.")
33
+ return f"finish:{slug}-m{milestone_index}"
34
+
35
+
36
+ def parse_resume_token(token: str) -> tuple[str, int]:
37
+ """Split a resume token into (slug, milestone_index). Validates format."""
38
+ m = TOKEN_RE.match(token.strip())
39
+ if not m:
40
+ raise CheckpointError(
41
+ f"Malformed resume token: {token!r}. Expected finish:<slug>-m<N>."
42
+ )
43
+ return m.group(1), int(m.group(2))
44
+
45
+
46
+ @dataclass
47
+ class Checkpoint:
48
+ objective: str
49
+ done: list[str] = field(default_factory=list)
50
+ current: str = ""
51
+ remaining: list[str] = field(default_factory=list)
52
+ next_action: str = ""
53
+ resume_token: str = ""
54
+ blockers: list[str] = field(default_factory=list)
55
+ # PARAM-FLASHY-004: SHA-256 binding over the content fields, stamped at
56
+ # write time. resume() verifies it; a mismatch aborts the resume.
57
+ digest: str = ""
58
+ # Set only via resume(allow_manual_edit=True): the operator explicitly
59
+ # acknowledged a hand edit. Rendered into the block so the override is
60
+ # visible, never silent. Excluded from the digest input.
61
+ manual_edit_override: bool = False
62
+
63
+ def __post_init__(self) -> None:
64
+ if not self.objective.strip():
65
+ raise CheckpointError("A checkpoint needs the locked objective.")
66
+ if not self.resume_token.strip():
67
+ raise CheckpointError("A checkpoint needs a resume token.")
68
+ parse_resume_token(self.resume_token) # validate format now
69
+ if not self.digest:
70
+ self.digest = self._compute_digest()
71
+
72
+ def _compute_digest(self) -> str:
73
+ """SHA-256 over the canonical content payload.
74
+
75
+ Covers objective, done-list, and core state. The resume token is a
76
+ pointer, not content; the digest itself and the override flag are
77
+ resume-act metadata — all three are excluded.
78
+ """
79
+ payload = json.dumps(
80
+ {
81
+ "objective": self.objective,
82
+ "done": list(self.done),
83
+ "current": self.current,
84
+ "remaining": list(self.remaining),
85
+ "next_action": self.next_action,
86
+ "blockers": list(self.blockers),
87
+ },
88
+ sort_keys=True,
89
+ )
90
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
91
+
92
+ def verify(self) -> None:
93
+ """Verify the checkpoint's content against its digest binding.
94
+
95
+ Raises CheckpointError on a missing digest (predates binding) or a
96
+ mismatch (content modified after the checkpoint was written).
97
+ """
98
+ if not self.digest:
99
+ raise CheckpointError(
100
+ "Checkpoint carries no digest — it predates digest binding "
101
+ "and cannot be integrity-verified. Re-emit the checkpoint "
102
+ "to bind it."
103
+ )
104
+ if self.digest != self._compute_digest():
105
+ raise CheckpointError(
106
+ "Checkpoint digest mismatch: the content was modified after "
107
+ "the checkpoint was written. Resume aborted — re-verify the "
108
+ "checkpoint, or resume with allow_manual_edit=True to "
109
+ "acknowledge the hand edit explicitly."
110
+ )
111
+
112
+ @classmethod
113
+ def resume(cls, text: str, *, allow_manual_edit: bool = False) -> "Checkpoint":
114
+ """Parse a checkpoint block and verify its digest binding.
115
+
116
+ Returns the reconstructed Checkpoint. On digest mismatch the resume
117
+ aborts with CheckpointError — unless allow_manual_edit=True, in
118
+ which case the checkpoint is returned with manual_edit_override set
119
+ (the override is rendered into the block, never silent).
120
+ """
121
+ cp = cls.parse(text)
122
+ if not cp.digest:
123
+ # Pre-binding block: no write-time attestation exists. The
124
+ # bypass is for acknowledged hand edits (digest mismatch), not
125
+ # for unverifiable provenance — refuse outright, no override.
126
+ raise CheckpointError(
127
+ "Checkpoint carries no digest — it predates digest binding "
128
+ "and cannot be integrity-verified. Re-emit the checkpoint "
129
+ "to bind it."
130
+ )
131
+ try:
132
+ cp.verify()
133
+ except CheckpointError:
134
+ if not allow_manual_edit:
135
+ raise
136
+ cp.manual_edit_override = True
137
+ return cp
138
+
139
+ def render(self) -> str:
140
+ lines = [
141
+ "FLASHY CHECKPOINT",
142
+ f"OBJECTIVE -> {self.objective}",
143
+ f"DONE -> {', '.join(self.done) if self.done else '(none yet)'}",
144
+ f"CURRENT -> {self.current or '(not started)'}",
145
+ f"REMAINING -> {', '.join(self.remaining) if self.remaining else '(none)'}",
146
+ f"NEXT -> {self.next_action or '(none recorded)'}",
147
+ f"RESUME -> {self.resume_token}",
148
+ f"DIGEST -> {self.digest}",
149
+ ]
150
+ if self.blockers:
151
+ lines.append(f"BLOCKERS -> {', '.join(self.blockers)}")
152
+ if self.manual_edit_override:
153
+ lines.append("OVERRIDE -> manual edit acknowledged by operator")
154
+ lines.append("")
155
+ lines.append("Continuing...")
156
+ return "\n".join(lines)
157
+
158
+ @classmethod
159
+ def parse(cls, text: str) -> "Checkpoint":
160
+ """Parse a rendered checkpoint back. Raises CheckpointError on bad input."""
161
+ if "FLASHY CHECKPOINT" not in text:
162
+ raise CheckpointError("Not a Flashy checkpoint.")
163
+ fields: dict[str, str] = {}
164
+ for line in text.splitlines():
165
+ if "->" in line:
166
+ key, _, value = line.partition("->")
167
+ fields[key.strip()] = value.strip()
168
+
169
+ def split_list(value: str) -> list[str]:
170
+ value = value.strip()
171
+ if not value or value in ("(none yet)", "(none)"):
172
+ return []
173
+ return [p.strip() for p in value.split(",") if p.strip()]
174
+
175
+ try:
176
+ cp = cls(
177
+ objective=fields["OBJECTIVE"],
178
+ done=split_list(fields.get("DONE", "")),
179
+ current="" if fields.get("CURRENT") == "(not started)" else fields.get("CURRENT", ""),
180
+ remaining=split_list(fields.get("REMAINING", "")),
181
+ next_action="" if fields.get("NEXT") == "(none recorded)" else fields.get("NEXT", ""),
182
+ resume_token=fields["RESUME"],
183
+ blockers=split_list(fields.get("BLOCKERS", "")),
184
+ digest=fields.get("DIGEST", ""),
185
+ manual_edit_override="OVERRIDE" in fields,
186
+ )
187
+ except KeyError as exc:
188
+ raise CheckpointError(f"Checkpoint missing field: {exc}") from exc
189
+ if not fields.get("DIGEST"):
190
+ # Pre-binding block: __post_init__ computed a fresh digest, but
191
+ # no write-time attestation exists — clear it so verify()
192
+ # refuses explicitly instead of blessing content that was never
193
+ # bound.
194
+ cp.digest = ""
195
+ return cp
flashy/detectors.py ADDED
@@ -0,0 +1,111 @@
1
+ """Detectors: artificial stops, drift, and the confidence engine.
2
+
3
+ Two of these are data plus small functions; the third is a discipline the
4
+ package makes explicit rather than automatic.
5
+
6
+ - Artificial-stop detection is genuinely codeable: the phrases are a fixed
7
+ list, and matching them in output text is a string scan.
8
+ - Drift detection is four questions, not a function — the package carries
9
+ the questions and the snap-back rule; the judgment call stays with the
10
+ operator (human or agent).
11
+ - The confidence engine is three levels with a hard rule: High and Medium
12
+ never block; only Low pauses, and only for decisions that materially
13
+ change the implementation.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ # Phrases that signal an artificial stop — an execution failure unless a
19
+ # genuine limit (context window, missing info, missing permission, safety)
20
+ # forces them. Ported verbatim from the original skill's list.
21
+ ARTIFICIAL_STOP_PHRASES = (
22
+ "here's a starting point",
23
+ "here's a foundation",
24
+ "this should get you started",
25
+ "you can expand this",
26
+ "you can build upon this",
27
+ "build upon this",
28
+ "the rest follows similarly",
29
+ "due to space",
30
+ "here's part 1",
31
+ )
32
+
33
+ # Permission-to-continue phrases: not valid stopping points. The default
34
+ # assumption is to continue unless genuinely blocked.
35
+ PERMISSION_PHRASES = (
36
+ "continue?",
37
+ "should i keep going?",
38
+ "want more?",
39
+ "need anything else?",
40
+ "shall i finish?",
41
+ )
42
+
43
+
44
+ def detect_artificial_stop(text: str) -> list[str]:
45
+ """Return the artificial-stop phrases found in the text (case-insensitive)."""
46
+ lowered = text.lower()
47
+ return [p for p in ARTIFICIAL_STOP_PHRASES if p in lowered]
48
+
49
+
50
+ def asks_permission_to_continue(text: str) -> list[str]:
51
+ """Return permission-to-continue phrases found (also not valid stops)."""
52
+ lowered = text.lower()
53
+ return [p for p in PERMISSION_PHRASES if p in lowered]
54
+
55
+
56
+ # The four drift questions, run after every milestone and whenever output
57
+ # starts feeling like explanation instead of building.
58
+ DRIFT_QUESTIONS = (
59
+ "Am I still solving the original problem?",
60
+ "Have I wandered into explanation instead of building?",
61
+ "Did I forget earlier requirements?",
62
+ "Would the user consider this actually finished?",
63
+ )
64
+
65
+ #: The snap-back rule, stated once so every consumer applies it identically.
66
+ DRIFT_CORRECTION = (
67
+ "On drift: discard the drifted thread, restate the locked objective, "
68
+ "resume from the last verified milestone. Correct and continue — no apology theater."
69
+ )
70
+
71
+ # --- Confidence engine --------------------------------------------------------
72
+
73
+ HIGH = "high"
74
+ MEDIUM = "medium"
75
+ LOW = "low"
76
+ CONFIDENCE_LEVELS = (HIGH, MEDIUM, LOW)
77
+
78
+ CONFIDENCE_GUIDANCE = {
79
+ HIGH: "One reasonable interpretation, low cost if wrong — proceed silently.",
80
+ MEDIUM: "Multiple reasonable interpretations, but cheap/reversible to redo — "
81
+ "proceed, state the assumption inline, don't block on it.",
82
+ LOW: "Materially different deliverables result, or it is expensive/irreversible — "
83
+ "pause and ask. This is the ONLY legitimate pause trigger.",
84
+ }
85
+
86
+
87
+ class DetectorError(ValueError):
88
+ """An invalid confidence level or detector input."""
89
+
90
+
91
+ def should_pause(confidence: str) -> bool:
92
+ """Only Low confidence pauses — and only for implementation-changing decisions."""
93
+ if confidence not in CONFIDENCE_LEVELS:
94
+ raise DetectorError(f"Confidence must be one of {CONFIDENCE_LEVELS}.")
95
+ return confidence == LOW
96
+
97
+
98
+ def is_implementation_changing(decision: str) -> bool:
99
+ """Heuristic: does this decision materially change the implementation?
100
+
101
+ Examples that do: choice of database/language/framework, deployment
102
+ target, a required API key or credential, an ambiguity where two
103
+ reasonable interpretations produce meaningfully different deliverables.
104
+ The package can't decide this for you — this helper documents the
105
+ bar so callers apply it consistently.
106
+ """
107
+ markers = (
108
+ "database", "framework", "language", "deploy", "api key",
109
+ "credential", "infrastructure", "architecture",
110
+ )
111
+ return any(m in decision.lower() for m in markers)
flashy/ledger.py ADDED
@@ -0,0 +1,132 @@
1
+ """Append-only local ledger. Vendored per tool; stdlib only.
2
+
3
+ CANONICAL COPY: edit this file, then re-vendor with vendor.sh.
4
+ Do not edit the vendored copies in place.
5
+
6
+ Privacy rule (non-negotiable): the ledger NEVER stores raw input text.
7
+ Records carry SHA-256 digests of inputs plus structural summaries
8
+ (counts, verdicts, deltas). What happened, and what kind — never the words.
9
+
10
+ Location: ~/.frontface/<tool>/ledger.jsonl
11
+ override: FRONTFACE_LEDGER_DIR=<dir> (ledgers go to <dir>/<tool>/)
12
+ disable: FRONTFACE_NO_LEDGER=1 (record() becomes a silent no-op)
13
+
14
+ The ledger must never break the tool it serves: record() swallows OSError
15
+ and returns False instead of raising.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import sys
24
+ from datetime import datetime, timezone
25
+
26
+ try:
27
+ import fcntl
28
+ except ImportError: # pragma: no cover — Windows has no fcntl
29
+ fcntl = None # type: ignore[assignment]
30
+
31
+
32
+ def _base_dir() -> str:
33
+ return os.environ.get("FRONTFACE_LEDGER_DIR") or os.path.join(
34
+ os.path.expanduser("~"), ".frontface"
35
+ )
36
+
37
+
38
+ def _disabled() -> bool:
39
+ return os.environ.get("FRONTFACE_NO_LEDGER") == "1"
40
+
41
+
42
+ class Ledger:
43
+ """One JSONL ledger per tool. Cheap to construct; construct per use."""
44
+
45
+ def __init__(self, tool: str) -> None:
46
+ self.tool = tool
47
+ self.dir = os.path.join(_base_dir(), tool)
48
+ self.path = os.path.join(self.dir, "ledger.jsonl")
49
+
50
+ @property
51
+ def version(self) -> str:
52
+ """The owning package's __version__, resolved lazily."""
53
+ try:
54
+ pkg = sys.modules.get(__package__ or "")
55
+ return str(getattr(pkg, "__version__", "unknown"))
56
+ except Exception:
57
+ return "unknown"
58
+
59
+ @staticmethod
60
+ def digest(text: str) -> str:
61
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
62
+
63
+ def record(
64
+ self,
65
+ event: str,
66
+ summary: dict,
67
+ *,
68
+ input_text: str | None = None,
69
+ ) -> bool:
70
+ """Append one record. Returns False when disabled or on I/O error.
71
+
72
+ Never raises. Never stores raw input text — only its digest.
73
+ """
74
+ if _disabled():
75
+ return False
76
+ rec: dict = {
77
+ "ts": datetime.now(timezone.utc).isoformat(),
78
+ "tool": self.tool,
79
+ "tool_version": self.version,
80
+ "event": event,
81
+ "summary": summary,
82
+ }
83
+ if input_text is not None:
84
+ rec["input_digest"] = self.digest(input_text)
85
+ try:
86
+ os.makedirs(self.dir, exist_ok=True)
87
+ with open(self.path, "a", encoding="utf-8") as f:
88
+ if fcntl is not None:
89
+ fcntl.flock(f, fcntl.LOCK_EX)
90
+ try:
91
+ f.write(json.dumps(rec) + "\n")
92
+ f.flush()
93
+ os.fsync(f.fileno())
94
+ finally:
95
+ fcntl.flock(f, fcntl.LOCK_UN)
96
+ else:
97
+ f.write(json.dumps(rec) + "\n")
98
+ except OSError:
99
+ return False
100
+ return True
101
+
102
+ def read(self, limit: int | None = None) -> list[dict]:
103
+ """All records, oldest first. Empty list when there's no ledger."""
104
+ try:
105
+ with open(self.path, encoding="utf-8") as f:
106
+ lines = f.read().splitlines()
107
+ except OSError:
108
+ return []
109
+ recs = []
110
+ for line in lines:
111
+ line = line.strip()
112
+ if line:
113
+ try:
114
+ recs.append(json.loads(line))
115
+ except ValueError:
116
+ continue # never let a corrupt line kill the read
117
+ return recs[-limit:] if limit else recs
118
+
119
+ def summary(self) -> dict:
120
+ """Counts by event plus first/last timestamps."""
121
+ recs = self.read()
122
+ by_event: dict[str, int] = {}
123
+ for r in recs:
124
+ ev = str(r.get("event", "?"))
125
+ by_event[ev] = by_event.get(ev, 0) + 1
126
+ return {
127
+ "tool": self.tool,
128
+ "records": len(recs),
129
+ "events": by_event,
130
+ "first": recs[0]["ts"] if recs else None,
131
+ "last": recs[-1]["ts"] if recs else None,
132
+ }
flashy/quality_gate.py ADDED
@@ -0,0 +1,51 @@
1
+ """The Quality Gate: the launch checklist that guards VERIFYING -> DONE.
2
+
3
+ There is no partial credit. Every box checked or the task is not done —
4
+ status stays IN PROGRESS, not "mostly done."
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ GATE_ITEMS = (
10
+ "Everything requested exists",
11
+ "Everything builds/holds together logically",
12
+ "Nothing obvious is missing",
13
+ "No placeholder implementations remain",
14
+ "No TODOs remain",
15
+ "No incomplete sections remain",
16
+ "No artificial stopping points remain",
17
+ )
18
+
19
+
20
+ class GateError(ValueError):
21
+ """An unknown gate item was referenced."""
22
+
23
+
24
+ class QualityGate:
25
+ """Seven boxes. Check them all or stay in VERIFYING."""
26
+
27
+ def __init__(self) -> None:
28
+ self._boxes: dict[str, bool] = {item: False for item in GATE_ITEMS}
29
+
30
+ def check(self, item: str) -> None:
31
+ if item not in self._boxes:
32
+ raise GateError(f"Unknown gate item: {item!r}")
33
+ self._boxes[item] = True
34
+
35
+ def uncheck(self, item: str) -> None:
36
+ if item not in self._boxes:
37
+ raise GateError(f"Unknown gate item: {item!r}")
38
+ self._boxes[item] = False
39
+
40
+ def all_checked(self) -> bool:
41
+ return all(self._boxes.values())
42
+
43
+ def missing(self) -> list[str]:
44
+ return [item for item, checked in self._boxes.items() if not checked]
45
+
46
+ def check_all(self) -> None:
47
+ for item in self._boxes:
48
+ self._boxes[item] = True
49
+
50
+ def status(self) -> str:
51
+ return "DONE" if self.all_checked() else "IN PROGRESS"
flashy/session.py ADDED
@@ -0,0 +1,181 @@
1
+ """Mission session: the objective, its milestones, and the live dashboard.
2
+
3
+ A session binds the three codeable pieces together — the state machine,
4
+ the quality gate, and checkpoints — around one locked objective. It also
5
+ carries the mission dashboard (objective / now / done / next / blockers)
6
+ and the end-of-response gate: a response ending is not a task ending.
7
+
8
+ Mission rule: a draft is not a delivery. Completion is measured by whether
9
+ the user actually received the usable thing they asked for.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+ from .checkpoints import Checkpoint, make_resume_token
17
+ from .ledger import Ledger
18
+ from .quality_gate import QualityGate
19
+ from .state_machine import (
20
+ BLOCKED, DONE, EXECUTING, LOCKED, PLANNING, VERIFYING,
21
+ ExecutionStateMachine, TransitionError,
22
+ )
23
+
24
+ PENDING = "pending"
25
+ ACTIVE = "active"
26
+ MILESTONE_DONE = "done"
27
+ DESCOPED = "descoped"
28
+
29
+
30
+ @dataclass
31
+ class Milestone:
32
+ name: str
33
+ status: str = PENDING
34
+
35
+ def __post_init__(self) -> None:
36
+ if not self.name.strip():
37
+ raise TransitionError("Milestones need names.")
38
+ if self.status not in (PENDING, ACTIVE, MILESTONE_DONE, DESCOPED):
39
+ raise TransitionError(f"Bad milestone status: {self.status!r}")
40
+
41
+
42
+ class MissionSession:
43
+ """One locked objective, driven through the state machine to DONE."""
44
+
45
+ def __init__(self, objective: str, *, slug: str,
46
+ implicit_quality_bar: str = "",
47
+ scope_boundaries: str = "") -> None:
48
+ self.machine = ExecutionStateMachine(objective)
49
+ self.slug = slug
50
+ self.implicit_quality_bar = implicit_quality_bar
51
+ self.scope_boundaries = scope_boundaries
52
+ self.milestones: list[Milestone] = []
53
+ self.gate = QualityGate()
54
+ self.constraints: list[str] = []
55
+ self.pending_decisions: list[str] = []
56
+ self.risks: list[str] = []
57
+ self._ledger = Ledger("flashy")
58
+ self._ledger.record("session_start", {"slug": slug},
59
+ input_text=objective)
60
+
61
+ # -- planning -----------------------------------------------------------
62
+
63
+ def plan(self, milestones: list[str]) -> None:
64
+ """Expand the objective into milestones and enter PLANNING."""
65
+ if self.machine.state != LOCKED:
66
+ raise TransitionError("plan() runs once, from LOCKED.")
67
+ self.milestones = [Milestone(name=m) for m in milestones]
68
+ if self.milestones:
69
+ self.milestones[0].status = ACTIVE
70
+ self.machine.transition(PLANNING)
71
+
72
+ def start_executing(self) -> None:
73
+ self.machine.transition(EXECUTING)
74
+
75
+ # -- execution ----------------------------------------------------------
76
+
77
+ @property
78
+ def current_milestone(self) -> Milestone | None:
79
+ for m in self.milestones:
80
+ if m.status == ACTIVE:
81
+ return m
82
+ return None
83
+
84
+ def complete_milestone(self, name: str) -> None:
85
+ """Mark a milestone done and advance to the next pending one."""
86
+ target = next((m for m in self.milestones if m.name == name), None)
87
+ if target is None:
88
+ raise TransitionError(f"Unknown milestone: {name!r}")
89
+ target.status = MILESTONE_DONE
90
+ nxt = next((m for m in self.milestones if m.status == PENDING), None)
91
+ if nxt is not None:
92
+ nxt.status = ACTIVE
93
+ self._ledger.record(
94
+ "milestone",
95
+ {"slug": self.slug,
96
+ "completed": len(self.completed()),
97
+ "remaining": len(self.remaining())},
98
+ input_text=name,
99
+ )
100
+
101
+ def descope(self, name: str) -> None:
102
+ """Explicit user descope only — milestones never silently shrink."""
103
+ target = next((m for m in self.milestones if m.name == name), None)
104
+ if target is None:
105
+ raise TransitionError(f"Unknown milestone: {name!r}")
106
+ target.status = DESCOPED
107
+ nxt = next((m for m in self.milestones if m.status == PENDING), None)
108
+ if nxt is not None and self.current_milestone is None:
109
+ nxt.status = ACTIVE
110
+ self._ledger.record(
111
+ "descope",
112
+ {"slug": self.slug,
113
+ "completed": len(self.completed()),
114
+ "remaining": len(self.remaining())},
115
+ input_text=name,
116
+ )
117
+
118
+ def remaining(self) -> list[str]:
119
+ return [m.name for m in self.milestones
120
+ if m.status in (PENDING, ACTIVE)]
121
+
122
+ def completed(self) -> list[str]:
123
+ return [m.name for m in self.milestones if m.status == MILESTONE_DONE]
124
+
125
+ # -- dashboard ----------------------------------------------------------
126
+
127
+ def dashboard(self) -> str:
128
+ """The compact mission dashboard, refreshed after every milestone."""
129
+ current = self.current_milestone
130
+ lines = [
131
+ f"OBJECTIVE -> {self.machine.objective}",
132
+ f"NOW -> {current.name if current else '(none — all milestones resolved)'}",
133
+ f"DONE -> {', '.join(self.completed()) or '(none yet)'}",
134
+ f"NEXT -> {self.next_action()}",
135
+ f"BLOCKERS -> {', '.join(self.pending_decisions) or 'none'}",
136
+ ]
137
+ return "\n".join(lines)
138
+
139
+ def next_action(self) -> str:
140
+ current = self.current_milestone
141
+ if current:
142
+ return f"Continue: {current.name}"
143
+ if self.remaining():
144
+ return f"Continue: {self.remaining()[0]}"
145
+ return "Verify against the Quality Gate"
146
+
147
+ # -- end-of-response gate -----------------------------------------------
148
+
149
+ def end_of_response_gate(self) -> str:
150
+ """A response ending is not a task ending. Returns the verdict:
151
+
152
+ - "verify" — no milestones remain; run the Quality Gate.
153
+ - "continue" — useful progress can still be made; keep executing.
154
+ - "checkpoint" — genuinely blocked; emit a checkpoint.
155
+ """
156
+ if self.machine.state == BLOCKED:
157
+ return "checkpoint"
158
+ if not self.remaining():
159
+ return "verify"
160
+ return "continue"
161
+
162
+ def checkpoint(self, milestone_index: int) -> Checkpoint:
163
+ """Emit an honest checkpoint for a genuine limit. Never a soft stop."""
164
+ return Checkpoint(
165
+ objective=self.machine.objective,
166
+ done=self.completed(),
167
+ current=self.current_milestone.name if self.current_milestone else "",
168
+ remaining=self.remaining(),
169
+ next_action=self.next_action(),
170
+ resume_token=make_resume_token(self.slug, milestone_index),
171
+ blockers=list(self.pending_decisions),
172
+ )
173
+
174
+ # -- verification -------------------------------------------------------
175
+
176
+ def begin_verification(self) -> None:
177
+ self.machine.transition(VERIFYING)
178
+
179
+ def finish(self) -> None:
180
+ """VERIFYING -> DONE. Raises unless every Quality Gate box is checked."""
181
+ self.machine.transition(DONE, quality_gate=self.gate)
@@ -0,0 +1,117 @@
1
+ """The execution state machine: the one codeable core of Flashy.
2
+
3
+ States: LOCKED -> PLANNING -> EXECUTING -> VERIFYING -> DONE, with BLOCKED
4
+ reachable from EXECUTING or VERIFYING. The rules are the point — they are
5
+ what stops a long task from quietly degrading into "here's a starting
6
+ point":
7
+
8
+ - VERIFYING -> DONE is forbidden unless every Quality Gate box is checked.
9
+ - EXECUTING -> PLANNING is forbidden without an explicit, user-driven
10
+ scope change. Silent re-planning is how objectives get replaced.
11
+ - BLOCKED -> EXECUTING is forbidden until a checkpoint has been emitted.
12
+ A blocked session that resumes with no checkpoint is a session that
13
+ forgot where it was.
14
+ - DONE is terminal. Nothing transitions out of DONE.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ LOCKED = "LOCKED"
20
+ PLANNING = "PLANNING"
21
+ EXECUTING = "EXECUTING"
22
+ VERIFYING = "VERIFYING"
23
+ DONE = "DONE"
24
+ BLOCKED = "BLOCKED"
25
+
26
+ STATES = (LOCKED, PLANNING, EXECUTING, VERIFYING, DONE, BLOCKED)
27
+
28
+ _ALLOWED = {
29
+ LOCKED: {PLANNING},
30
+ PLANNING: {EXECUTING},
31
+ EXECUTING: {VERIFYING, BLOCKED, PLANNING},
32
+ VERIFYING: {DONE, EXECUTING, BLOCKED},
33
+ BLOCKED: {EXECUTING},
34
+ DONE: set(),
35
+ }
36
+
37
+
38
+ class TransitionError(ValueError):
39
+ """An illegal state transition was attempted."""
40
+
41
+
42
+ class ExecutionStateMachine:
43
+ """Tracks one task's execution state and enforces the transition rules."""
44
+
45
+ def __init__(self, objective: str) -> None:
46
+ if not objective.strip():
47
+ raise TransitionError("An execution state machine needs a locked objective.")
48
+ self.objective = objective
49
+ self.state = LOCKED
50
+ self.history: list[tuple[str, str]] = [(LOCKED, "objective locked")]
51
+ self._checkpoint_emitted = False
52
+ self._block_reason = ""
53
+
54
+ def transition(self, target: str, *, scope_change: bool = False,
55
+ quality_gate: object | None = None) -> str:
56
+ """Move to a new state, enforcing the rules. Returns the new state."""
57
+ if target not in STATES:
58
+ raise TransitionError(f"Unknown state: {target!r}")
59
+ if target not in _ALLOWED[self.state]:
60
+ raise TransitionError(
61
+ f"Illegal transition: {self.state} -> {target}. "
62
+ f"Allowed: {sorted(_ALLOWED[self.state]) or 'none (terminal)'}."
63
+ )
64
+ if self.state == EXECUTING and target == PLANNING and not scope_change:
65
+ raise TransitionError(
66
+ "EXECUTING -> PLANNING requires an explicit user-driven scope "
67
+ "change (scope_change=True). Silent re-planning is how the "
68
+ "objective gets replaced."
69
+ )
70
+ if self.state == VERIFYING and target == DONE:
71
+ gate = quality_gate
72
+ if gate is None or not gate.all_checked():
73
+ missing = (
74
+ gate.missing() if gate is not None else ["no quality gate supplied"]
75
+ )
76
+ raise TransitionError(
77
+ "VERIFYING -> DONE requires every Quality Gate box checked. "
78
+ f"Missing: {missing}."
79
+ )
80
+ if self.state == BLOCKED and target == EXECUTING:
81
+ if not self._checkpoint_emitted:
82
+ raise TransitionError(
83
+ "BLOCKED -> EXECUTING requires a checkpoint to have been "
84
+ "emitted first. Call emit_checkpoint() (see checkpoints.py)."
85
+ )
86
+ self._checkpoint_emitted = False # checkpoint consumed by the resume
87
+
88
+ reason = ""
89
+ if target == BLOCKED:
90
+ reason = "blocked"
91
+ self.state = target
92
+ self.history.append((target, reason))
93
+ return self.state
94
+
95
+ def block(self, reason: str) -> str:
96
+ """Enter BLOCKED with a recorded reason."""
97
+ if not reason.strip():
98
+ raise TransitionError("Blocking requires a stated reason.")
99
+ self._block_reason = reason
100
+ return self.transition(BLOCKED)
101
+
102
+ def emit_checkpoint(self) -> None:
103
+ """Record that a checkpoint was emitted (see checkpoints.py).
104
+
105
+ The checkpoint content itself lives in checkpoints.py; the machine
106
+ only tracks that one was emitted, which unblocks BLOCKED -> EXECUTING.
107
+ """
108
+ self._checkpoint_emitted = True
109
+ self.history.append((self.state, "checkpoint emitted"))
110
+
111
+ @property
112
+ def block_reason(self) -> str:
113
+ return self._block_reason
114
+
115
+ @property
116
+ def is_terminal(self) -> bool:
117
+ return self.state == DONE
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: frontface-flashy
3
+ Version: 1.0.0
4
+ Summary: Persistent execution discipline for long tasks: lock the objective, track milestones, detect drift and artificial stops, verify before done
5
+ Author: Ryan Wesley Hitchcock
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ohpersonperson/frontface
8
+ Keywords: execution,task-management,state-machine,agents,workflow
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # Flashy — persistent execution discipline
18
+
19
+ Prevents an agent from abandoning a build halfway. Locks the objective,
20
+ expands it into milestones, tracks live execution state, scans for drift
21
+ after every milestone, detects artificial stops ("here's a starting
22
+ point"), checkpoints honestly on genuine limits, and gates completion on
23
+ a quality checklist.
24
+
25
+ Mission rule: **a draft is not a delivery.** Completion is measured by
26
+ whether the user actually received the usable thing they asked for.
27
+
28
+ No prerequisites. The mechanism is the whole product.
29
+
30
+ *(Lineage: this is the front-facing conversion of the `flashy` skill from
31
+ Ryan's skill-suite, MIT — itself a port of an execution-discipline
32
+ protocol into skill format. What changed in the conversion is documented
33
+ in CHANGELOG.md.)*
34
+
35
+ ## What it does
36
+
37
+ For any substantial build/create/write task, run the core loop — **lock,
38
+ move, verify**:
39
+
40
+ 1. **Lock the objective.** Extract three things from the request: the
41
+ stated deliverable, the implicit quality bar ("a login form" implies
42
+ validation and error states even if unsaid), and the implicit scope
43
+ boundaries. Once locked, the objective is immutable for the rest of the
44
+ task: subtasks don't replace the mission, explanations don't replace
45
+ execution, planning doesn't replace progress.
46
+ 2. **Expand into milestones.** Objective → Major Milestones → Subtasks →
47
+ Implementation Steps → Verification → Delivery.
48
+ 3. **Track execution state.** A real state machine —
49
+ `LOCKED → PLANNING → EXECUTING → VERIFYING → DONE`, with `BLOCKED`
50
+ reachable from `EXECUTING`/`VERIFYING` — enforces the transition rules
51
+ in code: no `VERIFYING → DONE` without every Quality Gate box checked,
52
+ no silent `EXECUTING → PLANNING` without an explicit user-driven scope
53
+ change, no `BLOCKED → EXECUTING` without an emitted checkpoint.
54
+ 4. **Scan for drift after every milestone.** Four questions: still solving
55
+ the original problem? wandered into explanation instead of building?
56
+ forgot earlier requirements? would the user call this finished? On
57
+ drift: discard the thread, restate the objective, resume from the last
58
+ verified milestone.
59
+ 5. **Detect artificial stops.** "Here's a starting point," "the rest follows
60
+ similarly," "due to space..." — these are execution failures, not
61
+ conclusions. The package scans output for the phrase list and flags
62
+ them; the operator rewrites the impulse as **Continue Working.**
63
+ 6. **Checkpoint honestly on genuine limits.** Context cutoff, tool error,
64
+ or a low-confidence blocker with no safe default — emit a checkpoint
65
+ with a resume token (`finish:<slug>-m<N>`) and continue next turn.
66
+ Fatigue and "this feels done" are failures, not checkpoints.
67
+ 7. **Verify before done.** The seven-box Quality Gate is a hard gate, not
68
+ a vibe check. Any unchecked box = IN PROGRESS.
69
+
70
+ **Execution bias: build before briefing.** Build → Verify → Polish →
71
+ Explain. Understanding the problem well enough to act is the signal to
72
+ stop analyzing and start executing. And the end-of-response gate: a
73
+ response ending is not a task ending — if useful progress can still be
74
+ made, keep executing; don't hand the turn back for permission.
75
+
76
+ **Pause only for implementation-changing decisions** — choice of
77
+ database/language/framework, deployment target, a required credential, or
78
+ an ambiguity where two reasonable interpretations produce meaningfully
79
+ different deliverables. "Continue?" / "Should I keep going?" are never
80
+ valid stopping points.
81
+
82
+ ## Why it works
83
+
84
+ Most abandoned builds don't fail — they *decay*. The objective quietly
85
+ gets replaced by a subtask. Explanation replaces execution. A response
86
+ "feels long enough" and stops. Flashy attacks the decay mechanically:
87
+
88
+ - **The state machine** makes the rules structural: you cannot declare
89
+ done without the gate, cannot silently re-plan, cannot resume from a
90
+ block without a checkpoint. The failure modes are `TransitionError`s,
91
+ not suggestions.
92
+ - **Artificial-stop detection** catches the exact phrases long tasks die
93
+ on — as a string scan, in code, every time.
94
+ - **The confidence engine** (High/Medium/Low) resolves ambiguity without
95
+ blocking: High proceeds silently, Medium proceeds stating the
96
+ assumption inline, only Low pauses. Most ambiguity never surfaces as a
97
+ stop.
98
+ - **Checkpoints with resume tokens** make interruption recoverable without
99
+ the user re-explaining anything — which removes the incentive to
100
+ quietly abandon a task at a session break.
101
+
102
+ ## Quickstart
103
+
104
+ Zero dependencies beyond Python 3.10+. No model, no network — this
105
+ package is pure discipline machinery.
106
+
107
+ ```bash
108
+ cd flashy
109
+ python3 -m unittest discover -s tests # 41 offline tests — prove the machinery first
110
+ python3 examples/demo_session.py # watch a session run LOCKED -> DONE
111
+ ```
112
+
113
+ Or use it as a library:
114
+
115
+ ```python
116
+ from flashy import MissionSession
117
+
118
+ session = MissionSession(
119
+ "Deliver a functional, usable, in-browser debate simulator.",
120
+ slug="debate-sim",
121
+ )
122
+ session.plan(["Architecture", "UI", "Logic", "Persistence", "Testing", "Delivery"])
123
+ session.start_executing()
124
+
125
+ for milestone in ["Architecture", "UI", "Logic", "Persistence", "Testing", "Delivery"]:
126
+ assert session.end_of_response_gate() == "continue"
127
+ session.complete_milestone(milestone)
128
+ print(session.dashboard())
129
+
130
+ session.begin_verification()
131
+ session.gate.check_all()
132
+ session.finish() # raises unless every box is checked
133
+ assert session.machine.state == "DONE"
134
+ ```
135
+
136
+ ## Inputs, outputs
137
+
138
+ - **Input:** a locked objective (the finished thing, not the artifact
139
+ type), milestones, constraints, pending decisions.
140
+ - **Output:** a driven session — dashboard state, checkpoints with resume
141
+ tokens, and a DONE that actually means done. The package never writes
142
+ files; your harness owns persistence.
143
+
144
+ ## Worked examples
145
+
146
+ Three in `examples/`, a stranger can follow all of them:
147
+
148
+ 1. `example-1-debate-simulator-walkthrough.md` — the full loop end to end
149
+ on a browser debate simulator (ported from the original skill, unchanged).
150
+ 2. `example-2-anti-patterns.md` — before/after pairs: artificial-stop
151
+ language vs. the corrected continuation (ported, unchanged).
152
+ 3. `example-3-flashy-session.md` — the machinery itself: every
153
+ transition, gate, and checkpoint shown as the code enforces it. Run
154
+ `demo_session.py` to watch it execute.
155
+
156
+ *All fields are constructed for teaching — they show the discipline's
157
+ shape, not real findings.*
158
+
159
+ ## FAQ
160
+
161
+ **Does this make the model smarter?** No. It makes it finish things.
162
+ Execution discipline, not reasoning quality.
163
+
164
+ **Does it need an internet connection?** No. No model calls at all — the
165
+ 41 unit tests and the demo run fully offline.
166
+
167
+ **What does it cost?** Nothing. Free, dependency-free, offline.
168
+
169
+ **When should it *not* fire?** Trivial tasks (a single short answer),
170
+ genuine user decisions pending, safety constraints, or real platform
171
+ limits. Never deactivate because a response "feels long enough."
172
+
173
+ ## License
174
+
175
+ MIT. See LICENSE.
@@ -0,0 +1,12 @@
1
+ flashy/__init__.py,sha256=1GqoxTAW-BSBO1l48QLknm6KyOee4Mhi9HqI4lbsIS0,1905
2
+ flashy/checkpoints.py,sha256=VJUUPnO-JZqAdWXEQhvauWVQR7hwnOQB3RReOdZq50s,8104
3
+ flashy/detectors.py,sha256=Q7Xx7AFNA6v9Q5xduNUzn4ZnsKZqxWLybxI6oByFwUU,4195
4
+ flashy/ledger.py,sha256=x8TH2et1Jt_UTzLMn49R8YKEiMMykYTW6UoMz-l8qCU,4245
5
+ flashy/quality_gate.py,sha256=R_fwlD7fA4chJgIH_kdnPiLBmiW1zjgZLRZuP0OC7lk,1519
6
+ flashy/session.py,sha256=PeoaLJQ72NgAFAJ6TQ-K38PIN4GsZWUU67GITyqRVtg,6974
7
+ flashy/state_machine.py,sha256=9usEOmlREbh3KTdEEVaUil6KB6y-meyXwTiwlGS4uys,4489
8
+ frontface_flashy-1.0.0.dist-info/licenses/LICENSE,sha256=Zx_XafyZDq7qBexUb7tER9kYlBZ0rCVaAwKuqMCfxLE,1078
9
+ frontface_flashy-1.0.0.dist-info/METADATA,sha256=u4hjfbr1598wz7ZqRhNCFR5kwfR31k_XYEWS_Sz9m8Q,7676
10
+ frontface_flashy-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ frontface_flashy-1.0.0.dist-info/top_level.txt,sha256=XNKwp8CalkM3b9QQhFeWwm6Fo3_E_8OoX0euJi7DAXY,7
12
+ frontface_flashy-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Wesley Hitchcock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flashy