Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/draft-registers-issue-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@thiagodiogo/pscode": minor
---

refactor(draft): `/ps:draft` apenas registra a Issue, brief migra para o refine

O `/ps:draft` deixa de criar `brief.md` (e a pasta local): agora só registra a
mudança como card no Backlog, com uma descrição curta no corpo da Issue. Sem
GitHub, há um fallback que grava um `brief.md` local mínimo. A pasta da change e o
`brief.md` passam a nascer no `/ps:refine` (a partir da descrição da Issue), antes
do `refine.md`.
14 changes: 14 additions & 0 deletions .changeset/optional-pr-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@thiagodiogo/pscode": minor
---

feat(init): torna o fluxo de PR opcional

Adiciona a pergunta "usar fluxo de PR?" no `pscode init` (antes da pergunta do
board) e as flags `--pr` / `--no-pr`. A escolha é gravada em
`pscode/config.yaml` (`pr_flow`) e seleciona qual forma dos comandos/skills de
dev é instalada: o fluxo com pull request (abre PR draft, marca Ready for Review,
não faz merge) ou o fluxo direto na branch atual (commit direto, sem PR). O
conteúdo condicional é resolvido via marcadores `{{#pr}}` / `{{^pr}}`
(`core/content/flags.ts`) no momento da renderização; `update` re-renderiza
respeitando o `pr_flow` do projeto.
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ One adapter per agent (claude, codex, cursor, gemini). All share a uniform layou
Writes/removes the rails: `installAgent`, `removeAgent`, `installChangeTemplates`, `ensureProjectStructure`, plus status helpers (`installedVersion`, `agentArtifactStatus`, `isAgentInstalled`).

**Config** (`src/core/pscode-config.ts`)
`pscode/config.yaml` (Zod-validated): agents, profile, the short-document `limits`, and the two guardrails (`apply_mode: one_task_at_a_time`, `approval_required`).
`pscode/config.yaml` (Zod-validated): agents, profile, the short-document `limits`, the two guardrails (`apply_mode: one_task_at_a_time`, `approval_required`), and `pr_flow` (whether the dev step opens a pull request or commits directly to the current branch).

**Conditional content** (`src/core/content/flags.ts`)
Command/skill bodies (and descriptions) carry `{{#pr}}…{{/pr}}` / `{{^pr}}…{{/pr}}` markers. `applyContentFlags` resolves them at render time so one source of truth installs either the PR flow or the commit-directly flow. The adapter (`renderCommand`/`renderSkill`) applies the flags; `installAgent(root, id, { prFlow })` selects which shape to write, and `update` re-renders with the project's recorded `pr_flow`.

