The fifteen-issue backlog sweep (0.21.0) - #96
Conversation
…ague /scope + /think + /challenge) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3
…evague /spec-to-plan) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtUixLUjgEt51PXS9TPVt3
…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
`_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.
… (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
…, 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.
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.
…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
…ks (#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
…, 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
… 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
… 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
… 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".
`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
#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
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.
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.
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.
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.
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
…ue 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.
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.
…resh 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
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
|
/agentic_review |
- 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
|
PR Summary by QodoIssue backlog sweep (0.21.0): 15 issues — new moves, cascades, and export fidelity fixes
AI Description
Diagram
High-Level Assessment
Files changed (72)
|




Closes the entire open issue backlog — fifteen issues — in one release, plus one opened and fixed during the run.
Closes #48, #49, #52, #79, #82, #83, #84, #85, #86, #87, #88, #90, #91, #92, #93, #94.
Executed as a single
/assign-to-workforcefan-out of theissue-backlog-sweepplan: 19 tasks, 6 dependency waves, one agent per task per wave in an isolated git worktree, every merge TDD-gated (suite green before and after). No merge was reverted.Why one release
Three of the fifteen were being worked around by hand downstream, and the workarounds were getting expensive:
.devague/frames/<slug>.jsonto setresolved: true, because a blocking hard question could never be cleared through the CLI — nothing in the codebase ever setHardQuestion.resolved, so one--blockingquestion wedgedconvergepermanently (Add a CLI verb to resolve blocking hard questions (converge is permanently blocked without one) #48, Add a CLI verb to resolve a claim's blocking hard question (convergence deadlock; unblocks /think exports) #52).scripts/render_plan.py, with a SHA-256 drift check) because a milestone-scoped plan could not converge — the coverage gate demanded every target, and rewarded tasks that merely named one. Its docstring says to delete it when Milestone-scoped plans cannot converge — the coverage gate rewards tasks that mention a target without delivering it #85 lands (Milestone-scoped plans cannot converge — the coverage gate rewards tasks that mention a target without delivering it #85).docs/specs/**from markdownlint entirely, because exported claim text naming Python identifiers (_read_file,__init__.py) failed MD037/MD050 in their CI (export renders claim text unescaped — underscore identifiers make the spec fail markdownlint (MD037) #87).All three are verified deletable — see the closure map.
What ships
New moves
devague interrogate <cN> --resolve <qN> [--decision]devague amend <cN> [--text] [--kind] [--reason]devague scope --amend <sN> --findingdevague plan defer <target> --reason/--undodevague plan risk --amend <rN> --textassign-to-workforce split-plan --writeSharpened —
rejectcascades onto honesty conditions and unresolved hard questions andconvergestops warning about rejected assumptions (#83);plan confirm/plan rejectare multi-id transactional and plan-group errors point atplan explain(#86);--dep/depend --onrefuse self-deps and unknown ids at creation (#86);cover/--coversvalidate against live-frame targets (#90);summaryscopes to confirmed tasks (#88);scope --seedsacceptsq*ids (#84).Export fidelity — all four park kinds render under
## Open parks, resolved hard questions carry(resolved: <decision>), hard questions on rejected claims are excluded, and a scope seed citing a rejected claim renders(rejected)(#93, #49). Newmd_safe_text()escapes every verbatim render site — presentational only, stored JSON untouched (#87). New read-onlydevague/contested.pymarks a confirmed claim contested by an approved deviation across the plan-slug boundary; the spec is not rewritten, it points forward to the ledger (#92).Both schema versions are now 4, and both stores check the declared version against the raw dict before parsing — previously a newer-schema file died with a bare
TypeErrorfromHardQuestion(**q)instead of the fail-closed error.Evidence
markdownlint-cli2 "**/*.md"— 39 files, 0 errors;.claude/skills/**force-linted — 19 files, 0 errorsdocs/deliveries/2026-07-28-issue-backlog-sweep-closure-map.mddocs/deliveries/2026-07-28-issue-backlog-sweep.mdWhat the verification pass caught
The final task was adversarial cross-task verification, and it earned its place — it found three defects the per-task agents missed:
spec_md.pycomposedautolink_urls(md_safe_text(t))whileplan_md.py/summary_md.pycomposed the opposite order, each documenting its own as correct. Both corrupted an underscore-bearing URL — the spec order truncated it at the first underscore, silently pointing a committed artifact's link at the wrong address. This was filed speculatively as autolink_urls and md_safe_text compose without knowing about each other — a URL containing an underscore can be corrupted #94; it was real. Fixed by carving URLs out ofmd_safe_textthe way code spans already were.(resolved)but dropped the decision text, while the issue asked for a pointer to what answered it.spec-to-plan/SKILL.mdwas never swept — it still taughtplan rejectas single-id with a shell loop, the exact workaround plan papercuts: self-dependency accepted at task creation; plan reject is single-id while frame reject is multi-id #86 removes, in a skill guildmaster re-broadcasts to the mesh.Deviations
Two approved mid-run deviations, both execution-order only, both recorded via
devague deviatebefore resuming:d1— t17 (learn/explain recipes) moved from wave 3 to after wave 5: three of the four surfaces its acceptance criterion names don't exist until waves 4–5, so documenting them in wave 3 meant writing recipes for unshipped verbs.d2— t18 (docs sweep + version bump) moved to last, same root cause.No plan state was mutated — deviate is the marking of the change.
Follow-ups
r4).qNnamespace collision (claim-attached hard questions vs. the durable questions file both mintqN) is documented as a hazard this run; not yet filed.Review map
The delivery summary is the audit trail: every plan task accounted for, both deviations quoted, every delivery claim carrying a resolvable evidence pointer or an explicit
unverifiedmarker.