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:
HomerandClaude Opus 5.5 committed 2026-10-28 01:20:42 +00:00
1 parent 4ca6af12b1
commit b1dcaded72
15 files changed
+167 -41

No files matched your search

+34 -2
View File
@@ -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"))
})
})