Feature Branch: 014-hook-health
Created: 2026-06-23
Status: Draft
Input: User description: "The only community install/update path (add --from <zip> --force) silently strips the bridge's after_* auto-sync hooks, so auto-sync quietly stops and the operator doesn't notice until the board drifts. Make the bridge detect its own missing hooks and surface a loud, named warning pointing at the one-command fix — turning silent drift into an obvious re-run-install."
The only sanctioned way to install/update a community spec-kit extension is
specify extension add linear --from <release-zip> --force. That --force
reinstall strips the bridge's six after_* auto-sync hook registrations from
the consumer's .specify/extensions.yml (it bypasses the CLI's hook
backup/restore). The failure is silent: auto-sync simply stops firing on
/speckit.* lifecycle commands, and the operator only discovers it when the
Linear board has drifted out of date. Because community extensions have no
frictionless update (a deliberate spec-kit gap, out of our control), this
stripping recurs on every upgrade.
This feature makes the bridge self-report its own hook health. When it runs —
speckit.linear.push (the reconcile) and speckit.linear.status — it checks
whether its six after_* hooks are still registered for the linear extension
and, if any are missing, emits a loud, named warning in the structured
summary naming the absent hooks and the one-command remediation
(/speckit.linear.install). status also reports hook health as a first-class
line. It is detection + surfacing only (surface, don't enforce — Principle
VIII): the warning never blocks the operation it rode in on, the restore path is
the existing /speckit.linear.install (which already re-registers idempotently),
and a hook the operator deliberately disabled (enabled: false) is not
treated as missing.
- Q: On detecting missing hooks, warn-only or also offer to self-heal (re-register)? → A: WARN + consented self-heal. push/status emit the named warning, and in an INTERACTIVE session additionally OFFER (operator y/N) to re-register the missing hooks in place; NON-INTERACTIVE (CI / hook-fired) is warn-only and never mutates. Restoration still also available via
/speckit.linear.install.
- Q: Does the consented self-heal offer also appear during
speckit.linear.status, or isstatusreport-only? → A: Both. When interactive and hooks are missing,statusALSO offers the y/N re-register (same rule as the reconcile/push path), not just a report line — the operator who proactively checks can fix on the spot. - Q: Does
statuschange its exit code when hooks are missing? → A: No — always exit 0. Hook health is surfaced as a report line only and never altersstatus's exit disposition (surface, don't enforce); it is not a CI gate. - Q: On consent, re-register all missing hooks at once or prompt per hook? → A: All at once. A single y/N re-registers every missing
after_*hook in place.
A developer updated the bridge via add --from <zip> --force, which silently
removed the after_* hooks. The next time they run a sync (speckit.linear.push,
or any reconcile), the run warns loudly that auto-sync hooks are not
registered and tells them exactly how to fix it (/speckit.linear.install). The
silent drift is now impossible to miss.
Why this priority: This is the whole point — converting a silent, board- drifting failure into an obvious, named, one-command fix. It directly addresses the reported pain.
Independent Test: In a repo whose .specify/extensions.yml has had the
linear after_* hooks removed, run speckit.linear.push and confirm the run
completes (not blocked) AND emits a warning naming the missing hooks + the
/speckit.linear.install remediation.
Acceptance Scenarios:
- Given the
linearafter_*hooks are absent from.specify/extensions.yml, When the bridge reconciles, Then it emits a warning naming how many / which hooks are missing and the/speckit.linear.installremediation, and the reconcile still completes normally (not blocked). - Given all six hooks are registered, When the bridge reconciles, Then no hook-health warning is emitted.
- Given the operator deliberately set a hook
enabled: false, When the bridge reconciles, Then that hook is treated as intentionally disabled (not "missing") and raises no warning.
An operator wants to verify auto-sync is wired without waiting for a drift. They
run speckit.linear.status and see hook-registration health as a first-class line
(all present / partial-with-the-missing-ones-named / none registered).
Why this priority: Useful for proactive checks and incident triage, but secondary to the automatic warning on the reconcile path (US1), which catches the problem without the operator thinking to look.
Independent Test: Run speckit.linear.status in repos with all / some / none
of the hooks registered and confirm each reports the corresponding health state.
Acceptance Scenarios:
- Given all six hooks registered, When
statusruns, Then it reports hook health as fully present. - Given some hooks missing, When
statusruns, Then it reports partial health and names the missing hooks. - Given no
linearhooks registered, Whenstatusruns, Then it reports auto-sync as not wired, with the/speckit.linear.installremediation.
- Operator-disabled hook (
enabled: false): intentional → not "missing", no warning (distinguished from absent). - No
.specify/extensions.ymlat all (bridge never installed in this tree): out of the feature's scope to nag — handled as the existing not-installed path, not a per-reconcile hook-health warning. - Partial set (some hooks present, some absent — e.g. a hand-edited file): warn, naming exactly the absent ones.
- Warning cadence: at most once per reconcile run (not once per spec), matching
the existing one-shot-warning pattern, so an
--allsweep doesn't repeat it. - The check never blocks: a malformed/unreadable
.specify/extensions.ymldegrades to "could not verify hook health" (informational), never halting the reconcile (Principle VIII).
- FR-001: On
speckit.linear.push(reconcile) andspeckit.linear.status, the bridge MUST determine whether each of its sixafter_*hooks (after_specify,after_clarify,after_plan,after_tasks,after_implement,after_analyze) is registered for thelinearextension in the consumer's.specify/extensions.yml. - FR-002: When one or more of those hooks are absent, the reconcile MUST
emit a single structured warning per run that states how many are missing, names
the missing hooks, and gives the
/speckit.linear.installremediation. - FR-003: The hook-health warning MUST NOT block or fail the operation it rode in on — the reconcile/push completes normally (surface, don't enforce).
- FR-004: A hook the operator has explicitly disabled (
enabled: false) MUST be treated as intentionally off, NOT as missing, and MUST NOT trigger the warning. - FR-005: When all six hooks are registered (or intentionally disabled), the bridge MUST emit no hook-health warning.
- FR-006:
speckit.linear.statusMUST report hook-registration health as a first-class result line: fully present, partial (naming the missing hooks), or none registered. This report MUST NOT changestatus's exit code —statusalways exits 0 regardless of hook health (the health line is informational, not a CI gate). (Clarified 2026-06-24.) - FR-007: The detection MUST use the same notion of "is hook X registered for
linear" that the install path uses to register them, so detection and the install-based restore agree (no false positives/negatives). - FR-008: The check MUST read only the consumer's local
.specify/extensions.yml, perform no Linear writes, add no new runtime dependency, and (on the warn path) mutate nothing; an unreadable/malformed file degrades to an informational "could not verify" rather than a halt. - FR-009: On detecting missing hooks in an interactive session, the
bridge MUST OFFER to re-register them in place (explicit operator y/N consent),
reusing the install path's idempotent registration; a non-interactive run
(CI / hook-fired / no TTY) MUST be warn-only and mutate nothing. This offer
applies on BOTH the reconcile/
pushpath ANDspeckit.linear.status(any interactive run that detects missing hooks offers the fix). A single y/N consent re-registers ALL missingafter_*hooks at once (not one prompt per hook). Declining the offer is a no-op + the warning./speckit.linear.installremains an equivalent restore. (Clarified 2026-06-23: warn + consented self-heal; 2026-06-24: offer on status too, single all-at-once consent.) - FR-010: The warning MUST fire at most once per reconcile run (deduped across
an
--allsweep), matching the bridge's existing one-shot-warning pattern. - FR-011: Documentation MUST give a crisp
--from <release-zip>install/update one-liner (pinned to the latest release tag) AND a note to re-run/speckit.linear.installafter a--forceupdate to restore the auto-sync hooks, so the workaround and the self-report point at the same fix. - FR-012: The feature MUST keep
extension.idaslinearand the command surface (speckit.linear.*) unchanged — detection lives inside existing commands, not a new command.
- Hook registration set: the six
after_*entries for thelinearextension in the consumer's.specify/extensions.yml, each either registered (enabled), intentionally disabled, or absent. - Hook-health state: the per-run assessment — fully present / partial (with the named absent hooks) / none — surfaced as a warning (reconcile) and a status line.
- SC-001: With one or more
after_*hooks absent, 100% of reconcile runs surface a warning naming the missing hooks + the remediation (no silent runs). - SC-002: With all hooks registered, 0 hook-health warnings are emitted
(no false positives), and a deliberately
enabled: falsehook produces 0 warnings. - SC-003: The hook-health warning never changes the reconcile's exit disposition — a run that would otherwise succeed still succeeds (the check is non-blocking in 100% of cases).
- SC-004: After the operator follows the remediation (
/speckit.linear.install), a subsequent reconcile emits 0 hook-health warnings (detect and restore agree). - SC-005:
speckit.linear.statusreports the correct hook-health state (present / partial / none) in 100% of the three cases, and exits 0 in 100% of cases regardless of hook health (the health line never alters status's exit disposition). - SC-006: The warning appears at most once per reconcile run regardless of how many specs are processed.
- Resolved disposition (clarification 2026-06-23): warn + consented self-heal —
interactive offers in-place re-registration (operator y/N); non-interactive is
warn-only;
/speckit.linear.installis an equivalent restore. - The install path's hook registration is idempotent and is the canonical restore (re-running install re-adds missing hooks), so no separate restore command is needed.
- "Registered" means an
after_<phase>entry whoseextensionislinearexists and is notenabled: false; this matches how install detects an already-present hook. - The warning reuses the bridge's existing structured-summary + one-shot-warning machinery (same surface as the operator-identity / overview warnings).
- Making
specify extension update linearwork (community extensions have no frictionless update by design; the trusted-catalog and default-catalog routes were considered and rejected). - Changing how the extension installs or copies files into a consumer repo.
- Auto-mutating
.specify/extensions.ymlwithout operator consent or in a non-interactive run (the self-heal is interactive + consented only; CI/ hook-fired runs never mutate). - The spec-kit-jira sibling (same hook-stripping exposure; parity follow-up).