feat: add .gsd/KNOWLEDGE.md — persistent project-specific context (#585)
This commit is contained in:
parent
e5244658b3
commit
a9b14dc181
10 changed files with 399 additions and 13 deletions
|
|
@ -89,7 +89,7 @@ export async function inlineDependencySummaries(
|
|||
export async function inlineGsdRootFile(
|
||||
base: string, filename: string, label: string,
|
||||
): Promise<string | null> {
|
||||
const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS";
|
||||
const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS" | "KNOWLEDGE";
|
||||
const absPath = resolveGsdRootFile(base, key);
|
||||
if (!existsSync(absPath)) return null;
|
||||
return inlineFileOptional(absPath, relGsdRootFile(key), label);
|
||||
|
|
@ -377,6 +377,8 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
|
|||
if (requirementsInline) inlined.push(requirementsInline);
|
||||
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
||||
if (decisionsInline) inlined.push(decisionsInline);
|
||||
const knowledgeInlineRM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
|
||||
inlined.push(inlineTemplate("research", "Research"));
|
||||
|
||||
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
||||
|
|
@ -413,6 +415,8 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|||
if (requirementsInline) inlined.push(requirementsInline);
|
||||
const decisionsInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "decisions.md", "Decisions") : null;
|
||||
if (decisionsInline) inlined.push(decisionsInline);
|
||||
const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
|
||||
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
||||
if (inlineLevel === "full") {
|
||||
inlined.push(inlineTemplate("decisions", "Decisions"));
|
||||
|
|
@ -461,6 +465,8 @@ export async function buildResearchSlicePrompt(
|
|||
if (decisionsInline) inlined.push(decisionsInline);
|
||||
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
||||
if (requirementsInline) inlined.push(requirementsInline);
|
||||
const knowledgeInlineRS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
|
||||
inlined.push(inlineTemplate("research", "Research"));
|
||||
|
||||
const depContent = await inlineDependencySummaries(mid, sid, base);
|
||||
|
|
@ -504,6 +510,8 @@ export async function buildPlanSlicePrompt(
|
|||
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
||||
if (requirementsInline) inlined.push(requirementsInline);
|
||||
}
|
||||
const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
|
||||
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
||||
if (inlineLevel === "full") {
|
||||
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
||||
|
|
@ -578,11 +586,16 @@ export async function buildExecuteTaskPrompt(
|
|||
? priorSummaries.slice(-1)
|
||||
: priorSummaries;
|
||||
const carryForwardSection = await buildCarryForwardSection(effectivePriorSummaries, base);
|
||||
|
||||
// Inline project knowledge if available
|
||||
const knowledgeInlineET = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
|
||||
const inlinedTemplates = inlineLevel === "minimal"
|
||||
? inlineTemplate("task-summary", "Task Summary")
|
||||
: [
|
||||
inlineTemplate("task-summary", "Task Summary"),
|
||||
inlineTemplate("decisions", "Decisions"),
|
||||
...(knowledgeInlineET ? [knowledgeInlineET] : []),
|
||||
].join("\n\n---\n\n");
|
||||
|
||||
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
||||
|
|
@ -624,6 +637,8 @@ export async function buildCompleteSlicePrompt(
|
|||
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
||||
if (requirementsInline) inlined.push(requirementsInline);
|
||||
}
|
||||
const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlineCS) inlined.push(knowledgeInlineCS);
|
||||
|
||||
// Inline all task summaries for this slice
|
||||
const tDir = resolveTasksDir(base, mid, sid);
|
||||
|
|
@ -697,6 +712,8 @@ export async function buildCompleteMilestonePrompt(
|
|||
const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
|
||||
if (projectInline) inlined.push(projectInline);
|
||||
}
|
||||
const knowledgeInlineCM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlineCM) inlined.push(knowledgeInlineCM);
|
||||
// Inline milestone context file (milestone-level, not GSD root)
|
||||
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
||||
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
||||
|
|
@ -825,6 +842,8 @@ export async function buildReassessRoadmapPrompt(
|
|||
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
||||
if (decisionsInline) inlined.push(decisionsInline);
|
||||
}
|
||||
const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
||||
if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
|
||||
|
||||
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
loadEffectiveGSDPreferences,
|
||||
resolveAllSkillReferences,
|
||||
} from "./preferences.js";
|
||||
import { loadFile, saveFile, appendOverride } from "./files.js";
|
||||
import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js";
|
||||
import {
|
||||
formatDoctorIssuesForPrompt,
|
||||
formatDoctorReport,
|
||||
|
|
@ -58,12 +58,12 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("gsd", {
|
||||
description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer",
|
||||
description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer|knowledge",
|
||||
getArgumentCompletions: (prefix: string) => {
|
||||
const subcommands = [
|
||||
"next", "auto", "stop", "pause", "status", "queue", "discuss",
|
||||
"history", "undo", "skip", "export", "cleanup", "prefs",
|
||||
"config", "hooks", "doctor", "migrate", "remote", "steer",
|
||||
"config", "hooks", "doctor", "migrate", "remote", "steer", "knowledge",
|
||||
];
|
||||
const parts = prefix.trim().split(/\s+/);
|
||||
|
||||
|
|
@ -126,6 +126,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
.map((cmd) => ({ value: `cleanup ${cmd}`, label: cmd }));
|
||||
}
|
||||
|
||||
if (parts[0] === "knowledge" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["rule", "pattern", "lesson"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `knowledge ${cmd}`, label: cmd }));
|
||||
}
|
||||
|
||||
if (parts[0] === "doctor") {
|
||||
const modePrefix = parts[1] ?? "";
|
||||
const modes = ["fix", "heal", "audit"];
|
||||
|
|
@ -266,6 +273,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("knowledge ")) {
|
||||
await handleKnowledge(trimmed.replace(/^knowledge\s+/, "").trim(), ctx);
|
||||
return;
|
||||
}
|
||||
if (trimmed === "knowledge") {
|
||||
ctx.ui.notify("Usage: /gsd knowledge <rule|pattern|lesson> <description>. Example: /gsd knowledge rule Use real DB for integration tests", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
|
||||
const { handleMigrate } = await import("./migrate/command.js");
|
||||
await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
|
||||
|
|
@ -284,7 +300,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
}
|
||||
|
||||
ctx.ui.notify(
|
||||
`Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>.`,
|
||||
`Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>|knowledge <type> <entry>.`,
|
||||
"warning",
|
||||
);
|
||||
},
|
||||
|
|
@ -972,6 +988,35 @@ async function handleCleanupSnapshots(ctx: ExtensionCommandContext, basePath: st
|
|||
ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
|
||||
}
|
||||
|
||||
async function handleKnowledge(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
||||
const parts = args.split(/\s+/);
|
||||
const typeArg = parts[0]?.toLowerCase();
|
||||
|
||||
if (!typeArg || !["rule", "pattern", "lesson"].includes(typeArg)) {
|
||||
ctx.ui.notify(
|
||||
"Usage: /gsd knowledge <rule|pattern|lesson> <description>\nExample: /gsd knowledge rule Use real DB for integration tests",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const entryText = parts.slice(1).join(" ").trim();
|
||||
if (!entryText) {
|
||||
ctx.ui.notify(`Usage: /gsd knowledge ${typeArg} <description>`, "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const type = typeArg as "rule" | "pattern" | "lesson";
|
||||
const basePath = process.cwd();
|
||||
const state = await deriveState(basePath);
|
||||
const scope = state.activeMilestone?.id
|
||||
? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
|
||||
: "global";
|
||||
|
||||
await appendKnowledge(basePath, type, entryText, scope);
|
||||
ctx.ui.notify(`Added ${type} to KNOWLEDGE.md: "${entryText}"`, "success");
|
||||
}
|
||||
|
||||
async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
||||
const basePath = process.cwd();
|
||||
const state = await deriveState(basePath);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
|
||||
- `skill_rules`: situational rules with a human-readable `when` trigger and one or more of `use`, `prefer`, or `avoid`.
|
||||
|
||||
- `custom_instructions`: extra durable instructions related to skill use.
|
||||
- `custom_instructions`: extra durable instructions related to skill use. For operational project knowledge (recurring rules, gotchas, patterns), use `.gsd/KNOWLEDGE.md` instead — it's injected into every agent prompt automatically and agents can append to it during execution.
|
||||
|
||||
- `models`: per-stage model selection for auto-mode. Keys: `research`, `planning`, `execution`, `completion`. Values can be:
|
||||
- Simple string: `"claude-sonnet-4-6"` — single model, no fallbacks
|
||||
|
|
|
|||
|
|
@ -951,6 +951,128 @@ export async function appendOverride(basePath: string, change: string, appliedAt
|
|||
}
|
||||
}
|
||||
|
||||
export async function appendKnowledge(
|
||||
basePath: string,
|
||||
type: "rule" | "pattern" | "lesson",
|
||||
entry: string,
|
||||
scope: string,
|
||||
): Promise<void> {
|
||||
const knowledgePath = resolveGsdRootFile(basePath, "KNOWLEDGE");
|
||||
const existing = await loadFile(knowledgePath);
|
||||
|
||||
if (existing) {
|
||||
// Find the next ID for this type
|
||||
const prefix = type === "rule" ? "K" : type === "pattern" ? "P" : "L";
|
||||
const idPattern = new RegExp(`^\\| ${prefix}(\\d+)`, "gm");
|
||||
let maxId = 0;
|
||||
let match;
|
||||
while ((match = idPattern.exec(existing)) !== null) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxId) maxId = num;
|
||||
}
|
||||
const nextId = `${prefix}${String(maxId + 1).padStart(3, "0")}`;
|
||||
|
||||
// Build the table row
|
||||
let row: string;
|
||||
if (type === "rule") {
|
||||
row = `| ${nextId} | ${scope} | ${entry} | — | manual |`;
|
||||
} else if (type === "pattern") {
|
||||
row = `| ${nextId} | ${entry} | — | ${scope} |`;
|
||||
} else {
|
||||
row = `| ${nextId} | ${entry} | — | — | ${scope} |`;
|
||||
}
|
||||
|
||||
// Find the right section and append after the table header
|
||||
const sectionHeading = type === "rule" ? "## Rules" : type === "pattern" ? "## Patterns" : "## Lessons Learned";
|
||||
const sectionIdx = existing.indexOf(sectionHeading);
|
||||
if (sectionIdx !== -1) {
|
||||
// Find the end of the table header row (the |---|...| line)
|
||||
const afterHeading = existing.indexOf("\n", sectionIdx);
|
||||
// Find the next section or end
|
||||
const nextSection = existing.indexOf("\n## ", afterHeading + 1);
|
||||
const insertPoint = nextSection !== -1 ? nextSection : existing.length;
|
||||
|
||||
// Insert row before the next section (or at end)
|
||||
const before = existing.slice(0, insertPoint).trimEnd();
|
||||
const after = existing.slice(insertPoint);
|
||||
await saveFile(knowledgePath, before + "\n" + row + "\n" + after);
|
||||
} else {
|
||||
// Section not found — append at end
|
||||
await saveFile(knowledgePath, existing.trimEnd() + "\n\n" + row + "\n");
|
||||
}
|
||||
} else {
|
||||
// Create file from scratch with template header
|
||||
const header = [
|
||||
"# Project Knowledge",
|
||||
"",
|
||||
"Append-only register of project-specific rules, patterns, and lessons learned.",
|
||||
"Agents read this before every unit. Add entries when you discover something worth remembering.",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
let content: string;
|
||||
if (type === "rule") {
|
||||
content = header + [
|
||||
"## Rules",
|
||||
"",
|
||||
"| # | Scope | Rule | Why | Added |",
|
||||
"|---|-------|------|-----|-------|",
|
||||
`| K001 | ${scope} | ${entry} | — | manual |`,
|
||||
"",
|
||||
"## Patterns",
|
||||
"",
|
||||
"| # | Pattern | Where | Notes |",
|
||||
"|---|---------|-------|-------|",
|
||||
"",
|
||||
"## Lessons Learned",
|
||||
"",
|
||||
"| # | What Happened | Root Cause | Fix | Scope |",
|
||||
"|---|--------------|------------|-----|-------|",
|
||||
"",
|
||||
].join("\n");
|
||||
} else if (type === "pattern") {
|
||||
content = header + [
|
||||
"## Rules",
|
||||
"",
|
||||
"| # | Scope | Rule | Why | Added |",
|
||||
"|---|-------|------|-----|-------|",
|
||||
"",
|
||||
"## Patterns",
|
||||
"",
|
||||
"| # | Pattern | Where | Notes |",
|
||||
"|---|---------|-------|-------|",
|
||||
`| P001 | ${entry} | — | ${scope} |`,
|
||||
"",
|
||||
"## Lessons Learned",
|
||||
"",
|
||||
"| # | What Happened | Root Cause | Fix | Scope |",
|
||||
"|---|--------------|------------|-----|-------|",
|
||||
"",
|
||||
].join("\n");
|
||||
} else {
|
||||
content = header + [
|
||||
"## Rules",
|
||||
"",
|
||||
"| # | Scope | Rule | Why | Added |",
|
||||
"|---|-------|------|-----|-------|",
|
||||
"",
|
||||
"## Patterns",
|
||||
"",
|
||||
"| # | Pattern | Where | Notes |",
|
||||
"|---|---------|-------|-------|",
|
||||
"",
|
||||
"## Lessons Learned",
|
||||
"",
|
||||
"| # | What Happened | Root Cause | Fix | Scope |",
|
||||
"|---|--------------|------------|-----|-------|",
|
||||
`| L001 | ${entry} | — | — | ${scope} |`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
await saveFile(knowledgePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadActiveOverrides(basePath: string): Promise<Override[]> {
|
||||
const overridesPath = resolveGsdRootFile(basePath, "OVERRIDES");
|
||||
const content = await loadFile(overridesPath);
|
||||
|
|
|
|||
|
|
@ -47,10 +47,11 @@ import {
|
|||
resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
|
||||
relSliceFile, relSlicePath, relTaskFile,
|
||||
buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
|
||||
resolveGsdRootFile,
|
||||
} from "./paths.js";
|
||||
import { Key } from "@gsd/pi-tui";
|
||||
import { join } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { shortcutDesc } from "../shared/terminal.js";
|
||||
import { Text } from "@gsd/pi-tui";
|
||||
import { pauseAutoForProviderError } from "./provider-error-pause.js";
|
||||
|
|
@ -272,6 +273,20 @@ export default function (pi: ExtensionAPI) {
|
|||
}
|
||||
}
|
||||
|
||||
// Load project knowledge if available
|
||||
let knowledgeBlock = "";
|
||||
const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
|
||||
if (existsSync(knowledgePath)) {
|
||||
try {
|
||||
const content = readFileSync(knowledgePath, "utf-8").trim();
|
||||
if (content) {
|
||||
knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
|
||||
}
|
||||
} catch {
|
||||
// File read error — skip knowledge injection
|
||||
}
|
||||
}
|
||||
|
||||
// Detect skills installed during this auto-mode session
|
||||
let newSkillsBlock = "";
|
||||
if (hasSkillSnapshot()) {
|
||||
|
|
@ -307,7 +322,7 @@ export default function (pi: ExtensionAPI) {
|
|||
}
|
||||
|
||||
return {
|
||||
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
|
||||
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`,
|
||||
...(injection
|
||||
? {
|
||||
message: {
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ export const GSD_ROOT_FILES = {
|
|||
STATE: "STATE.md",
|
||||
REQUIREMENTS: "REQUIREMENTS.md",
|
||||
OVERRIDES: "OVERRIDES.md",
|
||||
KNOWLEDGE: "KNOWLEDGE.md",
|
||||
} as const;
|
||||
|
||||
export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
|
||||
|
|
@ -259,6 +260,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
|
|||
STATE: "state.md",
|
||||
REQUIREMENTS: "requirements.md",
|
||||
OVERRIDES: "overrides.md",
|
||||
KNOWLEDGE: "knowledge.md",
|
||||
};
|
||||
|
||||
export function gsdRoot(basePath: string): string {
|
||||
|
|
|
|||
|
|
@ -54,11 +54,12 @@ Then:
|
|||
- Don't fix symptoms. Understand *why* something fails before changing code. A test that passes after a change you don't understand is luck, not a fix.
|
||||
11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
|
||||
12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (use the **Decisions** output template from the inlined templates below if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
|
||||
13. Use the **Task Summary** output template from the inlined templates below
|
||||
14. Write `{{taskSummaryPath}}`
|
||||
15. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
||||
16. Do not commit manually — the system auto-commits your changes after this unit completes.
|
||||
17. Update `.gsd/STATE.md`
|
||||
13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
|
||||
14. Use the **Task Summary** output template from the inlined templates below
|
||||
15. Write `{{taskSummaryPath}}`
|
||||
16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
||||
17. Do not commit manually — the system auto-commits your changes after this unit completes.
|
||||
18. Update `.gsd/STATE.md`
|
||||
|
||||
All work stays in your working directory: `{{workingDirectory}}`.
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
|
|||
PROJECT.md (living doc - what the project is right now)
|
||||
REQUIREMENTS.md (requirement contract - tracks active/validated/deferred/out-of-scope)
|
||||
DECISIONS.md (append-only register of architectural and pattern decisions)
|
||||
KNOWLEDGE.md (append-only register of project-specific rules, patterns, and lessons learned)
|
||||
OVERRIDES.md (user-issued overrides that supersede plan content via /gsd steer)
|
||||
QUEUE.md (append-only log of queued milestones via /gsd queue)
|
||||
STATE.md
|
||||
|
|
@ -100,6 +101,7 @@ All auto-mode work happens inside a worktree at `.gsd/worktrees/<MID>/`. This is
|
|||
- **PROJECT.md** is a living document describing what the project is right now - current state only, updated at slice completion when stale
|
||||
- **REQUIREMENTS.md** tracks the requirement contract — requirements move between Active, Validated, Deferred, Blocked, and Out of Scope as slices prove or invalidate them. Update at slice completion when evidence supports a status change.
|
||||
- **DECISIONS.md** is an append-only register of architectural and pattern decisions - read it during planning/research, append to it during execution when a meaningful decision is made
|
||||
- **KNOWLEDGE.md** is an append-only register of project-specific rules, patterns, and lessons learned. Read it at the start of every unit. Append to it when you discover a recurring issue, a non-obvious pattern, or a rule that future agents should follow.
|
||||
- **CONTEXT.md** files (milestone or slice level) capture the brief — scope, goals, constraints, and key decisions from discussion. When present, they are the authoritative source for what a milestone or slice is trying to achieve. Read them before planning or executing.
|
||||
- **Milestones** are major project phases (M001, M002, ...)
|
||||
- **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
|
||||
|
|
|
|||
19
src/resources/extensions/gsd/templates/knowledge.md
Normal file
19
src/resources/extensions/gsd/templates/knowledge.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Project Knowledge
|
||||
|
||||
Append-only register of project-specific rules, patterns, and lessons learned.
|
||||
Agents read this before every unit. Add entries when you discover something worth remembering.
|
||||
|
||||
## Rules
|
||||
|
||||
| # | Scope | Rule | Why | Added |
|
||||
|---|-------|------|-----|-------|
|
||||
|
||||
## Patterns
|
||||
|
||||
| # | Pattern | Where | Notes |
|
||||
|---|---------|-------|-------|
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
| # | What Happened | Root Cause | Fix | Scope |
|
||||
|---|--------------|------------|-----|-------|
|
||||
161
src/resources/extensions/gsd/tests/knowledge.test.ts
Normal file
161
src/resources/extensions/gsd/tests/knowledge.test.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Unit tests for KNOWLEDGE.md integration.
|
||||
*
|
||||
* Tests:
|
||||
* - KNOWLEDGE is registered in GSD_ROOT_FILES
|
||||
* - resolveGsdRootFile resolves KNOWLEDGE paths correctly
|
||||
* - inlineGsdRootFile works with the KNOWLEDGE key
|
||||
* - before_agent_start hook includes/omits knowledge block appropriately
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
|
||||
import { inlineGsdRootFile } from '../auto-prompts.ts';
|
||||
import { appendKnowledge } from '../files.ts';
|
||||
|
||||
// ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
|
||||
|
||||
test('knowledge: KNOWLEDGE key exists in GSD_ROOT_FILES', () => {
|
||||
assert.ok('KNOWLEDGE' in GSD_ROOT_FILES, 'GSD_ROOT_FILES should have KNOWLEDGE key');
|
||||
assert.strictEqual(GSD_ROOT_FILES.KNOWLEDGE, 'KNOWLEDGE.md');
|
||||
});
|
||||
|
||||
// ─── resolveGsdRootFile resolves KNOWLEDGE.md ───────────────────────────────
|
||||
|
||||
test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exists', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n');
|
||||
|
||||
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
||||
assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
writeFileSync(join(gsdDir, 'knowledge.md'), '# Project Knowledge\n');
|
||||
|
||||
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
||||
// On case-insensitive filesystems (macOS), canonical path matches;
|
||||
// on case-sensitive (Linux), legacy path matches. Either is valid.
|
||||
const canonical = join(gsdDir, 'KNOWLEDGE.md');
|
||||
const legacy = join(gsdDir, 'knowledge.md');
|
||||
assert.ok(
|
||||
resolved === canonical || resolved === legacy,
|
||||
`resolved path should be canonical or legacy, got: ${resolved}`,
|
||||
);
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: resolveGsdRootFile returns canonical path when file does not exist', () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
const resolved = resolveGsdRootFile(tmp, 'KNOWLEDGE');
|
||||
assert.strictEqual(resolved, join(gsdDir, 'KNOWLEDGE.md'));
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── inlineGsdRootFile works with knowledge.md ─────────────────────────────
|
||||
|
||||
test('knowledge: inlineGsdRootFile returns content when KNOWLEDGE.md exists', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n\n## Rules\n\nK001: Use real DB');
|
||||
|
||||
const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
|
||||
assert.ok(result !== null, 'should return content');
|
||||
assert.ok(result!.includes('Project Knowledge'), 'should include label');
|
||||
assert.ok(result!.includes('K001'), 'should include knowledge content');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: inlineGsdRootFile returns null when KNOWLEDGE.md does not exist', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
const result = await inlineGsdRootFile(tmp, 'knowledge.md', 'Project Knowledge');
|
||||
assert.strictEqual(result, null, 'should return null when file does not exist');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── appendKnowledge creates file and appends entries ──────────────────────
|
||||
|
||||
test('knowledge: appendKnowledge creates KNOWLEDGE.md with rule when file does not exist', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
await appendKnowledge(tmp, 'rule', 'Use real DB for integration tests', 'M001/S01');
|
||||
|
||||
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
||||
assert.ok(content.includes('# Project Knowledge'), 'should have header');
|
||||
assert.ok(content.includes('K001'), 'should have K001 id');
|
||||
assert.ok(content.includes('Use real DB for integration tests'), 'should have rule text');
|
||||
assert.ok(content.includes('M001/S01'), 'should have scope');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: appendKnowledge appends to existing KNOWLEDGE.md with auto-incrementing ID', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
// Create initial file with one rule
|
||||
await appendKnowledge(tmp, 'rule', 'First rule', 'M001');
|
||||
// Add second rule
|
||||
await appendKnowledge(tmp, 'rule', 'Second rule', 'M001/S02');
|
||||
|
||||
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
||||
assert.ok(content.includes('K001'), 'should have K001');
|
||||
assert.ok(content.includes('K002'), 'should have K002');
|
||||
assert.ok(content.includes('First rule'), 'should have first rule');
|
||||
assert.ok(content.includes('Second rule'), 'should have second rule');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: appendKnowledge handles pattern type', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
await appendKnowledge(tmp, 'pattern', 'Middleware chain for auth', 'M001');
|
||||
|
||||
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
||||
assert.ok(content.includes('P001'), 'should have P001 id');
|
||||
assert.ok(content.includes('Middleware chain for auth'), 'should have pattern text');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('knowledge: appendKnowledge handles lesson type', async () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
|
||||
const gsdDir = join(tmp, '.gsd');
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
await appendKnowledge(tmp, 'lesson', 'API timeout on large payloads', 'M002');
|
||||
|
||||
const content = readFileSync(join(gsdDir, 'KNOWLEDGE.md'), 'utf-8');
|
||||
assert.ok(content.includes('L001'), 'should have L001 id');
|
||||
assert.ok(content.includes('API timeout on large payloads'), 'should have lesson text');
|
||||
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue