Skip to content

Releases: tranquocthong/spec-flow

v0.15.1 — Derived state mirrors are git-ignored

Choose a tag to compare

@tranquocthong tranquocthong released this 14 Oct 14:05
12064d2

Changed

  • The two root mirrors are git-ignored, and /sf:doctor warns when a project still tracks them. .spec-flow/trace.json and .spec-flow/STATE.md are derived "active feature" mirrors, rewritten by every trace-build and state-update and shared by every concurrent session, but nothing kept them out of git. In a downstream project that ran features in parallel they were tracked: every build dirtied the tree, and two agents were seen resolving it by hand, each restoring the mirror to a different feature (one to the feature it was building, one to the last committed copy, which belonged to a third). A mirror had also been committed on main holding another feature's state. init-project now appends /.spec-flow/trace.json and /.spec-flow/STATE.md to .gitignore (idempotent; the durable specs/<feature>/ copies stay tracked), and the new state-mirror-tracked doctor check warns, with the exact git rm --cached fix, when the mirrors are tracked or sit untracked but not ignored. It never touches git itself. An existing project needs the git rm --cached once; the session re-anchor hook already tolerates a missing STATE.md, so a fresh clone simply has no mirror until the first state-update.
  • The new tests run against a git that reads no global config: a developer's global gitignore that already covers .spec-flow/ (the maintainer's does) made every ignore/track assertion depend on the machine.

v0.15.0 — Worktree cleanup is named at close

Choose a tag to compare

@tranquocthong tranquocthong released this 14 Oct 04:45
8726da7

Added

  • A ship now names the git worktrees it leaves behind. A parallel SD runs in its own linked git worktree so two sessions stop sharing STATE.md and tasks.json, but a worktree is a full checkout and nothing removed it when the work closed, so they accumulated until the disk filled. state-update --shipped now returns worktrees and a WORKTREE CLEANUP warning (path, size, branch, and the exact git worktree remove command, run from the main worktree); /sf:phase, /sf:bug and /sf:change tell the agent to end the close-out report with it. The new read-only worktree-info gives the same list on demand, covering the current directory and every config.repos root. It never removes anything: the PR is usually not merged at ship time, and a worktree cannot be removed from inside itself.

v0.14.0

Choose a tag to compare

@tranquocthong tranquocthong released this 01 Oct 07:07
cc1c1e0

Changed

  • The pre-ship code review is now off by default. It was documented as optional, but the default ask left the decision to the orchestrating agent, which in practice ran it without asking. Now config.phase.codeReview absent, null or unreadable resolves to off: review-scope returns gate: off, and /sf:phase step 3b and /sf:change step 6 skip it without offering. ask and always still work and are the opt-in; ask now has to be a real AskUserQuestion prompt, and anything but an explicit yes (no answer, no prompt shown, auto-mode) counts as no. init-project seeds and back-fills off, and /sf:phase now says in plain words that nothing but gate: ask|always authorises a review. Projects whose config already says ask (an earlier back-fill wrote it) keep asking until the key is set to off.

Added

  • expect_status: on http setup steps. A setup step aborted the whole test on any status >= 400, so a case that has to fail on purpose before its assertion (a wrong OTP submitted 2-4 times to reach a lockout, each answered 401) could never get past setup, and a capture on such a step never ran. A step can now declare the status it is meant to get: an int, a list ([400, 401]) or any, nested in http: or beside it. Undeclared steps behave as before. A declared step still fails on any other status, so it cannot go green on a 200 or a 500, and any accepts every real response but not "no response". lint-checklist.sh rejects a malformed value before the run, using the same parser as the runner.

v0.13.0 — Coding rules are enforced, not suggested

Choose a tag to compare

@tranquocthong tranquocthong released this 24 Sep 07:33
a1ac56b

