forgedock 1.1.11 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/bin/cli-spawn-shared.mjs +25 -0
  3. package/bin/engine-cli.mjs +22 -1
  4. package/bin/engine.mjs +46 -4
  5. package/bin/forgedock.mjs +87 -16
  6. package/bin/hooks/pre-tool-use.mjs +331 -10
  7. package/bin/init-enrich-api.mjs +12 -4
  8. package/bin/init-enrich-cli.mjs +328 -0
  9. package/bin/init-enrich.mjs +137 -0
  10. package/bin/journey.mjs +13 -4
  11. package/bin/runner.mjs +653 -10
  12. package/commands/adopt.md +1 -1
  13. package/commands/analytics.md +10 -7
  14. package/commands/audit-agents.md +29 -6
  15. package/commands/audit.md +9 -20
  16. package/commands/autopilot.md +52 -78
  17. package/commands/changelog.md +1 -1
  18. package/commands/ci-audit.md +19 -10
  19. package/commands/cleanup.md +1 -1
  20. package/commands/compat-audit.md +1 -1
  21. package/commands/deploy-info.md +1 -1
  22. package/commands/deploy-pr.md +1 -1
  23. package/commands/design-bench.md +1 -1
  24. package/commands/design-render-critique-loop.md +1 -1
  25. package/commands/diagnose.md +1 -1
  26. package/commands/forge-stats.md +1 -1
  27. package/commands/forgedock-init.md +1 -1
  28. package/commands/geo-audit.md +9 -6
  29. package/commands/incident-response.md +15 -5
  30. package/commands/issue.md +298 -15
  31. package/commands/milestone.md +45 -18
  32. package/commands/optimize.md +1 -1
  33. package/commands/orchestrate/phase-1-resolve.md +20 -5
  34. package/commands/orchestrate/phase-4-execution.md +6 -1
  35. package/commands/orchestrate/phase-5-cleanup.md +43 -2
  36. package/commands/orchestrate/phase-6-report.md +1 -0
  37. package/commands/orchestrate.md +1 -1
  38. package/commands/pipeline-health/phase-4-proposals.md +40 -10
  39. package/commands/pipeline-health/phase-5-report.md +140 -13
  40. package/commands/pipeline-health.md +1 -1
  41. package/commands/pipeline-resume.md +1 -1
  42. package/commands/pipeline-status.md +1 -1
  43. package/commands/qa-sweep.md +10 -4
  44. package/commands/recover-orphans.md +1 -1
  45. package/commands/review-pr-agents/protocols.md +4 -0
  46. package/commands/review-pr-agents.md +7 -4
  47. package/commands/review-pr-staging.md +44 -15
  48. package/commands/review-pr.md +73 -28
  49. package/commands/rollback.md +15 -5
  50. package/commands/security-audit.md +14 -11
  51. package/commands/signal-planner.md +32 -12
  52. package/commands/spec-doctor.md +1 -1
  53. package/commands/sync-ecosystem.md +54 -11
  54. package/commands/test-gate.md +49 -11
  55. package/commands/validate.md +16 -5
  56. package/commands/work-on/build/architect.md +1 -1
  57. package/commands/work-on/build/context.md +1 -1
  58. package/commands/work-on/build/implement.md +2 -2
  59. package/commands/work-on/build/validate.md +3 -1
  60. package/commands/work-on/build.md +1 -1
  61. package/commands/work-on/close.md +1 -1
  62. package/commands/work-on/decompose.md +27 -24
  63. package/commands/work-on/investigate.md +2 -2
  64. package/commands/work-on/remediate.md +1 -1
  65. package/commands/work-on/review.md +5 -3
  66. package/commands/work-on-monolithic.md +1 -1
  67. package/commands/work-on.md +28 -7
  68. package/docs/CI.md +18 -0
  69. package/docs/CONFIG.md +5 -2
  70. package/docs/site/annotations-explained.md +2 -2
  71. package/docs/site/command-reference.md +21 -0
  72. package/docs/spec/review-protocol.md +6 -0
  73. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Context-aware PR review — analyzes what the PR touches, spawns domain-specific agents with project conventions. Supports staging reviews.
3
- argument-hint: [PR number, URL, "open", or "staging" for feature→main review]
4
- allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
3
+ argument-hint: "[PR number, URL, \"open\", or \"staging\" for feature→main review]"
4
+ allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch, Skill
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
7
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -11,7 +11,7 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
11
11
  **Input**: $ARGUMENTS
12
12
 
13
13
  **NEVER use plan mode (EnterPlanMode)** during review — it breaks execution context.
14
- **NEVER use the Agent tool** — review-pr dispatches domain agents via `Task` tool only. `Agent` spawns opaque subprocesses that bypass the allowed-tools constraint and cannot post structured findings to the PR. Always use `Task(...)` for review agent launch (Phase 3C).
14
+ **Sub-agent dispatch tool: `Task` preferred, `Agent` is the documented fallback.** review-pr dispatches domain review agents via a sub-agent-spawning tool. Resolve which one is available ONCE per invocation, before Phase 3C, per the **Sub-Agent Dispatch Tool Resolution** rule below — do not halt to ask the operator which tool to use. Never fall back to reviewing inline in the orchestrator's own context; inline self-review is a strictly weaker substitute for an isolated fresh-context reviewer and is not a permitted fallback.
15
15
 
