devague 0.4.1__tar.gz → 0.5.0__tar.gz
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-0.4.1 → devague-0.5.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +13 -41
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/think/SKILL.md +16 -12
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/think/scripts/think.sh +13 -47
- {devague-0.4.1 → devague-0.5.0}/CHANGELOG.md +14 -0
- {devague-0.4.1 → devague-0.5.0}/CLAUDE.md +9 -0
- {devague-0.4.1 → devague-0.5.0}/PKG-INFO +1 -1
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/capture.py +9 -1
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/converge.py +18 -7
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/export.py +2 -2
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/plan.py +20 -12
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_frames.py +16 -1
- devague-0.5.0/devague/convergence.py +149 -0
- {devague-0.4.1 → devague-0.5.0}/devague/frame.py +45 -1
- {devague-0.4.1 → devague-0.5.0}/devague/plan_convergence.py +47 -3
- {devague-0.4.1 → devague-0.5.0}/devague/store.py +11 -1
- devague-0.5.0/docs/examples/contract-example.json +160 -0
- devague-0.5.0/docs/spec-contract.md +180 -0
- {devague-0.4.1 → devague-0.5.0}/pyproject.toml +1 -1
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_converge_export.py +7 -7
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_moves.py +27 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_plan.py +2 -2
- devague-0.5.0/tests/test_contract.py +129 -0
- devague-0.5.0/tests/test_convergence.py +97 -0
- devague-0.5.0/tests/test_frame.py +111 -0
- devague-0.5.0/tests/test_offline.py +58 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_plan_convergence.py +14 -14
- {devague-0.4.1 → devague-0.5.0}/tests/test_store.py +43 -1
- {devague-0.4.1 → devague-0.5.0}/uv.lock +1 -1
- devague-0.4.1/devague/convergence.py +0 -72
- devague-0.4.1/tests/test_convergence.py +0 -62
- devague-0.4.1/tests/test_frame.py +0 -42
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/.claude/skills.local.yaml.example +0 -0
- {devague-0.4.1 → devague-0.5.0}/.devague/current_plan +0 -0
- {devague-0.4.1 → devague-0.5.0}/.devague/frames/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.4.1 → devague-0.5.0}/.devague/plans/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.4.1 → devague-0.5.0}/.flake8 +0 -0
- {devague-0.4.1 → devague-0.5.0}/.github/workflows/publish.yml +0 -0
- {devague-0.4.1 → devague-0.5.0}/.github/workflows/security-checks.yml +0 -0
- {devague-0.4.1 → devague-0.5.0}/.github/workflows/tests.yml +0 -0
- {devague-0.4.1 → devague-0.5.0}/.gitignore +0 -0
- {devague-0.4.1 → devague-0.5.0}/.markdownlint-cli2.yaml +0 -0
- {devague-0.4.1 → devague-0.5.0}/.pre-commit-config.yaml +0 -0
- {devague-0.4.1 → devague-0.5.0}/LICENSE +0 -0
- {devague-0.4.1 → devague-0.5.0}/README.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/culture.yaml +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/__init__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/__main__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/__init__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/__init__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/confirm.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/explain.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/interrogate.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/learn.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/list_frames.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/new.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/park.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/reject.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_commands/show.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_errors.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_output.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/cli/_plans.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/plan.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/plan_store.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/render/__init__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/render/frame_md.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/render/plan_md.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/devague/render/spec_md.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/plans/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/reviews/spec-contract-frame-review.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/skill-sources.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/specs/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
- {devague-0.4.1 → devague-0.5.0}/sonar-project.properties +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/__init__.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_affordances.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_chassis.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_errors.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_cli_output.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_package.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_plan.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_plan_store.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_render.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_render_plan.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_spec_to_plan_skill.py +0 -0
- {devague-0.4.1 → devague-0.5.0}/tests/test_think_skill.py +0 -0
|
@@ -108,7 +108,6 @@ cmd_status() {
|
|
|
108
108
|
python3 - <<'PY'
|
|
109
109
|
import json
|
|
110
110
|
import os
|
|
111
|
-
import re
|
|
112
111
|
import sys
|
|
113
112
|
|
|
114
113
|
|
|
@@ -154,52 +153,25 @@ if conv is None:
|
|
|
154
153
|
print("next move: devague plan show # inspect the plan")
|
|
155
154
|
sys.exit(0)
|
|
156
155
|
|
|
157
|
-
if conv.get("
|
|
156
|
+
if conv.get("ready_for_plan"):
|
|
158
157
|
print("convergence: PASSED ✓")
|
|
158
|
+
for w in conv.get("warnings") or []:
|
|
159
|
+
print(f" ⚠ {w}")
|
|
159
160
|
print("next move: devague plan export # write the buildable plan")
|
|
160
161
|
sys.exit(0)
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
print(f"convergence: NOT passed — {len(
|
|
164
|
-
for
|
|
165
|
-
print(f" - {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
m = re.search(r"coverage target (\w+) ", gap)
|
|
172
|
-
if m:
|
|
173
|
-
tid = m.group(1)
|
|
174
|
-
return (
|
|
175
|
-
f'cover {tid}: devague plan task "<summary>" --covers {tid} --accept "<...>"'
|
|
176
|
-
f" (or: devague plan cover <tN> --target {tid})"
|
|
177
|
-
)
|
|
178
|
-
m = re.search(r"task (t\d+) has no acceptance", gap)
|
|
179
|
-
if m:
|
|
180
|
-
return f'devague plan accept {m.group(1)} "<acceptance criterion>"'
|
|
181
|
-
m = re.search(r"task (t\d+) still proposed", gap)
|
|
182
|
-
if m:
|
|
183
|
-
tid = m.group(1)
|
|
184
|
-
return (
|
|
185
|
-
f"this is an LLM proposal — the USER decides:"
|
|
186
|
-
f" devague plan confirm {tid} (or: devague plan reject {tid})"
|
|
187
|
-
)
|
|
188
|
-
m = re.search(r"task (t\d+) depends on unknown task (t\d+)", gap)
|
|
189
|
-
if m:
|
|
190
|
-
return f"fix {m.group(1)}'s dependency on missing {m.group(2)} (add it, or drop the dep)"
|
|
191
|
-
if "dependency cycle" in gap:
|
|
192
|
-
return "break the dependency cycle: re-point one task's --dep so the graph is acyclic"
|
|
193
|
-
m = re.search(r"blocking risk (r\d+)", gap)
|
|
194
|
-
if m:
|
|
195
|
-
return f"resolve {m.group(1)}: cover it with a task, or re-record it as non-blocking"
|
|
196
|
-
return "devague plan show # inspect and decide"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if missing:
|
|
163
|
+
blockers = conv.get("blockers") or []
|
|
164
|
+
print(f"convergence: NOT passed — {len(blockers)} gap(s):")
|
|
165
|
+
for b in blockers:
|
|
166
|
+
print(f" - {b}")
|
|
167
|
+
for w in conv.get("warnings") or []:
|
|
168
|
+
print(f" ⚠ {w}")
|
|
169
|
+
|
|
170
|
+
moves = conv.get("required_next_moves") or []
|
|
171
|
+
if moves:
|
|
200
172
|
print()
|
|
201
173
|
print("recommended next move (first gap):")
|
|
202
|
-
print(f" {
|
|
174
|
+
print(f" {moves[0]}")
|
|
203
175
|
PY
|
|
204
176
|
}
|
|
205
177
|
|
|
@@ -66,27 +66,31 @@ portable resolution and the `status` helper.
|
|
|
66
66
|
| `learn` / `explain <move>` | Teach the method / explain one move. |
|
|
67
67
|
|
|
68
68
|
Claim kinds: `announcement`, `audience`, `after_state`, `before_state`,
|
|
69
|
-
`why_it_matters`, `boundary`, `success_signal`, `open_question
|
|
70
|
-
`
|
|
69
|
+
`why_it_matters`, `boundary`, `success_signal`, `open_question`, `non_goal`,
|
|
70
|
+
`requirement`, `assumption`, `decision`. Vagueness kinds: `unknown_nonblocking`,
|
|
71
|
+
`unknown_blocking`, `out_of_scope`, `follow_up`.
|
|
71
72
|
|
|
72
73
|
These are exactly the kinds the **shipped CLI enforces** (`CLAIM_KINDS` /
|
|
73
74
|
`VAGUENESS_KINDS` in `devague/frame.py`) — the skill documents the surface as
|
|
74
|
-
built, so every command here passes the CLI's `choices=` validation.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
built, so every command here passes the CLI's `choices=` validation. `requirement`
|
|
76
|
+
is spec-affecting (needs a confirmed honesty condition); `non_goal` / `decision`
|
|
77
|
+
are descriptive; an unconfirmed `assumption` is a convergence *warning*, not a
|
|
78
|
+
blocker. The formal entity model, the `(state × origin)` vocabulary, and the
|
|
79
|
+
per-move input/output/transition/error contract are documented in
|
|
80
|
+
[`docs/spec-contract.md`](../../../docs/spec-contract.md) (issue
|
|
81
|
+
[#5](https://github.com/agentculture/devague/issues/5)); for the authoritative
|
|
78
82
|
live shape of any move, run it with `--json` (or `devague learn --json` /
|
|
79
|
-
`devague explain <move>`).
|
|
83
|
+
`devague explain <move>`).
|
|
80
84
|
|
|
81
85
|
### `status` — the next-move helper
|
|
82
86
|
|
|
83
87
|
`status` is a wrapper-only verb (the CLI has no `status`). It reads
|
|
84
88
|
`converge --json` + `list --json` and prints where the current frame stands, the
|
|
85
|
-
remaining gaps, and the recommended next move
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
89
|
+
remaining gaps, and the recommended next move. `converge --json` emits the
|
|
90
|
+
structured result `{ready_for_spec, blockers, warnings, parked_items,
|
|
91
|
+
required_next_moves}` (issue [#5](https://github.com/agentculture/devague/issues/5));
|
|
92
|
+
the helper reads `ready_for_spec`, lists the `blockers` and `warnings`, and shows
|
|
93
|
+
`required_next_moves[0]` as the recommended move — no longer deriving it itself.
|
|
90
94
|
|
|
91
95
|
```text
|
|
92
96
|
frame: my-feature (1 frame total)
|
|
@@ -114,7 +114,6 @@ cmd_status() {
|
|
|
114
114
|
python3 - <<'PY'
|
|
115
115
|
import json
|
|
116
116
|
import os
|
|
117
|
-
import re
|
|
118
117
|
import sys
|
|
119
118
|
|
|
120
119
|
|
|
@@ -161,58 +160,25 @@ if conv is None:
|
|
|
161
160
|
print("next move: devague show # inspect the frame")
|
|
162
161
|
sys.exit(0)
|
|
163
162
|
|
|
164
|
-
if conv.get("
|
|
163
|
+
if conv.get("ready_for_spec"):
|
|
165
164
|
print("convergence: PASSED ✓")
|
|
165
|
+
for w in conv.get("warnings") or []:
|
|
166
|
+
print(f" ⚠ {w}")
|
|
166
167
|
print("next move: devague export # write the buildable spec")
|
|
167
168
|
sys.exit(0)
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
print(f"convergence: NOT passed — {len(
|
|
171
|
-
for
|
|
172
|
-
print(f" - {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# work. Spell out who confirms wherever a confirm is in play.
|
|
179
|
-
m = re.search(r"missing confirmed '([a-z_]+)' claim", gap)
|
|
180
|
-
if m:
|
|
181
|
-
kind = m.group(1)
|
|
182
|
-
return (f'devague capture --kind {kind} "<text>"'
|
|
183
|
-
f' (a user capture auto-confirms; an --origin llm capture'
|
|
184
|
-
f' then needs the USER to confirm it)')
|
|
185
|
-
if "before_state" in gap and "why_it_matters" in gap:
|
|
186
|
-
return 'devague capture --kind why_it_matters "<text>"'
|
|
187
|
-
if "boundary" in gap:
|
|
188
|
-
return 'devague capture --kind boundary "<text>"'
|
|
189
|
-
if "success_signal" in gap:
|
|
190
|
-
return 'devague capture --kind success_signal "<text>"'
|
|
191
|
-
m = re.search(r"claim (c\d+) still proposed", gap)
|
|
192
|
-
if m:
|
|
193
|
-
cid = m.group(1)
|
|
194
|
-
return (f'this is an LLM proposal — the USER decides:'
|
|
195
|
-
f' devague confirm {cid} (or: devague reject {cid})')
|
|
196
|
-
m = re.search(r"claim (c\d+) has no confirmed honesty condition", gap)
|
|
197
|
-
if m:
|
|
198
|
-
cid = m.group(1)
|
|
199
|
-
return (f'devague interrogate {cid} --honesty "<what must be true>"'
|
|
200
|
-
f' then the USER runs: devague confirm <hN>')
|
|
201
|
-
m = re.search(r"blocking vagueness (v\d+)", gap)
|
|
202
|
-
if m:
|
|
203
|
-
return (f"resolve {m.group(1)}: capture+confirm the answer, "
|
|
204
|
-
f"or re-park it as non-blocking")
|
|
205
|
-
m = re.search(r"blocking hard question (q\d+) on (c\d+)", gap)
|
|
206
|
-
if m:
|
|
207
|
-
return (f"resolve {m.group(1)} on {m.group(2)}: answer it, then "
|
|
208
|
-
f"capture/confirm the resulting claim")
|
|
209
|
-
return "devague show # inspect and decide"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if missing:
|
|
170
|
+
blockers = conv.get("blockers") or []
|
|
171
|
+
print(f"convergence: NOT passed — {len(blockers)} gap(s):")
|
|
172
|
+
for b in blockers:
|
|
173
|
+
print(f" - {b}")
|
|
174
|
+
for w in conv.get("warnings") or []:
|
|
175
|
+
print(f" ⚠ {w}")
|
|
176
|
+
|
|
177
|
+
moves = conv.get("required_next_moves") or []
|
|
178
|
+
if moves:
|
|
213
179
|
print()
|
|
214
180
|
print("recommended next move (first gap):")
|
|
215
|
-
print(f" {
|
|
181
|
+
print(f" {moves[0]}")
|
|
216
182
|
PY
|
|
217
183
|
}
|
|
218
184
|
|
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Spec contract (#5): claim kinds non_goal / requirement / assumption / decision, each with a documented convergence-gate role (requirement is spec-affecting; non_goal/decision are descriptive; an unconfirmed assumption is a warning, not a blocker).
|
|
13
|
+
- Every frame carries a fail-closed schema_version: written on save, validated on load (a newer/unknown version is rejected with an actionable error); existing 0.4.0 frames still load.
|
|
14
|
+
- docs/spec-contract.md — the documented source of truth for the entity model, the (state x origin) vocabulary, the structured convergence result, and the per-move input/output/transition/validation-error contract — plus a test-verified worked example at docs/examples/contract-example.json.
|
|
15
|
+
- Contract test suite: claim provenance, honesty-condition confirmation, parking vagueness, structured convergence failure, lossless round-trip, schema versioning, and an offline-operation guarantee (no networking imports; a full session runs with sockets stubbed).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- BREAKING: converge --json now emits the structured result {ready_for_spec, blockers, warnings, parked_items, required_next_moves} (plans: ready_for_plan) instead of {passed, missing}. The /think and /spec-to-plan status helpers were updated in the same change; required_next_moves is now derived by the CLI. capture --json now includes origin.
|
|
20
|
+
- Frame loading raises distinct, actionable DevagueErrors (newer schema -> upgrade; malformed/hand-edited frame -> fix hint) instead of a generic 'invalid slug'.
|
|
21
|
+
|
|
8
22
|
## [0.4.1] - 2026-05-23
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -4,6 +4,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
+
**Spec contract landed (#5).** The entity model is documented in
|
|
8
|
+
`docs/spec-contract.md` (the source of truth for kinds, the `(state × origin)`
|
|
9
|
+
vocabulary, the structured convergence result, and the per-move I/O contract).
|
|
10
|
+
Claim kinds now include `non_goal` / `requirement` / `assumption` / `decision`;
|
|
11
|
+
every frame carries a fail-closed `schema_version`; and `converge --json` emits
|
|
12
|
+
the structured `{ready_for_spec, blockers, warnings, parked_items,
|
|
13
|
+
required_next_moves}` (plans: `ready_for_plan`) — a hard break from the old
|
|
14
|
+
`{passed, missing}`.
|
|
15
|
+
|
|
7
16
|
**Spec→plan engine landed (v0.4.0).** Both deterministic engines now ship.
|
|
8
17
|
The **frame engine** (idea→spec) — Frame domain model, JSON store, convergence
|
|
9
18
|
gate, renderer registry, and the flat moves `new` / `capture` / `interrogate` /
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devague
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: devague — turns a vague feature idea into a buildable spec, then a buildable plan.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/devague
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/devague/issues
|
|
@@ -15,7 +15,15 @@ def cmd_capture(args: argparse.Namespace) -> int:
|
|
|
15
15
|
claim = frame.add_claim(args.kind, args.text, origin=args.origin)
|
|
16
16
|
store.save(frame)
|
|
17
17
|
if getattr(args, "json", False):
|
|
18
|
-
emit_result(
|
|
18
|
+
emit_result(
|
|
19
|
+
{
|
|
20
|
+
"id": claim.id,
|
|
21
|
+
"kind": claim.kind,
|
|
22
|
+
"origin": claim.origin,
|
|
23
|
+
"status": claim.status,
|
|
24
|
+
},
|
|
25
|
+
json_mode=True,
|
|
26
|
+
)
|
|
19
27
|
else:
|
|
20
28
|
emit_result(f"captured {claim.id} ({claim.kind}, {claim.status})", json_mode=False)
|
|
21
29
|
return 0
|
|
@@ -13,20 +13,31 @@ from devague.convergence import evaluate
|
|
|
13
13
|
def cmd_converge(args: argparse.Namespace) -> int:
|
|
14
14
|
frame = resolve(args.frame)
|
|
15
15
|
result = evaluate(frame)
|
|
16
|
-
if result.
|
|
16
|
+
if result.ready and frame.status == "drafting":
|
|
17
17
|
frame.status = "converged"
|
|
18
18
|
store.save(frame)
|
|
19
|
-
elif not result.
|
|
19
|
+
elif not result.ready and frame.status == "converged":
|
|
20
20
|
frame.status = "drafting"
|
|
21
21
|
store.save(frame)
|
|
22
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
23
|
emit_result(
|
|
28
|
-
|
|
24
|
+
{
|
|
25
|
+
"ready_for_spec": result.ready,
|
|
26
|
+
"blockers": result.blockers,
|
|
27
|
+
"warnings": result.warnings,
|
|
28
|
+
"parked_items": result.parked_items,
|
|
29
|
+
"required_next_moves": result.required_next_moves,
|
|
30
|
+
},
|
|
31
|
+
json_mode=True,
|
|
29
32
|
)
|
|
33
|
+
elif result.ready:
|
|
34
|
+
msg = "converged ✓"
|
|
35
|
+
if result.warnings:
|
|
36
|
+
msg += "\nwarnings:\n" + "\n".join(f" - {w}" for w in result.warnings)
|
|
37
|
+
emit_result(msg, json_mode=False)
|
|
38
|
+
else:
|
|
39
|
+
lines = "\n".join(f" - {b}" for b in result.blockers)
|
|
40
|
+
emit_result("not converged:\n" + lines, json_mode=False)
|
|
30
41
|
return 0
|
|
31
42
|
|
|
32
43
|
|
|
@@ -17,11 +17,11 @@ SPECS_DIR = Path("docs/specs")
|
|
|
17
17
|
def cmd_export(args: argparse.Namespace) -> int:
|
|
18
18
|
frame = resolve(args.frame)
|
|
19
19
|
result = evaluate(frame)
|
|
20
|
-
if not result.
|
|
20
|
+
if not result.ready:
|
|
21
21
|
raise DevagueError(
|
|
22
22
|
EXIT_USER_ERROR,
|
|
23
23
|
"frame has not converged; cannot export",
|
|
24
|
-
"resolve: " + "; ".join(result.
|
|
24
|
+
"resolve: " + "; ".join(result.blockers),
|
|
25
25
|
)
|
|
26
26
|
text = render.render(frame, args.format)
|
|
27
27
|
SPECS_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -69,7 +69,7 @@ def _live(plan: Plan):
|
|
|
69
69
|
"""Re-load the source frame and re-derive targets; guard against frame drift."""
|
|
70
70
|
frame = _load_source_frame(plan.frame_slug)
|
|
71
71
|
fres = evaluate_frame(frame)
|
|
72
|
-
if not fres.
|
|
72
|
+
if not fres.ready:
|
|
73
73
|
raise DevagueError(
|
|
74
74
|
EXIT_USER_ERROR,
|
|
75
75
|
f"source frame '{frame.slug}' has regressed below convergence",
|
|
@@ -98,11 +98,11 @@ def _require_target(plan: Plan, target_id: str) -> None:
|
|
|
98
98
|
def cmd_plan_new(args: argparse.Namespace) -> int:
|
|
99
99
|
frame = resolve_frame(args.frame)
|
|
100
100
|
result = evaluate_frame(frame)
|
|
101
|
-
if not result.
|
|
101
|
+
if not result.ready:
|
|
102
102
|
raise DevagueError(
|
|
103
103
|
EXIT_USER_ERROR,
|
|
104
104
|
f"frame '{frame.slug}' has not converged; cannot start a plan",
|
|
105
|
-
"resolve: " + "; ".join(result.
|
|
105
|
+
"resolve: " + "; ".join(result.blockers),
|
|
106
106
|
)
|
|
107
107
|
if plan_store.path_for(frame.slug).exists():
|
|
108
108
|
raise DevagueError(
|
|
@@ -234,19 +234,27 @@ def cmd_plan_converge(args: argparse.Namespace) -> int:
|
|
|
234
234
|
_frame, targets = _live(plan)
|
|
235
235
|
plan.targets = targets # refresh the snapshot from the live frame
|
|
236
236
|
result = evaluate_plan(plan, targets=targets)
|
|
237
|
-
if result.
|
|
237
|
+
if result.ready and plan.status == "drafting":
|
|
238
238
|
plan.status = "converged"
|
|
239
|
-
elif not result.
|
|
239
|
+
elif not result.ready and plan.status == "converged":
|
|
240
240
|
plan.status = "drafting"
|
|
241
241
|
plan_store.save(plan)
|
|
242
242
|
if getattr(args, "json", False):
|
|
243
|
-
emit_result({"passed": result.passed, "missing": result.missing}, json_mode=True)
|
|
244
|
-
elif result.passed:
|
|
245
|
-
emit_result("converged ✓", json_mode=False)
|
|
246
|
-
else:
|
|
247
243
|
emit_result(
|
|
248
|
-
|
|
244
|
+
{
|
|
245
|
+
"ready_for_plan": result.ready,
|
|
246
|
+
"blockers": result.blockers,
|
|
247
|
+
"warnings": result.warnings,
|
|
248
|
+
"parked_items": result.parked_items,
|
|
249
|
+
"required_next_moves": result.required_next_moves,
|
|
250
|
+
},
|
|
251
|
+
json_mode=True,
|
|
249
252
|
)
|
|
253
|
+
elif result.ready:
|
|
254
|
+
emit_result("converged ✓", json_mode=False)
|
|
255
|
+
else:
|
|
256
|
+
lines = "\n".join(f" - {b}" for b in result.blockers)
|
|
257
|
+
emit_result("not converged:\n" + lines, json_mode=False)
|
|
250
258
|
return 0
|
|
251
259
|
|
|
252
260
|
|
|
@@ -255,11 +263,11 @@ def cmd_plan_export(args: argparse.Namespace) -> int:
|
|
|
255
263
|
frame, targets = _live(plan)
|
|
256
264
|
plan.targets = targets
|
|
257
265
|
result = evaluate_plan(plan, targets=targets)
|
|
258
|
-
if not result.
|
|
266
|
+
if not result.ready:
|
|
259
267
|
raise DevagueError(
|
|
260
268
|
EXIT_USER_ERROR,
|
|
261
269
|
"plan has not converged; cannot export",
|
|
262
|
-
"resolve: " + "; ".join(result.
|
|
270
|
+
"resolve: " + "; ".join(result.blockers),
|
|
263
271
|
)
|
|
264
272
|
plan.status = "exported"
|
|
265
273
|
text = plan_md.render_plan(plan, frame)
|
|
@@ -16,14 +16,29 @@ def resolve(slug: str | None) -> Frame:
|
|
|
16
16
|
"run 'devague new \"<announcement>\"' or pass --frame <slug>",
|
|
17
17
|
)
|
|
18
18
|
try:
|
|
19
|
-
|
|
19
|
+
store.validate_slug(slug)
|
|
20
20
|
except ValueError as exc:
|
|
21
21
|
raise DevagueError(
|
|
22
22
|
EXIT_USER_ERROR,
|
|
23
23
|
f"invalid frame slug: {slug!r}",
|
|
24
24
|
"slugs are lowercase letters, digits, and hyphens — no path separators",
|
|
25
25
|
) from exc
|
|
26
|
+
try:
|
|
27
|
+
return store.load(slug)
|
|
28
|
+
except store.IncompatibleSchemaError as exc:
|
|
29
|
+
raise DevagueError(
|
|
30
|
+
EXIT_USER_ERROR,
|
|
31
|
+
str(exc),
|
|
32
|
+
"this frame was written by a newer devague — upgrade: 'uv tool install -U devague'",
|
|
33
|
+
) from exc
|
|
26
34
|
except FileNotFoundError:
|
|
27
35
|
raise DevagueError(
|
|
28
36
|
EXIT_USER_ERROR, f"no such frame: {slug}", "run 'devague list' to see frames"
|
|
29
37
|
) from None
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise DevagueError(
|
|
40
|
+
EXIT_USER_ERROR,
|
|
41
|
+
f"frame {slug!r} is malformed: {exc}",
|
|
42
|
+
"the frame file was hand-edited or corrupted — "
|
|
43
|
+
"fix .devague/frames/<slug>.json or recreate the frame",
|
|
44
|
+
) from exc
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""The convergence gate: is a frame solid enough to export a buildable spec?"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from devague.frame import SPEC_AFFECTING_KINDS, Frame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ConvergenceResult:
|
|
13
|
+
"""Structured convergence verdict, shared by the frame and plan engines.
|
|
14
|
+
|
|
15
|
+
``ready`` is the gate (no blockers). The CLI serializes it under an
|
|
16
|
+
engine-specific key (``ready_for_spec`` for frames, ``ready_for_plan`` for
|
|
17
|
+
plans). ``blockers`` hold convergence back; ``warnings`` do not;
|
|
18
|
+
``parked_items`` are tracked-but-non-blocking unknowns; ``required_next_moves``
|
|
19
|
+
are derived from the blockers so an operator knows what to do next.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
ready: bool
|
|
23
|
+
blockers: list[str] = field(default_factory=list)
|
|
24
|
+
warnings: list[str] = field(default_factory=list)
|
|
25
|
+
parked_items: list[str] = field(default_factory=list)
|
|
26
|
+
required_next_moves: list[str] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _missing_required_kinds(confirmed_kinds: set[str]) -> list[str]:
|
|
30
|
+
"""Required confirmed claims for an honest announcement frame."""
|
|
31
|
+
missing = [
|
|
32
|
+
f"missing confirmed '{required}' claim"
|
|
33
|
+
for required in ("announcement", "audience", "after_state")
|
|
34
|
+
if required not in confirmed_kinds
|
|
35
|
+
]
|
|
36
|
+
if "before_state" not in confirmed_kinds and "why_it_matters" not in confirmed_kinds:
|
|
37
|
+
missing.append("missing 'before_state' or 'why_it_matters' claim")
|
|
38
|
+
if "boundary" not in confirmed_kinds:
|
|
39
|
+
missing.append("missing a 'boundary' / non-goal claim")
|
|
40
|
+
if "success_signal" not in confirmed_kinds:
|
|
41
|
+
missing.append("missing a 'success_signal' claim")
|
|
42
|
+
return missing
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _missing_claim_resolution(frame: Frame, confirmed: list) -> list[str]:
|
|
46
|
+
"""No spec-affecting claim left proposed; each confirmed one is pressure-tested."""
|
|
47
|
+
missing = [
|
|
48
|
+
f"claim {c.id} still proposed (confirm or reject it)"
|
|
49
|
+
for c in frame.claims
|
|
50
|
+
if c.kind in SPEC_AFFECTING_KINDS and c.status == "proposed"
|
|
51
|
+
]
|
|
52
|
+
missing += [
|
|
53
|
+
f"claim {c.id} has no confirmed honesty condition"
|
|
54
|
+
for c in confirmed
|
|
55
|
+
if c.kind in SPEC_AFFECTING_KINDS
|
|
56
|
+
and not any(h.status == "confirmed" for h in c.honesty_conditions)
|
|
57
|
+
]
|
|
58
|
+
return missing
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _missing_open_uncertainty(frame: Frame) -> list[str]:
|
|
62
|
+
"""No blocking vagueness or unresolved blocking hard question remains."""
|
|
63
|
+
missing = [
|
|
64
|
+
f"blocking vagueness {v.id} unresolved"
|
|
65
|
+
for v in frame.open_vagueness
|
|
66
|
+
if v.kind == "unknown_blocking"
|
|
67
|
+
]
|
|
68
|
+
missing += [
|
|
69
|
+
f"blocking hard question {q.id} on {c.id} unresolved"
|
|
70
|
+
for c in frame.claims
|
|
71
|
+
for q in c.hard_questions
|
|
72
|
+
if q.blocking and not q.resolved
|
|
73
|
+
]
|
|
74
|
+
return missing
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _assumption_warnings(frame: Frame) -> list[str]:
|
|
78
|
+
"""Unconfirmed assumptions are soft: a warning, never a blocker (#5, h14)."""
|
|
79
|
+
return [
|
|
80
|
+
f"assumption {c.id} is unconfirmed — confirm it or it ships as a stated assumption"
|
|
81
|
+
for c in frame.claims
|
|
82
|
+
if c.kind == "assumption" and c.status != "confirmed"
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parked_items(frame: Frame) -> list[str]:
|
|
87
|
+
"""Tracked, non-blocking open vagueness (everything but unknown_blocking)."""
|
|
88
|
+
return [f"[{v.kind}] {v.text}" for v in frame.open_vagueness if v.kind != "unknown_blocking"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def suggest_move(blocker: str) -> str:
|
|
92
|
+
"""Map a single blocker to the recommended next devague move.
|
|
93
|
+
|
|
94
|
+
Confirmation is a USER-only transition, so any confirm-related move spells
|
|
95
|
+
out who confirms — the agent must never imply it should confirm its own work.
|
|
96
|
+
"""
|
|
97
|
+
m = re.search(r"missing confirmed '([a-z_]+)' claim", blocker)
|
|
98
|
+
if m:
|
|
99
|
+
kind = m.group(1)
|
|
100
|
+
return (
|
|
101
|
+
f'devague capture --kind {kind} "<text>" (a user capture '
|
|
102
|
+
f"auto-confirms; an --origin llm capture then needs the USER to confirm it)"
|
|
103
|
+
)
|
|
104
|
+
if "before_state" in blocker and "why_it_matters" in blocker:
|
|
105
|
+
return 'devague capture --kind why_it_matters "<text>"'
|
|
106
|
+
if "boundary" in blocker:
|
|
107
|
+
return 'devague capture --kind boundary "<text>"'
|
|
108
|
+
if "success_signal" in blocker:
|
|
109
|
+
return 'devague capture --kind success_signal "<text>"'
|
|
110
|
+
m = re.search(r"claim (c\d+) still proposed", blocker)
|
|
111
|
+
if m:
|
|
112
|
+
cid = m.group(1)
|
|
113
|
+
return (
|
|
114
|
+
f"this is an LLM proposal — the USER decides: devague confirm {cid} (or reject {cid})"
|
|
115
|
+
)
|
|
116
|
+
m = re.search(r"claim (c\d+) has no confirmed honesty condition", blocker)
|
|
117
|
+
if m:
|
|
118
|
+
cid = m.group(1)
|
|
119
|
+
return (
|
|
120
|
+
f'devague interrogate {cid} --honesty "<what must be true>"'
|
|
121
|
+
f" then USER: devague confirm <hN>"
|
|
122
|
+
)
|
|
123
|
+
m = re.search(r"blocking vagueness (v\d+)", blocker)
|
|
124
|
+
if m:
|
|
125
|
+
return f"resolve {m.group(1)}: capture+confirm the answer, or re-park it as non-blocking"
|
|
126
|
+
m = re.search(r"blocking hard question (q\d+) on (c\d+)", blocker)
|
|
127
|
+
if m:
|
|
128
|
+
return (
|
|
129
|
+
f"resolve {m.group(1)} on {m.group(2)}: answer it, then "
|
|
130
|
+
f"capture/confirm the resulting claim"
|
|
131
|
+
)
|
|
132
|
+
return "devague show # inspect and decide"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def evaluate(frame: Frame) -> ConvergenceResult:
|
|
136
|
+
confirmed = [c for c in frame.claims if c.status == "confirmed"]
|
|
137
|
+
confirmed_kinds = {c.kind for c in confirmed}
|
|
138
|
+
blockers = (
|
|
139
|
+
_missing_required_kinds(confirmed_kinds)
|
|
140
|
+
+ _missing_claim_resolution(frame, confirmed)
|
|
141
|
+
+ _missing_open_uncertainty(frame)
|
|
142
|
+
)
|
|
143
|
+
return ConvergenceResult(
|
|
144
|
+
ready=not blockers,
|
|
145
|
+
blockers=blockers,
|
|
146
|
+
warnings=_assumption_warnings(frame),
|
|
147
|
+
parked_items=_parked_items(frame),
|
|
148
|
+
required_next_moves=[suggest_move(b) for b in blockers],
|
|
149
|
+
)
|