Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions mods/agents-md/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ builds (the next turn after the reload, a new conversation, `/clear`, a
compaction) carries the new mode's files. A hand-typed value outside the
four is told once in the transcript and reads as the default. `/plugin`
lists the plugin among the built-ins, where a person can turn it off; with
it off the engine reads `CLAUDE.md` alone.
it off the engine reads `CLAUDE.md` alone. No hooks setting or CLI mode turns
it off (`disableAllHooks`, `allowManagedHooksOnly` and `--bare` govern
settings hooks and installed plugins, not built-ins); where the engine loads
no instruction files (`--bare` without `--add-dir`, `--safe-mode`,
`CLAUDE_CODE_DISABLE_CLAUDE_MDS`) its walk finds none and it adds none,
`CLAUDE.md` and `AGENTS.md` alike.

The option was first keyed `projectInstructions`, with the values `claude`,
`agents-fallback`, `both` and `none`. A value still stored under that key is
Expand All @@ -88,7 +93,7 @@ same entry is keyed `"agents-md"`.
| `session.start` | in every mode: passes the start straight through and floats the usage row for the configured mode, never awaited; the first start of a load logs how a stored `projectInstructions` value is read. The session's start never waits on this plugin |
| `prompt.context` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: walks `$.fs.ancestors` for the `AGENTS.md` files above the working directory and answers them as `project` instruction files, each `@` import its own entry after its file, each placed where a project file of its directory stands (root first, before the first deeper project file, else after the last project file, before memory); files the engine already holds by path or by content are left out; under `claude-md-or-agents-md` it answers nothing when the project has a `CLAUDE.md` of its own (among the handed files, else found by a `$.fs.ancestors` walk, so a `CLAUDE.md` the engine loaded and then withheld still counts), and logs which files it loaded once, and again after a move to another project root; handed unknown files (a hook above rewrote the `claudeMd` text) it adds nothing; the first context of a load sends the load row (counts) and the feature mark; under `managed-only` (matcher: a `project`, `local` or `user` file present): answers the list without those kinds |
| `agent.spawn` on `fork: true` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`: a fork the Agent tool starts shares its parent's prompt prefix, so the parent loop's delivered nested files are copied to the fork's loop and not attached to it again (a `/fork` or `/subtask` fork does not raise `agent.spawn` yet and starts from an empty set, as every fork did before; a fork started in the same tool batch as a `Read` inherits that Read's file although its prefix holds a placeholder for it) |
| `tool.call` on `Read` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`, for a file under the session's project root (`$.session.root()`, read live, so `/cd`, a host's directory change and worktree moves are followed and a moved root starts the delivered sets and the fallback decision over; a file elsewhere gets nothing, as the engine attaches no nested `CLAUDE.md` there): walks only the directories strictly between the root and the read file (`$.fs.ancestors` with `below: root`, as the engine walks only those for a nested `CLAUDE.md`, never up to the filesystem root again) and attaches their `AGENTS.md` files not yet given to that agent loop, not already among the context's instruction files (by path or, for a project file, by text) and not claimed by a `CLAUDE.md` of the same directory (or imported by one), as `context` after the tool result, framed `Contents of <path>:` byte for byte as the engine frames a nested `CLAUDE.md`, whatever its size; each file once per loop and conversation (the context's recomputation after a compaction or `/clear` starts the count over), the context's files never; a Read that attached files sends the nested row. A `~` or `~/` path is read under the home directory as the Read tool reads it |
| `tool.call` on `Read` | under `claude-md-or-agents-md` and `claude-md-and-agents-md`, for a file under the session's project root (`$.session.root()`, read live, so `/cd`, a host's directory change and worktree moves are followed and a moved root starts the delivered sets and the fallback decision over; a file elsewhere gets nothing, as the engine attaches no nested `CLAUDE.md` there; and nothing anywhere in a run where the engine attaches nothing to a turn, `--bare` with its `CLAUDE_CODE_SIMPLE` or `CLAUDE_CODE_DISABLE_ATTACHMENTS`, read on every Read through `$.env.get` as the engine reads them on every turn): walks only the directories strictly between the root and the read file (`$.fs.ancestors` with `below: root`, as the engine walks only those for a nested `CLAUDE.md`, never up to the filesystem root again) and attaches their `AGENTS.md` files not yet given to that agent loop, not already among the context's instruction files (by path or, for a project file, by text) and not claimed by a `CLAUDE.md` of the same directory (or imported by one), as `context` after the tool result, framed `Contents of <path>:` byte for byte as the engine frames a nested `CLAUDE.md`, whatever its size; each file once per loop and conversation (the context's recomputation after a compaction or `/clear` starts the count over), the context's files never; a Read that attached files sends the nested row. A `~` or `~/` path is read under the home directory as the Read tool reads it |

## What it calls on `$`

Expand All @@ -97,7 +102,8 @@ apart; with `below` on a Read; it finds nothing on a thin client, whose
workspace files are remote, as the engine's own walk does), `session.root`,
`session.cwd`, `env.get` (`HOME` and `USERPROFILE`, once per load, the
profile first on a Windows spelling of the working directory, so a `~/` path
the model hands a Read resolves where the Read tool reads it), `ui.log`,
the model hands a Read resolves where the Read tool reads it; `CLAUDE_CODE_SIMPLE`
and `CLAUDE_CODE_DISABLE_ATTACHMENTS` on every Read), `ui.log`,
`telemetry.log` and `telemetry.mark`.

