Harness spec: one path form, the file tools' texts, the list tree, skills
filesystem.md, spec 3.0.0-draft, and the CLI made to match:
- Paths: absolute in every answer except a result's bracket (context.md),
which keeps the call's path. glob and grep now give full paths.
- A missing file is `No such file or directory: {path}` for every tool
that needs it, with no hint after it: in a chat a hidden file must read
as a missing one. The CLI's "does not exist. Use write to create it." and
"Write it first." are gone; an edit of a directory is `Is a directory`,
glob over a file `Not a directory`.
- write/edit refusals, exactly: "{path} exists and has not been read in
this session. Read it first." and "{path} changed since you last read
it. Read it again before changing it." (no "on disk"), absolute path.
- A listing sorts names in code-point order; `list` with a depth is a
tree indented two spaces a level, marks as in a single listing, cut at
400 lines with a line saying so.
- Skills: where they are found and in what order on each side (the
session's own place first, the person's next, what is shared last), and
the name rule ^[a-z0-9][a-z0-9-]{0,63}$ that skill_manage checks. The
CLI no longer makes names with _ or .; such skills still load.
README's 3.0.0 notes say so; CHANGELOG [Unreleased].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
15 files changed
+167
-41
No files matched your search
+34
-2
@@ -235,7 +235,39 @@ describe("paths the way a model writes them", () => {
|
||||
expect(absPath("~/notes.md", c)).toBe(join(homedir(), "notes.md"))
|
||||
expect(absPath("~", c)).toBe(homedir())
|
||||
expect(absPath("a/~b", c)).toBe(join(c.root, "a/~b"))
|
||||
await expect(listTool.run({ path: "nope" }, c)).rejects.toThrow("nope does not exist")
|
||||
await expect(globTool.run({ pattern: "*", path: "nope" }, c)).rejects.toThrow("nope does not exist")
|
||||
await expect(listTool.run({ path: "nope" }, c)).rejects.toThrow(`No such file or directory: ${join(c.cwd, "nope")}`)
|
||||
await expect(globTool.run({ pattern: "*", path: "nope" }, c)).rejects.toThrow(`No such file or directory: ${join(c.cwd, "nope")}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("the file system's texts (harness filesystem.md, *Errors*, *A listing*)", () => {
|
||||
test("a missing file is the one text for every tool, with the absolute path; write and edit refuse in the spec's words", async () => {
|
||||
const { listTool, globTool, grepTool } = await import("../src/tool/search.ts")
|
||||
const { writeTool } = await import("../src/tool/write.ts")
|
||||
const c = ctx()
|
||||
const gone = join(c.root, "gone.md")
|
||||
for (const run of [() => readTool.run({ path: "gone.md" }, c), () => editTool.run({ path: "gone.md", old: "a", new: "b" }, c), () => multieditTool.run({ path: "gone.md", edits: [{ old: "a", new: "b" }] }, c), () => listTool.run({ path: "gone.md" }, c), () => globTool.run({ pattern: "*", path: "gone.md" }, c), () => grepTool.run({ pattern: "x", path: "gone.md" }, c)])
|
||||
await expect(run()).rejects.toThrow(`No such file or directory: ${gone}`)
|
||||
writeFileSync(join(c.root, "a.md"), "one\n")
|
||||
const a = join(c.root, "a.md")
|
||||
await expect(editTool.run({ path: "a.md", old: "one", new: "two" }, c)).rejects.toThrow(`${a} exists and has not been read in this session. Read it first.`)
|
||||
await expect(writeTool.run({ path: "a.md", content: "x" }, c)).rejects.toThrow(`${a} exists and has not been read in this session. Read it first.`)
|
||||
await readTool.run({ path: "a.md" }, c)
|
||||
c.fileStamps.set(a, 0)
|
||||
await expect(editTool.run({ path: "a.md", old: "one", new: "two" }, c)).rejects.toThrow(`${a} changed since you last read it. Read it again before changing it.`)
|
||||
await expect(editTool.run({ path: ".", old: "one", new: "two" }, c)).rejects.toThrow(`Is a directory: ${c.root}`)
|
||||
await expect(globTool.run({ pattern: "*", path: "a.md" }, c)).rejects.toThrow(`Not a directory: ${a}`)
|
||||
})
|
||||
|
||||
test("a listing: directories first, then names in code-point order; a tree two spaces a level; search results absolute", async () => {
|
||||
const { mkdirSync } = await import("node:fs")
|
||||
const { listTool, globTool } = await import("../src/tool/search.ts")
|
||||
const c = ctx()
|
||||
mkdirSync(join(c.root, "docs/guide"), { recursive: true })
|
||||
for (const f of ["b.md", "B.md", "a.md", "docs/index.md", "docs/guide/x.md"]) writeFileSync(join(c.root, f), "x\n")
|
||||
expect((await readTool.run({ path: "." }, c)).output).toBe("docs/\nB.md\na.md\nb.md")
|
||||
expect((await listTool.run({}, c)).output).toBe("docs/\n guide/\n index.md\nB.md\na.md\nb.md")
|
||||
expect((await listTool.run({ depth: 3 }, c)).output).toBe("docs/\n guide/\n x.md\n index.md\nB.md\na.md\nb.md")
|
||||
expect((await globTool.run({ pattern: "**/index.md" }, c)).output).toBe(join(c.root, "docs/index.md"))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user