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
@@ -45,6 +45,10 @@ describe("skills", () => {
|
||||
rmSync(join(paths.config, "skills"), { recursive: true, force: true })
|
||||
const c = ctx()
|
||||
await expect(skillManageTool.run({ operations: [{ action: "create", name: "Bad Name", content: "x" }] }, c)).rejects.toThrow("not a valid skill name")
|
||||
// The spec's rule (filesystem.md, *Skills*): letters, digits and - only, up to 64.
|
||||
await expect(skillManageTool.run({ operations: [{ action: "create", name: "snake_case", content: "x" }] }, c)).rejects.toThrow("not a valid skill name")
|
||||
await expect(skillManageTool.run({ operations: [{ action: "create", name: "v1.2", content: "x" }] }, c)).rejects.toThrow("not a valid skill name")
|
||||
await expect(skillManageTool.run({ operations: [{ action: "create", name: "a".repeat(65), content: "x" }] }, c)).rejects.toThrow("not a valid skill name")
|
||||
await expect(skillManageTool.run({ operations: [{ action: "create", name: "a", content: skill("b", "wrong name") }] }, c)).rejects.toThrow('must be "a"')
|
||||
const long = "Use when the build breaks on the CI runner and nobody knows why it happens."
|
||||
const r = await skillManageTool.run({ operations: [{ action: "create", name: "ci", category: "devops", content: skill("ci", long) }, { action: "write_file", name: "ci", file_path: "references/runner.md", file_content: "runner notes" }] }, c)
|
||||
|
||||
@@ -45,7 +45,7 @@ test("an unchanged re-read is a stub; after an edit the file is read in full; a
|
||||
expect(r[0]).toMatch(/^\[#0001 a\.txt lines 1-3 of 3 sha ([0-9a-f]{8})\]\n1: one\n2: two\n3: three$/)
|
||||
const sha = /sha ([0-9a-f]{8})/.exec(r[0]!)![1]
|
||||
expect(r[1]).toBe(`[#0002 a.txt lines 2-2 of 3 sha ${sha}] Unchanged since #0001, which already shows lines 1-3.`)
|
||||
expect(r[2]).toBe("[#0003]\na.txt")
|
||||
expect(r[2]).toBe(`[#0003]\n${join(dir, "a.txt")}`)
|
||||
expect(r[3]).toBe("[#0004] Same result as #0003.")
|
||||
expect(r[4]).toMatch(/^\[#0005 a\.txt sha [0-9a-f]{8}\]\n@@ -1,3 \+1,3 @@\n one\n-two\n\+2\n three$/)
|
||||
expect(r[5]).toMatch(/^\[#0006 a\.txt lines 1-3 of 3 sha [0-9a-f]{8}\]\n1: one\n2: 2\n3: three$/)
|
||||
|
||||
+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