SLOPSHOPPER

dcc-plugin-agents-md

Loads `AGENTS.md` files where Claude Code loads `CLAUDE.md`: the user folder, each ancestor of the working directory, nested folders on each read or edit, and…

newguardprompt
★ 3v1.0.0MITupdated 2026-10-13xsyetopz/claudetto/plugins/dcc-plugin-agents-md
A shopper browsing a rack in a slop shop
README

dotclaude

dotclaude changes Claude Code through its official extension points: settings, hooks, skills, agents, and an output style. It does not patch the Claude Code binary. Its goal is the highest quality for the lowest token cost.

The wiki explains each part and the reason for it. Start with the Home page, or read the Install page for the full setup.

Plugins

dotclaude is a marketplace with one core plugin and seven add-on plugins. Each add-on works without the core plugin.

PluginWhat it gives
dotclaudeThe core: the settings snippet, the status line, the output style, six agents, ten skills, the guard and asks hooks, the spec store, and the second_opinion tool.
dotclaude-codegraphThe code graph of a project, from the CLI codegraph.
dotclaude-browserThe browse skill, and a headless browser search for each WebSearch call.
dotclaude-docsThe docs skill, with current library docs from the CLI ctx7.
dotclaude-sessionsThe start-cost and usage skills, which measure the session start and the usage.
dotclaude-labTools for plugin authors: the sandbox, capture, schema, and eval compare.
dcc-plugin-agents-mdLoads AGENTS.md files where Claude Code loads CLAUDE.md, in place of the built-in cc-plugin-agents-md.
dcc-plugin-you-should-knowOpt-in. Shows one thing to know before Claude changes something, in place of the built-in cc-plugin-you-should-know.

Install

/plugin marketplace add xsyetopz/dotclaude
/plugin install dotclaude@dotclaude

Then run /dotclaude:setup, and restart Claude Code. The setup skill shows each change to your settings file, and it writes the change after you confirm.

Requirements:

  • Claude Code 2.1.296 or later. dotclaude is tested only on 2.1.296.
  • git.
  • Bun 1.4.3 or later on PATH, for the status line, the setup command, and the second_opinion tool.
  • Node.js on PATH, for the npm installs of the add-ons.

A plugin cannot set permissions, environment variables, or models. So the core plugin gives these settings as a settings snippet. The setup skill applies the snippet for you. You can also copy the keys that you want into your own settings.json.

What the core plugin changes

PartEffect
CompactionOff. autoCompactEnabled is false, and DISABLE_COMPACT is 1, which also turns off /compact.
Context windowCLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW are 300000. A session that reaches the limit stops, and you run /clear.
MemoryautoMemoryEnabled is false. The spec store holds the state instead.
Output styleoutputStyle is dotclaude:Concise. Your own outputStyle setting can change it.
PermissionsNormal work passes. Work like sudo asks. Destructive actions are denied by autoMode.hard_deny. Reads of secret files are denied.
SandboxBash runs in the sandbox. Its network reaches GitHub and the package registries only.
GuardAsks before the first call of a session to a GitHub repository of another owner that has an AI policy file.
AsksAsks only when a change weakens a check. It never asks for a read.
Status lineThe folder, git, model, effort, context against the window, cost, the 5-hour, 7-day, and spend limits, and the progress of a spec change. The percent turns yellow at 75% and red at 90%.
HandoffWith the default manual value of mainHandover, a hint at 60% of the window tells you to run /dotclaude:handoff.
ChecksThe CLI dotclaude-run-checks runs a batch of check commands, with no model call.

The core plugin has one option, mainHandover. Its values are auto, manual, and off, and the default is manual.

Agents and models

AgentModelEffort
planner, reviewerclaude-opus-5-5high
debuggerclaude-sonnet-5-5high
implementer, researcherclaude-sonnet-5-5medium
investigatorclaude-haiku-5-5medium

The main session uses claude-opus-5-5, from the settings snippet. The maxEffortLevel setting is max, so /effort can go up to max for a hard check. See Agents.

