cadet-agent 0.24.0 → 0.25.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Cadet-Agent is an **opinionated** cross-IDE agent framework for game-development workflows. It is built on foundational software engineering practices and real-world game-development experience, with the goal of **guiding you through the entire development process** — from requirements and technical design through TDD, implementation, and review.
4
4
 
5
- Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished game from a single prompt. Instead, it walks you through each phase methodically: calibrating the learner model, scoping work into epics and stories, planning architecture, writing tests first, and iterating on feedback. The shared framework core integrates with GitHub Copilot, Cursor, Continue, and Claude Code.
5
+ Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished game from a single prompt. Instead, it walks you through each phase methodically: calibrating the learner model, scoping work into epics and stories, planning architecture, writing tests first, and iterating on feedback. The shared framework core integrates with GitHub Copilot, Cursor, Continue, Claude Code, and Deep Code.
6
6
 
7
7
  ## Repository Layout
8
8
  - `.cadet/agent/core/` contains the shared Cadet-Agent framework documents.
@@ -20,31 +20,32 @@ Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished g
20
20
  - `.cursor/` contains Cursor-specific authored files.
21
21
  - `.continue/` contains Continue-specific authored files.
22
22
  - `.claude/` contains Claude Code-specific authored files.
23
+ - `.agents/skills/` contains Deep Code (and cross-client) skill adapters.
23
24
  - These IDE folders hold thin integration shims; the core framework logic still lives in `.cadet/agent/core/`.
24
25
  - `package-agent.ps1` builds the distributable `cadet-agent.zip` package.
25
26
  - `publish-npm.ps1` publishes the CLI to npm using a token from `~/.npm_token`.
26
27
 
27
28
  ## Cross-IDE Support
28
29
 
29
- Cadet-Agent provides full workflow parity across four IDEs. The same 9 skills + reviewer are available in each:
30
-
31
- | Feature | GitHub Copilot | Cursor | Continue | Claude Code |
32
- |---|---|---|---|---|
33
- | Auto-load rules | Agent definition | `alwaysApply` rule | Project rule | Project skill |
34
- | Skill dispatch | `/cadet-<skill>` prompts | Natural language | `/cadet-<skill>` commands | `/cadet-<skill>` skills |
35
- | Requirements | ✅ | ✅ | ✅ | ✅ |
36
- | Architecture | ✅ | ✅ | ✅ | ✅ |
37
- | Spike | ✅ | ✅ | ✅ | ✅ |
38
- | Story Breakdown | ✅ | ✅ | ✅ | ✅ |
39
- | TDD | ✅ | ✅ | ✅ | ✅ |
40
- | Debugging | ✅ | ✅ | ✅ | ✅ |
41
- | Code Review | ✅ | ✅ | ✅ | ✅ |
42
- | Resume | ✅ | ✅ | ✅ | ✅ |
43
- | MCP Setup | ✅ | ✅ | ✅ | ✅ |
44
- | Reviewer mode | Agent picker | Rule toggle | `/cadet-agent-reviewer` | `/cadet-agent-reviewer` |
45
- | Git guard | PreToolUse hook | Manual | Manual | Manual |
46
-
47
- All adapters delegate to the canonical files under `.cadet/agent/core/` — no duplicated rules or skills. See `ADAPTERS.md` for the full inventory.
30
+ Cadet-Agent provides full workflow parity across five IDEs. The same 9 skills + reviewer are available in each:
31
+
32
+ | Feature | GitHub Copilot | Cursor | Continue | Claude Code | Deep Code |
33
+ |---|---|---|---|---|---|
34
+ | Auto-load rules | Agent definition | `alwaysApply` rule | Project rule | Project skill | Project skill (`.agents/skills/`) |
35
+ | Skill dispatch | `/cadet-<skill>` prompts | Natural language | `/cadet-<skill>` commands | `/cadet-<skill>` skills | `/skills` menu (`/`) |
36
+ | Requirements | ✅ | ✅ | ✅ | ✅ | ✅ |
37
+ | Architecture | ✅ | ✅ | ✅ | ✅ | ✅ |
38
+ | Spike | ✅ | ✅ | ✅ | ✅ | ✅ |
39
+ | Story Breakdown | ✅ | ✅ | ✅ | ✅ | ✅ |
40
+ | TDD | ✅ | ✅ | ✅ | ✅ | ✅ |
41
+ | Debugging | ✅ | ✅ | ✅ | ✅ | ✅ |
42
+ | Code Review | ✅ | ✅ | ✅ | ✅ | ✅ |
43
+ | Resume | ✅ | ✅ | ✅ | ✅ | ✅ |
44
+ | MCP Setup | ✅ | ✅ | ✅ | ✅ | ✅ |
45
+ | Reviewer mode | Agent picker | Rule toggle | `/cadet-agent-reviewer` | `/cadet-agent-reviewer` | `cadet-agent-reviewer` skill |
46
+ | Git guard | PreToolUse hook | Manual | Manual | Manual | `permissions.ask` (`mutate-git-log`) |
47
+
48
+ All adapters delegate to the canonical files under `.cadet/agent/core/` — no duplicated rules or skills. See `ADAPTERS.md` for the full inventory and `docs/guidance/DeepCode.md` for Deep Code setup.
48
49
 
