From cb4d50ad281d784aee9bb548dd8f922c215d0914 Mon Sep 17 00:00:00 2001 From: Avisek Date: Thu, 11 Jun 2026 21:53:23 +0530 Subject: [PATCH] feat: support multiple instruction files and add PR template guidelines to agent prompts --- packages/opencode/src/session/instruction.ts | 53 +++++++++-------- .../opencode/src/session/prompt/anthropic.txt | 5 ++ .../opencode/src/session/prompt/default.txt | 2 + .../opencode/test/session/instruction.test.ts | 58 +++++++++++++++++++ 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 38ac55bbb64d..a813a5c25a6c 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -35,7 +35,7 @@ export interface Interface { readonly clear: (messageID: MessageID) => Effect.Effect readonly systemPaths: () => Effect.Effect, FSUtil.Error> readonly system: () => Effect.Effect - readonly find: (dir: string) => Effect.Effect + readonly find: (dir: string) => Effect.Effect readonly resolve: ( messages: SessionV1.WithParts[], filepath: string, @@ -65,6 +65,11 @@ export const layer: Layer.Layer< "AGENTS.md", ...(!flags.disableClaudeCodePrompt ? ["CLAUDE.md"] : []), "CONTEXT.md", // deprecated + "CONTRIBUTING.md", + ".github/pull_request_template.md", + ".github/PULL_REQUEST_TEMPLATE.md", + "pull_request_template.md", + "PULL_REQUEST_TEMPLATE.md", ] const state = yield* InstanceState.make( @@ -126,8 +131,7 @@ export const layer: Layer.Layer< .findUp(file, ctx.directory, ctx.worktree) .pipe(Effect.catch(() => Effect.succeed([]))) if (matches.length > 0) { - matches.forEach((item) => paths.add(path.resolve(item))) - break + paths.add(path.resolve(matches[0])) } } } @@ -169,11 +173,14 @@ export const layer: Layer.Layer< }) const find = Effect.fn("Instruction.find")(function* (dir: string) { + const results: string[] = [] for (const file of instructionFiles) { const filepath = path.resolve(path.join(dir, file)) - if (yield* fs.existsSafe(filepath)) return filepath + if (yield* fs.existsSafe(filepath)) { + results.push(filepath) + } } - return undefined + return results }) const resolve = Effect.fn("Instruction.resolve")(function* ( @@ -192,26 +199,26 @@ export const layer: Layer.Layer< // Walk upward from the file being read and attach nearby instruction files once per message. while (current.startsWith(root) && current !== root) { - const found = yield* find(current) - if (!found || found === target || sys.has(found) || already.has(found)) { - current = path.dirname(current) - continue - } + const foundList = yield* find(current) + for (const found of foundList) { + if (found === target || sys.has(found) || already.has(found)) { + continue + } - let set = s.claims.get(messageID) - if (!set) { - set = new Set() - s.claims.set(messageID, set) - } - if (set.has(found)) { - current = path.dirname(current) - continue - } + let set = s.claims.get(messageID) + if (!set) { + set = new Set() + s.claims.set(messageID, set) + } + if (set.has(found)) { + continue + } - set.add(found) - const content = yield* read(found) - if (content) { - results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` }) + set.add(found) + const content = yield* read(found) + if (content) { + results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` }) + } } current = path.dirname(current) diff --git a/packages/opencode/src/session/prompt/anthropic.txt b/packages/opencode/src/session/prompt/anthropic.txt index 21d9c0e9f216..d1c82e0109ad 100644 --- a/packages/opencode/src/session/prompt/anthropic.txt +++ b/packages/opencode/src/session/prompt/anthropic.txt @@ -95,6 +95,11 @@ assistant: [Uses the Task tool] IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation. +# Pull Requests and Conventions + +- Before creating a pull request, you MUST read and follow the repository's pull request templates (e.g., `.github/pull_request_template.md` or `pull_request_template.md`) and contributing guidelines (e.g., `CONTRIBUTING.md`). Ensure the PR description follows the required format and answers all template questions. +- If a repository bot flags a PR or enforces rules with a deadline (e.g., a 2-hour edit/response window), prioritize honoring that deadline and edit the PR description or code immediately to resolve the issue. + # Code References When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location. diff --git a/packages/opencode/src/session/prompt/default.txt b/packages/opencode/src/session/prompt/default.txt index c8d904665e2d..45e4a8700763 100644 --- a/packages/opencode/src/session/prompt/default.txt +++ b/packages/opencode/src/session/prompt/default.txt @@ -63,6 +63,8 @@ When making changes to files, first understand the file's code conventions. Mimi - When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions. - When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic. - Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository. +- Before creating a pull request, you MUST read and follow the repository's pull request templates (e.g., `.github/pull_request_template.md` or `pull_request_template.md`) and contributing guidelines (e.g., `CONTRIBUTING.md`). Ensure the PR description follows the required format and answers all template questions. +- If a repository bot flags a PR or enforces rules with a deadline (e.g., a 2-hour edit/response window), prioritize honoring that deadline and edit the PR description or code immediately to resolve the issue. # Code style - IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index 53ccf06e120a..f783784a2a4b 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -199,6 +199,41 @@ describe("Instruction.resolve", () => { ) test.todo("fetches remote instructions from config URLs via HttpClient", () => {}) + + it.live("find returns all existing instruction files in the directory", () => + withFiles({ "AGENTS.md": "# Agents", "CONTRIBUTING.md": "# Contributing" }, (dir) => + Effect.gen(function* () { + const svc = yield* Instruction.Service + const files = yield* svc.find(dir) + expect(files).toHaveLength(2) + expect(files).toContain(path.join(dir, "AGENTS.md")) + expect(files).toContain(path.join(dir, "CONTRIBUTING.md")) + }), + ), + ) + + it.live("resolve returns multiple instruction files from subdirectory", () => + withFiles( + { + "subdir/AGENTS.md": "# Agents", + "subdir/CONTRIBUTING.md": "# Contributing", + "subdir/nested/file.ts": "const x = 1", + }, + (dir) => + Effect.gen(function* () { + const svc = yield* Instruction.Service + const results = yield* svc.resolve( + [], + path.join(dir, "subdir", "nested", "file.ts"), + MessageID.make("msg_message-test-multiresolve"), + ) + expect(results.length).toBe(2) + const paths = results.map((r) => r.filepath) + expect(paths).toContain(path.join(dir, "subdir", "AGENTS.md")) + expect(paths).toContain(path.join(dir, "subdir", "CONTRIBUTING.md")) + }), + ), + ) }) describe("Instruction.system", () => { @@ -221,6 +256,29 @@ describe("Instruction.system", () => { }), ) + it.live("loads multiple project instruction files (e.g. AGENTS.md and CONTRIBUTING.md) when both exist", () => + Effect.gen(function* () { + const globalTmp = yield* tmpdirScoped() + const projectTmp = yield* tmpWithFiles({ + "AGENTS.md": "# Project Agents", + "CONTRIBUTING.md": "# Project Contributing", + }) + + yield* Effect.gen(function* () { + const svc = yield* Instruction.Service + const paths = yield* svc.systemPaths() + expect(paths.has(path.join(projectTmp, "AGENTS.md"))).toBe(true) + expect(paths.has(path.join(projectTmp, "CONTRIBUTING.md"))).toBe(true) + + const rules = yield* svc.system() + expect(rules).toHaveLength(2) + const ruleContents = rules.join("\n") + expect(ruleContents).toContain("# Project Agents") + expect(ruleContents).toContain("# Project Contributing") + }).pipe(provideInstance(projectTmp), provideInstruction({ home: globalTmp, config: globalTmp })) + }), + ) + it.live("skips project and global CLAUDE.md when Claude Code prompt is disabled", () => Effect.gen(function* () { const globalTmp = yield* tmpWithFiles({ ".claude/CLAUDE.md": "# Global Claude" })