park --resolve / plan risk --resolve: the close-out for parked vagueness (0.20.0, #45 #55 #57 #60) - #81
Conversation
Frame + plan state and exported artifacts, produced via scope -> think -> challenge -> spec-to-plan. Supersedes the resolve-parked-vagueness branch's flat-verb spec: q1 decided park --resolve (no new verb), q2 pulled in the plan-side twin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
Add Vagueness.resolved/resolution (default False/""), Frame.resolve_vagueness (fails closed on unknown or already-resolved ids), and bump SCHEMA_VERSION to 3. set_status stays untouched — v-ids stay out of confirm/reject (decision c11). A v2 frame JSON without the new keys loads with defaults via the existing Vagueness(**v) splat in from_dict (dataclass defaults apply), and store.load still fails closed above the current version. Also fixes two pre-existing hardcoded schema_version pins (tests/test_store.py, tests/test_frame_schema_v2.py) that this legitimate bump made false, so the full suite stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
Mirror t1's frame-side Vagueness.resolved/resolution exactly on the plan engine: PlanRisk gains resolved: bool = False and resolution: str = "", Plan.resolve_risk(rid, resolution) raises ValueError on an unknown or already-resolved id, and PLAN_SCHEMA_VERSION bumps 2 -> 3. PlanRisk(**r) in from_dict already defaults missing keys via dataclass field defaults, so a v2 plan JSON without the new keys loads unchanged. Covers: c13, h9
…solve _missing_open_uncertainty no longer counts a resolved unknown_blocking vagueness as a blocker, and _parked_items drops any resolved item (not just unknown_blocking) from the tracked-open list — a resolved item is closed, not open. suggest_move's blocking-vagueness branch now emits the executable devague park --resolve VID --decision "<the decision>" move in place of the old dead-end "capture+confirm the answer, or re-park it as non-blocking" hint, which could never actually clear the blocker. Covers c3, h3, c18, h14. Method-only change to devague/convergence.py and its tests; no renderer, CLI, plan_convergence.py, or .devague state touched.
Mirrors t5's park --resolve semantics on the risk subcommand: positional text and --kind become optional at the parser level so --resolve can omit both, the create path refuses a missing --kind (or text) in the handler, and --resolve routes through Plan.resolve_risk with the same fail-closed contract — bare --resolve without --decision persists nothing, unknown and already-resolved ids are refused with a hint, and the transition echoes on stdout with --json parity.
Mirrors t3's frame-side change on the plan engine: `_missing_risks` and `_parked_items` in devague/plan_convergence.py now filter `r.resolved`, so a resolved `unknown_blocking` PlanRisk no longer blocks `devague plan converge` and no longer advertises as parked. The blocking-risk hint in `suggest_move` now names the executable `devague plan risk --resolve RID --decision TEXT` syntax verbatim instead of the old vague "cover it with a task, or re-record it as non-blocking" text. Adds 6 tests to tests/test_plan_convergence.py covering: unresolved risk still blocks (baseline), resolved risk unblocks convergence, resolved risk drops out of required_next_moves, the hint's verbatim executable syntax (direct + end-to-end via evaluate()), and resolved risk exclusion from parked_items. Full suite: 640 passed. black/isort/flake8 clean.
Harmonizes the plan-side hint with t3's frame-side shape — the hint now names the real rN instead of a literal RID placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
Add a user-only resolve surface for parked vagueness that mirrors question --resolve. park's positional text becomes optional (nargs='?'), mirroring question.py; --resolve VID, --decision TEXT, and --claim CN route through Frame.resolve_vagueness. A bare --resolve without --decision, an unknown/already-resolved vagueness id, positional text combined with --resolve, and an unknown --claim id are all refused with a hint and persist nothing (decision c21, issue #57's refuse-not-no-op answer). Grants a minimal frame.py extension: Vagueness gains resolution_claim_id (the deciding claim recorded at resolve time, distinct from claim_id, the owning claim set at park time) and resolve_vagueness gains an optional claim_id parameter, validated in the model the same way add_scope_entry validates seed ids. Dataclass defaults keep v2/early-v3 artifacts loading with no SCHEMA_VERSION bump. Covers c4, h4. Full suite: 651 passed, 98% coverage.
frame_md keeps the flat "Open vagueness" list but appends "— resolved: TEXT" to a resolved item's bullet; spec_md gains a new "Resolved vagueness" subsection rendering resolved items of any kind with their resolution verbatim (previously only follow_up/out_of_scope kinds were shown at all, and never with resolution text) — a resolved follow_up/ out_of_scope item now moves out of "Open / follow-up" into that section instead of being fabricated as still open. deliverables_md's surviving-open-items filter now excludes any resolved frame vagueness or plan risk, not just unknown_blocking-kind items. An empty resolution renders nothing extra, matching the existing never-fabricate convention (_instruction_lines). Covers c6, h6, c18, h14 (#53-esd t7). Full suite: 651 passed (was 634); black/isort/flake8 clean; new golden fixtures pass real markdownlint-cli2.
Drives devague.cli.main() end to end through issue 57's exact repro on both engines: park a blocking unknown, capture the deciding claim, resolve via park --resolve/plan risk --resolve, assert converge blocks before and passes after, and export renders the resolved item verbatim. Includes a markdownlint integration check on the exported spec (skips cleanly without the binary). Full suite: 687 passed, 98.12% coverage. flake8/black/isort/bandit clean.
…aught devague learn's move table + operating rules, docs/llm-guidance.md's park rows, .claude/skills/think/SKILL.md's move table + hard rules, and docs/spec-contract.md (Vagueness/PlanRisk entities, the resolve move rows, schema_version 3 for both engines) now all name `park --resolve VID --decision TEXT` / `plan risk --resolve RID --decision TEXT` — the fix shipped in t5/t6 for issues 45/55/57/60. A decided blocking park no longer reads as a permanent dead end in any teaching surface. devague plan learn already carried the risk --resolve text from t6; new tests in tests/test_cli_learn.py lock both `devague learn` and `devague plan learn` naming the resolve moves.
#60) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
|
/agentic_review |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
PR Summary by QodoAdd resolve close-out for parked vagueness and plan risks (schema v3)
AI Description
Diagram
High-Level Assessment
Files changed (35)
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyFmS7dRyBj87DpgmDCu9u
|
Code Review by Qodo
Context used✅ Tickets:
🎫 Add a move to reclassify/resolve a parked vagueness (unblocks /think convergence after a dependency is downgraded) 🎫 No verb to resolve parked vagueness (or edit task text) — operators hand-edit .devague state JSON 🎫 No CLI verb to resolve parked vagueness (blocking vagueness can only be removed by editing frame state)✅ Compliance rules (platform):
75 rules✅ Skills:
cicd, communicate 1. llm claims marked confirmed
|
|
Triage of the latest Qodo review (1 bug, 1 rule violation) — both rejected as not-a-defect after verification, consistent with this repo's prior review history: 1. "LLM claims marked confirmed" (rule 738290) — rejected. 2. "Committed plan pointer file" ( No code changes made for either finding.
|
* spec: issue-backlog-sweep — 15-issue sweep frame + exported spec (devague /scope + /think + /challenge) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * plan: issue-backlog-sweep — 19 tasks / 6 waves covering 52 targets (devague /spec-to-plan) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * feat(scope): fan exploration out to smaller-tier read-only subagents (#79, #91, t16) Steps 1-2 of the /scope method now instruct subagent fan-out for a broad survey (5+ candidate surfaces), one read-only exploration subagent per surface or tight cluster, defaulting to a smaller tier (sonnet). 4 or fewer surfaces still explore inline, and the no-wizard escape for small ideas is unchanged. The load-bearing rule survives explicitly in both step 2/4 and the Hard rules section: subagents explore and report only, never running a devague move — the main agent alone runs capture/scope/question/park so provenance and the anti-fabrication contract stay in one place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * fix(plan): validate cover targets against the live frame (#90, t8) `_require_target` only checked the plan's stored coverage-target snapshot, frozen at `plan new`/last `plan converge` time. But `plan status`/`converge` re-derive targets from the LIVE source frame, so a frame that legitimately grows a confirmed claim mid-run left `status` recommending a cover that `cover`/`task --covers` immediately refused as "unknown coverage target" -- the exact #90 repro. `_require_target` now falls back to the live frame when a target is absent from the stored snapshot, and persists the refreshed snapshot on success so the stored copy catches up without a separate `plan converge`. Decision (park v4 / plan risk r2): when the source frame has itself regressed below its own convergence gate, `_live`'s regression error is let through as-is rather than reworded into "unknown coverage target" -- an unverifiable target should say why, not blame the wrong thing. A target already known to the stored snapshot never touches the live frame at all, so it keeps working through a frame regression exactly as before. * fix(store): gate schema_version before parsing; tolerant nested loads (t2) store.load and plan_store.load parsed via from_dict before checking schema_version, so a genuinely newer-schema file crashed with a raw TypeError (HardQuestion(**q) / Vagueness(**v) reject unexpected kwargs) instead of the intended fail-closed IncompatibleSchemaError. Both loaders now read the raw JSON, check schema_version first, and only then build the domain object. HardQuestion/Vagueness construction in frame.from_dict is now tolerant of unknown keys the same way Claim already is, so a future field lands safely under a version bump instead of crashing same-version loads that happen to carry it. Bumps frame.SCHEMA_VERSION 3->4 (reserved for t4's HardQuestion resolution field) and plan.PLAN_SCHEMA_VERSION 3->4 (reserved for t9's per-target deferral state) - this task only hardens load order/tolerance, it does not add either field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * feat(render): identifier-aware markdown escaping for verbatim text (#87, t1) Add md_safe_text() to render/_md_safety.py: wraps underscore/dunder identifiers (_read_file, __init__.py) in code spans per the #87 issue comment's preferred fix (fixes MD037 and MD050 in one move, reads better than backslash-escaping), and backslash-escapes the remaining control characters (*, [, ], a stray backtick, a leading #) as the fallback. Text already inside a code span is left byte-for-byte untouched and double application is a no-op (c32/h25). No renderer call sites yet — those land in t3/t9/t13. * feat(assign-to-workforce): durable gate-2 split artifact (#82, t15) Add `split-plan --write`, producing docs/plans/<created-date>-<slug>-split.md alongside the exported plan-md: real per-task summaries, instructions, and acceptance criteria from `devague plan waves --json`; a Task assignments (Owner/Model) table the script reads back on re-run so a human's edited assignment survives regeneration; and the End state section. Re-running overwrites the same dated path in place. Artifact-only per decision c25 — no plan schema change, no new devague CLI verb; devague plan waves/show/ deliverables stay read-only. SKILL.md documents the flow. * feat(interrogate): --resolve clears a claim's blocking hard question (#48, #52, t4) interrogate <cN> --resolve <qN> [--decision "<text>"] adds Frame.resolve_hard_question and a resolve mode on the interrogate CLI parser (mirrors park --resolve), fixing the permanent convergence deadlock where nothing ever set HardQuestion.resolved. The blocking-question gate also skips rejected claims, and suggest_move now names the shipped move instead of the old dead-end "capture/confirm the resulting claim" hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * fix(summary): scope Planned Work and Actual Delivery to confirmed tasks (#88, t13) devague summary rendered every task on the plan -- including rejected ones -- into both Planned Work and the Actual Delivery table, so a plan carrying 19 confirmed and 68 rejected tasks (rebuilt twice after scope changes) emitted 87 undifferentiated rows. A rejected task paired with `<fill: status>` invites recording a planning decision as a delivery failure, which is a different fact. Planned Work and Actual Delivery (and the --json summary_data equivalent) now iterate confirmed tasks only, with a single line preserving the rejected count without padding either list ("N tasks were rejected during planning -- see `devague plan show`"). A proposed task -- neither the confirmed contract nor an explicit rejection -- is excluded from both lists and from the rejected count, so an open decision is never folded into a closed one. The --pr wave map is unchanged (dependency_waves already excludes rejected tasks), pinned here with a dedicated regression test. Also applies the #87 md_safe_text escaper at every verbatim interpolation site in this renderer (task summaries, deviation what/reason, announcement/after-state text). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * feat(plan): per-target deferral so milestone-scoped plans converge (#85, t9) Add `devague plan defer <target-id> --reason "<text>"` (+ `--undo` to reverse it) so a plan intentionally scoped to early milestones is not forced to fake coverage of a later-milestone target just to satisfy the gate — the exact perverse incentive #85 reported (a reviewer catching a task that "claims coverage ... but only installs and reports the counter"). - plan.py: CoverageTarget gains deferred/deferred_reason (schema v4, reserved by t2); Plan.defer_target/undefer_target mirror resolve_risk's fail-closed contract (unknown id / already-deferred / not-deferred all raise). - plan_convergence.py: _missing_coverage excludes deferred targets from blockers; _parked_items now also lists each deferred target labeled distinctly ("deferred: coverage target cN ... — <reason>") so `plan status`/`converge --json` can tell "deliberately deferred" apart from "not yet covered". - cli/_commands/plan.py: new `defer` subcommand + PLAN_MOVES entry; _live/_live_frame_and_targets now carry persisted deferral state across every live-frame re-derive (targets_from_frame builds fresh CoverageTarget instances with no memory of prior deferrals — without this merge, converge/export/status would silently drop a recorded deferral on the next call). - cli/_status.py: emit_status now prints parked_items in text mode too (a pre-existing gap — JSON already carried them) so `plan status` visibly shows deferred targets, not just JSON. - render/plan_md.py: new "Deferred targets" section naming each with its reason; applies the t1 escaper (md_safe_text) at every verbatim site (task heading/instruction/acceptance, risk text, announcement, title) — closes the MD050 regression from the #87 comment for underscore-bearing task text. Verified against real markdownlint-cli2 (0 errors) and an end-to-end `devague` CLI smoke run in a scratch dir (defer -> converge -> export -> status -> undo), plus the full pytest suite (760 passed) and black/isort/flake8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * fix(export): render all parks, resolved questions, dead seeds; escape verbatim text (#93, #49, #83, #87, t3) spec_md.py's park-kind filter only ever surfaced follow_up/out_of_scope, silently dropping every open unknown_nonblocking/unknown_blocking park — exactly the residual-risk kind that legitimately coexists with a converged frame (#93, #49). It now renders every open park kind, grouped and labeled. Hard questions previously carried no resolved marker and rendered regardless of parent-claim status, so a resolved question read as an open blocker and a rejected claim's question still leaked into the artifact (#49, #83). Both are fixed: resolved questions get a "(resolved)" marker, and any claim.status == "rejected" drops its hard questions entirely. A scope entry whose seeds cite a claim that was later rejected now renders a visible "(rejected)" marker instead of a bare dead id (#84's fourth acceptance criterion, c33/h26). Every verbatim interpolation site now composes the t1 escaper (md_safe_text) with the existing autolink_urls/heading_safe passes via new _safe()/_safe_heading() helpers, fixing MD037/MD050 on underscore/dunder identifiers throughout the exported spec (#87). Flips the pinned test_render.py:301-306 (nonblocking parks now render) and adds regression coverage for all four acceptance criteria, including a real markdownlint-cli2 double-export byte-stability check. Re-exporting docs/specs/2026-07-28-issue-backlog-sweep.md (a live corpus mixing backticked and bare identifiers) produces the expected one-time presentational diff — committed alongside, per park v5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * chore(export): re-export the 2026-07-17 plan artifact through the new escaper (#87) Proves the t1/t9 escaping fix end-to-end: three pre-existing MD037 errors in a committed artifact from PR #81 clear on re-export, with no source text change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * fix(reject): cascade over honesty conditions and hard questions (#83, t5) Frame.reject cascades a claim's rejection onto its still-live honesty conditions (status -> rejected) and reports both cascaded honesty and hard-question ids so the CLI can echo what it took with it (`c21 -> rejected (also rejected: h3, q1)`). The cascade fires only on the transition into rejected, so re-rejecting an already-rejected claim reports nothing (idempotent, no double-reporting). _assumption_warnings now skips rejected assumptions (only a still-proposed one is actionable "unconfirmed"), and devague review naturally stops listing an honesty condition once its parent claim's rejection cascades over it -- no renderer changes needed, since proposed_honesty() already filters on status == "proposed". * fix(plan): validate --dep at task creation (#86, t10) `plan task --dep` and `depend <tN> --on <tM>` now refuse a self-cycle (the about-to-be-assigned/naming-itself task id) and an unknown task id at authoring time, with an actionable hint, instead of silently recording a broken edge that only surfaces much later as a bare `dependency cycle: tN -> tN` at `plan converge`/`plan waves`. `depend --remove` is untouched by the new checks (it must still repair a dangling dep predating this fix), and the plan_convergence gate for multi-task cycles is left alone — this is creation-time feedback, not a replacement for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * feat(export): mark confirmed claims contested by an approved deviation (#92, t14) Adds a read-only derivation (devague/contested.py) joining a frame's confirmed claims to any approved `devague deviate` record naming them in --affects, since the deviation ledger knew a claim had been contradicted by execution but nothing rendered that back-reference (the spec, `show`, and `status` all stayed silent). Per the #92 maintainer ruling the spec itself is never rewritten -- this only ever derives a pointer at render time: `export` gets a rich per-claim marker (announcement blockquote, Requirements, and every generic claim section), `show`/`status` gain a summary "contested: ..." line in both text and --json, and the plan engine's own status is untouched (no "contested" key at all). The join crosses the frame/plan boundary the instruction called out: frames carry no reverse pointer to the plans seeded from them, so every plan slug is enumerated and filtered on frame_slug before its delivery ledger is read. Fails open throughout -- a plan or delivery file that is missing, truncated, or declares a newer schema degrades to "no markers from that source" plus a stderr diagnostic, never a crash or a refused export; zero frame/plan/ delivery mutation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * feat(plan): transactional multi-id confirm/reject (#86, t11) plan confirm/reject now accept N ids (nargs="+") and apply them transactionally — every id is validated against the plan first, and if any is unknown nothing is changed, matching the frame-side contract (confirm.py's _run). Argument errors raised inside a devague plan <move> parser now hint at `devague plan explain <move>` instead of the generic `<prog> --help`, scoped so top-level and other flat-verb errors keep their existing hint unchanged. * feat(amend): correct a claim or scope finding without id churn (#84, t6) New `devague amend <cN> --text/--kind [--reason]` and `devague scope --amend <sN> --finding` edit a claim or scope entry in place — same id, same honesty conditions/hard questions/instruction, same inbound scope-entry seeds. The only prior route (reject + recapture) churned the id and left scope-entry seeds dangling. Amending a confirmed claim flips it back to proposed and echoes the transition, mirroring the `interrogate --instruction` precedent; origin is never touched (no flag reaches it). A lightweight `Claim.revisions` trail (text/kind/reason) records what an amend superseded, added without a schema_version bump since `from_dict` loads it tolerantly. * feat(scope): --seeds accepts hard-question ids (#84, t7) Frame.add_scope_entry validates a seed against find_claim first and the new find_hard_question second, so a claim-attached hard question id (q*) records alongside claim ids (c*) -- unknown ids of either kind still refuse with the existing "unknown seed claim id" error and "run 'devague show'" hint. render/spec_md._seed_label renders a question seed as `(question)`, or `(question, resolved)` once answered, distinct from the existing claim/rejected-claim markers. This closes the "smaller, related gap" in issue #84: the /scope skill's own routing table sends a "genuinely unknown, needs a user decision" finding to the `question` move rather than `capture`, so a scope entry recording that finding previously had no way to link its provenance. * feat(plan): risk --amend corrects a stale risk in place (#84, t12) Add `Plan.amend_risk` and `devague plan risk --amend RID --text "<corrected>"` so a risk whose prose names a task id that rotated (rejected + recreated during a scope change) can be corrected in place instead of resolving it just to record a duplicate. Preserves id, kind, task link, and resolution state verbatim -- a resolved risk stays resolved after its text is fixed. * docs(learn): teach every surface shipped in the sweep (#52, t17) interrogate --resolve, amend, scope --amend (+ --seeds accepting q* hard-question ids), plan defer, plan risk --amend, transactional multi-id plan confirm/reject, live plan cover/--covers validation, the flat reject cascade, and the scope subagent fan-out threshold were all shipped ahead of learn/explain catching up — devague explain amend even 404'd with "unknown move: amend" before this. Also fixes plan.py's cmd_plan_learn, which still named "six operator skills" and omitted challenge from the list. Closes the #52 acceptance criterion: learn/explain document the resolve path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * docs: sweep README/CLAUDE/skills docs + changelog for the fifteen-issue sweep (t18) Bump 0.20.1 -> 0.21.0 (minor: feature release) and describe the shipped surface across every teaching doc. - CHANGELOG.md: one 0.21.0 entry mapping each of the fifteen issues to what changed (#48 #49 #52 #79 #82 #83 #84 #85 #86 #87 #88 #90 #91 #92 #93). - CLAUDE.md: new Status entry; working-backwards and spec->plan method lists gain amend / interrogate --resolve / scope --seeds q* / plan defer / risk --amend / multi-id plan confirm / live-frame cover; gate 2 now names its durable split artifact; module inventory picks up amend.py, contested.py, _md_safety.py and plan defer. - README.md: verb lists, the "nothing is deleted to make a gate go green" close-out rule, and the reject cascade. - docs/skills.md: scope subagent fan-out and the split-plan --write artifact; fixes the pre-existing drift that said briefs quote plan show --json / the plan-md (SKILL.md is right: plan waves --json is the single source). - docs/skill-sources.md: version stamps for scope and assign-to-workforce. - docs/spec-contract.md: schema_version 4 on both stores (check-before-parse), ClaimRevision, HardQuestion.resolution, CoverageTarget.deferred, the new moves and their validation errors, plus render-time contracts (md-safety, contested-by-deviation). - docs/llm-guidance.md + think/scope SKILL.md: the stale --seeds claim-ids-only wording, and the three new close-out/correct-in-place rules. Docs and version only — no behavior change. * test: end-to-end verification + issue closure map (t19) Adversarial verification of all fifteen issues in the sweep, each checked against its own stated acceptance criteria and — where the issue carried repro steps — by running the reporter's sequence against the built CLI in a scratch dir rather than by reading the tests. Closure map: docs/deliveries/2026-07-28-issue-backlog-sweep-closure-map.md (967 tests green, 98.37% coverage, 0 markdownlint errors including the force-linted .claude/skills/** files). Three defects the per-task agents did not catch, fixed here with tests: - #94 (regression this release introduced): md_safe_text and autolink_urls were composed in OPPOSITE orders by spec_md vs plan_md/summary_md, and both corrupted a URL containing an underscore — the plan order backticked inside the link, the spec order truncated it at the first underscore, silently pointing a committed artifact's link at the wrong address. md_safe_text now carves out URLs exactly as it already carved out code spans, so both orders produce identical intact links. - #49 was only half closed: a resolved hard question rendered "(resolved)" but dropped the recorded decision text, while the parks renderer already rendered its own. The issue asked for "a pointer to the claim/decision that answered them", so the export now renders "(resolved: <decision>)" when interrogate --resolve --decision recorded one. - spec-to-plan/SKILL.md was never swept and still taught `plan reject` as single-id with a shell loop — the exact workaround #86 removed — in the skill guildmaster re-broadcasts to the mesh. Moves table corrected and pinned against `devague plan --help` by a new test. The markdownlint integration test now exports the repo's own real issue-backlog-sweep frame and plan as lint corpus, per the task instruction. Two CHANGELOG factual errors corrected: "Three of those" listed only two workarounds (#87's lint ignores were missing), and the swept-surfaces list omitted spec-to-plan. * chore(deviate): commit the delivery ledger so d1/d2 reproduce for a fresh clone The contested-marker derivation (#92) reads .devague/deliveries/<plan-slug>.json; without it committed, a fresh clone cannot reproduce the markers. Flagged by t19. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * docs(delivery): delivery summary for the issue-backlog-sweep run Eight-section accountability artifact built from the devague summary skeleton: 19/19 tasks delivered, 2 approved deviations (d1/d2, execution-order only), 3 defects found and fixed by t19's cross-task verification, and the honest remaining-work list including the two follow-up issues opened during the run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 * refactor: address SonarCloud findings on the sweep's new modules - contested.py: extract _delivery_for_frame and _markers_from_delivery so find_contested_markers drops from cognitive complexity 24 to under the 15 allowed (python:S3776). - _md_safety.py: name the file-extension alternation instead of relying on implicit string concatenation in the regex (python:S5799). Behavior unchanged; 967 tests still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>



What ships
The close-out for parked vagueness, on both engines (0.20.0):
devague park --resolve <vN> --decision "<text>" [--claim <cN>]— a decided blocking park resolves through a CLI move: the item stays on the record with its resolution text (and optionally the deciding claim via the newresolution_claim_id), drops out of the convergence gate andparked_items, and renders under a## Resolved vaguenesssection in exported specs. A bare--resolvewithout--decisionis refused (evidence-bearing close-out); unknown and already-resolved ids are refused fail-closed.devague plan risk --resolve <rN> --decision "<text>"— the plan-side twin for blockingPlanRisks.SCHEMA_VERSIONandPLAN_SCHEMA_VERSIONbump 2 → 3; older binaries fail closed on v3 artifacts with the existing upgrade hint, v2 artifacts load with defaults.devague learn,docs/llm-guidance.md, the/thinkskill,docs/spec-contract.md) teach the resolve close-out wherever park is taught.Closes #45, closes #55, closes #57, closes #60 — four independent reports of the same gap in five weeks, across colleague, lobes-cli, league-of-agents, and devague's own dogfooding. (#60's task-text half was already shipped as
plan amendin 0.18.0/#68; only its vagueness half was live.)How it was built
Full seven-leg dogfood:
/scope→/think→/challenge→/spec-to-plan→/assign-to-workforce(9 tasks fanned out to subagent worktrees across 3 waves, TDD-gated merges) → this PR. Artifacts:docs/specs/2026-07-17-resolve-parked-vagueness.md,docs/plans/2026-07-17-resolve-parked-vagueness.md(frame/plan state committed under.devague/).The
/challengepass (rigorous — schema-migration + data-loss signals) caught the two secondary consumers of open vagueness (parked_itemsin both gates,plan deliverablesopen items) that the initial spec missed; they're covered here (c18).Verification
.devaguehand-edits anywhere.git diff main...HEAD -- devague/grep is empty).Note for the reviewer
The pre-existing
resolve-parked-vaguenessbranch (commit b5c9e7c, an earlier session) specs a different shape — a new flatresolveverb with--kindre-kinding, widened to scope entries. This session's frame decisions (q1: extendpark --resolve, no new verb; q3:--decisionrequired) supersede it; that branch was left untouched and can be deleted if you agree it's superseded.