49
50
  ## Quick Install
50
51
 
@@ -72,6 +73,20 @@ To sync a specific directory:
72
73
  npx cadet-agent@latest sync --target ./my-unity-project
73
74
  ```
74
75
 
76
+ #### AGENTS.md is create-only
77
+
78
+ Cadet ships a repository-root `AGENTS.md` (a thin pointer to `.cadet/agent/core/cadet-agent.md`). If your repo already has one, Cadet **never overwrites it**:
79
+
80
+ - In a terminal, `init`/`sync` ask whether to keep, overwrite, or merge (default: keep).
81
+ - Non-interactive installs (CI, `--yes`, piped output) always **keep** and print a tag-pinned link to Cadet's copy.
82
+ - Control it explicitly with `--agents-md keep|overwrite|merge`.
83
+ - `merge` inserts Cadet's text between `<!-- cadet-agent:begin -->` / `<!-- cadet-agent:end -->` markers and leaves the rest of your file untouched.
84
+
85
+ ```bash
86
+ npx cadet-agent@latest sync --agents-md keep # never touch an existing AGENTS.md
87
+ npx cadet-agent@latest sync --yes # non-interactive; keeps existing files
88
+ ```
89
+
75
90
  ## Manual Install (fallback)
76
91
 
77
92
  If you prefer to install from a packaged release artifact, download `cadet-agent.zip` from [GitHub Releases](https://github.com/naishtech/cadet-agent/releases) and extract it into your Unity project root:
@@ -227,6 +242,26 @@ Create a requirements outline for a single-player time-trial mode with ghost rep
227
242
 
228
243
  The Continue rule in `.continue/rules/cadet-agent.md` should steer the response back through the shared Cadet framework.
229
244
 
245
+ ### Deep Code request
246
+ With [Deep Code](https://deepcode.vegamo.cn/) installed (`npm install -g @vegamo/deepcode-cli`), run `deepcode` in the repository and use `/skills` to confirm the `cadet-*` skills are discovered from `.agents/skills/`. Then pick a phase skill from the `/` menu (there is no `/cadet-<skill>` command — select it by name, or ask for the phase in plain language):
247
+
248
+ ```text
249
+ Run the TDD skill for the ghost-replay story.
250
+ ```
251
+
252
+ Because Deep Code has no PreToolUse hook, enforce the commit/push approval gate in `.deepcode/settings.json`:
253
+
254
+ ```json
255
+ {
256
+ "permissions": {
257
+ "ask": ["mutate-git-log", "network"],
258
+ "defaultMode": "askAll"
259
+ }
260
+ }
261
+ ```
262
+
263
+ See `docs/guidance/DeepCode.md` for the full setup, MCP wiring, and configuration reference.
264
+
230
265
  ### Repository policy example
231
266
  If a specific game repository needs local conventions, add a policy file under `.cadet/agent/policies` using `.cadet/agent/core/Templates/PolicyTemplate.md`. For example, a repository policy could define:
232
267
  - where project plans should live
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadet-agent",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -51,6 +51,8 @@ function showHelp() {
51
51
  --gate Gate name (harness verify)
52
52
  --command Command override (harness verify)
53
53
  --files Comma-separated relevant files to bind evidence to (harness verify)
54
+ --agents-md keep|overwrite|merge for an existing AGENTS.md (init/sync)
55
+ --yes, -y Never prompt; keep existing files (non-interactive installs)
54
56
  --help, -h Show this help
55
57
  --version, -v Show version number
56
58
  `);
