Merge pull request #1323 from gsd-build/fix/text-mode-plan-phase-v2
fix: add text_mode support to plan-phase workflow
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:plan-phase
|
||||
description: Create detailed phase plan (PLAN.md) with verification loop
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd <file>] [--reviews]"
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd <file>] [--reviews] [--text]"
|
||||
agent: gsd-planner
|
||||
allowed-tools:
|
||||
- Read
|
||||
@@ -36,6 +36,7 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit
|
||||
- `--skip-verify` — Skip verification loop
|
||||
- `--prd <file>` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely.
|
||||
- `--reviews` — Replan incorporating cross-AI review feedback from REVIEWS.md (produced by `/gsd:review`)
|
||||
- `--text` — Use plain-text numbered lists instead of TUI menus (required for `/rc` remote sessions)
|
||||
|
||||
Normalize phase input in step 2 before any directory lookups.
|
||||
</context>
|
||||
|
||||
@@ -173,6 +173,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
||||
plan_checker_enabled: config.plan_checker,
|
||||
nyquist_validation_enabled: config.nyquist_validation,
|
||||
commit_docs: config.commit_docs,
|
||||
text_mode: config.text_mode,
|
||||
|
||||
// Phase info
|
||||
phase_found: !!phaseInfo,
|
||||
|
||||
@@ -26,7 +26,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init plan-phase "$PH
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`.
|
||||
Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`.
|
||||
|
||||
**File paths (for <files_to_read> blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`, `reviews_path`. These are null if files don't exist.
|
||||
|
||||
@@ -34,7 +34,9 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_
|
||||
|
||||
## 2. Parse and Normalize Arguments
|
||||
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--prd <filepath>`, `--reviews`).
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--prd <filepath>`, `--reviews`, `--text`).
|
||||
|
||||
Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (`/rc` mode) where TUI menus don't work through the Claude App.
|
||||
|
||||
Extract `--prd <filepath>` from $ARGUMENTS. If present, set PRD_FILE to the filepath.
|
||||
|
||||
@@ -192,7 +194,20 @@ Read discuss mode for context gate label:
|
||||
DISCUSS_MODE=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.discuss_mode 2>/dev/null || echo "discuss")
|
||||
```
|
||||
|
||||
Use AskUserQuestion:
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list:
|
||||
```
|
||||
No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included.
|
||||
|
||||
1. Continue without context — Plan using research + requirements only
|
||||
[If DISCUSS_MODE is "assumptions":]
|
||||
2. Gather context (assumptions mode) — Analyze codebase and surface assumptions before planning
|
||||
[If DISCUSS_MODE is "discuss" or unset:]
|
||||
2. Run discuss-phase first — Capture design decisions before planning
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
Otherwise use AskUserQuestion:
|
||||
- header: "No context"
|
||||
- question: "No CONTEXT.md found for Phase {X}. Plans will use research and requirements only — your design preferences won't be included. Continue or capture context first?"
|
||||
- options:
|
||||
@@ -225,6 +240,17 @@ If "Run discuss-phase first":
|
||||
**If no explicit flag (`--research` or `--skip-research`) and not `--auto`:**
|
||||
Ask the user whether to research, with a contextual recommendation based on the phase:
|
||||
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list:
|
||||
```
|
||||
Research before planning Phase {X}: {phase_name}?
|
||||
|
||||
1. Research first (Recommended) — Investigate domain, patterns, and dependencies before planning. Best for new features, unfamiliar integrations, or architectural changes.
|
||||
2. Skip research — Plan directly from context and requirements. Best for bug fixes, simple refactors, or well-understood tasks.
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
Otherwise use AskUserQuestion:
|
||||
```
|
||||
AskUserQuestion([
|
||||
{
|
||||
@@ -359,7 +385,18 @@ UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
||||
|
||||
**If UI-SPEC.md missing AND `UI_GATE_CFG` is `true`:**
|
||||
|
||||
Use AskUserQuestion:
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list:
|
||||
```
|
||||
Phase {N} has frontend indicators but no UI-SPEC.md. Generate a design contract before planning?
|
||||
|
||||
1. Generate UI-SPEC first — Run /gsd:ui-phase {N} then re-run /gsd:plan-phase {N}
|
||||
2. Continue without UI-SPEC
|
||||
3. Not a frontend phase
|
||||
|
||||
Enter number:
|
||||
```
|
||||
|
||||
Otherwise use AskUserQuestion:
|
||||
- header: "UI Design Contract"
|
||||
- question: "Phase {N} has frontend indicators but no UI-SPEC.md. Generate a design contract before planning?"
|
||||
- options:
|
||||
@@ -667,7 +704,7 @@ Options:
|
||||
3. Proceed anyway — accept coverage gaps
|
||||
```
|
||||
|
||||
Use AskUserQuestion to present the options.
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list (options already shown in the block above). Otherwise use AskUserQuestion to present the options.
|
||||
|
||||
## 14. Present Final Status
|
||||
|
||||
|
||||
@@ -1044,9 +1044,14 @@ describe('resolveWorktreeRoot', () => {
|
||||
describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
const { resolveWorktreeRoot } = require('../get-shit-done/bin/lib/core.cjs');
|
||||
const { execSync } = require('child_process');
|
||||
// On Windows CI, os.tmpdir() may return 8.3 short paths (RUNNER~1) while
|
||||
// git returns long paths (runneradmin). realpathSync.native resolves both.
|
||||
const normalizePath = (p) => {
|
||||
try { return fs.realpathSync.native(p); } catch { return fs.realpathSync(p); }
|
||||
};
|
||||
|
||||
test('returns linked worktree cwd when it has its own .planning/', () => {
|
||||
const mainDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
|
||||
const mainDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
|
||||
let worktreeDir;
|
||||
try {
|
||||
// Set up main repo with a commit
|
||||
@@ -1060,7 +1065,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
execSync('git commit -m "initial"', { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
// Create a linked worktree
|
||||
worktreeDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
fs.rmSync(worktreeDir, { recursive: true, force: true });
|
||||
execSync(`git worktree add "${worktreeDir}" -b test-linked`, { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
@@ -1068,7 +1073,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
fs.mkdirSync(path.join(worktreeDir, '.planning'), { recursive: true });
|
||||
|
||||
// resolveWorktreeRoot should return the linked worktree dir, not the main repo
|
||||
const result = resolveWorktreeRoot(worktreeDir);
|
||||
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
|
||||
assert.strictEqual(result, worktreeDir,
|
||||
'linked worktree with .planning/ should resolve to itself, not the main repo');
|
||||
} finally {
|
||||
@@ -1081,7 +1086,7 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
});
|
||||
|
||||
test('returns main repo root when linked worktree has no .planning/', () => {
|
||||
const mainDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
|
||||
const mainDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-main-')));
|
||||
let worktreeDir;
|
||||
try {
|
||||
// Set up main repo with a commit
|
||||
@@ -1094,15 +1099,13 @@ describe('resolveWorktreeRoot with linked worktree .planning/', () => {
|
||||
execSync('git commit -m "initial"', { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
// Create a linked worktree (no .planning/)
|
||||
worktreeDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
worktreeDir = normalizePath(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-linked-')));
|
||||
fs.rmSync(worktreeDir, { recursive: true, force: true });
|
||||
execSync(`git worktree add "${worktreeDir}" -b test-linked-no-plan`, { cwd: mainDir, stdio: 'pipe' });
|
||||
|
||||
// resolveWorktreeRoot should return the main repo root
|
||||
// Normalize both paths with fs.realpathSync to handle Windows 8.3 short paths
|
||||
// (e.g., RUNNER~1 vs runneradmin on CI)
|
||||
const result = fs.realpathSync(resolveWorktreeRoot(worktreeDir));
|
||||
const expected = fs.realpathSync(mainDir);
|
||||
const result = normalizePath(resolveWorktreeRoot(worktreeDir));
|
||||
const expected = normalizePath(mainDir);
|
||||
assert.strictEqual(result, expected,
|
||||
'linked worktree without .planning/ should resolve to main repo root');
|
||||
} finally {
|
||||
|
||||
@@ -117,6 +117,31 @@ describe('workflow.discuss_mode config', () => {
|
||||
assert.ok(workflow.includes('--text'), 'should handle --text flag');
|
||||
});
|
||||
|
||||
test('plan-phase workflow references text_mode', () => {
|
||||
const planPhase = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'workflows', 'plan-phase.md'), 'utf8'
|
||||
);
|
||||
assert.ok(planPhase.includes('text_mode'), 'plan-phase workflow should reference text_mode');
|
||||
assert.ok(planPhase.includes('TEXT_MODE'), 'plan-phase workflow should use TEXT_MODE variable');
|
||||
assert.ok(planPhase.includes('--text'), 'plan-phase workflow should handle --text flag');
|
||||
});
|
||||
|
||||
test('plan-phase command argument-hint includes --text', () => {
|
||||
const command = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'commands', 'gsd', 'plan-phase.md'), 'utf8'
|
||||
);
|
||||
assert.ok(command.includes('--text'), 'argument-hint should include --text flag');
|
||||
});
|
||||
|
||||
test('plan-phase init exposes text_mode in workflow flags', () => {
|
||||
const initSrc = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'init.cjs'), 'utf8'
|
||||
);
|
||||
// The cmdInitPlanPhase result object must include text_mode
|
||||
const planPhaseBlock = initSrc.slice(initSrc.indexOf('function cmdInitPlanPhase'));
|
||||
assert.ok(planPhaseBlock.includes('text_mode: config.text_mode'), 'init plan-phase must expose text_mode');
|
||||
});
|
||||
|
||||
test('progress workflow references discuss_mode', () => {
|
||||
const progress = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'workflows', 'progress.md'), 'utf8'
|
||||
|
||||
@@ -54,6 +54,27 @@ describe('init commands', () => {
|
||||
assert.strictEqual(output.uat_path, '.planning/phases/03-api/03-UAT.md');
|
||||
});
|
||||
|
||||
test('init plan-phase exposes text_mode from config (defaults false)', () => {
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.text_mode, false, 'text_mode should default to false');
|
||||
});
|
||||
|
||||
test('init plan-phase exposes text_mode true when set in config', () => {
|
||||
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
||||
const existing = fs.existsSync(configPath)
|
||||
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
||||
: {};
|
||||
const config = { ...existing, workflow: { ...(existing.workflow || {}), text_mode: true } };
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.text_mode, true, 'text_mode should reflect config value');
|
||||
});
|
||||
|
||||
test('init progress returns file paths', () => {
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
Reference in New Issue
Block a user