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.
Files changed (83) hide show
  1. devague-0.3.3/.claude/skills/devague/SKILL.md +164 -0
  2. devague-0.3.3/.claude/skills/devague/scripts/devague.sh +234 -0
  3. {devague-0.3.2 → devague-0.3.3}/.flake8 +1 -1
  4. {devague-0.3.2 → devague-0.3.3}/CHANGELOG.md +6 -0
  5. {devague-0.3.2 → devague-0.3.3}/PKG-INFO +1 -1
  6. {devague-0.3.2 → devague-0.3.3}/docs/skill-sources.md +11 -0
  7. {devague-0.3.2 → devague-0.3.3}/pyproject.toml +1 -1
  8. devague-0.3.3/tests/test_devague_skill.py +143 -0
  9. {devague-0.3.2 → devague-0.3.3}/uv.lock +1 -1
  10. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/SKILL.md +0 -0
  11. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  12. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  13. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  14. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  15. {devague-0.3.2 → devague-0.3.3}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  16. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/SKILL.md +0 -0
  17. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  18. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  19. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  20. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  21. {devague-0.3.2 → devague-0.3.3}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  22. {devague-0.3.2 → devague-0.3.3}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  23. {devague-0.3.2 → devague-0.3.3}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  24. {devague-0.3.2 → devague-0.3.3}/.claude/skills/run-tests/SKILL.md +0 -0
  25. {devague-0.3.2 → devague-0.3.3}/.claude/skills/run-tests/scripts/test.sh +0 -0
  26. {devague-0.3.2 → devague-0.3.3}/.claude/skills/sonarclaude/SKILL.md +0 -0
  27. {devague-0.3.2 → devague-0.3.3}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  28. {devague-0.3.2 → devague-0.3.3}/.claude/skills/version-bump/SKILL.md +0 -0
  29. {devague-0.3.2 → devague-0.3.3}/.claude/skills/version-bump/scripts/bump.py +0 -0
  30. {devague-0.3.2 → devague-0.3.3}/.claude/skills.local.yaml.example +0 -0
  31. {devague-0.3.2 → devague-0.3.3}/.github/workflows/publish.yml +0 -0
  32. {devague-0.3.2 → devague-0.3.3}/.github/workflows/security-checks.yml +0 -0
  33. {devague-0.3.2 → devague-0.3.3}/.github/workflows/tests.yml +0 -0
  34. {devague-0.3.2 → devague-0.3.3}/.gitignore +0 -0
  35. {devague-0.3.2 → devague-0.3.3}/.markdownlint-cli2.yaml +0 -0
  36. {devague-0.3.2 → devague-0.3.3}/.pre-commit-config.yaml +0 -0
  37. {devague-0.3.2 → devague-0.3.3}/CLAUDE.md +0 -0
  38. {devague-0.3.2 → devague-0.3.3}/LICENSE +0 -0
  39. {devague-0.3.2 → devague-0.3.3}/README.md +0 -0
  40. {devague-0.3.2 → devague-0.3.3}/culture.yaml +0 -0
  41. {devague-0.3.2 → devague-0.3.3}/devague/__init__.py +0 -0
  42. {devague-0.3.2 → devague-0.3.3}/devague/__main__.py +0 -0
  43. {devague-0.3.2 → devague-0.3.3}/devague/cli/__init__.py +0 -0
  44. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/__init__.py +0 -0
  45. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/capture.py +0 -0
  46. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/confirm.py +0 -0
  47. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/converge.py +0 -0
  48. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/explain.py +0 -0
  49. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/export.py +0 -0
  50. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/interrogate.py +0 -0
  51. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/learn.py +0 -0
  52. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/list_frames.py +0 -0
  53. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/new.py +0 -0
  54. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/park.py +0 -0
  55. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/reject.py +0 -0
  56. {devague-0.3.2 → devague-0.3.3}/devague/cli/_commands/show.py +0 -0
  57. {devague-0.3.2 → devague-0.3.3}/devague/cli/_errors.py +0 -0
  58. {devague-0.3.2 → devague-0.3.3}/devague/cli/_frames.py +0 -0
  59. {devague-0.3.2 → devague-0.3.3}/devague/cli/_output.py +0 -0
  60. {devague-0.3.2 → devague-0.3.3}/devague/convergence.py +0 -0
  61. {devague-0.3.2 → devague-0.3.3}/devague/frame.py +0 -0
  62. {devague-0.3.2 → devague-0.3.3}/devague/render/__init__.py +0 -0
  63. {devague-0.3.2 → devague-0.3.3}/devague/render/frame_md.py +0 -0
  64. {devague-0.3.2 → devague-0.3.3}/devague/render/spec_md.py +0 -0
  65. {devague-0.3.2 → devague-0.3.3}/devague/store.py +0 -0
  66. {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
  67. {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
  68. {devague-0.3.2 → devague-0.3.3}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
  69. {devague-0.3.2 → devague-0.3.3}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
  70. {devague-0.3.2 → devague-0.3.3}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
  71. {devague-0.3.2 → devague-0.3.3}/sonar-project.properties +0 -0
  72. {devague-0.3.2 → devague-0.3.3}/tests/__init__.py +0 -0
  73. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_affordances.py +0 -0
  74. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_chassis.py +0 -0
  75. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_converge_export.py +0 -0
  76. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_errors.py +0 -0
  77. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_moves.py +0 -0
  78. {devague-0.3.2 → devague-0.3.3}/tests/test_cli_output.py +0 -0
  79. {devague-0.3.2 → devague-0.3.3}/tests/test_convergence.py +0 -0
  80. {devague-0.3.2 → devague-0.3.3}/tests/test_frame.py +0 -0
  81. {devague-0.3.2 → devague-0.3.3}/tests/test_package.py +0 -0
  82. {devague-0.3.2 → devague-0.3.3}/tests/test_render.py +0 -0
  83. {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 "$@"
@@ -14,4 +14,4 @@ exclude =
14
14
  build
15
15
  per-file-ignores =
16
16
  # Bandit S-rules are noisy in tests (assert, subprocess, etc.) — mute them.
17
- tests/*:S101,S404,S603
17
+ tests/*:S101,S404,S603,S607
@@ -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.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devague"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "devague — turns a vague feature idea into a buildable spec by working backwards."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
@@ -183,7 +183,7 @@ wheels = [
183
183
 
184
184
  [[package]]
185
185
  name = "devague"
186
- version = "0.3.2"
186
+ version = "0.3.3"
187
187
  source = { editable = "." }
188
188
 
189
189
  [package.dev-dependencies]
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