fix(#3606): validate hook-kind coverage at call sites and dispatch generically (#3687)

* test(#3606): pin hook-kind coverage in the wired guard

* fix(#3606): validate hook-kind coverage at call sites and dispatch generically

* fix(#3606): address review - segment-granular narrowing, zero-coverage diagnosis, quick.md, fragment extraction

* fix(#3606): drop stale shrink-ack, export HOOK_GROUP_KINDS, dedupe scanner regex

* chore(#3606): regenerate install-tree fixtures for new wave-post fragment

* chore(#3606): sync canonical launcher preamble into new fragment

* fix(#3606): keep fragment preamble ahead of first gsd_run mention

* fix(#3606): revert sync script's preamble move in explore.md

* chore(#3606): regenerate derived manifests post-rebase

* chore(#3606): allowlist peer test files - base was red on the count lane

* chore(#3606): regenerate inventory for peer's verify-command-grounding doc

* chore(#3606): grounding test maps to its own module by longest prefix

* chore(#3606): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-19 16:41:27 -04:00
committed by GitHub
parent 79781e68eb
commit ea594300d9
35 changed files with 546 additions and 86 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3687
---
**Capability hooks can no longer be silently registered-but-never-run** — the capability validator now checks that each host call site's dispatch text covers every hook KIND registered at that point (a gate-only consumer fails validation when a step or contribution hook is registered there), and the plan/execute/verify host consumers now dispatch steps and contributions generically per the loop hook contract instead of hand-rolling one kind. (#3606)

View File

@@ -593,6 +593,7 @@
"execute-phase/steps/post-merge-gate.md",
"execute-phase/steps/regression-gate-run.md",
"execute-phase/steps/regression-gate.md",
"execute-phase/steps/wave-post-gate-hooks.md",
"execute-phase/steps/worktree-recovery-policy.md",
"new-milestone/steps/project-md-milestone-write.md",
"new-milestone/steps/reset-phase-safety.md",

View File

@@ -3435,26 +3435,55 @@ function topoSortContributions(entries) {
// ─── Gen-time wired guard ─────────────────────────────────────────────────────
/**
* Hook group (capability.json array name) → hook kind (dispatch discriminator).
* Single source of truth for both validateHooksWired and the scanner-parity
* test in tests/capability-registry.test.cjs (#3606).
*/
const HOOK_GROUP_KINDS = Object.freeze({
steps: 'step',
contributions: 'contribution',
gates: 'gate',
});
/**
* Validate that every hook point declared by a capability has a corresponding
* `loop render-hooks <point>` call site in one of the host-loop workflow files.
* `loop render-hooks <point>` call site in one of the host-loop workflow files,
* AND — #3606 — that the call site's dispatch text covers the hook's KIND.
*
* A call site proves hooks are rendered, not dispatched: a consumer that
* iterates only `kind == "gate"` (or narrows `kind == "step"` to one
* `ref.skill`) silently drops every other registered kind — a capability can
* be wired, enabled, resolved active, and never run. See
* gsd-core/references/loop-hook-dispatch.md ("A point whose workflow
* hand-rolls one kind does not implement this contract").
*
* Only valid loop points (in VALID_LOOP_POINTS) are checked here. Invalid points
* are already caught by validateStep/validateContribution/validateGate — do not
* double-report.
*
* KNOWN LIMITATION (#3606): coverage is the UNION across all call sites for a
* point in the five STEP_WORKFLOWS host files. Consumers outside that universe
* (quick.md, autonomous.md, code-review*.md, audit-milestone.md,
* secure-phase.md, validate-phase.md) are not per-file checked — a narrowed
* consumer there passes as long as one host file covers the point. Per-file
* coverage maps are the tightening path.
*
* @param {object} cap Validated capability object.
* @param {Set<string>} wiredSet Set of points that have call sites in host workflows.
* @returns {string[]} Array of error strings; empty means all points are wired.
* @param {Map<string, Set<string>>} wiredKinds Per point, the hook kinds the
* host workflows' call-site dispatch text covers (getWiredKinds). A point
* absent from the map is unwired.
* @returns {string[]} Array of error strings; empty means all points
* are wired and every registered kind is covered.
*/
function validateHooksWired(cap, wiredSet) {
function validateHooksWired(cap, wiredKinds) {
const errors = [];
const capId = cap.id || '(unknown)';
function checkPoint(point, groupName, idx) {
function checkPoint(point, groupName, kind, idx) {
// Only flag valid points that are unwired — invalid points are schema-validator's job.
if (!VALID_LOOP_POINTS.has(point)) return;
if (!wiredSet.has(point)) {
if (!wiredKinds.has(point)) {
errors.push(
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
'" is declared but not wired in any host-loop workflow ' +
@@ -3462,20 +3491,34 @@ function validateHooksWired(cap, wiredSet) {
'Wire the call site in the host workflow ' +
'(see scripts/gen-loop-host-contract.cjs STEP_WORKFLOWS) or remove the hook.',
);
return;
}
const covered = wiredKinds.get(point);
if (covered.size === 0) {
errors.push(
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
'" has `loop render-hooks ' + point + '` call site(s), but their dispatch text covers NO ' +
'hook kind — every consumer is narrowed to specific hooks. Dispatch every registered kind ' +
'per gsd-core/references/loop-hook-dispatch.md.',
);
return;
}
if (!covered.has(kind)) {
errors.push(
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
'" registers a ' + kind + ' hook, but the host call site\'s dispatch text never ' +
'covers `kind == "' + kind + '"` (it covers: ' + [...covered].sort().join(', ') + '). ' +
'A hand-rolled single-kind consumer silently never dispatches the other kinds — ' +
'dispatch every registered kind per gsd-core/references/loop-hook-dispatch.md.',
);
}
}
for (let i = 0; i < (cap.steps || []).length; i++) {
const hook = cap.steps[i];
if (hook.point !== undefined) checkPoint(hook.point, 'steps', i);
}
for (let i = 0; i < (cap.contributions || []).length; i++) {
const hook = cap.contributions[i];
if (hook.point !== undefined) checkPoint(hook.point, 'contributions', i);
}
for (let i = 0; i < (cap.gates || []).length; i++) {
const hook = cap.gates[i];
if (hook.point !== undefined) checkPoint(hook.point, 'gates', i);
for (const [group, kind] of Object.entries(HOOK_GROUP_KINDS)) {
for (let i = 0; i < (cap[group] || []).length; i++) {
const hook = cap[group][i];
if (hook.point !== undefined) checkPoint(hook.point, group, kind, i);
}
}
return errors;
@@ -3699,6 +3742,7 @@ module.exports = {
topoSortSteps,
topoSortContributions,
validateHooksWired,
HOOK_GROUP_KINDS,
validateConfigSliceEntry,
classifyCrossErrors,
runConfigFormatParityGate,

View File

@@ -644,7 +644,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
WAVE_PRE_HOOKS_JSON=$(gsd_run loop render-hooks execute:wave:pre --raw)
```
If a contribution's `activeHooks` entry provides an alternate wave dispatch, follow it instead of step 3's inline loop; otherwise proceed to step 3.
**Contribution dispatch:** inject every `kind == "contribution"` fragment per @gsd-core/references/loop-hook-dispatch.md (skip when none); one naming an alternate wave dispatch replaces step 3's inline loop. Then proceed to step 3.
3. **Spawn executor agents:**
@@ -1014,40 +1014,9 @@ increases monotonically across waves. `{status}` is `complete` (success),
**If `activeHooks` is empty or absent:** Skip silently to step 5.8.
⚠ **Validate `check` before shell use** (third-party manifest input) — `loop-hook-dispatch.md` § `gate`.
**Contribution dispatch:** inject every `kind == "contribution"` fragment per @gsd-core/references/loop-hook-dispatch.md (skip when none), before the gates below.
**For each active entry where `kind == "gate"`** (process in array order), run the gate check — a `predicate` gate (ADR-2008 / #2008) substitutes `gsd_run check predicate --predicate '<predicate JSON>' --phase-number "${PHASE_NUMBER}" --raw`:
```bash
GATE_RESULT=$(gsd_run check ${hook.check.query} "${PHASE_NUMBER}" --raw)
CHECK_EXIT=$?
```
**Step 1 — did the CHECK COMMAND itself succeed?**
If the check command failed (non-zero `CHECK_EXIT`, empty output, or unparseable JSON):
- `onError == "halt"` → treat as a fatal error: stop wave completion, do NOT proceed to step 5.8, and surface: `⚠ Gate check command failed ({hook.capId}): command error. Resolve before continuing.`
- `onError == "skip"` → log a warning and continue to the next hook. Do NOT read `GATE_RESULT.block`.
**Step 2 — read `GATE_RESULT.block` (boolean).** This step is only reached when the command succeeded.
- **Blocking gate (`hook.blocking == true`) AND `GATE_RESULT.block == true`:** HALT — stop wave completion, do NOT proceed to step 5.8, and present:
```
⚠ Wave {N} blocked by capability gate ({hook.capId}): {GATE_RESULT.message}
Resolve before continuing to next wave.
```
This halt is **not** bypassed by `onError` — `onError` only covers command errors (step 1 above), not the gate's block decision.
- **Non-blocking gate (`hook.blocking == false`):** never halts. If `GATE_RESULT.block` is `true` (or non-empty `message`), print `⚠ {hook.capId} advisory (wave {N}): {GATE_RESULT.message}`, then:
- If `GATE_RESULT.spawn_mapper == true` OR `GATE_RESULT.directive == "auto-remap"`: spawn `gsd-codebase-mapper` per `execute-phase/steps/codebase-drift-gate.md`; pass `--paths {GATE_RESULT.affected_paths}`. Continue regardless (wave NOT failed by remap failure).
- Otherwise: continue after advisory.
- If block `false` and no `message`: continue silently.
- **Blocking gate (`hook.blocking == true`) AND `GATE_RESULT.block == false`:** continue silently.
**When all active gates are processed without a blocking halt:** continue to step 5.8.
**For each active entry where `kind == "gate"`** (process in array order): read and execute `gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md` for the full evaluation contract (check validation, `onError`, blocking semantics, mapper spawn). When all active gates are processed without a blocking halt, continue to step 5.8.
5.8. **Handle test gate failures (when `WAVE_FAILURE_COUNT > 0`):**
@@ -1202,7 +1171,7 @@ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
```
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
Dispatch every `kind == "step"` hook per @gsd-core/references/loop-hook-dispatch.md (skip when none). The secure-phase routing below applies when that specific hook is active.
If no active secure-phase step hook exists: skip.

View File

@@ -0,0 +1,39 @@
# Wave-Post Gate Hook Evaluation (execute:wave:post)
Detail for the `kind == "gate"` dispatch at the execute:wave:post hook point, extracted from the host step 5.7 (#3606 — the host file carries a frozen pre-phase-6 byte ceiling, #1168; evaluation detail lives here).
⚠ **Validate `check` before shell use** (third-party manifest input) — `gsd-core/references/loop-hook-dispatch.md` § `gate`.
**For each active entry where `kind == "gate"`** (process in array order), run the gate check — a `predicate` gate (ADR-2008 / #2008) uses the check CLI's `predicate` form with the predicate JSON and phase number (shown in the block below):
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
GATE_RESULT=$(gsd_run check ${hook.check.query} "${PHASE_NUMBER}" --raw)
CHECK_EXIT=$?
```
**Step 1 — did the CHECK COMMAND itself succeed?**
If the check command failed (non-zero `CHECK_EXIT`, empty output, or unparseable JSON):
- `onError == "halt"` → treat as a fatal error: stop wave completion, do NOT proceed to step 5.8, and surface: `⚠ Gate check command failed ({hook.capId}): command error. Resolve before continuing.`
- `onError == "skip"` → log a warning and continue to the next hook. Do NOT read `GATE_RESULT.block`.
**Step 2 — read `GATE_RESULT.block` (boolean).** This step is only reached when the command succeeded.
- **Blocking gate (`hook.blocking == true`) AND `GATE_RESULT.block == true`:** HALT — stop wave completion, do NOT proceed to step 5.8, and present:
```
⚠ Wave {N} blocked by capability gate ({hook.capId}): {GATE_RESULT.message}
Resolve before continuing to next wave.
```
This halt is **not** bypassed by `onError` — `onError` only covers command errors (step 1 above), not the gate's block decision.
- **Non-blocking gate (`hook.blocking == false`):** never halts. If `GATE_RESULT.block` is `true` (or non-empty `message`), print `⚠ {hook.capId} advisory (wave {N}): {GATE_RESULT.message}`, then:
- If `GATE_RESULT.spawn_mapper == true` OR `GATE_RESULT.directive == "auto-remap"`: spawn `gsd-codebase-mapper` per `execute-phase/steps/codebase-drift-gate.md`; pass `--paths {GATE_RESULT.affected_paths}`. Continue regardless (wave NOT failed by remap failure).
- Otherwise: continue after advisory.
- If block `false` and no `message`: continue silently.
- **Blocking gate (`hook.blocking == true`) AND `GATE_RESULT.block == false`:** continue silently.
**When all active gates are processed without a blocking halt:** continue to step 5.8.

View File

@@ -423,6 +423,8 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED
PLAN_PRE_HOOKS_JSON=$(gsd_run loop render-hooks plan:pre --raw)
```
**Contribution dispatch (#3606):** inject every `kind == "contribution"` fragment from `PLAN_PRE_HOOKS_JSON` per @gsd-core/references/loop-hook-dispatch.md, in array order, into the role each entry's `into` names — planner-targeted ones land in the prompt block below, orchestrator-targeted ones in your working context. The security specialization below is one such contribution, not a replacement for the generic dispatch.
Resolve active contribution hooks from `PLAN_PRE_HOOKS_JSON` where `kind == "contribution"` and `capId == "security"`.
**If no active security contribution hook exists:** Skip to step 5.6.
@@ -1416,8 +1418,12 @@ PHASE_REQ_IDS=$(gsd_run query init.plan-phase "$PHASE" --pick phase_req_ids 2>/d
PHASE_REQ_IDS="${PHASE_REQ_IDS:-TBD}"
```
Read the `activeHooks` array from `PLAN_POST_HOOKS_JSON` in-context. If the
`gap-analysis` gate hook is absent (capability inactive), skip this step.
Read the `activeHooks` array from `PLAN_POST_HOOKS_JSON` in-context. If
`activeHooks` is empty or absent, skip this step silently — do NOT key the skip
on any one capability's gate being absent (#3606: that skip silently dropped
every other registered hook at this point).
**Step and contribution dispatch:** dispatch every `kind == "step"` hook and inject every `kind == "contribution"` fragment per @gsd-core/references/loop-hook-dispatch.md (skip each kind silently when none), before gate evaluation below.
⚠ **Validate `check` before shell use** (third-party manifest input) — `loop-hook-dispatch.md` § `gate`.

View File

@@ -549,9 +549,11 @@ Skip this step entirely if `$FULL_MODE` is false.
EXECUTE_POST_HOOKS_JSON=$(gsd_run loop render-hooks execute:post --raw)
```
**Generic step dispatch (#3606):** dispatch every `kind == "step"` hook from `EXECUTE_POST_HOOKS_JSON` per @gsd-core/references/loop-hook-dispatch.md (skip silently when none); each step is advisory and best-effort. The code-review specialization below is one such hook, not a replacement for the generic dispatch.
Resolve active step hooks from `EXECUTE_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "code-review"`.
If no active code-review step hook exists, skip with message "Code review skipped (code-review capability inactive)".
If no active code-review step hook exists, skip with message "Code review skipped (code-review capability inactive)" — after dispatching any other active step hooks above — and proceed.
**Scope files from executor's commits:**
```bash

View File

@@ -534,6 +534,8 @@ VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
```
**Generic step dispatch:** dispatch every `kind == "step"` hook from `VERIFY_POST_HOOKS_JSON` per @gsd-core/references/loop-hook-dispatch.md (skip silently when none). Each step is advisory and best-effort — honor `onError` and continue. The secure-phase handling below is an additional specialization of one such hook, not a replacement for the generic dispatch.
Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
If an active secure-phase step hook exists AND `SECURITY_FILE` is empty, dispatch the registry-provided skill stem:

View File

@@ -33,8 +33,8 @@ const CONFIG_SCHEMA_PATH = path.join(ROOT, 'gsd-core', 'bin', 'shared', 'config-
// registry generator and the loop-host-contract generator share one source of truth.
const { LOOP_HOST_CONTRACT } = require('../gsd-core/bin/lib/loop-host-contract.cjs');
// Wired-points helper — tells us which points actually have render-hooks call sites.
const { getWiredLoopPoints } = require('./gen-loop-host-contract.cjs');
// Wired-kinds helper — per point, which hook kinds the render-hooks call sites' dispatch text covers.
const { getWiredKinds } = require('./gen-loop-host-contract.cjs');
// Capability validator — shared runtime-callable module extracted per ADR-1244 D2.
const capValidator = require('../gsd-core/bin/lib/capability-validator.cjs');
@@ -419,9 +419,11 @@ function loadAndValidate(centralKeys, capabilitiesDir, centralPatterns) {
return { capMap, errors, warnings };
}
// Compute wired points ONCE before iterating capabilities so the filesystem
// scan is not repeated per-capability. ROOT is the repo root (defined at top of file).
const wiredSet = getWiredLoopPoints(ROOT);
// Compute wired points + covered kinds ONCE before iterating capabilities so
// the filesystem scan is not repeated per-capability. ROOT is the repo root
// (defined at top of file). #3606: the kinds map carries, per point, which
// hook kinds the call sites' dispatch text actually covers.
const wiredKinds = getWiredKinds(ROOT);
const folderEntries = fs.readdirSync(resolvedCapDir, { withFileTypes: true })
.filter((e) => e.isDirectory())
@@ -459,7 +461,7 @@ function loadAndValidate(centralKeys, capabilitiesDir, centralPatterns) {
}
// Gen-time wired guard: reject hooks that declare a valid point with no call site.
const wiredErrors = validateHooksWired(cap, wiredSet);
const wiredErrors = validateHooksWired(cap, wiredKinds);
if (wiredErrors.length > 0) {
for (const e of wiredErrors) errors.push(folderId + '/capability.json: ' + e);
continue;

View File

@@ -446,8 +446,11 @@ const HOST_LOOP_FILES = STEP_WORKFLOWS.map((w) => 'gsd-core/workflows/' + w.file
* @param {string} text Content of a workflow file (or any text).
* @returns {Set<string>}
*/
/** The call-site shape both scanners key on — one regex, two consumers (#3606). */
const CALL_SITE_RE = /loop render-hooks\s+([a-z:]+)/g;
function scanWiredPoints(text) {
const re = /loop render-hooks\s+([a-z:]+)/g;
const re = CALL_SITE_RE;
const result = new Set();
let m;
while ((m = re.exec(text)) !== null) {
@@ -456,6 +459,132 @@ function scanWiredPoints(text) {
return result;
}
// ─── Hook-kind coverage (#3606) ──────────────────────────────────────────────
const HOOK_KINDS = ['contribution', 'step', 'gate'];
/**
* The kinds one call site's dispatch text actually covers (#3606).
*
* A point having a `loop render-hooks <point>` call site proves the hooks are
* RENDERED, not that they are DISPATCHED — a consumer that iterates only
* `kind == "gate"` (or narrows `kind == "step"` to one `ref.skill`) silently
* drops every other registered kind. Coverage rules for the text following a
* call site, up to the next call site or the region cap, judged LINE by line:
*
* - A deferral line (one carrying an `@`-included path to the generic
* contract, e.g. `@gsd-core/references/loop-hook-dispatch.md`) that names a
* kind (`kind == "step"`) covers that kind; a deferral line with no kind
* discriminator ("apply each entry") covers every kind. A bare §-citation
* of the reference (validation guidance only, no `@`) covers nothing —
* plan-phase cites the gate-validation section while dispatching only gates.
* - Otherwise a kind is covered when some LINE dispatches it unconditionally:
* a `kind == "<kind>"` discriminator with NO same-line narrowing to one
* hook (`ref.skill ==`, `ref.agent ==`, `ref.command ==`). A narrowed line
* special-cases ONE hook and proves nothing about the kind generally — the
* exact hand-rolled-consumer shape the reference warns about.
*
* Quote style and spacing vary across the corpus (`kind == "step"`,
* `kind === 'gate'`), so the matcher is tolerant of both quote characters and
* of `==`/`===`.
*
* Pure: same input, same output; CRLF-safe (line splitting tolerates \r).
*
* @param {string} region Dispatch text following one call site.
* @returns {Set<string>}
*/
function coveredKindsInRegion(region) {
const covered = new Set();
// Same-SEGMENT narrowing to ONE hook voids credit: `ref.skill ==`, `capId ==`,
// and `into ==` each special-case a subset, not the kind generally
// (plan-phase's `kind == "contribution" and capId == "security"` is the
// hand-rolled shape; `into == "planner"` covers only planner-targeted
// contributions). Segments, not lines: execute-phase legitimately writes
// "dispatch `kind == "step"` hooks per … . `ref.skill == "code-review"`:" —
// the deferral is one sentence, the specialization the next; narrowing in a
// DIFFERENT segment must not void the deferral's credit.
const narrowingRe = /(?:ref\.(?:skill|agent|command)|capId|into)\s*={2,3}/;
// Negated mentions describe an absence, not a dispatch ("Branch 1 — no active
// step hooks (`activeHooks` has no entry with `kind == "step"`)" — ship.md).
const negationRe = /\b(?:no|without|absent|lacks?|missing)\b[^.|]*kind\s*={2,3}/;
const deferralRe = /@\S*loop-hook-dispatch\.md/;
for (const line of region.split(/\r?\n/)) {
// Sentence segments: a `.`/`;` followed by whitespace ends a segment. A
// period NOT followed by whitespace (the `.md` inside a deferral path,
// `ref.skill`) is not a boundary.
for (const segment of line.split(/(?<=[.;])\s+/)) {
const kindDiscriminators = [];
for (const kind of HOOK_KINDS) {
if (new RegExp(`kind\\s*={2,3}\\s*["']${kind}["']`).test(segment)) kindDiscriminators.push(kind);
}
if (kindDiscriminators.length === 0) {
// A deferral with no kind discriminator ("apply each entry per …")
// still covers every kind.
if (deferralRe.test(segment)) for (const kind of HOOK_KINDS) covered.add(kind);
continue;
}
if (negationRe.test(segment)) continue;
if (deferralRe.test(segment)) {
// Deferral naming kinds ("dispatch `kind == "step"` hooks per …").
if (!narrowingRe.test(segment)) for (const kind of kindDiscriminators) covered.add(kind);
continue;
}
if (!narrowingRe.test(segment)) for (const kind of kindDiscriminators) covered.add(kind);
}
}
return covered;
}
/**
* Scan every `loop render-hooks <point>` call site in `text` and accumulate,
* per point, the union of hook kinds its dispatch regions cover (#3606).
*
* @param {string} text Content of a workflow file (or any text).
* @returns {Map<string, Set<string>>} point → covered kinds.
*/
function scanWiredKinds(text) {
const result = new Map();
const siteRe = CALL_SITE_RE;
const sites = [];
let m;
while ((m = siteRe.exec(text)) !== null) sites.push({ point: m[1], start: m.index });
const REGION_CAP = 6000;
for (let i = 0; i < sites.length; i++) {
const regionEnd = i + 1 < sites.length ? sites[i + 1].start : Math.min(text.length, sites[i].start + REGION_CAP);
const region = text.slice(sites[i].start, regionEnd);
const covered = coveredKindsInRegion(region);
if (!result.has(sites[i].point)) result.set(sites[i].point, new Set());
for (const kind of covered) result.get(sites[i].point).add(kind);
}
return result;
}
/**
* Read every host-loop workflow file and return, per point, the union of hook
* kinds its call sites' dispatch text covers (#3606).
*
* @param {string} [repoRoot] Path to the repository root. Defaults to ROOT.
* @returns {Map<string, Set<string>>}
*/
function getWiredKinds(repoRoot) {
const resolvedRoot = repoRoot !== undefined ? repoRoot : ROOT;
const result = new Map();
for (const relPath of HOST_LOOP_FILES) {
const absPath = path.join(resolvedRoot, relPath);
let content;
try {
content = fs.readFileSync(absPath, 'utf8');
} catch (err) {
throw new Error('getWiredKinds: cannot read host-loop file ' + absPath + ': ' + err.message);
}
for (const [point, kinds] of scanWiredKinds(content)) {
if (!result.has(point)) result.set(point, new Set());
for (const kind of kinds) result.get(point).add(kind);
}
}
return result;
}
/**
* Read every host-loop workflow file and return the union of all wired loop points
* (i.e. points that have a `loop render-hooks <point>` call site).
@@ -497,6 +626,10 @@ module.exports = {
ROLE_TO_AGENT,
scanWiredPoints,
getWiredLoopPoints,
coveredKindsInRegion,
scanWiredKinds,
getWiredKinds,
HOOK_KINDS,
};
// ─── CLI entry point ──────────────────────────────────────────────────────────

View File

@@ -106,7 +106,8 @@
"verify-work-auto-transition.test.cjs",
"verify.test.cjs"
],
"issue": "3767"
"issue": "3767",
"justification": "verify-command-grounding added by #2401/#3678; allowlist follow-up landed with #3606 (base was red on this lane)."
},
"install": {
"files": [

View File

@@ -4893,7 +4893,12 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
// ─── Defect 2 — gen-time wired guard ────────────────────────────────────────
describe('Defect 2: validateHooksWired gen-time guard', () => {
// Shared #3606 fixtures: every hook kind, and a Map<point, Set<kind>> builder
// for validateHooksWired's wiredKinds argument.
const ALL = ['contribution', 'step', 'gate'];
const kindsMap = (spec) => new Map(Object.entries(spec).map(([pt, ks]) => [pt, new Set(ks)]));
describe('Defect 2: validateHooksWired gen-time wired guard', () => {
/** Minimal capability fixture with one hook at a given point */
function makeCapWithStep(point) {
return {
@@ -4930,7 +4935,7 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
test('returns non-empty error array mentioning "not wired" when step point is not in wiredSet', () => {
const cap = makeCapWithStep('discuss:pre');
const wiredSet = new Set(['plan:pre', 'plan:post']); // discuss:pre absent
const wiredSet = kindsMap({ 'plan:pre': ALL, 'plan:post': ALL }); // discuss:pre absent
const errs = validateHooksWired(cap, wiredSet);
assert.ok(Array.isArray(errs), 'must return an array');
assert.ok(errs.length > 0, 'must return errors when point is unwired');
@@ -4942,7 +4947,7 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
test('returns non-empty error array when contribution point is not in wiredSet', () => {
const cap = makeCapWithContribution('discuss:pre');
const wiredSet = new Set(['plan:pre']); // discuss:pre absent
const wiredSet = kindsMap({ 'plan:pre': ALL }); // discuss:pre absent
const errs = validateHooksWired(cap, wiredSet);
assert.ok(errs.length > 0, 'must return errors for unwired contribution point');
assert.match(errs.join(' '), /not wired/i);
@@ -4950,7 +4955,7 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
test('returns non-empty error array when gate point is not in wiredSet', () => {
const cap = makeCapWithGate('discuss:pre');
const wiredSet = new Set(['plan:pre']); // discuss:pre absent
const wiredSet = kindsMap({ 'plan:pre': ALL }); // discuss:pre absent
const errs = validateHooksWired(cap, wiredSet);
assert.ok(errs.length > 0, 'must return errors for unwired gate point');
assert.match(errs.join(' '), /not wired/i);
@@ -4958,35 +4963,37 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
test('returns empty array when all declared points are in wiredSet', () => {
const cap = makeCapWithStep('plan:pre');
const wiredSet = new Set(['plan:pre', 'plan:post', 'execute:post']);
const wiredSet = kindsMap({ 'plan:pre': ALL, 'plan:post': ALL, 'execute:post': ALL });
const errs = validateHooksWired(cap, wiredSet);
assert.deepEqual(errs, [], 'must return empty array when all points are wired');
assert.deepEqual(errs, [], 'must return empty array when all points are wired and kind-covered');
});
test('boundary: cap declaring discuss:pre is rejected against wiredSet lacking it', () => {
const cap = makeCapWithStep('discuss:pre');
const smallSet = new Set(['plan:pre', 'plan:post']);
const smallSet = kindsMap({ 'plan:pre': ALL, 'plan:post': ALL });
const errs = validateHooksWired(cap, smallSet);
assert.ok(errs.length > 0, 'must reject discuss:pre against a set that lacks it');
});
test('boundary: cap declaring discuss:pre is accepted against real getWiredLoopPoints(ROOT) post-fix', () => {
test('boundary: cap declaring discuss:pre is accepted against real getWiredKinds(ROOT) post-fix', () => {
const cap = makeCapWithStep('discuss:pre');
const realWired = getWiredLoopPoints(ROOT);
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
const realWired = getWiredKinds(ROOT);
const errs = validateHooksWired(cap, realWired);
assert.deepEqual(
errs, [],
`discuss:pre must be wired after the fix. Errors: ${errs.join('; ')}`,
`discuss:pre must be wired and kind-covered after the fix. Errors: ${errs.join('; ')}`,
);
});
test('boundary: cap declaring discuss:post is accepted against real getWiredLoopPoints(ROOT) post-fix', () => {
test('boundary: cap declaring discuss:post is accepted against real getWiredKinds(ROOT) post-fix', () => {
const cap = makeCapWithContribution('discuss:post');
const realWired = getWiredLoopPoints(ROOT);
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
const realWired = getWiredKinds(ROOT);
const errs = validateHooksWired(cap, realWired);
assert.deepEqual(
errs, [],
`discuss:post must be wired after the fix. Errors: ${errs.join('; ')}`,
`discuss:post must be wired and kind-covered after the fix. Errors: ${errs.join('; ')}`,
);
});
@@ -4999,7 +5006,7 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
gates: [],
config: {},
};
const wiredSet = new Set(['plan:pre']); // the invalid point is not here either
const wiredSet = kindsMap({ 'plan:pre': ALL }); // the invalid point is not here either
const errs = validateHooksWired(cap, wiredSet);
// Should NOT flag it — invalid points are the schema validator's job
const notWiredErrors = errs.filter((e) => /not wired/i.test(e));
@@ -5010,6 +5017,228 @@ describe('#1196 — discuss loop wiring + wired-point guard', () => {
});
});
// ─── Defect 3: hook-kind coverage (#3606) ────────────────────────────────────
//
// A point having a call site proves hooks are RENDERED, not DISPATCHED. A
// consumer that iterates only `kind == "gate"` (or narrows `kind == "step"`
// to one ref.skill) silently drops every other registered kind — mempalace's
// step hooks were wired, active, and never run for five days across
// plan:post / execute:wave:post / verify:post / execute:post consumers.
// The guard must validate, per registered kind, that the call site's
// dispatch text covers that kind.
describe('Defect 3: validateHooksWired hook-kind coverage (#3606)', () => {
test('a step hook at a point whose site covers only gate fails with a kind-coverage error', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [{ point: 'plan:post', ref: { skill: 'my-skill' }, produces: [], consumes: [], onError: 'skip' }],
contributions: [],
gates: [],
config: {},
};
const wired = kindsMap({ 'plan:post': ['gate'] }); // hand-rolled gate-only consumer
const errs = validateHooksWired(cap, wired);
assert.ok(errs.length > 0, 'a step registered at a gate-only point must fail validation');
const joined = errs.join(' ');
assert.match(joined, /plan:post/, 'error must name the point');
assert.match(joined, /test-cap/, 'error must name the capability id');
assert.match(joined, /step/, 'error must name the uncovered kind');
assert.doesNotMatch(joined, /not wired/i, 'the point IS wired — this is the coverage error, not the wiring error');
});
test('a contribution hook at a point whose site covers only gate fails', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [],
contributions: [{ point: 'execute:wave:post', into: 'orchestrator', fragment: { inline: 'hi' }, produces: [], consumes: [] }],
gates: [],
config: {},
};
const wired = kindsMap({ 'execute:wave:post': ['gate'] });
const errs = validateHooksWired(cap, wired);
assert.ok(errs.length > 0, 'a contribution registered at a gate-only point must fail validation');
assert.match(errs.join(' '), /contribution/);
});
test('a point covered for exactly the registered kinds passes (boundary: no over-reach)', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [{ point: 'ship:post', ref: { skill: 'my-skill' }, produces: [], consumes: [], onError: 'skip' }],
contributions: [],
gates: [],
config: {},
};
const wired = kindsMap({ 'ship:post': ['step'] }); // ship.md's real coverage
assert.deepEqual(validateHooksWired(cap, wired), [], 'step at a step-covered point must pass');
});
test('unwired-point errors are unchanged when the arg is a kinds Map', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [{ point: 'discuss:pre', ref: { skill: 'x' }, produces: [], consumes: [], onError: 'skip' }],
contributions: [],
gates: [],
config: {},
};
const wired = kindsMap({ 'plan:pre': ALL }); // discuss:pre absent entirely
const errs = validateHooksWired(cap, wired);
assert.ok(errs.length > 0, 'unwired point still fails');
assert.match(errs.join(' '), /not wired/i, 'the existing unwired error text is preserved');
});
test('boundary: invalid points are not kind-flagged (schema validator owns them)', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [{ point: 'not:a:real:point', ref: { skill: 'x' }, produces: [], consumes: [], onError: 'skip' }],
contributions: [],
gates: [],
config: {},
};
const errs = validateHooksWired(cap, kindsMap({}));
assert.deepEqual(errs, [], 'invalid points produce neither wiring nor coverage errors');
});
test('the real tree passes the extended guard for every in-tree registered kind', () => {
// Self-enforcement: the repo's own capabilities and host workflows must
// satisfy the new check — this is the test that forces consumer fixes.
const { getWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
const wiredKinds = getWiredKinds(ROOT);
const failures = [];
for (const capPath of fs.readdirSync(path.join(ROOT, 'capabilities'))) {
const manifest = path.join(ROOT, 'capabilities', capPath, 'capability.json');
if (!fs.existsSync(manifest)) continue;
let cap;
try { cap = JSON.parse(fs.readFileSync(manifest, 'utf8')); } catch { continue; }
for (const err of validateHooksWired(cap, wiredKinds)) failures.push(`${capPath}: ${err}`);
}
assert.deepEqual(
failures, [],
`in-tree capabilities must be fully kind-covered by host workflows:\n ${failures.join('\n ')}`,
);
});
});
describe('coveredKindsInRegion: dispatch-text kind coverage (#3606)', () => {
test('deferral line without a kind discriminator covers every kind', () => {
const region = 'Apply each entry in `activeHooks` per @gsd-core/references/loop-hook-dispatch.md\n';
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
assert.deepEqual([...coveredKindsInRegion(region)].sort(), ['contribution', 'gate', 'step']);
});
test('deferral line naming one kind covers only that kind', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = 'Dispatch `kind == "step"` hooks per @gsd-core/references/loop-hook-dispatch.md.\n';
assert.deepEqual([...coveredKindsInRegion(region)], ['step']);
});
test('a bare §-citation of the reference covers nothing (validation guidance, not dispatch)', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = '⚠ **Validate `check` before shell use** — `loop-hook-dispatch.md` § `gate`.\n**For each active entry where `kind == "gate"`**\n';
assert.deepEqual([...coveredKindsInRegion(region)], ['gate'], 'gate dispatched unconditionally, step/contribution NOT');
});
test('a narrowed kind line (kind == step AND ref.skill ==) does not cover the kind', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = 'Resolve hooks where `kind == "step"` and `ref.skill == "secure-phase"`.\n';
assert.deepEqual([...coveredKindsInRegion(region)], [], 'a one-skill special case proves nothing about the kind generally');
});
test('quote and operator variants are tolerated (=== and single quotes)', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = "for h in hooks: if h.kind === 'contribution' then inject\n";
assert.deepEqual([...coveredKindsInRegion(region)], ['contribution']);
});
test('CRLF input yields the same verdicts as LF', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const lf = 'per @gsd-core/references/loop-hook-dispatch.md\n`kind == "gate"` unconditional\n';
const crlf = lf.replace(/\n/g, '\r\n');
assert.deepEqual([...coveredKindsInRegion(crlf)].sort(), [...coveredKindsInRegion(lf)].sort());
});
test('scanWiredKinds accumulates per-point unions across multiple call sites', () => {
const { scanWiredKinds } = require('../scripts/gen-loop-host-contract.cjs');
const text = [
'X=$(gsd_run loop render-hooks verify:post --raw)',
'**For each active entry where `kind == "gate"`**',
'Y=$(gsd_run loop render-hooks verify:post --raw)',
'Dispatch `kind == "step"` hooks per @gsd-core/references/loop-hook-dispatch.md.',
'',
].join('\n');
const m = scanWiredKinds(text);
assert.ok(m.has('verify:post'), 'point must be present');
assert.deepEqual(
[...m.get('verify:post')].sort(),
['gate', 'step'],
'two call sites at one point accumulate their coverage',
);
});
test('capId == and into == narrowing void contribution credit (#3606 adversarial finding)', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const capIdNarrowed = 'Resolve hooks where `kind == "contribution"` and `capId == "security"`.\n';
assert.deepEqual([...coveredKindsInRegion(capIdNarrowed)], [],
'a one-capability hand-roll is not generic contribution coverage');
const intoNarrowed = 'For each entry where `kind == "contribution"` and `into == "planner"`: inject.\n';
assert.deepEqual([...coveredKindsInRegion(intoNarrowed)], [],
'planner-targeted-only injection leaves orchestrator-targeted contributions dispatched by no one');
});
test('a negated kind mention describes an absence, not a dispatch', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = 'Branch 1 — no active `ship:post` step hooks (`activeHooks` has no entry with `kind == "step"`): Skip.\n';
assert.deepEqual([...coveredKindsInRegion(region)], [],
'"has no entry with kind == step" must not count as step coverage');
});
test('a deferral sentence keeps its credit when the NEXT sentence specializes (segment split)', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = 'Dispatch `kind == "step"` hooks per @gsd-core/references/loop-hook-dispatch.md. `ref.skill == "code-review"`:\n';
assert.deepEqual([...coveredKindsInRegion(region)], ['step'],
'the generic deferral and the one-hook specialization are separate sentences');
});
test('a deferral path with a period (loop-hook-dispatch.md) is not split into segments', () => {
const { coveredKindsInRegion } = require('../scripts/gen-loop-host-contract.cjs');
const region = 'Apply each entry per @~/.claude/gsd-core/references/loop-hook-dispatch.md\n';
assert.deepEqual([...coveredKindsInRegion(region)].sort(), ['contribution', 'gate', 'step'],
'the .md inside the deferral path is not a sentence boundary');
});
test('a site whose consumers are all narrowed yields the zero-coverage error, not "not wired"', () => {
const cap = {
id: 'test-cap',
role: 'feature',
steps: [{ point: 'verify:post', ref: { skill: 'x' }, produces: [], consumes: [], onError: 'skip' }],
contributions: [],
gates: [],
config: {},
};
const wired = new Map([['verify:post', new Set()]]); // sites exist, all narrowed
const errs = validateHooksWired(cap, wired);
assert.ok(errs.length > 0, 'all-narrowed consumers must fail');
const joined = errs.join(' ');
assert.match(joined, /covers NO hook kind/i, 'error must name the zero-coverage diagnosis');
assert.doesNotMatch(joined, /not wired/i, 'the site EXISTS — misdiagnosing it as unwired points at the wrong remedy');
});
test('HOOK_KINDS vocabulary parity: scanner kinds match the validator group->kind mapping', () => {
const { HOOK_KINDS } = require('../scripts/gen-loop-host-contract.cjs');
const { HOOK_GROUP_KINDS } = require('../gsd-core/bin/lib/capability-validator.cjs');
assert.deepEqual(
[...HOOK_KINDS].sort(),
Object.values(HOOK_GROUP_KINDS).sort(),
'the scanner vocabulary must stay in lock-step with validateHooksWired\u2019s exported group->kind mapping — a rename on either side must fail here',
);
});
});
// ─── Anti-pattern parity guards ──────────────────────────────────────────────
describe('Anti-pattern parity: host-file set has a single source of truth', () => {

View File

@@ -2,7 +2,7 @@
"version": 1,
"paths": {
"verify-work.md": {
"reason": "#3357: the `03-VERIFICATION.md` staleness check now resolves the phase's own report via `gsd_run query verification.resolve-file \"$PHASE_DIR\" --raw` instead of `ls \"${PHASE_DIR}\"/*-VERIFICATION.md | head -1`, routing through the same shared resolver seam as transition.md so both call sites agree on which file is canonical. Growth is +13 bytes (38,983 -> 38,996), the delta between the old ls/head-1 pipeline and the gsd_run call. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 38996 -> 39107 bytes (+111)."
"reason": "#3357: the `03-VERIFICATION.md` staleness check now resolves the phase's own report via `gsd_run query verification.resolve-file \"$PHASE_DIR\" --raw` instead of `ls \"${PHASE_DIR}\"/*-VERIFICATION.md | head -1`, routing through the same shared resolver seam as transition.md so both call sites agree on which file is canonical. Growth is +13 bytes (38,983 -> 38,996), the delta between the old ls/head-1 pipeline and the gsd_run call. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 38996 -> 39107 bytes (+111). \u2014 #3606 append: verify:post consumer gains generic step dispatch (deferral to loop-hook-dispatch.md) before the secure-phase specialization, so ui/security/mempalace/nyquist steps registered at verify:post actually run instead of being filtered out by the one-skill narrow."
}
}
}

View File

@@ -4,4 +4,4 @@
"execute-phase.md": "#3370: the step-3 executor-routing line now also cites #3370 \u2014 the checkpoint gate rule itself lives in the execute-phase/steps/per-plan-executor-routing.md fragment (loaded per plan in every isolation mode immediately before the dispatch prompt is composed), the same keep-the-host-lean pattern #1689/#3417 used, because the host sits under the frozen ADR-857 Phase 6 ceiling (\u226493400). Net growth is 6 bytes (93386 -> 93392): the routing citation only; the rule text, which forbids the orchestrator from composing dispatch text that refuses or overrides auto-approval for the default gate=\"blocking\" (only blocking-human always surfaces), is in the fragment. Supersedes the spent #3324 fragment (merged into next), which also named execute-phase.md and would otherwise double-ack the same path. \u2014 #3423 append (epic #1891 F8): grew 12 bytes with the <files_to_read> -> <required_reading> tag rename (4 tag tokens, +3 bytes each), then trimmed 8 bytes of redundant prose (dropped 'current' from the model-inheritance note; #3478's growth had left only 8 bytes of ADR-857 margin) to stay under the Phase 6 margin ceiling; net +4 bytes against the #3370 baseline. \u2014 #3210 append: the checkpoint_handling blocking-human carve-out names precondition-unmet checkpoints (+29-byte marker '(precondition-unmet, #3210)'), and the decision bullet's 'Except blocking-human' conditional \u2014 required by tests/package-legitimacy-gate.test.cjs \u2014 was restored (+29), funded by trimming ', regardless of type' (-20, the carve-out already overrides all branches) and '(standard flow below)' -> '(standard flow)' (-6); net +3 bytes (93395 -> 93398), still under the frozen ADR-857 Phase 6 ceiling (<=93400). \u2014 #3576 append: bare `references/<name>.md` cites repaired to the canonical `gsd-core/references/<name>.md` form (+9 bytes, 1 cite(s) \u00d7 9). Dead-pointer fix; no content change.",
"execute-plan.md": "#3370: the Pattern A dispatch prompt spec gained the gate-semantics clause (gate=\"blocking\" (the default) is auto-approvable in auto-mode per the executor's own checkpoint protocol, gate=\"blocking-human\" always surfaces to a human; add no instruction overriding that protocol), closing the identically-shaped dispatch-time gap on the single-plan path named in the issue. Growth ~248 bytes (38913 -> 39161, still under the DEFAULT 40 KiB ceiling). Supersedes the spent #2652 fragment (merged into next), which also named execute-plan.md and would otherwise double-ack the same path."
}
}
}

View File

@@ -1,12 +1,12 @@
{
"version": 1,
"paths": {
"gsd-phase-researcher.md": "#3409: guarded `cat \"$phase_dir\"/*-CONTEXT.md` against nullglob wiping the pattern to zero operands when no CONTEXT.md exists — a bare `cat` with no operands blocks reading stdin (hangs the agent) instead of the `2>/dev/null` guard ever firing, since a stalled read is not a failing exit. Now checks `${_CTX[0]}` is a real path before invoking cat. Growth is the array-guard idiom itself (+43 bytes).",
"gsd-verifier.md": "#3409: same nullglob-hang fix as gsd-phase-researcher.md, applied to `cat \"$PHASE_DIR\"/*-VERIFICATION.md` in Step 0 — an absent VERIFICATION.md previously left a zero-operand `cat` blocking on stdin instead of falling through to first-verification mode. Growth is the array-guard idiom (+49 bytes). — #3206 append (merged into this fragment because two ack sources may never name the same path): +52 bytes, 49098 -> 49150 (2 under the LARGE cap). The growth is the literal fix for the term 5b used undefined: the compressed explicit-evidence definition inlined at 5b (+34 net on the rewritten line — the trailing honest-verifier cite there is dropped as superseded by the inline definition; honest-verifier.md stays cited at 5c) plus gsd-core/ path-prefix repairs on the two 404ing bare references/ cites at 5c (honest-verifier.md) and the MVP-mode section (verify-mvp-mode.md) (+9 each). Lazy extraction remains untakeable in this change: the large extractable blocks are content-pinned by tests that read the agent file directly (tests/verifier-behavior-unverified.test.cjs, tests/verification-overrides.test.cjs), so extraction is its own coordinated change.",
"complete-milestone.md": "#3409: guarded `cat .planning/phases/*-*/*-SUMMARY.md` — with `shopt -s nullglob` active in this block's preamble (#2962), zero matching phase summaries collapses the glob to nothing and a bare `cat` blocks reading stdin rather than producing empty output, wedging the milestone-completion review. Growth is the array-existence-check idiom (+73 bytes, two glob segments makes this longer than the single-glob sites). — #2142 append (merged into this fragment because two ack sources may never name the same path): +1605 bytes, 40498 -> 42103. The `archive_milestone` step now documents the opt-in `--archive-quick` quick-task archival flag (default OFF, deliberately NOT symmetrical with phase archival's default-ON posture), folds the AskUserQuestion decision for it into the SAME `milestone.complete` invocation (avoiding a redundant second call), and states the known bucket-all provenance limit.",
"discuss-phase-assumptions.md": "#3409: replaced the unreachable `AUTO_MODE=$(gsd_run query check auto-mode --pick active 2>/dev/null || echo \"false\")` — `||` never fires because the query exits 0 with empty stdout when the field is absent, not a failure, so AUTO_MODE silently ended up empty rather than \"false\" — with a two-line capture-then-default (`AUTO_MODE=\"${AUTO_MODE:-false}\"`) that actually reaches the fallback. Growth is the extra default-assignment line (+19 bytes).",
"plan-phase.md": "#3409: three sites. `AUTO_CHAIN` and `PHASE_REQ_IDS` get the same unreachable-`||`-fallback fix as discuss-phase-assumptions.md (empty-but-successful `gsd_run query` output never triggered `|| echo`, now uses `${VAR:-default}`); `PRIOR_SUMMARIES` additionally swapped `gsd_run query phases.list --pick summaries_total` for `--type summaries --pick count` since the old pick key produced the same unreachable-fallback failure mode for the walking-skeleton check. Net growth across the three sites is +39 bytes. — #3576 append: bare `references/<name>.md` cites repaired to the canonical `gsd-core/references/<name>.md` form (+36 bytes, 4 cite(s) × 9). Dead-pointer fix; no content change. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 90516 -> 90627 bytes (+111). — #2401 append (merged into this fragment because two ack sources may never name the same path): plan-phase.md now dispatches the deterministic `gsd_run check verify-command-paths` probe before spawning the plan-check pass and interpolates its result into the verification prompt as {VERIFY_PATHS} inside a new <verify_command_path_probe> block, plus a <proven_verify_commands> block carrying prior_verify_commands into planning context so the planner can reuse a proven path instead of re-deriving one. 90627 -> 92807 bytes (+2180). Deliberate runtime-loaded workflow text for the new feature, not converter drift.",
"session-report.md": "#3409: guarded `ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo \"No previous reports\"` — with nullglob active, zero prior reports collapses the pattern to nothing and `ls -la` with no operands lists the current directory (a successful exit, wrong output) instead of failing into the `|| echo` fallback, so the report-existence check silently printed a directory listing. Replaced with an array-existence check that only lists when a real report file is present. Growth is the guard idiom (+58 bytes).",
"transition.md": "#3409: guarded `cat .planning/phases/XX-current/*-SUMMARY.md` — same nullglob-hang defect as complete-milestone.md's phase-summary read: zero summaries left a bare `cat` blocking on stdin instead of proceeding with no summary content during PROJECT.md evolution. Growth is the array-existence-check idiom (+73 bytes)."
"gsd-phase-researcher.md": "#3409: guarded `cat \"$phase_dir\"/*-CONTEXT.md` against nullglob wiping the pattern to zero operands when no CONTEXT.md exists \u2014 a bare `cat` with no operands blocks reading stdin (hangs the agent) instead of the `2>/dev/null` guard ever firing, since a stalled read is not a failing exit. Now checks `${_CTX[0]}` is a real path before invoking cat. Growth is the array-guard idiom itself (+43 bytes).",
"gsd-verifier.md": "#3409: same nullglob-hang fix as gsd-phase-researcher.md, applied to `cat \"$PHASE_DIR\"/*-VERIFICATION.md` in Step 0 \u2014 an absent VERIFICATION.md previously left a zero-operand `cat` blocking on stdin instead of falling through to first-verification mode. Growth is the array-guard idiom (+49 bytes). \u2014 #3206 append (merged into this fragment because two ack sources may never name the same path): +52 bytes, 49098 -> 49150 (2 under the LARGE cap). The growth is the literal fix for the term 5b used undefined: the compressed explicit-evidence definition inlined at 5b (+34 net on the rewritten line \u2014 the trailing honest-verifier cite there is dropped as superseded by the inline definition; honest-verifier.md stays cited at 5c) plus gsd-core/ path-prefix repairs on the two 404ing bare references/ cites at 5c (honest-verifier.md) and the MVP-mode section (verify-mvp-mode.md) (+9 each). Lazy extraction remains untakeable in this change: the large extractable blocks are content-pinned by tests that read the agent file directly (tests/verifier-behavior-unverified.test.cjs, tests/verification-overrides.test.cjs), so extraction is its own coordinated change.",
"complete-milestone.md": "#3409: guarded `cat .planning/phases/*-*/*-SUMMARY.md` \u2014 with `shopt -s nullglob` active in this block's preamble (#2962), zero matching phase summaries collapses the glob to nothing and a bare `cat` blocks reading stdin rather than producing empty output, wedging the milestone-completion review. Growth is the array-existence-check idiom (+73 bytes, two glob segments makes this longer than the single-glob sites). \u2014 #2142 append (merged into this fragment because two ack sources may never name the same path): +1605 bytes, 40498 -> 42103. The `archive_milestone` step now documents the opt-in `--archive-quick` quick-task archival flag (default OFF, deliberately NOT symmetrical with phase archival's default-ON posture), folds the AskUserQuestion decision for it into the SAME `milestone.complete` invocation (avoiding a redundant second call), and states the known bucket-all provenance limit.",
"discuss-phase-assumptions.md": "#3409: replaced the unreachable `AUTO_MODE=$(gsd_run query check auto-mode --pick active 2>/dev/null || echo \"false\")` \u2014 `||` never fires because the query exits 0 with empty stdout when the field is absent, not a failure, so AUTO_MODE silently ended up empty rather than \"false\" \u2014 with a two-line capture-then-default (`AUTO_MODE=\"${AUTO_MODE:-false}\"`) that actually reaches the fallback. Growth is the extra default-assignment line (+19 bytes).",
"plan-phase.md": "#3409: three sites. `AUTO_CHAIN` and `PHASE_REQ_IDS` get the same unreachable-`||`-fallback fix as discuss-phase-assumptions.md (empty-but-successful `gsd_run query` output never triggered `|| echo`, now uses `${VAR:-default}`); `PRIOR_SUMMARIES` additionally swapped `gsd_run query phases.list --pick summaries_total` for `--type summaries --pick count` since the old pick key produced the same unreachable-fallback failure mode for the walking-skeleton check. Net growth across the three sites is +39 bytes. \u2014 #3576 append: bare `references/<name>.md` cites repaired to the canonical `gsd-core/references/<name>.md` form (+36 bytes, 4 cite(s) \u00d7 9). Dead-pointer fix; no content change. #3559: the generic gate-dispatch arm gained the in-context validation contract for manifest-supplied check values. gates[].check is not one of the four executable surfaces the install consent prompt discloses, so a capability consented to as declarative-only could still reach a shell through an unvalidated check.query interpolated into a command substitution. The reference (references/loop-hook-dispatch.md) stated this requirement for step -> ref.command and omitted it for gate; that omission is the root cause and is now closed at the reference plus all four dispatch sites. Growth is one validation paragraph per site. 90516 -> 90627 bytes (+111). \u2014 #2401 append (merged into this fragment because two ack sources may never name the same path): plan-phase.md now dispatches the deterministic `gsd_run check verify-command-paths` probe before spawning the plan-check pass and interpolates its result into the verification prompt as {VERIFY_PATHS} inside a new <verify_command_path_probe> block, plus a <proven_verify_commands> block carrying prior_verify_commands into planning context so the planner can reuse a proven path instead of re-deriving one. 90627 -> 92807 bytes (+2180). Deliberate runtime-loaded workflow text for the new feature, not converter drift. \u2014 #3606 append: plan:pre gains generic contribution dispatch (deferral covering every `into` target, not just planner); plan:post gains generic step + contribution dispatch and its skip condition no longer keys on the gap-analysis gate\u2019s absence (that skip silently dropped every other registered hook at the point).",
"session-report.md": "#3409: guarded `ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo \"No previous reports\"` \u2014 with nullglob active, zero prior reports collapses the pattern to nothing and `ls -la` with no operands lists the current directory (a successful exit, wrong output) instead of failing into the `|| echo` fallback, so the report-existence check silently printed a directory listing. Replaced with an array-existence check that only lists when a real report file is present. Growth is the guard idiom (+58 bytes).",
"transition.md": "#3409: guarded `cat .planning/phases/XX-current/*-SUMMARY.md` \u2014 same nullglob-hang defect as complete-milestone.md's phase-summary read: zero summaries left a bare `cat` blocking on stdin instead of proceeding with no summary content during PROJECT.md evolution. Growth is the array-existence-check idiom (+73 bytes)."
}
}

View File

@@ -0,0 +1,8 @@
{
"version": 1,
"paths": {
"quick.md": {
"reason": "#3606: the execute:post consumer gains generic step dispatch (deferral to loop-hook-dispatch.md) before the code-review specialization, so refactor-trigger's ref.command step and any other registered execute:post steps actually run on /gsd:quick runs instead of being filtered out by the one-skill narrow. Deliberate growth, not converter drift."
}
}
}

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -275,6 +275,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -308,6 +308,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -274,6 +274,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -274,6 +274,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -309,6 +309,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -241,6 +241,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -273,6 +273,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -344,6 +344,7 @@
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",
"gsd-core/workflows/execute-phase/steps/regression-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",