@@ -77,6 +79,8 @@ function parseArgs(argv) {
77
79
  case '--evidence-status': opts.evidenceStatus = argv[++i]; break;
78
80
  case '--files': opts.files = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean); break;
79
81
  case '--older-than-ms': opts.olderThanMs = Number(argv[++i]); break;
82
+ case '--agents-md': opts.agentsMd = argv[++i]; break;
83
+ case '--yes': case '-y': opts.yes = true; break;
80
84
  default: opts.rest.push(a);
81
85
  }
82
86
  }
@@ -381,13 +385,25 @@ export async function run(argv) {
381
385
  const command = argv[2];
382
386
  const opts = parseArgs(argv);
383
387
 
388
+ // Validate the create-only policy flag early so a typo fails loudly.
389
+ const AGENTS_MD_MODES = ['keep', 'overwrite', 'merge'];
390
+ if (opts.agentsMd !== undefined && !AGENTS_MD_MODES.includes(opts.agentsMd)) {
391
+ console.error(`Invalid --agents-md value "${opts.agentsMd}" (expected: ${AGENTS_MD_MODES.join('|')})`);
392
+ process.exit(1);
393
+ }
394
+ const installOpts = {
395
+ sourceUrl: opts.sourceUrl,
396
+ yes: opts.yes === true,
397
+ createOnlyPolicy: opts.agentsMd ? { 'AGENTS.md': opts.agentsMd } : undefined,
398
+ };
399
+
384
400
  try {
385
401
  switch (command) {
386
402
  case 'init':
387
- await install(opts.targetDir, { sourceUrl: opts.sourceUrl });
403
+ await install(opts.targetDir, installOpts);
388
404
  break;
389
405
  case 'sync':
390
- await sync(opts.targetDir, { sourceUrl: opts.sourceUrl });
406
+ await sync(opts.targetDir, installOpts);
391
407
  break;
392
408
  case 'state':
393
409
  await cmdState(opts);
package/src/install.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { readFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { readFileSync, unlinkSync, existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { join, relative } from 'node:path';
3
+ import { createInterface } from 'node:readline';
3
4
  import { runUpgrades } from './upgrades.mjs';
4
5
  import {
5
6
  extractArchive, readArchiveEntry, findEocd,
@@ -61,10 +62,99 @@ const ARCHIVE_LIMITS = DEFAULT_ARCHIVE_LIMITS;
61
62
 
62
63
  export { ArchiveError, findEocd };
63
64
 
64
- /** Extract every file entry into targetDir, returning the written paths. */
65
- export async function extractZip(buf, targetDir) {
66
- const { extracted } = extractArchive(buf, targetDir, { limits: ARCHIVE_LIMITS });
67
- return extracted;
65
+ // ── Create-only paths (never overwrite an existing consumer file) ─────────────
66
+ //
67
+ // Some packaged files are advisory conveniences that a consumer repository may
68
+ // already own (currently root `AGENTS.md`). These are listed in the manifest as
69
+ // `createOnlyPaths`: written when absent, and never overwritten when present.
70
+ // The canonical copy always remains available at the repository URL below.
71
+
72
+ const REPO_URL = 'https://github.com/naishtech/cadet-agent';
73
+
74
+ /** Tag-pinned URL for a create-only file, so the link cannot drift. */
75
+ export function createOnlyUrl(relPath, version) {
76
+ const tag = version && version !== 'unknown' ? `v${normalizeVersion(version)}` : 'main';
77
+ return `${REPO_URL}/blob/${tag}/${relPath}`;
78
+ }
79
+
80
+ function normalizeRel(p) {
81
+ return p.replace(/^\.?\//, '').replace(/\\/g, '/');
82
+ }
83
+
84
+ /** Does an absolute-or-relative entry match a create-only path (exact file)? */
85
+ function matchesCreateOnly(entryName, createOnlyPaths) {
86
+ const n = normalizeRel(entryName);
87
+ return (createOnlyPaths || []).some((c) => normalizeRel(c) === n);
88
+ }
89
+
90
+ /**
91
+ * Decide how to handle a create-only path that already exists on disk.
92
+ * Returns 'keep' | 'overwrite' | 'merge'.
93
+ *
94
+ * Non-interactive (no TTY, --yes, or an explicit policy) always resolves to the
95
+ * safe 'keep' — a scripted/CI install must never clobber a consumer file.
96
+ */
97
+ async function resolveExistingCreateOnly({ relPath, mode, interactive }) {
98
+ if (mode) return mode; // explicit --agents-md=keep|overwrite|merge
99
+ if (!interactive) return 'keep';
100
+
101
+ const answer = await promptLine(
102
+ `\n⚠️ ${relPath} already exists in this repository.\n` +
103
+ ` [k] Keep mine (leave it untouched) [o] Overwrite with Cadet's [m] Merge Cadet's block\n` +
104
+ ` Keep yours? (K/o/m): `
105
+ );
106
+ const a = (answer || '').trim().toLowerCase();
107
+ if (a === 'o' || a === 'overwrite') return 'overwrite';
108
+ if (a === 'm' || a === 'merge') return 'merge';
109
+ return 'keep';
110
+ }
111
+
112
+ /** Read one line from stdin. Resolves to '' if stdin ends without an answer. */
113
+ function promptLine(question) {
114
+ return new Promise((resolve) => {
115
+ let settled = false;
116
+ const done = (value) => { if (!settled) { settled = true; resolve(value); } };
117
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
118
+ rl.question(question, (answer) => {
119
+ done(answer);
120
+ rl.close();
121
+ });
122
+ // Only fall back to '' if the stream closes with no answer (EOF/piped input).
123
+ rl.on('close', () => done(''));
124
+ });
125
+ }
126
+
127
+ /** True when we may prompt: an interactive TTY and not disabled by --yes. */
128
+ export function canPrompt(opts = {}) {
129
+ if (opts.yes === true) return false;
130
+ if (opts.interactive === false) return false;
131
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
132
+ }
133
+
134
+ // Marker block used by the 'merge' resolution. Content between the markers is
135
+ // Cadet-owned and replaced on each sync; everything outside is the consumer's.
136
+ export const AGENTS_MARKER_BEGIN = '<!-- cadet-agent:begin -->';
137
+ export const AGENTS_MARKER_END = '<!-- cadet-agent:end -->';
138
+
139
+ /** Wrap a body in the Cadet marker block. */
140
+ export function wrapWithMarkers(body) {
141
+ return `${AGENTS_MARKER_BEGIN}\n${body.trim()}\n${AGENTS_MARKER_END}`;
142
+ }
143
+
144
+ /**
145
+ * Merge Cadet's marked block into an existing file's text:
146
+ * replace the block if present, otherwise append it. Content outside the
147
+ * markers is preserved verbatim.
148
+ */
149
+ export function mergeMarkerBlock(existing, cadetBody) {
150
+ const block = wrapWithMarkers(cadetBody);
151
+ const begin = existing.indexOf(AGENTS_MARKER_BEGIN);
152
+ const end = existing.indexOf(AGENTS_MARKER_END);
153
+ if (begin !== -1 && end !== -1 && end > begin) {
154
+ return existing.slice(0, begin) + block + existing.slice(end + AGENTS_MARKER_END.length);
155
+ }
156
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
157
+ return `${existing}${sep}${block}\n`;
68
158
  }
69
159
 
70
160
  // ── GitHub release download ─────────────────────────────────────────────────
@@ -174,9 +264,16 @@ export async function install(targetDir, opts = {}) {
174
264
  const zipBuf = await downloadZip(asset.browser_download_url);
175
265
  console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
176
266
 
177
- // 4. Extract
267
+ // 4. Extract. Create-only paths (e.g. AGENTS.md) must never overwrite an
268
+ // existing consumer file; if one is skipped, point the user at the source.
178
269
  console.log('📂 Extracting...');
179
- const extracted = await extractZip(zipBuf, targetDir);
270
+ const createOnly = readCreateOnlyPathsFromZip(zipBuf);
271
+ const extracted = await extractZip(zipBuf, targetDir, {
272
+ ...opts,
273
+ createOnlyPaths: createOnly,
274
+ interactive: canPrompt(opts),
275
+ });
276
+ reportCreateOnlySkips(createOnly, targetDir, releaseVersion, opts);
180
277
 
181
278
  // 5. Report
182
279
  console.log(`\n✅ Cadet-Agent v${releaseVersion} installed! Extracted ${extracted.length} files.\n`);
@@ -205,6 +302,12 @@ export async function install(targetDir, opts = {}) {
205
302
  console.log(' /cadet-breakdown, /cadet-tdd, /cadet-debug, /cadet-review, /cadet-resume');
206
303
  console.log(' Reviewer: /cadet-agent-reviewer');
207
304
  console.log(' Git guard: manual — see .claude\\skills\\cadet-agent\\SKILL.md for instructions');
305
+ console.log(' Deep Code:');
306
+ console.log(' Already active — .agents\\skills\\cadet-agent\\SKILL.md is discovered as a project skill');
307
+ console.log(' List skills with /skills, then pick a cadet-* skill from the / menu');
308
+ console.log(' Reviewer: the cadet-agent-reviewer skill');
309
+ console.log(' Git guard: no hook — approve via .deepcode\\settings.json permissions.ask (mutate-git-log)');
310
+ console.log(' Docs: https://deepcode.vegamo.cn/');
208
311
  console.log('');
209
312
  }
210
313
 
@@ -282,12 +385,91 @@ function deleteObsoleteManagedFiles(targetDir, managedPaths, zipFilenames) {
282
385
  return deleted;
283
386
  }
284
387
 
285
- export async function extractZipWithManifest(buf, targetDir, { preserved, managed, limits = ARCHIVE_LIMITS }) {
388
+ /**
389
+ * Resolve create-only paths against the target directory before extraction.
390
+ * Returns:
391
+ * - skip: set of normalized paths whose existing copy must be left alone
392
+ * - merge: map of normalized path -> existing text (to merge Cadet's block)
393
+ * - created: normalized paths that do not yet exist (write normally)
394
+ * - kept: human-readable list of paths left untouched
395
+ */
396
+ async function planCreateOnly(createOnlyPaths, targetDir, opts = {}) {
397
+ const skip = new Set();
398
+ const merge = new Map();
399
+ const created = [];
400
+ const kept = [];
401
+
402
+ for (const rel of createOnlyPaths || []) {
403
+ const n = normalizeRel(rel);
404
+ const full = join(targetDir, rel.replace(/^\.?\//, ''));
405
+ if (!existsSync(full)) {
406
+ created.push(n);
407
+ continue;
408
+ }
409
+ const resolution = await resolveExistingCreateOnly({
410
+ relPath: n,
411
+ mode: opts.createOnlyPolicy && opts.createOnlyPolicy[n],
412
+ interactive: canPrompt(opts),
413
+ });
414
+ if (resolution === 'overwrite') continue; // fall through and write
415
+ if (resolution === 'merge') {
416
+ // Remember the consumer's current text; the Cadet body is read from the
417
+ // archive after extraction (never written over the consumer's file).
418
+ merge.set(n, readFileSync(full, 'utf-8'));
419
+ skip.add(n);
420
+ } else {
421
+ skip.add(n);
422
+ }
423
+ kept.push(n);
424
+ }
425
+ return { skip, merge, created, kept };
426
+ }
427
+
428
+ /** Extract every file entry into targetDir, returning the written paths. */
429
+ export async function extractZip(buf, targetDir, opts = {}) {
430
+ const createOnlyPaths = opts.createOnlyPaths || [];
431
+ const plan = await planCreateOnly(createOnlyPaths, targetDir, opts);
432
+
433
+ const { extracted } = extractArchive(buf, targetDir, {
434
+ limits: ARCHIVE_LIMITS,
435
+ filter: (entry) => {
436
+ if (plan.skip.has(normalizeRel(entry.filename))) return { skip: true };
437
+ return true;
438
+ },
439
+ });
440
+
441
+ applyMerges(plan, targetDir, buf);
442
+ return extracted;
443
+ }
444
+
445
+ /**
446
+ * Merge Cadet's marker block into the existing file for every planned merge.
447
+ * Cadet's body is read from the archive (its entry was skipped, so the
448
+ * consumer's file on disk was never touched). Content outside the markers is
449
+ * preserved.
450
+ */
451
+ function applyMerges(plan, targetDir, buf) {
452
+ for (const [rel, existingText] of plan.merge) {
453
+ const full = join(targetDir, rel);
454
+ const data = readArchiveEntry(buf, rel, ARCHIVE_LIMITS);
455
+ if (!data) continue; // archive lacks the file — leave the consumer's text alone
456
+ const cadetBody = data.toString('utf-8');
457
+ writeFileSync(full, mergeMarkerBlock(existingText, cadetBody), 'utf-8');
458
+ plan.merged = plan.merged || [];
459
+ plan.merged.push(full);
460
+ }
461
+ }
462
+
463
+ export async function extractZipWithManifest(buf, targetDir, { preserved, managed, createOnly = [], limits = ARCHIVE_LIMITS, ...opts }) {
286
464
  const updated = [];
287
465
  const preserved_list = [];
288
466
  const added = [];
467
+ const kept = [];
289
468
  const zipFilenames = [];
290
469
 
470
+ const plan = await planCreateOnly(createOnly, targetDir, opts);
471
+ kept.push(...plan.kept);
472
+
291
473
  const { extracted } = extractArchive(buf, targetDir, {
292
474
  limits,
293
475
  filter: (entry) => {
@@ -296,10 +478,15 @@ export async function extractZipWithManifest(buf, targetDir, { preserved, manage
296
478
  preserved_list.push(entry.filename);
297
479
  return { skip: true };
298
480
  }
481
+ if (plan.skip.has(normalizeRel(entry.filename))) {
482
+ return { skip: true };
483
+ }
299
484
  return true;
300
485
  },
301
486
  });
302
487
 
488
+ applyMerges(plan, targetDir, buf);
489
+
303
490
  // `extractArchive` skips directory entries; classify the extracted files.
304
491
  for (const outPath of extracted) {
305
492
  const rel = outPath.replace(/\\/g, '/');
@@ -313,7 +500,7 @@ export async function extractZipWithManifest(buf, targetDir, { preserved, manage
313
500
  // Delete obsolete managed files no longer in the zip (renamed/removed managed paths)
314
501
  const deleted = deleteObsoleteManagedFiles(targetDir, managed, zipFilenames);
315
502
 
316
- return { updated, preserved: preserved_list, added, deleted, zipFilenames };
503
+ return { updated, preserved: preserved_list, added, deleted, kept, zipFilenames };
317
504
  }
318
505
 
319
506
  // ── Removed-managed-path cleanup ─────────────────────────────────────────────
@@ -330,6 +517,37 @@ export function findManagedPathsInZip(buf) {
330
517
  return [];
331
518
  }
332
519
 
520
+ /** Read `createOnlyPaths` from the manifest inside the zip. */
521
+ export function readCreateOnlyPathsFromZip(buf) {
522
+ try {
523
+ const data = readArchiveEntry(buf, '.cadet/agent/core/FrameworkManifest.json', ARCHIVE_LIMITS);
524
+ if (!data) return [];
525
+ const manifest = JSON.parse(data.toString('utf-8'));
526
+ return manifest.createOnlyPaths || [];
527
+ } catch {
528
+ return [];
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Print a note for each create-only path that was left untouched, including a
534
+ * tag-pinned URL so the user can copy the canonical version if they want it.
535
+ */
536
+ export function reportCreateOnlySkips(createOnlyPaths, targetDir, version, opts = {}) {
537
+ const skipped = (createOnlyPaths || []).filter((rel) => {
538
+ const full = join(targetDir, rel.replace(/^\.?\//, ''));
539
+ return existsSync(full);
540
+ });
541
+ for (const rel of skipped) {
542
+ const n = normalizeRel(rel);
543
+ const resolution = opts.createOnlyPolicy && opts.createOnlyPolicy[n];
544
+ if (resolution === 'overwrite') continue; // the user chose to replace it
545
+ console.log(` Kept: ${n} (existing file left untouched)`);
546
+ console.log(` Cadet's version: ${createOnlyUrl(n, version)}`);
547
+ }
548
+ return skipped;
549
+ }
550
+
333
551
  export function deleteRemovedManagedPaths(targetDir, oldManaged, newManaged) {
334
552
  const newSet = new Set(newManaged.map(p => p.replace(/^\.\//, '').replace(/\\/g, '/')));
335
553
  const deleted = [];
@@ -388,11 +606,16 @@ export async function sync(targetDir, opts = {}) {
388
606
  const zipBuf = await downloadZip(asset.browser_download_url);
389
607
  console.log(` Downloaded ${(zipBuf.length / 1024).toFixed(0)} KB\n`);
390
608
 
391
- // 4. Extract with manifest awareness
609
+ // 4. Extract with manifest awareness. Create-only paths (e.g. AGENTS.md) are
610
+ // never overwritten when the consumer already has them.
392
611
  console.log('📂 Extracting (preserving local policies and plans)...');
612
+ const createOnly = readCreateOnlyPathsFromZip(zipBuf);
393
613
  const result = await extractZipWithManifest(zipBuf, targetDir, {
394
614
  preserved: existingManifest.preservedPaths || [],
395
615
  managed: existingManifest.managedPaths || [],
616
+ createOnly,
617
+ ...opts,
618
+ interactive: canPrompt(opts),
396
619
  });
397
620
 
398
621
  // 4b. Find new managed paths from the zip and delete any old paths that were removed
@@ -425,6 +648,7 @@ export async function sync(targetDir, opts = {}) {
425
648
  if (result.deleted.length > 0) {
426
649
  console.log(` Removed: ${result.deleted.length} files (no longer managed)`);
427
650
  }
651
+ reportCreateOnlySkips(createOnly, targetDir, newVersion, opts);
428
652
  console.log('');
429
653
 
430
654
  // Print per-IDE next steps