diff --git a/.agents/skills/gstack-autoplan/agents/openai.yaml b/.agents/skills/gstack-autoplan/agents/openai.yaml
new file mode 100644
index 00000000..28794c1a
--- /dev/null
+++ b/.agents/skills/gstack-autoplan/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-autoplan"
+ short_description: "Auto-review pipeline — reads the full CEO, design, and eng review skills from disk and runs them sequentially with..."
+ default_prompt: "Use gstack-autoplan for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-benchmark/agents/openai.yaml b/.agents/skills/gstack-benchmark/agents/openai.yaml
new file mode 100644
index 00000000..4df54f31
--- /dev/null
+++ b/.agents/skills/gstack-benchmark/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-benchmark"
+ short_description: "Performance regression detection using the browse daemon. Establishes baselines for page load times, Core Web..."
+ default_prompt: "Use gstack-benchmark for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-browse/agents/openai.yaml b/.agents/skills/gstack-browse/agents/openai.yaml
new file mode 100644
index 00000000..851f8083
--- /dev/null
+++ b/.agents/skills/gstack-browse/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-browse"
+ short_description: "Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with elements, verify page..."
+ default_prompt: "Use gstack-browse for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-canary/agents/openai.yaml b/.agents/skills/gstack-canary/agents/openai.yaml
new file mode 100644
index 00000000..e51e4231
--- /dev/null
+++ b/.agents/skills/gstack-canary/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-canary"
+ short_description: "Post-deploy canary monitoring. Watches the live app for console errors, performance regressions, and page failures..."
+ default_prompt: "Use gstack-canary for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-careful/agents/openai.yaml b/.agents/skills/gstack-careful/agents/openai.yaml
new file mode 100644
index 00000000..f470fcaa
--- /dev/null
+++ b/.agents/skills/gstack-careful/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-careful"
+ short_description: "Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE, force-push, git reset --hard, kubectl..."
+ default_prompt: "Use gstack-careful for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-cso/agents/openai.yaml b/.agents/skills/gstack-cso/agents/openai.yaml
new file mode 100644
index 00000000..d9224815
--- /dev/null
+++ b/.agents/skills/gstack-cso/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-cso"
+ short_description: "Chief Security Officer mode. Performs OWASP Top 10 audit, STRIDE threat modeling, attack surface analysis, auth flow..."
+ default_prompt: "Use gstack-cso for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-design-consultation/agents/openai.yaml b/.agents/skills/gstack-design-consultation/agents/openai.yaml
new file mode 100644
index 00000000..3af30a8a
--- /dev/null
+++ b/.agents/skills/gstack-design-consultation/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-design-consultation"
+ short_description: "Design consultation: understands your product, researches the landscape, proposes a complete design system..."
+ default_prompt: "Use gstack-design-consultation for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-design-review/agents/openai.yaml b/.agents/skills/gstack-design-review/agents/openai.yaml
new file mode 100644
index 00000000..473554d3
--- /dev/null
+++ b/.agents/skills/gstack-design-review/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-design-review"
+ short_description: "Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems, AI slop patterns, and slow..."
+ default_prompt: "Use gstack-design-review for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-document-release/agents/openai.yaml b/.agents/skills/gstack-document-release/agents/openai.yaml
new file mode 100644
index 00000000..453bf5bd
--- /dev/null
+++ b/.agents/skills/gstack-document-release/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-document-release"
+ short_description: "Post-ship documentation update. Reads all project docs, cross-references the diff, updates..."
+ default_prompt: "Use gstack-document-release for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-freeze/agents/openai.yaml b/.agents/skills/gstack-freeze/agents/openai.yaml
new file mode 100644
index 00000000..0b643f68
--- /dev/null
+++ b/.agents/skills/gstack-freeze/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-freeze"
+ short_description: "Restrict file edits to a specific directory for the session. Blocks Edit and Write outside the allowed path. Use..."
+ default_prompt: "Use gstack-freeze for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-guard/agents/openai.yaml b/.agents/skills/gstack-guard/agents/openai.yaml
new file mode 100644
index 00000000..c7fe7902
--- /dev/null
+++ b/.agents/skills/gstack-guard/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-guard"
+ short_description: "Full safety mode: destructive command warnings + directory-scoped edits. Combines /careful (warns before rm -rf,..."
+ default_prompt: "Use gstack-guard for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-investigate/agents/openai.yaml b/.agents/skills/gstack-investigate/agents/openai.yaml
new file mode 100644
index 00000000..3c778414
--- /dev/null
+++ b/.agents/skills/gstack-investigate/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-investigate"
+ short_description: "Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron..."
+ default_prompt: "Use gstack-investigate for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-land-and-deploy/agents/openai.yaml b/.agents/skills/gstack-land-and-deploy/agents/openai.yaml
new file mode 100644
index 00000000..73a9d706
--- /dev/null
+++ b/.agents/skills/gstack-land-and-deploy/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-land-and-deploy"
+ short_description: "Land and deploy workflow. Merges the PR, waits for CI and deploy, verifies production health via canary checks...."
+ default_prompt: "Use gstack-land-and-deploy for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-office-hours/agents/openai.yaml b/.agents/skills/gstack-office-hours/agents/openai.yaml
new file mode 100644
index 00000000..51ac282d
--- /dev/null
+++ b/.agents/skills/gstack-office-hours/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-office-hours"
+ short_description: "YC Office Hours — two modes. Startup mode: six forcing questions that expose demand reality, status quo, desperate..."
+ default_prompt: "Use gstack-office-hours for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-plan-ceo-review/agents/openai.yaml b/.agents/skills/gstack-plan-ceo-review/agents/openai.yaml
new file mode 100644
index 00000000..6927e353
--- /dev/null
+++ b/.agents/skills/gstack-plan-ceo-review/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-plan-ceo-review"
+ short_description: "CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when..."
+ default_prompt: "Use gstack-plan-ceo-review for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-plan-design-review/agents/openai.yaml b/.agents/skills/gstack-plan-design-review/agents/openai.yaml
new file mode 100644
index 00000000..d3948212
--- /dev/null
+++ b/.agents/skills/gstack-plan-design-review/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-plan-design-review"
+ short_description: "Designer's eye plan review — interactive, like CEO and Eng review. Rates each design dimension 0-10, explains what..."
+ default_prompt: "Use gstack-plan-design-review for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-plan-eng-review/agents/openai.yaml b/.agents/skills/gstack-plan-eng-review/agents/openai.yaml
new file mode 100644
index 00000000..96eefa75
--- /dev/null
+++ b/.agents/skills/gstack-plan-eng-review/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-plan-eng-review"
+ short_description: "Eng manager-mode plan review. Lock in the execution plan — architecture, data flow, diagrams, edge cases, test..."
+ default_prompt: "Use gstack-plan-eng-review for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-qa-only/agents/openai.yaml b/.agents/skills/gstack-qa-only/agents/openai.yaml
new file mode 100644
index 00000000..afbd1ee3
--- /dev/null
+++ b/.agents/skills/gstack-qa-only/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-qa-only"
+ short_description: "Report-only QA testing. Systematically tests a web application and produces a structured report with health score,..."
+ default_prompt: "Use gstack-qa-only for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-qa/agents/openai.yaml b/.agents/skills/gstack-qa/agents/openai.yaml
new file mode 100644
index 00000000..6d940241
--- /dev/null
+++ b/.agents/skills/gstack-qa/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-qa"
+ short_description: "Systematically QA test a web application and fix bugs found. Runs QA testing, then iteratively fixes bugs in source..."
+ default_prompt: "Use gstack-qa for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-retro/agents/openai.yaml b/.agents/skills/gstack-retro/agents/openai.yaml
new file mode 100644
index 00000000..dbf45f2d
--- /dev/null
+++ b/.agents/skills/gstack-retro/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-retro"
+ short_description: "Weekly engineering retrospective. Analyzes commit history, work patterns, and code quality metrics with persistent..."
+ default_prompt: "Use gstack-retro for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-review/agents/openai.yaml b/.agents/skills/gstack-review/agents/openai.yaml
new file mode 100644
index 00000000..ba44751c
--- /dev/null
+++ b/.agents/skills/gstack-review/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-review"
+ short_description: "Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust boundary violations,..."
+ default_prompt: "Use gstack-review for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-setup-browser-cookies/agents/openai.yaml b/.agents/skills/gstack-setup-browser-cookies/agents/openai.yaml
new file mode 100644
index 00000000..5cab5186
--- /dev/null
+++ b/.agents/skills/gstack-setup-browser-cookies/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-setup-browser-cookies"
+ short_description: "Import cookies from your real browser (Comet, Chrome, Arc, Brave, Edge) into the headless browse session. Opens an..."
+ default_prompt: "Use gstack-setup-browser-cookies for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-setup-deploy/agents/openai.yaml b/.agents/skills/gstack-setup-deploy/agents/openai.yaml
new file mode 100644
index 00000000..b666712e
--- /dev/null
+++ b/.agents/skills/gstack-setup-deploy/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-setup-deploy"
+ short_description: "Configure deployment settings for /land-and-deploy. Detects your deploy platform (Fly.io, Render, Vercel, Netlify,..."
+ default_prompt: "Use gstack-setup-deploy for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-ship/agents/openai.yaml b/.agents/skills/gstack-ship/agents/openai.yaml
new file mode 100644
index 00000000..537ab155
--- /dev/null
+++ b/.agents/skills/gstack-ship/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-ship"
+ short_description: "Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push,..."
+ default_prompt: "Use gstack-ship for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-unfreeze/agents/openai.yaml b/.agents/skills/gstack-unfreeze/agents/openai.yaml
new file mode 100644
index 00000000..93de8da6
--- /dev/null
+++ b/.agents/skills/gstack-unfreeze/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-unfreeze"
+ short_description: "Clear the freeze boundary set by /freeze, allowing edits to all directories again. Use when you want to widen edit..."
+ default_prompt: "Use gstack-unfreeze for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack-upgrade/agents/openai.yaml b/.agents/skills/gstack-upgrade/agents/openai.yaml
new file mode 100644
index 00000000..ca055a01
--- /dev/null
+++ b/.agents/skills/gstack-upgrade/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack-upgrade"
+ short_description: "Upgrade gstack to the latest version. Detects global vs vendored install, runs the upgrade, and shows what's new...."
+ default_prompt: "Use gstack-upgrade for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/.agents/skills/gstack/agents/openai.yaml b/.agents/skills/gstack/agents/openai.yaml
new file mode 100644
index 00000000..ace5b243
--- /dev/null
+++ b/.agents/skills/gstack/agents/openai.yaml
@@ -0,0 +1,6 @@
+interface:
+ display_name: "gstack"
+ short_description: "Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with elements, verify page..."
+ default_prompt: "Use gstack for this task."
+policy:
+ allow_implicit_invocation: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89d7bf18..b7105cdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## [0.11.7.0] - 2026-03-23 — zsh Compatibility Fix
+## [0.11.8.0] - 2026-03-23 — zsh Compatibility Fix
### Fixed
@@ -10,6 +10,18 @@
- **Regression test for zsh glob safety.** New test verifies all generated SKILL.md files use `find` instead of bare shell globs for `.pending-*` pattern matching.
+## [0.11.7.0] - 2026-03-23 — /review → /ship Handoff Fix
+
+### Fixed
+
+- **`/review` now satisfies the ship readiness gate.** Previously, running `/review` before `/ship` always showed "NOT CLEARED" because `/review` didn't log its result and `/ship` only looked for `/plan-eng-review`. Now `/review` persists its outcome to the review log, and all dashboards recognize both `/review` (diff-scoped) and `/plan-eng-review` (plan-stage) as valid Eng Review sources.
+- **Ship abort prompt now mentions both review options.** When Eng Review is missing, `/ship` suggests "run `/review` or `/plan-eng-review`" instead of only mentioning `/plan-eng-review`.
+
+### For contributors
+
+- Based on PR #338 by @malikrohail. DRY improvement per eng review: updated the shared `REVIEW_DASHBOARD` resolver instead of creating a duplicate ship-only resolver.
+- 4 new validation tests covering review-log persistence, dashboard propagation, and abort text.
+
## [0.11.6.0] - 2026-03-23 — Infrastructure-First Security Audit
### Added
diff --git a/VERSION b/VERSION
index a9da9632..f3b6bd46 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.11.7.0
+0.11.8.0
diff --git a/agents/openai.yaml b/agents/openai.yaml
new file mode 100644
index 00000000..1bb2fd7c
--- /dev/null
+++ b/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "gstack"
+ short_description: "Bundle of gstack Codex skills"
+ default_prompt: "Use $gstack to locate the bundled gstack skills."
diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md
index f7d5c129..a6365fca 100644
--- a/plan-ceo-review/SKILL.md
+++ b/plan-ceo-review/SKILL.md
@@ -1258,7 +1258,7 @@ After completing the review, read the review log and config to display the dashb
~/.claude/skills/gstack/bin/gstack-review-read
```
-Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
+Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between `review` (diff-scoped pre-landing review) and `plan-eng-review` (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
```
+====================================================================+
@@ -1284,7 +1284,7 @@ Parse the output. Find the most recent entry for each skill (plan-ceo-review, pl
- **Outside Voice (optional):** Independent plan review from a different AI model. Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Falls back to Claude subagent if Codex is unavailable. Never gates shipping.
**Verdict logic:**
-- **CLEARED**: Eng Review has >= 1 entry within 7 days with status "clean" (or \`skip_eng_review\` is \`true\`)
+- **CLEARED**: Eng Review has >= 1 entry within 7 days from either \`review\` or \`plan-eng-review\` with status "clean" (or \`skip_eng_review\` is \`true\`)
- **NOT CLEARED**: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If \`skip_eng_review\` config is \`true\`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md
index 9e89bd15..e8d9fbbe 100644
--- a/plan-design-review/SKILL.md
+++ b/plan-design-review/SKILL.md
@@ -764,7 +764,7 @@ After completing the review, read the review log and config to display the dashb
~/.claude/skills/gstack/bin/gstack-review-read
```
-Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
+Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between `review` (diff-scoped pre-landing review) and `plan-eng-review` (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
```
+====================================================================+
@@ -790,7 +790,7 @@ Parse the output. Find the most recent entry for each skill (plan-ceo-review, pl
- **Outside Voice (optional):** Independent plan review from a different AI model. Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Falls back to Claude subagent if Codex is unavailable. Never gates shipping.
**Verdict logic:**
-- **CLEARED**: Eng Review has >= 1 entry within 7 days with status "clean" (or \`skip_eng_review\` is \`true\`)
+- **CLEARED**: Eng Review has >= 1 entry within 7 days from either \`review\` or \`plan-eng-review\` with status "clean" (or \`skip_eng_review\` is \`true\`)
- **NOT CLEARED**: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If \`skip_eng_review\` config is \`true\`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md
index d51a94c8..54d68fcc 100644
--- a/plan-eng-review/SKILL.md
+++ b/plan-eng-review/SKILL.md
@@ -879,7 +879,7 @@ After completing the review, read the review log and config to display the dashb
~/.claude/skills/gstack/bin/gstack-review-read
```
-Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
+Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between `review` (diff-scoped pre-landing review) and `plan-eng-review` (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
```
+====================================================================+
@@ -905,7 +905,7 @@ Parse the output. Find the most recent entry for each skill (plan-ceo-review, pl
- **Outside Voice (optional):** Independent plan review from a different AI model. Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Falls back to Claude subagent if Codex is unavailable. Never gates shipping.
**Verdict logic:**
-- **CLEARED**: Eng Review has >= 1 entry within 7 days with status "clean" (or \`skip_eng_review\` is \`true\`)
+- **CLEARED**: Eng Review has >= 1 entry within 7 days from either \`review\` or \`plan-eng-review\` with status "clean" (or \`skip_eng_review\` is \`true\`)
- **NOT CLEARED**: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If \`skip_eng_review\` config is \`true\`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
diff --git a/review/SKILL.md b/review/SKILL.md
index 74e141c1..c96f5ca5 100644
--- a/review/SKILL.md
+++ b/review/SKILL.md
@@ -899,6 +899,27 @@ High-confidence findings (agreed on by multiple sources) should be prioritized f
---
+## Step 5.8: Persist Eng Review result
+
+After all review passes complete, persist the final `/review` outcome so `/ship` can
+recognize that Eng Review was run on this branch.
+
+Run:
+
+```bash
+~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"commit":"COMMIT"}'
+```
+
+Substitute:
+- `TIMESTAMP` = ISO 8601 datetime
+- `STATUS` = `"clean"` if there are no remaining unresolved findings after Fix-First handling and adversarial review, otherwise `"issues_found"`
+- `issues_found` = total remaining unresolved findings
+- `critical` = remaining unresolved critical findings
+- `informational` = remaining unresolved informational findings
+- `COMMIT` = output of `git rev-parse --short HEAD`
+
+If the review exits early before a real review completes (for example, no diff against the base branch), do **not** write this entry.
+
## Important Rules
- **Read the FULL diff before commenting.** Do not flag issues already addressed in the diff.
diff --git a/review/SKILL.md.tmpl b/review/SKILL.md.tmpl
index a33b0fa8..8ae9045a 100644
--- a/review/SKILL.md.tmpl
+++ b/review/SKILL.md.tmpl
@@ -250,6 +250,27 @@ If no documentation files exist, skip this step silently.
{{ADVERSARIAL_STEP}}
+## Step 5.8: Persist Eng Review result
+
+After all review passes complete, persist the final `/review` outcome so `/ship` can
+recognize that Eng Review was run on this branch.
+
+Run:
+
+```bash
+~/.claude/skills/gstack/bin/gstack-review-log '{"skill":"review","timestamp":"TIMESTAMP","status":"STATUS","issues_found":N,"critical":N,"informational":N,"commit":"COMMIT"}'
+```
+
+Substitute:
+- `TIMESTAMP` = ISO 8601 datetime
+- `STATUS` = `"clean"` if there are no remaining unresolved findings after Fix-First handling and adversarial review, otherwise `"issues_found"`
+- `issues_found` = total remaining unresolved findings
+- `critical` = remaining unresolved critical findings
+- `informational` = remaining unresolved informational findings
+- `COMMIT` = output of `git rev-parse --short HEAD`
+
+If the review exits early before a real review completes (for example, no diff against the base branch), do **not** write this entry.
+
## Important Rules
- **Read the FULL diff before commenting.** Do not flag issues already addressed in the diff.
diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts
index 1a177421..06e78099 100644
--- a/scripts/gen-skill-docs.ts
+++ b/scripts/gen-skill-docs.ts
@@ -20,6 +20,7 @@ const DRY_RUN = process.argv.includes('--dry-run');
// ─── Template Context ───────────────────────────────────────
type Host = 'claude' | 'codex';
+const OPENAI_SHORT_DESCRIPTION_LIMIT = 120;
const HOST_ARG = process.argv.find(a => a.startsWith('--host'));
const HOST: Host = (() => {
@@ -1312,7 +1313,7 @@ After completing the review, read the review log and config to display the dashb
~/.claude/skills/gstack/bin/gstack-review-read
\`\`\`
-Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Adversarial row, show whichever is more recent between \`adversarial-review\` (new auto-scaled) and \`codex-review\` (legacy). For Design Review, show whichever is more recent between \`plan-design-review\` (full visual audit) and \`design-review-lite\` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
+Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between \`review\` (diff-scoped pre-landing review) and \`plan-eng-review\` (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between \`adversarial-review\` (new auto-scaled) and \`codex-review\` (legacy). For Design Review, show whichever is more recent between \`plan-design-review\` (full visual audit) and \`design-review-lite\` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
\`\`\`
+====================================================================+
@@ -1338,7 +1339,7 @@ Parse the output. Find the most recent entry for each skill (plan-ceo-review, pl
- **Outside Voice (optional):** Independent plan review from a different AI model. Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Falls back to Claude subagent if Codex is unavailable. Never gates shipping.
**Verdict logic:**
-- **CLEARED**: Eng Review has >= 1 entry within 7 days with status "clean" (or \\\`skip_eng_review\\\` is \\\`true\\\`)
+- **CLEARED**: Eng Review has >= 1 entry within 7 days from either \\\`review\\\` or \\\`plan-eng-review\\\` with status "clean" (or \\\`skip_eng_review\\\` is \\\`true\\\`)
- **NOT CLEARED**: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If \\\`skip_eng_review\\\` config is \\\`true\\\`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
@@ -2835,6 +2836,65 @@ function codexSkillName(skillDir: string): string {
return `gstack-${skillDir}`;
}
+function extractNameAndDescription(content: string): { name: string; description: string } {
+ const fmStart = content.indexOf('---\n');
+ if (fmStart !== 0) return { name: '', description: '' };
+ const fmEnd = content.indexOf('\n---', fmStart + 4);
+ if (fmEnd === -1) return { name: '', description: '' };
+
+ const frontmatter = content.slice(fmStart + 4, fmEnd);
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
+ const name = nameMatch ? nameMatch[1].trim() : '';
+
+ let description = '';
+ const lines = frontmatter.split('\n');
+ let inDescription = false;
+ const descLines: string[] = [];
+ for (const line of lines) {
+ if (line.match(/^description:\s*\|?\s*$/)) {
+ inDescription = true;
+ continue;
+ }
+ if (line.match(/^description:\s*\S/)) {
+ description = line.replace(/^description:\s*/, '').trim();
+ break;
+ }
+ if (inDescription) {
+ if (line === '' || line.match(/^\s/)) {
+ descLines.push(line.replace(/^ /, ''));
+ } else {
+ break;
+ }
+ }
+ }
+ if (descLines.length > 0) {
+ description = descLines.join('\n').trim();
+ }
+
+ return { name, description };
+}
+
+function condenseOpenAIShortDescription(description: string): string {
+ const firstParagraph = description.split(/\n\s*\n/)[0] || description;
+ const collapsed = firstParagraph.replace(/\s+/g, ' ').trim();
+ if (collapsed.length <= OPENAI_SHORT_DESCRIPTION_LIMIT) return collapsed;
+
+ const truncated = collapsed.slice(0, OPENAI_SHORT_DESCRIPTION_LIMIT - 3);
+ const lastSpace = truncated.lastIndexOf(' ');
+ const safe = lastSpace > 40 ? truncated.slice(0, lastSpace) : truncated;
+ return `${safe}...`;
+}
+
+function generateOpenAIYaml(displayName: string, shortDescription: string): string {
+ return `interface:
+ display_name: ${JSON.stringify(displayName)}
+ short_description: ${JSON.stringify(shortDescription)}
+ default_prompt: ${JSON.stringify(`Use ${displayName} for this task.`)}
+policy:
+ allow_implicit_invocation: true
+`;
+}
+
/**
* Transform frontmatter for Codex: keep only name + description.
* Strips allowed-tools, hooks, version, and all other fields.
@@ -2843,48 +2903,12 @@ function codexSkillName(skillDir: string): string {
function transformFrontmatter(content: string, host: Host): string {
if (host === 'claude') return content;
- // Find frontmatter boundaries
const fmStart = content.indexOf('---\n');
- if (fmStart !== 0) return content; // frontmatter must be at the start
+ if (fmStart !== 0) return content;
const fmEnd = content.indexOf('\n---', fmStart + 4);
if (fmEnd === -1) return content;
-
- const frontmatter = content.slice(fmStart + 4, fmEnd);
const body = content.slice(fmEnd + 4); // includes the leading \n after ---
-
- // Parse name
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
- const name = nameMatch ? nameMatch[1].trim() : '';
-
- // Parse description — handle both simple and block scalar (|) formats
- let description = '';
- const lines = frontmatter.split('\n');
- let inDescription = false;
- const descLines: string[] = [];
- for (const line of lines) {
- if (line.match(/^description:\s*\|?\s*$/)) {
- // Block scalar start: "description: |" or "description:"
- inDescription = true;
- continue;
- }
- if (line.match(/^description:\s*\S/)) {
- // Simple inline: "description: some text"
- description = line.replace(/^description:\s*/, '').trim();
- break;
- }
- if (inDescription) {
- // Block scalar continuation — indented lines (2 spaces) or blank lines
- if (line === '' || line.match(/^\s/)) {
- descLines.push(line.replace(/^ /, ''));
- } else {
- // End of block scalar — hit a non-indented, non-blank line
- break;
- }
- }
- }
- if (descLines.length > 0) {
- description = descLines.join('\n').trim();
- }
+ const { name, description } = extractNameAndDescription(content);
// Re-emit Codex frontmatter (name + description only)
const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n');
@@ -2931,6 +2955,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
const tmplContent = fs.readFileSync(tmplPath, 'utf-8');
const relTmplPath = path.relative(ROOT, tmplPath);
let outputPath = tmplPath.replace(/\.tmpl$/, '');
+ let outputDir: string | null = null;
// Determine skill directory relative to ROOT
const skillDir = path.relative(ROOT, path.dirname(tmplPath));
@@ -2938,14 +2963,14 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
// For codex host, route output to .agents/skills/{codexSkillName}/SKILL.md
if (host === 'codex') {
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
- const outputDir = path.join(ROOT, '.agents', 'skills', codexName);
+ outputDir = path.join(ROOT, '.agents', 'skills', codexName);
fs.mkdirSync(outputDir, { recursive: true });
outputPath = path.join(outputDir, 'SKILL.md');
}
// Extract skill name from frontmatter for TemplateContext
- const nameMatch = tmplContent.match(/^name:\s*(.+)$/m);
- const skillName = nameMatch ? nameMatch[1].trim() : path.basename(path.dirname(tmplPath));
+ const { name: extractedName, description: extractedDescription } = extractNameAndDescription(tmplContent);
+ const skillName = extractedName || path.basename(path.dirname(tmplPath));
// Extract benefits-from list from frontmatter (inline YAML: benefits-from: [a, b])
const benefitsMatch = tmplContent.match(/^benefits-from:\s*\[([^\]]*)\]/m);
@@ -2987,6 +3012,15 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
content = content.replace(/\.claude\/skills\/gstack/g, ctx.paths.localSkillRoot);
content = content.replace(/\.claude\/skills\/review/g, '.agents/skills/gstack/review');
content = content.replace(/\.claude\/skills/g, '.agents/skills');
+
+ if (outputDir) {
+ const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
+ const agentsDir = path.join(outputDir, 'agents');
+ fs.mkdirSync(agentsDir, { recursive: true });
+ const displayName = codexName;
+ const shortDescription = condenseOpenAIShortDescription(extractedDescription);
+ fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), generateOpenAIYaml(displayName, shortDescription));
+ }
}
// Prepend generated header (after frontmatter)
diff --git a/ship/SKILL.md b/ship/SKILL.md
index 8c875985..0d984f09 100644
--- a/ship/SKILL.md
+++ b/ship/SKILL.md
@@ -357,7 +357,7 @@ After completing the review, read the review log and config to display the dashb
~/.claude/skills/gstack/bin/gstack-review-read
```
-Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
+Parse the output. Find the most recent entry for each skill (plan-ceo-review, plan-eng-review, review, plan-design-review, design-review-lite, adversarial-review, codex-review, codex-plan-review). Ignore entries with timestamps older than 7 days. For the Eng Review row, show whichever is more recent between `review` (diff-scoped pre-landing review) and `plan-eng-review` (plan-stage architecture review). Append "(DIFF)" or "(PLAN)" to the status to distinguish. For the Adversarial row, show whichever is more recent between `adversarial-review` (new auto-scaled) and `codex-review` (legacy). For Design Review, show whichever is more recent between `plan-design-review` (full visual audit) and `design-review-lite` (code-level check). Append "(FULL)" or "(LITE)" to the status to distinguish. Display:
```
+====================================================================+
@@ -383,7 +383,7 @@ Parse the output. Find the most recent entry for each skill (plan-ceo-review, pl
- **Outside Voice (optional):** Independent plan review from a different AI model. Offered after all review sections complete in /plan-ceo-review and /plan-eng-review. Falls back to Claude subagent if Codex is unavailable. Never gates shipping.
**Verdict logic:**
-- **CLEARED**: Eng Review has >= 1 entry within 7 days with status "clean" (or \`skip_eng_review\` is \`true\`)
+- **CLEARED**: Eng Review has >= 1 entry within 7 days from either \`review\` or \`plan-eng-review\` with status "clean" (or \`skip_eng_review\` is \`true\`)
- **NOT CLEARED**: Eng Review missing, stale (>7 days), or has open issues
- CEO, Design, and Codex reviews are shown for context but never block shipping
- If \`skip_eng_review\` config is \`true\`, Eng Review shows "SKIPPED (global)" and verdict is CLEARED
@@ -406,7 +406,7 @@ If the Eng Review is NOT "CLEAR":
2. **If no override exists,** use AskUserQuestion:
- Show that Eng Review is missing or has open issues
- RECOMMENDATION: Choose C if the change is obviously trivial (< 20 lines, typo fix, config-only); Choose B for larger changes
- - Options: A) Ship anyway B) Abort — run /plan-eng-review first C) Change is too small to need eng review
+ - Options: A) Ship anyway B) Abort — run /review or /plan-eng-review first C) Change is too small to need eng review
- If CEO Review is missing, mention as informational ("CEO Review not run — recommended for product changes") but do NOT block
- For Design Review: run `source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.
diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl
index 0308dd8b..e7709a33 100644
--- a/ship/SKILL.md.tmpl
+++ b/ship/SKILL.md.tmpl
@@ -70,7 +70,7 @@ If the Eng Review is NOT "CLEAR":
2. **If no override exists,** use AskUserQuestion:
- Show that Eng Review is missing or has open issues
- RECOMMENDATION: Choose C if the change is obviously trivial (< 20 lines, typo fix, config-only); Choose B for larger changes
- - Options: A) Ship anyway B) Abort — run /plan-eng-review first C) Change is too small to need eng review
+ - Options: A) Ship anyway B) Abort — run /review or /plan-eng-review first C) Change is too small to need eng review
- If CEO Review is missing, mention as informational ("CEO Review not run — recommended for product changes") but do NOT block
- For Design Review: run `source <(~/.claude/skills/gstack/bin/gstack-diff-scope 2>/dev/null)`. If `SCOPE_FRONTEND=true` and no design review (plan-design-review or design-review-lite) exists in the dashboard, mention: "Design Review not run — this PR changes frontend code. The lite design check will run automatically in Step 3.5, but consider running /design-review for a full visual audit post-implementation." Still never block.
diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts
index 992d78d8..0e179c1e 100644
--- a/test/gen-skill-docs.test.ts
+++ b/test/gen-skill-docs.test.ts
@@ -410,6 +410,20 @@ describe('REVIEW_DASHBOARD resolver', () => {
expect(content).toContain('REVIEW READINESS DASHBOARD');
});
+ test('dashboard treats review as a valid Eng Review source', () => {
+ const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
+ expect(content).toContain('plan-eng-review, review, plan-design-review');
+ expect(content).toContain('`review` (diff-scoped pre-landing review)');
+ expect(content).toContain('`plan-eng-review` (plan-stage architecture review)');
+ expect(content).toContain('from either \\`review\\` or \\`plan-eng-review\\`');
+ });
+
+ test('shared dashboard propagates review source to plan-eng-review', () => {
+ const content = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
+ expect(content).toContain('plan-eng-review, review, plan-design-review');
+ expect(content).toContain('`review` (diff-scoped pre-landing review)');
+ });
+
test('resolver output contains key dashboard elements', () => {
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
expect(content).toContain('VERDICT');
@@ -939,6 +953,14 @@ describe('Codex generation (--host codex)', () => {
}
});
+ test('root gstack bundle has OpenAI metadata for Codex skill browsing', () => {
+ const rootMetadata = path.join(ROOT, 'agents', 'openai.yaml');
+ expect(fs.existsSync(rootMetadata)).toBe(true);
+ const content = fs.readFileSync(rootMetadata, 'utf-8');
+ expect(content).toContain('display_name: "gstack"');
+ expect(content).toContain('Use $gstack to locate the bundled gstack skills.');
+ });
+
test('codexSkillName mapping: root is gstack, others are gstack-{dir}', () => {
// Root → gstack
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack', 'SKILL.md'))).toBe(true);
@@ -968,6 +990,17 @@ describe('Codex generation (--host codex)', () => {
}
});
+ test('all Codex skills have agents/openai.yaml metadata', () => {
+ for (const skill of CODEX_SKILLS) {
+ const metadata = path.join(AGENTS_DIR, skill.codexName, 'agents', 'openai.yaml');
+ expect(fs.existsSync(metadata)).toBe(true);
+ const content = fs.readFileSync(metadata, 'utf-8');
+ expect(content).toContain(`display_name: "${skill.codexName}"`);
+ expect(content).toContain('short_description:');
+ expect(content).toContain('allow_implicit_invocation: true');
+ }
+ });
+
test('no .claude/skills/ in Codex output', () => {
for (const skill of CODEX_SKILLS) {
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
diff --git a/test/helpers/codex-session-runner.ts b/test/helpers/codex-session-runner.ts
index 77b45020..ac2b9e29 100644
--- a/test/helpers/codex-session-runner.ts
+++ b/test/helpers/codex-session-runner.ts
@@ -98,7 +98,8 @@ export function parseCodexJSONL(lines: string[]): ParsedCodexJSONL {
/**
* Install a SKILL.md into a temp HOME directory for Codex to discover.
- * Creates ~/.codex/skills/{skillName}/SKILL.md in the temp HOME.
+ * Creates ~/.codex/skills/{skillName}/SKILL.md in the temp HOME and copies
+ * agents/openai.yaml when present so Codex sees the same metadata as a real install.
*
* Returns the temp HOME path. Caller is responsible for cleanup.
*/
@@ -116,6 +117,13 @@ export function installSkillToTempHome(
fs.copyFileSync(srcSkill, path.join(destDir, 'SKILL.md'));
}
+ const srcOpenAIYaml = path.join(skillDir, 'agents', 'openai.yaml');
+ if (fs.existsSync(srcOpenAIYaml)) {
+ const destAgentsDir = path.join(destDir, 'agents');
+ fs.mkdirSync(destAgentsDir, { recursive: true });
+ fs.copyFileSync(srcOpenAIYaml, path.join(destAgentsDir, 'openai.yaml'));
+ }
+
return home;
}
diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts
index 3d76ae67..c4bc99af 100644
--- a/test/skill-validation.test.ts
+++ b/test/skill-validation.test.ts
@@ -1369,6 +1369,18 @@ describe('Codex skill', () => {
expect(content).toContain('codex exec');
});
+ test('/review persists a review-log entry for ship readiness', () => {
+ const content = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
+ expect(content).toContain('"skill":"review"');
+ expect(content).toContain('"issues_found":N');
+ expect(content).toContain('Persist Eng Review result');
+ });
+
+ test('/ship gate suggests /review or /plan-eng-review when Eng Review is missing', () => {
+ const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
+ expect(content).toContain('Abort — run /review or /plan-eng-review first');
+ });
+
test('Review Readiness Dashboard includes Adversarial Review row', () => {
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
expect(content).toContain('Adversarial');