devague 0.3.2__tar.gz → 0.3.3__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.3.3/.claude/skills/devague/SKILL.md +164 -0
- devague-0.3.3/.claude/skills/devague/scripts/devague.sh +234 -0
- {devague-0.3.2 → devague-0.3.3}/.flake8 +1 -1
- {devague-0.3.2 → devague-0.3.3}/CHANGELOG.md +6 -0
- {devague-0.3.2 → devague-0.3.3}/PKG-INFO +1 -1
- {devague-0.3.2 → devague-0.3.3}/docs/skill-sources.md +11 -0
- {devague-0.3.2 → devague-0.3.3}/pyproject.toml +1 -1
- devague-0.3.3/tests/test_devague_skill.py +143 -0
- {devague-0.3.2 → devague-0.3.3}/uv.lock +1 -1
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/run-tests/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/version-bump/SKILL.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/.claude/skills.local.yaml.example +0 -0
- {devague-0.3.2 → devague-0.3.3}/.github/workflows/publish.yml +0 -0
- {devague-0.3.2 → devague-0.3.3}/.github/workflows/security-checks.yml +0 -0
- {devague-0.3.2 → devague-0.3.3}/.github/workflows/tests.yml +0 -0
- {devague-0.3.2 → devague-0.3.3}/.gitignore +0 -0
- {devague-0.3.2 → devague-0.3.3}/.markdownlint-cli2.yaml +0 -0
- {devague-0.3.2 → devague-0.3.3}/.pre-commit-config.yaml +0 -0
- {devague-0.3.2 → devague-0.3.3}/CLAUDE.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/LICENSE +0 -0
- {devague-0.3.2 → devague-0.3.3}/README.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/culture.yaml +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/__init__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/__main__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/__init__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/__init__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/capture.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/confirm.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/converge.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/explain.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/export.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/interrogate.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/learn.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/list_frames.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/new.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/park.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/reject.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/show.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_errors.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_frames.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/cli/_output.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/convergence.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/frame.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/render/__init__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/render/frame_md.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/render/spec_md.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/devague/store.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
- {devague-0.3.2 → devague-0.3.3}/sonar-project.properties +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/__init__.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_affordances.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_chassis.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_converge_export.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_errors.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_moves.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_cli_output.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_convergence.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_frame.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_package.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_render.py +0 -0
- {devague-0.3.2 → devague-0.3.3}/tests/test_store.py +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: devague
|
|
3
|
+
description: >
|
|
4
|
+
Operate the devague working-backwards spec tool: turn a vague feature idea
|
|
5
|
+
into a buildable, pressure-tested spec by starting from the announcement
|
|
6
|
+
("pretend it shipped"), capturing and classifying claims, interrogating them
|
|
7
|
+
with honesty conditions and hard questions, parking open vagueness as a
|
|
8
|
+
first-class object, and exporting a spec only once the frame *converges*. Use
|
|
9
|
+
when the user says "spec this", "work backwards", "turn this idea into a
|
|
10
|
+
spec", "announcement frame", or "devague", or when a feature request is too
|
|
11
|
+
vague to build yet. Authored and maintained in agentculture/devague (origin =
|
|
12
|
+
devague); steward pulls this skill from here and broadcasts it to the
|
|
13
|
+
AgentCulture mesh — it is NOT vendored from steward like the other skills here.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# devague — work an idea backwards into a buildable spec
|
|
17
|
+
|
|
18
|
+
`devague` turns a vague feature idea into a buildable spec by **working
|
|
19
|
+
backwards**: you start from the announcement you'd make if it had already
|
|
20
|
+
shipped, then build an **Announcement Frame** by capturing claims, pressure
|
|
21
|
+
-testing them, parking what's still genuinely unknown, and only exporting once
|
|
22
|
+
the frame converges.
|
|
23
|
+
|
|
24
|
+
The CLI is **deterministic and move-driven** — it is *not* a wizard. There is no
|
|
25
|
+
fixed sequence of prompts. **You (the agent) choose the next move; the CLI just
|
|
26
|
+
tracks state and tells you what's still missing.** Run `devague learn` for the
|
|
27
|
+
canonical ten-stage arc and `devague explain <move>` for any single move.
|
|
28
|
+
|
|
29
|
+
This skill is the operator: a portable wrapper plus one helper (`status`) that
|
|
30
|
+
reads the convergence gate and tells you the recommended next move.
|
|
31
|
+
|
|
32
|
+
## How to run
|
|
33
|
+
|
|
34
|
+
The entry point is `scripts/devague.sh`. Invoke it from the repository you are
|
|
35
|
+
speccing (frames persist under `.devague/` in the current directory):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bash .claude/skills/devague/scripts/devague.sh <move> [args...]
|
|
39
|
+
bash .claude/skills/devague/scripts/devague.sh status
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It resolves the CLI portably — an installed `devague` on `PATH` (the normal
|
|
43
|
+
case), falling back to `uv run devague` when you are inside the devague checkout.
|
|
44
|
+
If neither resolves it prints an install hint (`uv tool install devague`). Every
|
|
45
|
+
move except `status` is forwarded verbatim, so you can equally call the CLI
|
|
46
|
+
directly (`devague <move> …`) when it is installed; the wrapper exists for
|
|
47
|
+
portable resolution and the `status` helper.
|
|
48
|
+
|
|
49
|
+
### Moves
|
|
50
|
+
|
|
51
|
+
| Move | What it does |
|
|
52
|
+
|------|--------------|
|
|
53
|
+
| `new "<announcement>"` | Start a frame from the announcement (the first move). Seeds an auto-confirmed `announcement` claim. |
|
|
54
|
+
| `capture --kind <kind> "<text>"` | Record + classify a claim. `--origin llm` lands it as `proposed`. |
|
|
55
|
+
| `interrogate <id> --honesty "…"` | Attach an honesty condition (what must be true). Also `--hard-question`, `--risk`, `--contradicts`, `--blocking`. |
|
|
56
|
+
| `confirm <id>` / `reject <id>` | Resolve a claim (`c*`) or honesty condition (`h*`). **User-only decision.** |
|
|
57
|
+
| `park "<text>" --kind <kind>` | Move uncertainty into first-class open vagueness instead of forcing an answer. |
|
|
58
|
+
| `converge` | Evaluate the gate; list remaining gaps. |
|
|
59
|
+
| `export` | Write the buildable spec to `docs/specs/` — only after `converge` passes. |
|
|
60
|
+
| `show` / `list` | Render a frame / list frames (`--json` for raw state). |
|
|
61
|
+
| `learn` / `explain <move>` | Teach the method / explain one move. |
|
|
62
|
+
|
|
63
|
+
Claim kinds: `announcement`, `audience`, `after_state`, `before_state`,
|
|
64
|
+
`why_it_matters`, `boundary`, `success_signal`, `open_question`. Vagueness kinds:
|
|
65
|
+
`unknown_nonblocking`, `unknown_blocking`, `out_of_scope`, `follow_up`.
|
|
66
|
+
|
|
67
|
+
These are exactly the kinds the **shipped CLI enforces** (`CLAIM_KINDS` /
|
|
68
|
+
`VAGUENESS_KINDS` in `devague/frame.py`) — the skill documents the surface as
|
|
69
|
+
built, so every command here passes the CLI's `choices=` validation. A fuller
|
|
70
|
+
proposed type/state set, plus the formal per-move input/output/transition
|
|
71
|
+
contract, is tracked on the CLI side in
|
|
72
|
+
[#5](https://github.com/agentculture/devague/issues/5); for the authoritative
|
|
73
|
+
live shape of any move, run it with `--json` (or `devague learn --json` /
|
|
74
|
+
`devague explain <move>`). When the CLI's contract grows, re-sync this list.
|
|
75
|
+
|
|
76
|
+
### `status` — the next-move helper
|
|
77
|
+
|
|
78
|
+
`status` is a wrapper-only verb (the CLI has no `status`). It reads
|
|
79
|
+
`converge --json` + `list --json` and prints where the current frame stands, the
|
|
80
|
+
remaining gaps, and the recommended next move derived from the first gap.
|
|
81
|
+
`converge --json` currently emits `{passed, missing}`, which is what the helper
|
|
82
|
+
consumes; if [#5](https://github.com/agentculture/devague/issues/5) enriches that
|
|
83
|
+
payload (e.g. structured `blockers` / `warnings` / `required_next_moves`),
|
|
84
|
+
`status` will surface the richer fields then.
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
frame: my-feature (1 frame total)
|
|
88
|
+
convergence: NOT passed — 2 gap(s):
|
|
89
|
+
- missing a 'boundary' / non-goal claim
|
|
90
|
+
- claim c2 has no confirmed honesty condition
|
|
91
|
+
|
|
92
|
+
recommended next move (first gap):
|
|
93
|
+
devague capture --kind boundary "<text>"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run it whenever you're unsure what to do next.
|
|
97
|
+
|
|
98
|
+
## Hard rules (do not violate)
|
|
99
|
+
|
|
100
|
+
These are the point of the method — convergence must mean something.
|
|
101
|
+
|
|
102
|
+
- **LLM proposals stay proposed.** A claim captured with `--origin llm`, and any
|
|
103
|
+
honesty condition you (the agent) propose, lands as `proposed`. **Never
|
|
104
|
+
`confirm` your own proposal.** Confirmation is a user-only decision — surface
|
|
105
|
+
the proposal and let the user confirm or reject it. Proposed content must not
|
|
106
|
+
silently become an authoritative requirement.
|
|
107
|
+
- **Honesty conditions route through the user.** Propose them freely with
|
|
108
|
+
`interrogate --honesty`; the user owns whether they hold.
|
|
109
|
+
- **Converge, don't vibe.** `export` is gated on `converge` passing. Never claim
|
|
110
|
+
the frame is ready on a hunch — run `converge` (or `status`) and resolve every
|
|
111
|
+
listed gap. The gate requires confirmed `announcement` / `audience` /
|
|
112
|
+
`after_state`, a `before_state` or `why_it_matters`, a `boundary`, a
|
|
113
|
+
`success_signal`, a confirmed honesty condition on every spec-affecting claim,
|
|
114
|
+
and no unresolved blocking vagueness or hard question.
|
|
115
|
+
- **Park real unknowns; don't paper over them.** If something is genuinely
|
|
116
|
+
unknown, `park` it (blocking or non-blocking) rather than fabricating an
|
|
117
|
+
answer. Blocking vagueness holds back convergence — by design.
|
|
118
|
+
|
|
119
|
+
## Output contract
|
|
120
|
+
|
|
121
|
+
Results go to **stdout**, diagnostics and errors to **stderr** — a strict split
|
|
122
|
+
you can rely on when parsing. Pass `--json` to any move for a structured payload
|
|
123
|
+
on the same stream. Exit code `0` on success, non-zero on user error (with a
|
|
124
|
+
`hint:` line). Frames live under `.devague/` in the current directory.
|
|
125
|
+
|
|
126
|
+
## Worked example
|
|
127
|
+
|
|
128
|
+
A short end-to-end session (the kind you'd run to spec a feature like
|
|
129
|
+
[devague#5](https://github.com/agentculture/devague/issues/5)):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
d() { bash .claude/skills/devague/scripts/devague.sh "$@"; }
|
|
133
|
+
|
|
134
|
+
d new "Devague ships a documented spec contract"
|
|
135
|
+
d capture --kind audience "devague + the assisting LLM"
|
|
136
|
+
d capture --kind after_state "a vague idea becomes a buildable, pressure-tested spec"
|
|
137
|
+
d capture --kind why_it_matters "specs converge on evidence, not vibes"
|
|
138
|
+
d capture --kind boundary "not a full PRD generator; no fixed wizard"
|
|
139
|
+
d capture --kind success_signal "a frame exports only after the gate passes"
|
|
140
|
+
|
|
141
|
+
# Pressure-test a claim, then let the USER confirm the condition:
|
|
142
|
+
d interrogate c1 --honesty "the contract round-trips: save -> load -> identical frame"
|
|
143
|
+
# ...user reviews and runs: d confirm h1
|
|
144
|
+
|
|
145
|
+
# Park a genuine unknown instead of guessing:
|
|
146
|
+
d park "exact JSON schema versioning policy" --kind unknown_nonblocking
|
|
147
|
+
|
|
148
|
+
d status # what's left + the next move
|
|
149
|
+
d converge # gate; resolve any listed gaps
|
|
150
|
+
d export # writes docs/specs/<slug>.md once converged
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The exported spec-md is a buildable artifact; it can feed directly into
|
|
154
|
+
`superpowers:writing-plans` or a normal implementation PR.
|
|
155
|
+
|
|
156
|
+
## Provenance
|
|
157
|
+
|
|
158
|
+
This is a **first-party** skill — its origin is `agentculture/devague`, where the
|
|
159
|
+
devague agent maintains it alongside the tool it operates (dogfooding). It is the
|
|
160
|
+
*inverse* of the other skills under `.claude/skills/`, which devague vendors
|
|
161
|
+
**from** steward. When this skill is ready, steward pulls it **from** devague and
|
|
162
|
+
broadcasts it to the rest of the AgentCulture mesh. The `cite, don't import`
|
|
163
|
+
policy still holds: downstream repos copy it, they don't symlink or depend on it.
|
|
164
|
+
See `docs/skill-sources.md`.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# devague.sh — operate the devague working-backwards spec tool.
|
|
3
|
+
#
|
|
4
|
+
# devague turns a vague feature idea into a buildable spec by working backwards.
|
|
5
|
+
# This wrapper is the agent-facing operator for the deterministic devague CLI:
|
|
6
|
+
# it resolves the CLI portably, forwards every move verbatim, and adds one
|
|
7
|
+
# value-add subcommand — `status` — that reads the convergence gate and names
|
|
8
|
+
# the recommended next move.
|
|
9
|
+
#
|
|
10
|
+
# Origin: authored and maintained in agentculture/devague. steward pulls this
|
|
11
|
+
# skill from here and broadcasts it to the rest of the AgentCulture mesh, so it
|
|
12
|
+
# is written to run anywhere — portable bash, no devague-checkout assumptions.
|
|
13
|
+
#
|
|
14
|
+
# Frames persist under .devague/ in the current directory, so run from the repo
|
|
15
|
+
# you are speccing.
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
# ── resolve the devague CLI (mesh-first, then local-dev fallback) ───────────
|
|
20
|
+
DEVAGUE=()
|
|
21
|
+
resolve_devague() {
|
|
22
|
+
if command -v devague >/dev/null 2>&1; then
|
|
23
|
+
DEVAGUE=(devague) # installed tool — the normal mesh case
|
|
24
|
+
return 0
|
|
25
|
+
fi
|
|
26
|
+
# Local-dev fallback: inside the devague checkout, run via uv.
|
|
27
|
+
local dir="$PWD"
|
|
28
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
29
|
+
if [ -f "$dir/pyproject.toml" ] \
|
|
30
|
+
&& grep -q '^name = "devague"' "$dir/pyproject.toml" 2>/dev/null; then
|
|
31
|
+
if command -v uv >/dev/null 2>&1; then
|
|
32
|
+
DEVAGUE=(uv run devague)
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
break
|
|
36
|
+
fi
|
|
37
|
+
dir=$(dirname "$dir")
|
|
38
|
+
done
|
|
39
|
+
cat >&2 <<'EOF'
|
|
40
|
+
error: devague CLI not found.
|
|
41
|
+
hint: install it with `uv tool install devague` (or `pipx install devague`),
|
|
42
|
+
or run from inside the devague checkout with `uv` available.
|
|
43
|
+
https://github.com/agentculture/devague
|
|
44
|
+
EOF
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
usage() {
|
|
49
|
+
cat <<'EOF'
|
|
50
|
+
devague.sh — operate the devague working-backwards spec tool.
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
devague.sh <move> [args...] forward a devague move
|
|
54
|
+
devague.sh status [--frame S] where the frame stands + the next move
|
|
55
|
+
devague.sh help this help
|
|
56
|
+
|
|
57
|
+
Moves (forwarded to the devague CLI; run `devague learn` for the full method):
|
|
58
|
+
new start a frame from the announcement ("pretend it shipped")
|
|
59
|
+
capture record + classify a claim (--kind audience|after_state|...)
|
|
60
|
+
interrogate pressure-test a claim (--honesty / --hard-question / --risk)
|
|
61
|
+
confirm confirm a claim or honesty condition (USER-only decision)
|
|
62
|
+
reject reject a claim or honesty condition
|
|
63
|
+
park record open vagueness instead of forcing an answer
|
|
64
|
+
converge check whether the frame can export a spec
|
|
65
|
+
export write the buildable spec (only after converge passes)
|
|
66
|
+
show / list render a frame / list frames
|
|
67
|
+
learn teach the method | explain <move> explain one move
|
|
68
|
+
|
|
69
|
+
Frames persist under .devague/ in the current directory — run from the repo
|
|
70
|
+
you are speccing. Results go to stdout, diagnostics to stderr; pass --json to
|
|
71
|
+
any move for structured output.
|
|
72
|
+
|
|
73
|
+
Note: `status` is a wrapper-only verb (the CLI has no `status`); everything
|
|
74
|
+
else is forwarded verbatim, so new devague moves work without editing this
|
|
75
|
+
script.
|
|
76
|
+
EOF
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ── status: read the convergence gate and recommend the next move ──────────
|
|
80
|
+
cmd_status() {
|
|
81
|
+
local list_json conv_out conv_err conv_rc req_frame="" prev="" tmp_err
|
|
82
|
+
|
|
83
|
+
# Pull the requested --frame (if any) so the header names the same frame
|
|
84
|
+
# that convergence is evaluated for; converge still receives it via "$@".
|
|
85
|
+
for arg in "$@"; do
|
|
86
|
+
case "$prev" in --frame) req_frame="$arg" ;; esac
|
|
87
|
+
case "$arg" in --frame=*) req_frame="${arg#--frame=}" ;; esac
|
|
88
|
+
prev="$arg"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
list_json="$("${DEVAGUE[@]}" list --json 2>/dev/null || true)"
|
|
92
|
+
|
|
93
|
+
# Capture converge's stdout, stderr, and exit code separately. converge
|
|
94
|
+
# exits 0 even when "not passed", so a non-zero code is a *real* error
|
|
95
|
+
# (bad/missing --frame, corrupt frame) we must surface, not swallow.
|
|
96
|
+
tmp_err="$(mktemp)"
|
|
97
|
+
set +e
|
|
98
|
+
conv_out="$("${DEVAGUE[@]}" converge --json "$@" 2>"$tmp_err")"
|
|
99
|
+
conv_rc=$?
|
|
100
|
+
set -e
|
|
101
|
+
conv_err="$(cat "$tmp_err")"
|
|
102
|
+
rm -f "$tmp_err"
|
|
103
|
+
|
|
104
|
+
DEVAGUE_LIST_JSON="$list_json" \
|
|
105
|
+
DEVAGUE_CONV_JSON="$conv_out" \
|
|
106
|
+
DEVAGUE_CONV_ERR="$conv_err" \
|
|
107
|
+
DEVAGUE_CONV_RC="$conv_rc" \
|
|
108
|
+
DEVAGUE_REQ_FRAME="$req_frame" \
|
|
109
|
+
python3 - <<'PY'
|
|
110
|
+
import json
|
|
111
|
+
import os
|
|
112
|
+
import re
|
|
113
|
+
import sys
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load(name):
|
|
117
|
+
raw = os.environ.get(name, "").strip()
|
|
118
|
+
if not raw:
|
|
119
|
+
return None
|
|
120
|
+
try:
|
|
121
|
+
return json.loads(raw)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
lst = load("DEVAGUE_LIST_JSON") or {}
|
|
127
|
+
conv = load("DEVAGUE_CONV_JSON")
|
|
128
|
+
conv_err = os.environ.get("DEVAGUE_CONV_ERR", "").strip()
|
|
129
|
+
req_frame = os.environ.get("DEVAGUE_REQ_FRAME", "").strip()
|
|
130
|
+
try:
|
|
131
|
+
conv_rc = int(os.environ.get("DEVAGUE_CONV_RC", "0") or "0")
|
|
132
|
+
except ValueError:
|
|
133
|
+
conv_rc = 0
|
|
134
|
+
|
|
135
|
+
frames = lst.get("frames") or []
|
|
136
|
+
current = lst.get("current")
|
|
137
|
+
|
|
138
|
+
if not frames:
|
|
139
|
+
print("no frames yet — start one:")
|
|
140
|
+
print(' devague new "<announcement>"')
|
|
141
|
+
print(' first question: "What\'s the announcement? Pretend this shipped'
|
|
142
|
+
' successfully — what would you announce?"')
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
shown = req_frame or current or "(none selected)"
|
|
146
|
+
total = len(frames)
|
|
147
|
+
print(f"frame: {shown} ({total} frame{'s' if total != 1 else ''} total)")
|
|
148
|
+
|
|
149
|
+
if conv is None:
|
|
150
|
+
# A non-zero converge exit on an existing frame is a genuine error —
|
|
151
|
+
# relay devague's own error:/hint: lines to stderr instead of masking it.
|
|
152
|
+
if conv_rc != 0 and conv_err:
|
|
153
|
+
sys.stderr.write(conv_err + "\n")
|
|
154
|
+
sys.exit(conv_rc)
|
|
155
|
+
print("convergence: unknown (could not evaluate the frame)")
|
|
156
|
+
print("next move: devague show # inspect the frame")
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
if conv.get("passed"):
|
|
160
|
+
print("convergence: PASSED ✓")
|
|
161
|
+
print("next move: devague export # write the buildable spec")
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
|
|
164
|
+
missing = conv.get("missing") or []
|
|
165
|
+
print(f"convergence: NOT passed — {len(missing)} gap(s):")
|
|
166
|
+
for gap in missing:
|
|
167
|
+
print(f" - {gap}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def suggest(gap):
|
|
171
|
+
# Confirmation is a USER-only transition; a plain (user-origin) capture
|
|
172
|
+
# is already confirmed, so never imply the agent should confirm its own
|
|
173
|
+
# work. Spell out who confirms wherever a confirm is in play.
|
|
174
|
+
m = re.search(r"missing confirmed '([a-z_]+)' claim", gap)
|
|
175
|
+
if m:
|
|
176
|
+
kind = m.group(1)
|
|
177
|
+
return (f'devague capture --kind {kind} "<text>"'
|
|
178
|
+
f' (a user capture auto-confirms; an --origin llm capture'
|
|
179
|
+
f' then needs the USER to confirm it)')
|
|
180
|
+
if "before_state" in gap and "why_it_matters" in gap:
|
|
181
|
+
return 'devague capture --kind why_it_matters "<text>"'
|
|
182
|
+
if "boundary" in gap:
|
|
183
|
+
return 'devague capture --kind boundary "<text>"'
|
|
184
|
+
if "success_signal" in gap:
|
|
185
|
+
return 'devague capture --kind success_signal "<text>"'
|
|
186
|
+
m = re.search(r"claim (c\d+) still proposed", gap)
|
|
187
|
+
if m:
|
|
188
|
+
cid = m.group(1)
|
|
189
|
+
return (f'this is an LLM proposal — the USER decides:'
|
|
190
|
+
f' devague confirm {cid} (or: devague reject {cid})')
|
|
191
|
+
m = re.search(r"claim (c\d+) has no confirmed honesty condition", gap)
|
|
192
|
+
if m:
|
|
193
|
+
cid = m.group(1)
|
|
194
|
+
return (f'devague interrogate {cid} --honesty "<what must be true>"'
|
|
195
|
+
f' then the USER runs: devague confirm <hN>')
|
|
196
|
+
m = re.search(r"blocking vagueness (v\d+)", gap)
|
|
197
|
+
if m:
|
|
198
|
+
return (f"resolve {m.group(1)}: capture+confirm the answer, "
|
|
199
|
+
f"or re-park it as non-blocking")
|
|
200
|
+
m = re.search(r"blocking hard question (q\d+) on (c\d+)", gap)
|
|
201
|
+
if m:
|
|
202
|
+
return (f"resolve {m.group(1)} on {m.group(2)}: answer it, then "
|
|
203
|
+
f"capture/confirm the resulting claim")
|
|
204
|
+
return "devague show # inspect and decide"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if missing:
|
|
208
|
+
print()
|
|
209
|
+
print("recommended next move (first gap):")
|
|
210
|
+
print(f" {suggest(missing[0])}")
|
|
211
|
+
PY
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main() {
|
|
215
|
+
case "${1:-help}" in
|
|
216
|
+
help | -h | --help)
|
|
217
|
+
usage
|
|
218
|
+
return 0
|
|
219
|
+
;;
|
|
220
|
+
status)
|
|
221
|
+
shift
|
|
222
|
+
resolve_devague
|
|
223
|
+
cmd_status "$@"
|
|
224
|
+
;;
|
|
225
|
+
*)
|
|
226
|
+
# Forward everything else to the CLI verbatim (including --version,
|
|
227
|
+
# and any future devague move), so its own parser owns the surface.
|
|
228
|
+
resolve_devague
|
|
229
|
+
exec "${DEVAGUE[@]}" "$@"
|
|
230
|
+
;;
|
|
231
|
+
esac
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main "$@"
|
|
@@ -5,6 +5,12 @@ 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.3.3] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- First-party `devague` skill (`.claude/skills/devague/`): a portable wrapper (`scripts/devague.sh`) that operates the working-backwards CLI, forwards every move, and adds a `status` next-move helper over the convergence gate; plus `tests/test_devague_skill.py` and an outbound-origin note in `docs/skill-sources.md`. Origin = devague; steward pulls it from here and broadcasts to the AgentCulture mesh.
|
|
13
|
+
|
|
8
14
|
## [0.3.2] - 2026-05-23
|
|
9
15
|
|
|
10
16
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devague
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: devague — turns a vague feature idea into a buildable spec by working backwards.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/devague
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/devague/issues
|
|
@@ -25,6 +25,17 @@ rows.
|
|
|
25
25
|
| `sonarclaude` | `steward` (`../steward/.claude/skills/sonarclaude/`) | 2026-05-22 | None — portable verbatim. Project key resolves from `$SONAR_PROJECT` / `--project` (here: `agentculture_devague`). |
|
|
26
26
|
| `doc-test-alignment` | `steward` (`../steward/.claude/skills/doc-test-alignment/`) | 2026-05-22 | **Stub upstream** — `scripts/check.sh` exits with a not-yet-implemented error today; the contract for what it will do lives in its `SKILL.md`. Vendored verbatim to carry the contract. |
|
|
27
27
|
|
|
28
|
+
## Origin skills (outbound)
|
|
29
|
+
|
|
30
|
+
Not every skill here is inbound. The `devague` skill is **authored and
|
|
31
|
+
maintained in this repo** — devague is its origin/upstream, not a downstream
|
|
32
|
+
consumer. The devague agent dogfoods it to operate the devague CLI while
|
|
33
|
+
improving the tool. The flow runs the *opposite* direction of the table above.
|
|
34
|
+
|
|
35
|
+
| Skill | Origin | Downstream | Notes |
|
|
36
|
+
|-------|--------|------------|-------|
|
|
37
|
+
| `devague` | **devague** (here: `.claude/skills/devague/`) | `steward`, then the AgentCulture mesh | Operator for the deterministic devague CLI: portable resolution + a `status` next-move helper over the convergence gate. When ready, `steward` re-vendors it from `../devague/.claude/skills/devague/` and broadcasts it to the mesh. `cite, don't import` still applies — downstream copies it, no symlink/dependency. Written portable-first so it passes steward's `portability-lint.sh`. |
|
|
38
|
+
|
|
28
39
|
## Vendoring policy
|
|
29
40
|
|
|
30
41
|
- **Cite, don't import.** Skills are copied, not symlinked or installed as
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Smoke tests for the first-party ``devague`` skill wrapper.
|
|
2
|
+
|
|
3
|
+
These drive ``.claude/skills/devague/scripts/devague.sh`` via subprocess in a
|
|
4
|
+
sandboxed ``tmp_path`` cwd (so ``.devague/`` never touches the repo). They pin
|
|
5
|
+
the contract steward relies on when it pulls this skill into the mesh: the
|
|
6
|
+
wrapper forwards moves verbatim, ``status`` reads the convergence gate and names
|
|
7
|
+
the next move, and ``export`` stays blocked until the frame converges.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
21
|
+
SCRIPT = REPO_ROOT / ".claude" / "skills" / "devague" / "scripts" / "devague.sh"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(*args: str, cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess:
|
|
25
|
+
return subprocess.run(
|
|
26
|
+
["bash", str(SCRIPT), *args],
|
|
27
|
+
cwd=str(cwd),
|
|
28
|
+
env=env,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _drive(cwd: Path, *args: str) -> subprocess.CompletedProcess:
|
|
35
|
+
proc = run(*args, cwd=cwd)
|
|
36
|
+
assert proc.returncode == 0, f"{args} failed: {proc.stderr}"
|
|
37
|
+
return proc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_script_is_executable_and_valid_bash() -> None:
|
|
41
|
+
assert SCRIPT.exists(), f"missing wrapper at {SCRIPT}"
|
|
42
|
+
assert os.access(SCRIPT, os.X_OK), "wrapper should be executable"
|
|
43
|
+
# `bash -n` parses without running.
|
|
44
|
+
assert subprocess.run(["bash", "-n", str(SCRIPT)]).returncode == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_help_lists_moves(tmp_path: Path) -> None:
|
|
48
|
+
proc = _drive(tmp_path, "help")
|
|
49
|
+
assert "operate the devague" in proc.stdout
|
|
50
|
+
for move in ("new", "capture", "interrogate", "converge", "export", "status"):
|
|
51
|
+
assert move in proc.stdout
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_forwards_learn_verbatim(tmp_path: Path) -> None:
|
|
55
|
+
proc = _drive(tmp_path, "learn")
|
|
56
|
+
assert "What's the announcement?" in proc.stdout
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_status_reports_no_frames(tmp_path: Path) -> None:
|
|
60
|
+
proc = _drive(tmp_path, "status")
|
|
61
|
+
assert "no frames yet" in proc.stdout
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_status_names_gaps_and_next_move(tmp_path: Path) -> None:
|
|
65
|
+
_drive(tmp_path, "new", "Devague ships a documented spec contract")
|
|
66
|
+
proc = _drive(tmp_path, "status")
|
|
67
|
+
assert "NOT passed" in proc.stdout
|
|
68
|
+
assert "missing confirmed 'audience' claim" in proc.stdout
|
|
69
|
+
# first gap -> capture the audience claim
|
|
70
|
+
assert "devague capture --kind audience" in proc.stdout
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_status_suggestion_does_not_imply_agent_confirm(tmp_path: Path) -> None:
|
|
74
|
+
# A user-origin capture auto-confirms; the suggestion must not imply the
|
|
75
|
+
# agent should run a follow-up `confirm` (the user-only-confirm hard rule).
|
|
76
|
+
_drive(tmp_path, "new", "Devague ships a documented spec contract")
|
|
77
|
+
proc = _drive(tmp_path, "status")
|
|
78
|
+
assert "auto-confirm" in proc.stdout
|
|
79
|
+
assert "then: devague confirm" not in proc.stdout
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_status_header_reflects_frame_flag(tmp_path: Path) -> None:
|
|
83
|
+
_drive(tmp_path, "new", "First frame")
|
|
84
|
+
second = _drive(tmp_path, "new", "Second frame", "--json")
|
|
85
|
+
slug = json.loads(second.stdout)["slug"]
|
|
86
|
+
# current pointer is now the second frame; ask about the first explicitly.
|
|
87
|
+
proc = _drive(tmp_path, "status", "--frame", "first-frame")
|
|
88
|
+
assert "frame: first-frame" in proc.stdout
|
|
89
|
+
assert slug != "first-frame" # guards the test against a no-op
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_status_surfaces_real_frame_errors(tmp_path: Path) -> None:
|
|
93
|
+
# A bad --frame must surface devague's error, not a misleading fallback.
|
|
94
|
+
_drive(tmp_path, "new", "A real frame")
|
|
95
|
+
proc = run("status", "--frame", "ghost", cwd=tmp_path)
|
|
96
|
+
assert proc.returncode != 0
|
|
97
|
+
assert "no such frame" in proc.stderr
|
|
98
|
+
assert "no frames yet" not in proc.stdout
|
|
99
|
+
assert "unknown" not in proc.stdout
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_export_blocked_until_converged(tmp_path: Path) -> None:
|
|
103
|
+
_drive(tmp_path, "new", "Devague ships a documented spec contract")
|
|
104
|
+
proc = run("export", cwd=tmp_path)
|
|
105
|
+
assert proc.returncode != 0
|
|
106
|
+
assert "has not converged" in proc.stderr
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_full_session_converges_and_exports(tmp_path: Path) -> None:
|
|
110
|
+
_drive(tmp_path, "new", "Devague ships a documented spec contract")
|
|
111
|
+
_drive(tmp_path, "capture", "--kind", "audience", "devague + the assisting LLM")
|
|
112
|
+
_drive(tmp_path, "capture", "--kind", "after_state", "a vague idea becomes a buildable spec")
|
|
113
|
+
_drive(tmp_path, "capture", "--kind", "why_it_matters", "specs converge on evidence not vibes")
|
|
114
|
+
_drive(tmp_path, "capture", "--kind", "boundary", "not a full PRD generator")
|
|
115
|
+
_drive(tmp_path, "capture", "--kind", "success_signal", "exports only after the gate passes")
|
|
116
|
+
|
|
117
|
+
# Every confirmed spec-affecting claim needs a confirmed honesty condition.
|
|
118
|
+
for cid in ("c1", "c2", "c3", "c4", "c5", "c6"):
|
|
119
|
+
out = _drive(tmp_path, "interrogate", cid, "--honesty", f"{cid} is testable", "--json")
|
|
120
|
+
hid = json.loads(out.stdout)["added"][0]["id"]
|
|
121
|
+
_drive(tmp_path, "confirm", hid)
|
|
122
|
+
|
|
123
|
+
status = _drive(tmp_path, "status")
|
|
124
|
+
assert "PASSED" in status.stdout
|
|
125
|
+
assert "devague export" in status.stdout
|
|
126
|
+
|
|
127
|
+
_drive(tmp_path, "converge")
|
|
128
|
+
exported = _drive(tmp_path, "export")
|
|
129
|
+
assert "exported spec" in exported.stdout
|
|
130
|
+
specs = list((tmp_path / "docs" / "specs").glob("*.md"))
|
|
131
|
+
assert specs, "export should write a spec file"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_missing_cli_emits_install_hint(tmp_path: Path) -> None:
|
|
135
|
+
"""With no resolvable ``devague``/``uv`` and no checkout, the wrapper hints."""
|
|
136
|
+
minimal_path = "/usr/bin:/bin"
|
|
137
|
+
env = {**os.environ, "PATH": minimal_path}
|
|
138
|
+
# Skip if this environment still resolves the tools under the minimal PATH.
|
|
139
|
+
if shutil.which("devague", path=minimal_path) or shutil.which("uv", path=minimal_path):
|
|
140
|
+
pytest.skip("devague/uv resolvable under minimal PATH; cannot test hint path")
|
|
141
|
+
proc = run("show", cwd=tmp_path, env=env)
|
|
142
|
+
assert proc.returncode != 0
|
|
143
|
+
assert "devague CLI not found" in proc.stderr
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/templates/skill-update-brief.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devague-0.3.2 → devague-0.3.3}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|