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/__init__.py +11 -0
- devague/__main__.py +8 -0
- devague/cli/__init__.py +123 -0
- devague/cli/_commands/__init__.py +0 -0
- devague/cli/_commands/capture.py +31 -0
- devague/cli/_commands/confirm.py +38 -0
- devague/cli/_commands/converge.py +37 -0
- devague/cli/_commands/explain.py +31 -0
- devague/cli/_commands/export.py +49 -0
- devague/cli/_commands/interrogate.py +66 -0
- devague/cli/_commands/learn.py +88 -0
- devague/cli/_commands/list_frames.py +27 -0
- devague/cli/_commands/new.py +33 -0
- devague/cli/_commands/park.py +31 -0
- devague/cli/_commands/reject.py +19 -0
- devague/cli/_commands/show.py +27 -0
- devague/cli/_errors.py +39 -0
- devague/cli/_frames.py +29 -0
- devague/cli/_output.py +45 -0
- devague/convergence.py +72 -0
- devague/frame.py +184 -0
- devague/render/__init__.py +39 -0
- devague/render/frame_md.py +61 -0
- devague/render/spec_md.py +48 -0
- devague/store.py +88 -0
- devague-0.3.2.dist-info/METADATA +23 -0
- devague-0.3.2.dist-info/RECORD +30 -0
- devague-0.3.2.dist-info/WHEEL +4 -0
- devague-0.3.2.dist-info/entry_points.txt +2 -0
- devague-0.3.2.dist-info/licenses/LICENSE +21 -0
devague/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""devague — turns a vague feature idea into a buildable spec by working backwards."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _v
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = _v("devague")
|
|
8
|
+
except PackageNotFoundError: # pragma: no cover — editable install without metadata
|
|
9
|
+
__version__ = "0.0.0+local"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
devague/__main__.py
ADDED
devague/cli/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Unified CLI entry point for devague.
|
|
2
|
+
|
|
3
|
+
Error-propagation contract: every handler raises
|
|
4
|
+
:class:`devague.cli._errors.DevagueError` on failure; ``main()`` catches it
|
|
5
|
+
via :func:`_dispatch` and routes through :mod:`devague.cli._output`. Unknown
|
|
6
|
+
exceptions are wrapped into a ``DevagueError`` so no Python traceback leaks.
|
|
7
|
+
|
|
8
|
+
Argparse errors (unknown verb, missing required arg) also route through the
|
|
9
|
+
structured format — :class:`_DevagueArgumentParser` overrides ``.error()``.
|
|
10
|
+
Whether errors render as text or JSON depends on whether ``--json`` appears in
|
|
11
|
+
the raw argv (:func:`main` sets ``_DevagueArgumentParser._json_hint`` before
|
|
12
|
+
``parse_args``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from devague import __version__
|
|
21
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
22
|
+
from devague.cli._output import emit_error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _DevagueArgumentParser(argparse.ArgumentParser):
|
|
26
|
+
"""ArgumentParser that routes errors through :func:`emit_error`."""
|
|
27
|
+
|
|
28
|
+
_json_hint: bool = False
|
|
29
|
+
|
|
30
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
31
|
+
err = DevagueError(
|
|
32
|
+
code=EXIT_USER_ERROR,
|
|
33
|
+
message=message,
|
|
34
|
+
remediation=f"run '{self.prog} --help' to see valid arguments",
|
|
35
|
+
)
|
|
36
|
+
emit_error(err, json_mode=type(self)._json_hint)
|
|
37
|
+
raise SystemExit(err.code)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _argv_has_json(argv: list[str] | None) -> bool:
|
|
41
|
+
tokens = argv if argv is not None else sys.argv[1:]
|
|
42
|
+
return any(t == "--json" or t.startswith("--json=") for t in tokens)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
parser = _DevagueArgumentParser(
|
|
47
|
+
prog="devague",
|
|
48
|
+
description="devague — turns a vague idea into a buildable spec by working backwards.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--version",
|
|
52
|
+
action="version",
|
|
53
|
+
version=f"%(prog)s {__version__}",
|
|
54
|
+
)
|
|
55
|
+
sub = parser.add_subparsers(dest="command", parser_class=_DevagueArgumentParser)
|
|
56
|
+
|
|
57
|
+
from devague.cli._commands import capture as _capture_cmd
|
|
58
|
+
from devague.cli._commands import confirm as _confirm_cmd
|
|
59
|
+
from devague.cli._commands import converge as _converge_cmd
|
|
60
|
+
from devague.cli._commands import explain as _explain_cmd
|
|
61
|
+
from devague.cli._commands import export as _export_cmd
|
|
62
|
+
from devague.cli._commands import interrogate as _interrogate_cmd
|
|
63
|
+
from devague.cli._commands import learn as _learn_cmd
|
|
64
|
+
from devague.cli._commands import list_frames as _list_cmd
|
|
65
|
+
from devague.cli._commands import new as _new_cmd
|
|
66
|
+
from devague.cli._commands import park as _park_cmd
|
|
67
|
+
from devague.cli._commands import reject as _reject_cmd
|
|
68
|
+
from devague.cli._commands import show as _show_cmd
|
|
69
|
+
|
|
70
|
+
_learn_cmd.register(sub)
|
|
71
|
+
_explain_cmd.register(sub)
|
|
72
|
+
_new_cmd.register(sub)
|
|
73
|
+
_capture_cmd.register(sub)
|
|
74
|
+
_interrogate_cmd.register(sub)
|
|
75
|
+
_confirm_cmd.register(sub)
|
|
76
|
+
_reject_cmd.register(sub)
|
|
77
|
+
_park_cmd.register(sub)
|
|
78
|
+
_converge_cmd.register(sub)
|
|
79
|
+
_export_cmd.register(sub)
|
|
80
|
+
_show_cmd.register(sub)
|
|
81
|
+
_list_cmd.register(sub)
|
|
82
|
+
|
|
83
|
+
return parser
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
87
|
+
"""Invoke the registered handler and translate exceptions to exit codes.
|
|
88
|
+
|
|
89
|
+
A handler may return ``None`` (treated as success, exit 0) or an ``int``
|
|
90
|
+
used directly as the exit code. Failures MUST raise :class:`DevagueError`;
|
|
91
|
+
any other exception is wrapped so no Python traceback leaks.
|
|
92
|
+
"""
|
|
93
|
+
json_mode = bool(getattr(args, "json", False))
|
|
94
|
+
try:
|
|
95
|
+
rc = args.func(args)
|
|
96
|
+
except DevagueError as err:
|
|
97
|
+
emit_error(err, json_mode=json_mode)
|
|
98
|
+
return err.code
|
|
99
|
+
except Exception as err: # noqa: BLE001 - last-resort; wrap and route cleanly
|
|
100
|
+
wrapped = DevagueError(
|
|
101
|
+
code=EXIT_USER_ERROR,
|
|
102
|
+
message=f"unexpected: {err.__class__.__name__}: {err}",
|
|
103
|
+
remediation="file a bug at https://github.com/agentculture/devague/issues",
|
|
104
|
+
)
|
|
105
|
+
emit_error(wrapped, json_mode=json_mode)
|
|
106
|
+
return wrapped.code
|
|
107
|
+
return rc if rc is not None else 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv: list[str] | None = None) -> int:
|
|
111
|
+
_DevagueArgumentParser._json_hint = _argv_has_json(argv)
|
|
112
|
+
parser = _build_parser()
|
|
113
|
+
args = parser.parse_args(argv)
|
|
114
|
+
|
|
115
|
+
if args.command is None:
|
|
116
|
+
parser.print_help()
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
return _dispatch(args)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``devague capture`` — record and classify a claim."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._frames import resolve
|
|
9
|
+
from devague.cli._output import emit_result
|
|
10
|
+
from devague.frame import CLAIM_KINDS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_capture(args: argparse.Namespace) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
claim = frame.add_claim(args.kind, args.text, origin=args.origin)
|
|
16
|
+
store.save(frame)
|
|
17
|
+
if getattr(args, "json", False):
|
|
18
|
+
emit_result({"id": claim.id, "kind": claim.kind, "status": claim.status}, json_mode=True)
|
|
19
|
+
else:
|
|
20
|
+
emit_result(f"captured {claim.id} ({claim.kind}, {claim.status})", json_mode=False)
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
25
|
+
p = sub.add_parser("capture", help="Record and classify a claim.")
|
|
26
|
+
p.add_argument("text", help="The claim text.")
|
|
27
|
+
p.add_argument("--kind", required=True, choices=CLAIM_KINDS, help="Claim kind.")
|
|
28
|
+
p.add_argument("--origin", choices=("user", "llm"), default="user", help="Who proposed it.")
|
|
29
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
30
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
31
|
+
p.set_defaults(func=cmd_capture)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""``devague confirm`` — confirm a claim or honesty condition (user-only transition)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
9
|
+
from devague.cli._frames import resolve
|
|
10
|
+
from devague.cli._output import emit_result
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _transition(args: argparse.Namespace, status: str) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
if not frame.set_status(args.id, status):
|
|
16
|
+
raise DevagueError(
|
|
17
|
+
EXIT_USER_ERROR,
|
|
18
|
+
f"no such claim or honesty condition: {args.id}",
|
|
19
|
+
"run 'devague show'",
|
|
20
|
+
)
|
|
21
|
+
store.save(frame)
|
|
22
|
+
if getattr(args, "json", False):
|
|
23
|
+
emit_result({"id": args.id, "status": status}, json_mode=True)
|
|
24
|
+
else:
|
|
25
|
+
emit_result(f"{args.id} -> {status}", json_mode=False)
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_confirm(args: argparse.Namespace) -> int:
|
|
30
|
+
return _transition(args, "confirmed")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
34
|
+
p = sub.add_parser("confirm", help="Confirm a claim or honesty condition.")
|
|
35
|
+
p.add_argument("id", help="Claim id (c*) or honesty id (h*).")
|
|
36
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
37
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
38
|
+
p.set_defaults(func=cmd_confirm)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""``devague converge`` — evaluate the convergence gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._frames import resolve
|
|
9
|
+
from devague.cli._output import emit_result
|
|
10
|
+
from devague.convergence import evaluate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_converge(args: argparse.Namespace) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
result = evaluate(frame)
|
|
16
|
+
if result.passed and frame.status == "drafting":
|
|
17
|
+
frame.status = "converged"
|
|
18
|
+
store.save(frame)
|
|
19
|
+
elif not result.passed and frame.status == "converged":
|
|
20
|
+
frame.status = "drafting"
|
|
21
|
+
store.save(frame)
|
|
22
|
+
if getattr(args, "json", False):
|
|
23
|
+
emit_result({"passed": result.passed, "missing": result.missing}, json_mode=True)
|
|
24
|
+
elif result.passed:
|
|
25
|
+
emit_result("converged ✓", json_mode=False)
|
|
26
|
+
else:
|
|
27
|
+
emit_result(
|
|
28
|
+
"not converged:\n" + "\n".join(f" - {m}" for m in result.missing), json_mode=False
|
|
29
|
+
)
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
34
|
+
p = sub.add_parser("converge", help="Check whether the frame can export a spec.")
|
|
35
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
36
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
37
|
+
p.set_defaults(func=cmd_converge)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``devague explain <move>`` — print docs for a single move."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague.cli._commands.learn import MOVES
|
|
8
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
9
|
+
from devague.cli._output import emit_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_explain(args: argparse.Namespace) -> int:
|
|
13
|
+
desc = MOVES.get(args.move)
|
|
14
|
+
if desc is None:
|
|
15
|
+
raise DevagueError(
|
|
16
|
+
EXIT_USER_ERROR,
|
|
17
|
+
f"unknown move: {args.move}",
|
|
18
|
+
f"available moves: {', '.join(MOVES)}",
|
|
19
|
+
)
|
|
20
|
+
if getattr(args, "json", False):
|
|
21
|
+
emit_result({"move": args.move, "description": desc}, json_mode=True)
|
|
22
|
+
else:
|
|
23
|
+
emit_result(f"{args.move}: {desc}", json_mode=False)
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
28
|
+
p = sub.add_parser("explain", help="Explain a devague move.")
|
|
29
|
+
p.add_argument("move", help="A move name (e.g. converge).")
|
|
30
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
31
|
+
p.set_defaults(func=cmd_explain)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""``devague export`` — write the buildable spec, only if the frame has converged."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devague import render, store
|
|
9
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
10
|
+
from devague.cli._frames import resolve
|
|
11
|
+
from devague.cli._output import emit_result
|
|
12
|
+
from devague.convergence import evaluate
|
|
13
|
+
|
|
14
|
+
SPECS_DIR = Path("docs/specs")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_export(args: argparse.Namespace) -> int:
|
|
18
|
+
frame = resolve(args.frame)
|
|
19
|
+
result = evaluate(frame)
|
|
20
|
+
if not result.passed:
|
|
21
|
+
raise DevagueError(
|
|
22
|
+
EXIT_USER_ERROR,
|
|
23
|
+
"frame has not converged; cannot export",
|
|
24
|
+
"resolve: " + "; ".join(result.missing),
|
|
25
|
+
)
|
|
26
|
+
text = render.render(frame, args.format)
|
|
27
|
+
SPECS_DIR.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
out_path = SPECS_DIR / f"{frame.slug}.md"
|
|
29
|
+
out_path.write_text(text, encoding="utf-8")
|
|
30
|
+
frame.status = "exported"
|
|
31
|
+
store.save(frame)
|
|
32
|
+
if getattr(args, "json", False):
|
|
33
|
+
emit_result({"path": str(out_path), "format": args.format}, json_mode=True)
|
|
34
|
+
else:
|
|
35
|
+
emit_result(f"exported spec to {out_path}", json_mode=False)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
40
|
+
p = sub.add_parser("export", help="Export the buildable spec (requires convergence).")
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--format",
|
|
43
|
+
default="spec-md",
|
|
44
|
+
choices=("spec-md",),
|
|
45
|
+
help="Renderer format (default: spec-md).",
|
|
46
|
+
)
|
|
47
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
48
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
49
|
+
p.set_defaults(func=cmd_export)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""``devague interrogate`` — pressure-test a claim with honesty conditions / hard questions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
9
|
+
from devague.cli._frames import resolve
|
|
10
|
+
from devague.cli._output import emit_result
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_interrogate(args: argparse.Namespace) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
claim = frame.find_claim(args.claim_id)
|
|
16
|
+
if claim is None:
|
|
17
|
+
raise DevagueError(EXIT_USER_ERROR, f"no such claim: {args.claim_id}", "run 'devague show'")
|
|
18
|
+
added: list[dict] = []
|
|
19
|
+
if args.honesty:
|
|
20
|
+
h = frame.add_honesty(claim, args.honesty, origin=args.origin)
|
|
21
|
+
added.append({"kind": "honesty", "id": h.id, "status": h.status})
|
|
22
|
+
if args.risk:
|
|
23
|
+
q = frame.add_hard_question(claim, f"risk: {args.risk}", blocking=False)
|
|
24
|
+
added.append({"kind": "hard_question", "id": q.id, "status": "open"})
|
|
25
|
+
if args.hard_question:
|
|
26
|
+
q = frame.add_hard_question(claim, args.hard_question, blocking=args.blocking)
|
|
27
|
+
added.append(
|
|
28
|
+
{"kind": "hard_question", "id": q.id, "status": "blocking" if q.blocking else "open"}
|
|
29
|
+
)
|
|
30
|
+
if args.contradicts:
|
|
31
|
+
q = frame.add_hard_question(claim, f"contradiction with {args.contradicts}?", blocking=True)
|
|
32
|
+
added.append({"kind": "hard_question", "id": q.id, "status": "blocking"})
|
|
33
|
+
if not added:
|
|
34
|
+
raise DevagueError(
|
|
35
|
+
EXIT_USER_ERROR,
|
|
36
|
+
"nothing to interrogate",
|
|
37
|
+
"pass --honesty / --hard-question / --risk / --contradicts",
|
|
38
|
+
)
|
|
39
|
+
store.save(frame)
|
|
40
|
+
if getattr(args, "json", False):
|
|
41
|
+
emit_result({"claim": claim.id, "added": added}, json_mode=True)
|
|
42
|
+
else:
|
|
43
|
+
emit_result(
|
|
44
|
+
f"interrogated {claim.id}: " + ", ".join(f"{a['kind']} {a['id']}" for a in added),
|
|
45
|
+
json_mode=False,
|
|
46
|
+
)
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
51
|
+
p = sub.add_parser("interrogate", help="Attach honesty conditions / hard questions to a claim.")
|
|
52
|
+
p.add_argument("claim_id", help="Claim id (e.g. c1).")
|
|
53
|
+
p.add_argument("--honesty", help="An honesty condition (what must be true).")
|
|
54
|
+
p.add_argument("--hard-question", dest="hard_question", help="A hard question.")
|
|
55
|
+
p.add_argument("--risk", help="A risk (recorded as a non-blocking hard question).")
|
|
56
|
+
p.add_argument("--contradicts", help="Claim id this contradicts (records a blocking question).")
|
|
57
|
+
p.add_argument("--blocking", action="store_true", help="Mark the hard question blocking.")
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--origin",
|
|
60
|
+
choices=("user", "llm"),
|
|
61
|
+
default="llm",
|
|
62
|
+
help="Who proposed the honesty condition.",
|
|
63
|
+
)
|
|
64
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
65
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
66
|
+
p.set_defaults(func=cmd_interrogate)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""``devague learn`` — teach the working-backwards method and the moves."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import __version__
|
|
8
|
+
from devague.cli._output import emit_result
|
|
9
|
+
|
|
10
|
+
MOVES = {
|
|
11
|
+
"new": "Start a frame from the announcement (pretend it shipped).",
|
|
12
|
+
"capture": "Record and classify a claim (audience, after_state, boundary, ...).",
|
|
13
|
+
"interrogate": "Pressure-test a claim: honesty conditions, hard questions, contradictions.",
|
|
14
|
+
"confirm": "Confirm a claim or honesty condition (user-only — no fabricated rigor).",
|
|
15
|
+
"reject": "Reject a claim or honesty condition.",
|
|
16
|
+
"park": "Move uncertainty into first-class open vagueness instead of forcing an answer.",
|
|
17
|
+
"converge": "Check whether the frame is solid enough to export a spec.",
|
|
18
|
+
"export": "Write the buildable spec — only once the frame converges.",
|
|
19
|
+
"show": "Render the Announcement Frame.",
|
|
20
|
+
"list": "List frames.",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
FIRST_QUESTION = "What's the announcement?"
|
|
24
|
+
SUPPORTING_PROMPT = (
|
|
25
|
+
"Pretend this shipped successfully. What would you announce to users, "
|
|
26
|
+
"teammates, or yourself?"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# The canonical guided sequence (devague#4). The engine is move-driven, not a
|
|
30
|
+
# rigid wizard — this is the recommended arc, with the move that advances each.
|
|
31
|
+
STAGES = [
|
|
32
|
+
("Announcement", "what are we saying shipped?", "new"),
|
|
33
|
+
("Audience", "who needs this?", "capture --kind audience"),
|
|
34
|
+
("After", "what changed for them?", "capture --kind after_state"),
|
|
35
|
+
("Matter", "why is it worth doing?", "capture --kind why_it_matters"),
|
|
36
|
+
("Before", "what pain made this necessary?", "capture --kind before_state"),
|
|
37
|
+
("Honest", "what must be true for the announcement to be honest?", "interrogate --honesty"),
|
|
38
|
+
("FAQ", "what hard questions remain?", "interrogate --hard-question"),
|
|
39
|
+
("Boundaries", "what are we not promising?", "capture --kind boundary"),
|
|
40
|
+
("Success", "how will we know?", "capture --kind success_signal"),
|
|
41
|
+
("Spec", "what should be built?", "converge -> export"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
_TEXT = (
|
|
45
|
+
"devague turns a vague idea into a buildable spec by working backwards.\n\n"
|
|
46
|
+
f"First question: {FIRST_QUESTION}\n"
|
|
47
|
+
f" {SUPPORTING_PROMPT}\n\n"
|
|
48
|
+
"Start from that announcement, then build an Announcement Frame by capturing\n"
|
|
49
|
+
"claims, interrogating them, parking what's still vague, and converging.\n"
|
|
50
|
+
"The arc emerges from the moves; it is not a fixed wizard. You (the agent)\n"
|
|
51
|
+
"choose the next move; devague tracks state. LLM-proposed claims and honesty\n"
|
|
52
|
+
"conditions stay 'proposed' until the user confirms them.\n\n"
|
|
53
|
+
"Guided stages (the recommended sequence — drive them with the moves):\n"
|
|
54
|
+
+ "\n".join(
|
|
55
|
+
f" {i:>2}. {name:<13} {prompt} [{move}]"
|
|
56
|
+
for i, (name, prompt, move) in enumerate(STAGES, 1)
|
|
57
|
+
)
|
|
58
|
+
+ "\n\nMoves:\n"
|
|
59
|
+
+ "\n".join(f" {name:<11} {desc}" for name, desc in MOVES.items())
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_learn(args: argparse.Namespace) -> int:
|
|
64
|
+
if getattr(args, "json", False):
|
|
65
|
+
emit_result(
|
|
66
|
+
{
|
|
67
|
+
"tool": "devague",
|
|
68
|
+
"version": __version__,
|
|
69
|
+
"first_question": FIRST_QUESTION,
|
|
70
|
+
"supporting_prompt": SUPPORTING_PROMPT,
|
|
71
|
+
"stages": [
|
|
72
|
+
{"step": i, "name": name, "prompt": prompt, "move": move}
|
|
73
|
+
for i, (name, prompt, move) in enumerate(STAGES, 1)
|
|
74
|
+
],
|
|
75
|
+
"moves": list(MOVES),
|
|
76
|
+
"summary": _TEXT,
|
|
77
|
+
},
|
|
78
|
+
json_mode=True,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
emit_result(_TEXT, json_mode=False)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
86
|
+
p = sub.add_parser("learn", help="Teach devague's working-backwards method.")
|
|
87
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
88
|
+
p.set_defaults(func=cmd_learn)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""``devague list`` — list frames and mark the current one."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._output import emit_result
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
12
|
+
slugs = store.list_slugs()
|
|
13
|
+
current = store.current_slug()
|
|
14
|
+
if getattr(args, "json", False):
|
|
15
|
+
emit_result({"frames": slugs, "current": current}, json_mode=True)
|
|
16
|
+
elif not slugs:
|
|
17
|
+
emit_result("no frames yet", json_mode=False)
|
|
18
|
+
else:
|
|
19
|
+
lines = [("* " if s == current else " ") + s for s in slugs]
|
|
20
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
25
|
+
p = sub.add_parser("list", help="List frames.")
|
|
26
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
27
|
+
p.set_defaults(func=cmd_list)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""``devague new`` — start a frame from an announcement (the first move)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._output import emit_result
|
|
9
|
+
from devague.frame import Frame
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cmd_new(args: argparse.Namespace) -> int:
|
|
13
|
+
title = args.title or args.announcement
|
|
14
|
+
frame = Frame(slug=store.unique_slug(store.slugify(title)), title=title)
|
|
15
|
+
frame.add_claim("announcement", args.announcement, origin="user")
|
|
16
|
+
store.save(frame)
|
|
17
|
+
if getattr(args, "json", False):
|
|
18
|
+
emit_result({"slug": frame.slug, "title": title, "claims": 1}, json_mode=True)
|
|
19
|
+
else:
|
|
20
|
+
emit_result(f"created frame '{frame.slug}' (announcement = c1)", json_mode=False)
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
25
|
+
p = sub.add_parser("new", help="Start a frame from an announcement.")
|
|
26
|
+
p.add_argument(
|
|
27
|
+
"announcement",
|
|
28
|
+
help="What's the announcement? Pretend this shipped successfully — what "
|
|
29
|
+
"would you announce to users, teammates, or yourself?",
|
|
30
|
+
)
|
|
31
|
+
p.add_argument("--title", help="Frame title (defaults to the announcement).")
|
|
32
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
33
|
+
p.set_defaults(func=cmd_new)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``devague park`` — move uncertainty into first-class open vagueness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import store
|
|
8
|
+
from devague.cli._frames import resolve
|
|
9
|
+
from devague.cli._output import emit_result
|
|
10
|
+
from devague.frame import VAGUENESS_KINDS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_park(args: argparse.Namespace) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
v = frame.add_vagueness(args.text, args.kind, claim_id=args.claim)
|
|
16
|
+
store.save(frame)
|
|
17
|
+
if getattr(args, "json", False):
|
|
18
|
+
emit_result({"id": v.id, "kind": v.kind}, json_mode=True)
|
|
19
|
+
else:
|
|
20
|
+
emit_result(f"parked {v.id} ({v.kind})", json_mode=False)
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
25
|
+
p = sub.add_parser("park", help="Record open vagueness instead of forcing an answer.")
|
|
26
|
+
p.add_argument("text", help="The uncertainty.")
|
|
27
|
+
p.add_argument("--kind", required=True, choices=VAGUENESS_KINDS, help="Vagueness kind.")
|
|
28
|
+
p.add_argument("--claim", help="Link to a claim id.")
|
|
29
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
30
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
31
|
+
p.set_defaults(func=cmd_park)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``devague reject`` — reject a claim or honesty condition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague.cli._commands.confirm import _transition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cmd_reject(args: argparse.Namespace) -> int:
|
|
11
|
+
return _transition(args, "rejected")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
15
|
+
p = sub.add_parser("reject", help="Reject a claim or honesty condition.")
|
|
16
|
+
p.add_argument("id", help="Claim id (c*) or honesty id (h*).")
|
|
17
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
18
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
19
|
+
p.set_defaults(func=cmd_reject)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""``devague show`` — render the current frame (markdown, or --json for raw state)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague import render
|
|
8
|
+
from devague.cli._frames import resolve
|
|
9
|
+
from devague.cli._output import emit_result
|
|
10
|
+
from devague.frame import to_dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cmd_show(args: argparse.Namespace) -> int:
|
|
14
|
+
frame = resolve(args.frame)
|
|
15
|
+
if getattr(args, "json", False):
|
|
16
|
+
emit_result(to_dict(frame), json_mode=True)
|
|
17
|
+
else:
|
|
18
|
+
emit_result(render.render(frame, args.format), json_mode=False)
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
23
|
+
p = sub.add_parser("show", help="Render the current frame.")
|
|
24
|
+
p.add_argument("--format", default="frame-md", help="Renderer format (default: frame-md).")
|
|
25
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
26
|
+
p.add_argument("--json", action="store_true", help="Emit the raw frame as JSON.")
|
|
27
|
+
p.set_defaults(func=cmd_show)
|