devague 0.3.2__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.
devague/cli/_errors.py ADDED
@@ -0,0 +1,39 @@
1
+ """DevagueError and exit-code policy.
2
+
3
+ Every failure inside devague raises :class:`DevagueError`. The top-level
4
+ ``main()`` catches it, formats via :mod:`devague.cli._output`, and exits with
5
+ :attr:`DevagueError.code`. This centralises the exit-code policy and
6
+ guarantees no Python traceback leaks to stderr.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ # Exit-code policy:
14
+ # 0 = success
15
+ # 1 = user-input error (bad flag, missing required arg, unknown path)
16
+ # 2 = environment / setup error (tool not installed, file unreadable)
17
+ # 3+ = reserved for future categorisation
18
+ EXIT_SUCCESS = 0
19
+ EXIT_USER_ERROR = 1
20
+ EXIT_ENV_ERROR = 2
21
+
22
+
23
+ @dataclass
24
+ class DevagueError(Exception):
25
+ """Structured error raised within devague; carries a remediation hint."""
26
+
27
+ code: int
28
+ message: str
29
+ remediation: str = ""
30
+
31
+ def __post_init__(self) -> None:
32
+ super().__init__(self.message)
33
+
34
+ def to_dict(self) -> dict[str, object]:
35
+ return {
36
+ "code": self.code,
37
+ "message": self.message,
38
+ "remediation": self.remediation,
39
+ }
devague/cli/_frames.py ADDED
@@ -0,0 +1,29 @@
1
+ """Resolve the target frame for a move: explicit --frame, else the current frame."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devague import store
6
+ from devague.cli._errors import EXIT_USER_ERROR, DevagueError
7
+ from devague.frame import Frame
8
+
9
+
10
+ def resolve(slug: str | None) -> Frame:
11
+ slug = slug or store.current_slug()
12
+ if not slug:
13
+ raise DevagueError(
14
+ EXIT_USER_ERROR,
15
+ "no frame selected",
16
+ "run 'devague new \"<announcement>\"' or pass --frame <slug>",
17
+ )
18
+ try:
19
+ return store.load(slug)
20
+ except ValueError as exc:
21
+ raise DevagueError(
22
+ EXIT_USER_ERROR,
23
+ f"invalid frame slug: {slug!r}",
24
+ "slugs are lowercase letters, digits, and hyphens — no path separators",
25
+ ) from exc
26
+ except FileNotFoundError:
27
+ raise DevagueError(
28
+ EXIT_USER_ERROR, f"no such frame: {slug}", "run 'devague list' to see frames"
29
+ ) from None
devague/cli/_output.py ADDED
@@ -0,0 +1,45 @@
1
+ """stdout / stderr helpers with a strict split.
2
+
3
+ Rule: **results go to stdout, diagnostics and errors go to stderr.** Agents
4
+ parsing devague output can rely on this invariant. JSON mode routes structured
5
+ payloads to the same streams — it never mixes them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from typing import Any, TextIO
13
+
14
+ from devague.cli._errors import DevagueError
15
+
16
+
17
+ def emit_result(data: Any, *, json_mode: bool, stream: TextIO | None = None) -> None:
18
+ """Write a command result to stdout (text or JSON), newline-terminated."""
19
+ s = stream if stream is not None else sys.stdout
20
+ if json_mode:
21
+ json.dump(data, s, ensure_ascii=False)
22
+ s.write("\n")
23
+ return
24
+ text = data if isinstance(data, str) else str(data)
25
+ s.write(text)
26
+ if not text.endswith("\n"):
27
+ s.write("\n")
28
+
29
+
30
+ def emit_error(err: DevagueError, *, json_mode: bool, stream: TextIO | None = None) -> None:
31
+ """Write a :class:`DevagueError` to stderr (text or JSON)."""
32
+ s = stream if stream is not None else sys.stderr
33
+ if json_mode:
34
+ json.dump(err.to_dict(), s, ensure_ascii=False)
35
+ s.write("\n")
36
+ return
37
+ s.write(f"error: {err.message}\n")
38
+ if err.remediation:
39
+ s.write(f"hint: {err.remediation}\n")
40
+
41
+
42
+ def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
43
+ """Write a human diagnostic (progress, summary) to stderr."""
44
+ s = stream if stream is not None else sys.stderr
45
+ s.write(message if message.endswith("\n") else message + "\n")
devague/convergence.py ADDED
@@ -0,0 +1,72 @@
1
+ """The convergence gate: is a frame solid enough to export a buildable spec?"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from devague.frame import SPEC_AFFECTING_KINDS, Frame
8
+
9
+
10
+ @dataclass
11
+ class ConvergenceResult:
12
+ passed: bool
13
+ missing: list[str] = field(default_factory=list)
14
+
15
+
16
+ def _missing_required_kinds(confirmed_kinds: set[str]) -> list[str]:
17
+ """Required confirmed claims for an honest announcement frame."""
18
+ missing = [
19
+ f"missing confirmed '{required}' claim"
20
+ for required in ("announcement", "audience", "after_state")
21
+ if required not in confirmed_kinds
22
+ ]
23
+ if "before_state" not in confirmed_kinds and "why_it_matters" not in confirmed_kinds:
24
+ missing.append("missing 'before_state' or 'why_it_matters' claim")
25
+ if "boundary" not in confirmed_kinds:
26
+ missing.append("missing a 'boundary' / non-goal claim")
27
+ if "success_signal" not in confirmed_kinds:
28
+ missing.append("missing a 'success_signal' claim")
29
+ return missing
30
+
31
+
32
+ def _missing_claim_resolution(frame: Frame, confirmed: list) -> list[str]:
33
+ """No spec-affecting claim left proposed; each confirmed one is pressure-tested."""
34
+ missing = [
35
+ f"claim {c.id} still proposed (confirm or reject it)"
36
+ for c in frame.claims
37
+ if c.kind in SPEC_AFFECTING_KINDS and c.status == "proposed"
38
+ ]
39
+ missing += [
40
+ f"claim {c.id} has no confirmed honesty condition"
41
+ for c in confirmed
42
+ if c.kind in SPEC_AFFECTING_KINDS
43
+ and not any(h.status == "confirmed" for h in c.honesty_conditions)
44
+ ]
45
+ return missing
46
+
47
+
48
+ def _missing_open_uncertainty(frame: Frame) -> list[str]:
49
+ """No blocking vagueness or unresolved blocking hard question remains."""
50
+ missing = [
51
+ f"blocking vagueness {v.id} unresolved"
52
+ for v in frame.open_vagueness
53
+ if v.kind == "unknown_blocking"
54
+ ]
55
+ missing += [
56
+ f"blocking hard question {q.id} on {c.id} unresolved"
57
+ for c in frame.claims
58
+ for q in c.hard_questions
59
+ if q.blocking and not q.resolved
60
+ ]
61
+ return missing
62
+
63
+
64
+ def evaluate(frame: Frame) -> ConvergenceResult:
65
+ confirmed = [c for c in frame.claims if c.status == "confirmed"]
66
+ confirmed_kinds = {c.kind for c in confirmed}
67
+ missing = (
68
+ _missing_required_kinds(confirmed_kinds)
69
+ + _missing_claim_resolution(frame, confirmed)
70
+ + _missing_open_uncertainty(frame)
71
+ )
72
+ return ConvergenceResult(passed=not missing, missing=missing)
devague/frame.py ADDED
@@ -0,0 +1,184 @@
1
+ """The Frame domain model — claims, honesty conditions, hard questions, vagueness.
2
+
3
+ Pure data + transitions, no I/O. Persistence lives in :mod:`devague.store`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import dataclasses
9
+ from dataclasses import dataclass, field
10
+ from typing import Optional
11
+
12
+ CLAIM_KINDS = (
13
+ "announcement",
14
+ "audience",
15
+ "after_state",
16
+ "before_state",
17
+ "why_it_matters",
18
+ "boundary",
19
+ "success_signal",
20
+ "open_question",
21
+ )
22
+ SPEC_AFFECTING_KINDS = tuple(k for k in CLAIM_KINDS if k != "open_question")
23
+ VAGUENESS_KINDS = (
24
+ "unknown_nonblocking",
25
+ "unknown_blocking",
26
+ "out_of_scope",
27
+ "follow_up",
28
+ )
29
+ CLAIM_STATUSES = ("proposed", "confirmed", "rejected")
30
+
31
+
32
+ @dataclass
33
+ class HonestyCondition:
34
+ id: str
35
+ text: str
36
+ status: str = "proposed" # proposed | confirmed | rejected
37
+
38
+
39
+ @dataclass
40
+ class HardQuestion:
41
+ id: str
42
+ text: str
43
+ resolved: bool = False
44
+ blocking: bool = False
45
+
46
+
47
+ @dataclass
48
+ class Claim:
49
+ id: str
50
+ kind: str
51
+ text: str
52
+ origin: str = "user" # user | llm
53
+ status: str = "confirmed" # proposed | confirmed | rejected
54
+ honesty_conditions: list[HonestyCondition] = field(default_factory=list)
55
+ hard_questions: list[HardQuestion] = field(default_factory=list)
56
+ links: list[str] = field(default_factory=list)
57
+
58
+
59
+ @dataclass
60
+ class Vagueness:
61
+ id: str
62
+ text: str
63
+ kind: str
64
+ claim_id: Optional[str] = None
65
+
66
+
67
+ @dataclass
68
+ class Frame:
69
+ slug: str
70
+ title: str
71
+ status: str = "drafting" # drafting | converged | exported
72
+ created: str = ""
73
+ updated: str = ""
74
+ claims: list[Claim] = field(default_factory=list)
75
+ open_vagueness: list[Vagueness] = field(default_factory=list)
76
+
77
+ @staticmethod
78
+ def _next(items: list, prefix: str) -> str:
79
+ n = 0
80
+ for it in items:
81
+ if it.id.startswith(prefix):
82
+ try:
83
+ n = max(n, int(it.id[len(prefix) :]))
84
+ except ValueError:
85
+ pass
86
+ return f"{prefix}{n + 1}"
87
+
88
+ def _all_honesty(self) -> list[HonestyCondition]:
89
+ return [h for c in self.claims for h in c.honesty_conditions]
90
+
91
+ def _all_hard_questions(self) -> list[HardQuestion]:
92
+ return [q for c in self.claims for q in c.hard_questions]
93
+
94
+ def add_claim(self, kind: str, text: str, origin: str = "user") -> Claim:
95
+ if kind not in CLAIM_KINDS:
96
+ raise ValueError(f"unknown claim kind: {kind}")
97
+ status = "proposed" if origin == "llm" else "confirmed"
98
+ claim = Claim(
99
+ id=self._next(self.claims, "c"),
100
+ kind=kind,
101
+ text=text,
102
+ origin=origin,
103
+ status=status,
104
+ )
105
+ self.claims.append(claim)
106
+ return claim
107
+
108
+ def find_claim(self, cid: str) -> Optional[Claim]:
109
+ return next((c for c in self.claims if c.id == cid), None)
110
+
111
+ def find_honesty(self, hid: str) -> Optional[HonestyCondition]:
112
+ return next((h for h in self._all_honesty() if h.id == hid), None)
113
+
114
+ def add_honesty(self, claim: Claim, text: str, origin: str = "llm") -> HonestyCondition:
115
+ status = "confirmed" if origin == "user" else "proposed"
116
+ h = HonestyCondition(
117
+ id=self._next(self._all_honesty(), "h"),
118
+ text=text,
119
+ status=status,
120
+ )
121
+ claim.honesty_conditions.append(h)
122
+ return h
123
+
124
+ def add_hard_question(self, claim: Claim, text: str, blocking: bool = False) -> HardQuestion:
125
+ q = HardQuestion(
126
+ id=self._next(self._all_hard_questions(), "q"),
127
+ text=text,
128
+ blocking=blocking,
129
+ )
130
+ claim.hard_questions.append(q)
131
+ return q
132
+
133
+ def add_vagueness(self, text: str, kind: str, claim_id: Optional[str] = None) -> Vagueness:
134
+ if kind not in VAGUENESS_KINDS:
135
+ raise ValueError(f"unknown vagueness kind: {kind}")
136
+ v = Vagueness(
137
+ id=self._next(self.open_vagueness, "v"),
138
+ text=text,
139
+ kind=kind,
140
+ claim_id=claim_id,
141
+ )
142
+ self.open_vagueness.append(v)
143
+ return v
144
+
145
+ def set_status(self, item_id: str, status: str) -> bool:
146
+ claim = self.find_claim(item_id)
147
+ if claim is not None:
148
+ claim.status = status
149
+ return True
150
+ honesty = self.find_honesty(item_id)
151
+ if honesty is not None:
152
+ honesty.status = status
153
+ return True
154
+ return False
155
+
156
+
157
+ def to_dict(frame: Frame) -> dict:
158
+ return dataclasses.asdict(frame)
159
+
160
+
161
+ def from_dict(d: dict) -> Frame:
162
+ claims = [
163
+ Claim(
164
+ id=c["id"],
165
+ kind=c["kind"],
166
+ text=c["text"],
167
+ origin=c.get("origin", "user"),
168
+ status=c.get("status", "confirmed"),
169
+ honesty_conditions=[HonestyCondition(**h) for h in c.get("honesty_conditions", [])],
170
+ hard_questions=[HardQuestion(**q) for q in c.get("hard_questions", [])],
171
+ links=list(c.get("links", [])),
172
+ )
173
+ for c in d.get("claims", [])
174
+ ]
175
+ vag = [Vagueness(**v) for v in d.get("open_vagueness", [])]
176
+ return Frame(
177
+ slug=d["slug"],
178
+ title=d["title"],
179
+ status=d.get("status", "drafting"),
180
+ created=d.get("created", ""),
181
+ updated=d.get("updated", ""),
182
+ claims=claims,
183
+ open_vagueness=vag,
184
+ )
@@ -0,0 +1,39 @@
1
+ """Renderer registry: frame/spec → text, selected by --format.
2
+
3
+ New output modalities (NotebookLM, HTML, user stories) register here.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Callable
9
+
10
+ from devague.cli._errors import EXIT_USER_ERROR, DevagueError
11
+ from devague.frame import Frame
12
+
13
+ _REGISTRY: dict[str, Callable[[Frame], str]] = {}
14
+
15
+
16
+ def register(name: str, fn: Callable[[Frame], str]) -> None:
17
+ _REGISTRY[name] = fn
18
+
19
+
20
+ def formats() -> list[str]:
21
+ return sorted(_REGISTRY)
22
+
23
+
24
+ def render(frame: Frame, fmt: str) -> str:
25
+ if fmt not in _REGISTRY:
26
+ raise DevagueError(
27
+ EXIT_USER_ERROR,
28
+ f"unknown format: {fmt}",
29
+ f"available formats: {', '.join(formats())}",
30
+ )
31
+ return _REGISTRY[fmt](frame)
32
+
33
+
34
+ # Register the built-in renderers (import-time side effect).
35
+ from devague.render import frame_md as _frame_md # noqa: E402
36
+ from devague.render import spec_md as _spec_md # noqa: E402
37
+
38
+ register("frame-md", _frame_md.render_frame)
39
+ register("spec-md", _spec_md.render_spec)
@@ -0,0 +1,61 @@
1
+ """Renderer: the Announcement Frame as markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devague.frame import Frame
6
+
7
+ _SECTIONS = [
8
+ ("announcement", "Announcement"),
9
+ ("audience", "Audience"),
10
+ ("after_state", "After-state experience"),
11
+ ("why_it_matters", "Why it matters"),
12
+ ("before_state", "Before-state pain"),
13
+ ("boundary", "Boundaries / non-goals"),
14
+ ("success_signal", "Success signals"),
15
+ ("open_question", "Open questions"),
16
+ ]
17
+
18
+
19
+ def _claim_lines(claim) -> list[str]:
20
+ mark = "" if claim.status == "confirmed" else f" _({claim.status})_"
21
+ lines = [f"- {claim.text}{mark}"]
22
+ for h in claim.honesty_conditions:
23
+ hm = "" if h.status == "confirmed" else f" _({h.status})_"
24
+ lines.append(f" - honesty: {h.text}{hm}")
25
+ for q in claim.hard_questions:
26
+ qm = "blocking" if q.blocking else "open"
27
+ lines.append(f" - Q ({qm}): {q.text}")
28
+ return lines
29
+
30
+
31
+ def _section_lines(frame: Frame, kind: str, heading: str) -> list[str]:
32
+ claims = [c for c in frame.claims if c.kind == kind and c.status != "rejected"]
33
+ if not claims:
34
+ return []
35
+ lines = [f"## {heading}"]
36
+ for c in claims:
37
+ lines.extend(_claim_lines(c))
38
+ lines.append("")
39
+ return lines
40
+
41
+
42
+ def _vagueness_lines(frame: Frame) -> list[str]:
43
+ if not frame.open_vagueness:
44
+ return []
45
+ lines = ["## Open vagueness"]
46
+ lines.extend(f"- [{v.kind}] {v.text}" for v in frame.open_vagueness)
47
+ lines.append("")
48
+ return lines
49
+
50
+
51
+ def render_frame(frame: Frame) -> str:
52
+ out = [
53
+ f"# Announcement Frame — {frame.title}",
54
+ "",
55
+ f"_slug: {frame.slug} · status: {frame.status}_",
56
+ "",
57
+ ]
58
+ for kind, heading in _SECTIONS:
59
+ out.extend(_section_lines(frame, kind, heading))
60
+ out.extend(_vagueness_lines(frame))
61
+ return "\n".join(out).rstrip() + "\n"
@@ -0,0 +1,48 @@
1
+ """Renderer: the buildable spec as markdown, derived from a converged frame."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devague.frame import Frame
6
+
7
+
8
+ def _texts(frame: Frame, kind: str) -> list[str]:
9
+ return [c.text for c in frame.claims if c.kind == kind and c.status == "confirmed"]
10
+
11
+
12
+ def render_spec(frame: Frame) -> str:
13
+ out: list[str] = [f"# {frame.title}", ""]
14
+ ann = _texts(frame, "announcement")
15
+ if ann:
16
+ out += ["> " + ann[0], ""]
17
+ aud = _texts(frame, "audience")
18
+ if aud:
19
+ out += ["## Audience", *[f"- {t}" for t in aud], ""]
20
+ out += ["## Before → After", ""]
21
+ for t in _texts(frame, "before_state"):
22
+ out.append(f"- Before: {t}")
23
+ for t in _texts(frame, "after_state"):
24
+ out.append(f"- After: {t}")
25
+ out.append("")
26
+ why = _texts(frame, "why_it_matters")
27
+ if why:
28
+ out += ["## Why it matters", *[f"- {t}" for t in why], ""]
29
+ reqs = [h.text for c in frame.claims for h in c.honesty_conditions if h.status == "confirmed"]
30
+ if reqs:
31
+ out += ["## Requirements / honesty conditions", *[f"- {t}" for t in reqs], ""]
32
+ succ = _texts(frame, "success_signal")
33
+ if succ:
34
+ out += ["## Success signals", *[f"- {t}" for t in succ], ""]
35
+ bnd = _texts(frame, "boundary")
36
+ if bnd:
37
+ out += ["## Non-goals", *[f"- {t}" for t in bnd], ""]
38
+ hqs = [q for c in frame.claims for q in c.hard_questions]
39
+ if hqs:
40
+ out += [
41
+ "## Hard questions",
42
+ *[f"- {q.text}" + (" (blocking)" if q.blocking else "") for q in hqs],
43
+ "",
44
+ ]
45
+ follow = [v.text for v in frame.open_vagueness if v.kind in ("follow_up", "out_of_scope")]
46
+ if follow:
47
+ out += ["## Open / follow-up", *[f"- {t}" for t in follow], ""]
48
+ return "\n".join(out).rstrip() + "\n"
devague/store.py ADDED
@@ -0,0 +1,88 @@
1
+ """Frame persistence: JSON under .devague/frames/, plus a current-frame pointer.
2
+
3
+ Paths are cwd-relative so the frames live in the repo being specced.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from devague.frame import Frame, from_dict, to_dict
14
+
15
+ FRAMES_DIR = Path(".devague/frames")
16
+ CURRENT = Path(".devague/current")
17
+
18
+ # A safe slug is a bounded, lowercase, hyphen-separated token with no path
19
+ # separators or `.` segments — so it can never escape FRAMES_DIR / SPECS_DIR.
20
+ _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,79}$")
21
+
22
+
23
+ def slugify(title: str) -> str:
24
+ s = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
25
+ s = s[:50].strip("-")
26
+ return s or "frame"
27
+
28
+
29
+ def validate_slug(slug: str) -> str:
30
+ """Return ``slug`` if it is filesystem-safe, else raise ``ValueError``.
31
+
32
+ Guards every path built from a slug (``--frame``, ``.devague/current``, a
33
+ persisted ``frame.slug``) against path traversal and absolute paths.
34
+ """
35
+ if not _SLUG_RE.fullmatch(slug or ""):
36
+ raise ValueError(f"invalid frame slug: {slug!r}")
37
+ return slug
38
+
39
+
40
+ def _now() -> str:
41
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
42
+
43
+
44
+ def path_for(slug: str) -> Path:
45
+ return FRAMES_DIR / f"{validate_slug(slug)}.json"
46
+
47
+
48
+ def unique_slug(base: str) -> str:
49
+ """Return ``base`` if free, else the first ``base-N`` (N>=2) that is unused."""
50
+ base = base or "frame"
51
+ if not path_for(base).exists():
52
+ return base
53
+ n = 2
54
+ while path_for(f"{base}-{n}").exists():
55
+ n += 1
56
+ return f"{base}-{n}"
57
+
58
+
59
+ def save(frame: Frame) -> Path:
60
+ FRAMES_DIR.mkdir(parents=True, exist_ok=True)
61
+ frame.updated = _now()
62
+ if not frame.created:
63
+ frame.created = frame.updated
64
+ p = path_for(frame.slug)
65
+ p.write_text(json.dumps(to_dict(frame), indent=2) + "\n", encoding="utf-8")
66
+ CURRENT.write_text(frame.slug + "\n", encoding="utf-8")
67
+ return p
68
+
69
+
70
+ def load(slug: str) -> Frame:
71
+ p = path_for(slug)
72
+ if not p.exists():
73
+ raise FileNotFoundError(slug)
74
+ frame = from_dict(json.loads(p.read_text(encoding="utf-8")))
75
+ validate_slug(frame.slug) # reject a tampered file whose internal slug escapes
76
+ return frame
77
+
78
+
79
+ def list_slugs() -> list[str]:
80
+ if not FRAMES_DIR.exists():
81
+ return []
82
+ return sorted(p.stem for p in FRAMES_DIR.glob("*.json"))
83
+
84
+
85
+ def current_slug() -> str | None:
86
+ if CURRENT.exists():
87
+ return CURRENT.read_text(encoding="utf-8").strip() or None
88
+ return None
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: devague
3
+ Version: 0.3.2
4
+ Summary: devague — turns a vague feature idea into a buildable spec by working backwards.
5
+ Project-URL: Homepage, https://github.com/agentculture/devague
6
+ Project-URL: Issues, https://github.com/agentculture/devague/issues
7
+ Author: AgentCulture
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # devague
19
+
20
+ An AgentCulture agent that turns a vague feature idea into a buildable spec by working backwards.
21
+
22
+ Greenfield: the CLI scaffold (`devague learn` / `explain`) ships as honest
23
+ "not yet implemented" stubs. See `CLAUDE.md` for project shape and commands.
@@ -0,0 +1,30 @@
1
+ devague/__init__.py,sha256=IKSTqWc8OquRgC2y9E0TTv5CtNH3d2r82D2gLZa6hOQ,372
2
+ devague/__main__.py,sha256=wmTRpe3jqjUBqCm9NnpB2wO5V8U-gHJ5L7qBJMfCS_k,145
3
+ devague/convergence.py,sha256=mY-rh9eyd0halOzyoWefGqXk9yFGU1_cHnSG_Pq6B7Q,2532
4
+ devague/frame.py,sha256=jhBd3Sq2ChE2Ax_e-rlVavjSM1qvv-3R11hGPNPCc-g,5346
5
+ devague/store.py,sha256=XQ56DZwTgC_UuvAfhUiIKY9vsoTTDRVWVJc2ihztab0,2519
6
+ devague/cli/__init__.py,sha256=p5kdxD4IOBNuKVPOJOwF0-KxApfic-FLbPceOPN7my4,4370
7
+ devague/cli/_errors.py,sha256=VxfMpU-ZFv_-oXnxTaYpsCpn7CheS4ppwp3mrrSZh0k,1108
8
+ devague/cli/_frames.py,sha256=BMxgZftUeVs9gy2RTQ-1Jl1FJzpz6RJByF51qKffXU4,963
9
+ devague/cli/_output.py,sha256=JTtXNyd2MfIp77eguICh5ybZm2Y9xriZkYLti2Z_LcE,1547
10
+ devague/cli/_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ devague/cli/_commands/capture.py,sha256=lHlgVlAXt4MPP-SQJw5oRQ2u-A_UyATbpPWBZSMbSPQ,1234
12
+ devague/cli/_commands/confirm.py,sha256=YoiqJvy4czbGGvSoMKzDaiyq2g4V_hprREiuTFLzyw0,1323
13
+ devague/cli/_commands/converge.py,sha256=wIX9GkxIfb2h58UwBXJIE4H72Db6IYRC88e-9O5WL8I,1300
14
+ devague/cli/_commands/explain.py,sha256=gGSbBmGOhnjaLYhLth0XWbzklVb3ilukbkf6HwtY7yI,1042
15
+ devague/cli/_commands/export.py,sha256=lVWg-ZjCNogdFjBS85ckibOcWaGjydIAuo56ce3CMe8,1673
16
+ devague/cli/_commands/interrogate.py,sha256=J0g9I-3P1fepK4NIkrP54iy7GisPvrb-H-IIs9y3QQQ,2919
17
+ devague/cli/_commands/learn.py,sha256=u8-e6bmBd3_xZeM-MZ1JrLIf9NcoWCG2ej2dV85iW3w,3844
18
+ devague/cli/_commands/list_frames.py,sha256=rzM6FU9SY_rwxPaGhG-G3EVhmI0LVVNl4UiDNMBOyDA,861
19
+ devague/cli/_commands/new.py,sha256=wUgJeS-drCnhZ-LGpA2WXKsm70iN_cmltVloFsUMpX4,1257
20
+ devague/cli/_commands/park.py,sha256=CvdrDFzFgBOitTb-9xgOcFGoFgZxxXmR9riCkhhvdrk,1183
21
+ devague/cli/_commands/reject.py,sha256=pc051ZrCzfzBOGtJW9rLiBnSNSq8qir1g4KBrE9YikM,653
22
+ devague/cli/_commands/show.py,sha256=vIXoUmbruT8EDI7FrER514TGfSq2k3qGxcOPtLlt1Ls,960
23
+ devague/render/__init__.py,sha256=DosV7sVtrOmkYTGKFgH0oMQZtJ_XhJBj1g0n__ysYJY,1043
24
+ devague/render/frame_md.py,sha256=_-9xOfd50XbPBmdQGZCDilFObTnFkmW0JIb9qzFgxMI,1861
25
+ devague/render/spec_md.py,sha256=uSOGmhe-NpSg6592iFEu0iQyCfdF6nRtPiJ1llwF_3c,1801
26
+ devague-0.3.2.dist-info/METADATA,sha256=YpJrnFpu73ujN_mkpm5JUcWHcE6O0mdJKjNer-2wiZc,900
27
+ devague-0.3.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
28
+ devague-0.3.2.dist-info/entry_points.txt,sha256=M6Y_rebZwi29hI8d4nihXrv6j1gt-XNyFwXocwGUfLk,45
29
+ devague-0.3.2.dist-info/licenses/LICENSE,sha256=wCcdPywGtFXx1P8N0j0eEDINSWfSjrIsU7ds1YZl-MA,1069
30
+ devague-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devague = devague.cli:main