feat: /gsd quick command & agent-instructions.md injection (#437)

* 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 <task>` — lightweight task execution with GSD guarantees
   (atomic commits, state tracking) without the full milestone ceremony.
   Creates `.gsd/quick/<num>-<slug>/` 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 <afromanguy@me.com>
This commit is contained in:
Jeremy McSpadden 2026-03-16 11:45:50 -05:00 committed by GitHub
parent 061d826a4e
commit 53edf284fa
5 changed files with 250 additions and 3 deletions

View file

@ -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;

View file

@ -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: {

View file

@ -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
- <concise list of changes>
## Files Modified
- <list of files>
## Verification
- <what was tested/verified>
```
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}} | <commit-hash> | [{{taskNum}}-{{slug}}](./quick/{{taskNum}}-{{slug}}/) |`
- Update the "Last activity" line
When done, say: "Quick task {{taskNum}} complete."

View file

@ -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 <task>` - 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

View file

@ -0,0 +1,156 @@
/**
* GSD Quick Mode /gsd quick <task>
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
*
* 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<void> {
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 <task description>\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 },
);
}