Added

  • Coding rules in project-author.md are now enforced, not suggested. A team's rules reached the code only as a paragraph the executor was told to "read if present". Nothing checked them, and the pre-ship reviewer was even told "House style is not a finding", so violations reached the team's own reviewers and the PR was rejected there. Four gaps, four fixes:
    • verify.rules, a diff-scoped rule gate. forbiddenPatterns scans the whole tree on every task, so adding a rule to a project whose old code already breaks it failed every task, and in practice nobody added one. verify-code now has a code-rules check that fails only on lines this branch added or changed (merge-base with branching.base, uncommitted and untracked included) and reports the old violations as a preexisting count. Rules can be scoped by glob, and besides forbid they can say "a file with A must also have B" (when + require), for rules like "@Scheduled needs @SchedulerLock". A broken rule is skipped with a warning and never fails the gate. forbiddenPatterns is unchanged.
    • The executor answers every rule. /sf:phase pastes the ## Code Rules bullets verbatim into each executor prompt, and the executor must return a pass | n/a | violated row per bullet. A missing table or an open violated row is sent back, like missing TDD evidence.
    • The reviewer checks every rule, and a violation blocks. The "house style is not a finding" instruction is gone. Each ## Code Rules bullet is a checklist item, and a violation is a project-rule finding: review-collect raises it to at least medium and treats it as blocking, with review-accept as the override. Taste outside ## Code Rules stays low.
    • /sf:doctor validates verify.rules, and new projects get an empty ## Code Rules section in project-author.md (existing files are not touched).
  • Backlog is now a record type, not a folder of free-form notes. .spec-flow/backlog/ was a convention nothing read: no order, no status, and /sf:status reported "nothing pending" while deferred work sat there. backlog-new --title --priority high|medium|low [--desc] [--feature] [--epic] writes NNN-bl-<slug>.md (id bl-NNN, numbered from the highest existing prefix, like bug-new). Priority is required and has no silent default, since a defaulted priority carries no information. backlog-list sorts high > medium > low > unset, then oldest first, and filters by --status (default open), --epic, --feature. backlog-set changes priority or status (open|done|dropped) in place, leaving the rest of the file byte-for-byte.
    • Existing hand-written backlog files keep working: without the record marker they are listed as legacy with priority unset, and a list never rewrites them. /sf:doctor warns about them (and about records with no priority) with the backlog-set command that fixes each.
    • After a ship, the "nothing pending" next step now names the top open backlog item, and /sf:status shows the open count and the top three. With no open backlog both outputs are unchanged.
  • An epic is now a directory, not a single file. An epic spans many specs and many months, and the documents that belong to it belong to no single spec: the per-phase SRS, cross-phase decisions, environment state (SIT, UAT, handoff), gotchas, e2e mocks. epic-new used to write one .spec-flow/epics/<slug>.md with a hand-written status: pending that was wrong after the first phase, and sub-features were grouped only by a name prefix. In practice teams skipped it and built their own epics/ folder outside .spec-flow/.
    • epic-new creates .spec-flow/epics/<slug>/ with EPIC.md and conventional srs/ decisions/ state/ assets/ subdirectories. The subdirectories are a convention; nothing is enforced.
    • epic-attach --epic --feature adds a feature to an epic and writes specs/<feature>/EPIC as the back link. A feature belongs to at most one epic (EPIC_CONFLICT otherwise). /sf:ingest <srs> --epic <slug> attaches as it ingests, creating the epic if needed, which fits the common case: phases ingested one at a time over months rather than split once.
    • epic-list and the new epic-show compute progress from each sub-feature's real tasks and ship record (done, total, shipped) instead of trusting a written status. /sf:status shows the epic's progress when the active feature belongs to one.
    • SD, tasks, checklist and verification stay in specs/<feature>/; the epic directory holds only cross-phase material.
    • Single-file epics still list and show (legacy: true); epic-attach refuses them with the one-line conversion command, and nothing is migrated automatically.
    • /sf:doctor warns (never fails) about single-file epics, links that disagree in either direction, and files in an epic directory whose names look like secrets (priv_key, secret, .pem, .env…) and that git does not ignore, since .spec-flow/ is usually committed.

