feat(hosts): /land-and-deploy and /modernize run only when typed on Claude Code and Cursor (C1) (#3808)

* feat(hosts): manual-only /land-and-deploy and /modernize on Claude Code and Cursor (C1, GST-804)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(routing): host-neutral router wording, update repo CLAUDE.md and the onboarding fixture for the manual-only skills

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(gen): escape conditionalFields keys, anchor values, skip keys the template already sets

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(modernize): trim always-on description to stay under the 8000-token context budget

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-04 03:33:48 +00:00
1 parent 92f22ea87e
commit 242e918bcb
17 files changed
+104 -38

No files matched your search

+1 -1
View File
@@ -554,7 +554,7 @@ Key routing rules:
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Ship/deploy/PR → invoke /ship (merge + deploy: the user types /land-and-deploy)
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
+2 -2
View File
@@ -210,8 +210,8 @@ quality gates that produce better results than answering inline.
- User asks to audit the live developer experience, time-to-hello-world → invoke `/devex-review`
- User asks to run the checks, make the tests pass, "verify my change", "check my work" while building → invoke `/verify`
- User asks to ship, deploy, push, create a PR, "let's land this", "send it" → invoke `/ship`
- User asks to merge + deploy + verify as one flow → invoke `/land-and-deploy`
- User asks to rebase or revive an old PR, "modernize this PR", whether a stale PR is still worth landing → invoke `/modernize`
- User asks to merge + deploy + verify as one flow → tell them to type `/land-and-deploy`; never merge or deploy by hand
- User asks to rebase or revive an old PR, "modernize this PR", whether a stale PR is still worth landing → tell them to type `/modernize`
- User asks to configure deployment for the project → invoke `/setup-deploy`
- User asks to monitor prod after shipping, post-deploy checks → invoke `/canary`
- User asks to update docs after shipping → invoke `/document-release`
+2 -2
View File
@@ -86,8 +86,8 @@ quality gates that produce better results than answering inline.
- User asks to audit the live developer experience, time-to-hello-world → invoke `/devex-review`
- User asks to run the checks, make the tests pass, "verify my change", "check my work" while building → invoke `/verify`
- User asks to ship, deploy, push, create a PR, "let's land this", "send it" → invoke `/ship`
- User asks to merge + deploy + verify as one flow → invoke `/land-and-deploy`
- User asks to rebase or revive an old PR, "modernize this PR", whether a stale PR is still worth landing → invoke `/modernize`
- User asks to merge + deploy + verify as one flow → tell them to type `/land-and-deploy`; never merge or deploy by hand
- User asks to rebase or revive an old PR, "modernize this PR", whether a stale PR is still worth landing → tell them to type `/modernize`
- User asks to configure deployment for the project → invoke `/setup-deploy`
- User asks to monitor prod after shipping, post-deploy checks → invoke `/canary`
- User asks to update docs after shipping → invoke `/document-release`
+1 -1
View File
@@ -706,7 +706,7 @@ Key routing rules:
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Ship/deploy/PR → invoke /ship (merge + deploy: the user types /land-and-deploy)
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec
@@ -0,0 +1 @@
Claude Code and Cursor no longer start /land-and-deploy or /modernize on their own. Type `/land-and-deploy` or `/modernize` to run them; a request in plain words like "land it" now gets a pointer to the command instead of a mistaken merge or rewrite of an old branch. Both skills already stopped for your approval before merging or pushing, so this removes surprise rather than a missing safeguard. /ship, /careful, /freeze, /guard and /unfreeze still start from plain words like "ship it". gstack's router and the CLAUDE.md routing block now tell the model to point you to these two commands instead of invoking them. Under the hood, a new `manual_only: true` template flag maps to `disable-model-invocation: true` on the Claude and Cursor hosts, and host `conditionalFields` now work in denylist mode too.
+1 -1
View File
@@ -508,7 +508,7 @@ Key fields:
| `frontmatter.mode` | `allowlist` (keep only listed) or `denylist` (strip listed) |
| `frontmatter.descriptionLimit` | Max chars, `null` for no limit |
| `frontmatter.descriptionLimitBehavior` | `error` (fail build), `truncate`, `warn` |
| `frontmatter.conditionalFields` | Add fields based on template values (e.g., sensitive → disable-model-invocation) |
| `frontmatter.conditionalFields` | Add fields based on template values, in allowlist and denylist mode. Two template flags map to `disable-model-invocation: true`: `sensitive` (broad: /ship, /land-and-deploy, /modernize and the safety skills; Copilot and Factory) and `manual_only` (narrow: /land-and-deploy and /modernize only; Claude Code and Cursor, where "ship it" must still start /ship). A host whose docs honor `disable-model-invocation` picks one |
| `frontmatter.renameFields` | Rename template fields (e.g., voice-triggers → triggers) |
| `generation.generateMetadata` | Emit `agents/openai.yaml` beside each skill (Codex only today; the format is hardcoded in `gen-skill-docs.ts`). Its `default_prompt` comes from the template's `codex_default_prompt:` frontmatter key, else `Use <name> for this task.` |
| `pathRewrites` | Literal replaceAll on content. Order matters. Replaces the derived trio. |
+1 -1
View File
@@ -47,7 +47,7 @@ Conventions:
- [/landing-report](landing-report/SKILL.md): Read-only queue dashboard for workspace-aware ship.
- [/learn](learn/SKILL.md): Manage project learnings.
- [/make-pdf](make-pdf/SKILL.md): Turn any markdown file into a publication-quality PDF.
- [/modernize](modernize/SKILL.md): Bring an aging pull request onto its current base and decide whether it should still exist: MODERNIZE, REWORK or CLOSE, with range-diff proof before any push.
- [/modernize](modernize/SKILL.md): Bring an aging PR onto its current base and decide: MODERNIZE, REWORK or CLOSE.
- [/office-hours](office-hours/SKILL.md): YC Office Hours — two modes.
- [/open-gstack-browser](open-gstack-browser/SKILL.md): Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.
- [/pair-agent](pair-agent/SKILL.md): Pair a remote AI agent with your browser.
+4 -1
View File
@@ -17,8 +17,11 @@ const claude = defineHost({
// allowed-tools/hooks; bin/gstack-brain-context-load reads gbrain: — which
// is why gbrain and hooks are NOT stripped). Stripping them trims the
// always-on frontmatter catalog every session loads.
stripFields: ['sensitive', 'voice-triggers', 'interactive', 'benefits-from', 'codex_default_prompt'],
stripFields: ['sensitive', 'manual_only', 'voice-triggers', 'interactive', 'benefits-from', 'codex_default_prompt'],
descriptionLimit: null,
conditionalFields: [
{ if: { manual_only: true }, add: { 'disable-model-invocation': true } },
],
},
generation: {
+9
View File
@@ -3,6 +3,15 @@ import { defineHost } from './define-host';
const cursor = defineHost({
name: 'cursor',
displayName: 'Cursor',
frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: null,
conditionalFields: [
{ if: { manual_only: true }, add: { 'disable-model-invocation': true } },
],
},
});
export default cursor;
+5 -4
View File
@@ -13,6 +13,7 @@ triggers:
- merge and deploy
- land the pr
- ship to production
disable-model-invocation: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
@@ -575,8 +576,8 @@ to check next. No deploy needed: merged, verification skipped for the stated rea
Staging-only: production remains unverified, not necessarily undeployed. Rollback
pending: identify who must resolve conflicts, merge the revert PR, or verify its deploy.
REVERTED: cite rollback evidence; do not claim the original branch survived cleanup.
Offer `/canary <url>` for extended monitoring, `/benchmark <url>` when performance
matters, and `/document-release` when docs need updating.
Offer `/canary <url>` for extended monitoring, `/benchmark <url>` for performance,
and `/document-release` for docs.
---
@@ -592,8 +593,8 @@ Recheck read-only evidence; never redo a merge/deploy because a section was miss
## Important Rules
- Never force-push, bypass CI, replay a confirmed merge, or hide missing evidence.
- Auto-detect facts; ask when unknown or when an explicit approval gate applies.
- Auto-detect facts; ask when unknown or an explicit approval gate applies.
- Step 3 is one `gstack-pr-wait` call; elsewhere poll at the stated interval (30 s for merge readback and deploy) with progress lines.
- After merge failures, offer approved rollback when appropriate; never revert a rollback automatically.
- Verify once; `/canary` provides extended monitoring. Rechecks require the user's choice.
- Verify once; `/canary` does extended monitoring; rechecks need the user's choice.
- Use `--delete-branch`; reconcile failed cleanup non-destructively with confirmation.
+5 -4
View File
@@ -15,6 +15,7 @@ allowed-tools:
- Glob
- AskUserQuestion
sensitive: true
manual_only: true
triggers:
- merge and deploy
- land the pr
@@ -422,8 +423,8 @@ to check next. No deploy needed: merged, verification skipped for the stated rea
Staging-only: production remains unverified, not necessarily undeployed. Rollback
pending: identify who must resolve conflicts, merge the revert PR, or verify its deploy.
REVERTED: cite rollback evidence; do not claim the original branch survived cleanup.
Offer `/canary <url>` for extended monitoring, `/benchmark <url>` when performance
matters, and `/document-release` when docs need updating.
Offer `/canary <url>` for extended monitoring, `/benchmark <url>` for performance,
and `/document-release` for docs.
---
@@ -439,8 +440,8 @@ Recheck read-only evidence; never redo a merge/deploy because a section was miss
## Important Rules
- Never force-push, bypass CI, replay a confirmed merge, or hide missing evidence.
- Auto-detect facts; ask when unknown or when an explicit approval gate applies.
- Auto-detect facts; ask when unknown or an explicit approval gate applies.
- Step 3 is one `gstack-pr-wait` call; elsewhere poll at the stated interval (30 s for merge readback and deploy) with progress lines.
- After merge failures, offer approved rollback when appropriate; never revert a rollback automatically.
- Verify once; `/canary` provides extended monitoring. Rechecks require the user's choice.
- Verify once; `/canary` does extended monitoring; rechecks need the user's choice.
- Use `--delete-branch`; reconcile failed cleanup non-destructively with confirmation.
+2 -1
View File
@@ -2,7 +2,7 @@
name: modernize
preamble-tier: 2
version: 0.1.0
description: "Bring an aging pull request onto its current base and decide whether it should still exist: MODERNIZE, REWORK or CLOSE, with range-diff proof before any push. (gstack)"
description: "Bring an aging PR onto its current base and decide: MODERNIZE, REWORK or CLOSE. (gstack)"
triggers:
- modernize this pr
- rebase this old pr
@@ -14,6 +14,7 @@ allowed-tools:
- Grep
- Glob
- AskUserQuestion
disable-model-invocation: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
+3 -2
View File
@@ -3,8 +3,8 @@ name: modernize
preamble-tier: 2
version: 0.1.0
description: |
Bring an aging pull request onto its current base and decide whether it should
still exist: MODERNIZE, REWORK or CLOSE, with range-diff proof before any push.
Bring an aging PR onto its current base and decide: MODERNIZE, REWORK or CLOSE.
Proves the rebase with range-diff before any push.
Use when asked to "modernize this PR", "rebase this old PR", "revive this
branch", or "is this PR still worth landing". Never closes a PR on its own.
(gstack)
@@ -21,6 +21,7 @@ allowed-tools:
- Glob
- AskUserQuestion
sensitive: true
manual_only: true
---
# /modernize — Is this PR still worth landing?
+12 -16
View File
@@ -486,13 +486,21 @@ policy:
/**
* Transform frontmatter for external hosts.
* Claude: strips `sensitive:` field (only Factory uses it).
* Claude: strips template-only fields, adds disable-model-invocation to manual_only skills.
* Codex: keeps name + description only, enforces 1024-char limit.
* Factory: keeps name + description + user-invocable, conditionally adds disable-model-invocation.
*/
function transformFrontmatter(content: string, host: Host): string {
const hostConfig = getHostConfig(host);
const fm = hostConfig.frontmatter;
const templateFrontmatter = content.startsWith('---\n') ? content.slice(4, content.indexOf('\n---', 4)) : '';
const escape = (s: unknown) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const conditionalLines = (fm.conditionalFields || [])
.filter(rule => Object.entries(rule.if).every(([k, v]) => new RegExp(`^${escape(k)}:[ \\t]*${escape(v)}[ \\t]*$`, 'm').test(templateFrontmatter)))
.flatMap(rule => Object.entries(rule.add))
.filter(([key]) => !new RegExp(`^${escape(key)}:`, 'm').test(templateFrontmatter))
.map(([key, value]) => `${key}: ${value}\n`)
.join('');
if (fm.mode === 'denylist') {
// Denylist mode: strip listed fields, keep everything else
@@ -503,7 +511,7 @@ function transformFrontmatter(content: string, host: Host): string {
content = content.replace(new RegExp(`^${field}:\\s*.*\\n`, 'm'), '');
}
}
return content;
return conditionalLines ? content.replace('\n---\n', () => `\n${conditionalLines}---\n`) : content;
}
// Allowlist mode: reconstruct frontmatter with only allowed fields
@@ -546,19 +554,7 @@ function transformFrontmatter(content: string, host: Host): string {
}
}
// Add conditional fields
if (fm.conditionalFields) {
for (const rule of fm.conditionalFields) {
const match = Object.entries(rule.if).every(([k, v]) =>
new RegExp(`^${k}:\\s*${v}`, 'm').test(frontmatter)
);
if (match) {
for (const [key, value] of Object.entries(rule.add)) {
newFm += `${key}: ${value}\n`;
}
}
}
}
newFm += conditionalLines;
// Preserve additional keepFields beyond name and description
if (fm.keepFields) {
@@ -853,7 +849,7 @@ function processTemplate(tmplPath: string, host: Host, options: RenderOptions):
// metadata gets the updated description with voice triggers included.
const postProcessDescription = extractNameAndDescription(content).description;
// For Claude: strip sensitive: field (only Factory uses it)
// For Claude: strip template-only fields, add conditional fields
// For external hosts: route output, transform frontmatter, rewrite paths
let symlinkLoop = false;
let metadata: { outputPath: string; content: string } | undefined;
@@ -16,7 +16,7 @@ Key routing rules:
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Ship/deploy/PR → invoke /ship (merge + deploy: the user types /land-and-deploy)
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec
+53
View File
@@ -772,6 +772,59 @@ describe('Copilot host render (#393)', () => {
});
});
describe('Manual-only skills on Claude Code and Cursor', () => {
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-cursor-'));
beforeAll(() => {
const r = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'cursor', '--out-dir', OUT], { cwd: ROOT, timeout: 120_000 });
if (r.exitCode !== 0) throw new Error(r.stderr.toString());
});
afterAll(() => fs.rmSync(OUT, { recursive: true, force: true }));
const frontmatter = (file: string) => {
const md = fs.readFileSync(file, 'utf8');
return md.slice(0, md.indexOf('\n---', 4));
};
const rendered: Record<string, (skill: string) => string> = {
claude: (skill) => frontmatter(path.join(ROOT, skill, 'SKILL.md')),
cursor: (skill) => frontmatter(path.join(OUT, '.cursor', 'skills', `gstack-${skill}`, 'SKILL.md')),
};
const shipKeys: Record<string, string[]> = {
claude: ['name', 'preamble-tier', 'version', 'description', 'allowed-tools', 'triggers'],
cursor: ['name', 'description'],
};
const manualOnly = fs.readdirSync(ROOT).filter(dir => /^manual_only: true$/m.test(fs.existsSync(path.join(ROOT, dir, 'SKILL.md.tmpl')) ? fs.readFileSync(path.join(ROOT, dir, 'SKILL.md.tmpl'), 'utf8') : ''));
test('manual_only marks exactly /land-and-deploy and /modernize, and no router line invokes them', () => {
expect(manualOnly.sort()).toEqual(['land-and-deploy', 'modernize']);
const routers = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf8') + fs.readFileSync(path.join(ROOT, 'bin', 'gstack-skill-start'), 'utf8');
for (const skill of manualOnly) {
expect(routers).not.toMatch(new RegExp(`invoke \`?/${skill}\\b`));
}
});
for (const [host, read] of Object.entries(rendered)) {
test(`${host}: /land-and-deploy and /modernize run only when the user types their name`, () => {
for (const skill of manualOnly) {
expect(read(skill)).toMatch(/^disable-model-invocation: true$/m);
expect(read(skill)).not.toContain('manual_only');
}
});
test(`${host}: /ship and the safety skills stay model-invocable`, () => {
expect([...read('ship').matchAll(/^([\w-]+):/gm)].map(m => m[1])).toEqual(shipKeys[host]);
for (const skill of ['careful', 'freeze', 'guard', 'unfreeze']) {
expect(read(skill)).not.toContain('disable-model-invocation');
}
});
}
test('claude and cursor map manual_only, never sensitive, to disable-model-invocation', () => {
for (const host of [claude, cursor]) {
expect(host.frontmatter.conditionalFields).toEqual([{ if: { manual_only: true }, add: { 'disable-model-invocation': true } }]);
}
});
});
describe('Antigravity CLI host render', () => {
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-agy-'));
beforeAll(() => {
+1 -1
View File
@@ -69,7 +69,7 @@ describe('/modernize skill', () => {
expect(read('AGENTS.md')).toMatch(/^\| `\/modernize` \| /m);
expect(read('docs/skills.md')).toMatch(/^\| \[`\/modernize`\]\(#modernize\) \| /m);
expect(read('docs/skills.md')).toContain('## `/modernize`');
expect(read('SKILL.md.tmpl')).toContain('invoke `/modernize`');
expect(read('SKILL.md.tmpl')).toContain('type `/modernize`');
expect(read('gstack/llms.txt')).toContain('[/modernize](modernize/SKILL.md)');
expect(read('docs/PROJECT_STRUCTURE.md')).toMatch(/^├── modernize\//m);
expect(read('lib/doctor-components.ts')).toMatch(/^ modernize: CLAUDE_ONLY,$/m);