diff --git a/README.md b/README.md index 780bf69..3c45f82 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,9 @@ That GitHub release then triggers `.github/workflows/release.yml`, which perform ```sh gx prompt # full checklist (paste into Codex/Claude) gx prompt --exec # commands only +gx prompt --part task-loop +gx prompt --exec --part finish --part cleanup +gx prompt --list-parts gx prompt --snippet # AGENTS.md managed-block template ``` diff --git a/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/.openspec.yaml b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/.openspec.yaml new file mode 100644 index 0000000..25345f4 --- /dev/null +++ b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-22 diff --git a/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/proposal.md b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/proposal.md new file mode 100644 index 0000000..03591b5 --- /dev/null +++ b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/proposal.md @@ -0,0 +1,28 @@ +## Why + +- `gx prompt` currently emits either the full setup checklist, the full + command-only block, or the AGENTS managed snippet. +- Agents and humans often need only one slice of that guidance + (`task-loop`, `finish`, `openspec`, etc.), so they either paste the whole + checklist or copy those lines into other docs/prompts by hand. +- That wastes tokens in agent handoffs and keeps prompt-facing docs more + repetitive than they need to be. + +## What Changes + +- Add named prompt parts to `gx prompt` so callers can request only the needed + guidance with `--part `. +- Add `gx prompt --list-parts` so the available slices are discoverable without + reading source or README prose. +- Support `gx prompt --exec --part ...` for command-capable parts only, with a + clear error when a selected part has no shell-safe command form. +- Update README/help text and focused prompt tests around the new surface. + +## Impact + +- Existing `gx prompt`, `gx prompt --exec`, and `gx prompt --snippet` behavior + stays intact for callers that do not request parts. +- Agent/token usage improves because handoffs can fetch just the required + prompt slices instead of the entire checklist. +- The change is limited to CLI prompt rendering, prompt docs, and focused + tests; no branch/lock/doctor workflow behavior changes. diff --git a/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/specs/gx-prompt-parts/spec.md b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/specs/gx-prompt-parts/spec.md new file mode 100644 index 0000000..b55062f --- /dev/null +++ b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/specs/gx-prompt-parts/spec.md @@ -0,0 +1,43 @@ +## ADDED Requirements + +### Requirement: `gx prompt` SHALL emit named prompt parts +`gx prompt` SHALL support selecting one or more named checklist slices with +`--part ` so callers can request only the needed guidance. + +#### Scenario: prompt mode returns only the selected parts +- **GIVEN** the user runs `gx prompt --part task-loop --part finish` +- **WHEN** the CLI renders prompt output +- **THEN** it SHALL include the `task-loop` and `finish` guidance in the order + requested +- **AND** it SHALL omit unrelated prompt sections such as `cleanup` and + `review-bot` +- **AND** the default `gx prompt` output without `--part` SHALL remain the full + checklist. + +### Requirement: `gx prompt --exec` SHALL support command-capable parts +Command-only prompt output SHALL allow part selection for sections that have a +shell-safe command form. + +#### Scenario: exec mode renders only the requested command-capable parts +- **GIVEN** the user runs `gx prompt --exec --part install --part task-loop` +- **WHEN** the CLI renders command-only output +- **THEN** it SHALL emit only the `install` and `task-loop` commands in the + order requested +- **AND** it SHALL omit command lines for other sections such as `cleanup` + unless they were requested. + +#### Scenario: exec mode rejects prompt-only parts +- **GIVEN** the user runs `gx prompt --exec --part openspec` +- **WHEN** `openspec` has no shell-safe command-only rendering +- **THEN** the CLI SHALL exit non-zero +- **AND** it SHALL report that the selected part is not available in exec mode. + +### Requirement: `gx prompt` SHALL list available parts +The CLI SHALL expose the available prompt part names without requiring source +inspection. + +#### Scenario: list parts +- **WHEN** the user runs `gx prompt --list-parts` +- **THEN** the CLI SHALL print the supported part names +- **AND** the list SHALL include both command-capable parts and prompt-only + parts. diff --git a/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/tasks.md b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/tasks.md new file mode 100644 index 0000000..afb1f26 --- /dev/null +++ b/openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/tasks.md @@ -0,0 +1,35 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05`; branch=`agent/codex/improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05`; scope=`src/context.js, src/cli/main.js, test/prompt.test.js, README.md, OpenSpec change docs`; action=`add gx prompt part selection, document the new surface, and keep the full prompt modes backward compatible`. +- Copy prompt: Continue `agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05` on branch `agent/codex/improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05`. Work inside the existing sandbox, review `openspec/changes/agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05 --base main --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05`. +- [x] 1.2 Define normative requirements in `specs/gx-prompt-parts/spec.md`. + +## 2. Implementation + +- [x] 2.1 Add named `gx prompt --part` / `--list-parts` support while keeping the existing full prompt, `--exec`, and `--snippet` outputs intact. +- [x] 2.2 Teach `gx prompt --exec --part ...` to emit only command-capable slices and fail clearly when a selected part has no command-only form. +- [x] 2.3 Update focused prompt docs/tests in `README.md` and `test/prompt.test.js`. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands (`node --check src/context.js`, `node --check src/cli/main.js`, `node --test test/prompt.test.js`) — passed on `2026-04-22`. +- [x] 3.2 Run `openspec validate agent-codex-improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05 --type change --strict` — passed on `2026-04-22`. +- [x] 3.3 Run `openspec validate --specs` — passed on `2026-04-22` with `No items found to validate.` + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/improve-gx-prompt-parts-for-token-usage-2026-04-22-16-05 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/src/cli/main.js b/src/cli/main.js index 208c8e9..e62e8cc 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -60,6 +60,9 @@ const { DEPRECATED_COMMAND_ALIASES, envFlagIsTruthy, defaultAgentWorktreeRelativeDir, + listAiSetupPartNames, + parseAiSetupPartNames, + renderAiSetupPrompt, AI_SETUP_PROMPT, AI_SETUP_COMMANDS, SCORECARD_RISK_BY_CHECK, @@ -5226,26 +5229,59 @@ function copyCommands() { function prompt(rawArgs) { const args = Array.isArray(rawArgs) ? rawArgs : []; let variant = 'prompt'; - for (const arg of args) { + let listParts = false; + const selectedParts = []; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; if (arg === '--exec' || arg === '--commands') variant = 'exec'; else if (arg === '--snippet' || arg === '--agents') variant = 'snippet'; else if (arg === '--prompt' || arg === '--full') variant = 'prompt'; + else if (arg === '--list-parts') listParts = true; + else if (arg === '--part' || arg === '--parts') { + const rawValue = args[index + 1]; + if (!rawValue || rawValue.startsWith('--')) { + throw new Error(`${arg} requires a value`); + } + selectedParts.push(...parseAiSetupPartNames(rawValue)); + index += 1; + } else if (arg.startsWith('--part=')) { + selectedParts.push(...parseAiSetupPartNames(arg.slice('--part='.length))); + } else if (arg.startsWith('--parts=')) { + selectedParts.push(...parseAiSetupPartNames(arg.slice('--parts='.length))); + } else if (arg === '-h' || arg === '--help') variant = 'help'; else throw new Error(`Unknown option: ${arg}`); } if (variant === 'help') { console.log( `${SHORT_TOOL_NAME} prompt commands:\n` + - ` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` + - ` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` + - ` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`, + ` ${SHORT_TOOL_NAME} prompt Print AI setup checklist\n` + + ` ${SHORT_TOOL_NAME} prompt --exec Print setup commands only (shell-ready)\n` + + ` ${SHORT_TOOL_NAME} prompt --part Print only the named checklist slice(s)\n` + + ` ${SHORT_TOOL_NAME} prompt --exec --part Print only the named exec-capable slice(s)\n` + + ` ${SHORT_TOOL_NAME} prompt --list-parts List prompt part names\n` + + ` ${SHORT_TOOL_NAME} prompt --exec --list-parts List exec-capable prompt part names\n` + + ` ${SHORT_TOOL_NAME} prompt --snippet Print the AGENTS.md managed-block template`, ); process.exitCode = 0; return; } - if (variant === 'exec') return copyCommands(); - if (variant === 'snippet') return printAgentsSnippet(); - return copyPrompt(); + if (variant === 'snippet') { + if (listParts || selectedParts.length > 0) { + throw new Error('--snippet does not support --list-parts or --part'); + } + return printAgentsSnippet(); + } + if (listParts) { + if (selectedParts.length > 0) { + throw new Error('--list-parts does not support --part'); + } + process.stdout.write(`${listAiSetupPartNames({ execOnly: variant === 'exec' }).join('\n')}\n`); + process.exitCode = 0; + return; + } + process.stdout.write(renderAiSetupPrompt({ exec: variant === 'exec', parts: selectedParts })); + process.exitCode = 0; } function branch(rawArgs) { diff --git a/src/context.js b/src/context.js index 2530708..ebff7f2 100644 --- a/src/context.js +++ b/src/context.js @@ -348,7 +348,7 @@ const CLI_COMMAND_DESCRIPTIONS = [ ['cleanup', 'Prune merged/stale agent branches and worktrees'], ['release', 'Create or update the current GitHub release with README-generated notes'], ['agents', 'Start/stop repo-scoped review + cleanup bots'], - ['prompt', 'Print AI setup checklist (--exec, --snippet)'], + ['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'], ['report', 'Security/safety reports (e.g. OpenSSF scorecard)'], ['help', 'Show this help output'], ['version', 'Print GitGuardex version'], @@ -369,6 +369,12 @@ const AGENT_BOT_DESCRIPTIONS = [ const DOCTOR_AUTO_FINISH_DETAIL_LIMIT = 6; const DOCTOR_AUTO_FINISH_BRANCH_LABEL_MAX = 72; const DOCTOR_AUTO_FINISH_MESSAGE_MAX = 160; +const AI_SETUP_PART_ALIASES = new Map([ + ['task', 'task-loop'], + ['loop', 'task-loop'], + ['reviewbot', 'review-bot'], + ['forksync', 'fork-sync'], +]); function envFlagIsTruthy(raw) { const lowered = String(raw || '').trim().toLowerCase(); @@ -383,35 +389,176 @@ function defaultAgentWorktreeRelativeDir(env = process.env) { return isClaudeCodeSession(env) ? CLAUDE_WORKTREE_RELATIVE_DIR : CODEX_WORKTREE_RELATIVE_DIR; } -const AI_SETUP_PROMPT = `GitGuardex (gx) setup checklist for Codex/Claude in this repo. - -1) Install: ${GLOBAL_INSTALL_COMMAND} && gh --version -2) Bootstrap: gx setup -3) Repair: gx doctor -4) Task loop: gx branch start "" "" - then gx locks claim --branch "" -> gx branch finish -5) Integrate: gx merge --branch --branch -6) Finish: gx finish --all -7) Cleanup: gx cleanup -8) OpenSpec: /opsx:propose -> /opsx:apply -> /opsx:archive -9) Optional: gx protect add release staging -10) Optional: gx sync --check && gx sync -11) Review bot: install https://github.com/apps/cr-gpt + set OPENAI_API_KEY -12) Fork sync: install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml -`; - -const AI_SETUP_COMMANDS = `${GLOBAL_INSTALL_COMMAND} -gh --version -gx setup -gx doctor -gx branch start "" "" -gx locks claim --branch "" -gx merge --branch "" --branch "" -gx finish --all -gx cleanup -gx protect add release staging -gx sync --check && gx sync -`; +const AI_SETUP_PARTS = [ + { + name: 'install', + label: 'Install', + promptLines: [`${GLOBAL_INSTALL_COMMAND} && gh --version`], + execLines: [GLOBAL_INSTALL_COMMAND, 'gh --version'], + }, + { + name: 'bootstrap', + label: 'Bootstrap', + promptLines: ['gx setup'], + execLines: ['gx setup'], + }, + { + name: 'repair', + label: 'Repair', + promptLines: ['gx doctor'], + execLines: ['gx doctor'], + }, + { + name: 'task-loop', + label: 'Task loop', + promptLines: [ + 'gx branch start "" ""', + 'then gx locks claim --branch "" -> gx branch finish', + ], + execLines: [ + 'gx branch start "" ""', + 'gx locks claim --branch "" ', + ], + }, + { + name: 'integrate', + label: 'Integrate', + promptLines: ['gx merge --branch --branch '], + execLines: ['gx merge --branch --branch '], + }, + { + name: 'finish', + label: 'Finish', + promptLines: ['gx finish --all'], + execLines: ['gx finish --all'], + }, + { + name: 'cleanup', + label: 'Cleanup', + promptLines: ['gx cleanup'], + execLines: ['gx cleanup'], + }, + { + name: 'openspec', + label: 'OpenSpec', + promptLines: ['/opsx:propose -> /opsx:apply -> /opsx:archive'], + }, + { + name: 'protect', + label: 'Protect', + promptLines: ['gx protect add release staging'], + execLines: ['gx protect add release staging'], + }, + { + name: 'sync', + label: 'Sync', + promptLines: ['gx sync --check && gx sync'], + execLines: ['gx sync --check && gx sync'], + }, + { + name: 'review-bot', + label: 'Review bot', + promptLines: ['install https://github.com/apps/cr-gpt + set OPENAI_API_KEY'], + }, + { + name: 'fork-sync', + label: 'Fork sync', + promptLines: ['install https://github.com/apps/pull + cp .github/pull.yml.example .github/pull.yml'], + }, +]; +const AI_SETUP_PARTS_BY_NAME = new Map(AI_SETUP_PARTS.map((part) => [part.name, part])); +const AI_SETUP_EXEC_PART_NAMES = AI_SETUP_PARTS + .filter((part) => Array.isArray(part.execLines)) + .map((part) => part.name); + +function normalizeAiSetupPartName(rawName) { + const normalized = String(rawName || '') + .trim() + .toLowerCase() + .replace(/_/g, '-'); + return AI_SETUP_PART_ALIASES.get(normalized) || normalized; +} + +function listAiSetupPartNames(options = {}) { + if (!options.execOnly) return AI_SETUP_PARTS.map((part) => part.name); + return AI_SETUP_EXEC_PART_NAMES.slice(); +} + +function parseAiSetupPartNames(rawValue) { + return String(rawValue || '') + .split(',') + .map((entry) => normalizeAiSetupPartName(entry)) + .filter(Boolean); +} + +function resolveAiSetupParts(rawPartNames, options = {}) { + const exec = Boolean(options.exec); + const requestedPartNames = Array.isArray(rawPartNames) ? rawPartNames : []; + const availablePartNames = listAiSetupPartNames(); + const execCapablePartNames = listAiSetupPartNames({ execOnly: true }); + const seen = new Set(); + const resolved = []; + + for (const rawName of requestedPartNames) { + const name = normalizeAiSetupPartName(rawName); + const part = AI_SETUP_PARTS_BY_NAME.get(name); + if (!part) { + throw new Error( + `Unknown prompt part: ${rawName}. Available parts: ${availablePartNames.join(', ')}`, + ); + } + if (exec && !Array.isArray(part.execLines)) { + throw new Error( + `Prompt part '${name}' is not available with --exec. ` + + `Exec-capable parts: ${execCapablePartNames.join(', ')}`, + ); + } + if (seen.has(name)) continue; + seen.add(name); + resolved.push(part); + } + + return resolved; +} + +function renderFullAiSetupPrompt() { + const lines = ['GitGuardex (gx) setup checklist for Codex/Claude in this repo.', '']; + const indentWidth = 18; + + AI_SETUP_PARTS.forEach((part, index) => { + const [lead, ...tail] = part.promptLines; + const prefix = `${index + 1}) ${part.label}:`; + lines.push(`${prefix.padEnd(indentWidth)}${lead}`); + tail.forEach((line) => lines.push(`${' '.repeat(indentWidth)}${line}`)); + }); + + return `${lines.join('\n')}\n`; +} + +function renderPartialAiSetupPrompt(parts) { + return `${parts + .map((part) => `${part.label}:\n${part.promptLines.join('\n')}`) + .join('\n\n')}\n`; +} + +function renderAiSetupCommands(parts) { + return `${parts.flatMap((part) => part.execLines).join('\n')}\n`; +} + +function renderAiSetupPrompt(options = {}) { + const exec = Boolean(options.exec); + const requestedPartNames = Array.isArray(options.parts) ? options.parts : []; + if (requestedPartNames.length === 0) { + return exec + ? renderAiSetupCommands(resolveAiSetupParts(AI_SETUP_EXEC_PART_NAMES, { exec: true })) + : renderFullAiSetupPrompt(); + } + const parts = resolveAiSetupParts(requestedPartNames, { exec }); + return exec ? renderAiSetupCommands(parts) : renderPartialAiSetupPrompt(parts); +} + +const AI_SETUP_PROMPT = renderAiSetupPrompt(); +const AI_SETUP_COMMANDS = renderAiSetupPrompt({ exec: true }); const SCORECARD_RISK_BY_CHECK = { 'Dangerous-Workflow': 'Critical', @@ -511,6 +658,9 @@ module.exports = { envFlagIsTruthy, isClaudeCodeSession, defaultAgentWorktreeRelativeDir, + listAiSetupPartNames, + parseAiSetupPartNames, + renderAiSetupPrompt, AI_SETUP_PROMPT, AI_SETUP_COMMANDS, SCORECARD_RISK_BY_CHECK, diff --git a/test/prompt.test.js b/test/prompt.test.js index f54a09f..1ac14ef 100644 --- a/test/prompt.test.js +++ b/test/prompt.test.js @@ -95,6 +95,49 @@ test('prompt --exec outputs command-only checklist', () => { assert.doesNotMatch(result.stdout, /GitGuardex \(gx\) setup checklist/); }); +test('prompt --part outputs only the selected checklist slices', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--part', 'task-loop', '--part', 'finish'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /^Task loop:/m); + assert.match(result.stdout, /gx branch start "" ""/); + assert.match(result.stdout, /^Finish:/m); + assert.match(result.stdout, /gx finish --all/); + assert.doesNotMatch(result.stdout, /GitGuardex \(gx\) setup checklist/); + assert.doesNotMatch(result.stdout, /^Cleanup:/m); + assert.doesNotMatch(result.stdout, /\/opsx:propose/); +}); + +test('prompt --exec --part outputs only selected command-capable slices', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--exec', '--part', 'install', '--part', 'task-loop'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /^npm i -g @imdeadpool\/guardex/m); + assert.match(result.stdout, /^gh --version$/m); + assert.match(result.stdout, /^gx branch start "" ""$/m); + assert.match(result.stdout, /^gx locks claim --branch "" $/m); + assert.doesNotMatch(result.stdout, /^gx cleanup$/m); + assert.doesNotMatch(result.stdout, /\/opsx:propose/); +}); + +test('prompt --list-parts prints the available prompt slices', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--list-parts'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /^install$/m); + assert.match(result.stdout, /^task-loop$/m); + assert.match(result.stdout, /^openspec$/m); + assert.match(result.stdout, /^review-bot$/m); +}); + +test('prompt --exec rejects prompt-only parts', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--exec', '--part', 'openspec'], repoDir); + assert.equal(result.status, 1, 'exec mode should reject prompt-only parts'); + assert.match(result.stderr, /Prompt part 'openspec' is not available with --exec/); + assert.match(result.stderr, /Exec-capable parts:/); +}); + test('deprecated copy-prompt alias still works and warns', () => { const repoDir = initRepo();