v0.12.1

Choose a tag to compare

@tranquocthong tranquocthong released this 24 Sep 01:46

Fixed

  • bug-new numbered its next id by counting files, not by reading the highest existing id. A count collides with a real id whenever the working tree doesn't have every file 1..N locally -- e.g. bug-004 already created on an unmerged branch/MR, while the current branch only sees 3 files and recomputes 004 for its own new bug. It now scans the NNN-bug- prefixes already on disk and assigns max + 1. Regression test added in test/flow-tools.test.cjs.

v0.12.0 — Pre-ship code review

Choose a tag to compare

@tranquocthong tranquocthong released this 23 Sep 08:08
0e03f06

Added

  • An optional code-review gate in the ship step. The agents that wrote a feature cannot review it — they are inside their own reasoning. /sf:phase close-out now offers one independent pass over the finished diff, in a context that never wrote a line of it, immediately before the commit.

    • config.phase.codeReview decides whether it runs. Three states, because the default is a question rather than a behaviour: ask (default — /sf:phase asks yes/no once per ship), always (no prompt), off (skip entirely). init-project back-fills the key into existing configs without changing behaviour, since absent already read as ask.
    • review-scope computes the scope deterministically: the base branch, <base>...HEAD (null when branching is off, when you are on base, or when the base ref will not resolve — never a bogus range), the files the executors actually wrote from file-links.json, and the repo roots on a multi-repo feature, where those files are the only reliable scope. It also reports a prior verdict and whether HEAD has moved past it, so an unchanged tip re-uses its review instead of paying for a second pass.
    • The reviewer is a sub-agent, read-only, pinned by config.models.codeReviewer (default sonnet). It reports — it never fixes. Fixing is a decision the user makes afterwards, and a post-review fix re-enters the verification gates.
    • The gate is built against its real failure mode, which is a noisy reviewer rather than a lazy one. A model handed a bare diff cannot tell a defect from a deliberate decision, so it reports both at the same severity, and after two such reviews the user declines forever and the gate is dead. So review-scope also returns a contextPack — the SD, CONTEXT.md, the checklist, VERIFICATION.md (including gaps already known and accepted), project-author.md and the trace — and the step's standing rules make the reviewer check every critical/high against the spec before writing it, cap taste at low, and name the inputs and wrong result that a blocking finding produces. A critical/high must carry checkedAgainst (the FR or TC it was tested against); review-collect returns any that do not as uncheckedBlocking, and /sf:phase sends those back for a re-rate instead of halting a ship on an unchecked claim.
    • review-collect turns the findings into specs/<feature>/CODE-REVIEW.md and a deterministic verdict: clean, advisory, or blocking on any critical/high. Blocking halts the ship and hands the user three choices — fix now, ship anyway, or abort.
    • review-accept --note records a human's "ship anyway" on disk. Without it the override would live only in a transcript, and the next session would read an unresolved blocker. /sf:status surfaces an unaccepted blocking verdict in its next step.
    • Codex: the code-review skill is a Claude Code built-in and is not part of the package. The Codex runtime contract tells the host to perform the review itself against the same output contract when it has no equivalent, and keeps the gate's shape either way — a review that was performed but never collected did not happen.
  • /sf:doctor now catches a stale local plugin install (plugin-source-sync). A plugin installed from a local directory marketplace does not run from the source repo: Claude Code copies it into ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ and runs that. The cache path is keyed by version, so while the version string holds still — exactly what a careful maintainer does between releases — edits to the source never reach the running plugin. The failure is silent and total: a new engine command answers UNKNOWN_COMMAND, an edited command doc is simply the old one. Nothing in doctor said so, and finding it by hand costs a session. The check compares the runtime surface (bin, lib, commands, agents, skills, templates, hooks) by content hash, names the differing files, explains why /plugin update will not fix it, and hands over a copy-pasteable rsync. It is silent for a normal github install, and for a contributor running the engine straight from a checkout.

