Harness spec 3.0.0-draft: where each tool is offered; the chat's tools

Every tool says its `sessions` (chat, agent or both). The file tools read, write,
edit, glob, grep and list are shared; mkdir, mv, rm, search, python and the forge
verbs (commit, branch, pr, merge, tag, release) are the chat's; show and recall
are new and shared. New fields: `requires` (page, display, forge) with the exact
`unavailable` answer, and `confirm: always` for merge, tag and release.
knowledge_*, notes_search, note_view and note_manage leave the chat. Default
rules for the new tools; modes.json says what `confirm` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-27 18:52:24 +00:00
1 parent a03d46cf74
commit 253ef4a8a0
50 files changed
+568 -20

No files matched your search

+11 -1
View File
@@ -11,6 +11,10 @@
"glob": "allow",
"grep": "allow",
"list": "allow",
"search": "allow",
"recall": "allow",
"show": "allow",
"python": "allow",
"ask_user": "allow",
"plan_submit": "allow",
"task": "allow",
@@ -74,6 +78,12 @@
"tree *-o*": "ask",
"tree *-R*": "ask",
"file *-C*": "ask"
}
},
"commit": "ask",
"branch": "ask",
"pr": "ask",
"merge": "ask",
"tag": "ask",
"release": "ask"
}
}
+1
View File
@@ -4,6 +4,7 @@
"order_about": "Loosest first. A project or a subagent may make the mode stricter, never looser; switching to a looser mode is always asked.",
"aliases": { "unrestricted": "auto" },
"floor": "The hardline (hardline.json) and the protected paths are refused in every mode, auto included.",
"confirm": "Spec 3.0.0: a tool with `confirm: always` (merge, tag, release) is put to the person before it runs, in every mode, auto included, as an approval card naming the call and what it reaches; it runs on a yes only. No rule allows it and no approval is remembered (an approval card offers once, never always). With nobody to answer, it is refused, as unattended says.",
"modes": {
"manual": {
"summary": "Everything not already allowed is asked.",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "apply_patch",
"scope": "cli",
"sessions": "agent",
"risk": "write",
"exclusive": false,
"block": "edit",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "ask_user",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": true,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "bash",
"scope": "cli",
"sessions": "agent",
"risk": "execute",
"exclusive": false,
"block": "shell",
+39
View File
@@ -0,0 +1,39 @@
{
"name": "branch",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"unavailable": "No repository from a forge is open to this session, so there is nothing to branch here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Create a branch in a repository (a mount), from `from` (default: the branch this session works on), and switch this session's working copy of it to the new branch. Staged changes come along.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"name": {
"type": "string",
"description": "The new branch's name"
},
"from": {
"type": "string",
"description": "Branch, tag or commit to start from"
}
},
"required": [
"path",
"name"
]
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "commit",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"unavailable": "No repository from a forge is open to this session, so there is nothing to commit here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Send the staged changes of a repository (a mount) to the forge as one commit on the branch this session works on, under the user's account. Every change this session made in that repository since its last commit goes in, unless `paths` names some of them.\n\n- Refused when the branch has moved on the forge since this session's working copy was based on it: the answer shows what changed there. Look at it, then commit again.\n- Write the message as the repository's history does: a short subject line, then why, where it is not obvious.\n- Needs commit access to the repository in this session. With stage access the changes stay staged, for the user to commit.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"message": {
"type": "string",
"description": "The commit message"
},
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Only these changed paths (default: every staged change)"
}
},
"required": [
"path",
"message"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "decisions",
"scope": "cli",
"sessions": "agent",
"risk": "interact",
"exclusive": false,
"block": "tool",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "edit",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "write",
"exclusive": false,
"block": "edit",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "glob",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "search",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "grep",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "search",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "job_list",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "job_output",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "job_send",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "job_stop",
"scope": "shared",
"sessions": "both",
"risk": "execute",
"exclusive": false,
"block": "tool",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "knowledge_get",
"scope": "shared",
"scope": "cli",
"sessions": "agent",
"risk": "read",
"exclusive": false,
"block": "read",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "knowledge_search",
"scope": "shared",
"scope": "cli",
"sessions": "agent",
"risk": "read",
"exclusive": false,
"block": "search",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "list",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "search",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "memory",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "tool",
+46
View File
@@ -0,0 +1,46 @@
{
"name": "merge",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": true,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"confirm": "always",
"unavailable": "No repository from a forge is open to this session, so there is nothing to merge here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Merge a pull request on the forge. The user is always asked first, in every mode, and shown the pull request: nothing is merged without their yes.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"number": {
"type": "integer",
"minimum": 1,
"description": "The pull request's number"
},
"method": {
"type": "string",
"enum": [
"merge",
"squash",
"rebase"
],
"description": "How to merge (default: the repository's setting)"
}
},
"required": [
"path",
"number"
]
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "mkdir",
"scope": "llembas",
"sessions": "chat",
"risk": "write",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Make a directory, and any missing directories above it. Making one that already exists is not an error.\n\n- `path` may be absolute or relative to the working directory.\n- In a repository (a mount), an empty directory stays in this session's working copy; a commit leaves empty directories out.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory to make"
}
},
"required": [
"path"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "multiedit",
"scope": "cli",
"sessions": "agent",
"risk": "write",
"exclusive": false,
"block": "edit",
+31
View File
@@ -0,0 +1,31 @@
{
"name": "mv",
"scope": "llembas",
"sessions": "chat",
"risk": "write",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Move or rename a file or directory. A directory moves with everything in it.\n\n- `from` and `to` may be absolute or relative to the working directory; `to` must not exist yet.\n- Inside one repository (a mount) the move is staged like any other change. Between a repository and the rest of the file system, or between two repositories, it is refused: read the file and write it where it should go instead.",
"parameters": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "What to move"
},
"to": {
"type": "string",
"description": "The new path, including the name"
}
},
"required": [
"from",
"to"
]
}
}
+2 -8
View File
@@ -1,6 +1,7 @@
{
"name": "note_manage",
"scope": "shared",
"scope": "cli",
"sessions": "agent",
"risk": "interact",
"exclusive": false,
"block": "tool",
@@ -53,12 +54,5 @@
"required": [
"action"
]
},
"narrow": {
"llembas": {
"drop": [
"scope"
]
}
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "note_view",
"scope": "shared",
"scope": "cli",
"sessions": "agent",
"risk": "read",
"exclusive": false,
"block": "read",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "notes_search",
"scope": "shared",
"scope": "cli",
"sessions": "agent",
"risk": "read",
"exclusive": false,
"block": "search",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "plan_submit",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": true,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "plan_update",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "plan",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "plan_write",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "plan",
+46
View File
@@ -0,0 +1,46 @@
{
"name": "pr",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"unavailable": "No repository from a forge is open to this session, so there is nothing to pr here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Open a pull request on the forge, from `head` (default: the branch this session works on) into `base` (default: the repository's default branch). Only committed changes are in it: commit first.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"title": {
"type": "string"
},
"body": {
"type": "string",
"description": "What it changes and why"
},
"head": {
"type": "string",
"description": "The branch with the changes"
},
"base": {
"type": "string",
"description": "The branch to merge into"
}
},
"required": [
"path",
"title"
]
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "python",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": false,
"block": "shell",
"purpose": "purpose",
"requires": [
"page"
],
"unavailable": "Python cannot run now: no open browser page is attached to this chat.",
"aliases": {
"cli": [],
"llembas": []
},
"variables": {
"packages": "the packages the instance provides beyond the standard library, as a list, or \"none\"",
"seconds": "the time limit of one call, in seconds",
"memory": "the memory limit of one call, with its unit",
"output": "how much printed output comes back, with its unit"
},
"description": "Run Python for calculations and for working with files — parse JSON or CSV, transform text, compute. It runs in the user's browser (Pyodide), sandboxed: no network, no shell, no packages beyond the standard library and these: {{packages}}. Returns what the code printed and the value of its last expression.\n\n- `files` are copied in before the run, at the same paths, from what this session can read.\n- Files the code writes come back only when listed in `save`: each is written as the write tool would write it, with this session's permissions.\n- At most {{seconds}} seconds and {{memory}} of memory per call; printed output past {{output}} is cut.\n- Every call starts fresh: nothing is kept between calls.",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The Python code to run"
},
"files": {
"type": "array",
"items": {
"type": "string"
},
"description": "Files to copy in before the run"
},
"save": {
"type": "array",
"items": {
"type": "string"
},
"description": "Files the code writes that are to be saved back"
}
},
"required": [
"code"
]
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "read",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "read",
+36
View File
@@ -0,0 +1,36 @@
{
"name": "recall",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "read",
"purpose": false,
"aliases": {
"cli": [],
"llembas": []
},
"description": "Show a stored tool result again, in full, by its id (`#a3f2`): the lines a shortened output left out, or a result from before the conversation was summarised. Every result of this session is kept, so nothing is lost.\n\n- Returns up to 2000 lines from `offset` (1-indexed); call again with a larger offset for more.\n- An unchanged file is not worth recalling when its earlier result is still above: that result is the content.",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The result's id, e.g. #a3f2"
},
"offset": {
"type": "integer",
"minimum": 1,
"description": "First line to return, 1-indexed"
},
"limit": {
"type": "integer",
"minimum": 1,
"description": "How many lines (default 2000)"
}
},
"required": [
"id"
]
}
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "release",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": true,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"confirm": "always",
"unavailable": "No repository from a forge is open to this session, so there is nothing to release here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Publish a release on the forge from a tag that already exists, with its notes. Make the tag first (tag): a release is never made for a tag that does not exist. The user is always asked first, in every mode.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"tag": {
"type": "string",
"description": "The existing tag"
},
"title": {
"type": "string"
},
"notes": {
"type": "string",
"description": "The release notes (usually the tag's message)"
},
"prerelease": {
"type": "boolean"
},
"draft": {
"type": "boolean"
}
},
"required": [
"path",
"tag",
"title",
"notes"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "report",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "tool",
+26
View File
@@ -0,0 +1,26 @@
{
"name": "rm",
"scope": "llembas",
"sessions": "chat",
"risk": "write",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Delete a file or directory by moving it to the trash, from which the user can restore it. A directory goes with everything in it: list it first.\n\n- `path` may be absolute or relative to the working directory.\n- In a repository (a mount) the deletion is staged in this session's working copy and reaches the repository only with a commit.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File or directory to delete"
}
},
"required": [
"path"
]
}
}
+36
View File
@@ -0,0 +1,36 @@
{
"name": "search",
"scope": "llembas",
"sessions": "chat",
"risk": "read",
"exclusive": false,
"block": "search",
"purpose": "purpose",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Search the files this session can see, by meaning and by words together: the best passages first, each with its path and line range. Use it to find where something is discussed or answered when you do not know the exact words; use grep for exact text or a pattern, glob for file names.\n\n- `path` limits the search to a directory (default: everything this session can see).\n- A passage is an extract. Read the file around it before answering from it when the answer turns on detail, and cite the path.\n- Only indexed files are searched: text, Markdown, code, JSON and the text of PDF and DOCX files, up to a size limit. A file just written is found once it has been indexed.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What to look for, in words: a question or the topic"
},
"path": {
"type": "string",
"description": "Directory to search (default: everything this session can see)"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"description": "How many passages (default 10)"
}
},
"required": [
"query"
]
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "session_search",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "search",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "settings",
"scope": "cli",
"sessions": "agent",
"risk": "interact",
"exclusive": false,
"block": "tool",
+35
View File
@@ -0,0 +1,35 @@
{
"name": "show",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "tool",
"purpose": "purpose",
"requires": [
"display"
],
"unavailable": "Nothing can be shown now: this session has no screen to show a file on. Give the user the path instead.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Open a file in the user's viewer, beside the conversation, without them having to look for it: Markdown rendered, code highlighted, plain text as it is, and HTML as a page in a sandboxed frame that cannot reach this app, the user's data or anything the instance does not allow. Use it when the user should see the file itself — a page or a chart you made, a document you wrote — not for files you only read.\n\n- `path` may be absolute or relative to the working directory. The file must exist and be readable in this session: write it first.\n- `line` scrolls a text file to that line.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File to show"
},
"line": {
"type": "integer",
"minimum": 1,
"description": "Line to scroll to (text files)"
}
},
"required": [
"path"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "skill_manage",
"scope": "shared",
"sessions": "both",
"risk": "write",
"exclusive": false,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "skill_view",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "read",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "skills_list",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "search",
+45
View File
@@ -0,0 +1,45 @@
{
"name": "tag",
"scope": "llembas",
"sessions": "chat",
"risk": "execute",
"exclusive": true,
"block": "tool",
"purpose": "purpose",
"requires": [
"forge"
],
"confirm": "always",
"unavailable": "No repository from a forge is open to this session, so there is nothing to tag here.",
"aliases": {
"cli": [],
"llembas": []
},
"description": "Create an annotated tag on the forge, at `at` (default: the newest commit of the branch this session works on), its message being the release note. The user is always asked first, in every mode.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Any path inside the repository (the mount) this is about"
},
"name": {
"type": "string",
"description": "The tag, e.g. v1.2.0"
},
"message": {
"type": "string",
"description": "The tag's message"
},
"at": {
"type": "string",
"description": "Branch or commit to tag"
}
},
"required": [
"path",
"name",
"message"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
{
"name": "task",
"scope": "shared",
"sessions": "both",
"risk": "interact",
"exclusive": false,
"block": "task",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "tasks",
"scope": "cli",
"sessions": "agent",
"risk": "interact",
"exclusive": false,
"block": "tool",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "view_image",
"scope": "cli",
"sessions": "agent",
"risk": "read",
"exclusive": false,
"block": "read",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "web_fetch",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "web",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "web_search",
"scope": "shared",
"sessions": "both",
"risk": "read",
"exclusive": false,
"block": "web",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "write",
"scope": "cli",
"scope": "shared",
"sessions": "both",
"risk": "write",
"exclusive": false,
"block": "edit",