cadet-agent 0.24.0 → 0.27.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 +69 -22
- package/package.json +4 -2
- package/src/cli.mjs +18 -2
- package/src/install.mjs +234 -10
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
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
|
|
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.
|
|
9
9
|
- `cadet-agent.md` is the thin global directive: identity, non-negotiable rules, workflow routing, hard-gate protocol, and skill dispatch.
|
|
10
10
|
- `Harness.md` is the canonical harness contract: budgets, evidence-backed gates, retries, context tiers, tool routing, privacy, and escalation.
|
|
11
11
|
- `harness.schema.json` and `state.schema.json` are the machine-readable schemas for harness records and session state.
|
|
12
|
-
- `skills/` contains scoped workflow-phase skills (Requirements, Architecture, Spike, StoryBreakdown, TDD, Debugging, CodeReview, Resume, MCPSetup, AgentReviewer).
|
|
12
|
+
- `skills/` contains scoped workflow-phase skills (PlanningReview, Requirements, Architecture, Spike, StoryBreakdown, TDD, Debugging, CodeReview, Resume, MCPSetup, AgentReviewer).
|
|
13
13
|
- `templates/` contains runtime templates for planning artifacts.
|
|
14
14
|
- `.cadet/harness.json` holds repository-local budget/policy overrides (preserved by sync).
|
|
15
15
|
- `.cadet/runs/` holds sanitized run ledgers (preserved by sync; no secrets or raw prompts by default).
|
|
@@ -20,31 +20,34 @@ 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
|
+
- `bump-version.ps1` bumps the version, updates version-bearing files, commits, tags, and pushes. It runs `npm run lint` first and refuses to release if the link check fails.
|
|
27
|
+
- `npm run lint` checks all markdown links (the same offline check CI runs); `npm run verify` runs tests + lint.
|
|
26
28
|
|
|
27
29
|
## Cross-IDE Support
|
|
28
30
|
|
|
29
|
-
Cadet-Agent provides full workflow parity across
|
|
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
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
Cadet-Agent provides full workflow parity across five IDEs. The same 10 skills + reviewer are available in each:
|
|
32
|
+
|
|
33
|
+
| Feature | GitHub Copilot | Cursor | Continue | Claude Code | Deep Code |
|
|
34
|
+
|---|---|---|---|---|---|
|
|
35
|
+
| Auto-load rules | Agent definition | `alwaysApply` rule | Project rule | Project skill | Project skill (`.agents/skills/`) |
|
|
36
|
+
| Skill dispatch | `/cadet-<skill>` prompts | Natural language | `/cadet-<skill>` commands | `/cadet-<skill>` skills | `/skills` menu (`/`) |
|
|
37
|
+
| Planning Review | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
38
|
+
| Requirements | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
39
|
+
| Architecture | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
40
|
+
| Spike | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
41
|
+
| Story Breakdown | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
|
+
| TDD | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
43
|
+
| Debugging | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
|
+
| Code Review | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
+
| Resume | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
46
|
+
| MCP Setup | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
47
|
+
| Reviewer mode | Agent picker | Rule toggle | `/cadet-agent-reviewer` | `/cadet-agent-reviewer` | `cadet-agent-reviewer` skill |
|
|
48
|
+
| Git guard | PreToolUse hook | Manual | Manual | Manual | `permissions.ask` (`mutate-git-log`) |
|
|
49
|
+
|
|
50
|
+
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
51
|
|
|
49
52
|
## Quick Install
|
|
50
53
|
|
|
@@ -72,6 +75,20 @@ To sync a specific directory:
|
|
|
72
75
|
npx cadet-agent@latest sync --target ./my-unity-project
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
#### AGENTS.md is create-only
|
|
79
|
+
|
|
80
|
+
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**:
|
|
81
|
+
|
|
82
|
+
- In a terminal, `init`/`sync` ask whether to keep, overwrite, or merge (default: keep).
|
|
83
|
+
- Non-interactive installs (CI, `--yes`, piped output) always **keep** and print a tag-pinned link to Cadet's copy.
|
|
84
|
+
- Control it explicitly with `--agents-md keep|overwrite|merge`.
|
|
85
|
+
- `merge` inserts Cadet's text between `<!-- cadet-agent:begin -->` / `<!-- cadet-agent:end -->` markers and leaves the rest of your file untouched.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx cadet-agent@latest sync --agents-md keep # never touch an existing AGENTS.md
|
|
89
|
+
npx cadet-agent@latest sync --yes # non-interactive; keeps existing files
|
|
90
|
+
```
|
|
91
|
+
|
|
75
92
|
## Manual Install (fallback)
|
|
76
93
|
|
|
77
94
|
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:
|
|
@@ -96,6 +113,7 @@ flowchart TD
|
|
|
96
113
|
REPORT["Report current phase,<br/>epics, stories & gates"]
|
|
97
114
|
CR["🔍 Context Resolution<br/>classify change size,<br/>calibrate learner,<br/>detect policy"]
|
|
98
115
|
REQ["📋 Requirements<br/>Given/When/Then criteria<br/>assumption audit"]
|
|
116
|
+
PLANREV["🧩 Planning Review<br/>interview one question<br/>at a time · decision tree"]
|
|
99
117
|
ARCH["🏗️ Architecture<br/>technical design,<br/>ADR decisions"]
|
|
100
118
|
SPIKE["🧪 Spikes<br/>resolve unverified<br/>assumptions"]
|
|
101
119
|
BREAKDOWN["📐 Story Breakdown<br/>epics → testable stories"]
|
|
@@ -110,8 +128,11 @@ flowchart TD
|
|
|
110
128
|
RESUME -->|"yes"| REPORT --> CR
|
|
111
129
|
|
|
112
130
|
CR -->|"large change"| REQ
|
|
131
|
+
CR -->|"plan fuzzy or contested"| PLANREV
|
|
113
132
|
CR -->|"small / no_test_required"| IMPL
|
|
114
133
|
|
|
134
|
+
PLANREV -->|"shared understanding reached"| REQ
|
|
135
|
+
|
|
115
136
|
REQ --> ARCH
|
|
116
137
|
ARCH -->|"unverified assumptions"| SPIKE
|
|
117
138
|
ARCH -->|"all assumptions resolved"| BREAKDOWN
|
|
@@ -128,6 +149,7 @@ flowchart TD
|
|
|
128
149
|
style START fill:#4a9,stroke:#333,color:#fff
|
|
129
150
|
style CLOSED fill:#4a9,stroke:#333,color:#fff
|
|
130
151
|
style RESUME fill:#e8a840,stroke:#333,color:#000
|
|
152
|
+
style PLANREV fill:#8a7ae8,stroke:#333,color:#fff
|
|
131
153
|
style REVIEW fill:#e87440,stroke:#333,color:#fff
|
|
132
154
|
style VALIDATE fill:#e87440,stroke:#333,color:#fff
|
|
133
155
|
```
|
|
@@ -191,6 +213,11 @@ Cadet Agent will classify the change, check `.cadet/state.json` for blocking gat
|
|
|
191
213
|
|
|
192
214
|
**Skill mode:** For a specific workflow phase, use the matching slash command so the skill becomes the primary instruction context:
|
|
193
215
|
|
|
216
|
+
```text
|
|
217
|
+
/cadet-planning-review
|
|
218
|
+
clarify the plan for a co-op loot system before I write the design
|
|
219
|
+
```
|
|
220
|
+
|
|
194
221
|
```text
|
|
195
222
|
/cadet-requirements
|
|
196
223
|
create a requirements doc for a kart handling prototype
|
|
@@ -227,6 +254,26 @@ Create a requirements outline for a single-player time-trial mode with ghost rep
|
|
|
227
254
|
|
|
228
255
|
The Continue rule in `.continue/rules/cadet-agent.md` should steer the response back through the shared Cadet framework.
|
|
229
256
|
|
|
257
|
+
### Deep Code request
|
|
258
|
+
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):
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
Run the TDD skill for the ghost-replay story.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Because Deep Code has no PreToolUse hook, enforce the commit/push approval gate in `.deepcode/settings.json`:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"permissions": {
|
|
269
|
+
"ask": ["mutate-git-log", "network"],
|
|
270
|
+
"defaultMode": "askAll"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
See `docs/guidance/DeepCode.md` for the full setup, MCP wiring, and configuration reference.
|
|
276
|
+
|
|
230
277
|
### Repository policy example
|
|
231
278
|
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
279
|
- where project plans should live
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadet-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cadet-agent": "bin/cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test test/*.test.mjs"
|
|
10
|
+
"test": "node --test test/*.test.mjs",
|
|
11
|
+
"lint": "lychee --offline --include-fragments \"**/*.md\"",
|
|
12
|
+
"verify": "npm test && npm run lint"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"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,
|
|
403
|
+
await install(opts.targetDir, installOpts);
|
|
388
404
|
break;
|
|
389
405
|
case 'sync':
|
|
390
|
-
await sync(opts.targetDir,
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|