Notes

  • The gate is optional; the hard gates are unchanged. verify-code, the regression sweep, and VERIFICATION.md: status: passed still guard the ship on their own, and declining the review is a silent, recorded-nowhere no-op rather than a clean verdict.

v0.11.1 — Codex support

Choose a tag to compare

@tranquocthong tranquocthong released this 15 Sep 06:39

Adds the generated Spec Flow plugin for Codex while preserving the shared project artifacts and Claude Code workflow. Includes reproducible packaging, validation, compatibility tests, and the Codex installation guide.

v0.11.0 — a green manual-test run can no longer mean nothing executed

Choose a tag to compare

@tranquocthong tranquocthong released this 15 Sep 04:22

The manual-test gate could report a green run having executed nothing, and this release makes that impossible to say. Found by auditing a downstream project's 842 test cases across 33 features, then reproduced against the runner in two lines of YAML. The plugin's own backlog note (run-checklist-no-verify-false-green.md, filed 2026-09-10) described one symptom of it; this is the root cause and the fix.

The defect

runner.py decides the outcome of a test with if errs: FAIL else: PASS. PASS means "no assertion reported an error" — not "an assertion passed." A test with nothing to send and nothing to assert produces no errors, so it passes without executing anything:

suites:
  - name: probe
    tests:
      - {id: TC-001, tags: [live-e2e, regression]}
      - {id: TC-002, tags: [no-verify, regression]}
run-checklist.sh probe.yaml --tag regression --json
  TC-001  GET
    ✓ PASS  status=0          <-- no request ever left the machine
  total: 2   passed: 2   failed: 0
{"passed": ["TC-001", "TC-002"], "failed": []}          exit 0

verify-collect then believed that count and wrote status: passed with - TC-001: verified under it. status=0 was the only tell, and it reads like a status code.

The 0.10.x lint already refuses a decorative expect: (a bare {n: 3} cannot fail a test, so it is not an assertion), but its own message offers the way around itself — "OR tag the test no-verify / live-e2e". Those tags mark a placeholder for a hand-run step, and in practice they are written alongside regression, so --tag regression selected them and counted them green. The carve-out in the lint was the entrance to the false green.

The fix

  • runner.py now separates executed from passed. A test with no request, no verify, no asserting expect key and no executing setup step is reported under a new notVerified group — named in the console summary, and emitted as "notVerified": [{id, reason}] in the --json line. It is never counted as passed. Exit code is unchanged, so nothing that shells out to the runner breaks.
  • verify-collect refuses to record evidence it does not have. An id under notVerified never becomes a - TC-xxx: verified truth. Its presence holds the status at incomplete — deliberately not verified-adhoc, which VERIFIED_STATUS_RE accepts as shippable because it asserts a human verified out of loop. Nobody has, yet. incomplete matches no gate, so G3 stays shut until someone runs those cases, writes down what they saw, and sets verified-adhoc themselves. The generated file names each owed case under ## Not verified with the reason.
  • The runner no longer synthesises a GET for a test with no request block. It used to send an empty request to base_url and print GET / status=0, which reads as a real HTTP check that passed — one downstream VERIFICATION.md had already recorded the confusion (for TC-013 the runner printed GET / status=404 / PASS). Against a live environment that stray call could also touch a real endpoint. Such a test is now labelled — asserted by setup/verify, and only its expect.poll still runs.
  • Lint gained a negative-path coverage warning. The per-test rule asks whether a test asserts anything; it never asked whether the checklist ever exercises a rejection. In the audited project 14 of 33 features had zero test cases asserting status >= 400 — every error code and guard clause unexercised while every gate read green. A warning, not a failure: it is a signal about the SD's §13.2 test cases, and existing checklists must stay runnable.