**Detection & Instruction Files** (`src/core/detect.ts`, `src/core/agents-md.ts`)
`detectAgents` finds agents in use. `agents-md.ts` writes the PSCode block into the instruction file each selected agent reads (Claude Code → `CLAUDE.md`, the others → `AGENTS.md`; both when mixed). Only the text between the `` markers is rewritten; user content is preserved.
Expand Down Expand Up @@ -103,7 +106,7 @@ test/
```

**CLI Commands** (`src/commands/`)
- `pscode init` — install the workflow (detects/prompts agents; `--agent`, `--lang`, `--bypass-permissions` / `--no-bypass-permissions`, `--open` / `--no-open`, `--yes`). For Claude Code it can also write `permissions.defaultMode: bypassPermissions` into `.claude/settings.json` (see `core/claude-settings.ts`). When done it can open the selected agent's CLI — Claude Code preferred — handing off the terminal (`core/launch.ts`); with no TTY it prints how to start instead.
- `pscode init` — install the workflow (detects/prompts agents; `--agent`, `--lang`, `--bypass-permissions` / `--no-bypass-permissions`, `--pr` / `--no-pr`, `--open` / `--no-open`, `--yes`). A wizard question (asked before the GitHub/board question) toggles the pull-request flow; `--pr` / `--no-pr` force it. For Claude Code it can also write `permissions.defaultMode: bypassPermissions` into `.claude/settings.json` (see `core/claude-settings.ts`). When done it can open the selected agent's CLI — Claude Code preferred — handing off the terminal (`core/launch.ts`); with no TTY it prints how to start instead.
- `pscode update` — refresh PSCode-controlled files in place, preserving user content.
- `pscode doctor` — verify config, structure, and per-agent install/version; non-zero exit on issues.
- `pscode clean` — remove the rails (`--all` also removes `pscode/`); destructive actions need `--yes`.
Expand Down
3 changes: 3 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function buildProgram(): Command {
.option('-l, --lang <code>', 'wizard language: en, pt')
.option('--bypass-permissions', 'enable Claude Code bypassPermissions mode in .claude/settings.json')
.option('--no-bypass-permissions', 'do not enable bypassPermissions mode')
.option('--pr', 'install the pull-request flow (a draft PR per change)')
.option('--no-pr', 'install the no-PR flow (commit directly to the current branch)')
.option('--open', 'open the selected agent CLI when init finishes (Claude Code preferred)')
.option('--no-open', 'do not open an agent when init finishes')
.option('--github', 'set up GitHub Projects + Issues sync')
Expand All @@ -45,6 +47,7 @@ export function buildProgram(): Command {
agents: opts.agent,
lang: opts.lang,
bypassPermissions: opts.bypassPermissions,
pr: opts.pr,
open: opts.open,
github: opts.github,
project: opts.project,
Expand Down
29 changes: 27 additions & 2 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export interface InitOptions {
* force it from a CLI flag. Only applies when Claude Code is selected.
*/
bypassPermissions?: boolean;
/**
* Install the pull-request flow (a draft PR per change) instead of committing
* directly to the current branch. Tri-state: `undefined` defers to the wizard
* (default yes), `true`/`false` force it from a CLI flag. Independent of the
* board; PR-on presumes a GitHub repo with a remote.
*/
pr?: boolean;
/**
* Open the selected agent's CLI after install (Claude Code preferred).
* Tri-state: `undefined` defers to the wizard (default yes), `true`/`false`
Expand Down Expand Up @@ -135,6 +142,20 @@ async function resolveBypassPermissions(
return instantConfirm({ message: t.bypassPermissionsPrompt, default: true });
}

/**
* Resolve whether to install the pull-request flow. Independent of the board —
* you can use PRs without a Project (and a Project without PRs) — and asked just
* before the GitHub question. PR-on presumes a GitHub repo with a remote, which
* the agent resolves via `gh` at run time.
*/
async function resolvePrFlow(opts: InitOptions, interactive: boolean, t: InitMessages): Promise<boolean> {
if (opts.pr !== undefined) return opts.pr;
if (!interactive) return true;

const { instantConfirm } = await import('../core/prompts.js');
return instantConfirm({ message: t.prFlowPrompt, default: true });
}

/**
* Resolve which agent (if any) to open after install. Prioritizes Claude Code;
* returns null when the user opts out or no selected agent is launchable.
Expand Down Expand Up @@ -177,6 +198,10 @@ export async function runInit(opts: InitOptions = {}): Promise<void> {
const preflight = collectPreflight(projectRoot, { agents });
printPreflight(preflight, t);

// Asked before the board question — the PR flow is independent of GitHub
// Projects (you can use either without the other).
const prFlow = await resolvePrFlow(opts, interactive, t);

const githubEnabled = await runGitHubSetup(
projectRoot,
{ github: opts.github, project: opts.project },
Expand All @@ -185,9 +210,9 @@ export async function runInit(opts: InitOptions = {}): Promise<void> {
);

ensureProjectStructure(projectRoot);
writeConfig(projectRoot, buildConfig({ agents, githubEnabled }));
writeConfig(projectRoot, buildConfig({ agents, githubEnabled, prFlow }));
installChangeTemplates(projectRoot);
for (const agentId of agents) installAgent(projectRoot, agentId);
for (const agentId of agents) installAgent(projectRoot, agentId, { prFlow });
const instructionFiles = syncInstructionFiles(projectRoot, agents);
const settingsFile = bypassPermissions ? enableBypassPermissions(projectRoot) : undefined;

Expand Down
12 changes: 9 additions & 3 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,19 @@ export async function runUpdate(opts: UpdateOptions = {}): Promise<void> {
.filter((id) => isAgentInstalled(projectRoot, id));
const agents = Array.from(new Set([...fromConfig, ...detected]));

for (const agentId of agents) installAgent(projectRoot, agentId);
// Re-render with the project's recorded PR-flow choice so update keeps the
// same shape of the dev commands/skills the project was installed with.
const prFlow = config.pr_flow;
for (const agentId of agents) installAgent(projectRoot, agentId, { prFlow });
installChangeTemplates(projectRoot);
const instructionFiles = syncInstructionFiles(projectRoot, agents);

// Refresh the version (and recorded agents) in place, preserving the GitHub
// integration flag — update must not silently disable it.
writeConfig(projectRoot, buildConfig({ agents, githubEnabled: config.github.enabled }));
// integration and PR-flow flags — update must not silently change them.
writeConfig(
projectRoot,
buildConfig({ agents, githubEnabled: config.github.enabled, prFlow })
);

console.log(chalk.green(`\n✓ PSCode updated to v${PSCODE_VERSION}\n`));
console.log(` ${chalk.bold('Agents:')} ${agents.join(', ') || '(none)'}`);
Expand Down
20 changes: 12 additions & 8 deletions src/core/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import path from 'path';
import { AGENTS, getAgent, PSCODE_VERSION } from './config.js';
import type { CommandSpec, SkillSpec } from './content/types.js';
import { applyContentFlags, type ContentFlags } from './content/flags.js';

/** Default rendering flags: the full pull-request flow. */
const DEFAULT_FLAGS: ContentFlags = { pr: true };

/** Quote a YAML scalar if it contains characters that need escaping. */
function yamlScalar(value: string): string {
Expand All @@ -25,8 +29,8 @@ export interface AgentAdapter {
dir: string;
commandPath(id: string): string;
skillPath(name: string): string;
renderCommand(spec: CommandSpec): string;
renderSkill(spec: SkillSpec): string;
renderCommand(spec: CommandSpec, flags?: ContentFlags): string;
renderSkill(spec: SkillSpec, flags?: ContentFlags): string;
}

function createAdapter(id: string): AgentAdapter {
Expand All @@ -40,18 +44,18 @@ function createAdapter(id: string): AgentAdapter {
dir,
commandPath: (cmdId) => path.join(dir, 'commands', 'ps', `${cmdId}.md`),
skillPath: (skillName) => path.join(dir, 'skills', skillName, 'SKILL.md'),
renderCommand: (spec) =>
renderCommand: (spec, flags = DEFAULT_FLAGS) =>
`---\n` +
`name: ${yamlScalar(spec.name)}\n` +
`description: ${yamlScalar(spec.description)}\n` +
`description: ${yamlScalar(applyContentFlags(spec.description, flags))}\n` +
`generatedBy: ${yamlScalar(PSCODE_VERSION)}\n` +
`---\n\n${spec.body}`,
renderSkill: (spec) =>
`---\n\n${applyContentFlags(spec.body, flags)}`,
renderSkill: (spec, flags = DEFAULT_FLAGS) =>
`---\n` +
`name: ${yamlScalar(spec.name)}\n` +
`description: ${yamlScalar(spec.description)}\n` +
`description: ${yamlScalar(applyContentFlags(spec.description, flags))}\n` +
`generatedBy: ${yamlScalar(PSCODE_VERSION)}\n` +
`---\n\n${spec.body}`,
`---\n\n${applyContentFlags(spec.body, flags)}`,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/content/commands/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ Use the **pscode-complete** skill.

Use the **pscode-github-sync** skill: **move the card → Done** (\`done\`) and
confirm the move landed, comment the conclusion, then **close** the Issue.
Merging the PR stays human/CI. Non-blocking only on \`gh\` failure.
{{#pr}}Merging the PR stays human/CI. {{/pr}}Non-blocking only on \`gh\` failure.
`,
};
18 changes: 11 additions & 7 deletions src/core/content/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const dev: CommandSpec = {
id: 'dev',
name: 'ps:dev',
description:
'Develops a Ready-to-Dev card: opens a draft PR, moves to In Development, implements one subtask at a time, then walks the card through Code Review → Test → Ready to Deploy.',
'Develops a Ready-to-Dev card: {{#pr}}opens a draft PR, {{/pr}}moves to In Development, implements one subtask at a time{{^pr}} on the current branch{{/pr}}, then walks the card through Code Review → Test → Ready to Deploy.',
body: `# /ps:dev <card#>

Take a **refined** change (Ready to Dev) and build it. Accepts the board
Expand All @@ -16,9 +16,11 @@ scope mid-subtask.
## Start (if \`pscode/github.yaml\` exists)

Use the **pscode-github-sync** skill, in order:
1. Open the **PR as a draft** and link it to the Issue (\`Closes #<card>\`).
{{#pr}}1. Open the **PR as a draft** and link it to the Issue (\`Closes #<card>\`).
2. **Move the card → In Development** (\`in_progress\`) and **assign the user** —
the assign does not replace the status move; confirm the move landed.
the assign does not replace the status move; confirm the move landed.{{/pr}}{{^pr}}1. **Move the card → In Development** (\`in_progress\`) — confirm the move landed.
2. **Assign the user.** The assign does not replace the status move; both must
run. Work directly on the current branch — no PR is opened.{{/pr}}

## Implement

Expand All @@ -31,15 +33,17 @@ Use the **pscode-github-sync** skill, in order:
short diff, run the relevant validation, and ask before ticking it \`[x]\`.
After ticking, **close its sub-issue** on the card. Repeat for each subtask.
5. When every subtask is done **and the project builds and its tests pass** (use
the project's own build/test commands), mark the PR **Ready for Review** and
move the card → **In Code Review** (\`review\`).
the project's own build/test commands), {{#pr}}mark the PR **Ready for Review**
and move the card → **In Code Review** (\`review\`).{{/pr}}{{^pr}}move the card →
**In Code Review** (\`review\`).{{/pr}}
6. With the user's approval, move the card → **In Test** (\`in_test\`).
7. Once the user confirms it is **working**, move the card → **Ready to Deploy**
(\`ready_to_deploy\`) and post the **next-step comment** (\`/ps:complete <card#>\`
in a fenced block).

Each of steps 5–7 **moves the card** on the board — confirm every move landed,
don't leave the card behind. Merging the PR stays a human/CI decision — never
merge here. \`gh\` calls are non-blocking only on failure, never optional.
don't leave the card behind. {{#pr}}Merging the PR stays a human/CI decision —
never merge here. {{/pr}}\`gh\` calls are non-blocking only on failure, never
optional.
`,
};
32 changes: 20 additions & 12 deletions src/core/content/commands/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@ export const draft: CommandSpec = {
description: 'Captures a natural-language request as a short draft/brief in the Backlog.',
body: `# /ps:draft

Take a natural-language request and capture it as a short **draft** — nothing
more. No analysis, no code: that is \`/ps:refine\`'s job.
Take a natural-language request and register it as a card in the **Backlog** —
nothing more. No analysis, no code, **and no \`brief.md\`**: structuring the change
is \`/ps:refine\`'s job. The draft only gets the idea onto the board.

Use the **pscode-guided-sdd** skill (draft step).

1. Understand the request well enough to name it.
2. Create the folder \`pscode/changes/<slug>\` (slug in kebab-case).
3. Write a short \`brief.md\` (objective, expected behavior, out of scope).
4. **Stop and ask for validation.**
1. Understand the request well enough to name it (kebab-case slug = title).
2. Draft a **short description** — objective plus a line on expected behavior and
what's out of scope. A few lines, not a spec.
3. **Stop and ask for validation** of that description.

Do not ask the Grill Me questions and do not write code here.
Do not ask the Grill Me questions, do not write a \`brief.md\`, and do not write
code here.

## GitHub sync (if \`pscode/github.yaml\` exists)
## Register the card

Use the **pscode-github-sync** skill: create the Issue, add it to the Project,
**set status Backlog** (confirm it landed), and save the number to \`.issue\`.
Then post the **next-step comment** (\`/ps:refine <card#>\` in a fenced block).
Non-blocking only on \`gh\` failure.
**With GitHub (\`pscode/github.yaml\` exists):** use the **pscode-github-sync**
skill — create the Issue with that short description as its **body**, add it to
the Project, **set status Backlog** (confirm it landed). No local files are
created here; the Issue *is* the draft (the change folder and \`.issue\` are
written later, by \`/ps:refine\`). Then post the **next-step comment**
(\`/ps:refine <card#>\` in a fenced block). Non-blocking only on \`gh\` failure.

**Without GitHub:** there is nowhere to register the card, so fall back to a
local record — create \`pscode/changes/<slug>/\` and save the short description as
a minimal \`brief.md\`. This is the only case where \`/ps:draft\` writes a file.
`,
};
14 changes: 10 additions & 4 deletions src/core/content/commands/refine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@ does not replace the status move; confirm the move landed. Non-blocking only on

