name: gate-check description: Run the project gate checks and report results. Use after completing any feature, before creating a PR, or to verify project health. argument-hint: '[--fix to auto-fix lint issues]'
Gate Check
Run the project's gating checks and report a clear pass/fail for each.
Layout + Tracker Mode Probes
Before running any commands:
- Tracker Mode Probe — Run the Schema L probe (see
docs/patterns.md§ Tracker Mode Probe). IfCLAUDE.mdhas no## Task Trackingsection, mode isnoneand tracker-mode hooks skip. If a tracker mode is active:- Run the 2-tier ticket-binding resolver and mandatory confirmation prompt per
docs/ticket-binding.md. Decline exits cleanly with zero side effects. - Re-fetch the ticket's
updatedAtand warn on mismatch against the value recorded at/implementstart; do NOT run bidirectional AC sync resolution here. - On gate pass, push the AC toggle via the active adapter's
push_ac_toggle(capability permitting; missing capability degrades with a canonical-shape warning otherwise). Seedocs/gate-check-tracker-mode.mdfor the full tracker-mode flow.
- Run the 2-tier ticket-binding resolver and mandatory confirmation prompt per
Conformance Probes (NFR-15)
Run these deterministic layout-invariant probes in addition to the normal gate:
Filename ↔ frontmatter convention (strict) — for every
specs/frs/**/*.md, resolveProvideronce (same rule as/implement), parse the YAML frontmatter, and compute the expected base name viaProvider.filenameFor(spec). Every base name must equalProvider.filenameFor(spec); any mismatch → GATE FAILED naming the file, the actual basename, and the expected name.Required frontmatter fields — every FR file must have the mode-invariant Schema Q keys
title,milestone,status,archived_at,created_atpresent. Theidandtrackerkeys are mode-conditional (required inmode: noneand tracker mode respectively, absent in the other), enforced by probe #13identity_mode_conditional. Missing a field → GATE FAILED naming the file and field.Stale lock scan — list every
.dpt-locks/<ulid>entry whosebranchfield names a merged-into-main or deleted branch. Each stale lock → GATE PASSED WITH NOTES. Offer$ARGUMENTS --cleanup-stale-locksaction that deletes them in a single commit.Plan post-freeze edit scan — for each
specs/plan/<M#>.mdwithstatus: active+ non-nullfrozen_at, scangit log --followfor commits to that path authored afterfrozen_at. Each post-freeze commit → GATE PASSED WITH NOTES listing the SHA. No auto-revert — user decides.Stale release marker scan — grep
specs/requirements.mdfor markers of the form(in flight — v<X.Y.Z>)or(planned — v<X.Y.Z>). For each captured version, check whetherCHANGELOG.mdalready contains a## [X.Y.Z]header (i.e., that version has shipped). Every match where the CHANGELOG says shipped → GATE PASSED WITH NOTES listing the stale marker + its line number + the shipped release so the operator can rewrite the overview to past-tense. Warn-only, never GATE FAILED — prose drift shouldn't block the gate.Per-milestone heading strip — grep
specs/technical-spec.mdandspecs/testing-spec.mdfor^#{1,3} M\d+(matches# M<N>:,## <N>. M<N> — …, or any other milestone-framed heading). Any match → GATE FAILED naming the file and line with a pointer to the cross-cutting spec hygiene rule (cross-cutting spec files carry zero per-milestone headings). Per-FR design / per-milestone narrative belongs inspecs/frs/<name>.mdorspecs/plan/<M#>.md, not in the cross-cutting spec files.Duplicate AC-prefix scan — call
acLint(specsDir)fromadapters/_shared/src/ac_lint.ts. It walks every activespecs/frs/*.md(excludingarchive/), extracts each file's## Acceptance Criteriasection, and countsAC-<prefix>.<N>occurrences per file. Any count > 1 → GATE FAILED naming the file +AC-<prefix>.<N>pair + occurrence count. The<prefix>is tracker-mode-aware (tracker ID or short-ULID tail), so both prefix shapes are checked by the same probe.Ticket-state drift — for every FR file under
specs/frs/archive/whose frontmatter hasstatus: archivedand a non-nulltracker.<key>binding, resolveProvideronce (same rule as/implement) and callProvider.getTicketStatus(<tracker-ref>). Every drifted ticket → GATE FAILED reporting a row with the FR's ULID + tracker ID + observed vs. expected state so the operator can manually transition or rerun/implement. Skipped formode: none—LocalProvider.getTicketStatusreturns thelocal-no-trackersentinel and there's nothing to compare. The check catches the regression where/implementPhase 4 lands a commit + archives FRs but dropsreleaseLock, stranding the tracker atIn Progressafter the commit — the write-path hardening landed earlier, and this probe is the read-side backstop.Tolerance wrapper integration (STE-305). The comparison against the adapter's
status_mapping.donecanonical name is routed throughwithTolerance(...)fromadapters/_shared/src/tracker_tolerance.tsrather than a bare strict-equality check. The wrapper maps the observed tracker label to its canonical role viaspecs/tracker-config.yamlroles:; when the mapped role equals the expected role (done) the probe passes, when the mapped role differs the probe surfaces genuine drift and GATE FAILED with the row shape above. Under non-tty (claude -p) execution the wrapper additionally routes any non-fatal label-shape advisory through the closing-summary capability tokentracker_status_advisory_non_tty— the row is rendered as ADVISORY (not GATE FAILED) so harness runs are not blocked by transient label-shape signals while the byte-checkable capability key preserves audit traceability.Root spec hygiene — call
runRootHygiene(specsDir, pluginJsonPath)fromadapters/_shared/src/root_hygiene.ts. It runs two sub-checks on the three root spec files (specs/requirements.md,specs/technical-spec.md,specs/testing-spec.md):- (a) Milestone-ID leakage — scans for
\bM\d+\btokens, walks up to the containing##/###heading, skips matches under the allowlist (Shipped milestones/Archived context/Shipped releases/Release notes/Release history), then reports any remaining match that resolves to an existingspecs/plan/archive/M<N>.md. Each hit → GATE FAILED with<file>:<line>: archived milestone M<N> in live-framing (heading "<title>"). - (b) Version/status freshness — reads
plugin.jsonversion; parsesrequirements.md§1 Overview forLatest shipped release: vX.Y.Zand optionalIn-flight milestone: M<N>lines. Declared version must matchplugin.json; in-flight milestone (if named) must resolve to a livespecs/plan/M<N>.md, not the archive. Each drift → GATE FAILED naming the specific line + observed vs. expected value. Existence-guarded: when.claude-plugin/plugin.jsonis absent (the common end-user case — only Claude Code plugin projects carry that manifest), the version-match arm of this sub-check is skipped and emits ann/arow with the reason "no plugin manifest in this project — probe skipped".RootHygieneReport.versionFreshnessSkippedcarries the n/a marker; the milestone-leakage sub-check (a) still runs unchanged. When the manifest is present (toolkit self-run), this sub-check fires unchanged and gates on drift as before.
Enforces the "root specs stay shape-only, current-only" invariant documented in
docs/patterns.md§ Root Spec Hygiene.- (a) Milestone-ID leakage — scans for
CLAUDE.md.template branch_template: hygiene — existence-guarded: if
plugins/dev-process-toolkit/templates/CLAUDE.md.templateis absent (the common end-user case — only the toolkit repo itself carries that path), this probe is skipped and emits ann/arow with the reason "toolkit template not present — probe skipped". When the template is present (toolkit self-run), grep it for the literal substringbranch_template:. Zero matches → GATE FAILED with the standard hygiene-gate error shape, pointing to the template file and asking the operator to re-add the key documentation. This gate protects/setupstep 7c's seeded-key contract: if the template silently loses itsbranch_template:documentation, downstream projects generated by/setupwill have branch automation silently disabled even when the user wants it (the backward-compat reading is "absent ⇒ disabled" — the template's job is to advertise the key so users know it exists).Tracker-mode ULID prose hygiene — tracker mode only (skipped when
mode: none). Grepspecs/plan/*.md(active only, excludingarchive/), the current release section ofCHANGELOG.md(content after the topmost## [X.Y.Z]heading and before the next## [heading), andREADME.mdfor the patternfr_[0-9A-HJKMNP-TV-Z]{26}(the full ULID regex). Each hit → GATE PASSED WITH NOTES listing<file>:<line>so the operator can rewrite the prose to use the tracker ID instead. Warn-only, never GATE FAILED — pre-existing content shouldn't block merges. Skipped entirely inmode: none(the full 26-char ULID does not appear in user-facing prose there either — the short-ULID tail is the human-facing form — but the probe is tracker-mode-scoped to avoid any risk of false-positive chatter on projects that hand-wrote ULIDs into historical docs).docs/README.md nav contract — docs-mode only (skipped when
readDocsConfig(CLAUDE.md)reports bothuser_facing_modeandpackages_modefalse). CallrunNavContractProbe(projectRoot)fromadapters/_shared/src/docs_nav_contract.ts. The probe (a) reads the## Docssection to decide whether to run, (b) parsesdocs/README.md, and (c) asserts exactly four##-level headings carrying the canonical{#tutorials},{#how-to},{#reference},{#explanation}anchors, each with a relative link resolving to an existing file or directory. Missing anchor, extra##-level heading, or broken subdirectory reference → GATE FAILED, one note per violation infile:line — reasonshape, with the NFR-10 remedy:/gate-check: docs/README.md nav contract violation. Remedy: docs/README.md must contain exactly four ##-level headings with {#tutorials}, {#how-to}, {#reference}, {#explanation} anchors, each linking to an existing file or directory. Run /docs --full to regenerate the canonical tree. Context: mode=<docs-mode>, skill=gate-checkIdentity mode conditional — call
runIdentityModeConditionalProbe(projectRoot)fromadapters/_shared/src/identity_mode_conditional.ts. The probe resolves the CLAUDE.md## Task Trackingmode once, then walks every activespecs/frs/*.md(archive excluded) and enforces the bimodal identity invariant:mode: nonerequires a validid: fr_<26-char ULID>line; any tracker mode requires theid:line to be absent. Violations surface asfile:line — reasonnotes in NFR-10 canonical shape. Severity is error (any violation → GATE FAILED) — the prior warn-only severity flipped to error once/spec-write's tracker-mode template stopped emittingid:, removing the regression source. Zero runtime dep onulid.ts— the ULID regex is inlined so the probe never pulls the module it's enforcing boundaries around.Ticket-state drift — active side — mirrors probe #8 symmetrically with two carve-outs. For every FR file under
specs/frs/*.md(excludingarchive/**) whose frontmatter hasstatus: activeAND a non-nulltracker.<key>binding, resolveProvideronce (same rule as/implement/ probe #8) and callProvider.getTicketStatus(<tracker-ref>). Then read the FR's milestone plan state viareadPlanTaskState(specsDir, milestone)fromadapters/_shared/src/plan_task_state.tsand decide viaactiveTicketDriftPasses(summary, planTaskState, statusMapping, currentUser)fromadapters/_shared/src/active_ticket_drift_predicate.ts. The composed predicate passes if (a) the ticket is in thestatus_mapping.in_progresslane ANDassignee == currentUser, OR (b) the single-FR-clean (STE-151) exemption applies — ticket=done, plan=active, plan has at least one[ ]task remaining, OR (c) the fully-checked-single-FR (STE-180) exemption applies — ticket=done, plan=active, plan has zero[ ]tasks AND at least one task total. Both exemptions recognise the canonical mid-canonical-chain state where/implement <FR-id>Phase 4 Close transitioned the ticket to Done but the FR file intentionally staysstatus: activeper the milestone-bulk-archive design (skills/implement/SKILL.md§ Invocation forms). Truth table:FR status Ticket status Assignee Plan tasks Plan status Outcome active in_progress currentUser any any pass active done any unchecked > 0 active pass (single-FR clean — STE-151 carve-out) active done any all checked + total > 0 active pass + advisory (fully-checked — STE-180 carve-out) active done any total = 0 active fail (empty/malformed plan, strict fallback) active done any any missing fail (strict fallback) active done any any archived vacuous (probe #27 owns) active backlog/etc. any any any fail (drift) active in_progress != current any any fail (drift) Advisory (STE-180 carve-out only). When the fully-checked branch fires, render exactly this advisory line in the gate-check report (severity = note, NOT GATE FAILED):
M<N> plan fully checked but not archived — run /spec-archive M<N> or /implement M<N> to close. The probe returns{ ok: true, advisory: { kind: "milestone_ready_to_close", milestone: "M<N>" } }and the renderer formats it; carve-out provenance is logged viacarve_out: fully_checked_milestone(vs. STE-151'scarve_out: partial_milestone). One advisory per affected milestone, deduplicated — if FRs A and B both target M5 and both fire the carve-out, render once mentioning M5.Every drifted ticket → GATE FAILED reporting a row with the FR's ULID + tracker ID + observed status vs. expected
in_progress+ observed assignee vs. expectedcurrentUserso the operator can manually transition, reassign, or rerun/implementPhase 1 step 0.c. The failure-row shape is unchanged across the strict and relaxed branches — callers see the same row format. Skipped formode: none—LocalProvider.getTicketStatusreturns thelocal-no-trackersentinel and there's nothing to compare. The probe is over observed tracker state, not over call-history: thealready-oursclaim shape (in_progress+currentUser) passes regardless of whether this session or a prior one calledclaimLock. Catches the regression where/implementPhase 1 step 0.c is skipped entirely (an early dogfood case had a ticket sitting at Backlog while implementation proceeded) and symmetrically completes the archive-side ticket-state probe. Both carve-outs strictly weaken the predicate — every prior failing case where the drift was real still fails (Phase 1 step 0.c skipped, wrong assignee, missing plan, archived plan vacuous to probe #27).Tolerance wrapper integration (STE-305). The
in_progress-lane membership check is routed throughwithTolerance(...)fromadapters/_shared/src/tracker_tolerance.tsrather than a bare strict-equality match againststatus_mapping.in_progress. The wrapper maps the observed tracker label to its canonical role viaspecs/tracker-config.yamlroles:; when the mapped role equalsin_progressthe lane check passes, and when the mapped role does not match the expected role the probe surfaces genuine drift and GATE FAILED with the row shape above (mismatch in mapped role is preserved as a hard failure — the wrapper relaxes label-shape brittleness, not the semantic predicate). Under non-tty (claude -p) execution the wrapper additionally routes any non-fatal label-shape advisory through the closing-summary capability tokentracker_status_advisory_non_tty— the row is rendered as ADVISORY (not GATE FAILED) so harness runs are not blocked by transient label-shape signals while the byte-checkable capability key preserves audit traceability.Guessed tracker-ID scan — for each
specs/frs/*.md(active, non-archive) whose frontmatter has a boundtracker.<key>, parse everyAC-<PREFIX>.<N>line (shape fromacPrefix). Every<PREFIX>must equal the file's owntracker.<key>value. Mismatch → GATE FAILED naming the file, the offending prefix used in an AC line, and the expected tracker ID. NFR-10 remedy: "AC prefix does not match the file's bound tracker — did you draft with a guessed ID? Substitute via the<tracker-id>placeholder convention and re-save." Skipped formode: none— short-ULID prefixes are scanned by the existingac_prefixduplicate-scan suite; the two scopes are mutually exclusive. Catches the regression where an FR is drafted with a guessed tracker number that doesn't match the allocator's return (origin: an early draft session narrated a specific unallocated ID before the Linear allocator responded).Archive plan-status invariant — call
runArchivePlanStatusProbe(projectRoot)fromadapters/_shared/src/archive_plan_status.ts. The probe walks everyspecs/plan/archive/M*.mdand asserts frontmatterstatus: archivedAND a non-nullarchived_atISO-8601 string. Any drift → GATE FAILED with afile:line — reasonnote in NFR-10 canonical shape (observed vs. expected). Defense-in-depth — an iteration-5 audit downgrade confirmed no live code path consumes archived plan status today (everyspecs/plan/reader excludesarchive/), but/implementPhase 4's plan-status flip prose plus this read-side probe close the door on future drift. Test coverage:tests/gate-check-archive-plan-status.test.tsper the probe-authoring contract.setup-output-completeness— callrunSetupOutputCompletenessProbe(projectRoot)fromadapters/_shared/src/setup_output_completeness.ts. If CLAUDE.md## Task Trackingdeclaresmode: <tracker>(≠ none),.mcp.jsonMUST exist at project root with the correspondingmcpServers.<adapter>entry. Skipped when mode = none or CLAUDE.md is absent. Catches the smoke-test failure mode where/setupself-aborted on.mcp.jsonwrites and silently moved on. Test coverage:tests/gate-check-setup-output-completeness.test.ts.claudemd-docs-section-present— callrunClaudeMdDocsSectionProbe(projectRoot)fromadapters/_shared/src/claudemd_docs_section.ts. If CLAUDE.md exists, it MUST contain a real (non-commented)## Docsheading. Vacuous when CLAUDE.md is absent. Sibling probe to existing## Task Trackingchecks; closes the silent feature-drop where/docsbecomes a no-op because/setupskipped emitting the section. Test coverage:tests/gate-check-claudemd-docs-section.test.ts.setup-audit-section-presence— callrunSetupAuditSectionPresenceProbe(projectRoot)fromadapters/_shared/src/setup_audit_section_presence.ts. If CLAUDE.md is toolkit-managed (carries the<!-- generated by /dev-process-toolkit:setup -->marker) AND any default-applied step outcome is detectable (branch_template:populated or## Docsblock present), the## /setup auditsection MUST exist. Vacuous on hand-written files (no marker) or fully-interactive runs that emitted no defaults. Test coverage:tests/gate-check-setup-audit-section-presence.test.ts.bun-zero-match-placeholder— callrunBunZeroMatchPlaceholderProbe(projectRoot)fromadapters/_shared/src/bun_zero_match_placeholder.ts. Ifbun.lockexists AND no*.test.tsfile matches outsidenode_modulesAND no source carries the marker commentBun zero-match workaround, fail. The probe enforces the/setupstep 2c scaffolding contract that preventsbun test's zero-match-exit-1 from killing the very first gate-check on a fresh Bun project. Vacuous on non-Bun projects (nobun.lock). Background:examples/bun-typescript.md. Test coverage:tests/gate-check-bun-zero-match-placeholder.test.ts.task-tracking-canonical-keys— callrunTaskTrackingCanonicalKeysProbe(projectRoot)fromadapters/_shared/src/task_tracking_canonical_keys.ts. Parses## Task Trackingline-by-line; fails if any top-level key is outside the closed set{mode, mcp_server, jira_ac_field, branch_template}. Empty/whitespace lines and### <Subsection>content (e.g.### Linear) are scoped out — tracker-specific metadata (project IDs, team names) belongs under sub-headings, not as Schema L keys. Vacuous when the section is absent (mode: nonecanonical form). Catches the smoke-test drift where/setupemitted non-canonicallinear_*keys at the top level. Migration helper:scripts/migrate-task-tracking-canonical.ts(dry-run only, prints unified diff to stdout). Test coverage:tests/gate-check-task-tracking-canonical-keys.test.ts.toolkit-bootstrap-committed— callrunToolkitBootstrapCommittedProbe(projectRoot)fromadapters/_shared/src/toolkit_bootstrap_committed.ts. If CLAUDE.md is toolkit-managed (carries<!-- generated by /dev-process-toolkit:setup -->) AND the project is a git repository, every toolkit-managed file in the working tree MUST be committed (no??orMstatus ingit status --porcelain). Scan-set widened by STE-179:CLAUDE.md,.claude/settings.json,.mcp.json, every active file underspecs/(specs/requirements.md,specs/technical-spec.md,specs/testing-spec.md,specs/plan/M*.md,specs/frs/*.md— archive excluded). Owner skill by file: CLAUDE.md/settings/mcp ⇒/setupbootstrap; specs/* ⇒/spec-writeStep 7a; per-FR feature files ⇒/implementPhase 4. Catches the regression where toolkit-skill outputs leak into a downstream commit. Vacuous when CLAUDE.md is absent, the file is hand-written (no marker), or the project is not a git repo. Test coverage:tests/gate-check-toolkit-bootstrap-committed.test.ts.traceability-link-validity— callrunTraceabilityLinkValidityProbe(projectRoot)fromadapters/_shared/src/traceability_link_validity.ts. Everyfrs/<id>.mdreference (any link form:](frs/X.md),](./frs/X.md), bare path) inspecs/requirements.mdand anyspecs/plan/<M>.mdmust resolve to an existing file underspecs/frs/<id>.mdORspecs/frs/archive/<id>.md. Broken links (e.g. live-path link when the file moved to archive) → GATE FAILED. Catches the smoke-test failure where/spec-archivemoved an FR but the traceability matrix still pointed at the live path. Companion to/spec-archive's "Rewrite traceability links" step (adapters/_shared/src/spec_archive/rewrite_links.ts). Test coverage:tests/gate-check-traceability-link-validity.test.ts.signature-strategy-honors-setup— callrunSignatureStrategyHonorsSetupProbe(projectRoot)fromadapters/_shared/src/signature_strategy_honors_setup.ts. Reads the per-stack preferred signature-extraction strategy recorded by/setupatdocs/.dpt-docs-toolchain.jsonand asserts each non-fallback recording —typedoc,dart-analyzer,griffe— still maps to a present toolchain viaprobeToolchains(). Vacuous when the config file is absent (projects whose/setupruns recorded no preference). Recordedregex-fallbackentries skip cleanly — there is no degradation possible. Recorded preferred tool gone missing → GATE FAILED withfile:line — reasonnote plus the standard NFR-10 remedy "Re-install the missing toolchain or re-run /setup to update the recorded preference." Test coverage:tests/gate-check-signature-strategy-honors-setup.test.ts.task-tracking-workspace-binding-present— callrunTaskTrackingWorkspaceBindingPresentProbe(projectRoot)fromadapters/_shared/src/task_tracking_workspace_binding_present.ts. In tracker mode, the## Task Trackingblock MUST carry a populated### Linear/### Jirasub-section identifying the workspace binding (Linear: team + project; Jira: project). Vacuous when CLAUDE.md is absent, the section is absent,mode: noneis explicit, or a custom (non-linear/non-jira) adapter is active. Sub-section absent OR required key missing OR value empty/whitespace-only → GATE FAILED withfile:line — reasonnote plus NFR-10 canonical remedy pointing atscripts/migrate-task-tracking-add-workspace.ts. Catches the silent-landing trap where tickets created without aprojectfield land outside the user's expected project board. Test coverage:tests/gate-check-task-tracking-workspace-binding-present.test.ts.tracker-project-milestone-attached— callrunTrackerProjectMilestoneAttachedProbe(projectRoot, deps)fromadapters/_shared/src/tracker_project_milestone_attached.ts. For eachstatus: activeFR with a tracker block, fetch the issue viadeps.getIssue(<ticket-id>)and verify the milestone landed on the ticket. The verification surface is adapter-aware, selected bydeps.milestoneBinding(wired in production from the active adapter'smilestone_binding:Schema M frontmatter; defaults toobjectwhen absent): theobjectbinding (Linear;mcp__linear__get_issue) asserts the issue'sprojectMilestone.namebyte-equals the canonical local heading (parsed fromspecs/plan/M<N>.md's H1, anchor stripped); thelabelbinding (Jira;mcp__atlassian__getJiraIssue) asserts the issue'slabelsarray containsmilestone-<M-token>(the leadingM\d+of that same canonical heading) — labels are Jira's create-on-write milestone surface, since the atlassian MCP exposes no milestone object. Vacuous whenmode: none, the FR is archived, the FR has no tracker block, or the plan file is missing (probe #27 owns that diagnostic). Missing or mismatched binding (object: name mismatch; label: the expectedmilestone-<M-token>absent) → GATE FAILED rendering the offending names/labels byte-by-byte (em-dash drift visible) plus NFR-10 remedy pointing at/spec-write --rename-milestone M<N>for the rename-on-mismatch escape hatch.Capability-gap downgrade (STE-194 + STE-214). When the FR's
## Notessection contains a word-bounded match of any of the three milestone-attach capability keys emitted by/spec-writeperskills/spec-write/SKILL.md§ Step 7 —milestone_attach_skipped_adapter_limit(canonical),milestone_attach_unavailable(deprecated alias per STE-198, honored for one minor-version cycle), ormilestone_create_required— AND the ticket has noprojectMilestonebinding, the missing-binding outcome routes to ADVISORY (severity = note, NOT GATE FAILED) with the canonical prosemilestone-attach skipped — capability gap declared in FR Notes (<key-found>). Detection rule: case-sensitive, word-bounded match (\b<key>\b) scoped to the FR's## Notesbody — substring matches (xmilestone_attach_skipped_adapter_limitX) and matches in other sections (## Acceptance Criteria,## Technical Design) do NOT count. Mismatched bindings still hard-fail; the token only excuses absence, not divergence. Token-present + binding-present is PASS (binding wins; the token is informational). The advisory closes the smoke-driver false-positive surface where Linear-mode runs created tickets in a project with zero milestones by construction. Decision table:## Notescontains any milestone-attach capability keyprojectMilestoneset on trackerProbe #26 outcome no yes PASS no no GATE FAILED yes yes PASS (binding wins; token informational) yes no ADVISORY (capability-gap downgrade — prose names the specific key found) Recognized key set:
milestone_attach_skipped_adapter_limit(canonical),milestone_attach_unavailable(deprecated alias),milestone_create_required.Skip-reason rendering (STE-238 AC-STE-238.8). When this probe skips for a vacuous cause (
mode: none, FR archived, FR has notracker:block, plan file missing), the rendered skip-reason MUST route throughrenderProbeSkipReason(...)fromadapters/_shared/src/tracker_probe_skip_reason.tsand MUST NOT contain the literal substringrequire Linear MCP(the F9 LLM-paraphrase regression caught by/conformance-loopiteration 1, 2026-05-07). The helper produces an accurate cause-named line —mode: none→skipped — \mode: none`…; archived/no-tracker FR →skipped — active FR has no `tracker:` block () ; etc. — so the operator sees the structural cause, not a vendor-MCP paraphrase. Vendor-neutral by construction: the helper handles Linear and Jira identically. Unit-tested atadapters/_shared/src/tracker_probe_skip_reason.test.ts`.Test coverage:
tests/gate-check-tracker-project-milestone-attached.test.ts.frontmatter-milestone-not-archived— callrunFrontmatterMilestoneNotArchivedProbe(projectRoot)fromadapters/_shared/src/frontmatter_milestone_not_archived.ts. For eachstatus: activeFR file underspecs/frs/, read frontmattermilestone:and check (a) if<specsDir>/plan/archive/<value>.mdexists → GATE FAILED withcollisiondiagnostic (active FR pointing at archived milestone — the post-edit catch for the partial-lsbug), (b) if no plan file exists → GATE FAILED withorphandiagnostic, (c) ifmilestone:frontmatter missing → GATE FAILED withmalformeddiagnostic. Active FRs whose milestone matches an active plan file pass. Archived FRs are vacuous (their milestone naturally matches their archived plan file by construction). Mode-agnostic — milestone numbers are local-file-system identifiers, not tracker objects. Test coverage:tests/gate-check-frontmatter-milestone-not-archived.test.ts.plan-verify-line-validity— callrunPlanVerifyLineValidityProbe(projectRoot)fromadapters/_shared/src/plan_verify_line_validity.ts. Severity: warning (NotesOnly, never GATE FAILED). Walks every activespecs/plan/M*.md(excludingarchive/) and inspectsverify:lines for path-shaped tokens (must include/) that don't resolve to an existing file in the project tree. Inline-backticked tokens are treated as prose references and skipped (soverify: smoke-test re-run shows no \tests/.placeholder.test.ts` reference leftis vacuous). Each unresolved path → one note infile:line — reasonshape with the standard NFR-10 remedy pointing at the cleanup helper atadapters/_shared/src/spec_archive/cleanup_plan_verify_lines.ts. Catches plan-file rot where/implementPhase 4 deleted a file but the verify line lived on. Test coverage:tests/gate-check-plan-verify-line-validity.test.ts`.Negative-assertion exemptions (STE-217). Verify lines that deliberately reference a deleted/missing path to assert its absence are pre-processed through
isNegativeAssertion(line)and skip the path-existence check entirely. Recognized patterns (any-match short-circuits):returns "No such file or directory"— POSIX file-not-found error message (case-insensitive on the message)! test -<flag>— POSIX shell negation prefix at start of line OR after;/&&/||(thetestkeyword is case-sensitive)--non-existent— flag substring indicating intentional absencedoes NOT exist— natural-language negative assertion (case-insensitive on the verb)
Example:
verify: ls src/missing.ts returns "No such file or directory"is recognized as a negative assertion and produces no advisory. Future negative-assertion shapes are added by amending theisNegativeAssertionrecognizer (single source of truth), not by ad-hoc match logic elsewhere.requirements-md-no-placeholder— callrunRequirementsMdNoPlaceholderProbe(projectRoot)fromadapters/_shared/src/requirements_md_no_placeholder.ts. Severity: warning (NotesOnly, never GATE FAILED). Scansspecs/requirements.mdfor surviving placeholder shapes — the legacy### FR-N: [Feature Name]heading, the[Feature Name]literal, and the<tracker-id>literal in active content. HTML comments, fenced code blocks, and inline-backticked spans are exempt (those are documentation/example surfaces). Each surviving placeholder → one note infile:line — reasonshape with the NFR-10 remedy pointing at the cross-cutting-scope rule (per-FR detail belongs inspecs/frs/<id>.md, not inrequirements.md). Catches the smoke-test failure mode where/setupscaffolded the FR-1 placeholder but the architecture moved per-FR detail tospecs/frs/. Vacuous whenspecs/requirements.mdis absent. Test coverage:tests/gate-check-requirements-md-no-placeholder.test.ts.setup-bootstrap-commit-subject— callrunSetupBootstrapCommitSubjectProbe(projectRoot)fromadapters/_shared/src/setup_bootstrap_commit_subject.ts. Scans the most recentchore: bootstrap dev-process-toolkit*commit on the current branch and asserts (a) subject is exactlychore: bootstrap dev-process-toolkit(no parenthesized version suffix), AND (b) body either has noToolkit:line, OR carries exactly one line matching^Toolkit: dev-process-toolkit v\d+\.\d+\.\d+$. Backwards-compat carve-out: commits authored before the FR ship date pass vacuously (covers downstream repos whose bootstrap commit predates this FR). Vacuous on non-git repos and when nochore: bootstrap dev-process-toolkit*commit exists. Test coverage:tests/setup-bootstrap-commit-subject.test.ts.implement-invocation-grammar-doc— callrunImplementInvocationGrammarDocProbe(projectRoot)fromadapters/_shared/src/implement_invocation_grammar_doc.ts. Documentation-existence probe. Verifiesskills/implement/SKILL.md(when present) carries (a) a## Invocation formsheading, (b) a comparison table with at least 6 rows (header + separator + one per phase 0–5), (c) the Phase 5 row contains bothsilent-skipandruns itliterals (case-insensitive). The probe enforces STE-181's documented user-visible contract that/implement <FR-id>and/implement M<N>diverge at Phase 5; missing or under-populated section ⇒ GATE FAILED. Vacuous on projects that don't ship the toolkit's own SKILL.md. Test coverage:tests/gate-check-implement-invocation-grammar-doc.test.ts.plan-file-single-milestone— callrunPlanFileSingleMilestoneProbe(projectRoot)fromadapters/_shared/src/plan_file_single_milestone.ts. Severity: warning (NotesOnly, never GATE FAILED). Walks everyspecs/plan/M*.md(active + archive) and counts^## M\d+:headings; >1 → ADVISORY note infile:1 — plan file carries N \## M:` headings; expected exactly 1 shape. Catches the F3.2 bug from the 2026-05-04 Dart-lib smoke run where/setupcopied the un-trimmed multi-milestone template verbatim intospecs/plan/M1.md. Legacy multi-milestone files exist in user projects (the bug shape we're fixing) — surfacing them as advisory rather than error gives operators the signal without blocking./spec-writeupgrades the same condition to refusal on edits. Vacuous on projects with nospecs/plan/. Test coverage:tests/gate-check-plan-file-single-milestone.test.ts`.plan-task-fr-coverage— callrunPlanTaskFrCoverageProbe(projectRoot)fromadapters/_shared/src/plan_task_fr_coverage.ts. Severity: warning (NotesOnly, never GATE FAILED). Walks every activespecs/plan/M*.md(excludingarchive/), parses each**Tasks:**block, and asserts every[ ]task either has explicit inline link— STE-NNNto an FR row, or its leading verb-phrase substring-matches a row title; tasks marked[deferred]are exempted. Each unbacked task → ADVISORY note infile:line — unchecked task has no backing FR row and no [deferred] marker: <task body>shape. Catches the F3.12 bug where/ship-milestonepre-flight walks FR rows but not task bullets and tags a release for a milestone whose plan still describes uncompleted scope. Vacuous on projects with nospecs/plan/. Test coverage:tests/gate-check-plan-task-fr-coverage.test.ts.mcp-config-shape— callrunMcpConfigShapeProbe(projectRoot)fromadapters/_shared/src/mcp_config_shape.ts. Validates.mcp.jsonentries against Claude Code's MCP server-configuration schema; flags the F6 bug shape"transport": "streamable-http"(canonical replacement:"type": "http"). Severity: routed per file — toolkit-shipped templates / docs (docs/setup-tracker-mode.md,templates/CLAUDE.md.template,skills/setup/SKILL.md) ⇒ ERROR (we ship the schema; we ship it correctly); user-project.mcp.json⇒ ADVISORY (don't break existing repos with the legacy shape). Vacuous when neither toolkit files nor.mcp.jsoncarry the bug shape. Test coverage:tests/gate-check-mcp-config-shape.test.ts.setup-permissions-shape— callrunSetupPermissionsShapeProbe(projectRoot)fromadapters/_shared/src/setup_permissions_shape.ts. Severity: advisory only. Walks user-project.claude/settings.jsonand flags glob-shaped Bash rules (Bash(<cmd> *)patterns); the harness denies any glob-shaped Bash rule when/setupwrites the file on a fresh repo (F3b). Each glob → ADVISORY notefile:line — reason. Migration is operator-driven via/setup --migrate; this probe is the read-side signal. Vacuous when.claude/settings.jsonis absent. Test coverage:tests/gate-check-mcp-config-shape.test.ts.archive-frontmatter-coherent— callrunArchiveFrontmatterCoherentProbe(projectRoot)fromadapters/_shared/src/archive_frontmatter_coherent.ts. Severity: error. Walks everyspecs/frs/archive/*.mdandspecs/plan/archive/*.mdand asserts frontmatterstatus: archivedANDarchived_at:populated. Any file inarchive/withstatus: activeor unsetarchived_at:→ GATE FAILED withfile:1 — reasonnote. Catches the F11 staging-order bug (an archive commit landing with un-flipped frontmatter because the editor wrote frontmatter BEFOREgit mv). Sibling to existing probe #16archive_plan_status(which walks ONLY plan archives and uses different message text); the two probes overlap on the plan-archive arm but the overlap is intentional defense-in-depth. Vacuous on fresh repos with no archive directories. Test coverage:tests/gate-check-archive-frontmatter-coherent.test.ts.cross-cutting-spec-stale-file-refs— callrunCrossCuttingSpecStaleFileRefsProbe(projectRoot)fromadapters/_shared/src/cross_cutting_spec_stale_file_refs.ts. Severity: warning (NotesOnly, never GATE FAILED). Walksspecs/technical-spec.mdandspecs/testing-spec.mdfor path-references inside triple-backtick directory-tree fences whose tokens contain/(full-path tree leaves). Tokens that don't resolve to an existing path on disk surface as ADVISORY rows infile:line — reasonshape. Bare-basename tree leaves (no/) are skipped — no parent context. Prose mentions outside fences are operator judgment surface and never flagged. Defense-in-depth read-side check for paths that bypass/implementPhase 4b's cross-cutting-spec propagation step (manual deletes,git rm, downstream toolkit consumers). Vacuous when both spec files are absent. Probe #37 (cross-cutting-spec-stale-file-refs) only fires on path tokens inside fenced directory-tree blocks in technical-spec.md / testing-spec.md; bare-prose path mentions outside fences are operator judgment surface and never flagged. Test coverage:tests/gate-check-cross-cutting-spec-stale-file-refs.test.ts. (STE-215 AC-STE-215.5)auto-approve-marker-in-canonical-spawns— callrunAutoApproveMarkerProbe(projectRoot)fromadapters/_shared/src/auto_approve_marker.ts. Severity: error. Globsplugins/dev-process-toolkit/skills/*/SKILL.mdand.claude/skills/*/SKILL.md, finds every fenced ```bash block whose body contains aclaude -pinvocation paired with a heredoc-on-stdin (<<'TAG',<<TAG,<<${VAR}), and asserts the canonical marker line<dpt:auto-approve>v1</dpt:auto-approve>appears on its own line inside that fence. Each missing-marker fence → **GATE FAILED** withfile:line — reasonnote in NFR-10 canonical shape. Non-prompt-bearing< /dev/nullsnippets (the/gate-check,/spec-review,/simplifyreference snippets in/smoke-testSKILL.md) are intentionally out of scope — those skills carry no operator-approval gate, so a marker would be redundant. Catches the regression where parent skills (/smoke-test,/conformance-loop) silently drop the marker from a documented spawn snippet, leaving downstreamclaude -pruns to halt at child-skill gates. Vacuous on repos that ship no SKILL.md files. Test coverage:tests/gate-check-auto-approve-marker.test.ts. (STE-226 AC-STE-226.5)tdd-orchestrator-integrity— callrunTddOrchestratorIntegrityProbe(projectRoot)fromadapters/_shared/src/tdd_orchestrator_integrity.ts. Severity: error. Asserts the load-bearing structural invariants of the multi-agent TDD orchestrator (STE-225): (a) the four skill paths exist (skills/tdd/SKILL.md+skills/tdd-write-test/SKILL.md+skills/tdd-implement/SKILL.md+skills/tdd-refactor/SKILL.md); (b) the three child skills carrycontext: fork; (c) each child'sagent:field resolves to an existingagents/*.md; (d) each child carriesuser-invocable: false; (e) each subagent'stoolsfield excludesAgent(plugin-bundled subagents must not nest-spawn). Probe does not assert specific allowed-tool composition beyond theAgentexclusion or specific prompt phrasing — content drift is verified by the headless live smoke (AC-STE-225.9), not the probe. Vacuous only on repos with neither aplugins/dev-process-toolkit/skills/nor aplugins/dev-process-toolkit/agents/directory (the canonical no-plugin-content shape). When one directory exists but the other is absent, the probe runs and surfaces the asymmetry as violations — that's intentional, not a bug: a repo withskills/but noagents/is a malformed plugin where the asymmetry IS the failure. Test coverage:tests/gate-check-tdd-orchestrator-integrity.test.ts. (STE-225 AC-STE-225.7)needs_technical_review_consistency— callrunNeedsTechnicalReviewConsistencyProbe(projectRoot)fromadapters/_shared/src/needs_technical_review_consistency.ts. Severity: error. Walks every activespecs/frs/*.md(archive excluded) and enforces the bidirectional invariant on theneeds_technical_review:frontmatter flag: when the flag is set totrue, the## Technical Designand## Testingbody sections MUST contain the canonical placeholder substring ([needs technical review — run /spec-write …]); when the flag is absent or false, those sections MUST be non-placeholder content (non-empty AND not containing the placeholder substring). Substring match — not byte-exact — so future copy edits of the placeholder don't break archived FRs. Each violation surfaces as afile:line — reasonnote in NFR-10 canonical shape. Catches the smoke-test failure mode where/spec-writeadvertises the placeholder convention but lands an FR whose Technical Design / Testing sections drift from the flag value. Vacuous whenspecs/frs/is absent. Test coverage:tests/needs-technical-review-consistency-probe.test.ts+tests/gate-check-needs-technical-review.test.ts. (STE-227 AC-STE-227.9)spec_research_result_shape— callrunSpecResearchResultShapeProbe(projectRoot)fromadapters/_shared/src/spec_research_result_shape.ts. Severity: error. Walks every recorded.dpt-locks/**/spec-research-result.txtlog produced by the/dev-process-toolkit:spec-researchforked skill (parent skills/brainstormand/spec-writeMAY persist the most recent subagent output for inspection). On each block, the probe asserts (a) the literal banner line> [historical reference — decisions below may be stale; use as background, not authority]is present immediately above the fenced block, (b) the fence opens with\``spec-research-result, (c) exactly three##headings appear inside the fence in the canonical order (## Related FRs,## Prior Decisions,## Reusable ACs / Patterns), and (d) the entire block (banner + opening fence + sections + closing fence) is ≤ 25 lines. Each violation surfaces as afile:line — reasonnote in NFR-10 canonical shape. Vacuous when no result log is recorded — the common no-invocation path. Sibling probe family: colocated with thecommit_producing_skill_branch_gateprobe added in M61. Test coverage:tests/gate-check-spec-research-result-shape.test.ts`. (STE-230 AC-STE-230.12)requires_input_sentinel_coverage— callrunRequiresInputSentinelCoverageProbe(projectRoot)fromadapters/_shared/src/requires_input_sentinel_coverage.ts. Severity: error. Globsplugins/dev-process-toolkit/skills/*/SKILL.mdand.claude/skills/*/SKILL.md(matching probe #38's scope). For every skill body carrying a non-commentrequires-input:annotation, asserts (a) arequireOrRefuse(...)reference (the canonical helper fromadapters/_shared/src/requires_input.ts) AND (b) a relative-path citation ofdocs/auto-mode-protocol.md. Each missing check surfaces as a separate violation (so the operator sees both gaps in one report) infile:line — reasonshape with NFR-10 remedy. HTML-comment-scoped mentions ofrequires-input:are stripped before scanning so a doc-only mention does not falsely trigger the probe. Skills carrying norequires-input:annotation are vacuously out of scope. Catches the v2.13.0 incident shape where arequires-input:step bypassed structurally because the skill body did not route through the canonical helper. Test coverage:tests/gate-check-requires-input-sentinel-coverage.test.ts. (STE-232 AC-STE-232.5)socratic_loop_uses_ask_user_question— callrunSocraticLoopUsesAskUserQuestionProbe(projectRoot)fromadapters/_shared/src/socratic_loop_uses_ask_user_question.ts. Severity: error. Globsplugins/dev-process-toolkit/skills/*/SKILL.mdand.claude/skills/*/SKILL.md(matching probe #38 / #42 scope). For every skill body that (a) citesPattern 26(substring match) OR (b) carries asocratic: trueSchema-K frontmatter key, asserts (i) the body references theAskUserQuestiontool primitive AND (ii) the body cites the canonical protocol docdocs/auto-mode-protocol.md. Each missing check surfaces as a separate violation infile:line — reasonshape with NFR-10 remedy. HTML-comment-scoped Pattern-26 mentions are stripped before scanning. Forward-extension hook: any new skill that ships Pattern 26 prose orsocratic: trueis automatically picked up — no manual list maintenance. Closes the magpie incident regression class structurally (gisthttps://gist.github.com/nesquikm/2904e50c7213b6aa392b998d4137f609, 2026-05-07, plugin v2.16.0). Test coverage:tests/gate-check-socratic-loop-uses-ask-user-question.test.ts. (STE-237 AC-STE-237.3)closing_summary_capability_keys— callrunClosingSummaryCapabilityKeysProbe(projectRoot)fromadapters/_shared/src/closing_summary_capability_keys.ts. Severity: error. Globsplugins/dev-process-toolkit/skills/{spec-write,gate-check,smoke-test}/SKILL.mdand.claude/skills/{spec-write,gate-check,smoke-test}/SKILL.md. For every capability key in the canonical static map (sourced from/spec-write§ 7), asserts the owner skill body carries a literalMUST emit \` directive (regex match on the documented directive shape — backticks required; paraphrased prose like "the closing summary will mention <key>" does NOT satisfy the directive). Each missing directive surfaces as afile:line — reasonnote with NFR-10 remedy. Modeled on probe #42 / probe #38 shape. Closes the/conformance-loopiteration 1 (2026-05-07, F1+F3+F4) systemic gap where SKILL.md prose specified byte-checkable capability rows but the runtime LLM emitted narrative prose, not the literal tokens. Test coverage:tests/gate-check-closing-summary-capability-keys.test.ts`. (STE-238 AC-STE-238.4)socratic_first_turn_post_hoc_drift— callrunSocraticFirstTurnPostHocDriftProbe(projectRoot)fromadapters/_shared/src/socratic_first_turn_post_hoc_drift.ts. Severity: error. Reads the latest commit on the active branch viagit log -1 --format=%H%n%s%n%b. When the subject matches a canonical/spec-writeshape (chore(specs): write FR <tracker-id>ORdocs(specs): edit cross-cutting specs) AND the body carries NEITHER an audit-row marker (spec_write_draft_default_applied/spec_write_commit_default_applied) NOR a refusal NFR-10 block (Verdict: ... Refusedshape — case-insensitive on the verb), surfacesocratic_first_turn_post_hoc_drift_violationcapability row and GATE FAILED with NFR-10 message naming the commit hash + offending subject. Vacuous on commits not matching the canonical subject patterns (anchored regex — quoted mentions in revert subjects do not falsely activate). Vacuous on repos with no.git/or no commits. Catches the F2 silent-commit shape captured by/conformance-loopiter-1 (commit9b75b4b, 2026-05-08): underclaude -pnon-tty stdin the model firesAskUserQuestion(passes first-turn), sees "dismissed" responses, self-rationalizes "picked safe defaults" and lands a real commit + tracker writes without operator consent. The legitimate paths always leave one of the two markers in the body — the audit row when default-apply is exercised, or the refusal NFR-10 block when the operator was surfaced the failure. Test coverage:tests/gate-check-socratic-first-turn-post-hoc-drift.test.ts+adapters/_shared/src/socratic_first_turn_post_hoc_drift.test.ts. (STE-251 AC-STE-251.3)conformance-loop-bypass-removed— callrunConformanceLoopBypassRemovedProbe(projectRoot)fromadapters/_shared/src/conformance_loop_bypass_removed.ts. Severity: error. Globs.claude/skills/{conformance-loop,smoke-test}/SKILL.md, finds every fenced ```bash block whose body contains aclaude -pinvocation, and asserts none carry--permission-mode bypassPermissions. Each offending fence → **GATE FAILED** withfile:line — reasonnote in NFR-10 canonical shape (cites the offendingbypassPermissionsline, not the fence start, for actionable diffs). Vacuous on repos that don't ship the project-local.claude/skills/{conformance-loop,smoke-test}/SKILL.mdfiles (toolkit consumers without the smoke harness). Catches the regression where a future edit reintroduces the blanket bypass at any reachable spawn site, defeating the audit-able policy artifact (permissions.allowblock in tracked.claude/settings.json). Sibling probe: probe #38auto-approve-marker-in-canonical-spawnsenforces marker presence at the same fences; probe #46 enforces flag absence. Test coverage:tests/gate-check-conformance-loop-bypass-removed.test.ts. (STE-252 AC-STE-252.4)spec_write_first_turn_drift_scan— callrunSpecWriteFirstTurnDriftScanProbe(projectRoot)fromadapters/_shared/src/spec_write_first_turn_drift_scan.ts. Severity: error. Globs ONLYplugins/dev-process-toolkit/skills/spec-write/SKILL.md(single-file scope; the regression is uniquely/spec-write's, not a cross-skill issue) and scans for six literal forbidden alternate-trigger paraphrases of the Pattern-26 first-turn contract:"pre-baked args allow","verbose <command-args> permits","autonomous-mode permits skip","marker absence implies","first AskUserQuestion may be deferred","Socratic loop is optional under". Each occurrence → GATE FAILED withfile:line:column — reasonnote in NFR-10 canonical shape, naming the matched phrase and the rephrase remedy. Sibling probe shape to STE-226'sauto_approve_marker.tsand STE-262'sspec_write_alternate_trigger_scan.ts; literal substring match (no regex) so future SKILL.md copy edits don't accidentally regress detection. Vacuous when/spec-writeSKILL.md is absent (downstream toolkit consumers without the plugin's own skills tree). Closes the/conformance-loopiter-1 (2026-05-10) F1 reproducer where/spec-write's diluting-prose surface (§ 0a Resolver entry + § 0b FR creation path) read to the LLM as license to skip the Socratic loop entirely under the autonomous-mode reminder — same prose-only failure shape STE-220 → STE-226 → STE-237 each hit before structural / byte-checkable hardening landed. Test coverage:tests/gate-check-spec-write-first-turn-drift-scan.test.ts. (STE-270 AC-STE-270.2)spec_write_marker_alternate_trigger_scan— callrunSpecWriteAlternateTriggerScanProbe(projectRoot)fromadapters/_shared/src/spec_write_alternate_trigger_scan.ts. Severity: error. Globs ONLYplugins/dev-process-toolkit/skills/spec-write/SKILL.mdand scans for six literal forbidden alternate-trigger paraphrases of the STE-226 marker contract:"Auto Mode","work without stopping","imputed approval","autonomous-mode reminder","inferred approval","non-interactive inference". Each occurrence → GATE FAILED withfile:line:column — reasonnote in NFR-10 canonical shape, naming the matched phrase and the rephrase remedy. Negation-context carve-out: lines containing any of"single deterministic"(canonical contract anchor at gate sites),"are removed"/"is removed"(legacy-removal historical reference),"regardless of"(disclaimer phrasing), or"NOT acceptable"(explicit forbidden-trigger marker) are excluded — those signatures only appear in legitimate negation/historical context, not as positive alternate triggers. Sibling probe shape to STE-226'sauto_approve_marker.tsand STE-270'sspec_write_first_turn_drift_scan.ts. Vacuous when/spec-writeSKILL.md is absent. Closes the/conformance-loopiter-1 (2026-05-08) F1 cross-tracker reproducer where/spec-write's draft + commit gates auto-applied silently onclaude -pruns whose prompt body lacked the marker, conflating the harness's autonomous-mode reminder with the byte-checkable STE-226 marker. Third attempt at this gate (STE-213 → STE-220 → STE-226 → STE-262); structural enforcement at runtime + this source-level probe close the prose-only LLM-inference path. Test coverage:tests/gate-check-spec-write-marker-alternate-trigger-scan.test.ts. (STE-262 AC-STE-262.4 + AC-STE-262.7)tracker_local_reconciliation_drift— callrunTrackerLocalReconciliationDriftProbe(projectRoot, { provider })fromadapters/_shared/src/tracker_local_reconciliation_drift.ts. Severity tiered:info(no drift),warning(any drift — tracker-orphan / unbound local FR / milestone-mismatch),error(hard FR-id collisions — same tracker ID bound by two or more local files OR local FR'stracker:value points at an FR id absent from the tracker's active set). WrapsreconcileTrackerLocalfromadapters/_shared/src/reconcile_tracker_local.tsand adds a local-side scan for the duplicate-binding shape the helper cannot see by construction (both files carry a binding, so neither is an orphan). Mode-aware via the helper:LocalProvider(mode: 'none') reconciles to three empty lists and the probe surfacesinfo/zero-violations on every local-only project. Read-side: never writes, never throws on missingspecs/directories. Closes the 2026-05-13 partial-scan trap shape where a milestone existed on the tracker but had no local plan file. Test coverage:tests/gate-check-tracker-local-reconciliation-drift.test.ts. (STE-284 AC-STE-284.4)Capability-key roll-up (STE-284 AC-STE-284.5). Probe #49 surfaces three byte-checkable capability keys into the closing summary so the operator sees the reconciliation outcome without scrolling probe output. Owner skill:
/spec-write(preamble reconcile is the canonical emission site);/gate-checkmirrors the keys here so the conformance probe at #44 (closing_summary_capability_keys) and the future cross-skill audit can find them in either surface. The plain-language rendering tracks/spec-write§ 7's static map row of the same name:tracker_local_reconciled— emitted whenever the preamble auto-import path reconciled ≥ 1 FR or milestone from the tracker into local files. Render:tracker → local reconciliation imported N FR(s) and M milestone(s) — see specs/frs/<id>.md and specs/plan/<M>.md for the imported content.tracker_local_orphan_local— emitted per offending entity when a local FR file carries notracker:binding. Render:local FR <id> has no tracker binding — run /spec-write <id> --push-to-tracker to sync, or remove the local file if abandoned.milestone_local_orphan— emitted per offending entity when a localspecs/plan/M<N>.mdhas no matching tracker milestone (the partial-scan trap inverse). Render:local milestone M<N> has no tracker milestone — auto-create via Provider.attachProjectMilestone(M<N>), or remove if abandoned.
The three keys MUST appear verbatim in the closing summary; paraphrased prose is insufficient (STE-220 lesson). Vacuous in
mode: none.tdd_spec_reviewer_subagent_invariants— callrunTddSpecReviewerInvariantsProbe(projectRoot)fromadapters/_shared/src/tdd_spec_reviewer_invariants.ts. Severity: error. Asserts the byte-checkable frontmatter invariants for the AUDIT-stage subagent + child skill: (a)agents/tdd-spec-reviewer.mdexists withtools: Read, Grep, Glob(read-only — Write/Edit/Bash/Agent excluded),maxTurns: 8,model: sonnet; (b)skills/tdd-spec-review/SKILL.mdexists withcontext: fork,agent: tdd-spec-reviewer,user-invocable: false, and (whenallowed-tools:is present)Agentexcluded. Mirrors probe #39tdd_orchestrator_integrityshape against the new fourth stage; both probes share frontmatter parsing + violation-rendering primitives fromadapters/_shared/src/tdd_probe_helpers.ts. Vacuous only on repos with neitherplugins/dev-process-toolkit/skills/norplugins/dev-process-toolkit/agents/. Test coverage:tests/gate-check-tdd-spec-reviewer-invariants.test.ts. (AC-STE-296.8)deps_researcher_subagent_invariants— callrunDepsResearcherInvariantsProbe(projectRoot)fromadapters/_shared/src/deps_researcher_invariants.ts. Severity: error. Asserts the byte-checkable frontmatter invariants for the deps-research subagent + child skill: (a)agents/deps-researcher.mdexists withtools: Read, Grep, Glob(read-only — Write/Edit/Bash/Agent excluded) andmodel: haiku; (b)skills/deps-research/SKILL.mdexists withcontext: fork,agent: deps-researcher,user-invocable: false, and (whenallowed-tools:is present)Agentexcluded. Mirrors probe #50tdd_spec_reviewer_subagent_invariantsshape (STE-296 AC.8) against the deps-research fork; both probes share frontmatter parsing + violation-rendering primitives fromadapters/_shared/src/tdd_probe_helpers.ts. Vacuous only on repos with neitherplugins/dev-process-toolkit/skills/norplugins/dev-process-toolkit/agents/. Test coverage:tests/gate-check-deps-researcher-invariants.test.ts. (STE-301 AC-STE-301.15)tracker_config_shape— callrunTrackerConfigShapeProbe(projectRoot)fromadapters/_shared/src/tracker_config_shape.ts. Severity: error. Byte-checksspecs/tracker-config.yamlshape when the file exists:tracker_key:matches the active adapter'sname:field (resolved viareadAdapterName(...)fromadapters/<mode>.md, where<mode>is declared by CLAUDE.md## Task Trackingmode:) per STE-321 AC.1 — the hard-codedlinear/jiraallowlist is gone, custom-adapter authors are first-class;statuses:is a non-empty list of verbatim tracker labels (AC.2); androles:declares exactly the canonical four-role enum{initial, in_progress, in_review, done}with every value present instatuses:(AC.3 + AC.4). Schema authority isvalidateTrackerConfigfromadapters/_shared/src/tracker_config.ts— the same validatorreadTrackerConfigcalls — so the probe and the runtime loader can never diverge. Each violation surfaces as aspecs/tracker-config.yaml:<line> — <reason>note in NFR-10 canonical shape (Refusing: / Remedy: / Context:). Vacuous whenspecs/tracker-config.yamlis absent (FR2 owns creation) OR CLAUDE.md## Task Trackingdeclaresmode: none(short-circuits before the file is read — even a malformed file is inert undermode: none). Test coverage:tests/gate-check-tracker-config-shape.test.ts.tracker_tolerance_wrapper_present— callrunTrackerToleranceWrapperPresentProbe(projectRoot)fromadapters/_shared/src/tracker_tolerance_wrapper_present.ts. Severity: error. Defense-in-depth byte-check thatplugins/dev-process-toolkit/adapters/_shared/src/tracker_tolerance.tsexists AND exportswithTolerance(matched asexport function withToleranceorexport const withTolerance). If a future refactor accidentally drops the wrapper or renames the symbol, the relaxed status-mapping probes would silently lose tolerance — this probe is the structural fuse that catches that regression at gate-check time. Each violation surfaces as aplugins/dev-process-toolkit/adapters/_shared/src/tracker_tolerance.ts:1 — <reason>note in NFR-10 canonical shape (Refusing: / Remedy: / Context:). Vacuous whenplugins/dev-process-toolkit/adapters/_shared/src/is absent (non-toolkit projects do not ship the shared adapter layer). Test coverage:tests/gate-check-tracker-tolerance-wrapper-present.test.ts.audit_fix_loop_pattern_invariants— callrunAuditFixLoopPatternInvariantsProbe(projectRoot)fromadapters/_shared/src/audit_fix_loop_pattern.ts. Severity: error. Asserts byte-checkable invariants on every canonical audit-fix loop declared in theAUDIT_FIX_LOOP_CANONICAL_LOOPSallowlist exported from the same module. The allowlist ships with the/tddaudit-fork pair only at FR ship (tdd-spec-review::tdd-spec-reviewer); STE-308 appends the/spec-reviewaudit-fork entry. The RED/GREEN/REFACTOR/tddforks (tdd-write-test,tdd-implement,tdd-refactor) are deliberately excluded — they are the action half of/tdd's loop and need Write/Edit/Bash; their orchestrator-level invariants are already enforced by probe #39tdd_orchestrator_integrity. For each allowlist entry the probe asserts (a) the child skill file exists atplugins/dev-process-toolkit/skills/<child>/SKILL.mdand carriescontext: fork,user-invocable: false, and anagent:resolving to an existingplugins/dev-process-toolkit/agents/<name>.md; (b) the subagent declarestools: Read, Grep, Globonly (Write/Edit/Bash/Agent excluded); (c) the child'sallowed-tools:(when present) excludesAgent. Generalises probes #39 / #50 / #51 to a single allowlist-driven scan so new audit-fix loops added downstream are picked up without bespoke probe code. Vacuous on repos with neitherplugins/dev-process-toolkit/skills/norplugins/dev-process-toolkit/agents/. Test coverage:tests/gate-check-audit-fix-loop-pattern-invariants.test.ts. (STE-307)not_a_trigger_anchor_present— callrunNotATriggerAnchorPresentProbe(projectRoot)fromadapters/_shared/src/not_a_trigger_anchor_present.ts. Severity: error. Asserts a § Rules NOT-a-trigger anchor lands in BOTHplugins/dev-process-toolkit/skills/spec-write/SKILL.mdANDplugins/dev-process-toolkit/skills/setup/SKILL.md. The anchor's byte-checkable contract is that every literal phrase inNOT_A_TRIGGER_REQUIRED_PHRASES("work without stopping","autonomous-mode","standing instruction",<command-args>,claude -p) appears in the file at or after the## Rulesheading, alongside a literal reference toadapters/_shared/src/check_marker_runtime.tsas the SOLE byte-checkable evaluation path. Each violation surfaces as a<skill SKILL.md>:<line>:<column> — <reason>note in NFR-10 canonical shape (Refusing: / Remedy: / Context:). Vacuous when neither SKILL.md ships. Test coverage:tests/gate-check-not-a-trigger-anchor-present.test.ts.marker_helper_invoked_per_gate— callrunMarkerHelperInvokedPerGateProbe(projectRoot, { sessionLogPath })fromadapters/_shared/src/marker_helper_invoked_per_gate.ts. Severity: error. Scans the NDJSON Bash-tool transcript of/spec-writeand/setupruns (via the stream-json session-log capture path) and refuses with NFR-10 if any marker-gated decision token (spec_write_draft_default_applied,spec_write_commit_default_applied,branch_gate_default_applied, or anysetup_socratic_first_turn_*token) surfaces in an assistant text block without a preceding Bash invocation ofbun run plugins/dev-process-toolkit/adapters/_shared/src/check_marker_runtime.tsin the same session log. Refusal events (RequiresInputRefusedErrortext) are not gate decisions — the probe ignores them. Each violation surfaces in NFR-10 canonical shape (Refusing: / Remedy: / Context:). Vacuous when nosessionLogPathis supplied OR the path points at a missing file (downstream toolkit consumers without the capture path shouldn't see false-positive violations). Test coverage:tests/gate-check-marker-helper-invoked-per-gate.test.ts.public_surface_count_drift— callrunPublicSurfaceCountDriftProbe(projectRoot)fromadapters/_shared/src/public_surface_count_drift.tswithPROBE_ID = "public_surface_count_drift". Severity: error. Asserts the documented skill / agent / probe count tokens inREADME.mdandCLAUDE.mdmatch the actual on-disk counts. Walks three input files (README.md,CLAUDE.md,skills/gate-check/SKILL.md) and the on-diskplugins/dev-process-toolkit/skills/*/+agents/*.mdtrees to compute observed counts; any documented value that disagrees with the corresponding observed value surfaces as an NFR-10 canonical refusal (<file>:<line>:<column> — <reason>withRefusing: / Remedy: / Context:sub-lines). Pattern list lives as a const at the top of the probe module so contributors editing public surfaces see the contract. Vacuous when none of the three input files exists (non-toolkit projects do not ship them). Test coverage:tests/gate-check-public-surface-count-drift.test.ts.cross_skill_contract_drift— callrunCrossSkillContractDriftProbe(projectRoot)fromadapters/_shared/src/cross_skill_contract_drift.tswithPROBE_ID = "cross_skill_contract_drift". Severity: error. Walks the active-surface globplugins/dev-process-toolkit/{skills,docs,agents}/**/*.mdplusplugins/dev-process-toolkit/README.md(archive directories excluded) and scans each line for the literal forbidden substrings exported asFORBIDDEN_SUBSTRINGSat the top of the probe module — see the module source for the authoritative list. Closes three already-shipped cross-skill contract drifts — A2 (/tdd 4-stage AUDIT vocabulary per STE-296 M77), A6 (2-tier ticket-binding resolver per v1.21.0), and A14 (a phantom probe reference deleted fromagents/deps-researcher.md). Each match surfaces as an NFR-10 canonical refusal (<file>:<line>:<column> — <reason>withRefusing: / Remedy: / Context:sub-lines). Literal substring match — no regex — so future SKILL.md copy edits don't accidentally regress detection. Vacuous whenplugins/dev-process-toolkit/is absent (downstream toolkit consumers without the plugin's own docs). Test coverage:tests/gate-check-cross-skill-contract-drift.test.ts.disable_model_invocation_allowlist— callrunDisableModelInvocationAllowlistProbe(projectRoot)fromadapters/_shared/src/disable_model_invocation_allowlist.tswithPROBE_ID = "disable_model_invocation_allowlist". Severity: error. Walks everyplugins/dev-process-toolkit/skills/*/SKILL.mdfrontmatter and refuses ifdisable-model-invocation: trueis declared on any skill not in the canonical allowlist exported asDISABLE_MODEL_INVOCATION_ALLOWLIST(currently["setup"]only —/setupis the bootstrap entry point and must not be auto-invoked mid-session). Composable skills like/ship-milestoneand/spec-archivemust remain model-invocable; surfacing the flag on them would silently break orchestration chains (/implement → /spec-archive, release pipelines). Each violation surfaces as an NFR-10 canonical refusal (<file>:<line>:<column> — <reason>withRefusing: / Remedy: / Context:sub-lines). Canonical-allowlist shape mirrorsclosing_summary_capability_keys(STE-238 AC.4). Vacuous whenplugins/dev-process-toolkit/skills/is absent (downstream toolkit consumers without the plugin's own skills tree). Test coverage:tests/m84-ste-324-low-polish-bundle.test.ts(AC-STE-324.8).kotlin-detekt-configured— callrunKotlinDetektConfiguredProbe(projectRoot)fromadapters/_shared/src/kotlin_detekt_configured.tswithPROBE_ID = "kotlin-detekt-configured". Severity: error. Fires when a Gradle Kotlin project is detected (build.gradle.ktsORsettings.gradle.kts) AND its gate-commands declaredetekt; verifies the detekt Gradle plugin is applied (io.gitlab.arturbosch.detektreferenced inbuild.gradle.ktsOR adetekt { … }block present). When the plugin is missing, fails with an NFR-10-shape error pointing to the/setupstep 2c Kotlin detekt scaffold-verify branch so the operator can re-add the detekt plugin. Vacuous on non-Kotlin projects (nobuild.gradle.kts/settings.gradle.kts) and on Kotlin projects that do not declare detekt as a gate. Test coverage:tests/gate-check-kotlin-detekt-configured.test.ts.
Full details: docs/layout-reference.md § /gate-check.
Conformance-probes summary line
After all conformance probes complete, render a single roll-up summary line in the canonical shape:
conformance-probes pass: <N>/<N> [<active> active, <vacuous> vacuous]
Where:
<N>is the total probe count (the same numerator and denominator unless a probe failed — failures route through GATE FAILED above).<active>is the count of probes that ran with non-empty input and exercised at least one assertion against active content.<vacuous>is the count of probes that early-returned because their scope was empty (no active FRs to walk,mode: noneskip,specs/absent, plan file missing, etc.). Vacuous probes are still passing probes — the count distinguishes them from active passes so the operator can tell at a glance whether the suite is exercising real content.
An earlier Jira smoke run caught the previous shape 29/29 (most vacuous post-archive) — the parenthetical wording was soft and operator-unparseable. The bracketed [N active, M vacuous] form is deterministic; the brackets are the parseable signal a CI step or smoke driver can scan for.
What counts as vacuous post-archive. When the only active FR is archived, the FR-traversal probes (probes that walk active specs/frs/*.md — e.g., #1 filename-frontmatter convention, #2 required frontmatter, #7 duplicate AC-prefix, #8 ticket-state drift, #14 ticket-state drift active-side, #15 guessed tracker-id scan, #25 task-tracking workspace-binding, #26 tracker-project-milestone-attached, #27 frontmatter-milestone-not-archived) early-return as vacuous because there are no active FRs to walk. The counter must reflect this — the same probe set that ran fully active before the archive becomes mostly vacuous after, and the summary line should make that visible to the operator.
Probe authoring contract
Every new /gate-check probe ships with a corresponding
tests/gate-check-<slug>.test.ts test file. Self-review refuses a probe
declaration without its test — a probe advertised in prose without an
integration test can drift from the implementation without detection. The
test must cover both a positive fixture (probe passes clean) and a
negative fixture (probe fires with the documented note shape:
file:line — reason). Each probe above has a corresponding
tests/gate-check-<slug>.test.ts — probe #14 (active-side
ticket-state drift) is covered by tests/gate-check-active-ticket-drift.test.ts,
probe #15 (guessed tracker-ID scan) by
tests/gate-check-guessed-tracker-id.test.ts, probe #16
(archive plan-status invariant) by
tests/gate-check-archive-plan-status.test.ts, probes 17–23
(setup-output-completeness, claudemd-docs-section-present,
setup-audit-section-presence, bun-zero-match-placeholder,
task-tracking-canonical-keys, toolkit-bootstrap-committed,
traceability-link-validity) by their corresponding
tests/gate-check-<slug>.test.ts files, probe #24
(signature-strategy-honors-setup) by
tests/gate-check-signature-strategy-honors-setup.test.ts, probe
#25 (task-tracking-workspace-binding-present) by
tests/gate-check-task-tracking-workspace-binding-present.test.ts,
probe #26 (tracker-project-milestone-attached) by
tests/gate-check-tracker-project-milestone-attached.test.ts,
probe #27 (frontmatter-milestone-not-archived) by
tests/gate-check-frontmatter-milestone-not-archived.test.ts, and
probe #28 (plan-verify-line-validity) by
tests/gate-check-plan-verify-line-validity.test.ts, and
probe #29 (requirements-md-no-placeholder) by
tests/gate-check-requirements-md-no-placeholder.test.ts,
probe #30 (setup-bootstrap-commit-subject) by
tests/setup-bootstrap-commit-subject.test.ts,
probe #31 (implement-invocation-grammar-doc) by
tests/gate-check-implement-invocation-grammar-doc.test.ts,
probes #32 (plan-file-single-milestone, STE-197),
#33 (plan-task-fr-coverage, STE-201),
#34 (mcp-config-shape, STE-209),
#35 (setup-permissions-shape, STE-209),
#36 (archive-frontmatter-coherent, STE-210),
#37 (cross-cutting-spec-stale-file-refs, STE-215), and
#38 (auto-approve-marker-in-canonical-spawns, STE-226), and
#42 (requires-input-sentinel-coverage, STE-232) by their corresponding
tests/gate-check-<slug>.test.ts files (mcp-config-shape and
setup-permissions-shape share tests/gate-check-mcp-config-shape.test.ts;
auto-approve-marker lives at tests/gate-check-auto-approve-marker.test.ts;
requires-input-sentinel-coverage at tests/gate-check-requires-input-sentinel-coverage.test.ts).
Contributors adding probe 60+ must ship the matching test file in the same commit.
Commands
Read the project's CLAUDE.md to find the gate check commands (look for "Key Commands" or "Gating rule" section). If no CLAUDE.md exists, ask the user what commands to run.
Typical commands by stack (use as fallback if CLAUDE.md doesn't specify):
- Run typecheck:
npm run typecheck(orfvm flutter analyze,mypy ., etc.) - Run lint:
npm run lint $ARGUMENTS(if$ARGUMENTScontains--fix, add-- --fix) - Run tests:
npm run test(orfvm flutter test,pytest, etc.) - Run build:
npm run build(optional — include if your project has a build step) - Run security audit (optional):
npm audit(orpip-audit,cargo audit,flutter pub audit). Flag known vulnerabilities. This step is advisory — failures here produce NOTES, not GATE FAILED, unless your project explicitly gates on audit.
Reporting
For each step:
- If it passes, report ✓ with the actual output summary (e.g., "✓ Tests: 47 passed, 0 failed")
- If it fails, report ✗ with the specific errors (include file:line references)
Cite actual output numbers — do not report GATE PASSED from memory of a previous run. Run each command fresh and read the result.
If a failure cause is unclear after reading the error output, use /dev-process-toolkit:debug for structured investigation.
Code Review
After all commands pass, review the changed code. Use git diff against the base branch (e.g., git diff main...HEAD) if on a feature branch, or git diff HEAD~1 if on the main branch. If there are uncommitted changes, include git diff (unstaged) and git diff --cached (staged) as well. Your job here is to find problems, not to praise the work. Approach this as if reviewing someone else's code — look for what's wrong, not what's right.
Use the canonical review rubric in agents/code-reviewer.md as the source of truth for criteria (quality, security, patterns, stack-specific). Run the review inline — gate-check must return a verdict in one turn, so do not delegate to the code-reviewer subagent from here. Also check spec compliance: every AC has a corresponding test, and no undocumented behavior has been added (security and spec-compliance concerns are the only critical criteria in gate-check; other concerns are non-critical).
For each criterion, report: OK or CONCERN with specifics. Use the exact shape documented at the bottom of agents/code-reviewer.md (<criterion> — OK or <criterion> — CONCERN: file:line — <reason>).
Drift Check
Never read
specs/frs/archive/orspecs/plan/archive/— only live spec files count for drift detection.
If specs/ directory exists, check whether the implementation has drifted from the spec:
- Read
specs/requirements.mdand extract all ACs - For each AC, search the codebase for implementing code and tests
- Build a traceability table:
| AC ID | Status | Location |
|---|---|---|
AC-<tracker-id>.1 |
implemented | src/feature.ts:42 |
AC-<tracker-id>.2 |
not found | — |
AC-<other-tracker-id>.1 |
implemented | src/service.ts:15 |
- implemented — code and/or tests found matching the AC
- not found — no implementing code found
- no AC — code exists in changed files with no corresponding spec AC (
potential drift)
If specs/ directory does not exist, skip this section silently.
Drift findings do NOT cause GATE FAILED. They appear under GATE PASSED WITH NOTES as informational items for the developer to review.
Verdict
Combine command results + code review into a final verdict:
- GATE PASSED — all commands pass AND no concerns in code review
- GATE PASSED WITH NOTES — all commands pass but code review found non-critical concerns or drift check found spec-implementation gaps (list them). These are things the user should be aware of but that don't block merging.
- GATE FAILED — any command failed OR code review found critical concerns (spec compliance or security issues)
Always state what needs fixing if not a clean pass.
Structured Output
Optionally produce a JSON summary alongside the Markdown report so CI pipelines can parse results.
{
"steps": [
{ "step": "typecheck", "status": "pass", "summary": "No type errors" },
{ "step": "test", "status": "pass", "summary": "47 passed, 0 failed" },
{ "step": "code-review", "status": "pass", "summary": "No critical concerns" },
{ "step": "drift-check", "status": "notes", "summary": "2 ACs not found" }
],
"verdict": "GATE PASSED WITH NOTES"
}
The verdict field uses one of: GATE PASSED, GATE PASSED WITH NOTES, GATE FAILED.
Rules
- The commands (typecheck, lint, tests, build) are the deterministic kill switch — if any command fails, it's GATE FAILED. Period. No judgment can override a failing command.
- The code review is an additional advisory layer — it can elevate GATE PASSED to GATE PASSED WITH NOTES (non-critical) or GATE FAILED (critical: security or spec violations). But it cannot downgrade a failing command to a pass.
- Do NOT skip any step — run all commands AND the code review
- Do NOT report GATE PASSED without running commands fresh this session
Red Flags
If you hear yourself thinking any of these, stop and run the gate anyway:
- "I'll run gate-check after the next task" → run it now
- "I know the tests pass" / "It should work now" / "Just this once" → run it now; "should" is not a gate result; there is no just-this-once
Rationalization Prevention
| Excuse | Reality |
|---|---|
| Should work now | Run the verification |
| I'm confident | Confidence ≠ evidence |
| Just this once | No exceptions |
| Linter passed | Linter ≠ compiler / tests |
| Agent said success | Verify independently |
| Partial check is enough | Partial proves nothing |