A correction worth recording, because the first two passes of this audit were wrong. An initial sweep called 228 of 842 test cases empty and flagged seven features as false-green. Both numbers were inflated. setup: steps carrying exec:/sql: are assertions — run_steps aborts on the first failure and the caller turns that into a FAIL, so exec: ... exit 1 asserts perfectly well without an expect block, and one feature's 26 test cases were built entirely that way. And most flagged VERIFICATION files carried real hand-written evidence (verified-live — 400 auth.invalid.otp, attemptsRemaining 5→4) that a status:-line check could not see. After both corrections: zero genuine false-greens on disk. The mechanism is real and reproducible; in that project humans had caught every instance by hand. What this release removes is the need to.

Task engine: MCP surface removed, CLI only

.mcp.json is deleted. The plugin declares no MCP server — every task operation goes through the bundled CLI (bin/flow-tools.cjs task-add / task-get / task-list / task-set-status / task-next, and bin/task-master for the AI ops). The native engine was already the only binding and the CLI twins have shipped since 0.8.x; what is gone is a second way in that could fail to connect, bind a stale surface, or shadow the engine from a project .mcp.json.

  • commands/init.md no longer prefers an initialize_project MCP tool — the CLI bin is the only path.
  • agents/hybrid-executor.md names flow-tools.cjs task-get instead of mcp__task-master-ai__get_task.
  • /sf:doctor dep-lock now verifies the CLI entry points exist and warns if an .mcp.json reappears at the plugin root. mcp-shadow now warns about any project-level task-master-ai entry, including the old "native" shape: with no server shipped, such an entry binds a separate implementation writing the same .taskmaster/ files.

bin/mcp-server.js and lib/mcp-server.cjs are retained but no longer referenced by any binding — the rollback and cutover machinery still exercises them on temp files. Removing them is a separate cleanup.

