devague 0.6.1__tar.gz → 0.8.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.6.1 → devague-0.8.0}/CHANGELOG.md +22 -0
- {devague-0.6.1 → devague-0.8.0}/CLAUDE.md +3 -1
- {devague-0.6.1 → devague-0.8.0}/PKG-INFO +1 -1
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/learn.py +47 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_plans.py +16 -1
- {devague-0.6.1 → devague-0.8.0}/devague/frame.py +19 -1
- {devague-0.6.1 → devague-0.8.0}/devague/plan.py +25 -1
- {devague-0.6.1 → devague-0.8.0}/devague/plan_store.py +15 -1
- {devague-0.6.1 → devague-0.8.0}/devague/store.py +5 -0
- devague-0.8.0/docs/llm-guidance.md +130 -0
- {devague-0.6.1 → devague-0.8.0}/docs/spec-contract.md +24 -2
- {devague-0.6.1 → devague-0.8.0}/pyproject.toml +1 -1
- devague-0.8.0/tests/test_cli_affordances.py +111 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_plan.py +23 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_frame.py +7 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_plan.py +51 -1
- {devague-0.6.1 → devague-0.8.0}/tests/test_plan_store.py +43 -1
- {devague-0.6.1 → devague-0.8.0}/tests/test_store.py +13 -0
- {devague-0.6.1 → devague-0.8.0}/uv.lock +1 -1
- devague-0.6.1/tests/test_cli_affordances.py +0 -64
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/think/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/think/scripts/think.sh +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/.claude/skills.local.yaml.example +0 -0
- {devague-0.6.1 → devague-0.8.0}/.devague/current_plan +0 -0
- {devague-0.6.1 → devague-0.8.0}/.devague/frames/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.6.1 → devague-0.8.0}/.devague/frames/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.6.1 → devague-0.8.0}/.devague/plans/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.6.1 → devague-0.8.0}/.devague/plans/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.6.1 → devague-0.8.0}/.flake8 +0 -0
- {devague-0.6.1 → devague-0.8.0}/.github/workflows/publish.yml +0 -0
- {devague-0.6.1 → devague-0.8.0}/.github/workflows/security-checks.yml +0 -0
- {devague-0.6.1 → devague-0.8.0}/.github/workflows/tests.yml +0 -0
- {devague-0.6.1 → devague-0.8.0}/.gitignore +0 -0
- {devague-0.6.1 → devague-0.8.0}/.markdownlint-cli2.yaml +0 -0
- {devague-0.6.1 → devague-0.8.0}/.pre-commit-config.yaml +0 -0
- {devague-0.6.1 → devague-0.8.0}/LICENSE +0 -0
- {devague-0.6.1 → devague-0.8.0}/README.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/culture.yaml +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/__init__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/__main__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/__init__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/__init__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/capture.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/confirm.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/converge.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/explain.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/export.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/interrogate.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/list_frames.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/new.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/park.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/plan.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/question.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/reject.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/review.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_commands/show.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_errors.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_frames.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/cli/_output.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/convergence.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/plan_convergence.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/questions_io.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/render/__init__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/render/frame_md.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/render/plan_md.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/render/review_md.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/devague/render/spec_md.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/examples/contract-example.json +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/plans/devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/plans/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/reviews/spec-contract-frame-review.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/skill-sources.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/specs/devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/specs/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
- {devague-0.6.1 → devague-0.8.0}/sonar-project.properties +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/__init__.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_chassis.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_converge_export.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_errors.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_moves.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_output.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_question.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_cli_review.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_contract.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_convergence.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_offline.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_package.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_plan_convergence.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_render.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_render_plan.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_review_loop_integration.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_review_loop_invariants.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_spec_to_plan_skill.py +0 -0
- {devague-0.6.1 → devague-0.8.0}/tests/test_think_skill.py +0 -0
|
@@ -5,6 +5,28 @@ 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.8.0] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **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).
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
## [0.7.0] - 2026-05-23
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Plan persistence hardening (#18).** Plans now carry an integer `schema_version` (`PLAN_SCHEMA_VERSION = 1`), written on save and checked on load — the plan-engine peer of the frame `schema_version` contract (#5). `plan_store.load` fails closed with a clean `DevagueError` (exit 1 + upgrade hint) when a plan declares a newer unsupported schema; pre-0.7.0 plans without the key load silently as the current schema.
|
|
20
|
+
- Loaded-object validation for plans: `Task.origin` / `Task.status` and `PlanRisk.kind` are now validated at construction (via `__post_init__`), so a hand-edited or corrupted plan file surfaces an actionable "malformed plan" `DevagueError` instead of a traceback. (Task/dep/cover *id* cross-references are deliberately not validated at load — coverage and acyclic-dependency checks already run against the live frame in `plan converge`.)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `devague/cli/_plans.py` `resolve_plan` now distinguishes an invalid `--plan` slug, a newer-schema plan, a missing plan, and a malformed plan file — each with its own remediation hint (mirroring the frame `resolve`).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Persistence integrity, both engines (PR #25 review).** `store.load` / `plan_store.load` now reject a file whose embedded `slug` disagrees with the requested slug (previously a tampered file could redirect a later `save` onto a different frame/plan). And `schema_version` is now parsed strictly via the shared `frame.parse_schema_version` — a non-integer value (`1.9`, `true`, `"1"`, `null`) is rejected instead of being silently coerced by `int()` (which truncated `1.9`→`1` and accepted `True`→`1`). Both guards were applied symmetrically to the frame engine to keep the persistence twins aligned.
|
|
29
|
+
|
|
8
30
|
## [0.6.1] - 2026-05-23
|
|
9
31
|
|
|
10
32
|
### Fixed
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devague
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -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,
|
|
@@ -16,14 +16,29 @@ def resolve_plan(slug: str | None) -> Plan:
|
|
|
16
16
|
"run 'devague plan new --frame <slug>' or pass --plan <slug>",
|
|
17
17
|
)
|
|
18
18
|
try:
|
|
19
|
-
|
|
19
|
+
plan_store.validate_slug(slug)
|
|
20
20
|
except ValueError as exc:
|
|
21
21
|
raise DevagueError(
|
|
22
22
|
EXIT_USER_ERROR,
|
|
23
23
|
f"invalid plan slug: {slug!r}",
|
|
24
24
|
"slugs are lowercase letters, digits, and hyphens — no path separators",
|
|
25
25
|
) from exc
|
|
26
|
+
try:
|
|
27
|
+
return plan_store.load(slug)
|
|
28
|
+
except plan_store.IncompatiblePlanSchemaError as exc:
|
|
29
|
+
raise DevagueError(
|
|
30
|
+
EXIT_USER_ERROR,
|
|
31
|
+
str(exc),
|
|
32
|
+
"this plan was written by a newer devague — upgrade: 'uv tool install -U devague'",
|
|
33
|
+
) from exc
|
|
26
34
|
except FileNotFoundError:
|
|
27
35
|
raise DevagueError(
|
|
28
36
|
EXIT_USER_ERROR, f"no such plan: {slug}", "run 'devague plan list' to see plans"
|
|
29
37
|
) from None
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise DevagueError(
|
|
40
|
+
EXIT_USER_ERROR,
|
|
41
|
+
f"plan {slug!r} is malformed: {exc}",
|
|
42
|
+
"the plan file was hand-edited or corrupted — "
|
|
43
|
+
"fix .devague/plans/<slug>.json or recreate the plan",
|
|
44
|
+
) from exc
|
|
@@ -200,6 +200,24 @@ def to_dict(frame: Frame) -> dict:
|
|
|
200
200
|
return dataclasses.asdict(frame)
|
|
201
201
|
|
|
202
202
|
|
|
203
|
+
def parse_schema_version(d: dict, default: int) -> int:
|
|
204
|
+
"""Read a persisted ``schema_version`` strictly.
|
|
205
|
+
|
|
206
|
+
A missing key means a pre-field artifact → treat as ``default`` (back-compat).
|
|
207
|
+
A present value must be a real ``int`` — ``bool`` and non-int types (float,
|
|
208
|
+
str, ``None``) are rejected rather than silently coerced (e.g. plain
|
|
209
|
+
``int(1.9)`` would truncate to ``1`` and ``int(True)`` would yield ``1``), so
|
|
210
|
+
a malformed version surfaces as a clean error instead of loading as current.
|
|
211
|
+
Shared by the frame and plan engines (the persistence twins).
|
|
212
|
+
"""
|
|
213
|
+
if "schema_version" not in d:
|
|
214
|
+
return default
|
|
215
|
+
v = d["schema_version"]
|
|
216
|
+
if isinstance(v, bool) or not isinstance(v, int):
|
|
217
|
+
raise ValueError(f"schema_version must be an integer, got {v!r}")
|
|
218
|
+
return v
|
|
219
|
+
|
|
220
|
+
|
|
203
221
|
def from_dict(d: dict) -> Frame:
|
|
204
222
|
claims = [
|
|
205
223
|
Claim(
|
|
@@ -219,7 +237,7 @@ def from_dict(d: dict) -> Frame:
|
|
|
219
237
|
slug=d["slug"],
|
|
220
238
|
title=d["title"],
|
|
221
239
|
# A 0.4.0 frame predates the field; treat it as the current schema.
|
|
222
|
-
schema_version=
|
|
240
|
+
schema_version=parse_schema_version(d, SCHEMA_VERSION),
|
|
223
241
|
status=d.get("status", "drafting"),
|
|
224
242
|
created=d.get("created", ""),
|
|
225
243
|
updated=d.get("updated", ""),
|
|
@@ -16,7 +16,18 @@ import dataclasses
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
from typing import Optional
|
|
18
18
|
|
|
19
|
-
from devague.frame import
|
|
19
|
+
from devague.frame import (
|
|
20
|
+
ORIGINS,
|
|
21
|
+
SPEC_AFFECTING_KINDS,
|
|
22
|
+
VAGUENESS_KINDS,
|
|
23
|
+
Frame,
|
|
24
|
+
parse_schema_version,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Bump when the persisted plan shape changes incompatibly. `plan_store.load`
|
|
28
|
+
# fails closed on a plan whose schema_version is newer/unknown (see #18; the
|
|
29
|
+
# plan-engine peer of frame.SCHEMA_VERSION).
|
|
30
|
+
PLAN_SCHEMA_VERSION = 1
|
|
20
31
|
|
|
21
32
|
TASK_STATUSES = ("proposed", "confirmed", "rejected")
|
|
22
33
|
# Risks reuse the frame's open-vagueness kinds: a plan risk is the task-level peer of
|
|
@@ -35,6 +46,12 @@ class Task:
|
|
|
35
46
|
deps: list[str] = field(default_factory=list) # task ids this task depends on
|
|
36
47
|
covers: list[str] = field(default_factory=list) # frame claim/honesty ids (c*/h*)
|
|
37
48
|
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
if self.origin not in ORIGINS:
|
|
51
|
+
raise ValueError(f"unknown task origin: {self.origin!r}")
|
|
52
|
+
if self.status not in TASK_STATUSES:
|
|
53
|
+
raise ValueError(f"unknown task status: {self.status!r}")
|
|
54
|
+
|
|
38
55
|
|
|
39
56
|
@dataclass
|
|
40
57
|
class PlanRisk:
|
|
@@ -43,6 +60,10 @@ class PlanRisk:
|
|
|
43
60
|
kind: str # one of RISK_KINDS
|
|
44
61
|
task_id: Optional[str] = None
|
|
45
62
|
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if self.kind not in RISK_KINDS:
|
|
65
|
+
raise ValueError(f"unknown plan risk kind: {self.kind!r}")
|
|
66
|
+
|
|
46
67
|
|
|
47
68
|
@dataclass
|
|
48
69
|
class CoverageTarget:
|
|
@@ -62,6 +83,7 @@ class Plan:
|
|
|
62
83
|
slug: str
|
|
63
84
|
title: str
|
|
64
85
|
frame_slug: str
|
|
86
|
+
schema_version: int = PLAN_SCHEMA_VERSION
|
|
65
87
|
status: str = "drafting" # drafting | converged | exported
|
|
66
88
|
created: str = ""
|
|
67
89
|
updated: str = ""
|
|
@@ -170,6 +192,8 @@ def from_dict(d: dict) -> Plan:
|
|
|
170
192
|
slug=d["slug"],
|
|
171
193
|
title=d["title"],
|
|
172
194
|
frame_slug=d["frame_slug"],
|
|
195
|
+
# A pre-0.7.0 plan predates the field; treat it as the current schema.
|
|
196
|
+
schema_version=parse_schema_version(d, PLAN_SCHEMA_VERSION),
|
|
173
197
|
status=d.get("status", "drafting"),
|
|
174
198
|
created=d.get("created", ""),
|
|
175
199
|
updated=d.get("updated", ""),
|
|
@@ -12,13 +12,17 @@ import json
|
|
|
12
12
|
import time
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
from devague.plan import Plan, from_dict, to_dict
|
|
15
|
+
from devague.plan import PLAN_SCHEMA_VERSION, Plan, from_dict, to_dict
|
|
16
16
|
from devague.store import validate_slug
|
|
17
17
|
|
|
18
18
|
PLANS_DIR = Path(".devague/plans")
|
|
19
19
|
CURRENT_PLAN = Path(".devague/current_plan")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class IncompatiblePlanSchemaError(ValueError):
|
|
23
|
+
"""A persisted plan declares a schema_version this devague cannot read."""
|
|
24
|
+
|
|
25
|
+
|
|
22
26
|
def _now() -> str:
|
|
23
27
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
24
28
|
|
|
@@ -45,6 +49,16 @@ def load(slug: str) -> Plan:
|
|
|
45
49
|
plan = from_dict(json.loads(p.read_text(encoding="utf-8")))
|
|
46
50
|
validate_slug(plan.slug) # reject a tampered file whose internal slug escapes
|
|
47
51
|
validate_slug(plan.frame_slug) # the linked frame slug must be safe to load too
|
|
52
|
+
if plan.slug != slug:
|
|
53
|
+
# The embedded slug drives save() and the current-plan pointer; a file
|
|
54
|
+
# whose internal slug disagrees with its filename could silently redirect
|
|
55
|
+
# a later save onto a different plan, so reject it.
|
|
56
|
+
raise ValueError(f"plan slug mismatch: file {slug!r} declares slug {plan.slug!r}")
|
|
57
|
+
if plan.schema_version > PLAN_SCHEMA_VERSION:
|
|
58
|
+
raise IncompatiblePlanSchemaError(
|
|
59
|
+
f"plan {slug!r} uses schema_version {plan.schema_version}, but this "
|
|
60
|
+
f"devague supports up to {PLAN_SCHEMA_VERSION}; upgrade devague to read it"
|
|
61
|
+
)
|
|
48
62
|
return plan
|
|
49
63
|
|
|
50
64
|
|
|
@@ -130,6 +130,11 @@ def load(slug: str) -> Frame:
|
|
|
130
130
|
raise FileNotFoundError(slug)
|
|
131
131
|
frame = from_dict(json.loads(p.read_text(encoding="utf-8")))
|
|
132
132
|
validate_slug(frame.slug) # reject a tampered file whose internal slug escapes
|
|
133
|
+
if frame.slug != slug:
|
|
134
|
+
# The embedded slug drives save() and the current-frame pointer; a file
|
|
135
|
+
# whose internal slug disagrees with its filename could silently redirect
|
|
136
|
+
# a later save onto a different frame, so reject it.
|
|
137
|
+
raise ValueError(f"frame slug mismatch: file {slug!r} declares slug {frame.slug!r}")
|
|
133
138
|
if frame.schema_version > SCHEMA_VERSION:
|
|
134
139
|
raise IncompatibleSchemaError(
|
|
135
140
|
f"frame {slug!r} uses schema_version {frame.schema_version}, but this "
|
|
@@ -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
|
|
@@ -153,8 +157,9 @@ the exit code is non-zero and `stderr` carries a `hint:` line.
|
|
|
153
157
|
**Validation errors** (all raise a clean `DevagueError`, exit code 1): unknown
|
|
154
158
|
claim kind / origin / status or vagueness kind (rejected at construction);
|
|
155
159
|
unknown claim or honesty id on `confirm`/`reject`; an invalid `--frame` slug; a
|
|
156
|
-
missing frame; a malformed or hand-edited frame file
|
|
157
|
-
`schema_version` is
|
|
160
|
+
missing frame; a malformed or hand-edited frame file (including one whose
|
|
161
|
+
embedded slug doesn't match the requested slug, or whose `schema_version` is not
|
|
162
|
+
an integer); a frame whose `schema_version` is too new.
|
|
158
163
|
|
|
159
164
|
## Anti-fabrication guarantee
|
|
160
165
|
|
|
@@ -178,3 +183,20 @@ targets derived from a converged frame, Tasks (`origin`/`status` like claims,
|
|
|
178
183
|
plus `deps`, `covers`, `acceptance_criteria`), and PlanRisks. It reuses the same
|
|
179
184
|
structured convergence result, serialized under `ready_for_plan`. See
|
|
180
185
|
`docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md`.
|
|
186
|
+
|
|
187
|
+
Plans carry the same persistence contract as frames. Every plan has an integer
|
|
188
|
+
`schema_version` (currently `1`, `PLAN_SCHEMA_VERSION`), written on save and
|
|
189
|
+
checked on load: `plan_store.load` **fails closed** with a clean `DevagueError`
|
|
190
|
+
(exit code 1, upgrade hint) when a plan declares a `schema_version` newer than
|
|
191
|
+
this devague supports. A pre-0.7.0 plan with no `schema_version` key loads
|
|
192
|
+
silently as the current schema. Loaded `Task.origin` / `Task.status` and
|
|
193
|
+
`PlanRisk.kind` are validated at construction; an invalid value surfaces as a
|
|
194
|
+
"malformed plan" `DevagueError` rather than a traceback. (Task/dep/cover **id**
|
|
195
|
+
cross-references are deliberately *not* validated at load — coverage and acyclic
|
|
196
|
+
dependency checks already run against the live frame in `plan converge`.)
|
|
197
|
+
|
|
198
|
+
Both `load`s also reject a file whose embedded slug disagrees with the requested
|
|
199
|
+
slug (so a tampered file can't silently redirect a later `save`), and parse
|
|
200
|
+
`schema_version` strictly via the shared `frame.parse_schema_version` — a
|
|
201
|
+
non-integer value is rejected rather than coerced. These guards are symmetric
|
|
202
|
+
across the frame and plan persistence twins.
|
|
@@ -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()
|
|
@@ -278,6 +278,29 @@ def test_resolve_plan_invalid_slug(tmp_path, monkeypatch, capsys) -> None:
|
|
|
278
278
|
assert "invalid plan slug" in capsys.readouterr().err
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
def test_resolve_plan_rejects_newer_schema(tmp_path, monkeypatch, capsys) -> None:
|
|
282
|
+
slug = _converged_plan(monkeypatch, tmp_path, capsys)
|
|
283
|
+
p = plan_store.path_for(slug)
|
|
284
|
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
285
|
+
raw["schema_version"] = raw["schema_version"] + 99
|
|
286
|
+
p.write_text(json.dumps(raw), encoding="utf-8")
|
|
287
|
+
rc = main(["plan", "show", "--plan", slug])
|
|
288
|
+
assert rc == 1
|
|
289
|
+
err = capsys.readouterr().err
|
|
290
|
+
assert "schema_version" in err and "upgrade" in err
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_resolve_plan_rejects_malformed_value(tmp_path, monkeypatch, capsys) -> None:
|
|
294
|
+
slug = _converged_plan(monkeypatch, tmp_path, capsys)
|
|
295
|
+
p = plan_store.path_for(slug)
|
|
296
|
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
297
|
+
raw["tasks"][0]["origin"] = "alien"
|
|
298
|
+
p.write_text(json.dumps(raw), encoding="utf-8")
|
|
299
|
+
rc = main(["plan", "show", "--plan", slug])
|
|
300
|
+
assert rc == 1
|
|
301
|
+
assert "malformed" in capsys.readouterr().err
|
|
302
|
+
|
|
303
|
+
|
|
281
304
|
# ── learn / explain ───────────────────────────────────────────────────────────
|
|
282
305
|
def test_learn_and_explain(tmp_path, monkeypatch, capsys) -> None:
|
|
283
306
|
monkeypatch.chdir(tmp_path)
|
|
@@ -98,6 +98,13 @@ def test_legacy_frame_without_schema_version_loads() -> None:
|
|
|
98
98
|
assert f.schema_version == SCHEMA_VERSION
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
@pytest.mark.parametrize("bad", [1.9, True, "1", None])
|
|
102
|
+
def test_from_dict_rejects_non_integer_schema_version(bad) -> None:
|
|
103
|
+
# int() would silently coerce 1.9->1 / True->1; a malformed type must raise.
|
|
104
|
+
with pytest.raises(ValueError, match="schema_version"):
|
|
105
|
+
from_dict({"slug": "s", "title": "t", "schema_version": bad})
|
|
106
|
+
|
|
107
|
+
|
|
101
108
|
def test_dataclasses_validate_enums() -> None:
|
|
102
109
|
with pytest.raises(ValueError):
|
|
103
110
|
Claim(id="c1", kind="bogus", text="x")
|