The ten skills are /dotclaude:handoff, /dotclaude:rules-creator, /dotclaude:setup, and the seven spec phases /dotclaude:specify, /dotclaude:plan, /dotclaude:implement, /dotclaude:review, /dotclaude:merge, /dotclaude:accept, and /dotclaude:archive. See Skills.

Long work without compaction

Each model call reads the whole context again, so the size of the context sets the cost of each turn.

  • For work that takes more than one session, run /dotclaude:specify <id>, or run dotclaude-spec specify <id>.
  • Before /clear, run dotclaude-spec status <id>. In the fresh session, run dotclaude-spec next <id>, which prints the brief of the next package.
  • A model change rebuilds the prompt cache. An effort change keeps the prompt cache.

See Context and handoff and Spec store.

Second opinion

The second_opinion tool of the core plugin asks TypeSafe Jev for a pick and a confidence on a decision. Export TYPESAFE_API_KEY before Claude Code starts. Each call sends the data of the decision to the TypeSafe API. With no key, or when Jev gives no answer, a small Claude model answers instead. The result says that the answer is not from Jev. See Second opinion.

Add-on plugins

Install each add-on that you need.

/plugin install dotclaude-codegraph@dotclaude
/plugin install dotclaude-browser@dotclaude
/plugin install dotclaude-docs@dotclaude
/plugin install dotclaude-sessions@dotclaude
/plugin install dotclaude-lab@dotclaude
/plugin install dcc-plugin-agents-md@dotclaude
/plugin install dcc-plugin-you-should-know@dotclaude
  • dotclaude-codegraph needs the CLI codegraph on PATH, and an index that codegraph init -y builds in the project. It syncs the index at session start, and it adds no MCP server. See Code graph.
  • dotclaude-browser uses agent-browser. Its backend option can select CloakBrowser, for sites with bot detection. See Browser.
  • dotclaude-docs needs no key. See Library docs.
  • dotclaude-sessions measures the start of a session and the usage of each run. See Sessions.
  • dotclaude-lab is for plugin authors, and it works alone. See Lab.
  • dcc-plugin-agents-md replaces the built-in cc-plugin-agents-md, so turn the built-in plugin off. See AGENTS.md.
  • dcc-plugin-you-should-know is opt-in, because each check makes one Haiku call, on an API key or a subscription. It replaces the built-in cc-plugin-you-should-know, so turn the built-in plugin off. See You should know.

Update

claude plugin marketplace update dotclaude
claude plugin update dotclaude@dotclaude

Run the claude plugin update line again for each add-on that you installed, with its name in place of dotclaude. Then restart Claude Code, read the changelog, and run /dotclaude:setup again. The status line commands hold the path of the plugin version, so each update needs the new setup run.

To try an unreleased checkout, run:

claude --plugin-dir /path/to/dotclaude/plugins/dotclaude

Development

Edit the source in src/, and run just generate. Do not edit plugins/ by hand. just check runs the lint, the type check, the check of the generated files, the tests, the validation, and the hook labs. just sandbox runs Claude Code with this checkout in a separate config. See Development and Sandbox.

License

MIT.

Source 5 files
hooks/register.ts 20 lines
1// Generated by `just generate` from `src/plugins/dcc-plugin-agents-md/slices/*/slice.ts`. Edit the source and run `just generate`.
2import type { On } from "claude-code";
3import {
4  onCompact as h0,
5  onContext as h1,
6  onMention as h2,
7  onToolCall as h3,
8} from "../slices/agents/adapters/mod.ts";
9
10export const register = (on: On): void => {
11  // eval: prompt.context agents
12  on("prompt.context", h1);
13  // eval: prompt.mention agents
14  on("prompt.mention", h2);
15  // eval: session.compact agents
16  on("session.compact", h0);
17  // eval: tool.call agents
18  on("tool.call", { tool: ["Read", "Edit", "Write", "Bash"] }, h3);
19};
20
slices/agents/adapters/mod.ts 222 lines
1// Generated by `just generate` from `src/plugins/dcc-plugin-agents-md/slices/agents/adapters/mod.ts`. Edit the source and run `just generate`.
2// The mod handlers of the `agents` slice: the `AGENTS` files of the start, the nested files on a read or a mention, and the `AGENTS` files of the added folders.
3// Each `$` call is in this file, because `$` is not followed across an import.
4// The handlers fail open: a failed read adds no file and never changes the result of the call.
5// The loader of Claude Code follows the `@` imports, so `content` already holds them and this file does not resolve them.
6
7import type { EngineInterface } from "claude-code";
8import type { Handler } from "../../../kernel/slice.ts";
9import {
10  AGENTS_NAMES,
11  dirOf,
12  type Fields,
13  type Found,
14  freshFiles,
15  isExcluded,
16  type Kind,
17  kindOf,
18  type Loaded,
19  nestedText,
20  type Rules,
21  readPaths,
22  rulesOf,
23} from "../domain/agents.ts";
24
25/** The session values that `types/index.d.ts` declares: the files of the start, and the nested files that a call has loaded. */
26const ROOT = { plugin: "dcc-plugin-agents-md", key: "root" } as const;
27const NESTED = { plugin: "dcc-plugin-agents-md", key: "nested" } as const;
28
29type Request = Parameters<EngineInterface["fs"]["ancestors"]>[0];
30type Ancestor = Awaited<ReturnType<EngineInterface["fs"]["ancestors"]>>[number];
31
32/** One loader entry as `Found`: the file, its tier, its text, and the files its imports brought. */
33const toFound = (a: Ancestor, tier?: Kind): Found => {
34  const [head, ...rest] = a.parts;
35  return {
36    path: head?.path ?? `${a.dir}/${a.name}`,
37    kind: tier ?? kindOf(a.name),
38    content: a.content,
39    imported: rest.map((part) => part.path),
40  };
41};
42
43/** The files the engine's loader finds for `request`. A failed read finds none. */
44const walk = async (
45  $: EngineInterface,
46  request: Request,
47  tier?: Kind,
48): Promise<Found[]> => {
49  try {
50    const found = await $.fs.ancestors(request);
51    return found.map((a) => toFound(a, tier));
52  } catch {
53    // A missing folder or a path outside the walk finds no file. The next call looks again.
54    return [];
55  }
56};
57
58/** The rules of `claudeMdExcludes` and `permissions.additionalDirectories`. A failed read gives none. */
59const readRules = async ($: EngineInterface): Promise<Rules> => {
60  try {
61    return rulesOf(await $.settings.read());
62  } catch {
63    // Without the settings the plugin excludes nothing and adds no folder.
64    return rulesOf({});
65  }
66};
67
68/** The folder of the user settings: `CLAUDE_CONFIG_DIR`, or `.claude` in the home folder. */
69const configDir = async ($: EngineInterface): Promise<string | undefined> => {
70  const dir = await $.env.get("CLAUDE_CONFIG_DIR");
71  if (dir) return dir;
72  const home = await $.env.get("HOME");
73  return home ? `${home}/.claude` : undefined;
74};
75
76/** The `AGENTS` files of the user folder, of each ancestor of the working directory, and of each added folder. */
77const startFiles = async (
78  $: EngineInterface,
79  rules: Rules,
80): Promise<Found[]> => {
81  const found: Found[] = [];
82  const dir = await configDir($);
83  if (dir) {
84    found.push(
85      ...(await walk(
86        $,
87        { names: ["AGENTS.md"], of: `${dir}/AGENTS.md`, below: dirOf(dir) },
88        "user",
89      )),
90    );
91  }
92  found.push(...(await walk($, { names: [...AGENTS_NAMES] })));
93  if (
94    (await $.env.get("CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD")) === "1"
95  ) {
96    for (const added of rules.dirs) {
97      found.push(
98        ...(await walk($, {
99          names: [...AGENTS_NAMES],
100          of: `${added}/.claude/AGENTS.md`,
101          below: dirOf(added),
102        })),
103      );
104    }
105  }
106  return found;
107};
108
109/** The `prompt.context` hook: adds the `AGENTS` files that the engine did not load, as its own `instructionFiles`. */
110export const onContext: Handler<"prompt.context"> = async ($, e, next) => {
111  const out = await next(e);
112  // A hook above rewrote the `claudeMd` text, so the files behind it are unknown and this hook adds none.
113  if (e.instructionFiles === undefined) return out;
114  const rules = await readRules($);
115  const found = (await startFiles($, rules)).filter(
116    (file) => !isExcluded(file.path, rules.excludes),
117  );
118  const fresh = freshFiles(e.instructionFiles, found);
119  const seen = [...e.instructionFiles, ...fresh].map((file) => ({
120    path: file.path,
121    content: file.content,
122  }));
123  await $.state.set(ROOT, seen);
124  if (fresh.length === 0) return out;
125  return {
126    ...out,
127    instructionFiles: [
128      ...(out.instructionFiles ?? e.instructionFiles),
129      ...fresh,
130    ],
131  };
132};
133
134/** The session root, which bounds the nested walk. A failed read gives none, so no nested file loads. */
135const rootOf = async ($: EngineInterface): Promise<string | undefined> => {
136  try {
137    return await $.session.root();
138  } catch {
139    return undefined;
140  }
141};
142
143/** The `AGENTS` files of the folders below the session root that hold `path`, with the files that they import. */
144const nestedFiles = async (
145  $: EngineInterface,
146  path: string,
147): Promise<Loaded[]> => {
148  const below = await rootOf($);
149  if (below === undefined) return [];
150  const rules = await readRules($);
151  const found = await walk($, { names: [...AGENTS_NAMES], of: path, below });
152  const kept = found.filter((file) => !isExcluded(file.path, rules.excludes));
153  const { value: root = [] } = await $.state.get(ROOT);
154  const { value: nested = [] } = await $.state.get(NESTED);
155  const fresh = freshFiles([...root, ...nested], kept);
156  if (fresh.length > 0) await $.state.set(NESTED, [...nested, ...fresh]);
157  return fresh;
158};
159
160/** The `context` entries of the nested files of the paths of a call. */
161const contextFor = async (
162  $: EngineInterface,
163  paths: readonly string[],
164): Promise<string[]> => {
165  const texts: string[] = [];
166  for (const path of paths) {
167    for (const file of await nestedFiles($, path)) texts.push(nestedText(file));
168  }
169  return texts;
170};
171
172/** The paths of a `Read`, `Edit`, or `Write` call, or the file operands of a `Bash` read. */
173const pathsOf = (
174  e: { readonly tool: string } & Fields<"command" | "file_path">,
175): string[] => {
176  if (e.tool === "Bash")
177    return typeof e.command === "string" ? readPaths(e.command) : [];
178  return typeof e.file_path === "string" ? [e.file_path] : [];
179};
180
181/**
182 * The `next` of a `tool.call` hook with a filter. A method signature makes its parameter bivariant, because the generated `register`
183 * gives `next` the narrow type of the filter and does not narrow the handler type.
184 */
185type PassOn = {
186  pass(
187    e: Parameters<Parameters<Handler<"tool.call">>[2]>[0],
188  ): ReturnType<Parameters<Handler<"tool.call">>[2]>;
189}["pass"];
190
191/** The `tool.call` hook: a call on a file in a nested folder adds the `AGENTS` files of that folder to the result. */
192export const onToolCall = async (
193  $: EngineInterface,
194  e: Parameters<Handler<"tool.call">>[1],
195  next: PassOn,
196) => {
197  const out = await next(e);
198  if (out.deny !== undefined) return out;
199  const texts = await contextFor($, pathsOf(e));
200  if (texts.length === 0) return out;
201  return { ...out, context: [...(out.context ?? []), ...texts] };
202};
203
204/** The `prompt.mention` hook: a mentioned file in a nested folder adds the `AGENTS` files of that folder. */
205export const onMention: Handler<"prompt.mention"> = async ($, e, next) => {
206  const out = await next(e);
207  if (out.deny !== undefined) return out;
208  const texts = await contextFor($, [e.path]);
209  if (texts.length === 0) return out;
210  return { ...out, context: [...(out.context ?? []), ...texts] };
211};
212
213/**
214 * The `session.compact` hook: a compaction removes the nested texts from the transcript, so the dedupe of them ends.
215 * A compaction that a hook skips keeps the transcript, so the dedupe stays.
216 */
217export const onCompact: Handler<"session.compact"> = async ($, e, next) => {
218  const out = await next(e);
219  if (out.skip === undefined) await $.state.set(NESTED, []);
220  return out;
221};
222
kernel/slice.ts 114 lines
1// Generated by `just generate` from `src/kernel/slice.ts`. Edit the source and run `just generate`.
2// The contract of a slice manifest: what a slice folder exports from its `slice.ts`.
3// The generator reads each manifest, and the types of the mod API come from `claude-code`.
4// This file has no runtime import, so the hooks module can load it.
5
6import type {
7  Args,
8  EventName,
9  Events,
10  Matcher,
11  PluginOptions,
12} from "claude-code";
13
14/** The order of the phases of a mod handler, from the first to the last. The generator sorts by it. */
15export const PHASES = [
16  "observe",
17  "normalize",
18  "rewrite",
19  "check",
20  "answer",
21] as const;
22
23/** One phase of a mod handler, from `PHASES`. */
24export type Phase = (typeof PHASES)[number];
25
26/** The filter of a mod handler: a matcher over the argument of the event `E`. */
27export type EventFilter<E extends EventName = EventName> = Matcher<Args<E>>;
28
29/** The hook signature of the event `E`, as the mod API declares it. */
30export type Handler<E extends EventName = EventName> = Events[E];
31
32/**
33 * Keeps the `userConfig` values of the plugin, which the mod runtime gives to `register`, for a hook to read.
34 * The generated `register` calls it once, before the `on()` calls.
35 * The hook itself goes to `on()` by its name, because `claude plugin validate --strict` follows `$` only into a function of `register.ts`.
36 */
37export type Configure = (options: PluginOptions) => void;
38
39/**
40 * The eval kinds of one hook, which the generator writes to `evals/hooks.json` under the id `<event> <slice>`.
41 * A test is `<file from the repository root> > <test name>`.
42 * A hook that runs a model has `requests` above 0 and names its `claude plugin eval` case in `eval`.
43 * A hook that can block also names `block` and `false`.
44 */
45export interface HookEvals {
46  readonly requests: number;
47  readonly tokens: number;
48  readonly blocks: boolean;
49  readonly allow: string;
50  readonly text: string;
51  readonly block?: string;
52  readonly false?: string;
53  readonly eval?: string;
54}
55
56/**
57 * One mod handler of a slice. A slice can have more than one for one event, each with its own filter.
58 * It has `run`, the hook, and `configure`, when the hook reads the `userConfig` values of the plugin.
59 */
60export interface ModHandler<E extends EventName = EventName> {
61  readonly event: E;
62  readonly filter?: EventFilter<E>;
63  readonly phase: Phase;
64  /**
65   * The hook. A hook with a `filter` can take the event that the filter narrows, as `MatchedHook` of the mod API types it.
66   * The generated `register.ts` gives the hook and the filter to `on()`, and the compiler checks the pair there.
67   */
68  readonly run: Handler<E> | ((...args: never[]) => unknown);
69  readonly configure?: Configure;
70  /** The eval kinds of the hook. One entry covers each handler of the slice on the same event. */
71  readonly evals?: HookEvals;
72}
73
74/** One classic command hook, for an event that the mod API does not cover. */
75export interface ClassicHook {
76  readonly event: string;
77  readonly matcher?: string;
78  readonly command: string;
79  readonly timeout?: number;
80  readonly async?: boolean;
81  /** The eval kinds of the hook. Its id is the event and the command, without the `bun` launcher and the plugin root. */
82  readonly evals?: HookEvals;
83}
84
85/** One segment of the status line. The `row` is the row of the status line that holds it. */
86export interface StatusSegment {
87  readonly id: string;
88  readonly row: number;
89  readonly render: (status: unknown) => string | null;
90}
91
92/** One `userConfig` field of a plugin manifest, such as the `backend` of `dotclaude-browser`. */
93export interface UserConfigOption {
94  readonly type: "string" | "number" | "boolean";
95  readonly title: string;
96  readonly description?: string;
97  readonly default: string | number | boolean;
98  readonly options?: readonly string[];
99}
100
101/** The manifest of one slice: the name, and each part that the slice adds to the plugin. */
102export interface Slice {
103  readonly name: string;
104  readonly mod?: readonly ModHandler[];
105  readonly hooks?: readonly ClassicHook[];
106  readonly cli?: { readonly bin: string };
107  readonly segments?: readonly StatusSegment[];
108  readonly skills?: readonly string[];
109  readonly agents?: readonly string[];
110  readonly outputStyles?: readonly string[];
111  readonly userConfig?: Readonly<Record<string, UserConfigOption>>;
112  readonly settings?: Readonly<Record<string, unknown>>;
113}
114
slices/agents/domain/agents.ts 149 lines
1// Generated by `just generate` from `src/plugins/dcc-plugin-agents-md/slices/agents/domain/agents.ts`. Edit the source and run `just generate`.
2// The pure rules of `dcc-plugin-agents-md`: the names of the files, the globs of `claudeMdExcludes`, the paths of a read command, and the skip of a loaded file.
3// This file has no `node:` import and no use of `Bun`, because Claude Code runs it in the mod runtime.
4// Paths use `/`, and a `\` is read as a separator too, so that a Windows path works.
5
6/** The names that the plugin looks for in each folder, relative to the folder. */
7export const AGENTS_NAMES = [
8  "AGENTS.md",
9  "AGENTS.local.md",
10  ".claude/AGENTS.md",
11] as const;
12
13/** The tier of a loaded file. */
14export type Kind = "user" | "project" | "local";
15
16/** One loaded file: its absolute path, its tier, and its text as loaded. */
17export type Loaded = {
18  readonly path: string;
19  readonly kind: Kind;
20  readonly content: string;
21};
22
23/** A file that the engine's loader read, with the files its `@` imports brought in. */
24export type Found = Loaded & { readonly imported: readonly string[] };
25
26/** The settings that the plugin reads: the globs of `claudeMdExcludes`, and the folders of `permissions.additionalDirectories`. */
27export type Rules = {
28  readonly excludes: readonly string[];
29  readonly dirs: readonly string[];
30};
31
32/** The folder of a path: `/` for a file at the root, `.` for a name with no folder. */
33export const dirOf = (path: string): string => {
34  const at = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
35  if (at < 0) return ".";
36  return at === 0 ? "/" : path.slice(0, at);
37};
38
39/** The path and the text of one file, which is all that the skip of a loaded file needs. */
40export type Seen = Pick<Loaded, "path" | "content">;
41
42/** The tier of a file, by its name. The user file is the one exception, and the caller sets it. */
43export const kindOf = (name: string): Kind =>
44  name.endsWith("AGENTS.local.md") ? "local" : "project";
45
46/**
47 * A glob of `claudeMdExcludes` as a regular expression on an absolute path.
48 * `**` crosses folders, `*` stops at a `/`, and `?` is one character that is not a `/`.
49 */
50export const globToRegExp = (glob: string): RegExp => {
51  let out = "";
52  for (let i = 0; i < glob.length; i++) {
53    const c = glob.charAt(i);
54    if (c === "*" && glob.charAt(i + 1) === "*") {
55      if (glob.charAt(i + 2) === "/") {
56        out += "(?:.*/)?";
57        i += 2;
58      } else {
59        out += ".*";
60        i += 1;
61      }
62    } else if (c === "*") {
63      out += "[^/]*";
64    } else if (c === "?") {
65      out += "[^/]";
66    } else {
67      out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
68    }
69  }
70  return new RegExp(`^${out}$`);
71};
72
73/** A path or a glob with each `\` as `/`, and a drive letter in lower case, because Windows does not tell the case apart. */
74const slashed = (text: string): string =>
75  text
76    .replaceAll("\\", "/")
77    .replace(/^[A-Za-z]:/, (drive) => drive.toLowerCase());
78
79/**
80 * True when one glob of `excludes` matches the absolute `path`.
81 * As in Claude Code, each `\` of the path and of the globs is a `/` for the match, so a Windows path matches a glob with `/`.
82 */
83export const isExcluded = (
84  path: string,
85  excludes: readonly string[],
86): boolean =>
87  excludes.some((glob) => globToRegExp(slashed(glob)).test(slashed(path)));
88
89const strings = (value: unknown): string[] =>
90  Array.isArray(value)
91    ? value.filter((item): item is string => typeof item === "string")
92    : [];
93
94/** The named fields of a value that may be an object. A value that is not an object has none. */
95export type Fields<K extends string> = { readonly [P in K]?: unknown };
96
97const fields = <K extends string>(value: unknown): Fields<K> =>
98  typeof value === "object" && value !== null ? (value as Fields<K>) : {};
99
100/** The rules that the plugin takes from the settings it reads. A missing key is an empty list. */
101export const rulesOf = (settings: unknown): Rules => {
102  const root = fields<"claudeMdExcludes" | "permissions">(settings);
103  return {
104    excludes: strings(root.claudeMdExcludes),
105    dirs: strings(
106      fields<"additionalDirectories">(root.permissions).additionalDirectories,
107    ),
108  };
109};
110
111const READERS = new Set(["cat", "head", "tail", "less", "more", "bat", "nl"]);
112
113const unquote = (word: string): string => word.replace(/^(["'])(.*)\1$/, "$2");
114
115/** The file operands of a `Bash` read such as `cat a.ts | head -5`. Each segment of a pipe or a list counts. */
116export const readPaths = (command: string): string[] =>
117  command.split(/&&|\|\||[;|]/).flatMap((segment) => {
118    const [name, ...args] = segment.trim().split(/\s+/).map(unquote);
119    if (name === undefined || !READERS.has(name)) return [];
120    return args.filter((arg) => arg !== "" && !arg.startsWith("-"));
121  });
122
123/**
124 * The files of `found` that load now: a path that `known` holds is skipped, a text that `known` holds is skipped,
125 * and so is a file that an earlier file of `found` imported.
126 * A file that a `CLAUDE` file imports has its path in `known`, so a link or an import loads it once.
127 */
128export const freshFiles = (
129  known: readonly Pick<Loaded, "path" | "content">[],
130  found: readonly Found[],
131): Loaded[] => {
132  const paths = new Set(known.map((file) => file.path));
133  const texts = new Set(known.map((file) => file.content.trim()));
134  const fresh: Loaded[] = [];
135  for (const file of found) {
136    const text = file.content.trim();
137    if (paths.has(file.path) || texts.has(text)) continue;
138    paths.add(file.path);
139    texts.add(text);
140    for (const path of file.imported) paths.add(path);
141    fresh.push({ path: file.path, kind: file.kind, content: file.content });
142  }
143  return fresh;
144};
145
146/** The text that a `prompt.mention` or `tool.call` `context` entry carries for one nested file. */
147export const nestedText = (file: Loaded): string =>
148  `Contents of ${file.path}:\n\n${file.content}`;
149
types/index.d.ts 19 lines
1// Generated by `just generate` from `src/plugins/dcc-plugin-agents-md/types/index.d.ts`. Edit the source and run `just generate`.
2// The contract of `dcc-plugin-agents-md`: the `$.state` values of the session, which `claude plugin validate` reads.
3// The values are `$.state` and not `$.store`, because a file loaded in one session must load again in the next session.
4// A contract has no import, so `AgentsMdSeen` repeats the shape of `Seen` in `slices/agents/domain/agents.ts`.
5
6/** The path and the text of one loaded `AGENTS` file. */
7export type AgentsMdSeen = { path: string; content: string };
8
9declare module "claude-code" {
10  interface PluginState {
11    "dcc-plugin-agents-md": {
12      /** The instruction files of the start, from the engine and from this plugin. */
13      root: AgentsMdSeen[];
14      /** The nested files that a call loaded since the start or the last compaction. */
15      nested: AgentsMdSeen[];
16    };
17  }
18}
19