`$.telemetry` is the [telemetry](../telemetry) plugin's noun; where that
Expand Down
2 changes: 1 addition & 1 deletion mods/agents-md/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"description": "AGENTS.md up and down the tree like CLAUDE.md, by the instructionFiles option: under claude-md-or-agents-md (the default; where the project has no CLAUDE.md of its own on the walk) or claude-md-and-agents-md, a prompt.context hook hands every ancestor AGENTS.md to the engine among its instruction files, skipping ones it already loaded, and a tool.call hook on Read attaches the AGENTS.md files of the directories between the project root and a read file, once a conversation and agent loop (an Agent-tool fork inherits its parent's); under managed-only, a prompt.context hook keeps only the organization's managed files and memory; under claude-md, nothing is added; in every mode a session.start hook sends the plugin's usage row through $.telemetry where that noun is seated",
"description": "AGENTS.md up and down the tree like CLAUDE.md, by the instructionFiles option: under claude-md-or-agents-md (the default; where the project has no CLAUDE.md of its own on the walk) or claude-md-and-agents-md, a prompt.context hook hands every ancestor AGENTS.md to the engine among its instruction files, skipping ones it already loaded, and a tool.call hook on Read attaches the AGENTS.md files of the directories between the project root and a read file, once a conversation and agent loop (an Agent-tool fork inherits its parent's), except in a run where the engine attaches nothing to a turn (--bare, CLAUDE_CODE_DISABLE_ATTACHMENTS); under managed-only, a prompt.context hook keeps only the organization's managed files and memory; under claude-md, nothing is added; in every mode a session.start hook sends the plugin's usage row through $.telemetry where that noun is seated",
"modules": ["./register.ts"]
}
1 change: 1 addition & 0 deletions mods/agents-md/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './frames'
export * from './modes'
export * from './names'
export * from './register.js'
export * from './switches'
export * from './telemetry'

export * as default from '.'
34 changes: 33 additions & 1 deletion mods/agents-md/hooks/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Files from './files'
import Frames from './frames'
import Modes from './modes'
import Names from './names'
import Switches from './switches'
import Telemetry from './telemetry'

/**
Expand All @@ -26,7 +27,9 @@ const NONE: readonly FsAncestor[] = []
* standing alone. `managed-only`: the project's and the person's instruction
* files dropped, the organization's kept. `claude-md-or-agents-md` (a project
* with none of its own) and `claude-md-and-agents-md`: AGENTS.md files joined
* to the engine's instruction files, nested ones on a Read. Every mode sends
* to the engine's instruction files, nested ones on a Read except in a run
* where the engine attaches nothing to a turn (--bare, which sets
* CLAUDE_CODE_SIMPLE, or CLAUDE_CODE_DISABLE_ATTACHMENTS). Every mode sends
* its usage rows through `$.telemetry` where that noun is seated and drops
* them where it is not.
*
Expand Down Expand Up @@ -182,6 +185,10 @@ export function register(on: On, options: PluginOptions): void {
return result
}

if (!(await attachesOnRead($))) {
return result
}

const [root, cwd] = await Promise.all([$.session.root(), $.session.cwd()])
home ??= await homeOf($, cwd)
const read = Frames.absoluteOf(e.file_path, cwd, home)
Expand Down Expand Up @@ -247,6 +254,31 @@ export function register(on: On, options: PluginOptions): void {
})
}

/**
* Whether a Read attaches nested AGENTS.md files in this run: not where the
* engine attaches nothing to a turn, a nested CLAUDE.md included, which is a
* --bare run (it sets CLAUDE_CODE_SIMPLE) or one with
* CLAUDE_CODE_DISABLE_ATTACHMENTS on.
*
* Read on every Read, as the engine reads them on every turn: a settings
* `env` block or a managed delivery can flip either mid-session. The files of
* the walk itself need no such check: where the engine loads no instruction
* files `$.fs.ancestors` finds none.
*
* @param $ the engine, as the `tool.call` hook holds it
* @returns whether nested files ride a Read's result here
*/
async function attachesOnRead($: EngineInterface): Promise<boolean> {
const [simple, attachmentsOff] = await Promise.all([
$.env.get('CLAUDE_CODE_SIMPLE'),
$.env.get('CLAUDE_CODE_DISABLE_ATTACHMENTS'),
])

return (
!Switches.isSwitchedOn(simple) && !Switches.isSwitchedOn(attachmentsOff)
)
}

/**
* The home directory a `~` in a Read's path stands for, read the way the
* Read tool reads it.
Expand Down
3 changes: 3 additions & 0 deletions mods/agents-md/hooks/switches/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './is-switched-on.js'

export * as default from '.'
14 changes: 14 additions & 0 deletions mods/agents-md/hooks/switches/is-switched-on.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* The spellings an environment switch is on with, as the engine reads its own.
*/
const ON_SPELLINGS: ReadonlySet<string> = new Set(['1', 'true', 'yes', 'on'])

/**
* Whether an environment variable's value turns a switch on, read the way the
* engine reads its own switches: `1`, `true`, `yes` or `on`, any case, trimmed.
*
* @param value the variable's value, or undefined when it is unset
* @returns whether the switch is on
*/
export const isSwitchedOn = (value: string | undefined): boolean =>
value !== undefined && ON_SPELLINGS.has(value.trim().toLowerCase())
Loading