Instruction files stack from the project root down to the working directory
instructionChain (spec 3.0.0, filesystem.md): from the boundary — the project root — down to the start directory, outermost first, each directory gives the first of AGENTS.md, CLAUDE.md that is a file really inside the project; one that is not (a link out) is passed over and the walk goes on below it. Before, only the nearest file counted. The chain's files are named to the model by their path in the project; agentsBlock reads them all: none means "make one", and git is settled when any of them settles it. Conformance runner for the `instructions` area (all 9 cases pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+127
-28
No files matched your search
@@ -1,11 +1,12 @@
|
||||
// config.yaml `instructions`: more files for the system prompt.
|
||||
// Instruction files: the stacked chain from the project root to the working directory (spec 3.0.0),
|
||||
// and config.yaml `instruction_files`, more files for the system prompt.
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs"
|
||||
import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { loadConfig } from "../src/config/load.ts"
|
||||
import { paths } from "../src/config/paths.ts"
|
||||
import { instructionFiles } from "../src/prompt/assemble.ts"
|
||||
import { assembleSystem, instructionFiles } from "../src/prompt/assemble.ts"
|
||||
|
||||
test("global entries may be anywhere; a project's only inside the project, links followed", () => {
|
||||
const outside = mkdtempSync(join(tmpdir(), "ph-outside-"))
|
||||
@@ -31,3 +32,49 @@ test("global entries may be anywhere; a project's only inside the project, links
|
||||
expect(files.map((f) => f.text)).toEqual(["Write tersely.", "Tabs, not spaces."])
|
||||
writeFileSync(join(paths.config, "config.yaml"), "")
|
||||
})
|
||||
|
||||
// Spec 3.0.0: instruction files stack from the project root down to the working directory,
|
||||
// outermost first, one per directory (AGENTS.md before CLAUDE.md); before, only the nearest counted.
|
||||
test("instruction files stack from the root down to the working directory, outermost first", () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), "ph-stack-")))
|
||||
const outside = mkdtempSync(join(tmpdir(), "ph-stack-out-"))
|
||||
writeFileSync(join(outside, "AGENTS.md"), "NOT THE PROJECT'S")
|
||||
mkdirSync(join(root, "pkg", "src", "deep"), { recursive: true })
|
||||
writeFileSync(join(root, "AGENTS.md"), "root rules")
|
||||
writeFileSync(join(root, "CLAUDE.md"), "never both")
|
||||
writeFileSync(join(root, "pkg", "CLAUDE.md"), "package rules\n## Git\ncommit freely")
|
||||
// A link out of the project is no instruction file: the walk goes on below it.
|
||||
symlinkSync(join(outside, "AGENTS.md"), join(root, "pkg", "src", "AGENTS.md"))
|
||||
writeFileSync(join(root, "pkg", "src", "deep", "AGENTS.md"), "deep rules")
|
||||
// Below the working directory: not in the chain (the read tool hands it over when reached).
|
||||
mkdirSync(join(root, "pkg", "src", "deep", "below"))
|
||||
writeFileSync(join(root, "pkg", "src", "deep", "below", "AGENTS.md"), "below")
|
||||
writeFileSync(join(paths.config, "config.yaml"), "")
|
||||
const files = instructionFiles(join(root, "pkg", "src", "deep"), root)
|
||||
expect(files.filter((f) => f.chain).map((f) => [f.path.slice(root.length + 1), f.text])).toEqual([
|
||||
["AGENTS.md", "root rules"],
|
||||
["pkg/CLAUDE.md", "package rules\n## Git\ncommit freely"],
|
||||
["pkg/src/deep/AGENTS.md", "deep rules"],
|
||||
])
|
||||
// A working directory outside the root is read as the root.
|
||||
expect(instructionFiles(outside, root).filter((f) => f.chain).map((f) => f.path)).toEqual([join(root, "AGENTS.md")])
|
||||
// In the prompt: each by its path in the project, outermost first; git is settled by any of them.
|
||||
const system = assembleSystem({ modelRef: "s/m", family: "local", cwd: join(root, "pkg", "src", "deep"), root, isGit: true, mode: "manual", planDir: join(root, ".agent", "plans"), toolNames: [] })
|
||||
const at = (s: string) => system.indexOf(s)
|
||||
expect(at("Instructions from AGENTS.md:")).toBeGreaterThan(-1)
|
||||
expect(at("Instructions from AGENTS.md:")).toBeLessThan(at("Instructions from pkg/CLAUDE.md:"))
|
||||
expect(at("Instructions from pkg/CLAUDE.md:")).toBeLessThan(at("Instructions from pkg/src/deep/AGENTS.md:"))
|
||||
expect(system).not.toContain("NOT THE PROJECT'S")
|
||||
expect(system).not.toContain("never both")
|
||||
expect(system).toContain("## Keeping AGENTS.md")
|
||||
expect(system).not.toContain("## Git in this project")
|
||||
expect(system).not.toContain("This project has no AGENTS.md yet")
|
||||
})
|
||||
|
||||
test("no instruction file anywhere on the way: the project is told to make one", () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), "ph-stack-none-")))
|
||||
mkdirSync(join(root, "a"))
|
||||
writeFileSync(join(paths.config, "config.yaml"), "")
|
||||
const system = assembleSystem({ modelRef: "s/m", family: "local", cwd: join(root, "a"), root, isGit: false, mode: "manual", planDir: join(root, ".agent", "plans"), toolNames: [] })
|
||||
expect(system).toContain(`It goes at the project root: ${root}/AGENTS.md.`)
|
||||
})
|
||||
Reference in new issue
Block a user