devague 0.7.0__tar.gz → 0.9.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.7.0 → devague-0.9.0}/.claude/skills/spec-to-plan/SKILL.md +1 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +1 -0
- {devague-0.7.0 → devague-0.9.0}/CHANGELOG.md +14 -0
- {devague-0.7.0 → devague-0.9.0}/CLAUDE.md +17 -4
- {devague-0.7.0 → devague-0.9.0}/PKG-INFO +1 -1
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/export.py +2 -1
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/learn.py +47 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/plan.py +40 -4
- devague-0.9.0/devague/cli/_paths.py +39 -0
- {devague-0.7.0 → devague-0.9.0}/devague/plan.py +38 -0
- {devague-0.7.0 → devague-0.9.0}/devague/plan_convergence.py +11 -0
- devague-0.9.0/docs/llm-guidance.md +130 -0
- {devague-0.7.0 → devague-0.9.0}/docs/spec-contract.md +15 -0
- {devague-0.7.0 → devague-0.9.0}/pyproject.toml +1 -1
- devague-0.9.0/tests/test_cli_affordances.py +111 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_converge_export.py +6 -2
- devague-0.9.0/tests/test_cli_paths.py +21 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_plan.py +86 -2
- {devague-0.7.0 → devague-0.9.0}/tests/test_plan.py +76 -0
- {devague-0.7.0 → devague-0.9.0}/uv.lock +1 -1
- devague-0.7.0/tests/test_cli_affordances.py +0 -64
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/think/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/think/scripts/think.sh +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/.claude/skills.local.yaml.example +0 -0
- {devague-0.7.0 → devague-0.9.0}/.devague/current_plan +0 -0
- {devague-0.7.0 → devague-0.9.0}/.devague/frames/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.7.0 → devague-0.9.0}/.devague/frames/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.7.0 → devague-0.9.0}/.devague/plans/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.7.0 → devague-0.9.0}/.devague/plans/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.7.0 → devague-0.9.0}/.flake8 +0 -0
- {devague-0.7.0 → devague-0.9.0}/.github/workflows/publish.yml +0 -0
- {devague-0.7.0 → devague-0.9.0}/.github/workflows/security-checks.yml +0 -0
- {devague-0.7.0 → devague-0.9.0}/.github/workflows/tests.yml +0 -0
- {devague-0.7.0 → devague-0.9.0}/.gitignore +0 -0
- {devague-0.7.0 → devague-0.9.0}/.markdownlint-cli2.yaml +0 -0
- {devague-0.7.0 → devague-0.9.0}/.pre-commit-config.yaml +0 -0
- {devague-0.7.0 → devague-0.9.0}/LICENSE +0 -0
- {devague-0.7.0 → devague-0.9.0}/README.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/culture.yaml +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/__init__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/__main__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/__init__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/__init__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/capture.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/confirm.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/converge.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/explain.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/interrogate.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/list_frames.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/new.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/park.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/question.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/reject.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/review.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_commands/show.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_errors.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_frames.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_output.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/cli/_plans.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/convergence.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/frame.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/plan_store.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/questions_io.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/render/__init__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/render/frame_md.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/render/plan_md.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/render/review_md.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/render/spec_md.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/devague/store.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/examples/contract-example.json +0 -0
- /devague-0.7.0/docs/plans/devague-0-6-0-ships-the-human-review-loop-devague.md → /devague-0.9.0/docs/plans/2026-05-23-devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- /devague-0.7.0/docs/plans/devague-now-ships-a-documented-spec-contract-every.md → /devague-0.9.0/docs/plans/2026-05-23-devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/reviews/spec-contract-frame-review.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/skill-sources.md +0 -0
- /devague-0.7.0/docs/specs/devague-0-6-0-ships-the-human-review-loop-devague.md → /devague-0.9.0/docs/specs/2026-05-23-devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- /devague-0.7.0/docs/specs/devague-now-ships-a-documented-spec-contract-every.md → /devague-0.9.0/docs/specs/2026-05-23-devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
- {devague-0.7.0 → devague-0.9.0}/sonar-project.properties +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/__init__.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_chassis.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_errors.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_moves.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_output.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_question.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_cli_review.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_contract.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_convergence.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_frame.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_offline.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_package.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_plan_convergence.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_plan_store.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_render.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_render_plan.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_review_loop_integration.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_review_loop_invariants.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_spec_to_plan_skill.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_store.py +0 -0
- {devague-0.7.0 → devague-0.9.0}/tests/test_think_skill.py +0 -0
|
@@ -57,6 +57,7 @@ install hint. Every move except `status` is forwarded verbatim as `devague plan
|
|
|
57
57
|
| `risk "<text>" --kind <kind>` | Record a first-class plan risk (`--task <tN>` to attach). |
|
|
58
58
|
| `converge` | Evaluate the gate against the **live** source frame; list remaining gaps. |
|
|
59
59
|
| `export` | Write the buildable plan to `docs/plans/` — only after `converge` passes. |
|
|
60
|
+
| `waves` | Emit deterministic dependency waves (`{plan, waves}`) — scheduling metadata only, *not* orchestration. Read-only, works on an in-progress plan; refuses a cyclic/dangling graph. Devague describes the graph; an operator decides how to run it (#20). |
|
|
60
61
|
| `show` / `list` | Render a plan / list plans (`--json` for raw state). |
|
|
61
62
|
| `learn` / `explain <move>` | Teach the method / explain one move. |
|
|
62
63
|
|
|
@@ -66,6 +66,7 @@ Moves (forwarded to `devague plan`; run `devague plan learn` for the method):
|
|
|
66
66
|
risk record a first-class plan risk
|
|
67
67
|
converge check whether the plan can export
|
|
68
68
|
export write the buildable plan (only after converge passes)
|
|
69
|
+
waves emit deterministic dependency waves (scheduling metadata, not orchestration)
|
|
69
70
|
show / list render a plan / list plans
|
|
70
71
|
learn teach the method | explain <move> explain one move
|
|
71
72
|
|
|
@@ -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.9.0] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Plan dependency waves (#20).** New `devague plan waves [--json]` move emits the plan's task dependency graph as deterministic, machine-readable scheduling metadata (`{plan, waves}` — ordered batches of task ids where wave 0 has no unsatisfied dependency and each later wave depends only on earlier ones). Read-only and convergence-agnostic, so it works on an in-progress plan. Rejected tasks are excluded; a cycle or a dependency on a missing/rejected task is refused by reusing the plan-convergence dependency blockers (`dependency_blockers`). This is the small deterministic primitive behind #13: Devague *describes* the parallelizable graph; it does not spawn subagents, manage worktrees, mark tasks done, or pick a backend.
|
|
13
|
+
- **Dated export filenames (#12).** `devague export` and `devague plan export` now prefix the written file with the frame/plan creation date — `docs/specs/<YYYY-MM-DD>-<slug>.md` and `docs/plans/<YYYY-MM-DD>-<slug>.md`. The date comes from the object's `created` timestamp (not today), so re-exporting an unchanged artifact overwrites the same file rather than spawning a dated duplicate. Existing exported docs were renamed to match.
|
|
14
|
+
|
|
15
|
+
## [0.8.0] - 2026-05-23
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Portable LLM guidance contract (#19).** New `docs/llm-guidance.md` — a runtime-agnostic operating contract for any assisting model driving Devague (not just Claude Code): the move-driven mental model, the (state × origin) vocabulary, the anti-fabrication hard rules, adaptive-not-scripted ordering, good/bad operator examples, and the forward (plan) leg. Distilled from the `/think` and `/spec-to-plan` skill contracts; it complements, and does not replace, an agent runtime's own main instruction file (`AGENTS.md`, `CLAUDE.md`, a system prompt).
|
|
20
|
+
- `devague learn` (text and `--json`) now always surfaces the operating rules: a `devague is NOT` framing (not a wizard / questionnaire / PRD generator), the anti-fabrication rules, and a pointer to `docs/llm-guidance.md`. JSON gains `not_a`, `operating_rules`, `guidance_doc` (a portable canonical URL — `docs/` is not shipped in the wheel), and `guidance_doc_repo_path` keys.
|
|
21
|
+
|
|
8
22
|
## [0.7.0] - 2026-05-23
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -30,8 +30,8 @@ gate, renderer registry, and the flat moves `new` / `capture` / `interrogate` /
|
|
|
30
30
|
structural peer:
|
|
31
31
|
`devague/plan.py`, `plan_convergence.py`, `plan_store.py`, `render/plan_md.py`,
|
|
32
32
|
and the nested group `devague plan <move>` (`new` / `task` / `accept` / `depend`
|
|
33
|
-
/ `cover` / `confirm` / `reject` / `risk` / `converge` / `export` / `
|
|
34
|
-
`list` / `learn` / `explain`). The two operator skills are `/think` (idea→spec,
|
|
33
|
+
/ `cover` / `confirm` / `reject` / `risk` / `converge` / `export` / `waves` /
|
|
34
|
+
`show` / `list` / `learn` / `explain`). The two operator skills are `/think` (idea→spec,
|
|
35
35
|
renamed from `/devague`) and `/spec-to-plan` (spec→plan). Coverage ≥ 95 %; all
|
|
36
36
|
linters pass. Run `git ls-files` to see the real surface.
|
|
37
37
|
|
|
@@ -50,7 +50,9 @@ itself. The workflow:
|
|
|
50
50
|
shipped successfully — what would you announce to users, teammates, or
|
|
51
51
|
yourself?"). Creates a Frame seeded with the announcement claim
|
|
52
52
|
(auto-confirmed, since it comes from the user). `devague learn` documents the
|
|
53
|
-
full ten-stage guided sequence
|
|
53
|
+
full ten-stage guided sequence plus the always-on **operating rules** (the
|
|
54
|
+
anti-fabrication contract); the portable, agent-agnostic version of that
|
|
55
|
+
contract lives in `docs/llm-guidance.md` (#19).
|
|
54
56
|
2. `devague capture --kind <kind> "<text>"` — add claims; LLM-proposed ones
|
|
55
57
|
(`--origin llm`) land as `proposed` and require explicit user `confirm`.
|
|
56
58
|
3. `devague interrogate <claim-id>` — attach honesty conditions and hard
|
|
@@ -84,7 +86,18 @@ verbs). The workflow:
|
|
|
84
86
|
covered by a confirmed task, every confirmed task has acceptance criteria, the
|
|
85
87
|
dependency graph is acyclic, and no blocking risk remains.
|
|
86
88
|
5. `devague plan export` — only after `converge` passes; writes a buildable
|
|
87
|
-
plan-md (topologically ordered) to `docs/plans
|
|
89
|
+
plan-md (topologically ordered) to `docs/plans/<created-date>-<slug>.md`.
|
|
90
|
+
6. `devague plan waves [--json]` — emit the plan's dependency graph as
|
|
91
|
+
deterministic **scheduling metadata** (`{plan, waves}`): ordered batches of
|
|
92
|
+
task ids that an external operator *could* fan out. Read-only,
|
|
93
|
+
convergence-agnostic (works on an in-progress plan), and explicitly **not
|
|
94
|
+
orchestration** — Devague describes the graph; it does not spawn subagents,
|
|
95
|
+
manage worktrees, mark tasks done, or pick a backend (#20). A cyclic or
|
|
96
|
+
dangling graph is refused via the plan-convergence dependency blockers.
|
|
97
|
+
|
|
98
|
+
Both `devague export` and `devague plan export` prefix the written file with the
|
|
99
|
+
frame/plan creation date (`<YYYY-MM-DD>-<slug>.md`, #12), so re-exporting an
|
|
100
|
+
unchanged artifact overwrites the same file rather than spawning a duplicate.
|
|
88
101
|
|
|
89
102
|
Full design: `docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md`.
|
|
90
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devague
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -9,6 +9,7 @@ from devague import render, store
|
|
|
9
9
|
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
10
10
|
from devague.cli._frames import resolve
|
|
11
11
|
from devague.cli._output import emit_result
|
|
12
|
+
from devague.cli._paths import dated_name
|
|
12
13
|
from devague.convergence import evaluate
|
|
13
14
|
|
|
14
15
|
SPECS_DIR = Path("docs/specs")
|
|
@@ -25,7 +26,7 @@ def cmd_export(args: argparse.Namespace) -> int:
|
|
|
25
26
|
)
|
|
26
27
|
text = render.render(frame, args.format)
|
|
27
28
|
SPECS_DIR.mkdir(parents=True, exist_ok=True)
|
|
28
|
-
out_path = SPECS_DIR /
|
|
29
|
+
out_path = SPECS_DIR / dated_name(frame.created, frame.slug)
|
|
29
30
|
out_path.write_text(text, encoding="utf-8")
|
|
30
31
|
frame.status = "exported"
|
|
31
32
|
store.save(frame)
|
|
@@ -26,6 +26,40 @@ SUPPORTING_PROMPT = (
|
|
|
26
26
|
"teammates, or yourself?"
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
# The portable, runtime-agnostic operating contract for any assisting model
|
|
30
|
+
# (devague#19). The full version lives in the guidance doc; this is the core
|
|
31
|
+
# surfaced in every `learn`. These rules are what make convergence mean something.
|
|
32
|
+
#
|
|
33
|
+
# `docs/` is not shipped in the wheel (only the `devague` package is), and an
|
|
34
|
+
# installed devague is operated from an arbitrary repo — so a bare relative path
|
|
35
|
+
# wouldn't resolve for most consumers. The portable, always-resolvable reference
|
|
36
|
+
# is the canonical URL; the in-repo path is kept for contributors.
|
|
37
|
+
GUIDANCE_DOC_URL = "https://github.com/agentculture/devague/blob/main/docs/llm-guidance.md"
|
|
38
|
+
GUIDANCE_DOC_REPO_PATH = "docs/llm-guidance.md"
|
|
39
|
+
|
|
40
|
+
# What devague is NOT — the framing that keeps it from degrading into a form.
|
|
41
|
+
NOT_A = (
|
|
42
|
+
"a wizard (no fixed prompt sequence)",
|
|
43
|
+
"a scripted questionnaire (you don't read questions off a form)",
|
|
44
|
+
"a PRD generator (it never invents content to fill a template)",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# The anti-fabrication rules. Agent-agnostic: repo-specific agreements live in
|
|
48
|
+
# your agent's main instruction file (AGENTS.md, CLAUDE.md, a system prompt, …),
|
|
49
|
+
# not here.
|
|
50
|
+
OPERATING_RULES = (
|
|
51
|
+
"LLM proposals stay proposed — capture your own ideas with --origin llm; "
|
|
52
|
+
"never confirm your own proposal. Confirmation is a user-only decision.",
|
|
53
|
+
"Honesty conditions route through the user — propose freely with "
|
|
54
|
+
"'interrogate --honesty'; the user owns whether each one holds.",
|
|
55
|
+
"Park real unknowns instead of papering over them — 'park' a genuine "
|
|
56
|
+
"unknown rather than writing confident prose that hides the gap.",
|
|
57
|
+
"Converge, don't vibe — 'export' is gated on 'converge'; resolve every "
|
|
58
|
+
"listed gap instead of declaring readiness on a hunch.",
|
|
59
|
+
"Order is adaptive — the ten stages are an artifact shape, not a mandatory "
|
|
60
|
+
"conversation order; capture what the user gives you and circle back.",
|
|
61
|
+
)
|
|
62
|
+
|
|
29
63
|
# The canonical guided sequence (devague#4). The engine is move-driven, not a
|
|
30
64
|
# rigid wizard — this is the recommended arc, with the move that advances each.
|
|
31
65
|
STAGES = [
|
|
@@ -57,6 +91,15 @@ _TEXT = (
|
|
|
57
91
|
)
|
|
58
92
|
+ "\n\nMoves:\n"
|
|
59
93
|
+ "\n".join(f" {name:<11} {desc}" for name, desc in MOVES.items())
|
|
94
|
+
+ "\n\ndevague is NOT:\n"
|
|
95
|
+
+ "\n".join(f" - {n}" for n in NOT_A)
|
|
96
|
+
+ "\n\nOperating rules (the anti-fabrication contract — do not violate):\n"
|
|
97
|
+
+ "\n".join(f" - {r}" for r in OPERATING_RULES)
|
|
98
|
+
+ "\n\nFull portable guidance for any assisting model:\n"
|
|
99
|
+
f" {GUIDANCE_DOC_URL}\n"
|
|
100
|
+
f" (in the devague repo: {GUIDANCE_DOC_REPO_PATH})\n"
|
|
101
|
+
"Agent-agnostic; your repo-specific agreements live in your agent's main\n"
|
|
102
|
+
"instruction file — AGENTS.md, CLAUDE.md, a system prompt — not there."
|
|
60
103
|
)
|
|
61
104
|
|
|
62
105
|
|
|
@@ -73,6 +116,10 @@ def cmd_learn(args: argparse.Namespace) -> int:
|
|
|
73
116
|
for i, (name, prompt, move) in enumerate(STAGES, 1)
|
|
74
117
|
],
|
|
75
118
|
"moves": list(MOVES),
|
|
119
|
+
"not_a": list(NOT_A),
|
|
120
|
+
"operating_rules": list(OPERATING_RULES),
|
|
121
|
+
"guidance_doc": GUIDANCE_DOC_URL,
|
|
122
|
+
"guidance_doc_repo_path": GUIDANCE_DOC_REPO_PATH,
|
|
76
123
|
"summary": _TEXT,
|
|
77
124
|
},
|
|
78
125
|
json_mode=True,
|
|
@@ -19,10 +19,12 @@ from devague import plan_store, store
|
|
|
19
19
|
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
20
20
|
from devague.cli._frames import resolve as resolve_frame
|
|
21
21
|
from devague.cli._output import emit_result
|
|
22
|
+
from devague.cli._paths import dated_name
|
|
22
23
|
from devague.cli._plans import resolve_plan
|
|
23
24
|
from devague.convergence import evaluate as evaluate_frame
|
|
24
25
|
from devague.frame import Frame
|
|
25
|
-
from devague.plan import RISK_KINDS, Plan, targets_from_frame, to_dict
|
|
26
|
+
from devague.plan import RISK_KINDS, Plan, dependency_waves, targets_from_frame, to_dict
|
|
27
|
+
from devague.plan_convergence import dependency_blockers
|
|
26
28
|
from devague.plan_convergence import evaluate as evaluate_plan
|
|
27
29
|
from devague.render import plan_md
|
|
28
30
|
|
|
@@ -30,6 +32,7 @@ PLANS_OUT_DIR = Path("docs/plans")
|
|
|
30
32
|
|
|
31
33
|
_JSON_HELP = "Emit structured JSON."
|
|
32
34
|
_TASK_ID_HELP = "Task id."
|
|
35
|
+
_RESOLVE_HINT = "resolve: " # prefix for a "; "-joined blocker remediation hint
|
|
33
36
|
|
|
34
37
|
PLAN_MOVES = {
|
|
35
38
|
"new": "Start a plan from a converged frame (derives coverage targets).",
|
|
@@ -42,6 +45,7 @@ PLAN_MOVES = {
|
|
|
42
45
|
"risk": "Record a first-class plan risk instead of papering over it.",
|
|
43
46
|
"converge": "Check whether the plan can export, against the live frame.",
|
|
44
47
|
"export": "Write the buildable plan — only once the plan converges.",
|
|
48
|
+
"waves": "Emit deterministic dependency waves (scheduling metadata, not orchestration).",
|
|
45
49
|
"show": "Render the plan.",
|
|
46
50
|
"list": "List plans.",
|
|
47
51
|
}
|
|
@@ -102,7 +106,7 @@ def cmd_plan_new(args: argparse.Namespace) -> int:
|
|
|
102
106
|
raise DevagueError(
|
|
103
107
|
EXIT_USER_ERROR,
|
|
104
108
|
f"frame '{frame.slug}' has not converged; cannot start a plan",
|
|
105
|
-
|
|
109
|
+
_RESOLVE_HINT + "; ".join(result.blockers),
|
|
106
110
|
)
|
|
107
111
|
if plan_store.path_for(frame.slug).exists():
|
|
108
112
|
raise DevagueError(
|
|
@@ -267,12 +271,12 @@ def cmd_plan_export(args: argparse.Namespace) -> int:
|
|
|
267
271
|
raise DevagueError(
|
|
268
272
|
EXIT_USER_ERROR,
|
|
269
273
|
"plan has not converged; cannot export",
|
|
270
|
-
|
|
274
|
+
_RESOLVE_HINT + "; ".join(result.blockers),
|
|
271
275
|
)
|
|
272
276
|
plan.status = "exported"
|
|
273
277
|
text = plan_md.render_plan(plan, frame)
|
|
274
278
|
PLANS_OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
275
|
-
out_path = PLANS_OUT_DIR /
|
|
279
|
+
out_path = PLANS_OUT_DIR / dated_name(plan.created, plan.slug)
|
|
276
280
|
out_path.write_text(text, encoding="utf-8")
|
|
277
281
|
plan_store.save(plan)
|
|
278
282
|
if getattr(args, "json", False):
|
|
@@ -282,6 +286,34 @@ def cmd_plan_export(args: argparse.Namespace) -> int:
|
|
|
282
286
|
return 0
|
|
283
287
|
|
|
284
288
|
|
|
289
|
+
def cmd_plan_waves(args: argparse.Namespace) -> int:
|
|
290
|
+
"""Emit the plan's dependency waves — deterministic scheduling metadata only.
|
|
291
|
+
|
|
292
|
+
Read-only and convergence-agnostic: waves derive purely from the task dependency
|
|
293
|
+
graph, so they work on an in-progress plan. Devague describes the graph; an
|
|
294
|
+
external operator (Culture, codexd, …) decides how to execute it — see #20. A cycle
|
|
295
|
+
or a dep on a missing/rejected task is refused by reusing the plan-convergence
|
|
296
|
+
integrity blockers.
|
|
297
|
+
"""
|
|
298
|
+
plan = resolve_plan(args.plan)
|
|
299
|
+
blockers = dependency_blockers(plan)
|
|
300
|
+
if blockers:
|
|
301
|
+
raise DevagueError(
|
|
302
|
+
EXIT_USER_ERROR,
|
|
303
|
+
"cannot derive waves: the dependency graph is not sound",
|
|
304
|
+
_RESOLVE_HINT + "; ".join(blockers),
|
|
305
|
+
)
|
|
306
|
+
waves = dependency_waves(plan.tasks)
|
|
307
|
+
if getattr(args, "json", False):
|
|
308
|
+
emit_result({"plan": plan.slug, "waves": waves}, json_mode=True)
|
|
309
|
+
elif not waves:
|
|
310
|
+
emit_result("no tasks to schedule", json_mode=False)
|
|
311
|
+
else:
|
|
312
|
+
lines = [f"wave {i}: {', '.join(w)}" for i, w in enumerate(waves)]
|
|
313
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
285
317
|
def cmd_plan_show(args: argparse.Namespace) -> int:
|
|
286
318
|
plan = resolve_plan(args.plan)
|
|
287
319
|
if getattr(args, "json", False):
|
|
@@ -424,6 +456,10 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
424
456
|
_plan_opt(pex)
|
|
425
457
|
pex.set_defaults(func=cmd_plan_export)
|
|
426
458
|
|
|
459
|
+
pwv = psub.add_parser("waves", help="Emit deterministic dependency waves (metadata only).")
|
|
460
|
+
_plan_opt(pwv)
|
|
461
|
+
pwv.set_defaults(func=cmd_plan_waves)
|
|
462
|
+
|
|
427
463
|
psh = psub.add_parser("show", help="Render the plan.")
|
|
428
464
|
_plan_opt(psh)
|
|
429
465
|
psh.set_defaults(func=cmd_plan_show)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Filesystem naming for exported artifacts (specs and plans)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
# Stable, date-shaped sentinel used when ``created`` is absent or malformed. It must
|
|
8
|
+
# not vary by run (e.g. today's date) or idempotence would break for that edge — see
|
|
9
|
+
# the rationale in ``dated_name``.
|
|
10
|
+
UNDATED_PREFIX = "0000-00-00"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dated_name(created: str, slug: str) -> str:
|
|
14
|
+
"""Return ``<YYYY-MM-DD>-<slug>.md`` — the export filename with a date prefix (#12).
|
|
15
|
+
|
|
16
|
+
The date is taken from ``created`` (the object's persisted ISO timestamp,
|
|
17
|
+
``2026-05-23T…``) rather than today, so re-exporting an unchanged object is
|
|
18
|
+
idempotent — it overwrites the same file instead of spawning a dated duplicate. An
|
|
19
|
+
object is always saved (creation stamped) before it can converge and export, so a
|
|
20
|
+
real date is the normal case.
|
|
21
|
+
|
|
22
|
+
If ``created`` is somehow absent or malformed (only reachable via a hand-corrupted
|
|
23
|
+
store file — ``store.save`` repopulates an *empty* stamp but not an *invalid* one),
|
|
24
|
+
we fall back to the constant :data:`UNDATED_PREFIX` rather than today's date: a
|
|
25
|
+
run-varying fallback would itself break the idempotence this prefix exists to
|
|
26
|
+
provide.
|
|
27
|
+
"""
|
|
28
|
+
date = (created or "")[:10]
|
|
29
|
+
if not _is_iso_date(date):
|
|
30
|
+
date = UNDATED_PREFIX
|
|
31
|
+
return f"{date}-{slug}.md"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_iso_date(value: str) -> bool:
|
|
35
|
+
try:
|
|
36
|
+
time.strptime(value, "%Y-%m-%d")
|
|
37
|
+
return True
|
|
38
|
+
except ValueError:
|
|
39
|
+
return False
|
|
@@ -169,6 +169,44 @@ def targets_from_frame(frame: Frame) -> list[CoverageTarget]:
|
|
|
169
169
|
return targets
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
def dependency_waves(tasks: list[Task]) -> list[list[str]]:
|
|
173
|
+
"""Layer active (non-rejected) tasks into deterministic dependency waves.
|
|
174
|
+
|
|
175
|
+
Wave 0 is every active task with no unsatisfied dependency; each later wave is the
|
|
176
|
+
tasks whose deps are all satisfied by earlier waves — the parallel batches an
|
|
177
|
+
external operator *could* fan out (Devague describes the graph; it does not run it).
|
|
178
|
+
Within a wave ids keep stored order, so the layering is deterministic for a given
|
|
179
|
+
plan.
|
|
180
|
+
|
|
181
|
+
Rejected tasks are excluded entirely. A dependency on a task outside the active set
|
|
182
|
+
(unknown, or rejected) is treated as already satisfied here so the function stays
|
|
183
|
+
**total**: those dangling edges are integrity failures surfaced separately by the
|
|
184
|
+
plan-convergence gate (:func:`devague.plan_convergence.dependency_blockers`), not
|
|
185
|
+
scheduling facts. The graph is assumed acyclic; any tasks left unplaceable by a
|
|
186
|
+
cycle are appended as a final wave so this never loops forever.
|
|
187
|
+
|
|
188
|
+
This is the wave-grouped peer of ``render.plan_md._topo_order`` (which flattens a
|
|
189
|
+
*greedy* topological order for the exported plan); the two intentionally differ in
|
|
190
|
+
grouping, so they are kept as separate functions.
|
|
191
|
+
"""
|
|
192
|
+
active = [t for t in tasks if t.status != "rejected"]
|
|
193
|
+
by_id = {t.id: t for t in active}
|
|
194
|
+
placed: set[str] = set()
|
|
195
|
+
remaining = list(active)
|
|
196
|
+
waves: list[list[str]] = []
|
|
197
|
+
progress = True
|
|
198
|
+
while remaining and progress:
|
|
199
|
+
ready = [t for t in remaining if all(d in placed or d not in by_id for d in t.deps)]
|
|
200
|
+
progress = bool(ready)
|
|
201
|
+
if ready:
|
|
202
|
+
waves.append([t.id for t in ready])
|
|
203
|
+
placed.update(t.id for t in ready)
|
|
204
|
+
remaining = [t for t in remaining if t.id not in placed]
|
|
205
|
+
if remaining: # cycle leftover — the caller should have blocked this
|
|
206
|
+
waves.append([t.id for t in remaining])
|
|
207
|
+
return waves
|
|
208
|
+
|
|
209
|
+
|
|
172
210
|
def to_dict(plan: Plan) -> dict:
|
|
173
211
|
return dataclasses.asdict(plan)
|
|
174
212
|
|
|
@@ -121,6 +121,17 @@ def _missing_dep_integrity(plan: Plan) -> list[str]:
|
|
|
121
121
|
return missing
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def dependency_blockers(plan: Plan) -> list[str]:
|
|
125
|
+
"""Public view of just the dependency-graph integrity blockers.
|
|
126
|
+
|
|
127
|
+
The dangling-dep / rejected-dep / cycle subset of the full gate — without the
|
|
128
|
+
coverage, acceptance, or resolution checks. ``devague plan waves`` uses this to
|
|
129
|
+
refuse an unsound graph (a cycle or a dep on a missing/rejected task) while still
|
|
130
|
+
emitting waves for an otherwise in-progress, not-yet-converged plan.
|
|
131
|
+
"""
|
|
132
|
+
return _missing_dep_integrity(plan)
|
|
133
|
+
|
|
134
|
+
|
|
124
135
|
def _missing_risks(plan: Plan) -> list[str]:
|
|
125
136
|
return [f"blocking risk {r.id} unresolved" for r in plan.risks if r.kind == "unknown_blocking"]
|
|
126
137
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Operating Devague — portable guidance for assisting models
|
|
2
|
+
|
|
3
|
+
This is the **portable, runtime-agnostic contract** for any LLM or agent that
|
|
4
|
+
operates Devague. It does not assume a particular agent runtime (Claude Code,
|
|
5
|
+
a Codex/`AGENTS.md` agent, Copilot, an ACP host, a bare system prompt, …). It
|
|
6
|
+
complements — it does **not** replace — your agent's own main instruction file
|
|
7
|
+
(`AGENTS.md`, `CLAUDE.md`, a system prompt, or equivalent), which carries the
|
|
8
|
+
repo-specific working agreements. Where the two overlap, this document is the
|
|
9
|
+
authority on *how Devague itself must be driven*.
|
|
10
|
+
|
|
11
|
+
The authoritative entity model and the per-move input/output/transition
|
|
12
|
+
contract live in [`spec-contract.md`](spec-contract.md). For the live shape of
|
|
13
|
+
any move, run it with `--json`, or run `devague learn` / `devague explain
|
|
14
|
+
<move>`.
|
|
15
|
+
|
|
16
|
+
## 1. What Devague is — and is not
|
|
17
|
+
|
|
18
|
+
Devague is a **deterministic, move-driven state machine** over claims, honesty
|
|
19
|
+
conditions, open vagueness, and a convergence gate. There are **no LLM calls
|
|
20
|
+
inside the CLI** — it only records moves and reports what is still missing. The
|
|
21
|
+
intelligence is *you*, the operating model.
|
|
22
|
+
|
|
23
|
+
It is **not**:
|
|
24
|
+
|
|
25
|
+
- **not a wizard** — there is no fixed sequence of prompts to march through;
|
|
26
|
+
- **not a scripted questionnaire** — you do not read questions off a form;
|
|
27
|
+
- **not a PRD generator** — it will not invent content to fill a template.
|
|
28
|
+
|
|
29
|
+
You choose each move from the live state; the CLI tracks state and tells you
|
|
30
|
+
what remains before a spec (or plan) can be exported.
|
|
31
|
+
|
|
32
|
+
## 2. The state you operate on
|
|
33
|
+
|
|
34
|
+
Two legs share one chassis:
|
|
35
|
+
|
|
36
|
+
- **Frame (idea → spec).** Claims (each with a *kind* — `announcement`,
|
|
37
|
+
`audience`, `after_state`, `before_state`, `why_it_matters`, `boundary`,
|
|
38
|
+
`success_signal`, `open_question`, `non_goal`, `requirement`, `assumption`,
|
|
39
|
+
`decision`); honesty conditions and hard questions attached to claims; and
|
|
40
|
+
**open vagueness** (parked unknowns, kinds `unknown_nonblocking` /
|
|
41
|
+
`unknown_blocking` / `out_of_scope` / `follow_up`).
|
|
42
|
+
- **Plan (spec → plan).** Coverage targets (derived from a converged frame),
|
|
43
|
+
tasks (with acceptance criteria, dependencies, and the targets they cover),
|
|
44
|
+
and first-class plan risks.
|
|
45
|
+
|
|
46
|
+
Every element carries two orthogonal axes:
|
|
47
|
+
|
|
48
|
+
- **origin** — `user` or `llm` (who proposed it);
|
|
49
|
+
- **status** — `proposed`, `confirmed`, or `rejected`.
|
|
50
|
+
|
|
51
|
+
`origin` and `status` are independent. An `llm`-proposed claim is *proposed*
|
|
52
|
+
until a human acts on it; a `user`-provided claim is *confirmed* on arrival.
|
|
53
|
+
Keeping these distinct is the whole point of the tool — see §4.
|
|
54
|
+
|
|
55
|
+
## 3. You choose the move; order is adaptive
|
|
56
|
+
|
|
57
|
+
The moves are `new`, `capture`, `interrogate`, `confirm`, `reject`, `review`,
|
|
58
|
+
`question`, `park`, `converge`, `export`, `show`, `list` (plus the `plan …`
|
|
59
|
+
moves for the forward leg). Pick the move that fits the live state — not a
|
|
60
|
+
predetermined script.
|
|
61
|
+
|
|
62
|
+
When unsure what to do next, ask the gate, don't guess: run `converge --json`
|
|
63
|
+
(it returns `{ready_for_spec, blockers, warnings, parked_items,
|
|
64
|
+
required_next_moves}`; plans return `ready_for_plan`) and act on the first
|
|
65
|
+
blocker.
|
|
66
|
+
|
|
67
|
+
The canonical **ten-stage arc** (announcement → audience → after → matter →
|
|
68
|
+
before → honest → FAQ → boundaries → success → spec) that `devague learn`
|
|
69
|
+
prints is an **artifact shape and a recommended arc — not a mandatory
|
|
70
|
+
conversation order**. If the user hands you the audience and the success signal
|
|
71
|
+
before the announcement is crisp, capture those now and circle back. Drive
|
|
72
|
+
toward the shape; do not impose a sequence on the user.
|
|
73
|
+
|
|
74
|
+
## 4. Hard rules — the anti-fabrication contract
|
|
75
|
+
|
|
76
|
+
These are not style preferences. Convergence is only meaningful if these hold.
|
|
77
|
+
|
|
78
|
+
- **LLM proposals stay proposed.** Capture your own ideas freely with `--origin
|
|
79
|
+
llm` (claims) or by attaching honesty conditions; they land as `proposed`.
|
|
80
|
+
**Never `confirm` your own proposal.** Confirmation is a **user-only**
|
|
81
|
+
decision. Surface the proposal and let the user confirm or reject it — proposed
|
|
82
|
+
content must never silently become an authoritative requirement.
|
|
83
|
+
- **Honesty conditions route through the user.** Propose them generously with
|
|
84
|
+
`interrogate --honesty`; the user owns whether each one actually holds.
|
|
85
|
+
- **Park real unknowns; do not paper over them.** If something is genuinely
|
|
86
|
+
unknown, `park` it (blocking or non-blocking) instead of writing confident
|
|
87
|
+
prose that hides the gap. Blocking vagueness holds back convergence by design.
|
|
88
|
+
- **Converge, don't vibe.** `export` is gated on `converge` passing. Never
|
|
89
|
+
declare a frame or plan "ready" on a hunch — run `converge` and resolve every
|
|
90
|
+
listed gap first.
|
|
91
|
+
|
|
92
|
+
## 5. Good vs. bad operator behavior
|
|
93
|
+
|
|
94
|
+
| Situation | ❌ Bad (fabricating) | ✅ Good (honest) |
|
|
95
|
+
|-----------|---------------------|------------------|
|
|
96
|
+
| You have a strong guess at the audience | `capture --kind audience … --origin user` (passing your guess off as the user's) | `capture --kind audience … --origin llm`, then ask the user to `confirm` |
|
|
97
|
+
| You proposed an honesty condition | `confirm h3` yourself so the gate passes | leave `h3` proposed; surface it for the user to confirm |
|
|
98
|
+
| A key detail is genuinely unknown | invent a plausible answer to keep momentum | `park "<the unknown>" --kind unknown_blocking` |
|
|
99
|
+
| User asks "is this ready?" | "Yes, looks solid." | run `converge`; report the actual blockers/warnings |
|
|
100
|
+
| The user skipped a stage | march through the stages in order anyway | capture what they gave you; let the arc fill in adaptively |
|
|
101
|
+
| Plan: a task has no clear acceptance test | mark it confirmed and move on | leave it without criteria (the gate blocks it) or `park` the risk |
|
|
102
|
+
|
|
103
|
+
## 6. The forward leg (spec → plan), in brief
|
|
104
|
+
|
|
105
|
+
The plan engine is the structural peer of the frame engine and obeys the same
|
|
106
|
+
spirit:
|
|
107
|
+
|
|
108
|
+
- **Seed from a converged spec only** — `plan new` refuses an unconverged frame.
|
|
109
|
+
- **LLM-proposed tasks stay proposed**; the user confirms them.
|
|
110
|
+
- **Cover every target, criteria on every task** — the gate requires it.
|
|
111
|
+
- **Keep the dependency graph honest** — real task ids, acyclic.
|
|
112
|
+
- **Park genuine unknowns as risks** (`unknown_blocking` holds convergence back).
|
|
113
|
+
- **Converge against the live frame** — `converge`/`export` re-load the source
|
|
114
|
+
frame; if it regressed below convergence, re-converge the spec first.
|
|
115
|
+
|
|
116
|
+
## 7. Output contract
|
|
117
|
+
|
|
118
|
+
Results go to **stdout**; diagnostics and errors go to **stderr** — a strict
|
|
119
|
+
split you can parse. Pass `--json` to any move for a structured payload on the
|
|
120
|
+
same stream. Exit code is `0` on success, non-zero on user error (with a
|
|
121
|
+
`hint:` line and no Python traceback). Frames and plans persist under
|
|
122
|
+
`.devague/` in the current directory.
|
|
123
|
+
|
|
124
|
+
## 8. Where authority lives
|
|
125
|
+
|
|
126
|
+
- **Entity model + per-move contract:** [`spec-contract.md`](spec-contract.md).
|
|
127
|
+
- **Live shape of any move:** run it with `--json`, or `devague learn` /
|
|
128
|
+
`devague explain <move>`.
|
|
129
|
+
- **Repo-specific working agreements:** your agent's main instruction file
|
|
130
|
+
(`AGENTS.md`, `CLAUDE.md`, system prompt, …) — not this document.
|
|
@@ -11,6 +11,10 @@ CLI tracks state. Every move accepts and emits JSON (`--json`), with a strict
|
|
|
11
11
|
without guessing internal state. All operations run fully offline against local
|
|
12
12
|
`.devague/` state.
|
|
13
13
|
|
|
14
|
+
For the portable, runtime-agnostic contract on *how* an assisting model should
|
|
15
|
+
operate Devague — the move-driven mental model and the anti-fabrication rules —
|
|
16
|
+
see [`llm-guidance.md`](llm-guidance.md) (also surfaced in `devague learn`).
|
|
17
|
+
|
|
14
18
|
## Versioning
|
|
15
19
|
|
|
16
20
|
Every frame carries an integer `schema_version` (currently `1`). It is written
|
|
@@ -180,6 +184,17 @@ plus `deps`, `covers`, `acceptance_criteria`), and PlanRisks. It reuses the same
|
|
|
180
184
|
structured convergence result, serialized under `ready_for_plan`. See
|
|
181
185
|
`docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md`.
|
|
182
186
|
|
|
187
|
+
`plan waves` emits the plan's dependency graph as deterministic, machine-readable
|
|
188
|
+
scheduling metadata — `{plan, waves}`, where `waves` is an ordered list of task-id
|
|
189
|
+
batches (wave 0 has no unsatisfied dependency; each later wave depends only on
|
|
190
|
+
earlier ones). It is **read-only**, never mutates state, and is **not** gated on
|
|
191
|
+
convergence, so it works on an in-progress plan. Rejected tasks are excluded; a
|
|
192
|
+
cycle or a dependency on a missing/rejected task is refused by reusing the
|
|
193
|
+
plan-convergence dependency blockers. The boundary is deliberate (issue #20):
|
|
194
|
+
Devague *describes* the parallelizable graph; an external operator (Culture,
|
|
195
|
+
codexd, …) decides how — or whether — to execute it. Devague does not spawn
|
|
196
|
+
subagents, manage worktrees, mark tasks done, or choose a backend.
|
|
197
|
+
|
|
183
198
|
Plans carry the same persistence contract as frames. Every plan has an integer
|
|
184
199
|
`schema_version` (currently `1`, `PLAN_SCHEMA_VERSION`), written on save and
|
|
185
200
|
checked on load: `plan_store.load` **fails closed** with a clean `DevagueError`
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Tests for the agent-affordance verbs: learn / explain."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from devague.cli import main
|
|
11
|
+
|
|
12
|
+
_GUIDANCE_DOC = Path(__file__).resolve().parents[1] / "docs" / "llm-guidance.md"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_learn_describes_moves(capsys: pytest.CaptureFixture[str]) -> None:
|
|
16
|
+
rc = main(["learn"])
|
|
17
|
+
assert rc == 0
|
|
18
|
+
out = capsys.readouterr().out.lower()
|
|
19
|
+
assert "working backwards" in out
|
|
20
|
+
assert "capture" in out and "converge" in out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_learn_teaches_first_question_and_guided_stages(
|
|
24
|
+
capsys: pytest.CaptureFixture[str],
|
|
25
|
+
) -> None:
|
|
26
|
+
rc = main(["learn"])
|
|
27
|
+
assert rc == 0
|
|
28
|
+
out = capsys.readouterr().out.lower()
|
|
29
|
+
# Issue #4's mandated entry point and supporting framing.
|
|
30
|
+
assert "what's the announcement?" in out
|
|
31
|
+
assert "users, teammates, or yourself" in out
|
|
32
|
+
# The canonical 10-step guided sequence is documented.
|
|
33
|
+
for stage in (
|
|
34
|
+
"announcement",
|
|
35
|
+
"audience",
|
|
36
|
+
"after",
|
|
37
|
+
"matter",
|
|
38
|
+
"before",
|
|
39
|
+
"honest",
|
|
40
|
+
"faq",
|
|
41
|
+
"boundaries",
|
|
42
|
+
"success",
|
|
43
|
+
"spec",
|
|
44
|
+
):
|
|
45
|
+
assert stage in out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_learn_json_lists_moves_and_stages(capsys: pytest.CaptureFixture[str]) -> None:
|
|
49
|
+
rc = main(["learn", "--json"])
|
|
50
|
+
assert rc == 0
|
|
51
|
+
payload = json.loads(capsys.readouterr().out)
|
|
52
|
+
assert payload["tool"] == "devague"
|
|
53
|
+
assert "capture" in payload["moves"]
|
|
54
|
+
assert len(payload["stages"]) == 10
|
|
55
|
+
assert payload["first_question"] == "What's the announcement?"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_learn_surfaces_operating_rules(capsys: pytest.CaptureFixture[str]) -> None:
|
|
59
|
+
# devague#19: the anti-fabrication contract is always-on in `learn` output.
|
|
60
|
+
rc = main(["learn"])
|
|
61
|
+
assert rc == 0
|
|
62
|
+
out = capsys.readouterr().out.lower()
|
|
63
|
+
assert "operating rules" in out
|
|
64
|
+
assert "anti-fabrication" in out
|
|
65
|
+
# The core rules and the not-a framing.
|
|
66
|
+
assert "stay proposed" in out and "user-only" in out
|
|
67
|
+
assert "not a mandatory conversation order" in out # order is adaptive
|
|
68
|
+
assert "questionnaire" in out and "prd generator" in out
|
|
69
|
+
# Agent-agnostic pointer — not hardcoded to one runtime.
|
|
70
|
+
assert "agents.md" in out and "claude.md" in out
|
|
71
|
+
# A portable, always-resolvable URL (the wheel doesn't ship docs/) plus the
|
|
72
|
+
# in-repo path for contributors.
|
|
73
|
+
assert "https://github.com/agentculture/devague" in out
|
|
74
|
+
assert "docs/llm-guidance.md" in out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_learn_json_exposes_operating_contract(capsys: pytest.CaptureFixture[str]) -> None:
|
|
78
|
+
rc = main(["learn", "--json"])
|
|
79
|
+
assert rc == 0
|
|
80
|
+
payload = json.loads(capsys.readouterr().out)
|
|
81
|
+
# guidance_doc is a portable, always-resolvable URL (docs/ isn't shipped in
|
|
82
|
+
# the wheel); the in-repo source path is exposed separately for contributors.
|
|
83
|
+
assert payload["guidance_doc"].startswith("https://")
|
|
84
|
+
assert payload["guidance_doc"].endswith("docs/llm-guidance.md")
|
|
85
|
+
assert payload["guidance_doc_repo_path"] == "docs/llm-guidance.md"
|
|
86
|
+
assert len(payload["operating_rules"]) >= 4
|
|
87
|
+
assert len(payload["not_a"]) == 3
|
|
88
|
+
assert any("proposed" in r.lower() for r in payload["operating_rules"])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_guidance_doc_exists_and_documents_the_contract() -> None:
|
|
92
|
+
# The CLI points agents at this doc, so it must exist and carry the contract.
|
|
93
|
+
assert _GUIDANCE_DOC.is_file()
|
|
94
|
+
text = _GUIDANCE_DOC.read_text(encoding="utf-8").lower()
|
|
95
|
+
assert "anti-fabrication" in text
|
|
96
|
+
assert "never `confirm` your own proposal" in text or "never confirm your own proposal" in text
|
|
97
|
+
assert "not a mandatory" in text # adaptive order
|
|
98
|
+
# Agent-agnostic, not Claude-specific.
|
|
99
|
+
assert "agents.md" in text
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_explain_a_move(capsys: pytest.CaptureFixture[str]) -> None:
|
|
103
|
+
rc = main(["explain", "converge"])
|
|
104
|
+
assert rc == 0
|
|
105
|
+
assert "converge" in capsys.readouterr().out.lower()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_explain_unknown_move_errors(capsys: pytest.CaptureFixture[str]) -> None:
|
|
109
|
+
rc = main(["explain", "nope"])
|
|
110
|
+
assert rc == 1
|
|
111
|
+
assert "unknown" in capsys.readouterr().err.lower()
|