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
| @@ -10,7 +10,7 @@ | ||
| 10 | 10 | | [`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 | |
| 11 | 11 | | [`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 | |
| 12 | 12 | | [`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 | | |
| 14 | 14 | |
| 15 | 15 | Each folder is a complete plugin: `.claude-plugin/plugin.json`, a |
| 16 | 16 | `hooks/hooks.json` naming the module, and TypeScript under `hooks/` typed |
| @@ -1,18 +1,23 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "agents-md", |
| 3 | 3 | "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.", | |
| 5 | 5 | "author": { |
| 6 | 6 | "name": "Anthropic" |
| 7 | 7 | }, |
| 8 | 8 | "userConfig": { |
| 9 | - "projectInstructions": { | |
| 9 | + "instructionFiles": { | |
| 10 | 10 | "type": "string", |
| 11 | 11 | "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.", | |
| 13 | 13 | "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 | + ] | |
| 16 | 21 | } |
| 17 | 22 | } |
| 18 | 23 | } |
| @@ -1,60 +1,59 @@ | ||
| 1 | 1 | # agents-md |
| 2 | 2 | |
| 3 | 3 | `AGENTS.md` read the way Claude Code reads `CLAUDE.md`, as a plugin, under |
| 4 | -one option, `projectInstructions`: | |
| 4 | +one option, `instructionFiles`: | |
| 5 | 5 | |
| 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 | |
| 27 | 25 | `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.) | |
| 31 | 29 | |
| 32 | 30 | 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. | |
| 44 | 42 | |
| 45 | 43 | `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>`. | |
| 47 | 46 | |
| 48 | 47 | ## Setting the option |
| 49 | 48 | |
| 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 | |
| 52 | 51 | |
| 53 | 52 | ```json |
| 54 | 53 | { |
| 55 | 54 | "pluginConfigs": { |
| 56 | 55 | "agents-md@builtin": { |
| 57 | - "options": { "projectInstructions": "agents-fallback" } | |
| 56 | + "options": { "instructionFiles": "claude-md-and-agents-md" } | |
| 58 | 57 | } |
| 59 | 58 | } |
| 60 | 59 | } |
| @@ -64,10 +63,21 @@ | ||
| 64 | 63 | settings; a project's `.claude/settings.json` is not read for plugin |
| 65 | 64 | options. Changing it reloads the module, and the next context the engine |
| 66 | 65 | 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. | |
| 70 | 70 | |
| 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 | + | |
| 71 | 81 | Run from this folder instead (`claude --plugin-dir mods/agents-md`), the |
| 72 | 82 | same entry is keyed `"agents-md"`. |
| 73 | 83 | |
| @@ -75,22 +85,24 @@ | ||
| 75 | 85 | |
| 76 | 86 | | event | what the hook does | |
| 77 | 87 | | --- | --- | |
| 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 | | |
| 82 | 92 | |
| 83 | 93 | ## What it calls on `$` |
| 84 | 94 | |
| 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`, | |
| 89 | 101 | `telemetry.log` and `telemetry.mark`. |
| 90 | 102 | |
| 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. | |
| 94 | 106 | |
| 95 | 107 | ## What it logs |
| 96 | 108 | |
| @@ -100,14 +112,15 @@ | ||
| 100 | 112 | |
| 101 | 113 | | event | when | properties | |
| 102 | 114 | | --- | --- | --- | |
| 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` | | |
| 105 | 117 | | `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 | | |
| 107 | 118 | |
| 108 | 119 | ## Where it still differs from CLAUDE.md |
| 109 | 120 | |
| 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. | |
| 111 | 124 | |
| 112 | 125 | 1. Nested files attach on a text `Read` only. The engine also attaches a |
| 113 | 126 | directory's `CLAUDE.md` for a file `@`-mentioned in the prompt, for the |
| @@ -125,10 +138,14 @@ | ||
| 125 | 138 | working directory before deciding a file is inside it. |
| 126 | 139 | 5. `--add-dir` directories contribute no `AGENTS.md`, where the engine can |
| 127 | 140 | 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). | |
| 129 | 144 | 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. | |
| 132 | 149 | 8. A subagent that is not a fork gets a nested `AGENTS.md` at its own first |
| 133 | 150 | `Read` under that directory even when its parent's loop was already given |
| 134 | 151 | it; the engine does not hand such a subagent the nested `CLAUDE.md` again. |
| @@ -138,6 +155,9 @@ | ||
| 138 | 155 | |
| 139 | 156 | claude plugin test mods/agents-md |
| 140 | 157 | |
| 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 @@ | ||
| 1 | 1 | import type { InstructionFileKind } from 'claude-code' |
| 2 | 2 | |
| 3 | 3 | /** |
| 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. | |
| 6 | 7 | */ |
| 7 | 8 | export const DROPPED_KINDS: readonly InstructionFileKind[] = [ |
| 8 | 9 | 'project', |
| @@ -9,9 +9,9 @@ | ||
| 9 | 9 | * Whether a handed instruction file is a CLAUDE.md, `.claude/CLAUDE.md` or |
| 10 | 10 | * CLAUDE.local.md of a directory on the walk down to the session's root. |
| 11 | 11 | * |
| 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. | |
| 15 | 15 | * |
| 16 | 16 | * @param file the handed instruction file |
| 17 | 17 | * @param root the session's project root, absolute |
| @@ -3,11 +3,11 @@ | ||
| 3 | 3 | import { DROPPED_KINDS } from './dropped-kinds.js' |
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | - * Whether `none` keeps an instruction file: the organization's managed | |
| 6 | + * Whether `managed-only` keeps an instruction file: the organization's managed | |
| 7 | 7 | * files and the engine's memory stay, the project's and the person's go. |
| 8 | 8 | * |
| 9 | 9 | * @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 | |
| 11 | 11 | */ |
| 12 | 12 | export const isKeptWithoutInstructions = (file: InstructionFile): boolean => |
| 13 | 13 | !DROPPED_KINDS.includes(file.kind) |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | { |
| 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", | |
| 3 | 3 | "modules": ["./register.ts"] |
| 4 | 4 | } |
| @@ -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' | |
| 1 | 3 | export * from './mode-of.js' |
| 2 | 4 | export * from './modes.js' |
| 3 | 5 | 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' | |
| 1 | 2 | import { MODES } from './modes.js' |
| 2 | 3 | import type { Mode } from './types' |
| 3 | 4 | |
| 4 | 5 | /** |
| 5 | 6 | * 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. | |
| 7 | 8 | * |
| 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 | |
| 10 | 11 | */ |
| 11 | 12 | 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 @@ | ||
| 4 | 4 | * Every mode, in the order the option's description names them. |
| 5 | 5 | */ |
| 6 | 6 | 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', | |
| 11 | 11 | ] |
| @@ -1,10 +1,15 @@ | ||
| 1 | 1 | /** |
| 2 | - * Which project instructions the plugin loads: the `projectInstructions` | |
| 2 | + * Which project instructions the plugin loads: the `instructionFiles` | |
| 3 | 3 | * option. |
| 4 | 4 | * |
| 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. | |
| 9 | 10 | */ |
| 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 @@ | ||
| 1 | 1 | export * from './agents-names.js' |
| 2 | 2 | export * from './claude-names.js' |
| 3 | 3 | export * from './main-loop.js' |
| 4 | -export * from './nudge.js' | |
| 5 | -export * from './nudge-timeout-ms.js' | |
| 6 | 4 | |
| 7 | 5 | export * as default from '.' |
| @@ -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 |
| @@ -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 @@ | ||
| 18 | 18 | const NONE: readonly FsAncestor[] = [] |
| 19 | 19 | |
| 20 | 20 | /** |
| 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). | |
| 23 | 24 | * |
| 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. | |
| 30 | 32 | * |
| 31 | 33 | * @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` | |
| 34 | 36 | */ |
| 35 | 37 | 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 | |
| 37 | 46 | |
| 38 | 47 | 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 | + ) | |
| 40 | 51 | |
| 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 | + | |
| 41 | 64 | return next(e) |
| 42 | 65 | }) |
| 43 | 66 | |
| 44 | - if (mode === 'claude') { | |
| 67 | + if (mode === 'claude-md') { | |
| 45 | 68 | return |
| 46 | 69 | } |
| 47 | 70 | |
| 48 | - if (mode === 'none') { | |
| 71 | + if (mode === 'managed-only') { | |
| 49 | 72 | on( |
| 50 | 73 | 'prompt.context', |
| 51 | 74 | { instructionFiles: { kind: Files.DROPPED_KINDS } }, |
| @@ -61,13 +84,13 @@ | ||
| 61 | 84 | return |
| 62 | 85 | } |
| 63 | 86 | |
| 64 | - const isFallback = mode === 'agents-fallback' | |
| 87 | + const isFallback = mode === 'claude-md-or-agents-md' | |
| 65 | 88 | const given = new Map<string, Set<string>>() |
| 66 | 89 | let inContext: readonly InstructionFile[] = [] |
| 67 | 90 | let home: string | undefined |
| 68 | 91 | let isClaudeProject: boolean | undefined |
| 69 | 92 | let rootSeen: string | undefined |
| 70 | - let isLogged = false | |
| 93 | + let rootLogged: string | undefined | |
| 71 | 94 | let isCounted = false |
| 72 | 95 | |
| 73 | 96 | on('prompt.context', async ($, e, next) => { |
| @@ -81,10 +104,20 @@ | ||
| 81 | 104 | |
| 82 | 105 | const root = isFallback ? await $.session.root() : undefined |
| 83 | 106 | 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. | |
| 84 | 110 | isClaudeProject = |
| 85 | 111 | 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 | + ))) | |
| 88 | 121 | const found = isClaudeProject |
| 89 | 122 | ? NONE |
| 90 | 123 | : await $.fs.ancestors({ names: Names.AGENTS_NAMES }).catch(() => { |
| @@ -98,7 +131,7 @@ | ||
| 98 | 131 | isCounted = true |
| 99 | 132 | const counts = Telemetry.loadCountsOf( |
| 100 | 133 | added, |
| 101 | - isClaudeProject, | |
| 134 | + isClaudeProject && !isWalkFailed, | |
| 102 | 135 | isWalkFailed, |
| 103 | 136 | ) |
| 104 | 137 | Telemetry.quietly(() => |
| @@ -107,10 +140,11 @@ | ||
| 107 | 140 | Telemetry.quietly(() => $.telemetry.mark(Telemetry.loadMarkOf(counts))) |
| 108 | 141 | } |
| 109 | 142 | |
| 110 | - const isFirstLoad = isFallback && !isLogged && added.length > 0 | |
| 143 | + const isFirstLoad = | |
| 144 | + root !== undefined && root !== rootLogged && added.length > 0 | |
| 111 | 145 | |
| 112 | 146 | if (isFirstLoad) { |
| 113 | - isLogged = true | |
| 147 | + rootLogged = root | |
| 114 | 148 | $.ui.log( |
| 115 | 149 | 'no CLAUDE.md found; AGENTS.md loaded: ' + |
| 116 | 150 | added |
| @@ -214,66 +248,6 @@ | ||
| 214 | 248 | } |
| 215 | 249 | |
| 216 | 250 | /** |
| 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 | -/** | |
| 277 | 251 | * The home directory a `~` in a Read's path stands for, read the way the |
| 278 | 252 | * Read tool reads it. |
| 279 | 253 | * |
| @@ -8,10 +8,6 @@ | ||
| 8 | 8 | export * from './mode-row-of.js' |
| 9 | 9 | export * from './nested-event.js' |
| 10 | 10 | 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' | |
| 15 | 11 | export * from './quietly.js' |
| 16 | 12 | export * from './types' |
| 17 | 13 | export * from './walk-failed-reason.js' |
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | * |
| 9 | 9 | * @param added the AGENTS.md instruction files handed to the engine, imports |
| 10 | 10 | * 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 | |
| 12 | 12 | * @param isWalkFailed whether the walk threw |
| 13 | 13 | * @returns the counts the load row and mark are built from |
| 14 | 14 | */ |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /** |
| 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. | |
| 4 | 4 | * |
| 5 | 5 | * The engine's own initial-load row counts its CLAUDE.md files the same way. |
| 6 | 6 | */ |
| @@ -1,32 +1,20 @@ | ||
| 1 | 1 | import type Modes from '../modes' |
| 2 | 2 | import { modeChoiceOf } from './mode-choice-of.js' |
| 3 | 3 | import { MODE_EVENT } from './mode-event.js' |
| 4 | -import type { StartFacts, TelemetryRow } from './types' | |
| 4 | +import type { TelemetryRow } from './types' | |
| 5 | 5 | |
| 6 | 6 | /** |
| 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. | |
| 9 | 9 | * |
| 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 | - * | |
| 13 | 10 | * @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 | |
| 15 | 12 | * @returns the row to send |
| 16 | 13 | */ |
| 17 | 14 | export const modeRowOf = ( |
| 18 | 15 | mode: Modes.Mode, |
| 19 | - facts: StartFacts, | |
| 16 | + isInteractive: boolean, | |
| 20 | 17 | ): TelemetryRow => ({ |
| 21 | 18 | 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 }, | |
| 32 | 20 | }) |
| @@ -1,4 +1,0 @@ | ||
| 1 | -/** | |
| 2 | - * The row the nudge sends: shown, or acted on. | |
| 3 | - */ | |
| 4 | -export const NUDGE_EVENT = 'agents_md_nudge' |
| @@ -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' |
| @@ -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'] |
| @@ -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 @@ | ||
| 1 | 1 | export * from './load-counts' |
| 2 | -export * from './nudge-outcome' | |
| 3 | -export * from './start-facts' | |
| 4 | 2 | export * from './telemetry-mark' |
| 5 | 3 | export * from './telemetry-prop' |
| 6 | 4 | export * from './telemetry-row' |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | * What the first context of a load handed the engine, as counts. |
| 3 | 3 | * |
| 4 | 4 | * 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 | |
| 6 | 6 | * project's own, and whether the walk threw so that nothing was found. |
| 7 | 7 | */ |
| 8 | 8 | export type LoadCounts = { |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './nudge-outcome.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -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' |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './start-facts.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -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 @@ | ||
| 1 | 1 | export * from './ancestor-of.js' |
| 2 | -export * from './late-ms.js' | |
| 3 | -export * from './late-project-of.js' | |
| 4 | 2 | export * from './project-of.js' |
| 5 | 3 | export * from './recording.js' |
| 6 | 4 | export * from './rows-of.js' |
| 7 | 5 | export * from './session.js' |
| 8 | -export * from './stalled-project-of.js' | |
| 9 | 6 | export * from './started-of.js' |
| 10 | 7 | export * from './types' |
| 11 | 8 | export * from './unreadable-project-of.js' |
| @@ -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 |
| @@ -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 @@ | ||
| 5 | 5 | |
| 6 | 6 | /** |
| 7 | 7 | * 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`. | |
| 9 | 10 | * |
| 10 | 11 | * @param on the test's `on` |
| 11 | 12 | * @param agents what a walk for AGENTS.md and .claude/AGENTS.md finds |
| 12 | 13 | * @param claude what a walk for CLAUDE.md, .claude/CLAUDE.md and |
| 13 | 14 | * 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 | |
| 16 | 16 | */ |
| 17 | 17 | export function projectOf( |
| 18 | 18 | on: On, |
| 19 | 19 | agents: readonly FsAncestor[], |
| 20 | 20 | claude: readonly FsAncestor[], |
| 21 | - stored: Readonly<Record<string, unknown>> = {}, | |
| 22 | 21 | ): Started { |
| 23 | - const started = startedOf(on, stored) | |
| 22 | + const started = startedOf(on) | |
| 24 | 23 | |
| 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] ?? '') | |
| 28 | 26 | |
| 27 | + return { value: e.names.includes('AGENTS.md') ? agents : claude } | |
| 28 | + }) | |
| 29 | + | |
| 29 | 30 | return started |
| 30 | 31 | } |
| @@ -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 @@ | ||
| 1 | 1 | import type { On } from 'claude-code' |
| 2 | 2 | import { mock } from 'claude-code/testing' |
| 3 | 3 | |
| 4 | +import { SESSION } from './session.js' | |
| 4 | 5 | import type { Started, ToastAsked } from './types' |
| 5 | 6 | |
| 6 | 7 | /** |
| 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. | |
| 11 | 11 | * |
| 12 | 12 | * @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 | |
| 15 | 14 | */ |
| 16 | -export function startedOf( | |
| 17 | - on: On, | |
| 18 | - stored: Readonly<Record<string, unknown>> = {}, | |
| 19 | -): Started { | |
| 15 | +export function startedOf(on: On): Started { | |
| 20 | 16 | const toasts: ToastAsked[] = [] |
| 21 | 17 | const lines: string[] = [] |
| 22 | - const store = new Map<string, unknown>(Object.entries(stored)) | |
| 23 | 18 | |
| 24 | 19 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 25 | 20 | |
| 21 | + on('session.root', () => ({ value: SESSION.cwd })) | |
| 22 | + | |
| 26 | 23 | on('ui.toast', ($, e) => { |
| 27 | 24 | toasts.push(e) |
| 28 | 25 | |
| @@ -35,13 +32,5 @@ | ||
| 35 | 32 | return { value: undefined } |
| 36 | 33 | }) |
| 37 | 34 | |
| 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) } | |
| 47 | 36 | } |
| @@ -3,13 +3,13 @@ | ||
| 3 | 3 | import type { ToastAsked } from '../toast-asked' |
| 4 | 4 | |
| 5 | 5 | /** |
| 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. | |
| 9 | 9 | */ |
| 10 | 10 | export type Started = { |
| 11 | 11 | toasts: ToastAsked[] |
| 12 | 12 | lines: string[] |
| 13 | - store: Map<string, unknown> | |
| 13 | + walks: string[] | |
| 14 | 14 | clock: MockClock |
| 15 | 15 | } |
| @@ -5,129 +5,99 @@ | ||
| 5 | 5 | |
| 6 | 6 | tier('builtin') |
| 7 | 7 | |
| 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 | + | |
| 8 | 16 | 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) => { | |
| 10 | 18 | const started = Fixtures.projectOf( |
| 11 | 19 | on, |
| 12 | 20 | [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')], |
| 13 | 21 | [], |
| 14 | 22 | ) |
| 15 | 23 | |
| 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 | + })) | |
| 19 | 28 | |
| 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: [], | |
| 34 | 32 | }) |
| 35 | 33 | |
| 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' }, | |
| 44 | 36 | ]) |
| 37 | + expect(context.blocks?.[0]?.text).toContain( | |
| 38 | + 'Contents of /repo/AGENTS.md (project instructions, checked into the ' + | |
| 39 | + 'codebase):\n\n# top', | |
| 40 | + ) | |
| 45 | 41 | |
| 46 | - expect(await $.session.start(Fixtures.SESSION)).toEqual({ | |
| 47 | - cwd: '/repo/a/b', | |
| 48 | - }) | |
| 42 | + await started.clock.settle() | |
| 49 | 43 | |
| 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', | |
| 58 | 46 | ]) |
| 59 | 47 | }) |
| 60 | 48 | |
| 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) => { | |
| 62 | 50 | const started = Fixtures.projectOf( |
| 63 | 51 | 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')], | |
| 66 | 54 | ) |
| 55 | + const handed = [ | |
| 56 | + { path: '/repo/a/b/CLAUDE.md', kind: 'project' as const, content: '#' }, | |
| 57 | + ] | |
| 67 | 58 | |
| 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 | + })) | |
| 70 | 63 | |
| 71 | - expect(started.toasts).toEqual([]) | |
| 72 | - }) | |
| 64 | + expect( | |
| 65 | + await $.prompt.context({ blocks: BLOCKS, instructionFiles: handed }), | |
| 66 | + ).toEqual({ blocks: BLOCKS, instructionFiles: handed }) | |
| 73 | 67 | |
| 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) | |
| 78 | 68 | await started.clock.settle() |
| 79 | 69 | |
| 80 | - expect(started.toasts).toEqual([]) | |
| 70 | + expect(started.walks).toEqual([]) | |
| 71 | + expect(started.lines).toEqual([]) | |
| 81 | 72 | }) |
| 82 | 73 | |
| 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) | |
| 85 | 76 | |
| 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 | - | |
| 106 | 77 | on('prompt.context', ($, e) => ({ |
| 107 | 78 | blocks: e.blocks, |
| 108 | 79 | instructionFiles: e.instructionFiles, |
| 109 | 80 | })) |
| 110 | 81 | |
| 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: [] }) | |
| 115 | 85 | }) |
| 116 | 86 | |
| 117 | 87 | 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', | |
| 119 | 89 | { plugins: [Fixtures.RECORDING] }, |
| 120 | 90 | async ($, on) => { |
| 121 | 91 | const started = Fixtures.projectOf( |
| 122 | 92 | 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')], | |
| 127 | 94 | [], |
| 128 | 95 | ) |
| 129 | 96 | |
| 130 | - await $.session.start(Fixtures.SESSION) | |
| 97 | + expect(await $.session.start(Fixtures.SESSION)).toEqual({ | |
| 98 | + cwd: '/repo/a/b', | |
| 99 | + }) | |
| 100 | + | |
| 131 | 101 | await started.clock.settle() |
| 132 | 102 | |
| 133 | 103 | expect(Fixtures.rowsOf(started)).toEqual([ |
| @@ -136,93 +106,32 @@ | ||
| 136 | 106 | { |
| 137 | 107 | event: Hooks.MODE_EVENT, |
| 138 | 108 | props: { |
| 139 | - mode: { value: 'claude', of: [...Hooks.MODES] }, | |
| 109 | + mode: { value: 'claude-md-or-agents-md', of: [...Hooks.MODES] }, | |
| 140 | 110 | is_interactive: true, |
| 141 | - agents_file_count: 2, | |
| 142 | - claude_file_count: 0, | |
| 143 | 111 | }, |
| 144 | 112 | }, |
| 145 | 113 | ], |
| 146 | - [ | |
| 147 | - 'log', | |
| 148 | - { | |
| 149 | - event: Hooks.NUDGE_EVENT, | |
| 150 | - props: { outcome: { value: 'shown', of: ['shown', 'acted'] } }, | |
| 151 | - }, | |
| 152 | - ], | |
| 153 | 114 | ]) |
| 154 | 115 | 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([]) | |
| 160 | 118 | }, |
| 161 | 119 | ) |
| 162 | 120 | |
| 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) => { | |
| 215 | 122 | const started = Fixtures.projectOf( |
| 216 | 123 | on, |
| 217 | 124 | [Fixtures.ancestorOf('/repo', 'AGENTS.md', '# top\n')], |
| 218 | 125 | [], |
| 219 | 126 | ) |
| 220 | 127 | |
| 221 | - await $.session.start(Fixtures.SESSION) | |
| 128 | + expect( | |
| 129 | + await $.session.start({ ...Fixtures.SESSION, isInteractive: false }), | |
| 130 | + ).toEqual({ cwd: '/repo/a/b' }) | |
| 131 | + | |
| 222 | 132 | await started.clock.settle() |
| 223 | 133 | |
| 224 | - expect(started.toasts).toHaveLength(1) | |
| 225 | 134 | expect(Fixtures.rowsOf(started)).toEqual([]) |
| 226 | - expect(started.store.get(Hooks.NUDGE_KEY)).toBe('shown') | |
| 135 | + expect(started.toasts).toEqual([]) | |
| 227 | 136 | }) |
| 228 | 137 | }) |
Large file, diff not shown.