aiblueprint-cli 1.4.100 → 1.4.102

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.
Files changed (21) hide show
  1. package/README.md +0 -1
  2. package/agents-config/skills/audit-memories/SKILL.md +161 -0
  3. package/agents-config/skills/audit-memories/agents/openai.yaml +10 -0
  4. package/agents-config/skills/audit-memories/assets/codex-icon.svg +19 -0
  5. package/agents-config/skills/audit-memories/references/cleanup-rubric.md +105 -0
  6. package/agents-config/skills/audit-memories/scripts/inventory_project_markdown.py +307 -0
  7. package/dist/cli.js +365 -363
  8. package/package.json +1 -1
  9. package/agents-config/skills/prompt-creator/SKILL.md +0 -285
  10. package/agents-config/skills/prompt-creator/agents/openai.yaml +0 -7
  11. package/agents-config/skills/prompt-creator/assets/codex-icon.svg +0 -16
  12. package/agents-config/skills/prompt-creator/references/anthropic-best-practices.md +0 -126
  13. package/agents-config/skills/prompt-creator/references/anti-patterns.md +0 -57
  14. package/agents-config/skills/prompt-creator/references/clarity-principles.md +0 -54
  15. package/agents-config/skills/prompt-creator/references/context-management.md +0 -389
  16. package/agents-config/skills/prompt-creator/references/few-shot-patterns.md +0 -47
  17. package/agents-config/skills/prompt-creator/references/openai-best-practices.md +0 -50
  18. package/agents-config/skills/prompt-creator/references/prompt-templates.md +0 -110
  19. package/agents-config/skills/prompt-creator/references/reasoning-techniques.md +0 -52
  20. package/agents-config/skills/prompt-creator/references/system-prompt-patterns.md +0 -48
  21. package/agents-config/skills/prompt-creator/references/xml-structure.md +0 -36
package/README.md CHANGED
@@ -151,7 +151,6 @@ npx skills add Melvynx/aiblueprint --skill skill-manager
151
151
  | `environments-manager` | Set up per-worktree agent environments |
152
152
  | `oneshot` | Implement one focused change quickly |
153
153
  | `prompt` | Create minimalist SVG logo variations |
154
- | `prompt-creator` | Expert prompt engineering |
155
154
  | `rules-manager` | Create and maintain agent rule files |
156
155
  | `skill-manager` | Manage skills and rules across Claude Code, Codex, and Cursor |
157
156
  | `tools` | AIBlueprint tools and libraries reference |
