Releases: tranquocthong/claude-spec-flow
Release list
v0.8.2 — escaped pipes in SD cells, header-based column mapping
A | inside an SD table cell silently corrupted the trace built from it. Found reviewing an SD whose §5.1 describes a signature payload joined by | and whose §12.2 trigger names a status enum — both perfectly ordinary things for a requirement to say, and both unreadable to the engine.
splitRow() split on every |, including an escaped \|. The escape is what markdown requires to keep a literal pipe inside a cell, so a correctly-written SD was exactly the case that broke: the row gained columns, and every reader after the pipe landed one cell off. This repo's own contract-shim SD is the demonstration — its ERR_INVALID_STATUS row parsed as 12 cells against a 6-column header, with Trigger cut short at the first escaped pipe and the rest of the enum scattered across columns that do not exist. Downstream, trace-build stored the truncated requirement and read priority/source out of their neighbours, and route scored complexity on the cut-short text before picking fast/expand/deep. splitRow now splits on (?<!\\)\| and unescapes \| to | on read, so the value that reaches code, YAML, or a payload is the plain U+007C character — the backslash is a rendering artifact and never leaves the document.
Pass-1 was the source: genSd interpolated raw SRS prose straight into cells. New core.mdCell() escapes on write at all 13 interpolation sites (revision history, §5.1 from AC / edges / BL rules / the ID-prefix fallback, §5.2 NFR, §10.4 state, §12.2 error triggers, §13.2 TC, glossary). It round-trips losslessly with splitRow and does not double-escape an SRS that already writes \| itself. The generated §5.1 now carries a one-line note that the escape is markdown-only and the value is U+007C — a backslash copy-pasted out of a rendered cell into code is a silent data bug that no test catches.
trace-build resolved all five SD tables by column position. New core.resolveCols(table, spec) claims each column by header name, falling back to the canonical Pass-1 position (and never to -1, which would read undefined cells). Applied to the FR / TC / error / state / NFR tables in trace-build and to route, which was destructuring [id, req, prio, src]. The 0.8.0 tcExpIdx special case folds into it, keeping its length-aware fallback (6-col enriched to index 4, 4-col skeleton to 3). route and trace-build now share one FR column spec: they must agree on which cell is the Requirement, or a routed FR and its trace node describe different things.
A shape-broken table now says so. core.tableShapeWarnings() flags any row whose cell count differs from its header — naming the offending row id and its real count, and pointing at the unescaped | — surfaced by trace-build (all five tables) and route (§5.1). It warns, never blocks: the SD stays readable to a human, and the trace built from it is wrong without looking wrong, which is the half nobody catches by eye. route's Result gains a warnings field.
The rule is written down where SDs are authored. agents/sd-author.md now requires escaping a literal | as \| in cell content, and requires a line under the table whenever a requirement or payload genuinely uses | as a delimiter, stating that the delimiter is the plain character. templates/sd-template.md §5.1 carries the same convention for hand-written SDs.
Upgrade note: an SD that already escapes its pipes will now produce a different (correct) trace.json when trace-build is re-run, and an SD with a raw | in a cell will surface a new warning from trace-build and route. Neither blocks.
790 Node tests green (8 new), 65 Python tests green.
v0.8.1 — MCP add_task fallback, doctor sees shadowed .mcp.json
A dogfooding finding: /sf:change dead-ended on a missing MCP tool it never needed. An agent running /sf:change had to add a task for a net-new FR, found no add_task in the MCP surface, found no add-task in bin/task-master either, and handed the work back to the user as "tooling unavailable". Two separate defects made that dead end reachable.
The command docs pointed at exactly one way to add a task, and it was the fragile one. commands/change.md step 4 named mcp__task-master-ai__add_task with no alternative, and commands/phase.md's Task Master note scoped its CLI fallback to a single trigger — "if any MCP TM call errors with a missing API key" — which does not cover a tool that is simply absent from the surface. Meanwhile the deterministic twin had shipped with the native engine all along: flow-tools.cjs task-add / task-get / task-list / task-set-status / task-next write the same .taskmaster/tasks/tasks.json with no AI and no MCP, and none of them were documented anywhere. The agent's conclusion was correct given what it could read. commands/phase.md now carries the full MCP-op to engine-CLI mapping table and a fallback that fires on an absent tool as well as a provider error; change.md and resync.md inline the two ops they actually need; the README documents all five commands.
/sf:doctor could not see the actual cause: a project-level .mcp.json shadowing the bundled server. The target project still had its own mcpServers["task-master-ai"] entry (npx task-master-ai, TASK_MASTER_TOOLS=core) left over from a pre-cutover task-master init. That entry wins over the plugin's manifest, so the session was talking to a legacy core-tier server whose 7-tool surface has no add_task. The existing dep-lock check reads .mcp.json at the plugin root only, so it reported "native task engine bound" while the live binding was legacy. New mcp-shadow check reads .mcp.json in the project cwd and warns when a task-master-ai entry there is not the native binding, naming the tier and the two fixes; skipped when cwd is the plugin root.
782 Node tests green (2 new).
v0.7.4 — plugin-root paths, Summer-only X-Userinfo, token: none
Three dogfooding bugs, all found running spec-flow against an external Node/Express project (claude-code-provider / tenant-usage-monitoring). None are in the target project's code — all three are spec-flow defects that cost real debugging time.
bin/task-masterwas invoked cwd-relative in the skill instructions.commands/{phase,ingest,init,change,resync}.mdandagents/hybrid-executor.mdtold the agent to runnode bin/task-master …, but those commands execute in the user's project, which has nobin/task-master— the binary lives in the plugin. Result:MODULE_NOT_FOUND, and the agent had to hand-resolve the plugin's absolute path to continue. Nownode ${CLAUDE_PLUGIN_ROOT}/bin/task-master …, matching how everyflow-tools.cjsinvocation was already written. Root cause:scripts/cutover.cjs'sNATIVE_CLI_PREFIX(v0.7.0) rewrote the oldnpxinvocations to a relative path. The cutover/rollback script pair is left as-is — it is one-shot historical migration tooling and the legacy dependency it targets is already removed.docs/invocations are unchanged: those are run from the spec-flow repo root, where the relative path is correct.detect-auth.shmissed customAuthorization: Bearerschemes, andchecklist-gendefaulted the wrong way. Two compounding bugs. (1) The detector classifies by dependency fingerprint (jsonwebtoken,jjwt,pyjwt, …), so a service that reads theAuthorizationheader itself and validates an opaque API key — no JWT library anywhere — fell through tounknown. It now also greps the source for header-read +Bearerprefix, across every stack, and reportsjwt-basic(same wire form; only how you mint the token differs). (2)checklist-gentreated the Summer/APISIXpayload:/X-Userinfoform as the default andbearer:as the special case — exactly backwards.X-Userinfois a Summer/APISIX-specific convention that is only trusted behind a real gateway upstream; an unclassified project got a scaffold that 401s every generated test with a failure that reads like an app bug. Now inverted: only an explicitly-detectedsummerproject getspayload:;jwt-basic,session,no-auth, andunknownall scaffoldbearer: "${TOKEN}"with an advisory comment naming the detected type. (Hit twice in one session, on two different projects.)token: nonefailed instead of sending an unauthenticated request. The natural way to write a 401 / public-endpoint test —token: none— was looked up as a token named"none"in thetokens:map, missed, and failed the test withunknown token 'none'. The working spelling (omit thetoken:line entirely) was documented nowhere.checklist_lib/runner.pynow treatsnone/null/no-auth/noauth/anonymous/false/-as "send no auth header", while a token genuinely declared under that name still wins (backward-compatible). The error for a real typo now lists the declared token names and points at the no-auth spelling. Documented inreferences/checklist.md,templates/CHECKLIST.yaml,commands/checklist.md, and the generated scaffold's own header comment..mcp.jsonmade plugin-root-absolute too (same root cause as the first item): the bundled MCP server entry was["bin/mcp-server.js"], now["${CLAUDE_PLUGIN_ROOT}/bin/mcp-server.js"]. Caveat:${CLAUDE_PLUGIN_ROOT}is only defined when the file is loaded as a plugin manifest — opening the spec-flow repo directly as a project no longer resolves it. Plugin distribution is the primary path, so that is the right trade; revert this one line if you need the repo-as-project case back.- 778 Node tests green (assertions updated for the flipped auth default), 48 Python tests green (5 new).
v0.8.0 — setup expect guard, db_ref, multi-repo auth detection
Five dogfooding findings from an external multi-repo, multi-database project. The first is a test-integrity bug; the rest are gaps that forced manual workarounds.
setup:/teardown:sqlexpect:was silently ignored — every pre-state guard was decoration.checklist_lib/setup.py_do_sql()captured the scalar and returned; it never looked atexpect:. Meanwhiletemplates/CHECKLIST.yamlandreferences/test-rigor.mdboth ship the exact form (expect: CREATED # pre-state confirmed) as the recommended way to confirm a seed landed. This is worse than having no guard: a checklist written from the template looks baseline-verified, so a wrong seed lets the test run anyway and PASS for an unrelated reason. Scalarexpect:is now a hard assertion that aborts the setup (mismatch → the test FAILs with the query and both values); dictexpect:stays descriptive, same rule as averify:block; inteardownit degrades to a warning like every other teardown failure.sql._check_scalar→sql.check_scalar(now used by two modules). Documented inreferences/checklist.md. 6 new Python tests.db_ref:— multi-database support, the DB-side twin ofbase_url_ref:.ctx["db"]was one database name for the whole run while HTTP already hadbase_urls+base_url_ref, so a feature spanning two services could only be SQL-verified on the near side; the far side had to be inferred from an HTTP side-channel (GET /{id}/status → 404), which is a weaker assertion than reading the row. Newconfig.databasesdeclares named alternates — either a plain database name (same server) or a mapping ofdatabase/host/port/user/password— anddb_ref: <name>selects one on any sql step:setup,teardown,seed, averify[]item,expect.poll, andcleanup. Fields left unset still come fromdb-creds.shdiscovery, so a second database on the same server needs only its name. An undefined ref fails rather than falling back to the default database — a silent fallback would query the wrong server and report a green PASS.db-query.shgained--host/--port/--user/--password(the existing-donly ever overrode the database name, which is why a service on another port was unreachable).lint-checklist.shnow rejects an undeclareddb_reforbase_url_refat lint time instead of mid-suite. 11 new Python tests.- Related, same class as the
expect:bug:templates/CHECKLIST.yamladvertisedconfig.db.host/port/username/password, none of which the runner reads — credentials always come fromdb-creds.sh. The template now declares onlydatabase:and says where the rest comes from. Precedence is deliberately unchanged: honouring those fields would let the template's${DB_PASS:-postgres}placeholder override a correctapplication.ymldiscovery on exactly the primary supported stack.
- Related, same class as the
detect-auth.shignoredconfig.repos, so multi-repo projects got an inverted answer. The detector ran against the cwd. In a spec-flow hub that holds only the SRS/SD while the services live in sibling repos, there is no service code to fingerprint — so a Summer/APISIX project classified as a custom-Bearer one, i.e. exactly backwards, and every generated test 401s. It now reads.spec-flow/config.json→repos, classifies each declared repo, and reconciles: one signal wins (and that repo's own hints are forwarded); repos that genuinely disagree reportCONFLICTand fall back tounknownrather than picking a scaffold that is wrong for the others; a missing repo path is reported and skipped. Single-repo projects take the unchanged path. Also fixes a latent bug this exposed —HEREwas computed aftercd "$ROOT", resolving a relative$0against the wrong directory.checklist-gentagged nearly every testsmoke. The rule wasEdge:-prefixed test-case name →regression, everything else →smoke, so an SD whose §13.2 doesn't use that naming convention (most of them) produced an all-smoke checklist:--tag smokeran the entire set and the smoke → regression escalation the skill documents stopped meaning anything. Now the first non-edge TC of each Flow is that flow's smoke test and every other TC is regression — one smoke test per user story. Suite tags reflect what their tests actually carry.- Found while testing the above:
hasApiSection(/^#{2,3}\s*9(\.\d+)?\s+API/) did not match## 9. API Design— the exact headingtemplates/sd-template.mdemits — only the### 9.2 API Endpointssubsection. An SD with §9 but no §9.x subsection silently classified asinternaland got the live-e2e scaffold instead of an HTTP stub.
- Found while testing the above:
TODO:MANUAL-REVIEWcounting had one loose copy left, and the command docs told the agent to grep by hand. 0.7.1 anchored the regex instatus-reportanddoctor, butgenSd's ownstats.todoManualReviewstill used a line-wise/TODO:MANUAL-REVIEW/— which matches the Pass-1 preamble banner it emits two lines earlier. Worse,commands/{ingest,resync}.mdjust said "count remaining markers", so the agent ran a bare grep and counted revision-history entries and sd-author'sTODO:MANUAL-REVIEW remaining: 0summary as unresolved — a clean, approved SD reported 3 outstanding TODOs and the gate blocked work that was ready. The regex now lives once, ascore.countSdTodos(), used by all four call sites; the command docs give the anchoredgrep -cE '^> \*\*TODO:MANUAL-REVIEW\*\*'and point at the reported count instead.- 780 Node tests green (2 new), 65 Python tests green (17 new).
v0.6.0 — native-task-manager drop-in engine (dark-launch)
native-task-manager — a self-built, zero-dependency drop-in replacement for the third-party task-master-ai@0.43.1 task engine.
Shipped dark-launch: taskCore.engine defaults to legacy, so nothing changes until a project opts in with taskCore.engine: "native". Removing the old package is deferred until the native engine has soaked through real features — the rollback safety net stays.
What's in it (5 subs)
- storage-core — atomic tag-keyed
tasks.jsonstore + 6 CRUD ops, byte-compatible with the legacy schema (reads legacy files, zero migration). - tags-deps — tag isolation, dependency graph with cycle detection, subtasks.
- contract-shim — dependency-free JSON-RPC MCP server (5 tools) + 9-subcommand CLI +
modelsno-op shim, byte-compatible with the legacy surface. No MCP SDK — pure Node. - ai-hybrid — agent-native AI ops driven by the orchestrator host as the LLM (zero-network core; host detected via
CLAUDECODE/SPEC_FLOW_HOST_AGENT); optional minimal headless HTTP fallback, off by default. - cutover — opt-in engine flip (one-commit / one-revert), equivalence-verify go/no-go gate,
/sf:doctorcontract check, instant rollback with zero data loss.
Verification
- 776 unit tests green.
- Live equivalence diff against the real legacy CLI; sandbox flip -> doctor -> rollback rehearsal.
- Two real bugs surfaced by a live e2e and fixed:
criticalpriority parity, and an engine-router stdout leak that corrupted the agent-native stdout channel. - Benchmarked ~29x faster per task op (~2.8s npx spawn per legacy CLI call vs ~95ms native).
Not yet done (deferred operator steps — see docs/cutover-runbook.md)
The actual engine flip, the soak through real features, and removal of the legacy dependency are supervised operator steps. This release makes the native engine available and opt-in; it does not switch anyone over.
v0.5.18 — bearer token form for pre-minted JWTs
New token-def form for /sf:manual-test auth, for services that expect a pre-minted JWT rather than a grant flow.
Added
- skills/manual-test/scripts/checklist_lib/auth.py — fourth token form
bearer: '<jwt-or-${ENV_VAR}>', resolving a literal token straight to anAuthorization: Bearer <token>header (override the header name withheader:). String fields are still${VAR}-expanded, so the token can be injected from the environment. Fails loudly (RuntimeError) when the value resolves empty, so a missing env var is never sent as an emptyBearerheader. Joins the existingkeycloak_ropc/keycloak-client-credentials/payloadforms.
Full changelog: see CHANGELOG.md
v0.5.17 — setup capture reads step block, not headers
Fixed
- skills/manual-test/scripts/checklist_lib/setup.py —
_do_httpread thecapture:map offh(the headers dict) instead ofsb(the setup step block), so anycapture:declared on a setup HTTP step silently resolved nothing — captured vars were never set. Read the map offsb.
No behavior change for setup steps without capture:.
Full changelog: see CHANGELOG.md
v0.5.16 — status/state read per-feature VERIFICATION.md
Per-feature verification state was being read from a single global file, so one feature's close-out leaked into another's status.
Fixed
- bin/flow-tools.cjs / lib/maintenance.cjs —
status-report,state-update, and doctor'sverify-integritycheck all read a single global.spec-flow/VERIFICATION.md. With per-feature specs, that surfaced a prior feature'sverifiedflag and live gaps as the active feature's status (e.g.wiki-coreshowedplatform-foundation's leftover gaps). All three now read.spec-flow/specs/<feature>/VERIFICATION.md, matching the per-feature pathtask-baselinealready used;status-reportguards a null feature.
Regression test asserts a stale global file does not leak. Tests: 117.
Full changelog: see CHANGELOG.md
v0.5.15 - taskmaster preflight + per-task test scoping
Two related fixes found while investigating why a 15-task SD's /sf:phase run was taking very long.
0.5.14 - Task Master model preflight + resilience
- New taskmaster-model-check command: zero-cost preflight that flags any Task Master role (main/research/fallback) on a keyed provider with no matching API key in env/.env, before the per-task loop can burn time on it. Wired into /sf:phase right after use-tag.
- update-task --append failures are now non-blocking: surface once, then proceed to trace-link/set_task_status instead of retrying in a loop or halting the phase.
- wave-plan's ready set is now checked before next_task; file-disjoint task batches can run in parallel instead of strictly one-at-a-time.
0.5.15 - Per-task test scoping
- The single biggest cost on a multi-task SD: verify-code's tests check ran the FULL test suite on every task close. New --task / --files "a,b" flags scope this to just the files a task touched (java-spring/java-maven derive a --tests/-Dtest= filter automatically; other stacks can configure verify.taskTestCommand). Wired into hybrid-executor's RED check and phase.md's per-task gate. The full suite now runs once, at phase close-out, instead of once per task.
Tests: 117 (110 + 7 new). See CHANGELOG.md for full detail.
v0.5.13 — missed node -e cleanup site
Follow-up to v0.5.12: one node -e JSON re-parse site was missed.
Fixed
- commands/phase.md — the
update-task --appendoverride block (Per-task loop, step 3) still chained twonode -e "JSON.parse(...)"calls to pullconfigured/previousinto shell variables. v0.5.12's cleanup covered the other 4 call sites (parse-prd x2, analyze-complexity x2, expand, research) but missed this one. Same fix applied: the agent readstaskmaster-model-plan's JSON directly and substitutesconfigured/previousas literal values into thetrap-guarded block.
Verified via full-repo grep: zero node -e "console.log(JSON.parse sites remain.
No behavior change. Tests: 104 (unchanged).
Full changelog: see CHANGELOG.md