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:
parent
061d826a4e
commit
53edf284fa
5 changed files with 250 additions and 3 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
48
src/resources/extensions/gsd/prompts/quick-task.md
Normal file
48
src/resources/extensions/gsd/prompts/quick-task.md
Normal 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."
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
156
src/resources/extensions/gsd/quick.ts
Normal file
156
src/resources/extensions/gsd/quick.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue