@sayansr26/agent-os 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * What counts as a feature, and which features the cartographer has mapped.
3
+ *
4
+ * Shared by the session hook, the pre-edit hook and the audit, so the three
5
+ * never disagree about coverage. Everything here is read-only, synchronous and
6
+ * swallows its own errors — hooks must never fail a session.
7
+ *
8
+ * Feature directories come from `.agent-os/config.json`:
9
+ *
10
+ * { "claude": { "features": ["src/features/*", "apps/*"] } }
11
+ *
12
+ * Each entry is `<dir>/*`: every direct subdirectory of `<dir>` is a feature.
13
+ * With no config, the first of DEFAULT_PARENTS that exists is used.
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
17
+ import { join } from "node:path";
18
+
19
+ export const DEFAULT_PARENTS = ["src/features", "src/modules", "app/features", "features", "modules"];
20
+
21
+ const read = (p) => { try { return readFileSync(p, "utf8"); } catch { return null; } };
22
+ const ls = (p) => { try { return readdirSync(p); } catch { return []; } };
23
+ const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
24
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+
26
+ /** `.agent-os/config.json`'s `claude` block, or {}. */
27
+ export function claudeConfig(root) {
28
+ try { return JSON.parse(read(join(root, ".agent-os/config.json")) || "{}").claude || {}; }
29
+ catch { return {}; }
30
+ }
31
+
32
+ /** The parent directories whose children are features, relative to root. */
33
+ export function featureParents(root) {
34
+ const cfg = claudeConfig(root).features;
35
+ if (Array.isArray(cfg) && cfg.length)
36
+ return cfg.map((g) => String(g).replace(/\/\*+$/, "").replace(/\/$/, "")).filter(Boolean);
37
+ const found = DEFAULT_PARENTS.find((d) => isDir(join(root, d)));
38
+ return found ? [found] : [];
39
+ }
40
+
41
+ /** Every feature: { name, dir } with dir relative to root. */
42
+ export function listFeatures(root) {
43
+ const out = [];
44
+ for (const parent of featureParents(root))
45
+ for (const name of ls(join(root, parent)))
46
+ if (!name.startsWith(".") && isDir(join(root, parent, name))) out.push({ name, dir: `${parent}/${name}` });
47
+ return out;
48
+ }
49
+
50
+ /** The feature a (relative or absolute) path belongs to, or null. */
51
+ export function featureOf(root, filePath) {
52
+ let rel = String(filePath || "").replace(/\\/g, "/");
53
+ const r = root.replace(/\\/g, "/").replace(/\/$/, "") + "/";
54
+ if (rel.startsWith(r)) rel = rel.slice(r.length);
55
+ for (const parent of featureParents(root)) {
56
+ const m = rel.match(new RegExp(`^${esc(parent)}/([^/]+)/`));
57
+ if (m) return { name: m[1], dir: `${parent}/${m[1]}` };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * The cartographer's memory directory. Plugin agents are namespaced, so the
64
+ * directory is `agent-os-feature-cartographer`; the bare name is what a copy
65
+ * installed as a standalone agent writes to.
66
+ */
67
+ export function cartographerDir(root) {
68
+ for (const base of [".claude/agent-memory", ".claude/agent-memory-local"])
69
+ for (const name of ["agent-os-feature-cartographer", "feature-cartographer"]) {
70
+ const d = join(root, base, name);
71
+ if (isDir(d)) return d;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /** Topic files in the cartographer's memory: [{ file, text, mapped }]. */
77
+ export function mapFiles(root) {
78
+ const dir = cartographerDir(root);
79
+ if (!dir) return [];
80
+ return ls(dir)
81
+ .filter((f) => f.endsWith(".md") && f !== "MEMORY.md")
82
+ .map((f) => {
83
+ const text = read(join(dir, f)) || "";
84
+ const m = text.match(/^mapped:\s*["']?(\d{4}-\d{2}-\d{2})/m);
85
+ return { file: f, text, mapped: m ? m[1] : null };
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Does some topic file map this feature? A map names its subject in a header
91
+ * line (description:, entry:, a heading), is named after it, or mentions its
92
+ * directory at least twice. `_architecture.md` is the system map and never
93
+ * counts as a feature map. `dashboard` must not match `dashboardV2`.
94
+ */
95
+ export function findMap(feature, maps) {
96
+ const named = new RegExp(`${esc(feature.dir)}(?![\\w-])`);
97
+ const slug = feature.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
98
+ for (const m of maps) {
99
+ if (m.file.startsWith("_")) continue;
100
+ if (m.file.replace(/\.md$/, "").toLowerCase() === slug) return m;
101
+ const header = m.text.split("\n").some((l) => /^(description:|entry:|#)/.test(l) && named.test(l));
102
+ if (header || m.text.split(`${feature.dir}/`).length - 1 >= 2) return m;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /** Is anything under this directory tracked by git? Unknown counts as yes. */
108
+ export function tracked(root, dir) {
109
+ try {
110
+ const out = execFileSync("git", ["ls-files", "--", dir], {
111
+ cwd: root, encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
112
+ });
113
+ return out.trim().length > 0;
114
+ } catch { return true; }
115
+ }
116
+
117
+ /** Date (YYYY-MM-DD) of the last commit touching dir, or null. */
118
+ export function lastChanged(root, dir) {
119
+ try {
120
+ const out = execFileSync("git", ["log", "-1", "--format=%cs", "--", dir], {
121
+ cwd: root, encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
122
+ }).trim();
123
+ return /^\d{4}-\d{2}-\d{2}$/.test(out) ? out : null;
124
+ } catch { return null; }
125
+ }
126
+
127
+ /**
128
+ * Coverage summary. `stale` compares each map's `mapped:` date with the last
129
+ * commit to its feature, which costs one git call per mapped feature — the
130
+ * audit asks for it, the hooks do not.
131
+ */
132
+ export function coverage(root, { stale = false } = {}) {
133
+ const features = listFeatures(root);
134
+ const maps = mapFiles(root);
135
+ const mapped = [], unmapped = [], outdated = [];
136
+ for (const f of features) {
137
+ const m = findMap(f, maps);
138
+ if (!m) { unmapped.push(f); continue; }
139
+ mapped.push({ ...f, map: m.file, date: m.mapped });
140
+ if (stale && m.mapped) {
141
+ const changed = lastChanged(root, f.dir);
142
+ if (changed && changed > m.mapped) outdated.push({ ...f, map: m.file, date: m.mapped, changed });
143
+ }
144
+ }
145
+ return {
146
+ parents: featureParents(root),
147
+ features,
148
+ mapped,
149
+ unmapped,
150
+ outdated,
151
+ architecture: maps.some((m) => m.file === "_architecture.md"),
152
+ dir: cartographerDir(root),
153
+ };
154
+ }
@@ -15,7 +15,7 @@ The layout this skill converges on:
15
15
  | Always-loaded | `CLAUDE.md`, ≤200 lines | small, fixed | Only what is true in *every* session |
16
16
  | On-demand | `.claude/rules/*.md` with `paths:` frontmatter | **zero** | Durable facts scoped to some files |
17
17
  | Self-writing | auto memory (`~/.claude/projects/<repo>/memory/`) | index only | Corrections, preferences, decisions |
18
- | Explored | `.claude/agent-memory/feature-cartographer/` | **zero** (subagent context) | How each feature is actually built |
18
+ | Explored | `.claude/agent-memory/agent-os-feature-cartographer/` | **zero** (subagent context) | How each feature is actually built |
19
19
  | Task state | the project's own vault / handoff file | zero (read selectively) | What is in flight right now |
20
20
 
21
21
  The principle the whole layout serves: **storing a fact and loading a fact are
@@ -28,7 +28,7 @@ a memory-bank read at every startup, a rules file with no `paths:` — is the bu
28
28
  |---|---|
29
29
  | *(none)* | The full pass: audit, then route by MODE |
30
30
  | `audit` | Run the audit and report. Change nothing. |
31
- | `settings` | The settings and permissions pass only — read `~/.claude/settings.json` and the project's, report what is set, and propose changes. See below. |
31
+ | `settings` | The settings pass only — git write protection and the task tools, applied to project and user settings after one confirmation. See below. |
32
32
 
33
33
  Related skills: `/agent-os:map` builds the architecture map, `/agent-os:memory`
34
34
  inspects and repairs what the project remembers.
@@ -51,12 +51,37 @@ It reports: always-loaded files and their line counts, whether every
51
51
  stores (`memory-bank/`, `.serena/memories/`, cursor and windsurf rules, memory-ish
52
52
  MCP servers), per-agent memory health, the machine layer (`~/.claude` CLAUDE.md,
53
53
  shadowing agents or skills, a duplicate session-resume hook, permissions
54
- posture), the startup byte and token cost, and a ranked finding list.
54
+ posture), the setup state (fresh, repair or healthy), the startup byte and
55
+ token cost, and a ranked finding list.
55
56
 
56
57
  **Show the output to the user before you change anything.** Do not restructure a
57
58
  repo you have only just opened.
58
59
 
59
- ## Step 2 — Route by MODE
60
+ ## Step 2 — Fresh setup or repair?
61
+
62
+ Above `MODE`, the audit prints a `SETUP` block: the install and settings state,
63
+ one line per item, and a verdict. Read it first — it decides how much of this
64
+ skill runs, so a project that is already set up is repaired, never re-initialised.
65
+
66
+ | SETUP | What it means | Do |
67
+ |---|---|---|
68
+ | `FRESH` | Nothing of agent-os here yet | The full pass: Step 3 by `MODE`, then the settings pass. If `.agent-os/ source` is missing and the user wants rules for other tools too, suggest `npx @sayansr26/agent-os init` once. |
69
+ | `REPAIR` | Set up, but items are `MISSING` | Fix **only** the `MISSING` lines, each by the route below, then `MODE` as usual. Do not rebuild anything marked `ok`. |
70
+ | `HEALTHY` | Every item `ok` | Skip the settings pass; `MODE` alone decides the rest. |
71
+
72
+ Routes for `MISSING` items — batch them into the **one** confirmation the
73
+ settings pass asks, rather than a question per item:
74
+
75
+ | Item | Fix |
76
+ |---|---|
77
+ | git write protection, task tools, task-tracking rule | the settings pass below |
78
+ | plugin version (older than latest) | `claude plugin marketplace update sayan-plugins`, then `claude plugin update agent-os@sayan-plugins --scope <its scope> --yes`. Tell the user to run `/reload-plugins` — the running session keeps the old version until then. If `claude` is not on PATH, give them `/plugin update agent-os@sayan-plugins`. |
79
+ | plugin enabled | `claude plugin enable agent-os@sayan-plugins` |
80
+ | CLAUDE.md says when to use each agent | `references/establishing.md`, Step 4b |
81
+ | CLAUDE.md | `MODE` routes it (ESTABLISH) |
82
+ | `.agent-os/ source` | optional — only if the user wants rules compiled for other tools: `npx @sayansr26/agent-os init` |
83
+
84
+ ## Step 3 — Route by MODE
60
85
 
61
86
  The audit ends with a `MODE` line. It decides what this run is for:
62
87
 
@@ -73,55 +98,70 @@ login flow from email to OTP"* execute from known structure instead of
73
98
  rediscovering the codebase. `references/changing-a-feature.md` is that workflow —
74
99
  point the user at it once the layer exists.
75
100
 
76
- ## Step 3 — Act on the findings
101
+ ## Step 4 — Act on the findings
77
102
 
78
103
  Each finding routes to one place. Load only what the audit actually surfaced:
79
104
 
80
105
  | Finding | Read |
81
106
  |---|---|
82
107
  | `MODE ESTABLISH` or `MODE MAP` | `references/establishing.md` |
108
+ | `CLAUDE.md` has no "Agents in this project" section | `references/establishing.md`, Step 4b |
83
109
  | user asks how to change an existing feature | `references/changing-a-feature.md` |
84
110
  | legacy store found; CLAUDE.md over budget | `references/migrating.md` |
85
111
  | rule without `paths:`; no rules layer yet; CLAUDE.md to trim | `references/writing-rules.md` |
86
- | `defaultMode` auto-approves with an empty `deny` | `references/git-permissions.md` |
112
+ | git write protection or task tools missing | the settings pass below — apply, don't hand over |
87
113
  | LSP plugin recommended; checked-in generated dirs | `references/establishing.md`, "Stop Claude reading what it should not" |
88
114
  | hook target missing | delete the hook entry, or restore the script — say which |
89
115
  | shadowing agent or skill in `~/.claude` or `.claude/agents/` | the user removes the standalone copy; a plugin cannot |
90
116
  | unindexed or near-duplicate agent memory topic files | merge into the best-named file, delete the rest, rebuild `MEMORY.md` as one line per file |
91
117
  | project CLAUDE.md refers to a `~/.claude/CLAUDE.md` that is absent | the user creates it or drops the reference |
92
118
 
93
- Findings on the machine layer are the user's to fix — a plugin cannot write
94
- `~/.claude`. Hand over the exact change rather than attempting it.
119
+ Shadowing agents and skills on the machine layer are the user's to remove.
120
+ Settings are not: the settings pass below writes them, including `~/.claude`.
95
121
 
96
- ## Step 4 — Verify by re-running
122
+ ## Step 5 — Verify by re-running
97
123
 
98
- Run the audit again and show the before and after: finding count, startup bytes,
99
- token estimate. Do not declare success on vibes — the script already produces the
124
+ Run the audit again and show the before and after: the `SETUP` verdict (a
125
+ repair should end `HEALTHY`), finding count, startup bytes, token estimate. Do not declare success on vibes — the script already produces the
100
126
  numbers, so quote them.
101
127
 
102
128
  ## The settings pass
103
129
 
104
- The audit already reads both settings files and reports `defaultMode`, the deny
105
- list, hook registrations and shadowing. To act on it:
106
-
107
- 1. **Show what is set** — project `.claude/settings.json` and `~/.claude/settings.json`,
108
- side by side, so the user can see which rules exist only in this project and
109
- therefore vanish in every other one.
110
- 2. **Propose, do not apply.** Write out the exact JSON block and ask. This is the
111
- one place where acting first is wrong: `permissions.deny` is the guardrail on
112
- your own behaviour, and a skill that edits its own guardrails without being
113
- asked is exactly the thing the setting exists to prevent. Apply only after an
114
- explicit yes, and never widen an existing deny list without pointing out what
115
- it would stop blocking.
116
- 3. **What to propose**, when the audit flagged it:
117
- - git write protection — `references/git-permissions.md` has the rule set
118
- - `Read` deny rules for checked-in generated or vendored paths
119
- - a code intelligence plugin for the detected language
120
- - `claudeMdExcludes` in a monorepo where other teams' files load
121
-
122
- Anything under `~/.claude/` affects every project on the machine. Say so before
123
- proposing it, and prefer the project's own settings file when the rule is really
124
- about this project.
130
+ Runs when `SETUP` is `FRESH`, or `REPAIR` with a settings item missing (after Step 4), and alone for `settings`. It
131
+ **writes** the setup rather than describing it — handing the user a JSON block
132
+ to paste is how a project ends up with no git protection and no task tools.
133
+
134
+ 1. **Preview.** One call:
135
+
136
+ ```bash
137
+ node "${CLAUDE_PLUGIN_ROOT}/skills/init/scripts/settings.mjs" --scope both
138
+ ```
139
+
140
+ It prints, per file, what it would add to project `.claude/settings.json` and
141
+ `~/.claude/settings.json` — the git write-protection deny rules, and
142
+ `env.CLAUDE_CODE_ENABLE_TODO_TOOLS` — plus the task-tracking rule for each
143
+ `CLAUDE.md`. If it reports everything already present, say so and stop.
144
+ 2. **Ask once.** One AskUserQuestion: apply to *project and user* (recommended —
145
+ `~/.claude` protects every other repo on this machine), *project only*, or
146
+ *skip*. This is the one confirmation: `permissions.deny` is the guardrail on
147
+ your own behaviour, so it is changed with the user's yes, never silently.
148
+ 3. **Apply** with the same command plus `--apply` and `--scope both` or
149
+ `--scope project`. The script merges — existing keys, allow rules, deny rules
150
+ and a user-set env value survive — and backs up any `~/.claude` file it
151
+ changes to `<file>.agent-os.bak`. Show its output.
152
+ 4. **If the Bash call is denied** (writing under `~/.claude` can need approval),
153
+ do not fall back to pasting JSON. Give the user the exact command to run
154
+ with the `!` prefix so it runs in this session:
155
+ `! node "<plugin root>/skills/init/scripts/settings.mjs" --scope both --apply`.
156
+
157
+ The rule set and every judgment call in it (why `git -C` is denied, why
158
+ `fetch` is allowed) are in `references/git-permissions.md`.
159
+
160
+ Also propose, when the audit flagged them — these are project-specific, so they
161
+ stay proposals:
162
+ - `Read` deny rules for checked-in generated or vendored paths
163
+ - a code intelligence plugin for the detected language
164
+ - `claudeMdExcludes` in a monorepo where other teams' files load
125
165
 
126
166
  ## What this skill will not do
127
167
 
@@ -41,6 +41,13 @@ That last line is the whole argument for this step. A cold grep for `login` woul
41
41
  not have found the OTP dialog that already exists — and you would have built a
42
42
  second one.
43
43
 
44
+ **In plan mode** the cartographer still comes first — plan mode's "Explore
45
+ agents only" phase is about not writing code, not about skipping the map. Ask it
46
+ read-only (*"read-only, plan mode: how is the login flow built?"*); it answers
47
+ and files nothing. The first step after plan mode ends is to call it again to
48
+ file the map. If the feature has no map at all and the user would rather not
49
+ wait, `/agent-os:map <feature>` before planning does the same job.
50
+
44
51
  ## Step 2 — Find the nearest precedent
45
52
 
46
53
  Before designing anything, ask what this codebase already did that resembles the
@@ -75,6 +75,10 @@ The highest-value things to capture are the ones a newcomer gets wrong:
75
75
 
76
76
  ## Step 4 — Write the rules
77
77
 
78
+ If the project has `.agent-os/`, write rules there (`.agent-os/rules/<name>.md`)
79
+ and run `npx @sayansr26/agent-os sync`; the `.claude/rules/` copies are
80
+ generated, and the pre-edit hook blocks edits to them.
81
+
78
82
  Group by **what the reader is touching**, not by topic. Each file gets `paths:`
79
83
  frontmatter narrow enough that it is absent most of the time.
80
84
 
@@ -95,6 +99,30 @@ Verified across three services; see the ones named in `_architecture.md`.
95
99
  Dense agent notes, not prose. Invariants and gotchas. Skip anything a `ls` or a
96
100
  grep answers — that is rule 2 in the README and it applies hardest here.
97
101
 
102
+ ## Step 4b — Wire the agents into CLAUDE.md
103
+
104
+ An agent nobody is told to use never runs. The session hook announces the
105
+ agents every session; `CLAUDE.md` makes it part of the project's own contract,
106
+ so it survives a machine without the plugin's hooks. Add this section once
107
+ (skip it if `CLAUDE.md` already has one), trimmed to the agents this project
108
+ actually needs:
109
+
110
+ ```markdown
111
+ ## Agents in this project (agent-os)
112
+
113
+ - Before changing an existing feature: ask `agent-os:feature-cartographer` how
114
+ it is built. In plan mode ask it read-only, and have it file the map as the
115
+ first step after plan mode ends.
116
+ - New subsystem, cross-module change, shared data model: `agent-os:architect`
117
+ first.
118
+ - Writing the change: `agent-os:builder`, given the map and the files to change.
119
+ - Done means: `agent-os:tester` verified it and `agent-os:reviewer` found no
120
+ rule violations.
121
+ - After it is verified: `agent-os:documenter` updates the changelog and docs.
122
+ - Work spanning several of these: `agent-os:orchestrator`.
123
+ - Rules live in `.agent-os/rules/`; `.claude/rules/` is generated.
124
+ ```
125
+
98
126
  ## Step 5 — Stop Claude reading what it should not
99
127
 
100
128
  Two settings with a large effect on how much gets read, both worth proposing:
@@ -4,8 +4,11 @@ A `permissions.deny` set that blocks every git command which changes the
4
4
  repository, while leaving read-only inspection (`status`, `log`, `diff`, `show`,
5
5
  `blame`, `rev-parse`, `ls-files`, `describe`, `grep`, `shortlog`, …) available.
6
6
 
7
- Paste into `~/.claude/settings.json` for every project, or a project's
8
- `.claude/settings.json` for one.
7
+ You do not paste this. `scripts/settings.mjs` applies it — to the project's
8
+ `.claude/settings.json`, `~/.claude/settings.json`, or both — merged into what
9
+ is already there, as part of `/agent-os:init` or `agent-os settings --apply`.
10
+ The list lives in that script (`GIT_DENY`); it is reproduced here so the
11
+ judgment calls below have something to point at.
9
12
 
10
13
  ```json
11
14
  {
@@ -15,6 +15,9 @@
15
15
  import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
16
16
  import { join, basename } from "node:path";
17
17
  import { homedir } from "node:os";
18
+ import { coverage } from "../../../lib/features.mjs";
19
+ import { GIT_DENY, TODO_ENV } from "./settings.mjs";
20
+ import { describeState, setupState } from "./state.mjs";
18
21
 
19
22
  const ROOT = process.argv[2] || process.cwd();
20
23
  const HOME = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
@@ -50,6 +53,8 @@ for (const f of ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"]) {
50
53
  if (over) flag("WARN", `${f} is ${n} lines; budget is 200. Move path-scoped content to .claude/rules/.`);
51
54
  }
52
55
  if (!residentBytes) flag("WARN", "No CLAUDE.md at all — this project has no always-loaded instructions.");
56
+ else if (!/agent-os:(feature-cartographer|builder|reviewer)/.test(read(join(ROOT, "CLAUDE.md")) || ""))
57
+ flag("INFO", "CLAUDE.md does not say when to use the agent-os agents — add the \"Agents in this project\" section (references/establishing.md, Step 4b).");
53
58
 
54
59
  // ---------------------------------------------------------- rules
55
60
  const rulesDir = join(ROOT, ".claude/rules");
@@ -66,6 +71,27 @@ for (const f of ruleFiles) {
66
71
  if (!scoped) { residentBytes += t.length; flag("WARN", `.claude/rules/${f} has no paths: frontmatter, so it loads every session like CLAUDE.md.`); }
67
72
  }
68
73
 
74
+ // Generated copies edited in place are reverted by the next `sync`, silently.
75
+ // A copy whose source body no longer appears in it has drifted.
76
+ const srcRulesDir = join(ROOT, ".agent-os/rules");
77
+ if (existsSync(srcRulesDir)) {
78
+ const body = (t) => (t.startsWith("---\n") && t.indexOf("\n---\n", 3) !== -1 ? t.slice(t.indexOf("\n---\n", 3) + 5) : t).trim();
79
+ const drifted = [], orphans = [];
80
+ for (const f of ruleFiles) {
81
+ const gen = read(join(rulesDir, f)) || "";
82
+ if (!gen.includes("agent-os: generated from .agent-os/")) continue;
83
+ const src = read(join(srcRulesDir, f));
84
+ if (src === null) { orphans.push(f); continue; }
85
+ if (!gen.includes(body(src))) drifted.push(f);
86
+ }
87
+ say(` source .agent-os/rules/ — ${drifted.length ? `${drifted.length} generated cop(ies) DRIFTED` : "generated copies match"}`);
88
+ for (const f of drifted) {
89
+ say(` DRIFT ${f}`);
90
+ flag("WARN", `.claude/rules/${f} was edited in place and differs from .agent-os/rules/${f} — the next sync reverts it. Port the edit to the source, then sync.`);
91
+ }
92
+ for (const f of orphans) flag("WARN", `.claude/rules/${f} is generated but .agent-os/rules/${f} no longer exists — delete it or restore its source.`);
93
+ }
94
+
69
95
  // ---------------------------------------------------------- hooks
70
96
  const readJson = (p) => { const t = read(p); if (!t) return null; try { return JSON.parse(t); } catch { return "INVALID"; } };
71
97
  const projSettingsPath = join(ROOT, ".claude/settings.json");
@@ -83,6 +109,8 @@ else {
83
109
  const m = cmd.match(/\$\{CLAUDE_PROJECT_DIR\}\/([^"']+)/);
84
110
  if (!m) { say(` ok ${evt.padEnd(14)} ${cmd.slice(0, 60)}`); continue; }
85
111
  const target = m[1], exists = existsSync(join(ROOT, target));
112
+ if (/cartographer-reminder/.test(target))
113
+ flag("INFO", `${target} duplicates the plugin's own pre-edit reminder — remove the project copy and its hook entry.`);
86
114
  say(` ${exists ? "ok " : "FAIL"} ${evt.padEnd(14)} ${target}${exists ? "" : " <- TARGET MISSING"}`);
87
115
  if (!exists) flag("FAIL", `${evt} hook points at ${target}, which does not exist. It fires at a missing path every session.`);
88
116
  }
@@ -151,6 +179,11 @@ for (const [label, dir] of [["agent memory", join(ROOT, ".claude/agent-memory")]
151
179
  const dupes = slugs.filter((s, i) => slugs.indexOf(s) !== i);
152
180
  if (dupes.length) flag("WARN", `${agent}: near-duplicate topic filenames (hyphen/underscore variants). Merge them.`);
153
181
  if (idx && idx.split("\n").length > 200) flag("WARN", `${agent}: MEMORY.md over 200 lines — everything past that is dropped at startup.`);
182
+ // An agent that writes `.claude/agent-memory/<agent>/x.md` relative to its
183
+ // own memory directory nests a copy of the tree inside it.
184
+ const nested = ls(adir).filter((e) => { try { return statSync(join(adir, e)).isDirectory(); } catch { return false; } });
185
+ for (const d of nested)
186
+ flag("WARN", `${agent}/${d}/ is a folder inside agent memory${d === ".claude" ? " — a memory write resolved against the wrong root" : ""}. Move any topic files up into ${agent}/ and delete it.`);
154
187
  }
155
188
  }
156
189
  if (!existsSync(join(ROOT, ".claude/agent-memory")) && !existsSync(join(ROOT, ".claude/agent-memory-local")))
@@ -194,6 +227,21 @@ else if (gs) {
194
227
  say(` SessionStart in user scope${" ".repeat(7)}${(gs.hooks || {}).SessionStart ? "yes" : "no"}`);
195
228
  } else say(" settings.json absent");
196
229
 
230
+ // Git write protection and the task tools, wherever either is set.
231
+ {
232
+ const norm = (r) => r.replace(/:\*\)$/, " *)").replace(/\s+/g, " ");
233
+ const denyOf = (st) => new Set(((st && st !== "INVALID" && st.permissions?.deny) || []).map(norm));
234
+ const envOn = (st) => st && st !== "INVALID" && ["1", "true", true].includes((st.env || {})[TODO_ENV]);
235
+ const u = denyOf(gs), pj = denyOf(projSettings);
236
+ const uHave = GIT_DENY.filter((r) => u.has(norm(r))).length, pHave = GIT_DENY.filter((r) => pj.has(norm(r))).length;
237
+ say(` git write protection user ${uHave}/${GIT_DENY.length} project ${pHave}/${GIT_DENY.length}`);
238
+ say(` env.${TODO_ENV} user ${envOn(gs) ? "on" : "off"} project ${envOn(projSettings) ? "on" : "off"}`);
239
+ if (uHave < GIT_DENY.length || pHave < GIT_DENY.length)
240
+ flag("INFO", `git write protection incomplete (user ${uHave}/${GIT_DENY.length}, project ${pHave}/${GIT_DENY.length}) — the settings pass applies it.`);
241
+ if (!envOn(gs) && !envOn(projSettings))
242
+ flag("INFO", `${TODO_ENV} is not on in user or project settings — the settings pass turns it on.`);
243
+ }
244
+
197
245
  // ---------------------------------------------------------- stack + scale
198
246
  say();
199
247
  say("PROJECT");
@@ -297,13 +345,36 @@ if (rec.length) flag("INFO", `${rec.length} extension(s) this project could use
297
345
 
298
346
  // ---------------------------------------------------------- mode
299
347
  const hasLayer = residentBytes > 0 || ruleFiles.length > 0;
300
- const mapped = existsSync(join(ROOT, ".claude/agent-memory/agent-os-feature-cartographer"));
348
+ // "Mapped" means the architecture map exists. Feature maps are built lazily —
349
+ // the first change to a feature maps it, and the pre-edit hook reminds — so
350
+ // coverage is reported honestly rather than used as a pass/fail gate.
351
+ const cov = coverage(ROOT, { stale: true });
352
+ const mapped = !!cov.dir && cov.architecture;
353
+ say();
354
+ say("CARTOGRAPHER");
355
+ say(` memory${" ".repeat(27)}${cov.dir ? cov.dir.slice(ROOT.length + 1) : "none yet"}`);
356
+ say(` architecture map${" ".repeat(17)}${cov.architecture ? "present" : "MISSING"}`);
357
+ if (cov.features.length) {
358
+ const pct = Math.round((cov.mapped.length / cov.features.length) * 100);
359
+ say(` features mapped${" ".repeat(18)}${cov.mapped.length} of ${cov.features.length} (${pct}%) under ${cov.parents.join(", ")}`);
360
+ if (cov.mapped.length) say(` ${cov.mapped.map((f) => f.name).join(", ")}`);
361
+ for (const f of cov.outdated) say(` STALE ${f.name.padEnd(26)} mapped ${f.date}, code changed ${f.changed} (${f.map})`);
362
+ if (cov.mapped.length < cov.features.length)
363
+ flag("INFO", `${cov.features.length - cov.mapped.length} of ${cov.features.length} features have no cartographer map. They are mapped on first change; map the ones in active work now with /agent-os:map.`);
364
+ if (cov.outdated.length)
365
+ flag("INFO", `${cov.outdated.length} map(s) older than their feature's last commit (${cov.outdated.map((f) => f.name).join(", ")}) — the cartographer re-checks them on next use.`);
366
+ } else say(` features${" ".repeat(25)}no feature directories found — set "claude.features" in .agent-os/config.json`);
301
367
  let MODE;
302
368
  if (srcFiles < 5 && !hasLayer) MODE = "TOO-EARLY";
303
369
  else if (!hasLayer) MODE = "ESTABLISH";
304
370
  else if (anyLegacy || findings.some((f) => f.startsWith("WARN") || f.startsWith("FAIL"))) MODE = "MIGRATE";
305
371
  else if (!mapped) MODE = "MAP";
306
372
  else MODE = "MAINTAIN";
373
+ // ---------------------------------------------------------- setup state
374
+ // Install and settings, as opposed to MODE, which is about the context layer.
375
+ say();
376
+ for (const l of describeState(setupState(ROOT))) say(l);
377
+
307
378
  say();
308
379
  say(`MODE ${MODE}`);
309
380
  say({
@@ -311,7 +382,7 @@ say({
311
382
  ESTABLISH: " Real code, no context layer. Build one FROM THE CODE: read\n references/establishing.md. Do not invent conventions.",
312
383
  MIGRATE: " A layer exists but has problems. Fix the findings below;\n read references/migrating.md for anything legacy.",
313
384
  MAP: " Layer is healthy but the codebase has never been mapped. Build the\n architecture map: read references/establishing.md, 'Map the architecture'.",
314
- MAINTAIN: " Layer is healthy and the codebase is mapped. Nothing to set up.",
385
+ MAINTAIN: ` Layer is healthy and the architecture is mapped${cov.features.length ? ` (${cov.mapped.length} of ${cov.features.length} features)` : ""}.\n Nothing to set up; remaining features are mapped as they are changed.`,
315
386
  }[MODE]);
316
387
 
317
388
  // ---------------------------------------------------------- verdict