specflo 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- specflo/__init__.py +3 -0
- specflo/_repo_skills/brainstorm/SKILL.md +150 -0
- specflo/_repo_skills/execute/SKILL.md +131 -0
- specflo/_repo_skills/plan/SKILL.md +112 -0
- specflo/_repo_skills/research/SKILL.md +99 -0
- specflo/_repo_skills/shelve/SKILL.md +70 -0
- specflo/_repo_skills/spec/SKILL.md +124 -0
- specflo/brainstorm.py +179 -0
- specflo/checkpoint.py +183 -0
- specflo/cli.py +1310 -0
- specflo/config.py +86 -0
- specflo/errors.py +9 -0
- specflo/guide.py +142 -0
- specflo/hook.py +246 -0
- specflo/markdown.py +203 -0
- specflo/plan.py +1108 -0
- specflo/projects.py +218 -0
- specflo/skills/.gitkeep +13 -0
- specflo/spec.py +281 -0
- specflo/status.py +121 -0
- specflo/validators.py +25 -0
- specflo/workflow.py +118 -0
- specflo-0.2.0.dist-info/METADATA +173 -0
- specflo-0.2.0.dist-info/RECORD +27 -0
- specflo-0.2.0.dist-info/WHEEL +4 -0
- specflo-0.2.0.dist-info/entry_points.txt +5 -0
- specflo-0.2.0.dist-info/licenses/LICENSE +625 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec
|
|
3
|
+
description: Use at a specflo project's spec phase, when turning a validated brainstorm into a structured, testable spec before any plan or code. Triggers include "write the spec", "let's spec this out", or `specflo status` showing the spec phase. Do NOT use for trivial fixes or before a brainstorm has been validated.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spec (specflo)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Turn a validated `brainstorm.md` into a **validated `spec.md`** for the active
|
|
11
|
+
specflo project: an objective, numbered **testable `REQ-NN` requirements** (each
|
|
12
|
+
with a pass/fail acceptance criterion, traced to the `D-NN` decision it derives
|
|
13
|
+
from), explicit in/out boundaries, and open questions — then hand off toward the
|
|
14
|
+
plan phase. The `specflo` CLI does the artifact I/O; you carry the synthesis,
|
|
15
|
+
judgment, and discipline.
|
|
16
|
+
|
|
17
|
+
## When to use / When NOT
|
|
18
|
+
|
|
19
|
+
**Use** at the spec phase (the second of `brainstorm → spec → plan → execute`),
|
|
20
|
+
once the brainstorm is validated. The spec is a **synthesis of the brainstorm,
|
|
21
|
+
not a new interview** — read `brainstorm.md` and write requirements; do not
|
|
22
|
+
re-litigate settled decisions.
|
|
23
|
+
|
|
24
|
+
**Do NOT use** for trivial fixes, or before a brainstorm exists and passes
|
|
25
|
+
`specflo validate brainstorm`. If the brainstorm is incomplete, finish it first.
|
|
26
|
+
|
|
27
|
+
## HARD-GATE
|
|
28
|
+
|
|
29
|
+
Do NOT write code, scaffold anything, or take any implementation action until the
|
|
30
|
+
spec is validated (`specflo validate spec` passes) and the user has explicitly
|
|
31
|
+
approved. The discipline here is the **rigor of the artifact** — testable
|
|
32
|
+
requirements with pass/fail acceptance — not merely "don't code yet".
|
|
33
|
+
|
|
34
|
+
## Process
|
|
35
|
+
|
|
36
|
+
1. **Preflight.** Confirm an active project at the spec phase (`specflo status`).
|
|
37
|
+
Run `specflo spec start` to create or locate `spec.md` (the command prints its
|
|
38
|
+
path — never build the path yourself). Read the project's `brainstorm.md`:
|
|
39
|
+
its Decisions (`D-NN`), Research, Current understanding, and Out of scope.
|
|
40
|
+
2. **Draft requirements — translate decisions, reframe vague → testable.** For
|
|
41
|
+
each thing the system must do, write a falsifiable requirement. Two moves:
|
|
42
|
+
- **Decision → behavior.** Brainstorm decisions are often *architecture or
|
|
43
|
+
tech-stack* choices ("build on SDK X", "UI in framework Y"), not behaviors —
|
|
44
|
+
and have no inherent pass/fail. Make each testable by spec'ing the
|
|
45
|
+
**behaviors it exists to enable**, and expect one decision to **fan out into
|
|
46
|
+
several requirements** (e.g. "build on the SDK" → separate auth, discovery,
|
|
47
|
+
and version-compatibility requirements, each `--from` that decision).
|
|
48
|
+
- **Reframe fuzzy wants:** `"make it fast"` →
|
|
49
|
+
`"the landscape scan returns within 5s on a warm cache"`, then ask "are these
|
|
50
|
+
the right targets?".
|
|
51
|
+
A good requirement is **✓** "API responds in < 200ms at p95"; a bad one is
|
|
52
|
+
**✗** "the system should be fast".
|
|
53
|
+
3. **Give every requirement a pass/fail acceptance criterion.** No subjective
|
|
54
|
+
criteria — write how a verifier confirms it. The check may be **behavioral**
|
|
55
|
+
(observe the running system) or **structural** (a source- or dependency-scan)
|
|
56
|
+
— both are valid as long as the verdict is mechanical. An architectural
|
|
57
|
+
constraint ("all server I/O goes through the SDK; no hand-rolled client") is
|
|
58
|
+
legitimately verified structurally; don't contort it into a fake runtime test.
|
|
59
|
+
Trace it to the decision it comes from with `--from D-NN` where applicable.
|
|
60
|
+
4. **Capture inline.** The moment a requirement is settled, record it:
|
|
61
|
+
`specflo requirement add --text "…" --acceptance "…" [--from D-NN]`
|
|
62
|
+
(add `--supersedes REQ-NN` when it replaces an earlier one). Don't batch —
|
|
63
|
+
capture as they happen. Keep the prose sections (Objective, Boundaries In/Out,
|
|
64
|
+
Open questions, Canonical refs) current by editing `spec.md` directly.
|
|
65
|
+
5. **Hold the scope boundary.** Fill **Boundaries** — In scope and Out of scope —
|
|
66
|
+
carrying the brainstorm's Out of scope / Deferred forward. Both lists must be
|
|
67
|
+
non-empty.
|
|
68
|
+
6. **No stale specifics.** The spec is behavioral and durable: no file paths or
|
|
69
|
+
code snippets that churn. One exception — a small prototype snippet that
|
|
70
|
+
encodes a decision more precisely than prose can (a schema or type shape).
|
|
71
|
+
7. **Self-review.** Re-read the spec with fresh eyes: placeholder scan; internal
|
|
72
|
+
consistency (do requirements contradict?); two-way ambiguity (pick one reading
|
|
73
|
+
and make it explicit); scope (one plan, or should this decompose?).
|
|
74
|
+
8. **Gate + validate.** Ask the user an explicit "ready?". On yes, run
|
|
75
|
+
`specflo validate spec`; fix any reported gaps inline and re-run until it
|
|
76
|
+
passes.
|
|
77
|
+
9. **Hand off — pause at the phase boundary.** Surface the end of the phase as one
|
|
78
|
+
clear beat, and **do not auto-advance**: the spec is complete and validated, the
|
|
79
|
+
**checkpoint is saved** (the project's `checkpoint.md`; resume any time with
|
|
80
|
+
`specflo checkpoint`), so this is a **safe place to clear context**. The plan
|
|
81
|
+
phase is next (it synthesizes from `spec.md` and cites `REQ-NN` — no
|
|
82
|
+
re-interview). Then **wait** — `specflo advance` is the user's to call; don't
|
|
83
|
+
change the phase yourself or start the plan.
|
|
84
|
+
|
|
85
|
+
## Anti-sycophancy
|
|
86
|
+
|
|
87
|
+
Take a position on every requirement and state what evidence would change it.
|
|
88
|
+
Avoid filler validation: "That's a great requirement," "That could work," "There
|
|
89
|
+
are many ways to think about this." Challenge the strongest version of the
|
|
90
|
+
user's intent, not a strawman. If a requirement isn't testable, say so and
|
|
91
|
+
reframe it.
|
|
92
|
+
|
|
93
|
+
## Common rationalizations
|
|
94
|
+
|
|
95
|
+
| Rationalization | Reality |
|
|
96
|
+
|---|---|
|
|
97
|
+
| "The brainstorm is enough." | The brainstorm captures *decisions*, not *testable requirements*. The spec adds acceptance criteria a verifier can check. |
|
|
98
|
+
| "A tech-stack decision isn't a requirement." | It is — spec the *behaviors* it exists to enable. One decision usually fans out into several testable requirements, each `--from` it. |
|
|
99
|
+
| "I'll write the acceptance after I code it." | That's a test report, not a specification. Decide pass/fail *before* building. |
|
|
100
|
+
| "This requirement is obviously testable." | If you can't state the pass/fail check in one line, it isn't. Write the line. |
|
|
101
|
+
| "I'll re-ask the user to be safe." | Re-interviewing is not synthesis. Read `brainstorm.md`; only ask about genuine gaps. |
|
|
102
|
+
| "Boundaries are obvious." | Unwritten scope is where plans balloon. Write In and Out explicitly. |
|
|
103
|
+
|
|
104
|
+
## Red flags (stop and correct)
|
|
105
|
+
|
|
106
|
+
- A requirement has no pass/fail acceptance criterion.
|
|
107
|
+
- You wrote a requirement into chat prose instead of `specflo requirement add`.
|
|
108
|
+
- You re-interviewed the user on something the brainstorm already decided.
|
|
109
|
+
- You put file paths or churn-prone code snippets into `spec.md`.
|
|
110
|
+
- You moved toward the plan without an explicit user "ready?" and a passing
|
|
111
|
+
`specflo validate spec`.
|
|
112
|
+
- You built or scaffolded something during the spec phase.
|
|
113
|
+
|
|
114
|
+
## Verification checklist
|
|
115
|
+
|
|
116
|
+
- [ ] `specflo spec start` was run; `spec.md` exists for the active project.
|
|
117
|
+
- [ ] Every requirement is in the Requirements section via `specflo requirement add`.
|
|
118
|
+
- [ ] Every requirement has a pass/fail **Acceptance** criterion.
|
|
119
|
+
- [ ] **Boundaries** — In scope and Out of scope — are both filled in.
|
|
120
|
+
- [ ] **Open questions** is present (may say "none").
|
|
121
|
+
- [ ] `specflo validate spec` passes.
|
|
122
|
+
- [ ] The user explicitly approved readiness before handoff.
|
|
123
|
+
- [ ] At hand-off, the checkpoint-saved phase-end beat was surfaced and `specflo advance` was left to the user.
|
|
124
|
+
- [ ] No code or scaffolding was produced.
|
specflo/brainstorm.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""The brainstorm artifact and its operations.
|
|
2
|
+
|
|
3
|
+
Each project gets a single ``brainstorm.md`` next to ``project.md``. The CLI owns
|
|
4
|
+
the structured, stateful parts of this file — scaffolding, the append-only
|
|
5
|
+
Decisions section (stable ``D-NN`` IDs, supersede-as-event), and read-only
|
|
6
|
+
linting. The brainstorm skill writes the prose sections (Current understanding,
|
|
7
|
+
Out of scope, Open questions, Canonical refs) directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .config import SpecfloConfig
|
|
18
|
+
from .errors import SpecfloError
|
|
19
|
+
from . import markdown
|
|
20
|
+
from .projects import load_project, project_dir
|
|
21
|
+
|
|
22
|
+
BRAINSTORM_FILENAME = "brainstorm.md"
|
|
23
|
+
|
|
24
|
+
_DECISION_ID_RE = re.compile(r"^### (D-\d+) —", re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
_TEMPLATE = """\
|
|
27
|
+
---
|
|
28
|
+
project: {slug}
|
|
29
|
+
phase: brainstorm
|
|
30
|
+
status: draft
|
|
31
|
+
created: {today}
|
|
32
|
+
updated: {today}
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Brainstorm: {name}
|
|
36
|
+
|
|
37
|
+
## Current understanding
|
|
38
|
+
<!-- rewritten as it converges; the synthesis the spec phase reads -->
|
|
39
|
+
|
|
40
|
+
## Research
|
|
41
|
+
<!-- findings + surprises from the research subagent (optional); links go to Canonical refs -->
|
|
42
|
+
|
|
43
|
+
## Decisions
|
|
44
|
+
<!-- append-only; managed by `specflo decision add`. Stable IDs D-NN. -->
|
|
45
|
+
|
|
46
|
+
## Out of scope / Deferred
|
|
47
|
+
<!-- required, must be non-empty before validate passes -->
|
|
48
|
+
|
|
49
|
+
## Open questions
|
|
50
|
+
<!-- required section (may say "none") -->
|
|
51
|
+
|
|
52
|
+
## Canonical refs
|
|
53
|
+
<!-- full paths the brainstorm leaned on -->
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Decision:
|
|
59
|
+
id: str
|
|
60
|
+
text: str
|
|
61
|
+
rationale: str
|
|
62
|
+
supersedes: str | None
|
|
63
|
+
status: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def brainstorm_path(root: Path, cfg: SpecfloConfig, slug: str) -> Path:
|
|
67
|
+
return project_dir(root, cfg, slug) / BRAINSTORM_FILENAME
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def start_brainstorm(
|
|
71
|
+
root: Path, cfg: SpecfloConfig, slug: str, today: str | None = None
|
|
72
|
+
) -> tuple[Path, bool]:
|
|
73
|
+
"""Create the brainstorm artifact, or locate an existing one.
|
|
74
|
+
|
|
75
|
+
Returns ``(path, created)``; ``created`` is False if the file already existed
|
|
76
|
+
(resume-friendly — never clobbers).
|
|
77
|
+
"""
|
|
78
|
+
project = load_project(root, cfg, slug) # raises SpecfloError if missing
|
|
79
|
+
path = brainstorm_path(root, cfg, slug)
|
|
80
|
+
if path.exists():
|
|
81
|
+
return path, False
|
|
82
|
+
today = today or datetime.date.today().isoformat()
|
|
83
|
+
path.write_text(
|
|
84
|
+
_TEMPLATE.format(slug=project.slug, name=project.name, today=today)
|
|
85
|
+
)
|
|
86
|
+
return path, True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_decision(
|
|
90
|
+
root: Path,
|
|
91
|
+
cfg: SpecfloConfig,
|
|
92
|
+
slug: str,
|
|
93
|
+
text: str,
|
|
94
|
+
rationale: str | None = None,
|
|
95
|
+
supersedes: str | None = None,
|
|
96
|
+
today: str | None = None,
|
|
97
|
+
) -> Decision:
|
|
98
|
+
"""Append a decision to the Decisions section and return it.
|
|
99
|
+
|
|
100
|
+
Assigns the next ``D-NN`` id. If ``supersedes`` is given, the named decision
|
|
101
|
+
is marked superseded (kept in place) and linked from the new entry.
|
|
102
|
+
"""
|
|
103
|
+
path = brainstorm_path(root, cfg, slug)
|
|
104
|
+
if not path.is_file():
|
|
105
|
+
raise SpecfloError("No brainstorm yet. Run `specflo brainstorm start` first.")
|
|
106
|
+
doc = path.read_text()
|
|
107
|
+
if "## Decisions" not in doc:
|
|
108
|
+
raise SpecfloError("Malformed brainstorm.md: no '## Decisions' section.")
|
|
109
|
+
|
|
110
|
+
if supersedes is not None and not re.search(
|
|
111
|
+
rf"^### {re.escape(supersedes)} —", doc, re.MULTILINE
|
|
112
|
+
):
|
|
113
|
+
raise SpecfloError(f"No decision {supersedes} to supersede.")
|
|
114
|
+
|
|
115
|
+
new_id = markdown.next_id(doc, "D-")
|
|
116
|
+
rationale_text = rationale if rationale else "—"
|
|
117
|
+
|
|
118
|
+
if supersedes is not None:
|
|
119
|
+
doc = markdown.mark_superseded(doc, supersedes, new_id)
|
|
120
|
+
|
|
121
|
+
entry_lines = [f"### {new_id} — {text}", f"- Rationale: {rationale_text}"]
|
|
122
|
+
if supersedes is not None:
|
|
123
|
+
entry_lines.append(f"- Supersedes: {supersedes}")
|
|
124
|
+
entry_lines.append("- Status: active")
|
|
125
|
+
entry = "\n".join(entry_lines) + "\n"
|
|
126
|
+
|
|
127
|
+
doc = markdown.append_to_section(doc, "## Decisions", entry)
|
|
128
|
+
doc = markdown.bump_updated(doc, today)
|
|
129
|
+
path.write_text(doc)
|
|
130
|
+
return Decision(
|
|
131
|
+
id=new_id,
|
|
132
|
+
text=text,
|
|
133
|
+
rationale=rationale_text,
|
|
134
|
+
supersedes=supersedes,
|
|
135
|
+
status="active",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def complete_brainstorm(
|
|
140
|
+
root: Path, cfg: SpecfloConfig, slug: str, today: str | None = None
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Mark the brainstorm complete (``status: draft → complete``); bump ``updated``.
|
|
143
|
+
|
|
144
|
+
Called when leaving the brainstorm phase (by `specflo advance`). Raises
|
|
145
|
+
``SpecfloError`` if the artifact is missing.
|
|
146
|
+
"""
|
|
147
|
+
path = brainstorm_path(root, cfg, slug)
|
|
148
|
+
if not path.is_file():
|
|
149
|
+
raise SpecfloError("No brainstorm yet. Run `specflo brainstorm start` first.")
|
|
150
|
+
doc = path.read_text()
|
|
151
|
+
# Frontmatter `status:` only — the leading-`-` decision `- Status:` lines and
|
|
152
|
+
# the count=1 (frontmatter comes first) keep this from touching entries.
|
|
153
|
+
doc = re.sub(r"(?m)^status:.*$", "status: complete", doc, count=1)
|
|
154
|
+
doc = markdown.bump_updated(doc, today)
|
|
155
|
+
path.write_text(doc)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_brainstorm(root: Path, cfg: SpecfloConfig, slug: str) -> list[str]:
|
|
159
|
+
"""Return a list of lint issues (empty == ready). Read-only."""
|
|
160
|
+
path = brainstorm_path(root, cfg, slug)
|
|
161
|
+
if not path.is_file():
|
|
162
|
+
return ["brainstorm.md not found — run `specflo brainstorm start`."]
|
|
163
|
+
doc = path.read_text()
|
|
164
|
+
body = markdown.strip_comments(doc)
|
|
165
|
+
issues = markdown.placeholder_issues(body)
|
|
166
|
+
|
|
167
|
+
if not _DECISION_ID_RE.search(doc):
|
|
168
|
+
issues.append("no decisions captured (need at least one).")
|
|
169
|
+
|
|
170
|
+
out_of_scope = markdown.section_body(doc, "## Out of scope / Deferred")
|
|
171
|
+
if out_of_scope is None:
|
|
172
|
+
issues.append("missing 'Out of scope / Deferred' section.")
|
|
173
|
+
elif not markdown.strip_comments(out_of_scope).strip():
|
|
174
|
+
issues.append("'Out of scope / Deferred' section is empty.")
|
|
175
|
+
|
|
176
|
+
if markdown.section_body(doc, "## Open questions") is None:
|
|
177
|
+
issues.append("missing 'Open questions' section.")
|
|
178
|
+
|
|
179
|
+
return issues
|
specflo/checkpoint.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""The `checkpoint` resume prompt — derived, agent-facing "resume here" state.
|
|
2
|
+
|
|
3
|
+
After every state-mutating command specflo rewrites a per-project
|
|
4
|
+
``checkpoint.md``: a short, derived prompt telling a freshly-cleared agent which
|
|
5
|
+
phase we're in, which files to read first, and the concrete next action. It is
|
|
6
|
+
fully derived from project state (phase + existing artifacts + the workflow's
|
|
7
|
+
next step), so it can be written automatically and is always current.
|
|
8
|
+
|
|
9
|
+
Mirrors ``guide.py``: pure derivation (``build_checkpoint``) + a renderer
|
|
10
|
+
(``render_checkpoint``) + a thin writer (``write_checkpoint``).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from . import plan as plan_module, validators, workflow
|
|
19
|
+
from .brainstorm import BRAINSTORM_FILENAME
|
|
20
|
+
from .config import SpecfloConfig, display_path
|
|
21
|
+
from .projects import (
|
|
22
|
+
COMPLETE_STATUS,
|
|
23
|
+
PROJECT_FILENAME,
|
|
24
|
+
SHELVED_STATUS,
|
|
25
|
+
Project,
|
|
26
|
+
project_dir,
|
|
27
|
+
)
|
|
28
|
+
from .spec import SPEC_FILENAME
|
|
29
|
+
|
|
30
|
+
CHECKPOINT_FILENAME = "checkpoint.md"
|
|
31
|
+
|
|
32
|
+
# Phase artifacts in pipeline order. ``checkpoint.md`` lists only the ones that
|
|
33
|
+
# actually exist, so there are no dangling references and the list grows on its
|
|
34
|
+
# own as later artifacts (plan.md, ...) land.
|
|
35
|
+
_ARTIFACT_ORDER: list[str] = [BRAINSTORM_FILENAME, SPEC_FILENAME, "plan.md"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def checkpoint_path(root: Path, cfg: SpecfloConfig, slug: str) -> Path:
|
|
39
|
+
return project_dir(root, cfg, slug) / CHECKPOINT_FILENAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_checkpoint(
|
|
43
|
+
root: Path,
|
|
44
|
+
project: Project,
|
|
45
|
+
cfg: SpecfloConfig | None = None,
|
|
46
|
+
today: str | None = None,
|
|
47
|
+
) -> dict:
|
|
48
|
+
"""Derive the resume-prompt payload for ``project`` from current state.
|
|
49
|
+
|
|
50
|
+
Read-only: inspects which artifacts exist on disk but mutates nothing.
|
|
51
|
+
|
|
52
|
+
``cfg`` is threaded from the write path so brainstorm/spec/plan can derive
|
|
53
|
+
honest doneness — running the phase's real validator inline (REQ-01/03).
|
|
54
|
+
Without it (a bare ``build_checkpoint`` call), the static work hint stands.
|
|
55
|
+
"""
|
|
56
|
+
directory = project.path
|
|
57
|
+
read_first = [display_path(directory / PROJECT_FILENAME, root, posix=True)]
|
|
58
|
+
for filename in _ARTIFACT_ORDER:
|
|
59
|
+
if (directory / filename).is_file():
|
|
60
|
+
read_first.append(display_path(directory / filename, root, posix=True))
|
|
61
|
+
shelved = project.status == SHELVED_STATUS
|
|
62
|
+
plan_file = directory / plan_module.PLAN_FILENAME
|
|
63
|
+
prog = None
|
|
64
|
+
plan_doc = None
|
|
65
|
+
milestone = None
|
|
66
|
+
boundary = None
|
|
67
|
+
# A shelved project's do_next ignores progress, so skip the plan-file read.
|
|
68
|
+
if not shelved and project.phase in ("plan", "execute") and plan_file.is_file():
|
|
69
|
+
plan_doc = plan_file.read_text()
|
|
70
|
+
prog = plan_module.progress_from_doc(plan_doc)
|
|
71
|
+
# The current milestone (None on a milestone-free or all-complete plan),
|
|
72
|
+
# named in the resume block at plan/execute so a resumed agent knows which
|
|
73
|
+
# slice the plan is on (REQ-15). Dormant without milestones (REQ-04).
|
|
74
|
+
milestone = plan_module.current_milestone_from_doc(plan_doc)
|
|
75
|
+
# The soft milestone-boundary verify beat (None off a boundary): the
|
|
76
|
+
# just-completed milestone's Exit checklist to verify before proceeding
|
|
77
|
+
# (REQ-14). Surfaced in the resume block; never a hard stop.
|
|
78
|
+
boundary = plan_module.milestone_boundary_from_doc(plan_doc)
|
|
79
|
+
if shelved:
|
|
80
|
+
# Paused: don't direct to the phase's work step — resume (or start new),
|
|
81
|
+
# while the recorded phase below is preserved so resume returns to it.
|
|
82
|
+
do_next = workflow.next_step(project.phase, shelved=True)
|
|
83
|
+
elif project.phase == "execute":
|
|
84
|
+
do_next = workflow.next_step(
|
|
85
|
+
"execute", progress=prog, complete=project.status == COMPLETE_STATUS
|
|
86
|
+
)
|
|
87
|
+
# Stuck on a superseded dependency: surface the same targeted rewire
|
|
88
|
+
# remediation as `task show`/`status`, replacing the generic hint.
|
|
89
|
+
if project.status != COMPLETE_STATUS and plan_doc is not None:
|
|
90
|
+
stuck = plan_module.stuck_next_step_from_doc(plan_doc)
|
|
91
|
+
if stuck:
|
|
92
|
+
do_next = stuck
|
|
93
|
+
else:
|
|
94
|
+
# Derived doneness (REQ-01/03): run the phase's real validator inline
|
|
95
|
+
# (no memoization) — a passing validator flips the hint to offer-advance,
|
|
96
|
+
# a failing or missing artifact reads as work-in-progress. cfg is threaded
|
|
97
|
+
# from the write path; without it the static work hint stands.
|
|
98
|
+
validates = False
|
|
99
|
+
if cfg is not None:
|
|
100
|
+
validator = validators.VALIDATORS.get(project.phase)
|
|
101
|
+
if validator is not None:
|
|
102
|
+
validates = not validator(root, cfg, project.slug)
|
|
103
|
+
do_next = workflow.next_step(project.phase, validates=validates)
|
|
104
|
+
# A plan that doesn't yet validate still names its next task; once it
|
|
105
|
+
# validates the offer-advance hint stands alone.
|
|
106
|
+
if project.phase == "plan" and not validates and prog is not None:
|
|
107
|
+
if prog["next_actionable"]:
|
|
108
|
+
do_next += " (next task: " + ", ".join(prog["next_actionable"]) + ")"
|
|
109
|
+
elif prog["all_done"]:
|
|
110
|
+
do_next += " (all tasks done)"
|
|
111
|
+
return {
|
|
112
|
+
"project": project.slug,
|
|
113
|
+
"phase": project.phase,
|
|
114
|
+
"status": project.status,
|
|
115
|
+
"shelved_reason": project.shelved_reason,
|
|
116
|
+
"generated": today or datetime.date.today().isoformat(),
|
|
117
|
+
"read_first": read_first,
|
|
118
|
+
"do_next": do_next,
|
|
119
|
+
"milestone": milestone,
|
|
120
|
+
"boundary": boundary,
|
|
121
|
+
"path": display_path(directory / CHECKPOINT_FILENAME, root, posix=True),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def render_checkpoint(payload: dict) -> str:
|
|
126
|
+
"""Render the payload to the markdown written to ``checkpoint.md``."""
|
|
127
|
+
shelved = payload.get("status") == SHELVED_STATUS
|
|
128
|
+
subtitle = f"_phase: {payload['phase']}"
|
|
129
|
+
if shelved:
|
|
130
|
+
subtitle += " (shelved)"
|
|
131
|
+
subtitle += f" | generated {payload['generated']}_"
|
|
132
|
+
lines = [
|
|
133
|
+
f"# Checkpoint - {payload['project']}",
|
|
134
|
+
subtitle,
|
|
135
|
+
"",
|
|
136
|
+
]
|
|
137
|
+
if shelved and payload.get("shelved_reason"):
|
|
138
|
+
lines += [f"**Shelved:** {payload['shelved_reason']}", ""]
|
|
139
|
+
lines += [
|
|
140
|
+
"## Read first",
|
|
141
|
+
*(f"- {path}" for path in payload["read_first"]),
|
|
142
|
+
"",
|
|
143
|
+
"## Do next",
|
|
144
|
+
payload["do_next"],
|
|
145
|
+
]
|
|
146
|
+
# Name the current milestone in the resume block so a resumed agent knows
|
|
147
|
+
# which slice the plan is on (REQ-15); absent on a milestone-free plan.
|
|
148
|
+
milestone = payload.get("milestone")
|
|
149
|
+
if milestone:
|
|
150
|
+
lines.append(
|
|
151
|
+
f"Current milestone: {milestone['id']} {milestone['title']} "
|
|
152
|
+
f"— {milestone['done']}/{milestone['total']} done."
|
|
153
|
+
)
|
|
154
|
+
# The soft milestone-boundary verify beat: the just-completed milestone's Exit
|
|
155
|
+
# checklist for a user-gated proceed (REQ-14). Absent off a boundary.
|
|
156
|
+
boundary = payload.get("boundary")
|
|
157
|
+
if boundary:
|
|
158
|
+
lines += ["", "## Milestone boundary", *plan_module.boundary_beat_lines(boundary)]
|
|
159
|
+
lines += [
|
|
160
|
+
"",
|
|
161
|
+
"## Resume",
|
|
162
|
+
"- `specflo status` - confirm phase/step",
|
|
163
|
+
"- `specflo checkpoint` - reprint this prompt",
|
|
164
|
+
"",
|
|
165
|
+
]
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def write_checkpoint(
|
|
170
|
+
root: Path,
|
|
171
|
+
project: Project,
|
|
172
|
+
cfg: SpecfloConfig | None = None,
|
|
173
|
+
today: str | None = None,
|
|
174
|
+
) -> Path:
|
|
175
|
+
"""Render the checkpoint for ``project`` and write ``checkpoint.md``.
|
|
176
|
+
|
|
177
|
+
``cfg`` is forwarded to :func:`build_checkpoint` so the written checkpoint
|
|
178
|
+
reflects derived doneness for brainstorm/spec/plan (REQ-01).
|
|
179
|
+
"""
|
|
180
|
+
payload = build_checkpoint(root, project, cfg=cfg, today=today)
|
|
181
|
+
path = project.path / CHECKPOINT_FILENAME
|
|
182
|
+
path.write_text(render_checkpoint(payload))
|
|
183
|
+
return path
|