spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

mods/agents-md: the instructionFiles option with its legacy key, as shipped; typings at 2.1.277Browse files
poteat authored on Sep 18, 2026 parent 37767ec · commit 84b59d23257c
40 changed files+314 -507compared with 37767ec
Mmods/README.md
Mmods/agents-md/.claude-plugin/plugin.json
Mmods/agents-md/README.md
Mmods/agents-md/hooks/files/dropped-kinds.ts
Mmods/agents-md/hooks/files/is-claude-file-on-walk.ts
Mmods/agents-md/hooks/files/is-kept-without-instructions.ts
Mmods/agents-md/hooks/hooks.json
Amods/agents-md/hooks/modes/default-mode.ts
Mmods/agents-md/hooks/modes/index.ts
Amods/agents-md/hooks/modes/legacy-mode-of.ts
Mmods/agents-md/hooks/modes/mode-of.ts
Mmods/agents-md/hooks/modes/modes.ts
Mmods/agents-md/hooks/modes/types/mode/mode.ts
Mmods/agents-md/hooks/names/index.ts
Dmods/agents-md/hooks/names/nudge-timeout-ms.ts
Dmods/agents-md/hooks/names/nudge.ts
Mmods/agents-md/hooks/register.ts
Mmods/agents-md/hooks/telemetry/index.ts
Mmods/agents-md/hooks/telemetry/load-counts-of.ts
Mmods/agents-md/hooks/telemetry/load-event.ts
Mmods/agents-md/hooks/telemetry/mode-row-of.ts
Dmods/agents-md/hooks/telemetry/nudge-event.ts
Dmods/agents-md/hooks/telemetry/nudge-key.ts
Dmods/agents-md/hooks/telemetry/nudge-outcomes.ts
Dmods/agents-md/hooks/telemetry/nudge-row-of.ts
Mmods/agents-md/hooks/telemetry/types/index.ts
Mmods/agents-md/hooks/telemetry/types/load-counts/load-counts.ts
Dmods/agents-md/hooks/telemetry/types/nudge-outcome/index.ts
Dmods/agents-md/hooks/telemetry/types/nudge-outcome/nudge-outcome.ts
Dmods/agents-md/hooks/telemetry/types/start-facts/index.ts
Dmods/agents-md/hooks/telemetry/types/start-facts/start-facts.ts
Mmods/agents-md/tests/fixtures/index.ts
Dmods/agents-md/tests/fixtures/late-ms.ts
Dmods/agents-md/tests/fixtures/late-project-of.ts
Mmods/agents-md/tests/fixtures/project-of.ts
Dmods/agents-md/tests/fixtures/stalled-project-of.ts
Mmods/agents-md/tests/fixtures/started-of.ts
Mmods/agents-md/tests/fixtures/types/started/started.ts
Mmods/agents-md/tests/register.test.ts
Mmods/types/claude-code.d.ts
@@ -10,7 +10,7 @@
1010 | [`sec-default`](sec-default) | Keeps an organization's classic hooks, prompt content, managed settings and tool policy out of reach of the plugins a person installs; adds no policy of its own. | Outermost, on a machine with managed settings or for a Team or Enterprise organization, unless managed `prependPlugins` says otherwise |
1111 | [`diff`](diff) | `/diff`: the session's uncommitted changes in a pane beside the transcript, file by file with their hunks, refreshed as Claude edits files and runs commands. | Built in |
1212 | [`telemetry`](telemetry) | Adds `$.telemetry` (`log`, `mark`) in the `engine.create` fold so a plugin can record an event as a first-party analytics row; sends nothing wherever Claude Code's analytics are off. | Built in |
13-| [`agents-md`](agents-md) | `AGENTS.md` as project instructions, by one option: loaded where the project has no `CLAUDE.md` of its own (`agents-fallback`), beside it (`both`), or neither (`none`), placed and framed exactly as the engine places `CLAUDE.md`; by default (`claude`) the engine reads `CLAUDE.md` alone and a project with only an `AGENTS.md` is told so at session start. | Built in |
13+| [`agents-md`](agents-md) | `AGENTS.md` as project instructions, by one option: loaded where the project has no `CLAUDE.md` of its own (`claude-md-or-agents-md`, the default) or beside it (`claude-md-and-agents-md`), placed and framed exactly as the engine places `CLAUDE.md`, nested ones on a `Read`; or the project's and the person's instruction files dropped and the organization's kept (`managed-only`); or `CLAUDE.md` alone, as the engine reads it (`claude-md`). | Built in |
1414
1515 Each folder is a complete plugin: `.claude-plugin/plugin.json`, a
1616 `hooks/hooks.json` naming the module, and TypeScript under `hooks/` typed
@@ -1,18 +1,23 @@
11 {
22 "name": "agents-md",
33 "version": "0.1.0",
4- "description": "AGENTS.md as project instructions, by one option. Under agents-fallback a project with no instruction files of its own gets its AGENTS.md files instead: a prompt.context hook asks $.fs.ancestors for every AGENTS.md and .claude/AGENTS.md from the filesystem root down to the working directory and hands them to the engine as project instruction files, which it renders, announces and withholds from agents exactly as it does CLAUDE.md, skipping any file the engine already loaded by import or link; a tool.call hook on Read attaches the ones under the project root as the engine attaches a nested CLAUDE.md. Under both every AGENTS.md is loaded beside CLAUDE.md. Under none the project's and the person's instruction files are dropped and the organization's managed ones kept. Under claude (the default) the engine's walk stands alone and a session.start hook says once when the project has an AGENTS.md it is not reading.",
4+ "description": "AGENTS.md as project instructions, by one option. Under claude-md-or-agents-md a project with no instruction files of its own gets its AGENTS.md files instead: a prompt.context hook asks $.fs.ancestors for every AGENTS.md and .claude/AGENTS.md from the filesystem root down to the working directory and hands them to the engine as project instruction files, which it renders, announces and withholds from agents exactly as it does CLAUDE.md, skipping any file the engine already loaded by import or link; a tool.call hook on Read attaches the ones under the project root as the engine attaches a nested CLAUDE.md. Under claude-md-and-agents-md every AGENTS.md is loaded beside CLAUDE.md. Under managed-only the project's and the person's instruction files are dropped and the organization's managed ones kept. Under claude-md the engine's walk stands alone. claude-md-or-agents-md is the default.",
55 "author": {
66 "name": "Anthropic"
77 },
88 "userConfig": {
9- "projectInstructions": {
9+ "instructionFiles": {
1010 "type": "string",
1111 "title": "Project instructions",
12- "description": "\"claude\": CLAUDE.md only, loaded by the engine as today (default). \"agents-fallback\": a project with no CLAUDE.md of its own gets its AGENTS.md files instead, loaded exactly where and how CLAUDE.md would be. \"both\": AGENTS.md files are loaded beside CLAUDE.md (a file CLAUDE.md already imports or links to is not loaded twice). \"none\": the project's and your own instruction files are dropped; the organization's managed CLAUDE.md and memory stay.",
12+ "description": "\"claude-md\": CLAUDE.md only, loaded by the engine as today. \"claude-md-or-agents-md\" (default): a project with no CLAUDE.md of its own gets its AGENTS.md files instead, loaded exactly where and how CLAUDE.md would be. \"claude-md-and-agents-md\": AGENTS.md files are loaded beside CLAUDE.md (a file CLAUDE.md already imports or links to is not loaded twice). \"managed-only\": the project's and your own instruction files are dropped; the organization's managed CLAUDE.md and memory stay.",
1313 "required": false,
14- "default": "claude",
15- "options": ["claude", "agents-fallback", "both", "none"]
14+ "default": "claude-md-or-agents-md",
15+ "options": [
16+ "claude-md",
17+ "claude-md-or-agents-md",
18+ "claude-md-and-agents-md",
19+ "managed-only"
20+ ]
1621 }
1722 }
1823 }
@@ -1,60 +1,59 @@
11 # agents-md
22
33 `AGENTS.md` read the way Claude Code reads `CLAUDE.md`, as a plugin, under
4-one option, `projectInstructions`:
4+one option, `instructionFiles`:
55
6-- `claude` (the default): only `CLAUDE.md` is loaded, by the engine, as
7- today. The plugin adds nothing to the context. When the project has an
8- `AGENTS.md` (or `.claude/AGENTS.md`) somewhere from the filesystem root
9- down to the working directory and no `CLAUDE.md`, one line on the
10- notification bar at session start says so and names this option.
11-- `agents-fallback`: a project with no instruction files of its own gets its
12- `AGENTS.md` files instead, loaded exactly where and how `CLAUDE.md` would
13- be. "Of its own" is read off what the engine loaded for the context: a
14- `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` in any directory from
15- the root down to the working directory leaves the whole project to the
16- engine, and the plugin stays out (the organization's managed file, the
17- person's `~/.claude/CLAUDE.md`, a `.claude/rules` file and an added
18- directory's `CLAUDE.md` do not count). With none, every `AGENTS.md` and
19- `.claude/AGENTS.md` on that path joins the instruction files the engine
20- renders, and a `Read` under a subdirectory attaches that directory's
21- `AGENTS.md` unless a `CLAUDE.md` there claims it.
22-- `both`: every `AGENTS.md` is loaded beside `CLAUDE.md`, up and down the
23- tree; a file `CLAUDE.md` already `@`-imports, or is a link to, is not loaded
24- a second time (compared by path, then by content).
25-- `none`: the project's checked-in and private instruction files and the
26- person's own are dropped from the context; the organization's managed
6+- `claude-md`: only `CLAUDE.md` is loaded, by the engine, as today. The plugin
7+ adds nothing.
8+- `claude-md-or-agents-md` (the default): a project with no instruction files
9+ of its own gets its `AGENTS.md` files instead, loaded exactly where and how
10+ `CLAUDE.md` would be. "Of its own" is read off what the engine loaded for
11+ the context: a `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` in any
12+ directory from the root down to the working directory leaves the whole
13+ project to the engine, and the plugin stays out (the organization's managed
14+ file, the person's `~/.claude/CLAUDE.md`, a `.claude/rules` file and an
15+ added directory's `CLAUDE.md` do not count, as the nested walk does not see
16+ them either). With none, every `AGENTS.md` and `.claude/AGENTS.md` on that
17+ path joins the instruction files the engine renders, and a `Read` under a
18+ subdirectory attaches that directory's `AGENTS.md` unless a `CLAUDE.md`
19+ there claims it.
20+- `claude-md-and-agents-md`: every `AGENTS.md` is loaded beside `CLAUDE.md`,
21+ up and down the tree; a file `CLAUDE.md` already `@`-imports, or is a link
22+ to, is not loaded a second time (compared by path, then by content).
23+- `managed-only`: the project's checked-in and private instruction files and
24+ the person's own are dropped from the context; the organization's managed
2725 `CLAUDE.md` and the engine's memory stay. The engine's nested `CLAUDE.md`
28- attachments on `Read` are not an event yet and still arrive. (The
29- `claudeMdExcludes` setting applies to the `AGENTS.md` files this plugin
30- reads too.)
26+ attachments on `Read` are not an event yet and still arrive. (The engine's
27+ `claudeMdExcludes` setting also exists, for user, project and local files,
28+ and applies to the `AGENTS.md` files this plugin reads too.)
3129
3230 How the files reach the model is the engine's doing, not the plugin's:
33-`prompt.context` hands a hook the instruction files behind the `claudeMd`
34-block (`{ path, kind, content, parent? }`, kinds `managed`, `user`,
35-`project`, `local`, `memory`, in load order) and a hook answers the list
36-changed. The engine then renders `claudeMd` from the answered files with its
37-own preamble and framing, announces them by name, and keeps only the
38-`managed` ones for an agent that omits project instructions (Explore, Plan, a
39-custom agent with `omitClaudeMd`). So an `AGENTS.md` this plugin adds as a
40-`project` file is, to everything downstream, a project instruction file: same
41-place in the context, same framing, same omission rules, same announcement.
42-An organization's prepended plugin on `prompt.context` sits above this one
43-and has the last word on the files.
31+`prompt.context` hands a hook the instruction files behind `claudeMd`
32+(`{ path, kind, content, parent? }`, kinds `managed`, `user`, `project`,
33+`local`, `memory`, in load order) and a hook answers the list changed. The
34+engine then renders `claudeMd` from the answered files with its own preamble
35+and framing, announces them by name, and keeps only the `managed` ones for an
36+agent that omits project instructions (Explore, Plan, a custom agent with
37+`omitClaudeMd`). So an `AGENTS.md` this plugin adds as a `project` file is, to
38+everything downstream, a project instruction file: same place in the context,
39+same framing, same omission rules, same announcement. An organization's
40+prepended plugin on `prompt.context` sits above this one and has the last
41+word on the files.
4442
4543 `hooks/register.ts` is the module; everything under `hooks/` is its parts,
46-importing `claude-code` and one another alone.
44+importing `claude-code` and one another alone. `tests/` runs under
45+`claude plugin test <this folder>`.
4746
4847 ## Setting the option
4948
50-Built in, the option is the `/config` row "Project instructions", a picker
51-over the four values, each described there. By hand it is
49+As a built-in its option is the `/config` row "Project instructions", a
50+picker over the four values, each described there. By hand it is
5251
5352 ```json
5453 {
5554 "pluginConfigs": {
5655 "agents-md@builtin": {
57- "options": { "projectInstructions": "agents-fallback" }
56+ "options": { "instructionFiles": "claude-md-and-agents-md" }
5857 }
5958 }
6059 }
@@ -64,10 +63,21 @@
6463 settings; a project's `.claude/settings.json` is not read for plugin
6564 options. Changing it reloads the module, and the next context the engine
6665 builds (the next turn after the reload, a new conversation, `/clear`, a
67-compaction) carries the new mode's files. A value outside the four is told
68-once in the transcript and reads as the default. `/plugin` lists the plugin
69-among the built-ins, where a person can turn it off.
66+compaction) carries the new mode's files. A hand-typed value outside the
67+four is told once in the transcript and reads as the default. `/plugin`
68+lists the plugin among the built-ins, where a person can turn it off; with
69+it off the engine reads `CLAUDE.md` alone.
7070
71+The option was first keyed `projectInstructions`, with the values `claude`,
72+`agents-fallback`, `both` and `none`. A value still stored under that key is
73+honoured for now while `instructionFiles` reads as its default: `none` as
74+`managed-only`, `claude` as `claude-md`, `agents-fallback` as
75+`claude-md-or-agents-md`, `both` as `claude-md-and-agents-md`, any other
76+value as `claude-md` (which adds nothing, never as the default, which loads
77+`AGENTS.md`); the first `session.start` of a load says in the transcript how
78+it was read. Once `instructionFiles` is set to anything but its default, the
79+old key is not read and the transcript says to remove it.
80+
7181 Run from this folder instead (`claude --plugin-dir mods/agents-md`), the
7282 same entry is keyed `"agents-md"`.
7383
@@ -75,22 +85,24 @@
7585
7686 | event | what the hook does |
7787 | --- | --- |
78-| `session.start` | In every mode: passes the start straight through and floats the rest, never awaited. Under `claude`, two `$.fs.ancestors` walks for the `AGENTS.md` and `CLAUDE.md` files above the working directory, and when they finish the nudge toast (ten seconds on the bar) if there are the former and none of the latter, with a note in `$.store` that it was shown; under any other mode, a note that an earlier nudge was acted on, once. The session's start never waits on this plugin. |
79-| `prompt.context` | Under `agents-fallback` and `both`: 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 `agents-fallback` it answers nothing when the handed files include a `CLAUDE.md` of the project's own, and logs once which files it loaded. Handed unknown files (a hook above rewrote the `claudeMd` text) it adds nothing. Under `none` (matcher: a `project`, `local` or `user` file present): answers the list without those kinds. |
80-| `agent.spawn` on `fork: true` | Under `agents-fallback` and `both`: 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. |
81-| `tool.call` on `Read` | Under `agents-fallback` and `both`, for a file under the session's project root (`$.session.root()`, read live, so `/cd` 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`) and attaches their `AGENTS.md` files not yet given to that agent loop, not already among the context's instruction files, 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>:` 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). A `~` or `~/` path is read under the home directory as the Read tool reads it. |
88+| `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 |
89+| `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 |
90+| `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) |
91+| `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 |
8292
8393 ## What it calls on `$`
8494
85-`fs.ancestors` (with each found file's `parts`, the file and its imports
86-apart; with `below` on a Read), `session.root`, `session.cwd`, `env.get`
87-(`HOME` and `USERPROFILE`, once per load), `ui.toast`, `ui.log`,
88-`store.get` and `store.set` (the nudge's note, key `nudge`),
95+`fs.ancestors` (with each found file's `parts`: the file and its imports
96+apart; with `below` on a Read; it finds nothing on a thin client, whose
97+workspace files are remote, as the engine's own walk does), `session.root`,
98+`session.cwd`, `env.get` (`HOME` and `USERPROFILE`, once per load, the
99+profile first on a Windows spelling of the working directory, so a `~/` path
100+the model hands a Read resolves where the Read tool reads it), `ui.log`,
89101 `telemetry.log` and `telemetry.mark`.
90102
91-`$.telemetry` is the telemetry plugin's noun; where that plugin is not
92-seated the calls find no noun and are dropped without a trace, and nothing
93-else changes.
103+`$.telemetry` is the [telemetry](../telemetry) plugin's noun; where that
104+plugin is not seated the calls find no noun and are dropped without a trace,
105+and nothing else changes.
94106
95107 ## What it logs
96108
@@ -100,14 +112,15 @@
100112
101113 | event | when | properties |
102114 | --- | --- | --- |
103-| `agents_md_mode` | once per fresh load, at `session.start` | `mode`, `is_interactive`; under `claude` also `agents_file_count` and `claude_file_count`, the directories of the walk holding each |
104-| `agents_md_load` | the first context of a load, under `agents-fallback` and `both` | `mode`, `file_count`, `import_count`, `total_content_length`, `yielded` (`agents-fallback` stood down for a `CLAUDE.md` of the project's own), `walk_failed`; with it one `$.telemetry.mark` for feature `agents_md`: `ok`, or `sad` with reason `walk_failed` |
115+| `agents_md_mode` | once per fresh load, at `session.start` | `mode` (`claude-md` \| `claude-md-or-agents-md` \| `claude-md-and-agents-md` \| `managed-only`), `is_interactive` |
116+| `agents_md_load` | the first context of a load, under `claude-md-or-agents-md` and `claude-md-and-agents-md` | `mode`, `file_count` (`AGENTS.md` files handed to the engine), `import_count` (their `@` imports), `total_content_length`, `yielded` (`claude-md-or-agents-md` stood down for a `CLAUDE.md` of the project's own), `walk_failed`; with it one `$.telemetry.mark` for feature `agents_md`: `ok`, or `sad` with reason `walk_failed` |
105117 | `agents_md_nested` | a Read that attached nested files | `mode`, `file_count` |
106-| `agents_md_nudge` | the nudge | `outcome`: `shown` when the toast goes up, `acted` once when a later load runs under another mode after one was shown |
107118
108119 ## Where it still differs from CLAUDE.md
109120
110-All of these apply only to the opt-in modes.
121+All of these apply only to the modes that load `AGENTS.md`,
122+`claude-md-or-agents-md` (the default) and `claude-md-and-agents-md`. Each
123+names a loader fact a plugin cannot reach through the events it has today.
111124
112125 1. Nested files attach on a text `Read` only. The engine also attaches a
113126 directory's `CLAUDE.md` for a file `@`-mentioned in the prompt, for the
@@ -125,10 +138,14 @@
125138 working directory before deciding a file is inside it.
126139 5. `--add-dir` directories contribute no `AGENTS.md`, where the engine can
127140 load their `CLAUDE.md`.
128-6. `/memory` and the `#` shortcut do not know `AGENTS.md` files.
141+6. `/memory` and the `#` shortcut do not know `AGENTS.md` files, and the
142+ engine's own initial-load row does not count them (this plugin's
143+ `agents_md_load` row does).
129144 7. An `@` import outside the working directory inside an `AGENTS.md` is
130- honoured only if the `CLAUDE.md` external-includes approval was already
131- given; the approval dialog is raised for `CLAUDE.md` imports alone.
145+ honoured only once the approval the engine asks for a `CLAUDE.md`'s
146+ external imports has been given (without it the import is left out, as a
147+ `CLAUDE.md`'s is); the approval dialog itself is raised for `CLAUDE.md`
148+ imports alone.
132149 8. A subagent that is not a fork gets a nested `AGENTS.md` at its own first
133150 `Read` under that directory even when its parent's loop was already given
134151 it; the engine does not hand such a subagent the nested `CLAUDE.md` again.
@@ -138,6 +155,9 @@
138155
139156 claude plugin test mods/agents-md
140157
141-`tests/register.test.ts` covers the default mode's `session.start`: the
142-nudge, the walks it never waits on, and the rows it hands `$.telemetry`
143-where a test seats a provider for that noun.
158+`tests/register.test.ts` covers the default mode: a project with `AGENTS.md`
159+alone gets it as a project instruction file and one transcript line naming
160+it, a project with a `CLAUDE.md` of its own is left to the engine without a
161+walk, a failed walk leaves the context as handed, and the start hands
162+`$.telemetry` the mode row alone where a test seats a provider for that
163+noun, and goes on untouched where none is seated.
@@ -1,8 +1,9 @@
11 import type { InstructionFileKind } from 'claude-code'
22
33 /**
4- * The kinds `none` drops: the project's checked-in and private instruction
5- * files and the person's own; what it keeps is the organization's and memory.
4+ * The kinds `managed-only` drops: the project's checked-in and private
5+ * instruction files and the person's own; what it keeps is the organization's
6+ * and memory.
67 */
78 export const DROPPED_KINDS: readonly InstructionFileKind[] = [
89 'project',
@@ -9,9 +9,9 @@
99 * Whether a handed instruction file is a CLAUDE.md, `.claude/CLAUDE.md` or
1010 * CLAUDE.local.md of a directory on the walk down to the session's root.
1111 *
12- * What makes a project "have a CLAUDE.md of its own" for `agents-fallback`,
13- * the same files the nudge and the Read walk ask for; a rules file, an
14- * imported file or an added directory's CLAUDE.md is not one.
12+ * What makes a project "have a CLAUDE.md of its own" for
13+ * `claude-md-or-agents-md`, the same files the Read walk asks for; a rules
14+ * file, an imported file or an added directory's CLAUDE.md is not one.
1515 *
1616 * @param file the handed instruction file
1717 * @param root the session's project root, absolute
@@ -3,11 +3,11 @@
33 import { DROPPED_KINDS } from './dropped-kinds.js'
44
55 /**
6- * Whether `none` keeps an instruction file: the organization's managed
6+ * Whether `managed-only` keeps an instruction file: the organization's managed
77 * files and the engine's memory stay, the project's and the person's go.
88 *
99 * @param file the instruction file
10- * @returns true for a file `none` leaves in place
10+ * @returns true for a file `managed-only` leaves in place
1111 */
1212 export const isKeptWithoutInstructions = (file: InstructionFile): boolean =>
1313 !DROPPED_KINDS.includes(file.kind)
@@ -1,4 +1,4 @@
11 {
2- "description": "AGENTS.md up and down the tree like CLAUDE.md, by the projectInstructions option: under agents-fallback (where the project has no CLAUDE.md of its own on the walk) or both, 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 none, a prompt.context hook keeps only the organization's managed files and memory; under claude, a session.start hook names the option when the project has an AGENTS.md and no CLAUDE.md; in every mode that hook sends the plugin's usage rows through $.telemetry where that noun is seated",
2+ "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",
33 "modules": ["./register.ts"]
44 }
@@ -1,0 +1,8 @@
1+import type { Mode } from './types'
2+
3+/**
4+ * The mode an unset option reads as: AGENTS.md where the project has no
5+ * CLAUDE.md of its own, the engine's CLAUDE.md walk standing alone where it
6+ * has one.
7+ */
8+export const DEFAULT_MODE: Mode = 'claude-md-or-agents-md'
@@ -1,3 +1,5 @@
1+export * from './default-mode.js'
2+export * from './legacy-mode-of.js'
13 export * from './mode-of.js'
24 export * from './modes.js'
35 export * from './types'
@@ -1,0 +1,33 @@
1+import type { Mode } from './types'
2+
3+/**
4+ * Each value the option took under its old key, `projectInstructions`, and
5+ * the mode that keeps its meaning.
6+ */
7+const LEGACY_MODES: Readonly<Record<string, Mode>> = {
8+ none: 'managed-only',
9+ claude: 'claude-md',
10+ 'agents-fallback': 'claude-md-or-agents-md',
11+ both: 'claude-md-and-agents-md',
12+}
13+
14+/**
15+ * The mode a value stored under the old `projectInstructions` key keeps; a
16+ * value it never took reads as `claude-md`, which adds nothing, never as the
17+ * default, which loads AGENTS.md.
18+ *
19+ * COMPAT_BREAK(agents-md-project-instructions): drop the projectInstructions
20+ * mapping once stored settings have migrated.
21+ *
22+ * @param value the `projectInstructions` option as stored
23+ * @returns the mode it keeps, undefined when nothing is stored there
24+ */
25+export function legacyModeOf(value: unknown): Mode | undefined {
26+ if (value === undefined) {
27+ return undefined
28+ }
29+
30+ return (
31+ (typeof value === 'string' ? LEGACY_MODES[value] : undefined) ?? 'claude-md'
32+ )
33+}
@@ -1,12 +1,13 @@
1+import { DEFAULT_MODE } from './default-mode.js'
12 import { MODES } from './modes.js'
23 import type { Mode } from './types'
34
45 /**
56 * The mode a stored option names; unset, a typo or a value of another type
6- * names `claude`, the engine's own behaviour.
7+ * names the default.
78 *
8- * @param value the `projectInstructions` option as stored
9- * @returns the mode named, `claude` for anything else
9+ * @param value the `instructionFiles` option as stored
10+ * @returns the mode named, DEFAULT_MODE for anything else
1011 */
1112 export const modeOf = (value: unknown): Mode =>
12- MODES.find(mode => mode === value) ?? 'claude'
13+ MODES.find(mode => mode === value) ?? DEFAULT_MODE
@@ -4,8 +4,8 @@
44 * Every mode, in the order the option's description names them.
55 */
66 export const MODES: readonly Mode[] = [
7- 'claude',
8- 'agents-fallback',
9- 'both',
10- 'none',
7+ 'claude-md',
8+ 'claude-md-or-agents-md',
9+ 'claude-md-and-agents-md',
10+ 'managed-only',
1111 ]
@@ -1,10 +1,15 @@
11 /**
2- * Which project instructions the plugin loads: the `projectInstructions`
2+ * Which project instructions the plugin loads: the `instructionFiles`
33 * option.
44 *
5- * `claude` loads none of its own, the engine's CLAUDE.md walk standing alone;
6- * `agents-fallback` loads AGENTS.md where the project has no CLAUDE.md;
7- * `both` loads AGENTS.md beside CLAUDE.md; `none` loads neither, taking the
8- * engine's CLAUDE.md block out of the conversation's context.
5+ * `claude-md` loads none of its own, the engine's CLAUDE.md walk standing
6+ * alone; `claude-md-or-agents-md` loads AGENTS.md where the project has no
7+ * CLAUDE.md; `claude-md-and-agents-md` loads AGENTS.md beside CLAUDE.md;
8+ * `managed-only` loads neither, taking the project's and the person's
9+ * CLAUDE.md files out of the conversation's context.
910 */
10-export type Mode = 'claude' | 'agents-fallback' | 'both' | 'none'
11+export type Mode =
12+ | 'claude-md'
13+ | 'claude-md-or-agents-md'
14+ | 'claude-md-and-agents-md'
15+ | 'managed-only'
@@ -1,7 +1,5 @@
11 export * from './agents-names.js'
22 export * from './claude-names.js'
33 export * from './main-loop.js'
4-export * from './nudge.js'
5-export * from './nudge-timeout-ms.js'
64
75 export * as default from '.'
Dmods/agents-md/hooks/names/nudge-timeout-ms.ts+0 -8
@@ -1,8 +1,0 @@
1-/**
2- * How long the nudge stays on the notification bar.
3- *
4- * It is raised at the session's start, before the first frame settles and
5- * while the engine's own startup notices take their turns on the bar, so
6- * the bar's default few seconds can pass before the person reads it.
7- */
8-export const NUDGE_TIMEOUT_MS = 10_000
Dmods/agents-md/hooks/names/nudge.ts+0 -7
@@ -1,7 +1,0 @@
1-/**
2- * The one line shown at session start under `claude` when the project has
3- * an AGENTS.md the engine is not reading and no CLAUDE.md it is.
4- */
5-export const NUDGE =
6- 'This project has AGENTS.md but no CLAUDE.md; set the agents-md ' +
7- "plugin's projectInstructions option to agents-fallback to load it."
@@ -18,34 +18,57 @@
1818 const NONE: readonly FsAncestor[] = []
1919
2020 /**
21- * Registers the plugin's hooks for the mode `projectInstructions` names
22- * (`claude` when unset; the manifest lists the four, nothing else arrives).
21+ * Registers the plugin's hooks for the mode `instructionFiles` names
22+ * (`claude-md-or-agents-md` when unset; the manifest lists the four, nothing
23+ * else arrives).
2324 *
24- * `claude`: a nudge for a project with AGENTS.md alone. `none`: the project's
25- * and the person's instruction files dropped, the organization's kept.
26- * `agents-fallback` (a project with none of its own) and `both`: AGENTS.md
27- * files joined to the engine's instruction files, nested ones on a Read.
28- * Every mode sends its usage rows through `$.telemetry` where that noun is
29- * seated and drops them where it is not.
25+ * `claude-md`: nothing beyond the usage row, the engine's CLAUDE.md walk
26+ * standing alone. `managed-only`: the project's and the person's instruction
27+ * files dropped, the organization's kept. `claude-md-or-agents-md` (a project
28+ * with none of its own) and `claude-md-and-agents-md`: AGENTS.md files joined
29+ * to the engine's instruction files, nested ones on a Read. Every mode sends
30+ * its usage rows through `$.telemetry` where that noun is seated and drops
31+ * them where it is not.
3032 *
3133 * @param on the engine's registrar
32- * @param options the plugin's options; `projectInstructions` is `claude`,
33- * `agents-fallback`, `both` or `none`
34+ * @param options the plugin's options; `instructionFiles` is `claude-md`,
35+ * `claude-md-or-agents-md`, `claude-md-and-agents-md` or `managed-only`
3436 */
3537 export function register(on: On, options: PluginOptions): void {
36- const mode = Modes.modeOf(options.projectInstructions)
38+ const named = Modes.modeOf(options.instructionFiles)
39+ // COMPAT_BREAK(agents-md-project-instructions): drop the projectInstructions
40+ // mapping once stored settings have migrated. The host fills in the default,
41+ // so an instructionFiles set to it cannot be told from one left unset.
42+ const legacy = Modes.legacyModeOf(options.projectInstructions)
43+ const isLegacyRead = legacy !== undefined && named === Modes.DEFAULT_MODE
44+ const mode = isLegacyRead ? legacy : named
45+ let isRenameTold = legacy === undefined
3746
3847 on('session.start', ($, e, next) => {
39- void started($, mode, e.isInteractive).catch(() => undefined)
48+ Telemetry.quietly(() =>
49+ $.telemetry.log(Telemetry.modeRowOf(mode, e.isInteractive)),
50+ )
4051
52+ if (!isRenameTold) {
53+ isRenameTold = true
54+ $.ui.log(
55+ isLegacyRead
56+ ? 'option projectInstructions in settings is honoured for now, ' +
57+ `read as instructionFiles ${mode}; set instructionFiles to ` +
58+ `${mode} and remove projectInstructions`
59+ : `option projectInstructions in settings is not read: ` +
60+ `instructionFiles ${mode} is set; remove projectInstructions`,
61+ )
62+ }
63+
4164 return next(e)
4265 })
4366
44- if (mode === 'claude') {
67+ if (mode === 'claude-md') {
4568 return
4669 }
4770
48- if (mode === 'none') {
71+ if (mode === 'managed-only') {
4972 on(
5073 'prompt.context',
5174 { instructionFiles: { kind: Files.DROPPED_KINDS } },
@@ -61,13 +84,13 @@
6184 return
6285 }
6386
64- const isFallback = mode === 'agents-fallback'
87+ const isFallback = mode === 'claude-md-or-agents-md'
6588 const given = new Map<string, Set<string>>()
6689 let inContext: readonly InstructionFile[] = []
6790 let home: string | undefined
6891 let isClaudeProject: boolean | undefined
6992 let rootSeen: string | undefined
70- let isLogged = false
93+ let rootLogged: string | undefined
7194 let isCounted = false
7295
7396 on('prompt.context', async ($, e, next) => {
@@ -81,10 +104,20 @@
81104
82105 const root = isFallback ? await $.session.root() : undefined
83106 rootSeen = root ?? rootSeen
107+ let isWalkFailed = false
108+ // The walk, not only what was handed: the engine can withhold a project
109+ // CLAUDE.md it loaded, and the project still has one.
84110 isClaudeProject =
85111 root !== undefined &&
86- handed.some(file => Files.isClaudeFileOnWalk(file, root))
87- let isWalkFailed = false
112+ (handed.some(file => Files.isClaudeFileOnWalk(file, root)) ||
113+ (await $.fs.ancestors({ names: Names.CLAUDE_NAMES }).then(
114+ files => files.length > 0,
115+ () => {
116+ isWalkFailed = true
117+
118+ return true
119+ },
120+ )))
88121 const found = isClaudeProject
89122 ? NONE
90123 : await $.fs.ancestors({ names: Names.AGENTS_NAMES }).catch(() => {
@@ -98,7 +131,7 @@
98131 isCounted = true
99132 const counts = Telemetry.loadCountsOf(
100133 added,
101- isClaudeProject,
134+ isClaudeProject && !isWalkFailed,
102135 isWalkFailed,
103136 )
104137 Telemetry.quietly(() =>
@@ -107,10 +140,11 @@
107140 Telemetry.quietly(() => $.telemetry.mark(Telemetry.loadMarkOf(counts)))
108141 }
109142
110- const isFirstLoad = isFallback && !isLogged && added.length > 0
143+ const isFirstLoad =
144+ root !== undefined && root !== rootLogged && added.length > 0
111145
112146 if (isFirstLoad) {
113- isLogged = true
147+ rootLogged = root
114148 $.ui.log(
115149 'no CLAUDE.md found; AGENTS.md loaded: ' +
116150 added
@@ -214,66 +248,6 @@
214248 }
215249
216250 /**
217- * What a fresh load does once the session has started, never awaited there:
218- * sends the mode row; under `claude` runs the nudge's two walks, toasts the
219- * nudge for a project with AGENTS.md and no CLAUDE.md and notes it shown;
220- * under any other mode notes a nudge shown earlier as acted on, once.
221- *
222- * A failed walk finds nothing; a store or telemetry call that fails is
223- * dropped. The session's start waits on none of it.
224- *
225- * @param $ the engine, as the `session.start` hook holds it
226- * @param mode the configured mode
227- * @param isInteractive whether a person is at the terminal
228- */
229-async function started(
230- $: EngineInterface,
231- mode: Modes.Mode,
232- isInteractive: boolean,
233-): Promise<void> {
234- if (mode !== 'claude') {
235- Telemetry.quietly(() =>
236- $.telemetry.log(Telemetry.modeRowOf(mode, { isInteractive })),
237- )
238- const noted = await $.store.get(Telemetry.NUDGE_KEY)
239-
240- if (noted === 'shown') {
241- await $.store.set(Telemetry.NUDGE_KEY, 'acted')
242- Telemetry.quietly(() => $.telemetry.log(Telemetry.nudgeRowOf('acted')))
243- }
244-
245- return
246- }
247-
248- const [agents, claude] = await Promise.all([
249- $.fs.ancestors({ names: Names.AGENTS_NAMES }),
250- $.fs.ancestors({ names: Names.CLAUDE_NAMES }),
251- ]).catch((): [typeof NONE, typeof NONE] => [NONE, NONE])
252- const isAgentsOnlyProject = agents.length > 0 && claude.length === 0
253- Telemetry.quietly(() =>
254- $.telemetry.log(
255- Telemetry.modeRowOf(mode, {
256- isInteractive,
257- agentsFileCount: agents.length,
258- claudeFileCount: claude.length,
259- }),
260- ),
261- )
262-
263- if (!isAgentsOnlyProject) {
264- return
265- }
266-
267- $.ui.toast(Names.NUDGE, { timeoutMs: Names.NUDGE_TIMEOUT_MS })
268- Telemetry.quietly(() => $.telemetry.log(Telemetry.nudgeRowOf('shown')))
269- const noted = await $.store.get(Telemetry.NUDGE_KEY)
270-
271- if (noted === undefined) {
272- await $.store.set(Telemetry.NUDGE_KEY, 'shown')
273- }
274-}
275-
276-/**
277251 * The home directory a `~` in a Read's path stands for, read the way the
278252 * Read tool reads it.
279253 *
@@ -8,10 +8,6 @@
88 export * from './mode-row-of.js'
99 export * from './nested-event.js'
1010 export * from './nested-row-of.js'
11-export * from './nudge-event.js'
12-export * from './nudge-key.js'
13-export * from './nudge-outcomes.js'
14-export * from './nudge-row-of.js'
1511 export * from './quietly.js'
1612 export * from './types'
1713 export * from './walk-failed-reason.js'
@@ -8,7 +8,7 @@
88 *
99 * @param added the AGENTS.md instruction files handed to the engine, imports
1010 * among them
11- * @param isYielded whether `agents-fallback` stood down for a CLAUDE.md
11+ * @param isYielded whether `claude-md-or-agents-md` stood down for a CLAUDE.md
1212 * @param isWalkFailed whether the walk threw
1313 * @returns the counts the load row and mark are built from
1414 */
@@ -1,6 +1,6 @@
11 /**
2- * The row the first context of a load sends under `agents-fallback` and
3- * `both`: what was handed on, in counts.
2+ * The row the first context of a load sends under `claude-md-or-agents-md` and
3+ * `claude-md-and-agents-md`: what was handed on, in counts.
44 *
55 * The engine's own initial-load row counts its CLAUDE.md files the same way.
66 */
@@ -1,32 +1,20 @@
11 import type Modes from '../modes'
22 import { modeChoiceOf } from './mode-choice-of.js'
33 import { MODE_EVENT } from './mode-event.js'
4-import type { StartFacts, TelemetryRow } from './types'
4+import type { TelemetryRow } from './types'
55
66 /**
7- * The `session.start` row: the configured mode, whether a person is at the
8- * terminal, and under `claude` the two walk counts the nudge read.
7+ * The `session.start` row: the configured mode and whether a person is at
8+ * the terminal.
99 *
10- * With the counts a project with AGENTS.md alone is told apart (some
11- * AGENTS.md directories, no CLAUDE.md ones) where nothing is loaded.
12- *
1310 * @param mode the configured mode
14- * @param facts what the load knows at the start
11+ * @param isInteractive whether a person is at the terminal
1512 * @returns the row to send
1613 */
1714 export const modeRowOf = (
1815 mode: Modes.Mode,
19- facts: StartFacts,
16+ isInteractive: boolean,
2017 ): TelemetryRow => ({
2118 event: MODE_EVENT,
22- props: {
23- mode: modeChoiceOf(mode),
24- is_interactive: facts.isInteractive,
25- ...(facts.agentsFileCount !== undefined && {
26- agents_file_count: facts.agentsFileCount,
27- }),
28- ...(facts.claudeFileCount !== undefined && {
29- claude_file_count: facts.claudeFileCount,
30- }),
31- },
19+ props: { mode: modeChoiceOf(mode), is_interactive: isInteractive },
3220 })
Dmods/agents-md/hooks/telemetry/nudge-event.ts+0 -4
@@ -1,4 +1,0 @@
1-/**
2- * The row the nudge sends: shown, or acted on.
3- */
4-export const NUDGE_EVENT = 'agents_md_nudge'
Dmods/agents-md/hooks/telemetry/nudge-key.ts+0 -7
@@ -1,7 +1,0 @@
1-/**
2- * The `$.store` key the nudge's outcome is kept under, across sessions.
3- *
4- * Unset until a nudge is shown, then `shown`, then `acted` once a later load
5- * runs under another mode.
6- */
7-export const NUDGE_KEY = 'nudge'
Dmods/agents-md/hooks/telemetry/nudge-outcomes.ts+0 -6
@@ -1,6 +1,0 @@
1-import type { NudgeOutcome } from './types'
2-
3-/**
4- * Every nudge outcome, the closed list its row's `outcome` is chosen from.
5- */
6-export const NUDGE_OUTCOMES: readonly NudgeOutcome[] = ['shown', 'acted']
Dmods/agents-md/hooks/telemetry/nudge-row-of.ts+0 -14
@@ -1,14 +1,0 @@
1-import { NUDGE_EVENT } from './nudge-event.js'
2-import { NUDGE_OUTCOMES } from './nudge-outcomes.js'
3-import type { NudgeOutcome, TelemetryRow } from './types'
4-
5-/**
6- * The nudge's row: shown now, or acted on since.
7- *
8- * @param outcome what happened to the nudge
9- * @returns the row to send
10- */
11-export const nudgeRowOf = (outcome: NudgeOutcome): TelemetryRow => ({
12- event: NUDGE_EVENT,
13- props: { outcome: { value: outcome, of: NUDGE_OUTCOMES } },
14-})
@@ -1,6 +1,4 @@
11 export * from './load-counts'
2-export * from './nudge-outcome'
3-export * from './start-facts'
42 export * from './telemetry-mark'
53 export * from './telemetry-prop'
64 export * from './telemetry-row'
@@ -2,7 +2,7 @@
22 * What the first context of a load handed the engine, as counts.
33 *
44 * The AGENTS.md files added, the files they `@`-imported, their characters
5- * in all, whether `agents-fallback` stood down for a CLAUDE.md of the
5+ * in all, whether `claude-md-or-agents-md` stood down for a CLAUDE.md of the
66 * project's own, and whether the walk threw so that nothing was found.
77 */
88 export type LoadCounts = {
Dmods/agents-md/hooks/telemetry/types/nudge-outcome/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './nudge-outcome.js'
2-
3-export * as default from '.'
Dmods/agents-md/hooks/telemetry/types/nudge-outcome/nudge-outcome.ts+0 -5
@@ -1,5 +1,0 @@
1-/**
2- * What happened to the nudge: `shown`, the toast went up in a session under
3- * `claude`; `acted`, a later load ran under another mode after one was shown.
4- */
5-export type NudgeOutcome = 'shown' | 'acted'
Dmods/agents-md/hooks/telemetry/types/start-facts/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './start-facts.js'
2-
3-export * as default from '.'
Dmods/agents-md/hooks/telemetry/types/start-facts/start-facts.ts+0 -13
@@ -1,13 +1,0 @@
1-/**
2- * What a fresh load knows at `session.start`: whether a person is at the
3- * terminal, and under `claude` the nudge's two walk counts.
4- *
5- * The counts are the directories on the walk holding an AGENTS.md and those
6- * holding a CLAUDE.md; absent under every other mode, which walks nothing at
7- * the start.
8- */
9-export type StartFacts = {
10- isInteractive: boolean
11- agentsFileCount?: number
12- claudeFileCount?: number
13-}
@@ -1,11 +1,8 @@
11 export * from './ancestor-of.js'
2-export * from './late-ms.js'
3-export * from './late-project-of.js'
42 export * from './project-of.js'
53 export * from './recording.js'
64 export * from './rows-of.js'
75 export * from './session.js'
8-export * from './stalled-project-of.js'
96 export * from './started-of.js'
107 export * from './types'
118 export * from './unreadable-project-of.js'
Dmods/agents-md/tests/fixtures/late-ms.ts+0 -4
@@ -1,4 +1,0 @@
1-/**
2- * How long a late project's walks take to answer, on the mock clock.
3- */
4-export const LATE_MS = 5_000
Dmods/agents-md/tests/fixtures/late-project-of.ts+0 -25
@@ -1,25 +1,0 @@
1-import type { FsAncestor, On } from 'claude-code'
2-
3-import { LATE_MS } from './late-ms.js'
4-import { startedOf } from './started-of.js'
5-import type { Started } from './types'
6-
7-/**
8- * A project whose walks answer only once the clock has moved LATE_MS: the
9- * AGENTS.md walk with the given files, the CLAUDE.md walk with none.
10- *
11- * @param on the test's `on`
12- * @param agents what the walk for AGENTS.md and .claude/AGENTS.md finds, late
13- * @returns the toasts and lines the plugins raised, and the clock to move
14- */
15-export function lateProjectOf(on: On, agents: readonly FsAncestor[]): Started {
16- const started = startedOf(on)
17-
18- on('fs.ancestors', async ($, e) => {
19- await started.clock.sleep(LATE_MS)
20-
21- return { value: e.names.includes('AGENTS.md') ? agents : [] }
22- })
23-
24- return started
25-}
@@ -5,26 +5,27 @@
55
66 /**
77 * A project whose walks up from the working directory find the given files,
8- * one list per set of names, its session starting untouched (startedOf).
8+ * one list per set of names, its session starting untouched (startedOf);
9+ * each walk's first name is kept in `walks`.
910 *
1011 * @param on the test's `on`
1112 * @param agents what a walk for AGENTS.md and .claude/AGENTS.md finds
1213 * @param claude what a walk for CLAUDE.md, .claude/CLAUDE.md and
1314 * CLAUDE.local.md finds
14- * @param stored what the plugin's store already holds, by key
15- * @returns the toasts and lines the plugins raised, in order
15+ * @returns the toasts, lines and walks the plugins raised, in order
1616 */
1717 export function projectOf(
1818 on: On,
1919 agents: readonly FsAncestor[],
2020 claude: readonly FsAncestor[],
21- stored: Readonly<Record<string, unknown>> = {},
2221 ): Started {
23- const started = startedOf(on, stored)
22+ const started = startedOf(on)
2423
25- on('fs.ancestors', ($, e) => ({
26- value: e.names.includes('AGENTS.md') ? agents : claude,
27- }))
24+ on('fs.ancestors', ($, e) => {
25+ started.walks.push(e.names[0] ?? '')
2826
27+ return { value: e.names.includes('AGENTS.md') ? agents : claude }
28+ })
29+
2930 return started
3031 }
Dmods/agents-md/tests/fixtures/stalled-project-of.ts+0 -19
@@ -1,19 +1,0 @@
1-import type { On } from 'claude-code'
2-
3-import { startedOf } from './started-of.js'
4-import type { Started } from './types'
5-
6-/**
7- * A project whose walks never answer, its session starting untouched
8- * (startedOf): what a hook that waited on a walk would hang on.
9- *
10- * @param on the test's `on`
11- * @returns the toasts and lines the plugins raised, and the clock
12- */
13-export function stalledProjectOf(on: On): Started {
14- const started = startedOf(on)
15-
16- on('fs.ancestors', () => new Promise<never>(() => undefined))
17-
18- return started
19-}
@@ -1,28 +1,25 @@
11 import type { On } from 'claude-code'
22 import { mock } from 'claude-code/testing'
33
4+import { SESSION } from './session.js'
45 import type { Started, ToastAsked } from './types'
56
67 /**
7- * A session that starts untouched beneath the plugins, every toast and
8- * transcript line they raise kept for the test to read, on a mock clock the
9- * test settles or moves before it reads them, over a store holding `stored`
10- * that the test reads back as `store`.
8+ * A session that starts untouched beneath the plugins, rooted at SESSION's
9+ * working directory, every toast and transcript line they raise kept for the
10+ * test to read, on a mock clock the test settles before it reads them.
1111 *
1212 * @param on the test's `on`
13- * @param stored what the plugin's store already holds, by key
14- * @returns the toasts and lines raised, in order, the store, and the clock
13+ * @returns the toasts, lines and walks raised, in order, and the clock
1514 */
16-export function startedOf(
17- on: On,
18- stored: Readonly<Record<string, unknown>> = {},
19-): Started {
15+export function startedOf(on: On): Started {
2016 const toasts: ToastAsked[] = []
2117 const lines: string[] = []
22- const store = new Map<string, unknown>(Object.entries(stored))
2318
2419 on('session.start', ($, e) => ({ cwd: e.cwd }))
2520
21+ on('session.root', () => ({ value: SESSION.cwd }))
22+
2623 on('ui.toast', ($, e) => {
2724 toasts.push(e)
2825
@@ -35,13 +32,5 @@
3532 return { value: undefined }
3633 })
3734
38- on('store.get', ($, e) => ({ value: store.get(e.key) }))
39-
40- on('store.set', ($, e) => {
41- store.set(e.key, e.value)
42-
43- return { value: undefined }
44- })
45-
46- return { toasts, lines, store, clock: mock.clock(on) }
35+ return { toasts, lines, walks: [], clock: mock.clock(on) }
4736 }
@@ -3,13 +3,13 @@
33 import type { ToastAsked } from '../toast-asked'
44
55 /**
6- * What the plugins raised while a session started: each toast as asked for,
7- * each transcript line, and the plugin's store as it stands; `clock` settles
8- * what the start floated.
6+ * What the plugins raised while a session ran: each toast as asked for, each
7+ * transcript line, the first name of each walk they asked for; `clock`
8+ * settles what they floated.
99 */
1010 export type Started = {
1111 toasts: ToastAsked[]
1212 lines: string[]
13- store: Map<string, unknown>
13+ walks: string[]
1414 clock: MockClock
1515 }
@@ -5,129 +5,99 @@
55
66 tier('builtin')
77
8+/**
9+ * The blocks the engine hands `prompt.context`.
10+ */
11+const BLOCKS = [
12+ { name: 'claudeMd', text: 'x' },
13+ { name: 'currentDate', text: 'today' },
14+]
15+
816 describe('register', () => {
9- test('a project with AGENTS.md alone is told so for 10 s', async ($, on) => {
17+ test('by default a project with AGENTS.md alone gets it', async ($, on) => {
1018 const started = Fixtures.projectOf(
1119 on,
1220 [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
1321 [],
1422 )
1523
16- expect(await $.session.start(Fixtures.SESSION)).toEqual({
17- cwd: '/repo/a/b',
18- })
24+ on('prompt.context', ($, e) => ({
25+ blocks: e.blocks,
26+ instructionFiles: e.instructionFiles,
27+ }))
1928
20- await started.clock.settle()
21-
22- expect(started.toasts).toEqual([
23- { text: Hooks.NUDGE, timeoutMs: Hooks.NUDGE_TIMEOUT_MS },
24- ])
25- expect(Hooks.NUDGE_TIMEOUT_MS).toBe(10_000)
26- expect(started.lines).toEqual([])
27- })
28-
29- test('the start waits on no walk, even one that never answers', async ($, on) => {
30- const stalled = Fixtures.stalledProjectOf(on)
31-
32- expect(await $.session.start(Fixtures.SESSION)).toEqual({
33- cwd: '/repo/a/b',
29+ const context = await $.prompt.context({
30+ blocks: BLOCKS,
31+ instructionFiles: [],
3432 })
3533
36- await stalled.clock.settle()
37-
38- expect(stalled.toasts).toEqual([])
39- })
40-
41- test('walks that answer late still raise the nudge', async ($, on) => {
42- const late = Fixtures.lateProjectOf(on, [
43- Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n'),
34+ expect(context.instructionFiles).toEqual([
35+ { path: '/repo/AGENTS.md', kind: 'project', content: '# top\n' },
4436 ])
37+ expect(context.blocks?.[0]?.text).toContain(
38+ 'Contents of /repo/AGENTS.md (project instructions, checked into the ' +
39+ 'codebase):\n\n# top',
40+ )
4541
46- expect(await $.session.start(Fixtures.SESSION)).toEqual({
47- cwd: '/repo/a/b',
48- })
42+ await started.clock.settle()
4943
50- await late.clock.settle()
51-
52- expect(late.toasts).toEqual([])
53-
54- await late.clock.advance(Fixtures.LATE_MS)
55-
56- expect(late.toasts).toEqual([
57- { text: Hooks.NUDGE, timeoutMs: Hooks.NUDGE_TIMEOUT_MS },
44+ expect(started.lines).toEqual([
45+ 'no CLAUDE.md found; AGENTS.md loaded: /repo/AGENTS.md',
5846 ])
5947 })
6048
61- test('a project with a CLAUDE.md up the walk is left be', async ($, on) => {
49+ test('by default a project with a CLAUDE.md is left to the engine', async ($, on) => {
6250 const started = Fixtures.projectOf(
6351 on,
64- [Fixtures.ancestorOf('/repo/a', '.claude/AGENTS.md', '# a\n')],
65- [Fixtures.ancestorOf('/repo/a/b', 'CLAUDE.local.md', '# mine\n')],
52+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
53+ [Fixtures.ancestorOf('/repo/a/b', 'CLAUDE.md', '# mine\n')],
6654 )
55+ const handed = [
56+ { path: '/repo/a/b/CLAUDE.md', kind: 'project' as const, content: '#' },
57+ ]
6758
68- await $.session.start(Fixtures.SESSION)
69- await started.clock.settle()
59+ on('prompt.context', ($, e) => ({
60+ blocks: e.blocks,
61+ instructionFiles: e.instructionFiles,
62+ }))
7063
71- expect(started.toasts).toEqual([])
72- })
64+ expect(
65+ await $.prompt.context({ blocks: BLOCKS, instructionFiles: handed }),
66+ ).toEqual({ blocks: BLOCKS, instructionFiles: handed })
7367
74- test('a project with neither file is left alone', async ($, on) => {
75- const started = Fixtures.projectOf(on, [], [])
76-
77- await $.session.start(Fixtures.SESSION)
7868 await started.clock.settle()
7969
80- expect(started.toasts).toEqual([])
70+ expect(started.walks).toEqual([])
71+ expect(started.lines).toEqual([])
8172 })
8273
83- test('a failed walk leaves the session starting untouched', async ($, on) => {
84- const started = Fixtures.unreadableProjectOf(on)
74+ test('a failed walk leaves the context as handed', async ($, on) => {
75+ Fixtures.unreadableProjectOf(on)
8576
86- expect(await $.session.start(Fixtures.SESSION)).toEqual({
87- cwd: '/repo/a/b',
88- })
89-
90- await started.clock.settle()
91-
92- expect(started.toasts).toEqual([])
93- })
94-
95- test('by default the context passes through untouched', async ($, on) => {
96- Fixtures.projectOf(
97- on,
98- [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
99- [],
100- )
101- const blocks = [
102- { name: 'claudeMd', text: 'x' },
103- { name: 'currentDate', text: 'today' },
104- ]
105-
10677 on('prompt.context', ($, e) => ({
10778 blocks: e.blocks,
10879 instructionFiles: e.instructionFiles,
10980 }))
11081
111- expect(await $.prompt.context({ blocks, instructionFiles: [] })).toEqual({
112- blocks,
113- instructionFiles: [],
114- })
82+ expect(
83+ await $.prompt.context({ blocks: BLOCKS, instructionFiles: [] }),
84+ ).toEqual({ blocks: BLOCKS, instructionFiles: [] })
11585 })
11686
11787 test(
118- 'the start sends its mode with the walk counts, the nudge its row',
88+ 'the start sends the mode row alone: no walk, no toast',
11989 { plugins: [Fixtures.RECORDING] },
12090 async ($, on) => {
12191 const started = Fixtures.projectOf(
12292 on,
123- [
124- Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n'),
125- Fixtures.ancestorOf('/repo/a', '.claude/AGENTS.md', '# a\n'),
126- ],
93+ [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
12794 [],
12895 )
12996
130- await $.session.start(Fixtures.SESSION)
97+ expect(await $.session.start(Fixtures.SESSION)).toEqual({
98+ cwd: '/repo/a/b',
99+ })
100+
131101 await started.clock.settle()
132102
133103 expect(Fixtures.rowsOf(started)).toEqual([
@@ -136,93 +106,32 @@
136106 {
137107 event: Hooks.MODE_EVENT,
138108 props: {
139- mode: { value: 'claude', of: [...Hooks.MODES] },
109+ mode: { value: 'claude-md-or-agents-md', of: [...Hooks.MODES] },
140110 is_interactive: true,
141- agents_file_count: 2,
142- claude_file_count: 0,
143111 },
144112 },
145113 ],
146- [
147- 'log',
148- {
149- event: Hooks.NUDGE_EVENT,
150- props: { outcome: { value: 'shown', of: ['shown', 'acted'] } },
151- },
152- ],
153114 ])
154115 expect(Hooks.MODE_EVENT).toBe('agents_md_mode')
155- expect(Hooks.NUDGE_EVENT).toBe('agents_md_nudge')
156- expect(started.toasts).toEqual([
157- { text: Hooks.NUDGE, timeoutMs: Hooks.NUDGE_TIMEOUT_MS },
158- ])
159- expect(started.store.get(Hooks.NUDGE_KEY)).toBe('shown')
116+ expect(started.walks).toEqual([])
117+ expect(started.toasts).toEqual([])
160118 },
161119 )
162120
163- test(
164- 'a project with CLAUDE.md sends the mode row alone, no paths in it',
165- { plugins: [Fixtures.RECORDING] },
166- async ($, on) => {
167- const started = Fixtures.projectOf(
168- on,
169- [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
170- [Fixtures.ancestorOf('/repo/a/b', 'CLAUDE.md', '# mine\n')],
171- )
172-
173- await $.session.start({ ...Fixtures.SESSION, isInteractive: false })
174- await started.clock.settle()
175-
176- expect(Fixtures.rowsOf(started)).toEqual([
177- [
178- 'log',
179- {
180- event: Hooks.MODE_EVENT,
181- props: {
182- mode: { value: 'claude', of: [...Hooks.MODES] },
183- is_interactive: false,
184- agents_file_count: 1,
185- claude_file_count: 1,
186- },
187- },
188- ],
189- ])
190- expect(started.lines.join('\n')).not.toContain('/repo')
191- expect(started.store.has(Hooks.NUDGE_KEY)).toBe(false)
192- },
193- )
194-
195- test(
196- 'a nudge acted on earlier is not marked shown again',
197- { plugins: [Fixtures.RECORDING] },
198- async ($, on) => {
199- const started = Fixtures.projectOf(
200- on,
201- [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
202- [],
203- { [Hooks.NUDGE_KEY]: 'acted' },
204- )
205-
206- await $.session.start(Fixtures.SESSION)
207- await started.clock.settle()
208-
209- expect(started.toasts).toHaveLength(1)
210- expect(started.store.get(Hooks.NUDGE_KEY)).toBe('acted')
211- },
212- )
213-
214- test('with no telemetry noun the nudge shows and nothing throws', async ($, on) => {
121+ test('with no telemetry noun the start goes on untouched', async ($, on) => {
215122 const started = Fixtures.projectOf(
216123 on,
217124 [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')],
218125 [],
219126 )
220127
221- await $.session.start(Fixtures.SESSION)
128+ expect(
129+ await $.session.start({ ...Fixtures.SESSION, isInteractive: false }),
130+ ).toEqual({ cwd: '/repo/a/b' })
131+
222132 await started.clock.settle()
223133
224- expect(started.toasts).toHaveLength(1)
225134 expect(Fixtures.rowsOf(started)).toEqual([])
226- expect(started.store.get(Hooks.NUDGE_KEY)).toBe('shown')
135+ expect(started.toasts).toEqual([])
227136 })
228137 })
Large file, diff not shown.