devague 0.5.1__tar.gz → 0.6.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.5.1 → devague-0.6.0}/.claude/skills/think/SKILL.md +3 -1
- {devague-0.5.1 → devague-0.6.0}/CHANGELOG.md +15 -0
- {devague-0.5.1 → devague-0.6.0}/CLAUDE.md +12 -2
- {devague-0.5.1 → devague-0.6.0}/PKG-INFO +45 -1
- devague-0.6.0/README.md +85 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/__init__.py +4 -0
- devague-0.6.0/devague/cli/_commands/confirm.py +96 -0
- devague-0.6.0/devague/cli/_commands/question.py +80 -0
- devague-0.6.0/devague/cli/_commands/reject.py +15 -0
- devague-0.6.0/devague/cli/_commands/review.py +63 -0
- devague-0.6.0/devague/questions_io.py +64 -0
- {devague-0.5.1 → devague-0.6.0}/devague/render/__init__.py +2 -0
- devague-0.6.0/devague/render/review_md.py +76 -0
- {devague-0.5.1 → devague-0.6.0}/devague/store.py +52 -0
- {devague-0.5.1 → devague-0.6.0}/pyproject.toml +1 -1
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_moves.py +32 -0
- devague-0.6.0/tests/test_cli_question.py +70 -0
- devague-0.6.0/tests/test_cli_review.py +153 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_render.py +20 -0
- devague-0.6.0/tests/test_review_loop_integration.py +66 -0
- devague-0.6.0/tests/test_review_loop_invariants.py +72 -0
- {devague-0.5.1 → devague-0.6.0}/uv.lock +1 -1
- devague-0.5.1/README.md +0 -41
- devague-0.5.1/devague/cli/_commands/confirm.py +0 -38
- devague-0.5.1/devague/cli/_commands/reject.py +0 -19
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/think/scripts/think.sh +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/.claude/skills.local.yaml.example +0 -0
- {devague-0.5.1 → devague-0.6.0}/.devague/current_plan +0 -0
- {devague-0.5.1 → devague-0.6.0}/.devague/frames/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.5.1 → devague-0.6.0}/.devague/frames/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.5.1 → devague-0.6.0}/.devague/plans/devague-0-6-0-ships-the-human-review-loop-devague.json +0 -0
- {devague-0.5.1 → devague-0.6.0}/.devague/plans/devague-now-ships-a-documented-spec-contract-every.json +0 -0
- {devague-0.5.1 → devague-0.6.0}/.flake8 +0 -0
- {devague-0.5.1 → devague-0.6.0}/.github/workflows/publish.yml +0 -0
- {devague-0.5.1 → devague-0.6.0}/.github/workflows/security-checks.yml +0 -0
- {devague-0.5.1 → devague-0.6.0}/.github/workflows/tests.yml +0 -0
- {devague-0.5.1 → devague-0.6.0}/.gitignore +0 -0
- {devague-0.5.1 → devague-0.6.0}/.markdownlint-cli2.yaml +0 -0
- {devague-0.5.1 → devague-0.6.0}/.pre-commit-config.yaml +0 -0
- {devague-0.5.1 → devague-0.6.0}/LICENSE +0 -0
- {devague-0.5.1 → devague-0.6.0}/culture.yaml +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/__init__.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/__main__.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/__init__.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/capture.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/converge.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/explain.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/export.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/interrogate.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/learn.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/list_frames.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/new.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/park.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/plan.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_commands/show.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_errors.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_frames.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_output.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/cli/_plans.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/convergence.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/frame.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/plan.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/plan_convergence.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/plan_store.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/render/frame_md.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/render/plan_md.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/devague/render/spec_md.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/examples/contract-example.json +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/plans/devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/plans/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/reviews/spec-contract-frame-review.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/skill-sources.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/spec-contract.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/specs/devague-0-6-0-ships-the-human-review-loop-devague.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/specs/devague-now-ships-a-documented-spec-contract-every.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/plans/2026-05-22-specifix-onboarding.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/plans/2026-05-23-devague-rename.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/plans/2026-05-23-devague-working-backwards-engine.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/specs/2026-05-22-specifix-onboarding-design.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/specs/2026-05-23-devague-spec-to-plan-design.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/docs/superpowers/specs/2026-05-23-devague-working-backwards-design.md +0 -0
- {devague-0.5.1 → devague-0.6.0}/sonar-project.properties +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/__init__.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_affordances.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_chassis.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_converge_export.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_errors.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_output.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_cli_plan.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_contract.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_convergence.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_frame.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_offline.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_package.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_plan.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_plan_convergence.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_plan_store.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_render_plan.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_spec_to_plan_skill.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_store.py +0 -0
- {devague-0.5.1 → devague-0.6.0}/tests/test_think_skill.py +0 -0
|
@@ -58,7 +58,9 @@ portable resolution and the `status` helper.
|
|
|
58
58
|
| `new "<announcement>"` | Start a frame from the announcement (the first move). Seeds an auto-confirmed `announcement` claim. |
|
|
59
59
|
| `capture --kind <kind> "<text>"` | Record + classify a claim. `--origin llm` lands it as `proposed`. |
|
|
60
60
|
| `interrogate <id> --honesty "…"` | Attach an honesty condition (what must be true). Also `--hard-question`, `--risk`, `--contradicts`, `--blocking`. |
|
|
61
|
-
| `confirm <id
|
|
61
|
+
| `confirm <id> [<id>…]` / `reject <id> [<id>…]` | Resolve one or more claims (`c*`) / honesty conditions (`h*`) in one **transactional** call. **User-only decision.** Also `confirm --from-review <file>` to apply an edited review artifact. |
|
|
62
|
+
| `review` | List every **proposed** (unconfirmed) claim + honesty condition with ids (`--json` too); writes a non-authoritative artifact to `.devague/reviews/<slug>.md`. Un-gated; never mutates. |
|
|
63
|
+
| `question "<text>"` | Record / list / `--resolve` a pending user decision as durable working state in `.devague/questions/<slug>.md`. |
|
|
62
64
|
| `park "<text>" --kind <kind>` | Move uncertainty into first-class open vagueness instead of forcing an answer. |
|
|
63
65
|
| `converge` | Evaluate the gate; list remaining gaps. |
|
|
64
66
|
| `export` | Write the buildable spec to `docs/specs/` — only after `converge` passes. |
|
|
@@ -5,6 +5,21 @@ 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.6.0] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Human Review Loop (#17).** Makes the user-only confirmation step ergonomic at scale, preserving the anti-fabrication guarantee.
|
|
13
|
+
- `devague review` (+ `--json`) lists every proposed (unconfirmed) claim and honesty condition with ids — un-gated by convergence and without mutating state — and persists a non-authoritative artifact to `.devague/reviews/<slug>.md`.
|
|
14
|
+
- `confirm` / `reject` now accept multiple ids in one transactional call (any unknown id ⇒ nothing changes).
|
|
15
|
+
- `confirm --from-review <file>` applies a reviewed decision set: each item is emitted with a `pending` marker the human edits to `confirm`/`reject`; `pending` lines are never auto-confirmed (round-trippable artifact).
|
|
16
|
+
- `devague question` records / lists / resolves pending user decisions as durable working state in `.devague/questions/<slug>.md`.
|
|
17
|
+
- devague manages `.gitignore` so `.devague/reviews/` and `.devague/questions/` stay uncommitted working state by default.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `confirm --json` now emits `{confirmed, rejected}` (lists) instead of `{id, status}`, reflecting the multi-id, transactional batch.
|
|
22
|
+
|
|
8
23
|
## [0.5.1] - 2026-05-23
|
|
9
24
|
|
|
10
25
|
### Added
|
|
@@ -4,6 +4,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
+
**Human Review Loop landed (0.6.0, #17).** `devague review` (+ `--json`) lists
|
|
8
|
+
every proposed claim + honesty condition with ids — un-gated by convergence,
|
|
9
|
+
never mutating state — and writes a non-authoritative artifact to
|
|
10
|
+
`.devague/reviews/<slug>.md`. `confirm` / `reject` now take multiple ids in one
|
|
11
|
+
**transactional** call; `confirm --from-review <file>` applies an edited review
|
|
12
|
+
artifact (`pending` lines are never auto-confirmed). `devague question` records
|
|
13
|
+
pending decisions in `.devague/questions/<slug>.md`. devague manages
|
|
14
|
+
`.gitignore` so `reviews/` and `questions/` stay uncommitted working state.
|
|
15
|
+
|
|
7
16
|
**Spec contract landed (#5).** The entity model is documented in
|
|
8
17
|
`docs/spec-contract.md` (the source of truth for kinds, the `(state × origin)`
|
|
9
18
|
vocabulary, the structured convergence result, and the per-move I/O contract).
|
|
@@ -16,8 +25,9 @@ required_next_moves}` (plans: `ready_for_plan`) — a hard break from the old
|
|
|
16
25
|
**Spec→plan engine landed (v0.4.0).** Both deterministic engines now ship.
|
|
17
26
|
The **frame engine** (idea→spec) — Frame domain model, JSON store, convergence
|
|
18
27
|
gate, renderer registry, and the flat moves `new` / `capture` / `interrogate` /
|
|
19
|
-
`confirm` / `reject` / `
|
|
20
|
-
`learn` / `explain`. The **plan engine** (spec→plan) is its
|
|
28
|
+
`confirm` / `reject` / `review` / `question` / `park` / `converge` / `export` /
|
|
29
|
+
`show` / `list` / `learn` / `explain`. The **plan engine** (spec→plan) is its
|
|
30
|
+
structural peer:
|
|
21
31
|
`devague/plan.py`, `plan_convergence.py`, `plan_store.py`, `render/plan_md.py`,
|
|
22
32
|
and the nested group `devague plan <move>` (`new` / `task` / `accept` / `depend`
|
|
23
33
|
/ `cover` / `confirm` / `reject` / `risk` / `converge` / `export` / `show` /
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devague
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -49,6 +49,50 @@ devague --version
|
|
|
49
49
|
Run `devague learn` (or `devague plan learn`) to learn the method, and `devague
|
|
50
50
|
explain <move>` for any single move.
|
|
51
51
|
|
|
52
|
+
## Human Review Loop
|
|
53
|
+
|
|
54
|
+
LLM-proposed claims and honesty conditions stay `proposed` until **you**
|
|
55
|
+
confirm them — that anti-fabrication rule is the point of the method. The review
|
|
56
|
+
loop makes that human step ergonomic at scale:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
devague review # list every proposed (unconfirmed) item, with ids
|
|
60
|
+
devague review --json # same, structured
|
|
61
|
+
devague confirm c2 h1 h3 # confirm many ids in one transactional call
|
|
62
|
+
devague reject c4 c5 # reject many ids in one call
|
|
63
|
+
devague confirm --from-review .devague/reviews/<slug>.md # apply an edited review file
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`review` is **not** gated on convergence and never mutates state. It writes a
|
|
67
|
+
durable, explicitly non-authoritative artifact you can review out of band, then
|
|
68
|
+
apply: each item is emitted with a `pending` marker — change it to `confirm` or
|
|
69
|
+
`reject` and feed the file back with `confirm --from-review`. `pending` lines are
|
|
70
|
+
never auto-confirmed; a batch is transactional (one bad id ⇒ nothing changes).
|
|
71
|
+
|
|
72
|
+
Open questions / pending decisions live as durable working state too:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
devague question "should batch confirm be transactional?" # record a pending decision
|
|
76
|
+
devague question --list # review them
|
|
77
|
+
devague question --resolve q1 --decision "yes, transactional"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Applying a resolved decision into the frame stays an explicit move (e.g.
|
|
81
|
+
`devague capture --kind decision "…"` then `devague confirm`).
|
|
82
|
+
|
|
83
|
+
### `.devague/` — what's committed vs working state
|
|
84
|
+
|
|
85
|
+
| Path | Committed? |
|
|
86
|
+
|------|-----------|
|
|
87
|
+
| `.devague/frames/`, `.devague/plans/` | yes — the converged frame/plan state |
|
|
88
|
+
| `.devague/reviews/<slug>.md` | no — local review working state |
|
|
89
|
+
| `.devague/questions/<slug>.md` | no — local pending-decision working state |
|
|
90
|
+
| `.devague/current`, `.devague/current_plan` | no — local pointers |
|
|
91
|
+
|
|
92
|
+
devague keeps `reviews/` and `questions/` out of git for you (it manages
|
|
93
|
+
`.gitignore`). Promote one into `docs/` only if you intentionally want it
|
|
94
|
+
committed.
|
|
95
|
+
|
|
52
96
|
## Driving it from an agent
|
|
53
97
|
|
|
54
98
|
Inside AgentCulture, an assistant drives this CLI through two operator skills —
|
devague-0.6.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# devague
|
|
2
|
+
|
|
3
|
+
**`devague` is a command-line tool** that turns a vague feature idea into a
|
|
4
|
+
buildable **spec**, then that spec into a buildable **plan** — by working
|
|
5
|
+
backwards, then forwards. It is a small, deterministic Python CLI (no LLM calls
|
|
6
|
+
inside it, fully unit-tested) — not an agent, service, or daemon. You install it
|
|
7
|
+
and run `devague` from the repository you are speccing; state is plain JSON under
|
|
8
|
+
`.devague/`.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
vague idea ──▶ buildable spec ──▶ buildable plan ──▶ build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv tool install devague # or: pipx install devague / pip install devague
|
|
18
|
+
devague --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Two engines, one CLI
|
|
22
|
+
|
|
23
|
+
- **Frame engine** (idea→spec) — start from the announcement ("pretend it
|
|
24
|
+
shipped"), capture and pressure-test claims, park open vagueness, and `export`
|
|
25
|
+
a spec only once the frame *converges*. Flat verbs: `devague new` /
|
|
26
|
+
`capture` / `interrogate` / `confirm` / `converge` / `export` / …
|
|
27
|
+
- **Plan engine** (spec→plan) — seed a plan from a converged frame, cover every
|
|
28
|
+
target with tasks that carry acceptance criteria and an acyclic dependency
|
|
29
|
+
order, and `export` a plan only once it *converges*. Nested group:
|
|
30
|
+
`devague plan new` / `task` / `cover` / `converge` / `export` / …
|
|
31
|
+
|
|
32
|
+
Run `devague learn` (or `devague plan learn`) to learn the method, and `devague
|
|
33
|
+
explain <move>` for any single move.
|
|
34
|
+
|
|
35
|
+
## Human Review Loop
|
|
36
|
+
|
|
37
|
+
LLM-proposed claims and honesty conditions stay `proposed` until **you**
|
|
38
|
+
confirm them — that anti-fabrication rule is the point of the method. The review
|
|
39
|
+
loop makes that human step ergonomic at scale:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
devague review # list every proposed (unconfirmed) item, with ids
|
|
43
|
+
devague review --json # same, structured
|
|
44
|
+
devague confirm c2 h1 h3 # confirm many ids in one transactional call
|
|
45
|
+
devague reject c4 c5 # reject many ids in one call
|
|
46
|
+
devague confirm --from-review .devague/reviews/<slug>.md # apply an edited review file
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`review` is **not** gated on convergence and never mutates state. It writes a
|
|
50
|
+
durable, explicitly non-authoritative artifact you can review out of band, then
|
|
51
|
+
apply: each item is emitted with a `pending` marker — change it to `confirm` or
|
|
52
|
+
`reject` and feed the file back with `confirm --from-review`. `pending` lines are
|
|
53
|
+
never auto-confirmed; a batch is transactional (one bad id ⇒ nothing changes).
|
|
54
|
+
|
|
55
|
+
Open questions / pending decisions live as durable working state too:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
devague question "should batch confirm be transactional?" # record a pending decision
|
|
59
|
+
devague question --list # review them
|
|
60
|
+
devague question --resolve q1 --decision "yes, transactional"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Applying a resolved decision into the frame stays an explicit move (e.g.
|
|
64
|
+
`devague capture --kind decision "…"` then `devague confirm`).
|
|
65
|
+
|
|
66
|
+
### `.devague/` — what's committed vs working state
|
|
67
|
+
|
|
68
|
+
| Path | Committed? |
|
|
69
|
+
|------|-----------|
|
|
70
|
+
| `.devague/frames/`, `.devague/plans/` | yes — the converged frame/plan state |
|
|
71
|
+
| `.devague/reviews/<slug>.md` | no — local review working state |
|
|
72
|
+
| `.devague/questions/<slug>.md` | no — local pending-decision working state |
|
|
73
|
+
| `.devague/current`, `.devague/current_plan` | no — local pointers |
|
|
74
|
+
|
|
75
|
+
devague keeps `reviews/` and `questions/` out of git for you (it manages
|
|
76
|
+
`.gitignore`). Promote one into `docs/` only if you intentionally want it
|
|
77
|
+
committed.
|
|
78
|
+
|
|
79
|
+
## Driving it from an agent
|
|
80
|
+
|
|
81
|
+
Inside AgentCulture, an assistant drives this CLI through two operator skills —
|
|
82
|
+
**`/think`** (idea→spec) and **`/spec-to-plan`** (spec→plan) — which add a
|
|
83
|
+
portable wrapper and a `status` next-move helper over the convergence gate. The
|
|
84
|
+
CLI is the deterministic affordance; the agent decides the next move. See
|
|
85
|
+
`CLAUDE.md` for that workflow and `docs/superpowers/specs/` for the design docs.
|
|
@@ -65,7 +65,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
65
65
|
from devague.cli._commands import new as _new_cmd
|
|
66
66
|
from devague.cli._commands import park as _park_cmd
|
|
67
67
|
from devague.cli._commands import plan as _plan_cmd
|
|
68
|
+
from devague.cli._commands import question as _question_cmd
|
|
68
69
|
from devague.cli._commands import reject as _reject_cmd
|
|
70
|
+
from devague.cli._commands import review as _review_cmd
|
|
69
71
|
from devague.cli._commands import show as _show_cmd
|
|
70
72
|
|
|
71
73
|
_learn_cmd.register(sub)
|
|
@@ -75,6 +77,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
75
77
|
_interrogate_cmd.register(sub)
|
|
76
78
|
_confirm_cmd.register(sub)
|
|
77
79
|
_reject_cmd.register(sub)
|
|
80
|
+
_review_cmd.register(sub)
|
|
81
|
+
_question_cmd.register(sub)
|
|
78
82
|
_park_cmd.register(sub)
|
|
79
83
|
_converge_cmd.register(sub)
|
|
80
84
|
_export_cmd.register(sub)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""``devague confirm`` — confirm claims/honesty conditions (user-only transition).
|
|
2
|
+
|
|
3
|
+
Accepts one or more ids in a single call, or a reviewed decision set via
|
|
4
|
+
``--from-review <file>`` (apply the confirm/reject markers a human edited into a
|
|
5
|
+
``devague review`` artifact). Either way the batch is **transactional**: every
|
|
6
|
+
id is validated first, and if any is unknown nothing is changed. Confirmation
|
|
7
|
+
stays a user-only action, and ``--from-review`` applies only what the file
|
|
8
|
+
explicitly marks — ``pending`` lines are never auto-confirmed. See issue #17.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from devague import store
|
|
17
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
18
|
+
from devague.cli._frames import resolve
|
|
19
|
+
from devague.cli._output import emit_result
|
|
20
|
+
from devague.render.review_md import parse_decisions
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _exists(frame, item_id: str) -> bool:
|
|
24
|
+
return frame.find_claim(item_id) is not None or frame.find_honesty(item_id) is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(args: argparse.Namespace, confirm_ids: list[str], reject_ids: list[str]) -> int:
|
|
28
|
+
frame = resolve(args.frame)
|
|
29
|
+
all_ids = confirm_ids + reject_ids
|
|
30
|
+
if not all_ids:
|
|
31
|
+
raise DevagueError(EXIT_USER_ERROR, "no ids to resolve", "pass at least one id")
|
|
32
|
+
unknown = [i for i in all_ids if not _exists(frame, i)]
|
|
33
|
+
if unknown:
|
|
34
|
+
raise DevagueError(
|
|
35
|
+
EXIT_USER_ERROR,
|
|
36
|
+
f"no such claim or honesty condition: {', '.join(unknown)}",
|
|
37
|
+
"run 'devague show'; the batch is transactional — nothing was changed",
|
|
38
|
+
)
|
|
39
|
+
for item_id in confirm_ids:
|
|
40
|
+
frame.set_status(item_id, "confirmed")
|
|
41
|
+
for item_id in reject_ids:
|
|
42
|
+
frame.set_status(item_id, "rejected")
|
|
43
|
+
store.save(frame)
|
|
44
|
+
if getattr(args, "json", False):
|
|
45
|
+
emit_result({"confirmed": confirm_ids, "rejected": reject_ids}, json_mode=True)
|
|
46
|
+
else:
|
|
47
|
+
lines = [f"{i} -> confirmed" for i in confirm_ids]
|
|
48
|
+
lines += [f"{i} -> rejected" for i in reject_ids]
|
|
49
|
+
emit_result("\n".join(lines), json_mode=False)
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _from_review(path: str) -> tuple[list[str], list[str]]:
|
|
54
|
+
try:
|
|
55
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
56
|
+
except OSError as err:
|
|
57
|
+
raise DevagueError(EXIT_USER_ERROR, f"cannot read review file: {path}", str(err))
|
|
58
|
+
try:
|
|
59
|
+
decisions = parse_decisions(text)
|
|
60
|
+
except ValueError as err:
|
|
61
|
+
raise DevagueError(EXIT_USER_ERROR, str(err), "fix the conflicting decision and retry")
|
|
62
|
+
confirm_ids = [i for i, d in decisions.items() if d == "confirm"]
|
|
63
|
+
reject_ids = [i for i, d in decisions.items() if d == "reject"]
|
|
64
|
+
if not confirm_ids and not reject_ids:
|
|
65
|
+
raise DevagueError(
|
|
66
|
+
EXIT_USER_ERROR,
|
|
67
|
+
"no decisions found in review file",
|
|
68
|
+
"change a line's 'pending' to 'confirm' or 'reject' first",
|
|
69
|
+
)
|
|
70
|
+
return confirm_ids, reject_ids
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_confirm(args: argparse.Namespace) -> int:
|
|
74
|
+
if args.from_review:
|
|
75
|
+
if args.ids:
|
|
76
|
+
raise DevagueError(
|
|
77
|
+
EXIT_USER_ERROR,
|
|
78
|
+
"pass ids or --from-review, not both",
|
|
79
|
+
"drop the positional ids when applying a review file",
|
|
80
|
+
)
|
|
81
|
+
confirm_ids, reject_ids = _from_review(args.from_review)
|
|
82
|
+
return _run(args, confirm_ids, reject_ids)
|
|
83
|
+
return _run(args, list(args.ids), [])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_reject(args: argparse.Namespace) -> int:
|
|
87
|
+
return _run(args, [], list(args.ids))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
91
|
+
p = sub.add_parser("confirm", help="Confirm claims/honesty conditions, or apply a review file.")
|
|
92
|
+
p.add_argument("ids", nargs="*", help="One or more claim ids (c*) or honesty ids (h*).")
|
|
93
|
+
p.add_argument("--from-review", help="Apply confirm/reject decisions from a review file.")
|
|
94
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
95
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
96
|
+
p.set_defaults(func=cmd_confirm)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""``devague question`` — record/list/resolve pending user decisions.
|
|
2
|
+
|
|
3
|
+
Durable, uncommitted working state under .devague/questions/<slug>.md. The CLI
|
|
4
|
+
owns the format (decision c20). Nothing here auto-resolves: ``--resolve`` only
|
|
5
|
+
records a decision a human made; applying it into the frame is a separate,
|
|
6
|
+
explicit move (see the file header). Issue #14/#17.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
from devague import questions_io, store
|
|
14
|
+
from devague.cli._errors import EXIT_USER_ERROR, DevagueError
|
|
15
|
+
from devague.cli._frames import resolve
|
|
16
|
+
from devague.cli._output import emit_diagnostic, emit_result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load(slug: str) -> list[dict]:
|
|
20
|
+
path = store.questions_path(slug)
|
|
21
|
+
return questions_io.parse(path.read_text(encoding="utf-8")) if path.exists() else []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_question(args: argparse.Namespace, slug: str, items: list[dict]) -> None:
|
|
25
|
+
target = next((i for i in items if i["id"] == args.resolve), None)
|
|
26
|
+
if target is None:
|
|
27
|
+
raise DevagueError(
|
|
28
|
+
EXIT_USER_ERROR,
|
|
29
|
+
f"no such question: {args.resolve}",
|
|
30
|
+
"run 'devague question --list'",
|
|
31
|
+
)
|
|
32
|
+
target["resolved"] = True
|
|
33
|
+
target["decision"] = args.decision
|
|
34
|
+
store.write_questions(slug, questions_io.render(slug, items))
|
|
35
|
+
if args.json:
|
|
36
|
+
emit_result({"id": args.resolve, "resolved": True}, json_mode=True)
|
|
37
|
+
else:
|
|
38
|
+
emit_result(f"{args.resolve} -> resolved", json_mode=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _add_question(args: argparse.Namespace, slug: str, items: list[dict]) -> None:
|
|
42
|
+
new_id = questions_io.next_id(items)
|
|
43
|
+
items.append({"id": new_id, "text": args.text, "resolved": False, "decision": None})
|
|
44
|
+
path = store.write_questions(slug, questions_io.render(slug, items))
|
|
45
|
+
if args.json:
|
|
46
|
+
emit_result({"id": new_id, "text": args.text, "path": str(path)}, json_mode=True)
|
|
47
|
+
else:
|
|
48
|
+
emit_result(f"recorded {new_id}", json_mode=False)
|
|
49
|
+
emit_diagnostic(f"wrote pending decision to {path} (uncommitted working state)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _list_questions(args: argparse.Namespace, slug: str, items: list[dict]) -> None:
|
|
53
|
+
if args.json:
|
|
54
|
+
emit_result({"slug": slug, "questions": items}, json_mode=True)
|
|
55
|
+
else:
|
|
56
|
+
emit_result(questions_io.render(slug, items), json_mode=False)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_question(args: argparse.Namespace) -> int:
|
|
60
|
+
frame = resolve(args.frame) # validates the frame exists; never mutates it
|
|
61
|
+
slug = frame.slug
|
|
62
|
+
items = _load(slug)
|
|
63
|
+
if args.resolve:
|
|
64
|
+
_resolve_question(args, slug, items)
|
|
65
|
+
elif args.text:
|
|
66
|
+
_add_question(args, slug, items)
|
|
67
|
+
else: # default / --list: show the pending-decisions state
|
|
68
|
+
_list_questions(args, slug, items)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
73
|
+
p = sub.add_parser("question", help="Record / list / resolve pending user decisions.")
|
|
74
|
+
p.add_argument("text", nargs="?", help="Question text to record (omit to list).")
|
|
75
|
+
p.add_argument("--resolve", metavar="QID", help="Mark a question id resolved.")
|
|
76
|
+
p.add_argument("--decision", help="The decision note recorded with --resolve.")
|
|
77
|
+
p.add_argument("--list", action="store_true", help="List pending decisions (default).")
|
|
78
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
79
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
80
|
+
p.set_defaults(func=cmd_question)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""``devague reject`` — reject one or more claims / honesty conditions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from devague.cli._commands.confirm import cmd_reject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
11
|
+
p = sub.add_parser("reject", help="Reject one or more claims / honesty conditions.")
|
|
12
|
+
p.add_argument("ids", nargs="+", help="One or more claim ids (c*) or honesty ids (h*).")
|
|
13
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
14
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
15
|
+
p.set_defaults(func=cmd_reject)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""``devague review`` — surface every *proposed* (unconfirmed) item for human review.
|
|
2
|
+
|
|
3
|
+
Lists proposed claims and proposed honesty conditions with their ids. It is
|
|
4
|
+
deliberately NOT gated on convergence and never mutates state — it is a
|
|
5
|
+
read-only view of what is awaiting a user decision (issue #17). Confirm/reject
|
|
6
|
+
the listed ids with ``devague confirm`` / ``devague reject``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
from devague import render, store
|
|
14
|
+
from devague.cli._frames import resolve
|
|
15
|
+
from devague.cli._output import emit_diagnostic, emit_result
|
|
16
|
+
from devague.render.review_md import proposed_claims, proposed_honesty
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _json_payload(frame) -> dict:
|
|
20
|
+
return {
|
|
21
|
+
"slug": frame.slug,
|
|
22
|
+
"proposed_claims": [
|
|
23
|
+
{"id": c.id, "kind": c.kind, "text": c.text} for c in proposed_claims(frame)
|
|
24
|
+
],
|
|
25
|
+
"proposed_honesty": [
|
|
26
|
+
{"id": h.id, "claim_id": c.id, "claim_kind": c.kind, "text": h.text}
|
|
27
|
+
for c, h in proposed_honesty(frame)
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_review(args: argparse.Namespace) -> int:
|
|
33
|
+
frame = resolve(args.frame) # read-only: the frame is never saved/converged
|
|
34
|
+
artifact = render.render(frame, "review-md")
|
|
35
|
+
path = None
|
|
36
|
+
if not getattr(args, "no_write", False):
|
|
37
|
+
# Persist a durable, NON-authoritative artifact to uncommitted working
|
|
38
|
+
# state (.devague/reviews/<slug>.md) — distinct from docs/specs/.
|
|
39
|
+
path = store.write_review(frame.slug, artifact)
|
|
40
|
+
if getattr(args, "json", False):
|
|
41
|
+
payload = _json_payload(frame)
|
|
42
|
+
payload["path"] = str(path) if path else None
|
|
43
|
+
emit_result(payload, json_mode=True)
|
|
44
|
+
else:
|
|
45
|
+
emit_result(artifact, json_mode=False)
|
|
46
|
+
if path:
|
|
47
|
+
emit_diagnostic(f"wrote review artifact to {path} (unconfirmed, not authoritative)")
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
52
|
+
p = sub.add_parser(
|
|
53
|
+
"review",
|
|
54
|
+
help="List proposed (unconfirmed) claims + honesty conditions for human review.",
|
|
55
|
+
)
|
|
56
|
+
p.add_argument("--frame", help="Frame slug (default: current).")
|
|
57
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--no-write",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Print only; do not persist .devague/reviews/<slug>.md.",
|
|
62
|
+
)
|
|
63
|
+
p.set_defaults(func=cmd_review)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Pending-decision working state: ``.devague/questions/<slug>.md`` (CLI-owned).
|
|
2
|
+
|
|
3
|
+
A durable, **uncommitted** list of open questions / pending user decisions raised
|
|
4
|
+
while working a frame (issue #14/#17, decision c20). The CLI owns the markdown
|
|
5
|
+
format so it round-trips (``parse`` ⇄ ``render``). A resolved decision is applied
|
|
6
|
+
back into the frame with the normal moves (e.g. ``devague capture --kind decision
|
|
7
|
+
"<the decision>"`` then ``devague confirm``); resolving here only records it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
# Bounded whitespace only (single literal spaces, rest stripped in parse) so the
|
|
15
|
+
# pattern is linear — no \s+…\s+…\.* shape that trips ReDoS heuristics.
|
|
16
|
+
_LINE = re.compile(r"^- \[(?P<mark>[ x])\] `(?P<id>q\d+)`:(?P<rest>.*)$")
|
|
17
|
+
_DECIDED = " — decided: "
|
|
18
|
+
|
|
19
|
+
_BANNER = (
|
|
20
|
+
"> **Working state — not committed by default.** Open questions / pending "
|
|
21
|
+
"decisions for this frame. Apply a decision into the frame with the normal "
|
|
22
|
+
'moves (e.g. `devague capture --kind decision "…"` then `devague confirm`), '
|
|
23
|
+
"then mark it resolved here with `devague question --resolve <id>`."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse(text: str) -> list[dict]:
|
|
28
|
+
items: list[dict] = []
|
|
29
|
+
for line in text.splitlines():
|
|
30
|
+
m = _LINE.match(line.strip())
|
|
31
|
+
if not m:
|
|
32
|
+
continue
|
|
33
|
+
rest = m.group("rest")
|
|
34
|
+
resolved = m.group("mark") == "x"
|
|
35
|
+
decision = None
|
|
36
|
+
if resolved and _DECIDED in rest:
|
|
37
|
+
# render() appends the decided tail last, so split from the right —
|
|
38
|
+
# otherwise a question whose *text* contains the delimiter corrupts.
|
|
39
|
+
rest, decision = rest.rsplit(_DECIDED, 1)
|
|
40
|
+
items.append(
|
|
41
|
+
{"id": m.group("id"), "text": rest.strip(), "resolved": resolved, "decision": decision}
|
|
42
|
+
)
|
|
43
|
+
return items
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def next_id(items: list[dict]) -> str:
|
|
47
|
+
nums = [int(i["id"][1:]) for i in items if i["id"][1:].isdigit()]
|
|
48
|
+
return f"q{(max(nums) + 1) if nums else 1}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render(slug: str, items: list[dict]) -> str:
|
|
52
|
+
out = [f"# Pending decisions — {slug}", "", _BANNER, ""]
|
|
53
|
+
open_items = [i for i in items if not i["resolved"]]
|
|
54
|
+
done_items = [i for i in items if i["resolved"]]
|
|
55
|
+
out += ["## Open", ""]
|
|
56
|
+
out += [f"- [ ] `{i['id']}`: {i['text']}" for i in open_items] or ["None."]
|
|
57
|
+
out += [""]
|
|
58
|
+
if done_items:
|
|
59
|
+
out += ["## Resolved", ""]
|
|
60
|
+
for i in done_items:
|
|
61
|
+
tail = f"{_DECIDED}{i['decision']}" if i["decision"] else ""
|
|
62
|
+
out.append(f"- [x] `{i['id']}`: {i['text']}{tail}")
|
|
63
|
+
out += [""]
|
|
64
|
+
return "\n".join(out).rstrip() + "\n"
|
|
@@ -33,7 +33,9 @@ def render(frame: Frame, fmt: str) -> str:
|
|
|
33
33
|
|
|
34
34
|
# Register the built-in renderers (import-time side effect).
|
|
35
35
|
from devague.render import frame_md as _frame_md # noqa: E402
|
|
36
|
+
from devague.render import review_md as _review_md # noqa: E402
|
|
36
37
|
from devague.render import spec_md as _spec_md # noqa: E402
|
|
37
38
|
|
|
38
39
|
register("frame-md", _frame_md.render_frame)
|
|
39
40
|
register("spec-md", _spec_md.render_spec)
|
|
41
|
+
register("review-md", _review_md.render_review)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Renderer + parser for the human-review artifact — every *proposed* item.
|
|
2
|
+
|
|
3
|
+
Explicitly NON-authoritative and distinct from spec-md: it exists so a human can
|
|
4
|
+
review LLM proposals (in-terminal or out of band) before confirming any. Nothing
|
|
5
|
+
here is authoritative until the user runs ``devague confirm``. See issue #17.
|
|
6
|
+
|
|
7
|
+
The artifact is **round-trippable**: each proposed item is emitted with a leading
|
|
8
|
+
``pending`` marker. Edit a line's marker to ``confirm`` or ``reject``, then feed
|
|
9
|
+
the file to ``devague confirm --from-review <file>`` to apply exactly those
|
|
10
|
+
decisions (``render_review`` ⇄ :func:`parse_decisions`).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from devague.frame import Frame
|
|
18
|
+
|
|
19
|
+
# A decision line: "- <marker> `<id>` ...". The marker is the only editable part.
|
|
20
|
+
_DECISIONS = ("pending", "confirm", "reject")
|
|
21
|
+
_LINE = re.compile(r"^- (?P<decision>pending|confirm|reject)\s+`(?P<id>[ch]\d+)`")
|
|
22
|
+
|
|
23
|
+
_BANNER = (
|
|
24
|
+
"> **Review artifact — nothing confirmed yet.** These are unconfirmed, "
|
|
25
|
+
"LLM-proposed items; they are NOT authoritative and NOT a buildable spec. "
|
|
26
|
+
"To apply, change a line's `pending` to `confirm` or `reject`, then run "
|
|
27
|
+
"`devague confirm --from-review <file>` (or `devague confirm <id> ...`)."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def proposed_claims(frame: Frame) -> list:
|
|
32
|
+
return [c for c in frame.claims if c.status == "proposed"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def proposed_honesty(frame: Frame) -> list:
|
|
36
|
+
return [(c, h) for c in frame.claims for h in c.honesty_conditions if h.status == "proposed"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def render_review(frame: Frame) -> str:
|
|
40
|
+
out: list[str] = [f"# Review — {frame.title}", "", _BANNER, ""]
|
|
41
|
+
claims = proposed_claims(frame)
|
|
42
|
+
pairs = proposed_honesty(frame)
|
|
43
|
+
if not claims and not pairs:
|
|
44
|
+
out += ["No proposed items — nothing awaiting review.", ""]
|
|
45
|
+
return "\n".join(out).rstrip() + "\n"
|
|
46
|
+
if claims:
|
|
47
|
+
out += ["## Proposed claims", ""]
|
|
48
|
+
out += [f"- pending `{c.id}` ({c.kind}): {c.text}" for c in claims]
|
|
49
|
+
out += [""]
|
|
50
|
+
if pairs:
|
|
51
|
+
out += ["## Proposed honesty conditions", ""]
|
|
52
|
+
out += [f"- pending `{h.id}` (on `{c.id}` {c.kind}): {h.text}" for c, h in pairs]
|
|
53
|
+
out += [""]
|
|
54
|
+
return "\n".join(out).rstrip() + "\n"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_decisions(text: str) -> dict[str, str]:
|
|
58
|
+
"""Parse a (possibly edited) review artifact into ``{id: "confirm"|"reject"}``.
|
|
59
|
+
|
|
60
|
+
``pending`` lines carry no decision and are skipped — applying a review file
|
|
61
|
+
therefore touches only what the human explicitly marked (no auto-confirm).
|
|
62
|
+
Raises ``ValueError`` on a duplicate, conflicting decision for one id.
|
|
63
|
+
"""
|
|
64
|
+
decisions: dict[str, str] = {}
|
|
65
|
+
for line in text.splitlines():
|
|
66
|
+
m = _LINE.match(line.strip())
|
|
67
|
+
if not m:
|
|
68
|
+
continue
|
|
69
|
+
decision, item_id = m.group("decision"), m.group("id")
|
|
70
|
+
if decision == "pending":
|
|
71
|
+
continue
|
|
72
|
+
if item_id in decisions and decisions[item_id] != decision:
|
|
73
|
+
prior = decisions[item_id]
|
|
74
|
+
raise ValueError(f"conflicting decisions for {item_id}: {prior} vs {decision}")
|
|
75
|
+
decisions[item_id] = decision
|
|
76
|
+
return decisions
|