Skip to content

Releases: tranquocthong/claude-spec-flow

v0.8.2 — escaped pipes in SD cells, header-based column mapping

Choose a tag to compare

@tranquocthong tranquocthong released this 13 Aug 01:09

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

Choose a tag to compare

@tranquocthong tranquocthong released this 05 Aug 04:29

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

Choose a tag to compare

@tranquocthong tranquocthong released this 04 Aug 06:06

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-master was invoked cwd-relative in the skill instructions. commands/{phase,ingest,init,change,resync}.md and agents/hybrid-executor.md told the agent to run node bin/task-master …, but those commands execute in the user's project, which has no bin/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. Now node ${CLAUDE_PLUGIN_ROOT}/bin/task-master …, matching how every flow-tools.cjs invocation was already written. Root cause: scripts/cutover.cjs's NATIVE_CLI_PREFIX (v0.7.0) rewrote the old npx invocations 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.sh missed custom Authorization: Bearer schemes, and checklist-gen defaulted the wrong way. Two compounding bugs. (1) The detector classifies by dependency fingerprint (jsonwebtoken, jjwt, pyjwt, …), so a service that reads the Authorization header itself and validates an opaque API key — no JWT library anywhere — fell through to unknown. It now also greps the source for header-read + Bearer prefix, across every stack, and reports jwt-basic (same wire form; only how you mint the token differs). (2) checklist-gen treated the Summer/APISIX payload:/X-Userinfo form as the default and bearer: as the special case — exactly backwards. X-Userinfo is 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-detected summer project gets payload:; jwt-basic, session, no-auth, and unknown all scaffold bearer: "${TOKEN}" with an advisory comment naming the detected type. (Hit twice in one session, on two different projects.)
  • token: none failed 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 the tokens: map, missed, and failed the test with unknown token 'none'. The working spelling (omit the token: line entirely) was documented nowhere. checklist_lib/runner.py now treats none / 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 in references/checklist.md, templates/CHECKLIST.yaml, commands/checklist.md, and the generated scaffold's own header comment.
  • .mcp.json made 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

Choose a tag to compare

@tranquocthong tranquocthong released this 03 Aug 09:18

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: sql expect: was silently ignored — every pre-state guard was decoration. checklist_lib/setup.py _do_sql() captured the scalar and returned; it never looked at expect:. Meanwhile templates/CHECKLIST.yaml and references/test-rigor.md both 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. Scalar expect: is now a hard assertion that aborts the setup (mismatch → the test FAILs with the query and both values); dict expect: stays descriptive, same rule as a verify: block; in teardown it degrades to a warning like every other teardown failure. sql._check_scalarsql.check_scalar (now used by two modules). Documented in references/checklist.md. 6 new Python tests.
  • db_ref: — multi-database support, the DB-side twin of base_url_ref:. ctx["db"] was one database name for the whole run while HTTP already had base_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. New config.databases declares named alternates — either a plain database name (same server) or a mapping of database/host/port/user/password — and db_ref: <name> selects one on any sql step: setup, teardown, seed, a verify[] item, expect.poll, and cleanup. Fields left unset still come from db-creds.sh discovery, 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.sh gained --host/--port/--user/--password (the existing -d only ever overrode the database name, which is why a service on another port was unreachable). lint-checklist.sh now rejects an undeclared db_ref or base_url_ref at lint time instead of mid-suite. 11 new Python tests.
    • Related, same class as the expect: bug: templates/CHECKLIST.yaml advertised config.db.host/port/username/password, none of which the runner reads — credentials always come from db-creds.sh. The template now declares only database: 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 correct application.yml discovery on exactly the primary supported stack.
  • detect-auth.sh ignored config.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.jsonrepos, classifies each declared repo, and reconciles: one signal wins (and that repo's own hints are forwarded); repos that genuinely disagree report CONFLICT and fall back to unknown rather 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 — HERE was computed after cd "$ROOT", resolving a relative $0 against the wrong directory.
  • checklist-gen tagged nearly every test smoke. The rule was Edge:-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 smoke ran 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 heading templates/sd-template.md emits — only the ### 9.2 API Endpoints subsection. An SD with §9 but no §9.x subsection silently classified as internal and got the live-e2e scaffold instead of an HTTP stub.
  • TODO:MANUAL-REVIEW counting had one loose copy left, and the command docs told the agent to grep by hand. 0.7.1 anchored the regex in status-report and doctor, but genSd's own stats.todoManualReview still used a line-wise /TODO:MANUAL-REVIEW/ — which matches the Pass-1 preamble banner it emits two lines earlier. Worse, commands/{ingest,resync}.md just said "count remaining markers", so the agent ran a bare grep and counted revision-history entries and sd-author's TODO:MANUAL-REVIEW remaining: 0 summary as unresolved — a clean, approved SD reported 3 outstanding TODOs and the gate blocked work that was ready. The regex now lives once, as core.countSdTodos(), used by all four call sites; the command docs give the anchored grep -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)

Choose a tag to compare

@tranquocthong tranquocthong released this 28 Jul 04:56

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.json store + 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 + models no-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:doctor contract 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: critical priority 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

Choose a tag to compare

@tranquocthong tranquocthong released this 20 Jul 23:40

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 an Authorization: Bearer <token> header (override the header name with header:). 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 empty Bearer header. Joins the existing keycloak_ropc / keycloak-client-credentials / payload forms.

Full changelog: see CHANGELOG.md

v0.5.17 — setup capture reads step block, not headers

Choose a tag to compare

@tranquocthong tranquocthong released this 20 Jul 23:40

Fixed

  • skills/manual-test/scripts/checklist_lib/setup.py_do_http read the capture: map off h (the headers dict) instead of sb (the setup step block), so any capture: declared on a setup HTTP step silently resolved nothing — captured vars were never set. Read the map off sb.

No behavior change for setup steps without capture:.

Full changelog: see CHANGELOG.md

v0.5.16 — status/state read per-feature VERIFICATION.md

Choose a tag to compare

@tranquocthong tranquocthong released this 20 Jul 23:40

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.cjsstatus-report, state-update, and doctor's verify-integrity check all read a single global .spec-flow/VERIFICATION.md. With per-feature specs, that surfaced a prior feature's verified flag and live gaps as the active feature's status (e.g. wiki-core showed platform-foundation's leftover gaps). All three now read .spec-flow/specs/<feature>/VERIFICATION.md, matching the per-feature path task-baseline already used; status-report guards 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

Choose a tag to compare

@tranquocthong tranquocthong released this 20 Jul 18:12

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

Choose a tag to compare

@tranquocthong tranquocthong released this 11 Jul 01:08

Follow-up to v0.5.12: one node -e JSON re-parse site was missed.

Fixed

  • commands/phase.md — the update-task --append override block (Per-task loop, step 3) still chained two node -e "JSON.parse(...)" calls to pull configured/previous into 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 reads taskmaster-model-plan's JSON directly and substitutes configured/previous as literal values into the trap-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