codebase-snapshot 0.0.0-stage → 0.9.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,491 @@
1
+ type Mode = "generate" | "validate" | "report";
2
+ type OutputFormat = "text" | "json";
3
+ /**
4
+ * How files are discovered.
5
+ *
6
+ * `auto` lists files through git when git can do it and walks the tree otherwise; `git` and `walk`
7
+ * force one path. It is part of the manifest's recorded settings because it decides which files a
8
+ * snapshot contains — the same repository scanned both ways is two different snapshots.
9
+ */
10
+ type DiscoveryMode = "auto" | "git" | "walk";
11
+ /**
12
+ * How a file's bytes became text.
13
+ *
14
+ * Absent from a `FileEntry` means plain UTF-8, which is the overwhelmingly common case. The other
15
+ * values are recorded because they change what the artifact actually contains: `utf-16le`/`utf-16be`
16
+ * files were transcoded, and `utf-8-lossy` files carry U+FFFD where their undecodable bytes were.
17
+ */
18
+ type TextEncoding = "utf-8" | "utf-8-lossy" | "utf-16le" | "utf-16be";
19
+ /**
20
+ * How much of a file made it into the snapshot.
21
+ * `full` – the complete file body was emitted.
22
+ * `preview` – the body was truncated (by lines and/or bytes) before emission.
23
+ */
24
+ type InclusionMode = "full" | "preview";
25
+ interface SourceSummary {
26
+ directives: string[];
27
+ imports: string[];
28
+ exports: string[];
29
+ /** Module specifiers re-exported wholesale via `export * from "..."`. */
30
+ reExports: string[];
31
+ functions: string[];
32
+ classes: string[];
33
+ interfaces: string[];
34
+ types: string[];
35
+ enums: string[];
36
+ variables: string[];
37
+ reactComponents: string[];
38
+ hooks: string[];
39
+ routeHandlers: string[];
40
+ /** Number of parser diagnostics, present only when the file has syntax errors. */
41
+ parseErrors?: number;
42
+ }
43
+ interface JsonSummary {
44
+ shape: string;
45
+ itemCount: number | null;
46
+ keys: string[];
47
+ sampleKeys: string[];
48
+ }
49
+ interface MarkdownSummary {
50
+ headings: string[];
51
+ frontmatterKeys: string[];
52
+ }
53
+ /**
54
+ * How a summary was produced.
55
+ *
56
+ * `ast` — a real parser (TypeScript compiler; the script block of a
57
+ * Vue/Svelte/Astro component).
58
+ * `structural` — a real grammar for a data format (JSON, Markdown).
59
+ * `heuristic` — line patterns. Useful, but not a parse, and never presented
60
+ * as one.
61
+ */
62
+ type SummaryQuality = "ast" | "structural" | "heuristic";
63
+ /**
64
+ * Why a discovered path was not emitted.
65
+ *
66
+ * Every reason below is produced by the scanner; nothing in the type is
67
+ * aspirational. `asset` was removed because no code path ever emitted it.
68
+ */
69
+ type SkipReason = "binary" | "read-error" | "not-file"
70
+ /** A credential-bearing name (`.env`, `*.pem`, `.npmrc`, …). */
71
+ | "sensitive-file"
72
+ /** The path itself matched a secret rule; the emitted path is span-masked. */
73
+ | "sensitive-path"
74
+ /** A directory link (never followed), or a link to neither file nor directory. */
75
+ | "symlink"
76
+ /** A file link whose target resolves outside the scanned root. */
77
+ | "outside-root"
78
+ /** Excluded by a `.gitignore` in a scan without git. */
79
+ | "gitignore"
80
+ /** Excluded by a built-in rule: the ignore list, a preset, or an output dir. */
81
+ | "default-ignore"
82
+ /** Excluded by the user's own `excludePatterns` or `include` allow-list. */
83
+ | "excluded"
84
+ /**
85
+ * A directory that belongs to another repository: a nested checkout or a submodule.
86
+ *
87
+ * A submodule is a single gitlink entry, so `stat` reports a directory where a file was
88
+ * expected — which used to be recorded as the generic `not-file`, implying the path was a
89
+ * socket or a FIFO rather than a repository whose contents are simply somebody else's.
90
+ */
91
+ | "nested-repository";
92
+ interface SkipRecord {
93
+ /** Path as it may be shown in an artifact: secret spans are masked. */
94
+ path: string;
95
+ reason: SkipReason;
96
+ detail?: string;
97
+ }
98
+ /**
99
+ * One scanned file. Field names are canonical and unique by design:
100
+ * earlier releases carried `path`/`relativePath`, `sizeBytes`/`bytes` and
101
+ * `hashSha256`/`sha256` aliases that could only ever agree.
102
+ */
103
+ interface FileEntry {
104
+ relativePath: string;
105
+ sizeBytes: number;
106
+ lines: number;
107
+ hashSha256: string;
108
+ part: string;
109
+ layer: string;
110
+ kind: string;
111
+ language: string;
112
+ inclusionMode: InclusionMode;
113
+ isLarge: boolean;
114
+ isPreviewTruncated: boolean;
115
+ /** Line count of the file on disk. */
116
+ originalLines: number;
117
+ /** Lines actually emitted into the snapshot. */
118
+ includedLines: number;
119
+ /** UTF-8 bytes of file content actually emitted into the snapshot. */
120
+ includedBytes: number;
121
+ summary?: SourceSummary | JsonSummary | MarkdownSummary;
122
+ /** How the summary was produced. Absent when there is no summary. */
123
+ summaryQuality?: SummaryQuality;
124
+ /** True when the summary alone was too large for the part budget and was dropped. */
125
+ summaryOmitted?: boolean;
126
+ /**
127
+ * How the file's bytes were decoded. Absent means plain UTF-8. Present when the file was
128
+ * transcoded from UTF-16, or when it was not valid UTF-8 and lost bytes to U+FFFD.
129
+ */
130
+ encoding?: TextEncoding;
131
+ /** Emitted body after redaction/truncation. Never serialized into the manifest. */
132
+ content?: string;
133
+ }
134
+ interface PartFileEntry {
135
+ relativePath: string;
136
+ sourceBytes: number;
137
+ includedBytes: number;
138
+ lines: number;
139
+ layer: string;
140
+ kind: string;
141
+ }
142
+ interface PartMetadata {
143
+ index: number;
144
+ filename: string;
145
+ /** Exact UTF-8 byte length of the emitted part file. */
146
+ byteSize: number;
147
+ sha256: string;
148
+ lines: number;
149
+ fileCount: number;
150
+ layersCovered: string[];
151
+ includedFiles: PartFileEntry[];
152
+ }
153
+ interface SecretRule {
154
+ rule: string;
155
+ pattern: RegExp;
156
+ /**
157
+ * Index of the capture group that holds the secret, when the rule has to match
158
+ * its surrounding context to identify one (`password: "…"`). Redaction then
159
+ * masks only that group, so the key and the rest of the line survive.
160
+ */
161
+ valueGroup?: number;
162
+ }
163
+ /**
164
+ * Shape accepted for `secrets.customPatterns`.
165
+ *
166
+ * A bare `RegExp` or string is accepted because it is the documented form; the
167
+ * object form exists to name a rule. `valueGroup` is only useful with the object
168
+ * form, since a bare pattern has no group to name.
169
+ */
170
+ type SecretRuleInput = {
171
+ rule: string;
172
+ pattern: RegExp | string;
173
+ valueGroup?: number;
174
+ } | RegExp | string;
175
+ /**
176
+ * A detected secret. Deliberately carries no excerpt of the matched source:
177
+ * the previous `snippet` field was the vector by which raw credentials were
178
+ * written into `reports/audit.md`.
179
+ */
180
+ interface SecretFinding {
181
+ /**
182
+ * Every rule that matched this line, in rule-table order.
183
+ *
184
+ * One finding is one *line*, not one (line, rule) pair. A single line can match two rules — an
185
+ * unquoted `AUTH_TOKEN=<value>` assignment matches both `credential-assignment-unquoted` and
186
+ * `env-secret-assignment` — and reporting it twice doubled the figure a user is asked to explain,
187
+ * in the CLI summary and in `reports/audit.md` alike.
188
+ */
189
+ rules: string[];
190
+ line: number;
191
+ path?: string;
192
+ }
193
+ interface ManifestTotals {
194
+ candidateFiles: number;
195
+ includedFiles: number;
196
+ fullFiles: number;
197
+ previewFiles: number;
198
+ skippedFiles: number;
199
+ totalSourceBytes: number;
200
+ totalContentBytes: number;
201
+ partCount: number;
202
+ warningCount: number;
203
+ /**
204
+ * Lines whose values were masked, counted once per line.
205
+ *
206
+ * Separate from `warningCount` because they are not the same event: a warning says the snapshot
207
+ * may surprise you, while a redaction says the tool did its job. Folding them together made a
208
+ * clean repository report `Warnings: 156` and left the summary's own slice of ten looking like
209
+ * a truncated warning list.
210
+ */
211
+ redactedLines: number;
212
+ }
213
+ interface ManifestArtifact {
214
+ byteSize: number;
215
+ sha256: string;
216
+ }
217
+ interface ManifestIndexes {
218
+ routes: Array<{
219
+ path: string;
220
+ methods: string[];
221
+ part: string;
222
+ }>;
223
+ components: Array<{
224
+ path: string;
225
+ symbols: string[];
226
+ part: string;
227
+ layer: string;
228
+ }>;
229
+ handlers: Array<{
230
+ path: string;
231
+ exports: string[];
232
+ part: string;
233
+ }>;
234
+ features: Array<{
235
+ name: string;
236
+ files: number;
237
+ }>;
238
+ imports: {
239
+ byFile: Array<{
240
+ path: string;
241
+ imports: string[];
242
+ exports: string[];
243
+ }>;
244
+ centralFiles: Array<{
245
+ path: string;
246
+ importedByCount: number;
247
+ }>;
248
+ };
249
+ directives: Array<{
250
+ path: string;
251
+ directives: string[];
252
+ }>;
253
+ }
254
+ /** Effective configuration recorded so `validate` can detect config drift. */
255
+ interface ManifestConfigSnapshot {
256
+ /** Digest of the effective settings; see `configDigest`. */
257
+ digest?: string;
258
+ maxPartBytes: number;
259
+ largeFileBytes: number;
260
+ previewHeadLines: number;
261
+ previewTailLines: number;
262
+ includeMonolith: boolean;
263
+ includeSensitiveFiles: boolean;
264
+ /** How files were discovered; `undefined` on manifests written before this was recorded. */
265
+ discovery?: DiscoveryMode;
266
+ artifacts: ResolvedArtifacts;
267
+ include: string[];
268
+ preset: string;
269
+ layersMode: "merge" | "replace";
270
+ budgets: ResolvedBudgets;
271
+ excludePatterns: string[];
272
+ layers: string[];
273
+ kinds: string[];
274
+ }
275
+ interface Manifest {
276
+ schemaVersion: string;
277
+ generatedAt: string;
278
+ /**
279
+ * Present only in the manifest embedded in `full-codebase.json`.
280
+ *
281
+ * That file cannot record its own sha256, so its embedded copy leaves `artifacts.fullJson` null
282
+ * and points at `manifest.json` for the real digest instead of writing a number that describes
283
+ * different bytes. The shipped manifest never carries this field.
284
+ */
285
+ selfReference?: string;
286
+ generator: {
287
+ name: string;
288
+ version: string;
289
+ };
290
+ git: {
291
+ branch: string;
292
+ commit: string;
293
+ isDirty: boolean;
294
+ };
295
+ budgets: {
296
+ maxPartBytes: number;
297
+ largeFileBytes: number;
298
+ };
299
+ config: ManifestConfigSnapshot;
300
+ counts: {
301
+ totalFiles: number;
302
+ totalSizeBytes: number;
303
+ totalLines: number;
304
+ partsCount: number;
305
+ skippedCount: number;
306
+ largeFileCount: number;
307
+ truncatedPreviewCount: number;
308
+ };
309
+ totals: ManifestTotals;
310
+ fingerprint: string;
311
+ validation: {
312
+ status: "passed" | "failed";
313
+ errors: string[];
314
+ };
315
+ artifacts: {
316
+ fullJson: ManifestArtifact | null;
317
+ fullMarkdown: ManifestArtifact | null;
318
+ };
319
+ layers: Record<string, number>;
320
+ kinds: Record<string, number>;
321
+ indexes: ManifestIndexes;
322
+ parts: PartMetadata[];
323
+ files: FileEntry[];
324
+ skipped: SkipRecord[];
325
+ warnings: string[];
326
+ }
327
+ interface LayerRule {
328
+ name: string;
329
+ match: string | RegExp | ((filePath: string) => boolean);
330
+ }
331
+ /**
332
+ * Which snapshot artifacts get written.
333
+ *
334
+ * Everything a developer opens is produced by default; each entry exists only so
335
+ * a single artifact can be switched off without disabling the rest. `monolith`
336
+ * is the one exception, because it duplicates the entire payload: it follows
337
+ * `includeMonolith` (off by default) unless set here explicitly.
338
+ */
339
+ interface ArtifactSelection {
340
+ /** `README.md` — the orientation page for humans and models. */
341
+ readme?: boolean;
342
+ /** `manifest.json` — the registry `validate` and `report` read. */
343
+ manifest?: boolean;
344
+ /** `parts/part-XXX.md` — the partitioned code payload. */
345
+ parts?: boolean;
346
+ /** `reports/navigation.md` — file-to-part lookup index. */
347
+ navigation?: boolean;
348
+ /** `reports/project-tree.md` — ASCII repository tree. */
349
+ projectTree?: boolean;
350
+ /** `reports/audit.md` — security, layer, kind and skip findings. */
351
+ audit?: boolean;
352
+ /** `full-codebase.{json,md}` — one file per format, the whole payload again. */
353
+ monolith?: boolean;
354
+ }
355
+ /** Every artifact decision made concrete. */
356
+ type ResolvedArtifacts = Required<ArtifactSelection>;
357
+ interface KindRule {
358
+ kind: string;
359
+ match: string | RegExp | ((filePath: string) => boolean);
360
+ }
361
+ /** A byte budget: either raw bytes or a human-readable size such as `"120k"`. */
362
+ type ByteSize = number | string;
363
+ /** Per-layer part budgets. Unlisted layers fall back to `default`. */
364
+ interface BudgetConfig {
365
+ default?: ByteSize;
366
+ byLayer?: Record<string, ByteSize>;
367
+ }
368
+ /** Every budget resolved to concrete byte counts. */
369
+ interface ResolvedBudgets {
370
+ default: number;
371
+ byLayer: Record<string, number>;
372
+ }
373
+ /** Lifecycle hooks. Not serialized into the manifest (functions are not data). */
374
+ interface HooksConfig {
375
+ /** Runs once after a successful generate, with the manifest that was written. */
376
+ afterGenerate?: (manifest: Manifest) => void;
377
+ }
378
+ interface SnapshotConfig {
379
+ cwd?: string;
380
+ branch?: string;
381
+ outDir?: string;
382
+ tmpDir?: string;
383
+ /** Raw bytes, or a human-readable size (`"120k"`, `"1.5mb"`). */
384
+ maxPartBytes?: ByteSize;
385
+ largeFileBytes?: ByteSize;
386
+ previewHeadLines?: number;
387
+ previewTailLines?: number;
388
+ includeMonolith?: boolean;
389
+ maxMonolithBytes?: ByteSize | null;
390
+ /** Opt in to emitting `.env`/`*.pem`/`.npmrc`/credential files. Default false. */
391
+ includeSensitiveFiles?: boolean;
392
+ /** Allow-list of globs. When non-empty, only matching files are included. */
393
+ include?: string[];
394
+ /** Force a preset by name instead of auto-detecting one. */
395
+ preset?: string;
396
+ /** Switch individual snapshot artifacts off. Everything is on by default. */
397
+ artifacts?: ArtifactSelection;
398
+ /**
399
+ * How user `layers`/`kinds` interact with the detected preset (default
400
+ * `"merge"`): user rules are evaluated first, then the preset fills the gaps.
401
+ * `"replace"` discards the preset entirely.
402
+ */
403
+ layersMode?: "merge" | "replace";
404
+ /** Per-layer part budgets. */
405
+ budgets?: BudgetConfig;
406
+ hooks?: HooksConfig;
407
+ layers?: LayerRule[];
408
+ kinds?: KindRule[];
409
+ excludePatterns?: string[];
410
+ secrets?: {
411
+ customPatterns?: SecretRuleInput[];
412
+ disableDefaultPatterns?: boolean;
413
+ };
414
+ }
415
+ interface ResolvedSnapshotConfig {
416
+ cwd: string;
417
+ branch: string;
418
+ outDir: string;
419
+ tmpDir: string;
420
+ maxPartBytes: number;
421
+ largeFileBytes: number;
422
+ previewHeadLines: number;
423
+ previewTailLines: number;
424
+ includeMonolith: boolean;
425
+ maxMonolithBytes: number | null;
426
+ includeSensitiveFiles: boolean;
427
+ /**
428
+ * How files are discovered; `auto` picks git when it can list files and walks the tree otherwise.
429
+ * Optional because a resolved config can arrive from an older caller that never named it — every
430
+ * reader treats `undefined` as `auto`, so the effective mode is never guessed twice.
431
+ */
432
+ discovery?: DiscoveryMode;
433
+ /** Empty means "no allow-list"; every discovered file is a candidate. */
434
+ include: string[];
435
+ /** The preset that was resolved (detected or forced). */
436
+ preset: string;
437
+ /** Ecosystem ignores contributed by the preset, merged with `excludePatterns`. */
438
+ presetExclude: string[];
439
+ artifacts: ResolvedArtifacts;
440
+ layersMode: "merge" | "replace";
441
+ budgets: ResolvedBudgets;
442
+ hooks: HooksConfig;
443
+ layers: LayerRule[];
444
+ kinds: KindRule[];
445
+ excludePatterns: string[];
446
+ secretRules: SecretRule[];
447
+ }
448
+ interface CliOptions {
449
+ mode: Mode;
450
+ format: OutputFormat;
451
+ dryRun: boolean;
452
+ cwd?: string;
453
+ outDir?: string;
454
+ includeMonolith?: boolean;
455
+ includeSensitiveFiles?: boolean;
456
+ maxMonolithBytes?: number | null;
457
+ maxPartBytes?: number;
458
+ largeFileBytes?: number;
459
+ /** Artifact names to suppress for this invocation (`--skip-artifacts`). */
460
+ skipArtifacts?: string[];
461
+ }
462
+ interface ValidationDrift {
463
+ missingInSnapshot: string[];
464
+ missingOnDisk: string[];
465
+ /** Files that exist but could not be read, so their content could not be verified. */
466
+ unreadable: string[];
467
+ hashMismatches: Array<{
468
+ path: string;
469
+ diskHash: string;
470
+ snapshotHash: string;
471
+ }>;
472
+ /** Emitted `parts/*.md` that are missing or no longer match their recorded hash. */
473
+ partIssues: Array<{
474
+ part: string;
475
+ issue: string;
476
+ }>;
477
+ /** `counts`/`totals`/`budgets` that disagree with the rest of the manifest. */
478
+ totalsMismatches: string[];
479
+ /** Effective-configuration differences between the manifest and the current run. */
480
+ configMismatches: string[];
481
+ }
482
+ interface ValidationResult {
483
+ valid: boolean;
484
+ manifestPresent: boolean;
485
+ drift: ValidationDrift;
486
+ errors: string[];
487
+ /** Non-fatal observations (e.g. branch moved on). */
488
+ warnings: string[];
489
+ }
490
+
491
+ export type { ArtifactSelection as A, ByteSize as B, CliOptions as C, DiscoveryMode as D, FileEntry as F, HooksConfig as H, InclusionMode as I, JsonSummary as J, KindRule as K, LayerRule as L, MarkdownSummary as M, OutputFormat as O, PartMetadata as P, ResolvedSnapshotConfig as R, SecretRule as S, TextEncoding as T, ValidationResult as V, SkipRecord as a, SourceSummary as b, SnapshotConfig as c, SecretFinding as d, Manifest as e, BudgetConfig as f, ManifestArtifact as g, ManifestConfigSnapshot as h, ManifestIndexes as i, ManifestTotals as j, Mode as k, PartFileEntry as l, ResolvedArtifacts as m, ResolvedBudgets as n, SecretRuleInput as o, SkipReason as p, SummaryQuality as q, ValidationDrift as r };
package/docs/agent.md ADDED
@@ -0,0 +1,135 @@
1
+ # Using this with an AI agent
2
+
3
+ The package ships two things that make it usable by an agent that has never heard of it: a
4
+ **machine-readable contract** and a **bundled skill**.
5
+
6
+ Nothing needs to be installed globally and no prior knowledge of the tool is required.
7
+
8
+ ## The three commands an agent needs
9
+
10
+ ```bash
11
+ # 1. Read the contract. This is the machine-readable replacement for --help.
12
+ npx codebase-snapshot describe --format json
13
+
14
+ # 2. Write a commented config that lists every setting (non-interactive).
15
+ npx codebase-snapshot init --yes
16
+
17
+ # 3. Generate and verify.
18
+ npx codebase-snapshot generate
19
+ npx codebase-snapshot validate
20
+ ```
21
+
22
+ The bare package name works; pin an exact version (`npx codebase-snapshot --version` prints the current one) when the same version must run on every
23
+ machine.
24
+
25
+ ## The contract
26
+
27
+ `describe --format json` returns the whole tool as data on stdout:
28
+
29
+ | Field | Contents |
30
+ | :--- | :--- |
31
+ | `invocation` | The supported one-shot and installed commands, and the bin names. |
32
+ | `workflow` | An ordered six-step procedure: verify → understand → configure → generate → verify output → consume. |
33
+ | `commands` | Every command with what it does, its positionals and its exit codes. |
34
+ | `options` | Every option with its type, default, which commands it applies to, and a summary. |
35
+ | `exitCodes` | `0` success, `1` drift or refused generation, `2` usage error. |
36
+ | `presets` | All 25, with priority, recommended budget and ecosystem ignores. |
37
+ | `artifacts` | Each artifact with its on/off default and what it contains. |
38
+ | `config` | Every config filename and every setting with its default. |
39
+ | `output` | The directory layout and how to read `manifest.json`. |
40
+ | `safety` | What is excluded or redacted, and which directories are refused. |
41
+ | `troubleshooting` | Symptom → cause → fix. |
42
+
43
+ It is generated from the same code the CLI runs — presets, artifact names and versions are
44
+ read directly — and a test compares its documented defaults against a real
45
+ `generate --dry-run` manifest. If a default changes, the test fails rather than the contract
46
+ silently misleading an agent.
47
+
48
+ `describe` with no `--format` renders the same content for a human.
49
+
50
+ ## The skill
51
+
52
+ ```bash
53
+ npx codebase-snapshot skill # print it (stdout)
54
+ npx codebase-snapshot skill --install # write it into the repository
55
+ ```
56
+
57
+ Printing goes to stdout deliberately, so it can be redirected:
58
+
59
+ ```bash
60
+ npx codebase-snapshot skill > SKILL.md
61
+ ```
62
+
63
+ `--install` writes the same skill into the registries agent tools read by name:
64
+
65
+ | Target | Path | Read by |
66
+ | :--- | :--- | :--- |
67
+ | `claude` | `.claude/skills/codebase-snapshot/SKILL.md` | Claude Code auto-discovery |
68
+ | `cursor` | `.cursor/rules/codebase-snapshot.mdc` | Cursor rules |
69
+
70
+ ```bash
71
+ npx codebase-snapshot skill --install --target claude # one registry
72
+ npx codebase-snapshot skill --install # both (default)
73
+ ```
74
+
75
+ Both are **registries keyed by skill name**: the path carries `codebase-snapshot`, so the file
76
+ cannot be mistaken for your project's own documentation, and two skills cannot collide.
77
+
78
+ **It never writes `AGENTS.md`.** That file is the repository's own agent contract — build
79
+ commands, conventions, house rules — and an install that creates or edits it puts a tool's
80
+ marketing where the project's rules belong. If you want the skill in there, print it and paste
81
+ it, so the diff and the commit are yours:
82
+
83
+ ```bash
84
+ npx codebase-snapshot skill >> AGENTS.md
85
+ ```
86
+
87
+ The install is **idempotent**, and whatever it *creates* is appended to `.gitignore` in the
88
+ same run — these are a tool's files written into your repository, and `git status` should not
89
+ turn that into a decision about what to commit. A file that already existed is never ignored.
90
+ `--format json` reports what was created, updated or unchanged, and which ignore entries were
91
+ added.
92
+
93
+ `codebase-snapshot init` does none of this: it writes the config, the npm scripts, the
94
+ devDependency and the `.gitignore` entries for the snapshot directory, and nothing else.
95
+
96
+ ## Why both a contract and a skill
97
+
98
+ The contract is for **doing** — the data an agent needs to pick correct commands and options.
99
+ The skill is for **knowing when to act** — it carries the trigger conditions, the ordered
100
+ workflow, and the surprises an agent cannot infer from a schema: sensitive files excluded by
101
+ default, secrets redacted, the output directory being tool-owned.
102
+
103
+ ## What an agent should avoid
104
+
105
+ - **Do not point `--out-dir` at source code.** It is refused, and the refusal is deliberate.
106
+ - **Do not add `--include-sensitive-files`** unless the user explicitly asked for credential
107
+ files; by default they are excluded and recorded as `sensitive-file` in `manifest.skipped[]`.
108
+ - **Do not work around redaction.** If a value is masked, ask the user rather than reading the
109
+ original file.
110
+ - **Do not assume exit 2 means the repository is broken.** It means the command was wrong.
111
+ Re-read the contract.
112
+ - **Do not treat a bare `npx` that prints nothing as a repository problem.** A stale global
113
+ install shadows `npx`; see [Troubleshooting](troubleshooting.md#the-command-does-nothing).
114
+
115
+ ## Verifying an agent can drive it
116
+
117
+ `tests/agent-simulation.test.js` runs the published instructions as an agent would: read
118
+ `--help`, discover `describe`, then execute the six workflow steps in order against an 8-file
119
+ Django fixture.
120
+
121
+ It asserts what the contract promises at each step: `--version` prints a version, every
122
+ published option states where it applies, `init --yes` writes a config naming **every**
123
+ setting the contract lists, `generate --format json` exits `0` with
124
+ `validation.status: "passed"` and the preset `python-django`, every fixture file is either in
125
+ `manifest.files[]` or explained in `manifest.skipped[]`, each `files[]` entry carries its
126
+ classification and hash, and `validate` then exits `0`. If an agent following the published
127
+ words would fail, that test fails.
128
+
129
+ When you parse the manifest, remember what is conditional: `manifest.files[]` carries
130
+ `relativePath`, `layer`, `kind`, `language`, `part`, `inclusionMode` and `hashSha256` for
131
+ every included file, but `summary` and `summaryQuality` only for a language the tool can
132
+ analyse. A `.gitignore` or plain-text entry has neither key, and a file whose summary was too
133
+ large for its part carries `summaryOmitted: true` instead of a `summary`. `encoding` appears only
134
+ when a file was not plain UTF-8: `utf-16le`/`utf-16be` were transcoded, and `utf-8-lossy` means
135
+ the body you are reading contains `U+FFFD` where undecodable bytes were.