Full suite green: 910 engine tests (node --test test/*.test.cjs, +3) and 80 checklist-runner tests (python3 -m unittest checklist_lib.tests.test_checklist_lib, +8).

v0.10.1

Choose a tag to compare

@tranquocthong tranquocthong released this 11 Sep 08:33

Four bugs surfaced by running /sf:ingest against a real SRS in a downstream project, all in the Pass-1 deterministic harvest and the traceability engine. None block a fix directly in code — bin/flow-tools.cjs, lib/core.cjs, lib/trace.cjs.

  • Pass-1 silently truncated harvested FR/TC/NFR cells at a hard 200/220-char cap. Measured on a real SRS: every requirement sentence past ~220 chars got cut mid-word with no … and no signal — several lost only the closing period, reading as a complete sentence. The cap is now a generous backstop (2000 chars) meant only for a genuinely pathological row; ordinary harvested text (verified up to 357 chars) now survives whole. A cell that still hits the backstop is named in data.warnings (TRUNCATED: ...) instead of disappearing silently.
  • §5.2 NFR harvest read columns positionally, not by header. A heading-harvested NFR table assumed the bare Requirement | Target shape (r[0]/r[1]); a richer ID | Category | Requirement | Target table — the SD's own §5.2 convention — got its ID column read as the requirement text, its real Category dumped into Target next to the actual target value, and every row force-labeled Perf/Sec regardless of its real category (Resilience, Regression, ...). Columns now resolve by header keyword (English + Vietnamese lang packs), falling back to the old positional read only for a genuinely unlabeled 2-3 col table.
  • trace-build's §10.4 header-drift check false-positived on a repurposed section. A ### 10.4 section kept its number but retitled to unrelated content (e.g. an OUTBOX vs DIRECT route comparison — sd-template.md explicitly allows deleting/repurposing §10.4 when a feature has no state machine) was reported as a "state table header mismatch" even though the table was never meant to be a state table. The check now also requires the heading title to still carry the word "state" before warning.
  • Switching the active feature's global trace.json/STATE.md mirror was silent. trace-build and state-update already computed switchedFrom but returned it as inert JSON metadata with no severity — easy for a caller that only checks data.warnings to miss entirely. Both now push an ACTIVE FEATURE SWITCHED warning when the feature being switched away from has no ship record (still-open work); switching away from an already-shipped feature stays silent (expected churn).

All four fixed with regression tests (8 added to test/core.test.cjs + test/flow-tools.test.cjs); full suite 907/907.

v0.9.0

Choose a tag to compare

@tranquocthong tranquocthong released this 08 Sep 08:12

Nineteen rough edges from one real dogfooding session (an eid-gateway feature and a 26-FR/47-TC phase), grouped by what they actually cost. Group 1 is false-green / lost-evidence: a command reports success, or asserts nothing, while the thing it claims to check never happened. Group 2 is noise that wastes time without corrupting the result. Both groups share one theme this release keeps hitting: an engine that silently drops the SECOND (or third, or fourth) matching thing — a table, a repo, an FR, a stderr stream — instead of merging or naming what it dropped.

Group 1 — false-green / lost evidence

  • verify-collect never wrote VERIFICATION.md. It computed {status, passed, failed, truths} and returned them as JSON only — commands/manual-test.md step 4 says "This writes VERIFICATION.md," but nothing did. status-report, doctor's verify-integrity check, and task-baseline all read that file to decide a feature is verified; a full 68/68 pass reported status:"passed" on stdout while the feature stayed "not verified" forever and the ship gate never opened. It now actually writes the file, in the status: passed|failed + - TC-xxx: verified shape those readers already expect. Also fixed in the same command: --results only accepted a file path, but the doc's own worked example passes the JSON result line inline — now accepts either. --feature is required (no inference from the shared active-feature mirror — the same write-safety rule trace-link already enforces).
  • A dict expect: on a SQL verify:/setup:/teardown: step was never asserted. db-query.sh -t has no column headers to match a dict key by, so check_scalar() treated a dict expect as purely descriptive — which is also the MOST NATURAL way to write a multi-column assertion (it's exactly how expect.body works for HTTP). A real bug in one of those columns printed green. lint-checklist.sh now flags it before the run: split into one scalar verify: row per column, or assert via the HTTP response's json_path.
  • expect: had no contains operator. expect: "contains idx_foo" fell to the exact-string fallback — the whole (often multi-line) scalar result compared against the literal string "contains idx_foo", never equal, so an EXPLAIN-plan-has-this-index assertion always failed regardless of whether the index was actually there. Added contains / not contains (substring match) to check_scalar/cmp, matching what json_path: assertions already supported.
  • A failed teardown: step vanished into console-only output. teardown: is deliberately best-effort (never aborts the run), but a teardown step is often RECOVERY — undo a mutation, restore a row — and a failed recovery only printed a line to stdout; the caller had no way to know it happened. Its dirty leftover state then broke a LATER, unrelated test with no link back to the real cause. setup.run_steps() now returns the warning list instead of swallowing it; run-checklist.sh --json reports it as teardownWarnings, and verify-collect records it in VERIFICATION.md under its own heading — visible, not misattributed.
  • sd-skeleton harvested only the FIRST FR-prefixed table. An SRS with FRs split across several sub-tables (one per module — 26 FRs across 4 tables is a real, common shape) reported fr: 6 and silently dropped the rest. Same bug for the TC/NFR ID-prefix fallbacks. tableByIdPrefix now merges every matching table's rows instead of .find()-ing the first.
  • sd-skeleton's §12.2 error-code harvest never read the SRS's own §6.2 "Error & Notification Messages" table — only per-story Edge Cases bullets, even though the generated §12.2 fallback text has said "derive from SRS §6.2" since Pass-1 existed. A project that lists its error codes in that dedicated table (rather than as story edge-case prose) harvested 0, with no signal. It's now read and merged, with the table's own Message column used as the real user-facing message instead of a TODO.
  • trace-build built tables purely by content-matched header keywords, with no cross-check against the section NUMBER. A §12.2 (or §5.1/§13.2/§10.4/§5.2) table whose column headers drifted — translated, renamed — silently produced 0 nodes for that whole section: the SD reads complete, the trace quietly drops it. Now warns by name when the numbered section exists with a table the content-matcher didn't recognize, quoting the actual headers found.
  • trace-build never flagged an FR with zero linked TCs. An orphaned requirement — present in §5.1, uncovered by any §13.2 test — built a valid-looking trace. Now warned by FR id.
  • checklist-gen's auth detection scanned the WHOLE multi-repo hub, majority-voting across every config.repos entry — a feature scoped to one no-auth/HMAC repo in a hub of Summer/APISIX services got the hub's summer classification and 401'd every generated test. Now resolves the feature's own repo scope first (--repos > declared trace-repos > file-links inference — the same precedence verify-code already uses) and detects against that one repo when it resolves unambiguously.
  • detect-auth.sh defaulted an unrecognized custom scheme to no-auth. A gateway verifying its own HMAC/signature header (no Spring Security dep, no Authorization: Bearer) has real access control, and no-auth scaffolds unsigned tests that never exercise it. Added an HMAC/signature-pattern check (X-Signature, Mac.getInstance("Hmac..."), crypto.createHmac, …) that now classifies as unknown instead — same safe-default fix as the 0.7.4 Summer/X-Userinfo flip, one auth family later.
  • parse-prd/expand, run as a bare CLI subcommand, look like they finish in one shot — they don't. Default taskCore.aiMode is agent-native: the command prints a GenerationSpec and exits 0 without calling an LLM (zero-network by design). commands/ingest.md and commands/phase.md presented it as a single self-sufficient bash command; the agent could read exit 0 + a large JSON blob as "done," run use-tag successfully, and leave tasks.json with zero tasks for that tag — silent, textbook false-success. Both docs now spell out the handoff: parse the printed spec, generate the Task[] yourself (Phase 2), then tasks-import --file <path> (Phase 3) — {"imported": N} is the real signal, not the Phase 1 exit code.

Group 2 — noise, not wrong

  • srs-snapshot's filename-derived-slug warning only fired for a date-prefixed filename. Any other filename shape (phase-3-agentgw-client.md, drifting from a project's own naming convention) was silently adopted. Now warns whenever the slug came from the filename at all (no Feature: line, no --feature).
  • checklist-gen leaked detect-auth.sh's stderr diagnostics into a caller that merges stdout+stderr. The engine's own stdout stayed pure JSON, but execFileSync's default stdio inherits the child's stderr straight into this process's — any 2>&1 capture (or a tool wrapper that combines streams) saw prose land BEFORE the JSON and failed to parse it, even though the command had already succeeded. Now explicitly captured and discarded.
  • checklist-gen always scaffolded config.db/config.redis/cleanup:, even for a phase with no persistence at all. sd-template.md's own §7 convention is "no DB change → delete this section" — now honored: no §7 (or a §7 that says N/A/stateless/no database), no db/redis/cleanup block, with a warning explaining why.
  • checklist-gen silently degenerated to one suite per test. When every §13.2 TC row carries a distinct Flow value (common for business-rule-derived TCs — 26 rules, 28 suites), grouping-by-Flow produces 28 singleton suites, and a singleton suite's lone test is ALWAYS tagged smoke, never regression (the rule needs ≥2 tests per flow to ever emit a regression tag) — so --tag regression silently skips the entire checklist with no error. Now warned by name, with the count.
  • genSd's §10.4 State Management forced a TODO:MANUAL-REVIEW marker on every internal/hybrid phase, even one with no state machine at all. sd-template.md's own convention is "no state → delete this sub-section," the same carve-out §7 gets — but Pass-1 never offered it, so a plain batch job or pure transform carried a permanently unresolvable marker blocking approval until someone hand-deleted the section. Now a plain advisory note, not a TODO.

857 Node tests green (15 new), 68 Python checklist_lib unit tests green (3 new).