Releases: dfch/biz.dfch.SpecMgr
Release list
v0.25.0 - QA Introduction Accepts Any Markdown Content
Changed
qa: the### Introductionbody now accepts any markdown content (a
bullet list, code block, block quote, etc.), not just plain paragraphs
-- the field is retyped from a paragraph list to an opaque markdown
leaf. The optional leading comment and the section's optionality are
unchanged (GitHub issue #114).
Fixed
models/md: a soft-wrapped (CommonMark lazy-continuation) list item
now fails with an actionable error -- field path, 1-based line, and an
explicit "soft-wrapped/lazy-continuation list items are not supported"
cause/fix hint -- instead of an opaque parsing failure, across every
structurally-checked list item in all twelve whole-body domains
(tsk,feat,rsk) (GitHub issue #99).
v0.24.0 - Cap Truncated Validate Error Messages
Fixed
validate:ValidationErrorEntry.messageis now capped at 300
characters (_MAX_VALIDATE_ERROR_CHARS, plus a trailing
"... (truncated)"suffix when truncation occurs) instead of reusing
the caught exception'sstr()verbatim without limit -- a structurally
malformed document (e.g. an unexpected/duplicate heading) could
otherwise produce a message several hundred characters long once
wrap_tool_errors's domain/tool/channel label was prepended. Also fixed
models/md/_markdown.py::not_in_mdformat_message()'s global-mismatch
(line_no == 0) branch, which embedded the full rawtext/formatted
viarepr()with no bound -- it now routes both through the existing
snippet()helper, matching every sibling message-builder in that
module (GitHub issue #110).
v0.23.0 - Non-Raising Invalid-Status Result For Set Status
Fixed
set_status: an out-of-vocabularystatusvalue for a giventype(all
13 domains, includingadr) no longer raises apydantic.ValidationError
-- it returns a new, non-raisingInvalidStatusResult
(valid/type/status/allowed_values/message) instead, so the
allowed-values detail survives MCP clients that truncateisError: true
results. Every otherset_statusfailure mode (unknownid,
path-injection/wrong-shapeid,superseded_bymisuse on a non-adr
type) still raises unchanged. New ADR
b399f1ce-ed42-4929-b01c-7a57d18e8014 extends ADR
519d1206's non-raising, structured-result workaround (previously scoped
tovalidate) to this case (GitHub issue #103).
v0.22.0 - Consolidate Validation Into A Generic Validate Tool
Added
- Generic
validate(type, content, full)MCP tool ingeneral/tools/:
the disk-free, id-free dry-run content validator for the twelve
whole-body domains (typeis one of
req/uc/tsk/qa/prb/gol/rsk/dec/sop/feat/vcr/sysrs;
adris not supported --validate_adrremains its own standalone,
unchanged tool). Unlike every other generic tool ingeneral/tools/, it
never raises for a content-validation failure: it always returns a
structured{valid: bool, errors: list[{message: str}]}result,
reusing feat-27-validation's already-enriched exception messages
verbatim as each error'smessage-- only afull/content-shape
mismatch (full=Truewith body-only content, orfull=Falsewith a
complete document) or an unsupportedtypestill raisesValueError,
since that is a caller-usage error, not a content-validation failure.
This is the sole validate entry point for these twelve domains: every
current and future domain implements avalidateadapter in the
generic tool, never a per-domainvalidate_<d>tool. New ADR
(078bf395-0a5f-4afd-84f6-b7a2191a00e6) extends ADR
36905d5b-8057-4294-8665-c7eed5534db0's dispatch-only convention
(previously covering only mutation-adjacent tools) to this read-only/
dry-run tool category (GitHub issues #81/#83). - Dedicated Pydantic models with drift-guard unittests for
specmgr://dtais,specmgr://rsk/tara,specmgr://rsk/risk-matrix,
andspecmgr://rasci; all four are now also parsed on every resource
call to fail fast on structural drift, discarding the parsed result,
matchingspecmgr://iso25010's existing pattern. New
specmgr://earsresource documenting the EARS (Easy Approach to
Requirements Syntax) five requirement-phrasing templates, likewise
backed by a model and drift-guard tests -- content aligned with the
source paper's (Mavin et al., "Easy Approach to Requirements Syntax
(EARS)", RE'09) exact pattern names, order, and sentence templates,
and now includes the paper's own worked example for each pattern. New
ADR (356d8781-e446-4c26-917a-eda85648ce9d) documenting the resulting
repo-wide convention for reference resources (GitHub issue #92). - Generated JSON Schema for the twelve whole-body domains now documents
thecreated/updatedfrontmatter fields'yyyy-MM-dd HH:mm:ss.fff+
Z/±HH:mmtimestamp format as apatternconstraint, without
changing the existing runtime validation behavior (adr's schema is
unaffected) (GitHub issue #94).
Changed
- BREAKING (0.x):
list_<d>(all twelve whole-body domains --
req/uc/tsk/qa/prb/gol/rsk/dec/sop/feat/vcr/sysrs;
list_adris unaffected, out of scope) no longer silently skips a
document that fails to parse: it now appears inline inresultsas its
own failed entry (id=None,title/statusboth the fixed marker
"<failed to parse>",ref/pathpopulated the same way as a
successful entry, and a newerror: str | Nonefield carrying the
caught exception's message), andtotalnow includes failed entries
alongside successes -- a deliberate semantics change from the previous
"parseable documents only"total.PagedResultgains a new
error_count: int = 0field, counting failed entries across the whole
base directory, independent ofoffset/max_resultspaging (mirroring
total's own already-documented across-all-pages semantics). Every
domain's summary type (DocSummarysubclass) also gains apath: str
field -- an absolute, resolved filesystem path -- for the eleven
non-featwhole-body domains. Callers relying on a malformed document
being silently absent fromresults/uncounted intotalmust instead
check each entry'serrorfield (GitHub issue #83). feat/FeatSummary's own, previouslyfeat-onlypathfield is removed
in favor of the same, now-sharedDocSummary.pathfield every other
whole-body domain's summary carries (see above), and is retrofitted in
the same pass to the same absolute, resolved form the other eleven
domains use --FeatSummary.pathwas previously left in its pre-existing
unresolvedstr(path)form. This completeslist_<d>'spath-field
parity across all twelve whole-body domains (GitHub issue #81).specmgr://iso25010now returns raw markdown (text/markdown)
instead of a structuredIso25010JSON object, still parsed via
parse_iso25010()on every read to fail fast on structural drift
(GitHub issue #92).- Local
pytestpre-commit hook and CI's "Run unit tests with coverage"
step now run the (unchanged, still plainunittest.TestCase-based) test
suite viapytestwithpytest-xdist(-n auto) instead of serial
coverage run -m unittest discover, cutting the local pre-commit gate's
test step from 9-11 minutes to roughly a minute.pytest-covproduces
the same.coveragefile format, sospecmgr coverage-badgeis
unaffected. set_feat_idnow returns the domain'sFeatFrontmatterobject only (no
body) on a successful rename, matching the frontmatter-only return
shapecreate_feat/update/set_status/set_classificationalready
use -- it was a bespokefeat-only tool that predated, and was missed
by, that conversion. Callers needing the full document should call
get_featafterward (GitHub issue #80).
Removed
- BREAKING (0.x): the twelve per-domain
validate_<d>MCP tools are
deleted outright (no deprecated wrappers):validate_req,validate_uc,
validate_tsk,validate_qa,validate_prb,validate_gol,
validate_rsk,validate_dec,validate_sop,validate_feat,
validate_vcr,validate_sysrs-- each raised on a content-validation
failure instead of returning a structured result. The twelve
per-domainvalidate_<d>.pymodules, their__init__.py
registrations, and their dedicated tests are gone with them. Callers
must switch fromtools/call --tool-name validate_<d>to
tools/call --tool-name validatewith the explicittypeparameter
(see "Added" above) -- and, sincevalidatenever raises for a
content-validation failure, callers checking{valid: bool}on the
returned result instead of catching an exception.validate_adris
unaffected and remains unchanged.
v0.21.0 - Fix Placeholder Timestamps in Templates and Examples
Changed
- README now flags the Architecture Decision Record (ADR) artifact type
as deprecated in favor of Decision (DEC).
Fixed
- Replaced round, all-zero-time-of-day placeholder timestamps (frontmatter
created/updated, andfeat's body-level#### {timestamp}headings)
across all 24 affected domain template/example files with realistic,
non-round values, so they no longer invite copy-paste-without-substitution
(GitHub issue #67).
v0.20.0 - Confluence Page Title From Markdown H1
Added
confluence_updatenow sets the Confluence page's title from the source
markdown's first H1 heading, falling back to the existing (GET-fetched)
title unchanged when no H1 is present (GitHub issue #76).
Fixed
specmgr://confignow reports thesysrsdomain (it was missing from
the resource'sdomainsdict even thoughsysrswas already fully
wired into every dispatch tool), matching every other domain's entry
(GitHub issue #74).- Corrected stale/incorrect copyright holders in
NOTICEforpydantic,
python-dotenv,typer,rich, andmcp; added missing attributions
formdformat,mdformat-simple-breaks, andhttpx(GitHub issue #73).
v0.19.0 - MCP Write Tools Return Frontmatter-Only
Changed
- On a successful write, the generic
update,set_status(its twelve
non-adradapters), andset_classificationtools, and every
per-domaincreate_<d>tool (req/uc/tsk/qa/prb/gol/rsk/
dec/sop/feat/vcr/sysrs), now return the domain's frontmatter
object only, instead of the full document with its (potentially large,
ever-growing) body. Theadrdispatch branch ofset_statusand every
ADR-specific tool (create_adr,update_frontmatter,update_section,
theoption_*tools) are unchanged and still return the full document
(GitHub issue #69).
v0.18.0 - Add System Requirements Specification (SYSRS) Domain
Added
-
New
sysrs(System Requirements Specification) domain: an aggregator
document type that ties together existinggol/prb/qa/uc/req/
rsk/dec/adr/vcrartifacts into one coherent, navigable
specification via per-section, type-tagged cross-reference lists (e.g.
### Goalsaccepts onlyGOLbullets,## DecisionsacceptsDECor
ADR, and the nine## RequirementsH3s plus six## Other CharacteristicsH3s each accept onlyREQ) rather than duplicating
their content. Dispatch-only from day one, with noupdate_sysrs/
set_status_sysrstools of its own — whole-body/line-range updates,
status changes, classification changes, and deletions all go through
the existing genericupdate/set_status/set_classification/
deletetools (type="sysrs"). Ships 7 tools (create_sysrs,
parse_sysrs,list_sysrs,get_sysrs,get_sysrs_example,
get_sysrs_template,validate_sysrs), 3 resources
(specmgr://sysrs/schema,specmgr://sysrs/example,
specmgr://sysrs/template), and 2 prompts (create_sysrs,
update_sysrs) (GitHub issue #32). -
New
specmgr://configresource: reports, for all twelve document
domains, the resolved absolute base directory and whether the domain's
SPECMGR_*_DIRenvironment variable is explicitly set, so a client can
self-diagnose a working-directory-relative base-directory
misconfiguration. Only the twelve known env var names are ever read
(neveros.environwholesale), so no unrelated secret is ever
disclosed. The "Add to OpenCode" README example now shows two
alternatives for pinning the resolved base directory —uv/uvx's
--directoryflag, or explicitSPECMGR_*_DIRenvironment variables in
the MCP client config — and documents the previously-missing
SPECMGR_FEAT_DIRvariable (GitHub issue #51). -
create_uc/update_ucMCP prompts for theuc(Use Case) domain,
mirroring thereqdomain's prompt pattern and including
set_classificationguidance (GitHub issue #57).
Fixed
format_text()/format_markdown_document()(models/md/_markdown.py),
and transitively every domain'sparse_<d>/create_<d>/validate_<d>/
updatepath plus themdformatCLI command and MCP tool, now render a
thematic break (---,***,___, or any other CommonMark-valid
variant) as a literal---instead ofmdformat's hardcoded 70-character
underscore line ("_" * 70, not otherwise configurable upstream — see
hukkin/mdformat#69). Fixed by wiring the third-party
mdformat-simple-breaksplugin (pinned exactly,==0.1.0) into the
sharedmdformat.text(...)call via itsmdformat.parser_extension
entry point (GitHub issue #47).
v0.17.0 - Classification Frontmatter Field and Confluence Update Tool
Added
-
create_feat(feat domain) now accepts an optional, caller-chosen
id: str | None = Noneparameter — a full, well-formedfeat-NNN-slug
value, validated viaassert_feat_id(general/tools/_path_safety.py)
before any lock/filesystem access. Whenidis omitted, the default is
nowfeat-0-<slug-from-title>— the previousfeat-{max existing NNN + 1}-{slug}auto-increment fallback is gone entirely, sinceNNNis
meant to be the GitHub issue number a feature tracks, andfeat-0-...
now signals "no issue yet" rather than a scan-derived guess. Either way
(caller-supplied or defaulted),create_featraisesFileExistsError
before any write if the resulting id/folder already exists, and raises
ValueErrorbefore any write if a caller-suppliediddoesn't match the
feat-NNN-slugshape. A newset_feat_id(id, new_id)@mcp.tool()
(feat domain,feat/tools/set_feat_id.py) complements this by letting an
existing feature's id be renamed afterwards (e.g. once its GitHub issue
number becomes known): it validatesnew_id's shape, refuses via
FileExistsErrorif the target folder already exists, renames
<base>/<id>/to<base>/<new_id>/, rewrites the frontmatteridand
bumpsupdated, leaves the body byte-identical, and raises
FeatNotFoundErrorifiddoes not resolve. It runs under
feat_create_lock()(outermost) thenfeat_lock(id)(nested) to avoid
races withcreate_feat/update/set_status/deleteon the same id.
featremains dispatch-only for whole-body updates/status changes — no
update_feat/set_status_feattool of its own;set_feat_idis a
distinct, bespoke tool for id changes specifically (GitHub issue #48). -
Windowed raw reads on the eleven
get_<d>MCP tools
(req/uc/tsk/qa/prb/gol/rsk/dec/sop/feat/vcr):
each now accepts optional read-styleoffset/limitcoordinates for a
windowed raw read — valid withraw=Trueonly (coordinates with
raw=FalseraiseValueError),offset1-based with default 1
(floored, never errors),limita line count defaulting to through end
of body (capped at the remaining lines), andoffset > Nreturning the
empty string; out-of-range values clamp, consistent with thelist_<d>
paging convention. The window is served by a new no-I/O
window_body(text, offset, limit)helper ingeneral/tools/_splice.py,
besidebody_text/splice_body, so the raw/splice invariant (the line
numbers a client sees in anyget_<d>(raw=True)read, windowed or not,
index byte-for-byte into the same text the genericupdatetool splices
against) is defined once and shared by all eleven tools (GitHub issue
#28; ADR 4ec08dcb-fcb7-4961-abaf-ff7803e2f21d). -
confluence_updateMCP tool (general/tools/): writes a local Markdown
file's rendered content into an existing Confluence page's body via the
REST API, resolving a bare page id, a browsable page URL, or a REST
content URL to a numeric page id,GET-ing the page's current
version/title, rendering the Markdown viamarkdown-it-pyto an
HTML fragment, andPUT-ing the incremented version. Local images
referenced by the Markdown file are uploaded as Confluence attachments
on a best-effort basis (POST .../child/attachment, falling back to
.../child/attachment/{id}/dataif the filename already exists --
duplicate-filename detection is confirmed against a real Confluence
server's actual 400 response, "Cannot add a new attachment with same
file name as an existing attachment:<filename>. Log referral number
is<uuid>") and their<img>tags are rewritten into Confluence's
<ac:image>/<ri:attachment>storage-format macro. Also sanitizes any
raw--sequence inside rendered<!-- -->HTML comments (valid
CommonMark but rejected outright by Confluence's strict XHTML
storage-format parser, confirmed against a real instance:"Error parsing xhtml: String '--' not allowed in comment") and converts a
leading YAML frontmatter block into a fenced code block before
rendering, instead of letting CommonMark's thematic-break/Setext-heading
rules mangle it into a stray<h2>heading (also confirmed against a
real instance). Closes GitHub issue #50, per ADR
a156fdf9-052c-4f43-93a2-eeec04a91eac. -
confluence_update/confluence_fetchMCP prompts (general/prompts/):
thin, single-tool-call prompts sharing their respective tools' exact
names (a separate MCP registry from tools). Each returns instructional
text telling the LLM to call the matchingconfluence_update/
confluence_fetchtool with the given parameters -- neither prompt ever
calls its tool itself.confluence_updatealso tells the LLM to report
back the tool's returnedversion/failed_images;confluence_fetch
documents thatdestination_pathis only required for binary/non-text
content. Part of feat-50-confluence Phase 8, REQ-012/REQ-013. -
Optional, free-text
classificationfrontmatter field on the shared
MarkdownFrontmatterbase, inherited by all eleven whole-body domains
(req/uc/tsk/qa/prb/gol/rsk/dec/sop/feat/vcr; ADR
excluded, since it has its own separate frontmatter model). A new
genericset_classification(id, type, classification)MCP tool
(general/tools/) mirrorsset_status's dispatch pattern to change it
after creation, bumpingupdatedand leaving the body and every other
frontmatter field untouched; a blank/whitespace-only value clears
classificationback toNone/absent, same as every other optional
frontmatter field's blank-to-Nonenormalization. Existing documents
without aclassificationkey keep parsing unchanged. The ten
whole-body domains' packaged create/update prompt instruction files
(uc, which has no prompts sub-package yet, is untouched) now mention
set_classificationalongside the existingset_statusmentions
(GitHub issue #56).
Changed
-
BREAKING (0.x): the generic
updateMCP tool's 1-based inclusive
begin/endbody-line range (with theN+1end-of-body sentinel) is
replaced by read-styleoffset/limitcoordinates in a hard rename (no
compatibility alias):offsetis the 1-based first line to replace
(allowed1..N+1, whereN+1is the virtual end-of-body append
position),limitis the number of lines (offset..offset+limit-1);
omittedlimitreplaces through the last body line,limit=0is a pure
insert. Out-of-range coordinates raiseValueError(strict, never
clamped, nothing written) andlimitwithoutoffsetraises
ValueErrorbefore any file access; splice-then-validate-whole, verbatim
persistence, and frontmatter carry-over are unchanged. Every LLM-facing
surface (the packaged prompt instruction files, tool descriptions,
docstrings,AGENTS.md) moved to the new vocabulary in this same
release. The revised contract is recorded in ADR
4ec08dcb-fcb7-4961-abaf-ff7803e2f21d (referencing, not superseding, ADR
36905d5b-8057-4294-8665-c7eed5534db0) (GitHub issue #28). -
Eliminated all 42 pylint W0622 (redefined-builtin) findings via 39
explicit, per-file# pylint: disable=redefined-builtincomments (with
a one-line rationale) on the files whose public API intentionally uses
id/typeas parameter names — the twelveget_<d>tools, the
per-domain update/implement prompts, the ADR tools/resources/prompts,
the genericupdate/set_status/deletepublic functions, and
models/md/markdown.py/alias.py. Not breaking — no behavior change,
purely an internal lint-suppression change. No globalpyproject.toml
pylint configuration change: a future file that shadows a builtin
without adding its own disable comment still warns (GitHub issue #41,
Phase 5 of feat-38-39-41-43-44). -
The twelve
get_<d>tools (includingget_adr), the genericupdate
tool, and the genericset_statustool now validateidfor
path-injection/wrong-format before any filesystem access, and confine
the resolved path to the domain's own base directory after resolution —
the samegeneral.tools._path_safetyguards the genericdeletetool
already had (feat-36-delete)._path_safety.validate_idnow also
accepts"adr"as a UUID-shaped domain. This is purely additive
validation: a previously well-formed id for its domain is unaffected; a
path-injection attempt or a malformed id — which would already have
failed downstream (e.g. via aFileNotFoundError/XNotFoundError) —
now fails earlier and more explicitly with aValueError.delete
itself is unchanged (GitHub issue #43, Phase 4 of
feat-38-39-41-43-44). -
BREAKING: renamed the
webfetchMCP tool toconfluence_fetch(and
its environment variablesSPECMGR_WEBFETCH_BASE_URL/
SPECMGR_WEBFETCH_BEARERtoSPECMGR_CONFLUENCE_BASE_URL/
SPECMGR_CONFLUENCE_BEARER); part of feat-50-confluence. Beyond the
rename,confluence_fetchnow auto-converts browsable Confluence page
URLs (Cloud-style/pages/<id>/<title>and Server-style
?pageId=<id>) into the equivalent
{base}/rest/api/content/{id}?expand=body.storageREST API URL before
fetching, rejects the/x/<tinyid>tiny-link URL shape with a clear
error (unresolvable to a page id without an authenticated browser
session), detects when a request is redirected off the configured base
URL's host (e.g. to an SSO login page) and raises instead of returning
that content, and supports binary/image download via a
destination_pathparameter (content-type based). GitHub issue #50,
ADR a156fdf9-052c-4f43-93a2-eeec04a91eac. -
BREAKING: frontmatter
created/updatednow strictly require the
date+time variantyyyy-MM-dd HH:mm:ss.ffffollowed byZ(UTC) or a
signed±HH:mmoffset — date-only,T-separated, six-digit-microsecond,
and timezone-less values are all rejected at parse time
(pydantic.ValidationError), eagerly, o...
v0.16.0 - Generic delete tool
Added
- Generic
delete(id, type)MCP tool ingeneral/tools/: the
type-dispatched hard-delete for the eleven whole-body domains (typeis
one ofreq/uc/tsk/qa/prb/gol/rsk/dec/sop/feat/vcr;
adris not supported). Resolves the document byidunder the
domain's own per-id lock and removes it — the single*.mdfile for the
ten flat domains, or the entire<base>/<id>/folder forfeat—
returning the deleted path as a string. An invalidid(path-injection
attempt or wrong format) is aValueErrorraised before any file access;
a missing document is the domain's ownXNotFoundError; an I/O failure
during the delete is aDeleteError(anOSErrorsubclass). This is
the sole delete entry point: every current and future domain implements
adeleteadapter in the generic tool, never a per-domaindelete_<d>
tool. - A reusable, doc-type-agnostic path-safety module
general/tools/_path_safety.py:assert_no_traversal,assert_uuid,
assert_feat_id,validate_id, andassert_within— pure, no-I/O
guards preventing path-injection throughtype/idinputs and confining
resolved paths to their base directory. Wired into the newdeletetool
now; designed so theget_<d>,update, andset_statustools can
adopt it later with zero rework (they are not modified in this change).
Changed
- The release SOP (now
active— its status wasdraftuntil the first
release executed under it succeeded end to end, v0.15.0) was clarified:
tool prerequisites and the stage-to-step execution map up front in
Scope, the fast-forward-only merge mechanism the script actually uses
(pre- and post-merge SHA assertions around the plain merge method,
replacing the description of a nonexistentgh pr merge --ff-only),
the publication workflow's name ("Publish to PyPI") vs. file
(.github/workflows/publish.yml) distinction, and a dedicated
precaution about the oldgh2.4.0 the script targets.
Removed
- BREAKING (0.x): the eleven per-domain
delete_<d>stub MCP tools are
deleted outright (no deprecated wrappers):delete_req,delete_uc,
delete_tsk,delete_qa,delete_prb,delete_gol,delete_rsk,
delete_dec,delete_sop,delete_feat,delete_vcr— each was a
registered stub that always raisedNotImplementedError. The eleven
per-domaindelete_<d>.pymodules, their__init__.pyregistrations,
and their stub tests are gone with them. Callers must switch from
tools/call --tool-name delete_<d>totools/call --tool-name delete
with the explicittypeparameter (see "Added" above).
Fixed
scripts/release.sh: thepr-merge,publish-wait,status, and
release-notesstages no longer rely onghCLI features that do not
exist in this environment'sgh2.4.0 — a nonexistent--ff-only
merge flag,gh run list --commit,gh run view --json jobs, and
gh release view --json/gh release edit. Fast-forward-only merging
is now enforced by pre-merge and post-merge SHA assertions around the
plain merge method; the publication run is located by workflow name
("Publish to PyPI") plus the tag's commit SHA (filtered withjq);
the GitHub Release is read and its notes set throughgh api.