Skip to content

Commit e2ce73d

Browse files
committed
feat: auto-load OpenSpec specs into compile/recompile/review prompts
When openspec/ directory exists, forge prompt commands automatically inject OpenSpec behavioral requirements as additional context: - Reads openspec/project.md (global context) - Reads all openspec/specs/*/spec.md (capability specs) - Injected as "Behavioral Requirements (OpenSpec)" section - No dependency on OpenSpec package — just reads markdown files
1 parent 981a8eb commit e2ce73d

5 files changed

Lines changed: 71 additions & 1 deletion

File tree

packages/cli/resolve.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import { readFile } from "node:fs/promises";
44
import path from "node:path";
5-
import { readGraphDocs, readNodeDocs, readNodeRefs, readGraphRefs } from "../core/index.js";
5+
import {
6+
readGraphDocs,
7+
readNodeDocs,
8+
readNodeRefs,
9+
readGraphRefs,
10+
readOpenSpecContext,
11+
} from "../core/index.js";
612
import type {
713
CheckInput,
814
CompileTask,
@@ -27,6 +33,7 @@ export function createResolver(root: string): ContextResolver {
2733
l1Files?: FileContent[];
2834
docs?: string;
2935
refs?: RefFile[];
36+
openspec?: string;
3037
} = {};
3138

3239
for (const ref of task.context) {
@@ -66,6 +73,9 @@ export function createResolver(root: string): ContextResolver {
6673
ctx.docs = (await readGraphDocs(root, l4Ref.id)) ?? undefined;
6774
ctx.refs = await readGraphRefs(root, l4Ref.id);
6875
}
76+
77+
// Auto-load OpenSpec behavioral requirements if openspec/ exists
78+
ctx.openspec = (await readOpenSpecContext(root)) ?? undefined;
6979
}
7080

7181
return ctx;

packages/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export {
4646
readL2Docs,
4747
readNodeRefs,
4848
readGraphRefs,
49+
readOpenSpecContext,
4950
} from "./store.js";
5051
export type { CheckIssue, CheckReport, CheckInput, IssueSeverity } from "./check.js";
5152
export { check } from "./check.js";

packages/core/skill.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ResolvedContext {
2121
readonly l1Files?: readonly FileContent[];
2222
readonly docs?: string;
2323
readonly refs?: readonly RefFile[];
24+
readonly openspec?: string;
2425
}
2526

2627
/** L1 源文件内容 */

packages/core/store.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,49 @@ async function readRefsDir(root: string, relDir: string): Promise<RefFile[]> {
186186
return refs;
187187
}
188188

189+
// ── OpenSpec integration ──
190+
191+
/** Read OpenSpec context if openspec/ exists. Returns null if not present. */
192+
export async function readOpenSpecContext(root: string): Promise<string | null> {
193+
const openspecDir = path.join(root, "openspec");
194+
try {
195+
await readdir(openspecDir);
196+
} catch {
197+
return null;
198+
}
199+
200+
const parts: string[] = [];
201+
202+
// Read project.md (global context)
203+
try {
204+
const projectMd = await readFile(path.join(openspecDir, "project.md"), "utf8");
205+
parts.push("### Project Context\n\n" + projectMd);
206+
} catch {
207+
// no project.md
208+
}
209+
210+
// Read all specs/*/spec.md (behavioral requirements)
211+
const specsDir = path.join(openspecDir, "specs");
212+
let capabilities: string[];
213+
try {
214+
capabilities = await readdir(specsDir);
215+
} catch {
216+
capabilities = [];
217+
}
218+
219+
for (const cap of capabilities.toSorted()) {
220+
try {
221+
const specPath = path.join(specsDir, cap, "spec.md");
222+
const content = await readFile(specPath, "utf8");
223+
parts.push(`### ${cap}\n\n${content}`);
224+
} catch {
225+
// skip non-directories or missing spec.md
226+
}
227+
}
228+
229+
return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
230+
}
231+
189232
// ── Docs ──
190233

191234
/** 读取节点的模块化文档 nodes/<nodeId>/docs.md,不存在返回 null */

packages/skills/prompt-builder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ function buildCompileInput(input: SkillInput): string[] {
192192
parts.push("", "### Reference Materials", "", formatRefs(resolved.refs));
193193
}
194194

195+
// OpenSpec 行为需求
196+
if (resolved.openspec !== undefined) {
197+
parts.push("", "### Behavioral Requirements (OpenSpec)", "", resolved.openspec);
198+
}
199+
195200
return parts;
196201
}
197202

@@ -230,6 +235,11 @@ function buildRecompileInput(input: SkillInput): string[] {
230235
parts.push("", "### Reference Materials", "", formatRefs(resolved.refs));
231236
}
232237

238+
// OpenSpec 行为需求
239+
if (resolved.openspec !== undefined) {
240+
parts.push("", "### Behavioral Requirements (OpenSpec)", "", resolved.openspec);
241+
}
242+
233243
return parts;
234244
}
235245

@@ -268,6 +278,11 @@ function buildReviewInput(input: SkillInput): string[] {
268278
parts.push("", "### Reference Materials", "", formatRefs(resolved.refs));
269279
}
270280

281+
// OpenSpec 行为需求
282+
if (resolved.openspec !== undefined) {
283+
parts.push("", "### Behavioral Requirements (OpenSpec)", "", resolved.openspec);
284+
}
285+
271286
return parts;
272287
}
273288

0 commit comments

Comments
 (0)