@@ -0,0 +1,161 @@
1
+ ---
2
+ name: audit-memories
3
+ description: Manual-only audit and cleanup of agent-facing Markdown inside the current project. Run only from an explicit `$audit-memories` or `/audit-memories` user command; never select it implicitly.
4
+ argument-hint: "[audit|clean]"
5
+ disable-model-invocation: true
6
+ user-invocable: true
7
+ ---
8
+
9
+ # Audit Project Memories
10
+
11
+ Treat agent instructions, rules, skills, plans, task traces, and outputs inside the current project as its agent memory. Compare them with current project truth, then remove noise without destroying durable guidance.
12
+
13
+ ## Invocation guard
14
+
15
+ Proceed only when the user explicitly invokes `$audit-memories` or `/audit-memories`. An OpenCode command may satisfy this guard by explicitly stating that the user invoked `/audit-memories` before injecting this file.
16
+
17
+ Supported actions:
18
+
19
+ - `$audit-memories audit`: exhaustive read-only audit. This is the default action when only `$audit-memories` is supplied.
20
+ - `$audit-memories clean`: run or refresh the exhaustive audit, then apply justified local cleanup.
21
+ - `/audit-memories audit` and `/audit-memories clean`: equivalent manual commands in clients that use slash-command syntax.
22
+
23
+ Do not read or modify `~/.codex/memories`. Do not classify product documentation, public docs, README files, changelogs, or general project plans. They may be read only as truth evidence when an agent document references them.
24
+
25
+ ## Hard boundaries
26
+
27
+ - Resolve the project root from the current working directory. Never silently switch to another checkout.
28
+ - Read the nearest applicable `AGENTS.md`, `CLAUDE.md`, and repository rules before auditing.
29
+ - Preserve unrelated dirty-tree changes. Record `git status --short` before and after.
30
+ - Never inspect secret values. Compare environment-variable names through examples, schemas, and code only.
31
+ - Never use `rm -rf`. Use `trash` for approved file removal.
32
+ - Use `apply_patch` for textual edits. Use formatters only for mechanical normalization.
33
+ - Do not infer deployed truth from local code. Label provider/runtime claims `NOT VERIFIED` unless the audit explicitly checks the live system.
34
+ - Do not delete instruction files, security guidance, migration procedures, incident runbooks, or destructive-operation safeguards merely because they are old or rarely linked.
35
+
36
+ ## Action: audit
37
+
38
+ ### 1. Inventory every agent-facing Markdown file
39
+
40
+ Run:
41
+
42
+ ```bash
43
+ python3 ~/.agents/skills/audit-memories/scripts/inventory_project_markdown.py \
44
+ --root "$PWD" \
45
+ --format json
46
+ ```
47
+
48
+ Include only:
49
+
50
+ - `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, and equivalent agent entrypoints;
51
+ - `.agents/**/*.md`, `.claude/**/*.md`, and `.cursor/**/*.{md,mdc}`;
52
+ - `.github/copilot-instructions.md` and `.github/instructions/**/*.md`;
53
+ - project skill files under `skills/**/SKILL.md` or `.agents/skills/**`.
54
+
55
+ This intentionally includes agent-generated plans, task traces, and output folders because they are prime cleanup candidates. Exclude public/local documentation such as `content/docs`, normal README files, product specs, and application changelogs from classification.
56
+
57
+ The byte-level ledger must show `visited == eligible`. It is not a semantic audit by itself.
58
+
59
+ ### 2. Build the project truth map
60
+
61
+ Inspect current primary evidence before classifying documents:
62
+
63
+ 1. repository instructions and package/workspace manifests;
64
+ 2. source tree, routes, exported APIs, schemas, migrations, and configuration;
65
+ 3. tests and CI workflows;
66
+ 4. current Git history only when a document makes a historical claim;
67
+ 5. live provider/runtime state only when explicitly requested and safely accessible.
68
+
69
+ Start with `rg --files`, `package.json` or equivalent manifests, key configs, and the files referenced by Markdown. Follow repository instructions about required reading. Use targeted searches rather than loading the entire source tree blindly.
70
+
71
+ Truth priority:
72
+
73
+ ```text
74
+ explicit current user instruction
75
+ > current code/config/schema
76
+ > current tests and CI contracts
77
+ > verified provider/runtime read-back
78
+ > Markdown claims
79
+ > historical memory
80
+ ```
81
+
82
+ ### 3. Review files one by one
83
+
84
+ Read every eligible agent-facing Markdown file. Sampling is not completion.
85
+
86
+ For more than 30 files, split the exact manifest into disjoint batches of 10–20. Use parallel subagents when available. Give each worker the shared truth map plus exact Markdown paths and read-only boundaries. Require one result row per path; verify that batch union equals the manifest with no duplicates.
87
+
88
+ For each file, record:
89
+
90
+ - purpose and intended audience;
91
+ - whether referenced paths, commands, APIs, routes, names, versions, statuses, and architecture match current evidence;
92
+ - overlap with other Markdown files;
93
+ - broken local links and missing referenced files;
94
+ - historical value and operational risk;
95
+ - evidence paths and line numbers;
96
+ - verdict: `KEEP`, `UPDATE`, `MERGE`, `ARCHIVE`, `DELETE`, or `VERIFY`.
97
+
98
+ For important claims, use:
99
+
100
+ - `VERIFIED`: directly supported by current local evidence;
101
+ - `CONTRADICTED`: current evidence disproves it;
102
+ - `NOT VERIFIED`: needs external/runtime evidence;
103
+ - `HISTORICAL`: intentionally describes an old state;
104
+ - `OPINION`: product/design intent, not a factual implementation claim.
105
+
106
+ Load [cleanup-rubric.md](references/cleanup-rubric.md) before classification.
107
+
108
+ ### 4. Produce the audit report
109
+
110
+ Return:
111
+
112
+ - project root and Git status boundary;
113
+ - files eligible, byte-visited, semantically reviewed, skipped, and failed;
114
+ - current lines/bytes and projected post-clean totals;
115
+ - verdict counts and defensible removal percentage;
116
+ - one concise row per agent-facing Markdown file;
117
+ - contradiction table with exact document and truth-source lines;
118
+ - duplicate/merge clusters and canonical destination;
119
+ - broken local links;
120
+ - protected documents and why they must survive;
121
+ - claims marked `NOT VERIFIED`.
122
+
123
+ Do not create another report Markdown inside the project unless the user explicitly asks for a durable report.
124
+
125
+ ## Action: clean
126
+
127
+ Run the complete audit first unless a still-current exhaustive audit exists for the same Git tree. Recheck hashes and `git status`; refresh changed files.
128
+
129
+ Apply verdicts as follows:
130
+
131
+ - `KEEP`: no change.
132
+ - `UPDATE`: make the smallest accurate edit and preserve useful history.
133
+ - `MERGE`: move unique knowledge into the declared canonical file, update inbound links, then `trash` the redundant file.
134
+ - `ARCHIVE`: use only when historical value is real and the repository already has an archive convention.
135
+ - `DELETE`: `trash` only after proving the file is redundant, contradicted without historical value, generated noise, or obsolete execution residue.
136
+ - `VERIFY`: leave unchanged unless the required evidence is obtained.
137
+
138
+ Before modifying any file, obey repository-specific read requirements. If the worktree is dirty, never overwrite overlapping user changes; downgrade that action to `BLOCKED` and continue with disjoint files.
139
+
140
+ After edits:
141
+
142
+ 1. rerun the inventory and local-link checks;
143
+ 2. search for references to removed or renamed files;
144
+ 3. run proportionate repository validation through its required supervisor;
145
+ 4. inspect the final diff and confirm no non-Markdown behavior changed unintentionally;
146
+ 5. report exact files updated, merged, archived, trashed, blocked, and untouched.
147
+
148
+ Do not commit or push unless explicitly requested.
149
+
150
+ ## Completion criteria
151
+
152
+ Complete `audit` only when every eligible Markdown file has one semantic verdict and evidence.
153
+
154
+ Complete `clean` only when:
155
+
156
+ - the exhaustive ledger still matches the current tree;
157
+ - every applied deletion has a proven canonical replacement or explicit no-value rationale;
158
+ - links and references are repaired;
159
+ - protected operational knowledge remains accessible;
160
+ - validation results and blockers are reported separately;
161
+ - the final dirty tree contains no accidental out-of-scope edits.
@@ -0,0 +1,10 @@
1
+ interface:
2
+ display_name: "Audit Agent Memories"
3
+ short_description: "Audit and clean project agent documentation"
4
+ icon_small: "./assets/codex-icon.svg"
5
+ icon_large: "./assets/codex-icon.svg"
6
+ brand_color: "#5F3DC4"
7
+ default_prompt: "Use $audit-memories audit to compare every agent instruction, rule, skill, and task trace in this project with the current code and configuration."
8
+
9
+ policy:
10
+ allow_implicit_invocation: false
@@ -0,0 +1,19 @@
1
+ <svg role="img" aria-label="audit memories skill icon"
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="128"
4
+ height="128"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <ellipse cx="9" cy="5" rx="6" ry="3" />
13
+ <path d="M3 5v8c0 1.7 2.7 3 6 3" />
14
+ <path d="M15 5v5" />
15
+ <path d="M3 9c0 1.7 2.7 3 6 3 2 0 3.8-.5 4.9-1.3" />
16
+ <circle cx="16" cy="16" r="4" />
17
+ <path d="m19 19 2 2" />
18
+ <path d="m14.5 16 1 1 2-2" />
19
+ </svg>
@@ -0,0 +1,105 @@
1
+ # Project agent-memory cleanup rubric
2
+
3
+ Inventory is not semantic review. Classify every eligible agent instruction, rule, skill, command, task trace, plan, and output against current project evidence. Public documentation and normal README files are evidence sources, not cleanup targets.
4
+
5
+ ## Evidence checks
6
+
7
+ Verify claims with the closest primary source:
8
+
9
+ | Markdown claim | Primary local evidence |
10
+ | --- | --- |
11
+ | Commands and scripts | package/workspace manifest and executable scripts |
12
+ | Paths and filenames | current filesystem and imports |
13
+ | Routes and APIs | route files, handlers, schemas, generated API contracts |
14
+ | Data model | schema and migrations |
15
+ | Architecture | source imports, boundaries, configuration, tests |
16
+ | Environment variables | example files, validators, and code references; never secret values |
17
+ | Feature status | current implementation and tests |
18
+ | Deployment/runtime status | live provider read-back, otherwise `NOT VERIFIED` |
19
+ | Historical decision | Git history, ADR, migration, or dated plan context |
20
+
21
+ ## Score
22
+
23
+ Score each agent-memory document:
24
+
25
+ | Dimension | 0 | 1 | 2 | 3 |
26
+ | --- | --- | --- | --- | --- |
27
+ | Current utility | none | occasional | useful | operationally critical |
28
+ | Accuracy | contradicted | mostly stale | mixed | verified/current |
29
+ | Uniqueness | exact duplicate | mostly repeated | partly unique | canonical |
30
+ | Discoverability | orphaned | weakly linked | findable | canonical entry point |
31
+ | Historical value | none | weak | useful | required audit trail |
32
+
33
+ Penalties:
34
+
35
+ - −3 for dangerous commands or guidance contradicted by current code.
36
+ - −2 for pretending an old plan/status is current.
37
+ - −2 for a redundant file whose unique content fits in the canonical document.
38
+ - −1 for broken links, stale paths, temporary identifiers, or execution narration.
39
+
40
+ Interpretation:
41
+
42
+ - 11–15: `KEEP`, with small corrections if needed.
43
+ - 7–10: `UPDATE` or `MERGE`.
44
+ - 4–6: `ARCHIVE` only when history matters; otherwise `DELETE`.
45
+ - 0–3: `DELETE`.
46
+ - External truth required: `VERIFY`, regardless of score.
47
+
48
+ The score informs judgment; it never overrides a safety boundary.
49
+
50
+ ## Keep
51
+
52
+ - Root agent onboarding and instruction documentation that still matches the project.
53
+ - Current architecture, API, schema, development, and operational guidance.
54
+ - AGENTS/CLAUDE/rules/skills that encode active agent behavior.
55
+ - Security, billing, sending, migration, recovery, and production runbooks.
56
+ - ADRs and completed plans that explain a still-relevant non-obvious constraint.
57
+ - Product intent clearly labeled as intent rather than implementation truth.
58
+
59
+ ## Update
60
+
61
+ - A canonical agent document with stale commands, paths, names, or architecture.
62
+ - A useful runbook with drifted implementation details.
63
+ - A plan that should be labeled completed, superseded, or historical.
64
+ - A document mixing verified local facts with unverified production claims.
65
+
66
+ ## Merge
67
+
68
+ - Multiple entry points explaining the same workflow.
69
+ - A temporary analysis whose unique conclusion belongs in README, architecture, ADR, or runbook.
70
+ - Repeated command lists that should derive from one canonical source.
71
+
72
+ Before merging, list every unique fact from the source and its destination. Update inbound links before trashing the source.
73
+
74
+ ## Archive
75
+
76
+ Archive only if the repository already has a discoverable archive convention and the document retains real historical or compliance value. Archiving junk is not cleanup.
77
+
78
+ ## Delete
79
+
80
+ - Empty, generated, accidental, or abandoned Markdown residue.
81
+ - Fully superseded implementation plans with no unique decision history.
82
+ - Point-in-time reports, pasted logs, temporary TODO dumps, or completed execution checklists with no reusable lesson.
83
+ - Exact duplicates and near-duplicates after preserving unique content.
84
+ - Documents describing removed code as current and offering no useful historical context.
85
+ - Unreferenced analyses whose conclusions are already canonical elsewhere.
86
+
87
+ ## Protected deletion questions
88
+
89
+ Before `DELETE`, answer all of these with evidence:
90
+
91
+ 1. Is every unique fact obsolete, duplicated, or valueless?
92
+ 2. Could deletion hide a security, billing, migration, recovery, sending, or production constraint?
93
+ 3. Are inbound links and references known?
94
+ 4. Is there a canonical replacement when readers still need the topic?
95
+ 5. Does Git preserve recoverability, and will `trash` be used locally?
96
+
97
+ If any answer is unclear, choose `UPDATE`, `MERGE`, or `VERIFY`.
98
+
99
+ ## Required result row
100
+
101
+ ```text
102
+ path | lines | purpose | truth status | overlap | verdict | evidence | reason
103
+ ```
104
+
105
+ Never mark the audit complete with sampled rows or project-level guesses.
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only inventory of agent-facing Markdown inside a project."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ import subprocess
12
+ from collections import Counter, defaultdict
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from urllib.parse import unquote
17
+
18
+
19
+ EXCLUDED_DIRS = {
20
+ ".git",
21
+ ".next",
22
+ ".turbo",
23
+ ".cache",
24
+ "node_modules",
25
+ "vendor",
26
+ "dist",
27
+ "build",
28
+ "coverage",
29
+ "tmp",
30
+ "temp",
31
+ "__pycache__",
32
+ }
33
+ MARKDOWN_SUFFIXES = {".md", ".mdx", ".mdc"}
34
+ AGENT_ROOTS = {".agents", ".claude", ".cursor"}
35
+ AGENT_ENTRYPOINTS = {"agents.md", "claude.md", "gemini.md", ".cursorrules"}
36
+ LINK_RE = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
37
+ HEADING_RE = re.compile(r"^#{1,6}\s+", re.MULTILINE)
38
+ STATUS_RE = re.compile(
39
+ r"\b(?:todo|fixme|wip|draft|deprecated|obsolete|superseded|completed|done|current|production)\b",
40
+ re.IGNORECASE,
41
+ )
42
+
43
+
44
+ def parse_args() -> argparse.Namespace:
45
+ parser = argparse.ArgumentParser(description=__doc__)
46
+ parser.add_argument("--root", type=Path, default=Path.cwd())
47
+ parser.add_argument("--scope", default="", help="Optional case-insensitive path/content filter")
48
+ parser.add_argument("--format", choices=("json", "markdown"), default="json")
49
+ parser.add_argument("--output", type=Path, help="Optional report path outside the project root")
50
+ return parser.parse_args()
51
+
52
+
53
+ def project_root(value: Path) -> Path:
54
+ root = value.expanduser().resolve()
55
+ result = subprocess.run(
56
+ ["git", "-C", str(root), "rev-parse", "--show-toplevel"],
57
+ check=False,
58
+ capture_output=True,
59
+ text=True,
60
+ )
61
+ if result.returncode == 0:
62
+ resolved = Path(result.stdout.strip()).resolve()
63
+ if resolved == root or resolved in root.parents:
64
+ return resolved
65
+ return root
66
+
67
+
68
+ def tracked_paths(root: Path) -> tuple[set[str], set[str]]:
69
+ tracked = subprocess.run(
70
+ ["git", "-C", str(root), "ls-files"],
71
+ check=False,
72
+ capture_output=True,
73
+ text=True,
74
+ )
75
+ untracked = subprocess.run(
76
+ ["git", "-C", str(root), "ls-files", "--others", "--exclude-standard"],
77
+ check=False,
78
+ capture_output=True,
79
+ text=True,
80
+ )
81
+ return set(tracked.stdout.splitlines()), set(untracked.stdout.splitlines())
82
+
83
+
84
+ def markdown_files(root: Path) -> tuple[list[Path], list[dict[str, str]]]:
85
+ files: list[Path] = []
86
+ skipped: list[dict[str, str]] = []
87
+ for directory, dirnames, filenames in os.walk(root, followlinks=False):
88
+ current = Path(directory)
89
+ kept: list[str] = []
90
+ for dirname in sorted(dirnames):
91
+ child = current / dirname
92
+ relative = child.relative_to(root).as_posix()
93
+ if dirname in EXCLUDED_DIRS:
94
+ skipped.append({"path": relative + "/", "reason": "generated or control directory"})
95
+ elif child.is_symlink():
96
+ skipped.append({"path": relative, "reason": "symlink directory outside traversal"})
97
+ else:
98
+ kept.append(dirname)
99
+ dirnames[:] = kept
100
+
101
+ for filename in sorted(filenames):
102
+ path = current / filename
103
+ relative_path = path.relative_to(root)
104
+ if not is_agent_markdown(relative_path):
105
+ continue
106
+ relative = relative_path.as_posix()
107
+ if path.is_symlink():
108
+ skipped.append({"path": relative, "reason": "symlink file"})
109
+ else:
110
+ files.append(path)
111
+ return sorted(files), skipped
112
+
113
+
114
+ def is_agent_markdown(relative: Path) -> bool:
115
+ parts = tuple(part.lower() for part in relative.parts)
116
+ name = relative.name.lower()
117
+ suffix = relative.suffix.lower()
118
+ if name in AGENT_ENTRYPOINTS:
119
+ return True
120
+ if parts and parts[0] in AGENT_ROOTS and suffix in MARKDOWN_SUFFIXES:
121
+ return True
122
+ if parts[:2] == (".github", "copilot-instructions.md"):
123
+ return True
124
+ if len(parts) >= 3 and parts[:2] == (".github", "instructions") and suffix in MARKDOWN_SUFFIXES:
125
+ return True
126
+ return name == "skill.md" and "skills" in parts
127
+
128
+
129
+ def document_kind(relative: Path) -> str:
130
+ name = relative.name.lower()
131
+ parts = {part.lower() for part in relative.parts}
132
+ if name in AGENT_ENTRYPOINTS or "rules" in parts or "skills" in parts:
133
+ return "instruction"
134
+ if "commands" in parts:
135
+ return "command"
136
+ if "plans" in parts or "tasks" in parts or "ralph-tasks" in parts:
137
+ return "plan_or_task"
138
+ if "output" in parts:
139
+ return "agent_output"
140
+ if "docs" in parts:
141
+ return "agent_reference"
142
+ if "styles" in parts:
143
+ return "agent_style"
144
+ return "agent_other"
145
+
146
+
147
+ def local_target(root: Path, source: Path, raw_target: str) -> tuple[str, bool | None, str] | None:
148
+ target = raw_target.strip().split(maxsplit=1)[0].strip("<>")
149
+ if not target or target.startswith(("#", "http://", "https://", "mailto:", "tel:", "data:")):
150
+ return None
151
+ if "{{" in target or "}}" in target:
152
+ return None
153
+ target = unquote(target.split("#", 1)[0].split("?", 1)[0])
154
+ if not target:
155
+ return None
156
+ if target.startswith("/"):
157
+ return target, None, "application_route"
158
+ resolved = source.parent / target
159
+ resolved = resolved.resolve()
160
+ try:
161
+ relative = resolved.relative_to(root).as_posix()
162
+ except ValueError:
163
+ return str(resolved), False, "outside_project"
164
+ return relative, resolved.exists(), "project_file"
165
+
166
+
167
+ def inspect_file(path: Path, root: Path, tracked: set[str], untracked: set[str], scope: str) -> tuple[dict[str, Any], str]:
168
+ raw = path.read_bytes()
169
+ text = raw.decode("utf-8", errors="replace")
170
+ relative = path.relative_to(root)
171
+ relative_text = relative.as_posix()
172
+ links: list[dict[str, Any]] = []
173
+ for match in LINK_RE.finditer(text):
174
+ checked = local_target(root, path, match.group(1))
175
+ if checked is None:
176
+ continue
177
+ target, exists, kind = checked
178
+ links.append(
179
+ {
180
+ "target": target,
181
+ "exists": exists,
182
+ "kind": kind,
183
+ "line": text.count("\n", 0, match.start()) + 1,
184
+ }
185
+ )
186
+
187
+ normalized = re.sub(r"\s+", " ", text).strip().lower().encode("utf-8")
188
+ record = {
189
+ "path": relative_text,
190
+ "kind": document_kind(relative),
191
+ "git_state": "tracked" if relative_text in tracked else "untracked" if relative_text in untracked else "ignored_or_external",
192
+ "bytes": len(raw),
193
+ "lines": text.count("\n") + (1 if text and not text.endswith("\n") else 0),
194
+ "sha256": hashlib.sha256(raw).hexdigest(),
195
+ "normalized_sha256": hashlib.sha256(normalized).hexdigest(),
196
+ "modified_utc": datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat(),
197
+ "headings": len(HEADING_RE.findall(text)),
198
+ "local_links": links,
199
+ "broken_local_links": [
200
+ link for link in links if link["kind"] in {"project_file", "outside_project"} and link["exists"] is False
201
+ ],
202
+ "application_route_links": [link for link in links if link["kind"] == "application_route"],
203
+ "status_markers": sorted({match.group(0).lower() for match in STATUS_RE.finditer(text)}),
204
+ "scope_hits": text.lower().count(scope.lower()) if scope else None,
205
+ }
206
+ return record, text
207
+
208
+
209
+ def build_report(root: Path, scope: str) -> dict[str, Any]:
210
+ paths, skipped = markdown_files(root)
211
+ tracked, untracked = tracked_paths(root)
212
+ records: list[dict[str, Any]] = []
213
+ failures: list[dict[str, str]] = []
214
+ hashes: defaultdict[str, list[str]] = defaultdict(list)
215
+ normalized_hashes: defaultdict[str, list[str]] = defaultdict(list)
216
+
217
+ for path in paths:
218
+ relative = path.relative_to(root).as_posix()
219
+ try:
220
+ record, _ = inspect_file(path, root, tracked, untracked, scope)
221
+ records.append(record)
222
+ hashes[record["sha256"]].append(relative)
223
+ normalized_hashes[record["normalized_sha256"]].append(relative)
224
+ except OSError as error:
225
+ failures.append({"path": relative, "reason": str(error)})
226
+
227
+ kind_counts = Counter(record["kind"] for record in records)
228
+ matching = [
229
+ record["path"]
230
+ for record in records
231
+ if not scope or scope.lower() in record["path"].lower() or bool(record["scope_hits"])
232
+ ]
233
+ return {
234
+ "generated_at": datetime.now(timezone.utc).isoformat(),
235
+ "root": str(root),
236
+ "scope": scope or None,
237
+ "ledger": {
238
+ "eligible": len(paths),
239
+ "visited": len(records),
240
+ "skipped": skipped,
241
+ "failed": failures,
242
+ "complete": len(records) + len(failures) == len(paths),
243
+ },
244
+ "totals": {
245
+ "files": len(records),
246
+ "lines": sum(record["lines"] for record in records),
247
+ "bytes": sum(record["bytes"] for record in records),
248
+ "by_kind": dict(sorted(kind_counts.items())),
249
+ },
250
+ "scope_matching_files": matching,
251
+ "exact_duplicate_groups": [group for group in hashes.values() if len(group) > 1],
252
+ "normalized_duplicate_groups": [group for group in normalized_hashes.values() if len(group) > 1],
253
+ "broken_local_links": [
254
+ {"path": record["path"], "links": record["broken_local_links"]}
255
+ for record in records
256
+ if record["broken_local_links"]
257
+ ],
258
+ "application_route_links": [
259
+ {"path": record["path"], "links": record["application_route_links"]}
260
+ for record in records
261
+ if record["application_route_links"]
262
+ ],
263
+ "files": records,
264
+ }
265
+
266
+
267
+ def markdown_report(report: dict[str, Any]) -> str:
268
+ ledger = report["ledger"]
269
+ totals = report["totals"]
270
+ lines = [
271
+ "# Project agent-memory inventory",
272
+ "",
273
+ f"- Root: `{report['root']}`",
274
+ f"- Visited: {ledger['visited']} / {ledger['eligible']}",
275
+ f"- Failed: {len(ledger['failed'])}",
276
+ f"- Total: {totals['files']} files, {totals['lines']} lines, {totals['bytes']} bytes",
277
+ f"- Exact duplicate groups: {len(report['exact_duplicate_groups'])}",
278
+ f"- Broken-link files: {len(report['broken_local_links'])}",
279
+ "",
280
+ "| Path | Kind | Git | Lines | Broken links |",
281
+ "| --- | --- | --- | ---: | ---: |",
282
+ ]
283
+ for record in report["files"]:
284
+ lines.append(
285
+ f"| `{record['path']}` | {record['kind']} | {record['git_state']} | "
286
+ f"{record['lines']} | {len(record['broken_local_links'])} |"
287
+ )
288
+ return "\n".join(lines) + "\n"
289
+
290
+
291
+ def main() -> None:
292
+ args = parse_args()
293
+ root = project_root(args.root)
294
+ report = build_report(root, args.scope.strip())
295
+ rendered = json.dumps(report, indent=2, ensure_ascii=False) if args.format == "json" else markdown_report(report)
296
+ if args.output:
297
+ output = args.output.expanduser().resolve()
298
+ if output == root or root in output.parents:
299
+ raise SystemExit("Refusing to write an audit report inside the project without explicit agent handling")
300
+ output.parent.mkdir(parents=True, exist_ok=True)
301
+ output.write_text(rendered + ("\n" if args.format == "json" else ""), encoding="utf-8")
302
+ else:
303
+ print(rendered)
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()