## Then refine

1. Gather context before refining:
- Read \`brief.md\` (the draft).
1. **Set up the change and write the brief.** The draft now lives on the card,
not in a file. Resolve the slug (Issue title in kebab-case), create
\`pscode/changes/<slug>/\`, and save the card number to \`.issue\`. Turn the
**Issue description** (the draft) into \`brief.md\` (objective, expected
behavior, out of scope). If a local \`brief.md\` already exists (no-GitHub
draft), use it as-is.
2. Gather context before refining:
- Read \`brief.md\`.
- Read \`questions.md\`: fold in answered questions, note any still open.
- **Analyze the code** the change will touch, so the refinement is grounded.
- If \`pscode/github.yaml\` exists, read the Issue **description + comments**
via **pscode-github-sync** — recent discussion may add or cut scope.
2. Run the **Grill Me** logic (skill \`pscode-grill-me\`) to close blocking
3. Run the **Grill Me** logic (skill \`pscode-grill-me\`) to close blocking
ambiguities — at most 5 questions.
3. Write \`refine.md\` in the standard format: lean summary, technical detail,
4. Write \`refine.md\` in the standard format: lean summary, technical detail,
in/out of scope, and a **\`## Subtasks\`** checklist (the unit \`/ps:dev\` runs).

Do not write production code in this step. **Stop and ask for approval.**
Expand Down
21 changes: 21 additions & 0 deletions src/core/content/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Conditional-content markers for command/skill bodies.
*
* `{{#pr}}…{{/pr}}` is kept only when the PR flow is enabled; `{{^pr}}…{{/pr}}`
* is kept only when it's disabled. This lets one source of truth install two
* shapes of the dev flow — pull-request-based vs. commit-directly — without
* duplicating whole files. Removing a block can leave blank-line runs (or a
* dangling space) behind, so we tidy those up to keep the Markdown clean.
*/
export interface ContentFlags {
/** Install the pull-request flow (draft PR per change). */
pr: boolean;
}

export function applyContentFlags(text: string, flags: ContentFlags): string {
return text
.replace(/\{\{#pr\}\}([\s\S]*?)\{\{\/pr\}\}/g, flags.pr ? '$1' : '')
.replace(/\{\{\^pr\}\}([\s\S]*?)\{\{\/pr\}\}/g, flags.pr ? '' : '$1')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n');
}
2 changes: 1 addition & 1 deletion src/core/content/skills/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Close out a change and keep the history tidy. Two paths share this skill:
\`pscode/changes/archive/<YYYY-MM-DD>-<slug>/\` (use today's date).
4. If \`pscode/github.yaml\` exists, use \`pscode-github-sync\`: **move the card →
Done** (\`done\`) and confirm it landed, comment the conclusion, then **close**
the Issue. Merging the PR stays human/CI.
the Issue.{{#pr}} Merging the PR stays human/CI.{{/pr}}

## Cancel path (\`/ps:cancel\`)

Expand Down
Loading
Loading