From 53edf284fa16036a284093ec75253e941b6d680a Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Mon, 16 Mar 2026 11:45:50 -0500 Subject: [PATCH] feat: /gsd quick command & agent-instructions.md injection (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: bg_shell ready_port timeout and error handling (#428) When a server fails to bind to the configured ready_port, the process would stay in "starting" status indefinitely after the probing interval cleared, with no error surfaced to the agent. This fixes the hang by: - Transitioning process to "error" status when port probing times out - Detecting process exit during port polling and reporting stderr context - Adding ready_timeout parameter for custom timeout values - Including stderr output in waitForReady timeout/error responses - Registering SIGTERM/SIGINT handlers to clean up bg processes on exit Closes #428 * feat: add /gsd quick command and agent-instructions.md injection (#425) Implements two features from issue #425: 1. `/gsd quick ` — lightweight task execution with GSD guarantees (atomic commits, state tracking) without the full milestone ceremony. Creates `.gsd/quick/-/` directory, a git branch, and dispatches a focused prompt for in-session execution. 2. Agent instructions file — loads `~/.gsd/agent-instructions.md` (global) and `.gsd/agent-instructions.md` (project), injects into every GSD agent session via the before_agent_start hook. Lets users add durable instructions like notification preferences or environment constraints. Closes #425 --------- Co-authored-by: TÂCHES --- src/resources/extensions/gsd/commands.ts | 10 +- src/resources/extensions/gsd/index.ts | 38 ++++- .../extensions/gsd/prompts/quick-task.md | 48 ++++++ .../extensions/gsd/prompts/system.md | 1 + src/resources/extensions/gsd/quick.ts | 156 ++++++++++++++++++ 5 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 src/resources/extensions/gsd/prompts/quick-task.md create mode 100644 src/resources/extensions/gsd/quick.ts diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index dff84e70f..02f7053d1 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -36,6 +36,7 @@ import { import { loadPrompt } from "./prompt-loader.js"; import { handleRemote } from "../remote-questions/remote-command.js"; +import { handleQuick } from "./quick.js"; import { handleHistory } from "./history.js"; import { handleUndo } from "./undo.js"; import { handleExport } from "./export.js"; @@ -66,10 +67,10 @@ function projectRoot(): string { export function registerGSDCommand(pi: ExtensionAPI): void { pi.registerCommand("gsd", { - description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|capture|triage|history|undo|skip|export|cleanup|prefs|config|hooks|run-hook|skill-health|doctor|migrate|remote|steer|knowledge", + description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|prefs|config|hooks|run-hook|skill-health|doctor|migrate|remote|steer|knowledge", getArgumentCompletions: (prefix: string) => { const subcommands = [ - "help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "discuss", + "help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "quick", "discuss", "capture", "triage", "history", "undo", "skip", "export", "cleanup", "prefs", "config", "hooks", "run-hook", "skill-health", "doctor", "migrate", "remote", "steer", "inspect", "knowledge", @@ -282,6 +283,11 @@ export function registerGSDCommand(pi: ExtensionAPI): void { return; } + if (trimmed === "quick" || trimmed.startsWith("quick ")) { + await handleQuick(trimmed.replace(/^quick\s*/, "").trim(), ctx, pi); + return; + } + if (trimmed === "config") { await handleConfig(ctx); return; diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index 110744257..903cc4c97 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -54,10 +54,39 @@ import { import { Key } from "@gsd/pi-tui"; import { join } from "node:path"; import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; import { shortcutDesc } from "../shared/terminal.js"; import { Text } from "@gsd/pi-tui"; import { pauseAutoForProviderError } from "./provider-error-pause.js"; +// ── Agent Instructions ──────────────────────────────────────────────────── +// Lightweight "always follow" files injected into every GSD agent session. +// Global: ~/.gsd/agent-instructions.md Project: .gsd/agent-instructions.md +// Both are loaded and concatenated (global first, project appends). + +function loadAgentInstructions(): string | null { + const parts: string[] = []; + + const globalPath = join(homedir(), ".gsd", "agent-instructions.md"); + if (existsSync(globalPath)) { + try { + const content = readFileSync(globalPath, "utf-8").trim(); + if (content) parts.push(content); + } catch { /* non-fatal — skip unreadable file */ } + } + + const projectPath = join(process.cwd(), ".gsd", "agent-instructions.md"); + if (existsSync(projectPath)) { + try { + const content = readFileSync(projectPath, "utf-8").trim(); + if (content) parts.push(content); + } catch { /* non-fatal — skip unreadable file */ } + } + + if (parts.length === 0) return null; + return parts.join("\n\n"); +} + // ── Depth verification state ────────────────────────────────────────────── let depthVerificationDone = false; @@ -527,6 +556,13 @@ export default function (pi: ExtensionAPI) { } } + // Load agent instructions (global + project) + let agentInstructionsBlock = ""; + const agentInstructions = loadAgentInstructions(); + if (agentInstructions) { + agentInstructionsBlock = `\n\n## Agent Instructions\n\nThe following instructions were provided by the user and must be followed in every session:\n\n${agentInstructions}`; + } + const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd()); // Worktree context — override the static CWD in the system prompt @@ -571,7 +607,7 @@ export default function (pi: ExtensionAPI) { } return { - systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`, + systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${agentInstructionsBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`, ...(injection ? { message: { diff --git a/src/resources/extensions/gsd/prompts/quick-task.md b/src/resources/extensions/gsd/prompts/quick-task.md new file mode 100644 index 000000000..06b9c18d0 --- /dev/null +++ b/src/resources/extensions/gsd/prompts/quick-task.md @@ -0,0 +1,48 @@ +You are executing a GSD quick task — a lightweight, focused unit of work outside the milestone/slice ceremony. + +## QUICK TASK: {{description}} + +**Task directory:** `{{taskDir}}` +**Branch:** `{{branch}}` + +## Instructions + +1. Read the task description above carefully. This is a focused, self-contained task. +2. If a `GSD Skill Preferences` block is present in system context, follow it. +3. Read relevant code before modifying. Understand existing patterns. +4. Execute the task completely: + - Build the real thing, not stubs or placeholders. + - Write or update tests where appropriate. + - Handle error cases and edge cases. +5. Verify your work: + - Run tests if applicable. + - Verify both happy path and failure modes for non-trivial changes. +6. Commit your changes atomically: + - Use conventional commit messages (feat:, fix:, refactor:, etc.) + - Stage only relevant files — never commit secrets or runtime files. + - Commit logical units separately if the task involves distinct changes. +7. Write a brief summary to `{{summaryPath}}`: + +```markdown +# Quick Task: {{description}} + +**Date:** {{date}} +**Branch:** {{branch}} + +## What Changed +- + +## Files Modified +- + +## Verification +- +``` + +8. Update `.gsd/STATE.md` — add or update the "Quick Tasks Completed" table: + - If the section doesn't exist, create it after "### Blockers/Concerns" + - Table format: `| # | Description | Date | Commit | Directory |` + - Add a row: `| {{taskNum}} | {{description}} | {{date}} | | [{{taskNum}}-{{slug}}](./quick/{{taskNum}}-{{slug}}/) |` + - Update the "Last activity" line + +When done, say: "Quick task {{taskNum}} complete." diff --git a/src/resources/extensions/gsd/prompts/system.md b/src/resources/extensions/gsd/prompts/system.md index a82b8a28e..4e7716eef 100644 --- a/src/resources/extensions/gsd/prompts/system.md +++ b/src/resources/extensions/gsd/prompts/system.md @@ -128,6 +128,7 @@ Templates showing the expected format for each artifact type are in: - `/gsd stop` - stop auto-mode - `/gsd status` - progress dashboard overlay - `/gsd queue` - queue future milestones (safe while auto-mode is running) +- `/gsd quick ` - quick task with GSD guarantees (atomic commits, state tracking) but no milestone ceremony - `Ctrl+Alt+G` - toggle dashboard overlay - `Ctrl+Alt+B` - show shell processes diff --git a/src/resources/extensions/gsd/quick.ts b/src/resources/extensions/gsd/quick.ts new file mode 100644 index 000000000..69bbc8ecc --- /dev/null +++ b/src/resources/extensions/gsd/quick.ts @@ -0,0 +1,156 @@ +/** + * GSD Quick Mode — /gsd quick + * Copyright (c) 2026 Jeremy McSpadden + * + * Lightweight task execution with GSD guarantees (atomic commits, state + * tracking) but without the full milestone/slice ceremony. + * + * Quick tasks live in `.gsd/quick/` and are tracked in STATE.md's + * "Quick Tasks Completed" table. + */ + +import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { existsSync, mkdirSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { loadPrompt } from "./prompt-loader.js"; +import { gsdRoot } from "./paths.js"; +import { GitServiceImpl, runGit } from "./git-service.js"; +import { loadEffectiveGSDPreferences } from "./preferences.js"; + +// ─── Quick Task Helpers ─────────────────────────────────────────────────────── + +/** + * Generate a URL-friendly slug from a description. + * Lowercase, hyphens, max 40 chars. + */ +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 40) + .replace(/-$/, ""); +} + +/** + * Determine the next quick task number by scanning existing directories. + */ +function getNextTaskNum(quickDir: string): number { + if (!existsSync(quickDir)) return 1; + try { + const entries = readdirSync(quickDir, { withFileTypes: true }); + let max = 0; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const match = entry.name.match(/^(\d+)-/); + if (match) { + const num = parseInt(match[1], 10); + if (num > max) max = num; + } + } + return max + 1; + } catch { + return 1; + } +} + +/** + * Ensure the quick task directory structure exists. + * Returns the task directory path. + */ +function ensureQuickDir(basePath: string, taskNum: number, slug: string): string { + const quickDir = join(gsdRoot(basePath), "quick"); + const taskDir = join(quickDir, `${taskNum}-${slug}`); + mkdirSync(taskDir, { recursive: true }); + return taskDir; +} + +// ─── Main Handler ───────────────────────────────────────────────────────────── + +export async function handleQuick( + args: string, + ctx: ExtensionCommandContext, + pi: ExtensionAPI, +): Promise { + const basePath = process.cwd(); + const root = gsdRoot(basePath); + + // Validate: .gsd/ must exist + if (!existsSync(root)) { + ctx.ui.notify( + "No .gsd/ directory found. Run /gsd to initialize a project first.", + "error", + ); + return; + } + + // Parse description from args + let description = args.trim(); + if (!description) { + ctx.ui.notify( + "Usage: /gsd quick \n\nExample: /gsd quick fix login button not responding on mobile", + "info", + ); + return; + } + + // Setup + const quickDir = join(root, "quick"); + const taskNum = getNextTaskNum(quickDir); + const slug = slugify(description); + const taskDir = ensureQuickDir(basePath, taskNum, slug); + const taskDirRel = `.gsd/quick/${taskNum}-${slug}`; + const date = new Date().toISOString().split("T")[0]; + + // Create git branch for the quick task + const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {}; + const git = new GitServiceImpl(basePath, gitPrefs); + const branchName = `gsd/quick/${taskNum}-${slug}`; + + let branchCreated = false; + try { + const current = git.getCurrentBranch(); + if (current !== branchName) { + // Auto-commit any dirty state before switching + try { + git.autoCommit("quick-task", `Q${taskNum}`, []); + } catch { /* nothing to commit — fine */ } + + runGit(basePath, ["checkout", "-b", branchName]); + branchCreated = true; + } + } catch (err) { + // Branch creation failed — continue on current branch + const message = err instanceof Error ? err.message : String(err); + ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning"); + } + + const actualBranch = branchCreated ? branchName : git.getCurrentBranch(); + + // Notify user + ctx.ui.notify( + `Quick task ${taskNum}: ${description}\nDirectory: ${taskDirRel}\nBranch: ${actualBranch}`, + "info", + ); + + // Build and dispatch the quick task prompt + const summaryPath = `${taskDirRel}/${taskNum}-SUMMARY.md`; + const prompt = loadPrompt("quick-task", { + description, + taskDir: taskDirRel, + branch: actualBranch, + summaryPath, + date, + taskNum: String(taskNum), + slug, + }); + + pi.sendMessage( + { + customType: "gsd-quick-task", + content: prompt, + display: false, + }, + { triggerTurn: true }, + ); +}