16
16
  **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier); the General Security & Quality reviewer spawned as always-runs Task uses `effort: xhigh` (deep tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`. Pass model and effort explicitly in every `Task` tool call. Feature gate: pass `effort` only on Claude Code >= 2.1.154. **The domain-review Task agents this file dispatches** (per `commands/review-pr-agents/*.md`) resolve via `model: "{SUBAGENT_MODEL}"` — forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"` — not `{DEFAULT_MODEL}` directly.
17
17
 
@@ -19,7 +19,7 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
19
19
 
20
20
  ## HARD RULES — READ BEFORE ANYTHING ELSE
21
21
 
22
- 1. **Use `Task(...)` for ALL domain agent launches.** Never substitute `Agent(...)`. Task agents run in a constrained context, post findings to the PR via `gh pr comment`, and their output is structured. Agent spawns opaque subprocesses outside allowed-tools.
22
+ 1. **Use the resolved `{DISPATCH_TOOL}(...)` for ALL domain agent launches.** Resolve `{DISPATCH_TOOL}` once, per the **Sub-Agent Dispatch Tool Resolution** section below: `Task` when available, `Agent` as the documented fallback when `Task` is absent from the environment. Every dispatch call in this run uses the same resolved tool — do not mix. Whichever tool is used, each agent MUST run in an isolated, fresh-context sub-agent (not inline in the orchestrator's own context) and post findings to the PR via `gh pr comment` in structured form.
23
23
 
24
24
  2. **Post the FORGE:REVIEW verdict regardless of finding severity.** A review that completes but posts no `<!-- FORGE:REVIEW -->` comment is invisible to the pipeline. Even a PASS verdict must be posted.
25
25
 
@@ -29,16 +29,29 @@ allowed-tools: Task, Bash, Read, Grep, Glob, WebFetch, Skill
29
29
 
30
30
  5. **`spec-evolution` PRs are NEVER auto-merged.** When a PR carries the `spec-evolution` label (created by `/spec-doctor`), Phase -1 MUST set `AUTO_MERGE=false` and add `needs-human` before any other processing. This cannot be overridden by the caller — the eval gate plus human review are the only permitted merge path. See Phase -1 `spec-evolution guard` block. <!-- Added: forge#1742 -->
31
31
 
32
+ ## Sub-Agent Dispatch Tool Resolution (MANDATORY — run once, before Phase 3C)
33
+
34
+ This spec dispatches domain review agents via a sub-agent-spawning tool. Different runtimes expose different tools for this — resolve deterministically, once per invocation, and use the same tool for every dispatch call in this run:
35
+
36
+ 1. **If `Task` is available in the current environment**: set `{DISPATCH_TOOL} = Task`. This is the preferred tool — tightest `allowed-tools` scoping.
37
+ 2. **Else if `Agent` is available**: set `{DISPATCH_TOOL} = Agent`. This is the documented fallback, not a degraded path — use it exactly as you would `Task`: one call per selected domain agent, same prompt template, `subagent_type: "general-purpose"` (or the closest equivalent the environment offers), same requirement that each agent posts its own findings directly to the PR via `gh pr comment`. Isolation and fresh-context review are preserved either way.
38
+ 3. **Neither tool is available**: this is a genuine setup defect, not a routing decision — HARD STOP, post a PR/issue comment explaining that no sub-agent dispatch tool is available, add `needs-human`, and exit without posting a verdict. Do NOT fall back to reviewing inline in the orchestrator's own context — inline self-review is strictly weaker than an isolated fresh-context reviewer and is never a substitute for a missing dispatch tool.
39
+
40
+ **Do not halt to ask the operator which tool to use.** Steps 1–2 are deterministic and fully resolve the common case; only step 3 (both absent) requires a stop, and even then the action is HARD STOP + `needs-human`, not a question back to the operator.
41
+
42
+ Everywhere this file (and `review-pr-agents.md` / the `review-pr-agents/*.md` persona files) says `Task(...)`, read it as `{DISPATCH_TOOL}(...)` using the value resolved here.
43
+
32
44
  ## Forbidden Tools Self-Check
33
45
 
34
- **Before executing any phase**, verify you are NOT using any of these tools:
46
+ **Before executing any phase**, verify you are NOT using any of these:
35
47
 
36
- | Tool | Status | Reason |
48
+ | Tool/Pattern | Status | Reason |
37
49
  |------|--------|--------|
38
- | `Agent` | **FORBIDDEN** | Spawns opaque subprocesses outside allowed-tools; bypasses spec workflow; cannot post structured findings |
50
+ | `Agent`, when `Task` is available | **FORBIDDEN** | `Task` is preferred whenever present — `Agent` is only the fallback for when `Task` is absent, not a free substitute |
51
+ | Inline self-review (no sub-agent spawn at all) | **FORBIDDEN** | Bypasses isolated fresh-context review entirely — always spawn via the resolved `{DISPATCH_TOOL}`, never review directly in the orchestrator's own context |
39
52
  | `EnterPlanMode` | **FORBIDDEN** | Breaks execution context; must run phases, not plan them |
40
53
 
41
- If you find yourself about to call `Agent(...)`, stop and use `Task(...)` instead. If you find yourself about to use `EnterPlanMode`, stop and execute the next phase directly.
54
+ If you find yourself about to call `Agent(...)` while `Task` is available, stop and use `Task(...)` instead. If neither `Task` nor `Agent` is available, do not fall through to inline review — follow step 3 of Sub-Agent Dispatch Tool Resolution above. If you find yourself about to use `EnterPlanMode`, stop and execute the next phase directly.
42
55
 
43
56
  ## Architecture — How This Command Works
44
57
 
@@ -48,11 +61,11 @@ This is the **orchestrator**. It routes to the right review mode, runs automated
48
61
 
49
62
  | File | What | How to invoke |
50
63
  |------|------|---------------|
51
- | `$FORGE_HOME/commands/review-pr-agents/protocols.md` | Shared review protocols (Evidence-Based + Structured Findings + Input Scoping) | `Read` tool during Phase 3C (always) |
52
- | `$FORGE_HOME/commands/review-pr-agents/<persona>.md` | Per-persona agent prompt templates (9 files) | `Read` tool during Phase 3C (selected agents only) |
53
- | `$FORGE_HOME/commands/review-pr-staging.md` | Full staging→main review pipeline | `Skill("review-pr-staging", ...)` during Phase 0 |
64
+ | `${FORGE_HOME:-$REPO_PATH}/commands/review-pr-agents/protocols.md` | Shared review protocols (Evidence-Based + Structured Findings + Input Scoping) | `Read` tool during Phase 3C (always) |
65
+ | `${FORGE_HOME:-$REPO_PATH}/commands/review-pr-agents/<persona>.md` | Per-persona agent prompt templates (9 files) | `Read` tool during Phase 3C (selected agents only) |
66
+ | `${FORGE_HOME:-$REPO_PATH}/commands/review-pr-staging.md` | Full staging→main review pipeline | `Skill("review-pr-staging", ...)` during Phase 0 |
54
67
 
55
- `$FORGE_HOME` defaults to `~/.claude` (the directory where `npx forgedock` symlinks commands). Override by setting `FORGE_HOME` in your environment.
68
+ `$FORGE_HOME` defaults to `~/.claude` (the directory where `npx forgedock` symlinks commands). When unset, every resolution in this file falls back to `$REPO_PATH` (the repo root, from `forge.yaml → paths.root`) rather than degrading to a bare root-anchored path — see the `TEMPLATE_BASE` tiered guard in Phase 3C and the verification-script resolution in Step 2.5B for the actual fallback chains. Never resolve a missing file via a filesystem-wide `find` — see the guardrail in `commands/review-pr-agents/protocols.md`.
56
69
 
57
70
  **Invocation flow:**
58
71
  ```
@@ -580,7 +593,7 @@ Map each changed file to its activation requirements.
580
593
 
581
594
  ### Step 2.5B: Run Verification
582
595
 
583
- For each changed file, execute the relevant checks using the standalone verification scripts in `$FORGE_HOME/scripts/`. These scripts can also be run independently outside the review context (e.g., from `/quality-gate` or `/work-on` builder steps).
596
+ For each changed file, execute the relevant checks using the standalone verification scripts in `${FORGE_HOME:-$SCRIPTS_HOME}/scripts/`. These scripts can also be run independently outside the review context (e.g., from `/quality-gate` or `/work-on` builder steps).
584
597
 
585
598
  **Platform note**: The verify-*.sh scripts require bash and standard POSIX tools. On Windows without bash (Git Bash / WSL / MSYS2), these checks are skipped with an explicit message — the review continues without them.
586
599
 
@@ -588,6 +601,20 @@ For each changed file, execute the relevant checks using the standalone verifica
588
601
  CHANGED_FILES=$(gh pr diff $ARGUMENTS --name-only)
589
602
  REPO_ROOT="." # Assumes cwd is the repo root
590
603
 
604
+ # Resolve the verify-*.sh scripts source directory with the same deterministic
605
+ # fallback as the Phase 3C TEMPLATE_BASE guard: $FORGE_HOME first (the installed
606
+ # ForgeDock location), then this repo's own root (forge.yaml -> paths.root, or
607
+ # git top-level). A bare/unset $FORGE_HOME must never be used directly in a path —
608
+ # that degrades to a root-anchored path (/scripts/verify-*.sh) and silently skips
609
+ # every check below. Never fall back to a filesystem-wide `find`.
610
+ # <!-- Added: forge#2035 -->
611
+ if [ -n "$FORGE_HOME" ] && [ -f "$FORGE_HOME/scripts/verify-route-registration.sh" ]; then
612
+ SCRIPTS_HOME="$FORGE_HOME"
613
+ else
614
+ FORGE_YAML="${FORGE_CONFIG:-$(git rev-parse --show-toplevel 2>/dev/null)/forge.yaml}"
615
+ SCRIPTS_HOME=$(yq '.paths.root' "$FORGE_YAML" 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null || pwd)
616
+ fi
617
+
591
618
  # --- Platform / bash capability guard ---
592
619
  # The verify-*.sh scripts require bash. Detect availability before invoking.
593
620
  # On Windows without Git Bash/WSL, skip gracefully rather than crash.
@@ -627,11 +654,11 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
627
654
  [ -n "$_API_MIDDLEWARE" ] && export FORGE_API_MIDDLEWARE_DIR="$_API_MIDDLEWARE"
628
655
  fi
629
656
  echo "=== Running: verify-route-registration.sh ==="
630
- bash "$FORGE_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
657
+ bash "$SCRIPTS_HOME/scripts/verify-route-registration.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
631
658
 
632
659
  # 2. Environment variable wiring (checks .env.example, docker-compose, env_validation, SOPS mapping)
633
660
  echo "=== Running: verify-env-vars.sh ==="
634
- bash "$FORGE_HOME/scripts/verify-env-vars.sh" "$DIFF_TMP" "$REPO_ROOT" || true
661
+ bash "$SCRIPTS_HOME/scripts/verify-env-vars.sh" "$DIFF_TMP" "$REPO_ROOT" || true
635
662
 
636
663
  # 3. Host headers in shell scripts + client-side proxy bypass check
637
664
  # Read project-specific internal service patterns from forge.yaml (if present)
@@ -645,11 +672,11 @@ if [ "$BASH_AVAILABLE" = "true" ]; then
645
672
  fi
646
673
  export FORGE_INTERNAL_PATTERNS
647
674
  echo "=== Running: verify-host-headers.sh ==="
648
- bash "$FORGE_HOME/scripts/verify-host-headers.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
675
+ bash "$SCRIPTS_HOME/scripts/verify-host-headers.sh" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
649
676
 
650
677
  # 4. SOPS deploy chain (ENV_MAPPING consistency, deploy path drift, hotfix sync)
651
678
  echo "=== Running: verify-sops-chain.sh ==="
652
- bash "$FORGE_HOME/scripts/verify-sops-chain.sh" "$DIFF_TMP" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
679
+ bash "$SCRIPTS_HOME/scripts/verify-sops-chain.sh" "$DIFF_TMP" "$CHANGED_FILES_TMP" "$REPO_ROOT" || true
653
680
 
654
681
  # Cleanup temp files
655
682
  rm -f "$CHANGED_FILES_TMP" "$DIFF_TMP"
@@ -1485,9 +1512,9 @@ The `protocols.md` file contains the Evidence-Based Review Protocol, Structured
1485
1512
  6. Substitute code index slice: `[INDEX_SLICE]` → the matching `$INDEX_SLICE_{DOMAIN}` variable for this agent (e.g., `$INDEX_SLICE_AUTH` for the auth agent). Agents MUST query index data first; fall back to grep only when index slice is empty or unavailable.
1486
1513
  7. Substitute per-agent diff slice: `[DOMAIN_DIFF_SLICE]` → the matching `$DIFF_SLICE_*` variable (e.g., `$DIFF_SLICE_AUTH` for the auth agent, `$DIFF_SLICE_SECURITY` for the security agent). This replaces any `gh pr diff [PR_NUMBER]` call inside the agent template — the agent works from the pre-computed slice, not the full changeset.
1487
1514
  8. If Phase 2.5 found broken assumptions, append them to the agent's prompt as "Pre-found integration issues to verify"
1488
- 9. Launch via `Task` tool with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited)
1515
+ 9. Launch via the resolved `{DISPATCH_TOOL}` (see Sub-Agent Dispatch Tool Resolution above) with `model: "{SUBAGENT_MODEL}"` (forge.yaml `agents.subagent_model`, else `agents.default_model`, else `"sonnet"`; fallback `"opus"` if rate-limited)
1489
1516
 
1490
- **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple Task tool calls. Each agent posts findings directly to the PR via `gh pr comment`.
1517
+ **CRITICAL**: Launch ALL selected agents in a SINGLE message using multiple `{DISPATCH_TOOL}` calls. Each agent posts findings directly to the PR via `gh pr comment`.
1491
1518
 
1492
1519
  #### Domain Diff Slicing
1493
1520
 
@@ -1629,7 +1656,7 @@ if [ "$DEDUP_EXIT" -eq 1 ]; then
1629
1656
  # Add a comment on the existing issue referencing this recurrence in PR #${PR_NUMBER}
1630
1657
  elif [ "$DEDUP_EXIT" -eq 2 ]; then
1631
1658
  echo "DEDUP: Usage error — $DEDUP_RESULT"
1632
- # Skip this finding — do NOT fall through to gh issue create on a usage error
1659
+ # Skip this finding — do NOT fall through to issue creation on a usage error
1633
1660
  fi
1634
1661
  ```
1635
1662
 
@@ -1688,13 +1715,17 @@ fi
1688
1715
  - Open `review-finding` issue at same file with similar title (3+ shared keywords) → **skip** (likely same finding despite line drift)
1689
1716
  - Closed `review-finding` at same file within ±5 lines → create with regression warning, elevate to `priority:P1`
1690
1717
 
1691
- **For each finding** (that passes dedup), create issue:
1718
+ **For each finding** (that passes dedup), create issue through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") — this preserves the bespoke line-range/title dedup above as a precise pre-check, while `/issue`'s own Phase 2D dedup runs as a coarser second pass:
1692
1719
  ```bash
1693
- ISSUE_NUM=$(gh issue create \
1694
- --title "fix: [summary] (review finding — PR #${PR_NUMBER})" \
1695
- --label "review-finding,needs-validation,{priority}" \
1696
- ${MILESTONE_FLAG} \
1697
- --body "$(cat <<'ISSUE_EOF'
1720
+ FINDING_ISSUE_TITLE="fix: [summary] (review finding — PR #${PR_NUMBER})"
1721
+ # Defense-in-depth: /issue's arg tokenizer (commands/issue.md Argument Parsing,
1722
+ # forge#2094) uses an xargs-based tokenizer that never expands backtick/$(...)
1723
+ # substitution, so this is no longer required for safety — but strip it anyway
1724
+ # so the raw title stays readable if it round-trips through any other
1725
+ # eval-based consumer.
1726
+ FINDING_ISSUE_TITLE=$(printf '%s' "$FINDING_ISSUE_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
1727
+ FINDING_ISSUE_BODY_FILE=$(mktemp)
1728
+ cat <<'ISSUE_EOF' > "$FINDING_ISSUE_BODY_FILE"
1698
1729
  ## Problem
1699
1730
 
1700
1731
  [One sentence: what bug or issue was found. Where it occurs (`file:line`) and what it causes.]
@@ -1745,7 +1776,21 @@ Files that need changes:
1745
1776
  - [ ] Reproduce or construct proof-of-concept
1746
1777
  [BATCHABLE_ANNOTATION]
1747
1778
  ISSUE_EOF
1748
- )" --json number --jq '.number')
1779
+
1780
+ # --label is repeatable (not comma-joined) per the /issue programmatic contract.
1781
+ Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"{priority}\" ${MILESTONE_FLAG}")
1782
+ rm -f "$FINDING_ISSUE_BODY_FILE"
1783
+
1784
+ # /issue has no machine-readable return contract (it's a user-facing command, not a work-on
1785
+ # subcommand) — resolve the created issue's number by exact-title search immediately after
1786
+ # the call. The title embeds ${PR_NUMBER} and the finding summary, making it unique enough
1787
+ # for a reliable single-match lookup. Retry to absorb GitHub Search API indexing lag.
1788
+ ISSUE_NUM=""
1789
+ for _resolve_attempt in 1 2 3; do
1790
+ ISSUE_NUM=$(gh issue list -R ${REPO} --search "in:title \"${FINDING_ISSUE_TITLE}\"" --state open --limit 1 --json number --jq '.[0].number // empty')
1791
+ [ -n "$ISSUE_NUM" ] && break
1792
+ sleep 2
1793
+ done
1749
1794
  ```
1750
1795
 
1751
1796
  Labels: `review-finding` + `needs-validation` + priority (`priority:P1` CONFIRMED, `priority:P2` LIKELY, `priority:P3` POSSIBLE).
@@ -1888,7 +1933,7 @@ ATTRIBUTION_FOOTER_LINE=""
1888
1933
  if [ "$ATTRIBUTION_PR_FOOTER" = "true" ]; then
1889
1934
  ATTRIBUTION_FOOTER_LINE="
1890
1935
  ---
1891
- > Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
1936
+ > ⚒️ Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub."
1892
1937
  fi
1893
1938
 
1894
1939
  # Build trust annotation line for verdict body — always included so the decision is auditable.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Create a revert PR to roll back a shipped feature or fix that caused a production incident
3
- argument-hint: [PR number to revert, or "last" for most recent deploy]
3
+ argument-hint: "[PR number to revert, or \"last\" for most recent deploy]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -213,9 +213,10 @@ fi
213
213
  ### Step 4B: Create a follow-up issue
214
214
 
215
215
  ```bash
216
- gh issue create --title "fix: {original_title} (reverted — needs new approach)" \
217
- --label "bug,priority:P1" \
218
- --body "$(cat <<'BODY_EOF'
216
+ FOLLOWUP_TITLE="fix: {original_title} (reverted — needs new approach)"
217
+ FOLLOWUP_BODY_TMPFILE=$(mktemp)
218
+ trap 'rm -f "$FOLLOWUP_BODY_TMPFILE"' EXIT
219
+ cat > "$FOLLOWUP_BODY_TMPFILE" <<'BODY_EOF'
219
220
  ## Problem
220
221
 
221
222
  PR #{PR_NUMBER} ({original_title}) was merged and deployed but caused a production incident. It was reverted in PR #{REVERT_PR_NUMBER}. The original fix needs to be re-implemented with a different approach that avoids the production issue.
@@ -249,7 +250,16 @@ Files that need changes (same as original fix, different approach):
249
250
 
250
251
  Regression of #{ORIGINAL_ISSUE}. Should investigate why the original approach caused issues before implementing.
251
252
  BODY_EOF
252
- )"
253
+ # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
254
+ # `gh issue create`.
255
+ Skill(skill="issue", args="--title \"$FOLLOWUP_TITLE\" --body-file \"$FOLLOWUP_BODY_TMPFILE\" --label bug --label \"priority:P1\"")
256
+ rm -f "$FOLLOWUP_BODY_TMPFILE"
257
+ trap - EXIT
258
+ FOLLOWUP_ISSUE_NUMBER=$(gh issue list \
259
+ --state open \
260
+ --search "$FOLLOWUP_TITLE" \
261
+ --json number,title \
262
+ --jq --arg t "$FOLLOWUP_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
253
263
  ```
254
264
 
255
265
  ### Step 4C: Clean up worktree
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Periodic security posture audit — runs a scripted 4-phase checklist against repo files (not diffs), creates GitHub issues for confirmed findings
3
- argument-hint: [--repo <prefix>] [--phase 1|2|3|4|all] [--dry-run]
3
+ argument-hint: "[--repo <prefix>] [--phase 1|2|3|4|all] [--dry-run]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -515,20 +515,18 @@ gh label create "priority:P3" --color "C2E0C6" --description "Low priority — m
515
515
 
516
516
  ### 5C: Create GitHub issues for CONFIRMED findings
517
517
 
518
- For each CONFIRMED finding (not REVIEW-only), create an issue:
518
+ For each CONFIRMED finding (not REVIEW-only), create an issue — routed through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` → Programmatic Invocation Contract) so dedup and mandatory-section validation run on every finding:
519
519
 
520
520
  ```bash
521
- gh issue create -R "{GH_REPO}" \
522
- --title "{ISSUE_TITLE}" \
523
- --label "security,audit-finding,{SEVERITY}" \
524
- --body "$(cat <<'ISSUE_EOF'
521
+ SECURITY_BODY_FILE=$(mktemp)
522
+ cat > "$SECURITY_BODY_FILE" <<'ISSUE_EOF'
525
523
  ## Problem
526
524
 
527
525
  {2-3 sentences describing the finding, its location, and exploitation path.}
528
526
 
529
527
  ## Evidence
530
528
 
531
- **File**: \`{FILE}\`
529
+ **File**: `{FILE}`
532
530
  **Line**: {LINE}
533
531
  **Finding**: {GREP_OUTPUT}
534
532
 
@@ -538,23 +536,28 @@ gh issue create -R "{GH_REPO}" \
538
536
 
539
537
  ## Affected Files
540
538
 
541
- 1. \`{FILE}\` — {what needs to change}
539
+ 1. `{FILE}` — {what needs to change}
542
540
 
543
541
  ## Acceptance Criteria
544
542
 
545
543
  - [ ] {Specific remediation step}
546
- - [ ] Verified by re-running \`/security-audit\` after fix
544
+ - [ ] Verified by re-running `/security-audit` after fix
547
545
 
548
546
  ## Context
549
547
 
550
548
  **Audited at**: {TIMESTAMP}
551
549
  **Audit phase**: {PHASE}
552
550
  **Severity**: {SEVERITY}
553
- **Source**: \`/security-audit\` run on \`{GH_REPO}\`
551
+ **Source**: `/security-audit` run on `{GH_REPO}`
554
552
  ISSUE_EOF
555
- )"
556
553
  ```
557
554
 
555
+ ```
556
+ Skill(skill="issue", args="--title \"{ISSUE_TITLE}\" --body-file \"$SECURITY_BODY_FILE\" --label \"security\" --label \"audit-finding\" --label \"{SEVERITY}\"")
557
+ ```
558
+
559
+ `security`, `audit-finding`, and `{SEVERITY}` are each passed as their own `--label` flag — `/issue`'s programmatic mode does not comma-split a single `--label` value. Note: `/issue` resolves the target repo from `forge.yaml → project.owner/repo` (or the satellite-prefix table), not from a caller-supplied `-R`/`--repo` flag — this is correct for the common case where `{GH_REPO}` matches `forge.yaml`'s project repo.
560
+
558
561
  ### 5D: Post audit summary comment (if invoked from a GitHub issue context)
559
562
 
560
563
  If this command was invoked from within a `/work-on` context or with an issue reference:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Closed-loop autonomy — convert a production/analytics/incident signal into a dependency-ordered issue DAG, execute via /orchestrate, then verify the originating signal is resolved. Event-driven companion to /autopilot.
3
- argument-hint: [--signal '<json>' | --metric '<name> <threshold>' | --incident <issue#> | --geo | --dry-run | --max-issues N]
3
+ argument-hint: "[--signal '<json>' | --metric '<name> <threshold>' | --incident <issue#> | --geo | --dry-run | --max-issues N]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -224,10 +224,10 @@ Detect cycles — if a cycle exists, break it by demoting the lower-priority uni
224
224
  Post the DAG to a tracker issue on GitHub. First, create the tracker issue:
225
225
 
226
226
  ```bash
227
- TRACKER_ISSUE=$(gh issue create -R {GH_REPO} \
228
- --title "signal-planner: DAG tracker for {SIGNAL.name}" \
229
- --label "signal-planner,priority:{SIGNAL.severity}" \
230
- --body "$(cat <<'TRACKER_EOF'
227
+ TRACKER_TITLE="signal-planner: DAG tracker for {SIGNAL.name}"
228
+ TRACKER_BODY_TMPFILE=$(mktemp)
229
+ trap 'rm -f "$TRACKER_BODY_TMPFILE"' EXIT
230
+ cat > "$TRACKER_BODY_TMPFILE" <<'TRACKER_EOF'
231
231
  ## Signal
232
232
 
233
233
  **Type**: {SIGNAL.type}
@@ -247,7 +247,18 @@ This tracker was created by \`/signal-planner\`. The dependency DAG below repres
247
247
  - [ ] All DAG issues merged
248
248
  - [ ] Originating signal verified resolved (FORGE:SIGNAL_RESOLVED posted)
249
249
  TRACKER_EOF
250
- )" --json number --jq '.number')
250
+ # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
251
+ # `gh issue create`. This body uses "## Acceptance" rather than the mandatory "## Acceptance
252
+ # Criteria"/"## Affected Files"/"## Problem" sections — /issue's Phase 3F will non-blockingly
253
+ # append placeholder mandatory sections; this is expected for a coordination tracker issue.
254
+ Skill(skill="issue", args="--title \"$TRACKER_TITLE\" --body-file \"$TRACKER_BODY_TMPFILE\" --label signal-planner --label \"priority:{SIGNAL.severity}\"")
255
+ rm -f "$TRACKER_BODY_TMPFILE"
256
+ trap - EXIT
257
+ TRACKER_ISSUE=$(gh issue list -R {GH_REPO} \
258
+ --state open \
259
+ --search "$TRACKER_TITLE" \
260
+ --json number,title \
261
+ --jq --arg t "$TRACKER_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
251
262
  ```
252
263
 
253
264
  Then post the `FORGE:PLAN_DAG` comment:
@@ -312,10 +323,10 @@ If governor would be breached, **hard fail** — do not create any issues, do no
312
323
  For each work unit with `existing == null`, create a GitHub issue:
313
324
 
314
325
  ```bash
315
- ISSUE_NUM=$(gh issue create -R {GH_REPO} \
316
- --title "{work_unit.title}" \
317
- --label "priority:{work_unit.priority},{work_unit.type}" \
318
- --body "$(cat <<'ISSUE_EOF'
326
+ WORK_UNIT_TITLE="{work_unit.title}"
327
+ WORK_UNIT_BODY_TMPFILE=$(mktemp)
328
+ trap 'rm -f "$WORK_UNIT_BODY_TMPFILE"' EXIT
329
+ cat > "$WORK_UNIT_BODY_TMPFILE" <<'ISSUE_EOF'
319
330
  ## Problem
320
331
 
321
332
  {work_unit.rationale — why this is needed to resolve the originating signal}
@@ -341,10 +352,19 @@ Signal context: {relevant subset of SIGNAL.context for this work unit}
341
352
  ---
342
353
  *Created by \`/signal-planner\` on $(date -u +%Y-%m-%dT%H:%M:%SZ). Will be investigated before any fix is applied.*
343
354
  ISSUE_EOF
344
- )" --json number --jq '.number')
355
+ # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
356
+ # `gh issue create`.
357
+ Skill(skill="issue", args="--title \"$WORK_UNIT_TITLE\" --body-file \"$WORK_UNIT_BODY_TMPFILE\" --label \"priority:{work_unit.priority}\" --label \"{work_unit.type}\"")
358
+ rm -f "$WORK_UNIT_BODY_TMPFILE"
359
+ trap - EXIT
360
+ ISSUE_NUM=$(gh issue list -R {GH_REPO} \
361
+ --state open \
362
+ --search "$WORK_UNIT_TITLE" \
363
+ --json number,title \
364
+ --jq --arg t "$WORK_UNIT_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
345
365
  ```
346
366
 
347
- Record each created issue number and update the DAG table in the tracker issue.
367
+ Record each created issue number and update the DAG table in the tracker issue. If `/issue` deduped this work unit against an existing open issue, `ISSUE_NUM` will come back empty — fall back to `work_unit.existing` (if already resolved during Phase 2) or treat the work unit as already covered and skip re-creation.
348
368
 
349
369
  ### 3C: Compile final DAG issue list
350
370
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Evidence-backed spec evolution — reads finding concentration for a spec file, drafts a spec diff, opens a spec-evolution PR with per-change citations
3
- argument-hint: [spec file path | "auto"]
3
+ argument-hint: "[spec file path | \"auto\"]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Detect API changes, sync satellite repos, and publish releases
3
- argument-hint: [check | auto | status | publish | PR-number]
3
+ argument-hint: "[check | auto | status | publish | PR-number]"
4
4
  install: extras
5
5
  ---
6
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -218,10 +218,16 @@ Create issues in the affected repos:
218
218
 
219
219
  ```bash
220
220
  # For satellite repos (separate GitHub repos):
221
- gh issue create -R {TARGET_REPO} \
222
- --title "feat(sync): {description of what API change needs to be reflected}" \
223
- --label "feature,priority:P2" \
224
- --body "$(cat <<'BODY_EOF'
221
+ #
222
+ # /issue resolves its target repo in Phase 1A from a `{prefix}:` token at the start of the
223
+ # free-text input, not from an explicit -R override — its multi-repo prefix table is keyed
224
+ # off forge.yaml → repos.satellites (the same registry this file already iterates). Prepend
225
+ # the satellite's configured {COMPONENT} (= satellites[].prefix) to the title so /issue routes
226
+ # to {TARGET_REPO} correctly instead of the default project repo.
227
+ SYNC_TITLE="feat(sync): {description of what API change needs to be reflected}"
228
+ SYNC_BODY_TMPFILE=$(mktemp)
229
+ trap 'rm -f "$SYNC_BODY_TMPFILE"' EXIT
230
+ cat > "$SYNC_BODY_TMPFILE" <<'BODY_EOF'
225
231
  ## Problem
226
232
 
227
233
  {1-3 sentences: what API or schema change in the upstream repo needs to be reflected in this satellite repo. What will break or be missing if this sync is not done.}
@@ -252,13 +258,41 @@ Files that need changes:
252
258
 
253
259
  {If applicable: describe the new API shape with an example}
254
260
  BODY_EOF
255
- )"
261
+ # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
262
+ # `gh issue create`. Prefixing the title with "{COMPONENT}: " lets /issue's Phase 1A resolve
263
+ # the satellite repo ({TARGET_REPO}) from forge.yaml → repos.satellites, matching how a human
264
+ # would invoke /issue for the same satellite (e.g. "mcp: add list_schemas tool"). The prefix
265
+ # is stripped by /issue's own routing — it does not become part of the created issue title.
266
+ Skill(skill="issue", args="\"{COMPONENT}: $SYNC_TITLE\" --title \"$SYNC_TITLE\" --body-file \"$SYNC_BODY_TMPFILE\" --label feature --label \"priority:P2\"")
267
+ rm -f "$SYNC_BODY_TMPFILE"
268
+ trap - EXIT
269
+ SYNC_ISSUE_NUMBER=$(gh issue list -R {TARGET_REPO} \
270
+ --state open \
271
+ --search "$SYNC_TITLE" \
272
+ --json number,title \
273
+ --jq --arg t "$SYNC_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
274
+
275
+ # Safety net: the "{COMPONENT}: " prefix trick above is not a documented part of the
276
+ # Programmatic Invocation Contract (issue.md has no --repo/-R flag for programmatic mode) —
277
+ # if /issue's Phase 1A did not honor the prefix and created the issue in the default project
278
+ # repo instead of {TARGET_REPO}, the lookup above returns empty and this would otherwise be
279
+ # silently treated as "deduped by /issue" further down. Distinguish the two cases explicitly.
280
+ if [ -z "$SYNC_ISSUE_NUMBER" ]; then
281
+ MISROUTED_CHECK=$(gh issue list \
282
+ --state open \
283
+ --search "$SYNC_TITLE" \
284
+ --json number,title \
285
+ --jq --arg t "$SYNC_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
286
+ if [ -n "$MISROUTED_CHECK" ]; then
287
+ echo "WARNING: '/issue' created #$MISROUTED_CHECK in the default project repo instead of {TARGET_REPO} — the '{COMPONENT}:' prefix routing did not take effect. Move/re-file the issue in {TARGET_REPO} manually."
288
+ fi
289
+ fi
256
290
 
257
291
  # For monorepo issues (SDKs, packages inside a single repo):
258
- gh issue create \
259
- --title "feat(sync): {description of what API change needs to be reflected}" \
260
- --label "feature,priority:P2" \
261
- --body "$(cat <<'BODY_EOF'
292
+ MONOREPO_SYNC_TITLE="feat(sync): {description of what API change needs to be reflected}"
293
+ MONOREPO_SYNC_BODY_TMPFILE=$(mktemp)
294
+ trap 'rm -f "$MONOREPO_SYNC_BODY_TMPFILE"' EXIT
295
+ cat > "$MONOREPO_SYNC_BODY_TMPFILE" <<'BODY_EOF'
262
296
  ## Problem
263
297
 
264
298
  {1-3 sentences: what API or schema change needs to be reflected in this SDK or package. What will be out of sync if not updated.}
@@ -285,7 +319,16 @@ Files that need changes:
285
319
  **Change**: {description of upstream API change}
286
320
  **Detected by**: `/sync-ecosystem`
287
321
  BODY_EOF
288
- )"
322
+ # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
323
+ # `gh issue create`. No prefix needed — this targets the default/monorepo repo.
324
+ Skill(skill="issue", args="--title \"$MONOREPO_SYNC_TITLE\" --body-file \"$MONOREPO_SYNC_BODY_TMPFILE\" --label feature --label \"priority:P2\"")
325
+ rm -f "$MONOREPO_SYNC_BODY_TMPFILE"
326
+ trap - EXIT
327
+ MONOREPO_SYNC_ISSUE_NUMBER=$(gh issue list \
328
+ --state open \
329
+ --search "$MONOREPO_SYNC_TITLE" \
330
+ --json number,title \
331
+ --jq --arg t "$MONOREPO_SYNC_TITLE" '.[] | select(.title == $t) | .number' 2>/dev/null | head -1)
289
332
  ```
290
333
 
291
334
  ### Add sync issues to Project board
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Deterministic deploy-gate — verify a staging→main bundle's acceptance criteria against running code before deploy
3
- argument-hint: [--prs "<N1 N2 ...>"] [--base <branch>]
3
+ argument-hint: "[--prs \"<N1 N2 ...>\"] [--base <branch>]"
4
4
  allowed-tools: Task, Bash, Read, Grep, Glob
5
5
  install: extras
6
6
  ---
@@ -520,10 +520,16 @@ else
520
520
  SOURCE_ISSUES_LIST="${SOURCE_ISSUES_LIST}\n- #${issue_num}"
521
521
  done
522
522
 
523
- ISSUE_NUM=$(gh issue create ${GH_FLAG} \
524
- --title "fix: test-gate FAIL — cluster '${cluster}' broken by staging→${DEFAULT_BRANCH} bundle" \
525
- --label "test-failure,priority:P1" \
526
- --body "$(cat <<ISSUE_EOF
523
+ TESTGATE_FAIL_TITLE="fix: test-gate FAIL — cluster '${cluster}' broken by staging→${DEFAULT_BRANCH} bundle"
524
+ # Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
525
+ # an xargs-based tokenizer that never expands backtick/$(...) substitution, so
526
+ # this is no longer required for safety — but strip it anyway so the raw title
527
+ # stays readable if it round-trips through any other eval-based consumer.
528
+ # ${cluster} is config-controlled (forge.yaml verification.integration_tests), but sanitize
529
+ # unconditionally for defense-in-depth consistency with the other converted call sites.
530
+ TESTGATE_FAIL_TITLE=$(printf '%s' "$TESTGATE_FAIL_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
531
+ TESTGATE_FAIL_BODY_FILE=$(mktemp)
532
+ cat <<ISSUE_EOF > "$TESTGATE_FAIL_BODY_FILE"
527
533
  ## Problem
528
534
 
529
535
  The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. The failure did not exist on \`${BASE_BRANCH}\` (baseline PASS), confirming it is batch-introduced, not pre-existing.
@@ -570,7 +576,21 @@ $(echo -e "$SOURCE_ISSUES_LIST")
570
576
  $(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
571
577
  \`\`\`
572
578
  ISSUE_EOF
573
- )" --json number --jq '.number' 2>/dev/null || echo "FAILED")
579
+
580
+ # Route through the /issue create-hook's programmatic invocation contract (see
581
+ # commands/issue.md § "Programmatic Invocation Contract") instead of the raw issue-creation call.
582
+ Skill(skill="issue", args="--title \"$TESTGATE_FAIL_TITLE\" --body-file \"$TESTGATE_FAIL_BODY_FILE\" --label test-failure --label priority:P1")
583
+ rm -f "$TESTGATE_FAIL_BODY_FILE"
584
+
585
+ # /issue has no machine-readable return contract — resolve the created issue's number by
586
+ # exact-title search immediately after the call. Retry to absorb GitHub Search API indexing lag.
587
+ ISSUE_NUM=""
588
+ for _resolve_attempt in 1 2 3; do
589
+ ISSUE_NUM=$(gh issue list ${GH_FLAG} --search "in:title \"${TESTGATE_FAIL_TITLE}\"" --state open --limit 1 --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
590
+ [ -n "$ISSUE_NUM" ] && break
591
+ sleep 2
592
+ done
593
+ [ -z "$ISSUE_NUM" ] && ISSUE_NUM="FAILED"
574
594
 
575
595
  if [ "$ISSUE_NUM" != "FAILED" ] && [ -n "$ISSUE_NUM" ]; then
576
596
  echo "Filed test-failure issue #${ISSUE_NUM} for cluster '${cluster}'"
@@ -786,10 +806,14 @@ if [ "${UNCOVERED_COUNT}" -gt 0 ]; then
786
806
  continue
787
807
  fi
788
808
 
789
- GAP_ISSUE_NUM=$(gh issue create ${GH_FLAG} \
790
- --title "fix: test-gap — uncovered criterion in issue #${GAP_ISSUE} (${GAP_TYPE})" \
791
- --label "test-gap" \
792
- --body "$(cat <<TGAP_EOF
809
+ TESTGAP_TITLE="fix: test-gap — uncovered criterion in issue #${GAP_ISSUE} (${GAP_TYPE})"
810
+ # Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
811
+ # an xargs-based tokenizer that never expands backtick/$(...) substitution, so
812
+ # this is no longer required for safety — but strip it anyway so the raw title
813
+ # stays readable if it round-trips through any other eval-based consumer.
814
+ TESTGAP_TITLE=$(printf '%s' "$TESTGAP_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
815
+ TESTGAP_BODY_FILE=$(mktemp)
816
+ cat <<TGAP_EOF > "$TESTGAP_BODY_FILE"
793
817
  ## Problem
794
818
 
795
819
  The \`/test-gate\` criteria-adequacy check (Phase 6) found a runtime-testable acceptance criterion in issue #${GAP_ISSUE} that is not covered by any configured test cluster. Coverage gaps cannot produce a clean PASS verdict — this criterion must be covered before the staging→${DEFAULT_BRANCH} bundle can pass the deploy gate.
@@ -822,7 +846,21 @@ Files to be identified during investigation. Start with the test suite for the \
822
846
  ${GAP_CRITERION}
823
847
  \`\`\`
824
848
  TGAP_EOF
825
- )" --json number --jq '.number' 2>/dev/null || echo "FAILED")
849
+
850
+ # Route through the /issue create-hook's programmatic invocation contract (see
851
+ # commands/issue.md § "Programmatic Invocation Contract") instead of the raw issue-creation call.
852
+ Skill(skill="issue", args="--title \"$TESTGAP_TITLE\" --body-file \"$TESTGAP_BODY_FILE\" --label test-gap")
853
+ rm -f "$TESTGAP_BODY_FILE"
854
+
855
+ # /issue has no machine-readable return contract — resolve the created issue's number by
856
+ # exact-title search immediately after the call. Retry to absorb GitHub Search API indexing lag.
857
+ GAP_ISSUE_NUM=""
858
+ for _resolve_attempt in 1 2 3; do
859
+ GAP_ISSUE_NUM=$(gh issue list ${GH_FLAG} --search "in:title \"${TESTGAP_TITLE}\"" --state open --limit 1 --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
860
+ [ -n "$GAP_ISSUE_NUM" ] && break
861
+ sleep 2
862
+ done
863
+ [ -z "$GAP_ISSUE_NUM" ] && GAP_ISSUE_NUM="FAILED"
826
864
 
827
865
  if [ "$GAP_ISSUE_NUM" != "FAILED" ] && [ -n "$GAP_ISSUE_NUM" ]; then
828
866
  echo "Filed test-gap issue #${GAP_ISSUE_NUM} for uncovered criterion in #${GAP_ISSUE} (${GAP_TYPE})"