@orderful/droid 0.45.1 → 0.47.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +4 -1
  2. package/.github/workflows/claude-issue-agent.yml +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/dist/tools/pii/.claude-plugin/plugin.json +25 -0
  5. package/dist/tools/pii/TOOL.yaml +22 -0
  6. package/dist/tools/pii/agents/pii-scanner.md +85 -0
  7. package/dist/tools/pii/commands/pii.md +33 -0
  8. package/dist/tools/pii/skills/pii/SKILL.md +97 -0
  9. package/dist/tools/pii/skills/pii/references/supported-entities.md +90 -0
  10. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts +18 -0
  11. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts.map +1 -0
  12. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  13. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts +17 -0
  14. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts.map +1 -0
  15. package/dist/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  16. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts +21 -0
  17. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts.map +1 -0
  18. package/dist/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  19. package/dist/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
  20. package/dist/tools/project/.claude-plugin/plugin.json +1 -1
  21. package/dist/tools/project/TOOL.yaml +2 -2
  22. package/dist/tools/project/skills/project/SKILL.md +4 -3
  23. package/dist/tools/project/skills/project/references/changelog.md +10 -21
  24. package/dist/tools/project/skills/project/references/creating.md +12 -2
  25. package/dist/tools/project/skills/project/references/loading.md +2 -1
  26. package/dist/tools/project/skills/project/references/templates.md +115 -71
  27. package/dist/tools/project/skills/project/references/updating.md +78 -4
  28. package/package.json +1 -1
  29. package/src/tools/pii/.claude-plugin/plugin.json +25 -0
  30. package/src/tools/pii/TOOL.yaml +22 -0
  31. package/src/tools/pii/agents/pii-scanner.md +85 -0
  32. package/src/tools/pii/commands/pii.md +33 -0
  33. package/src/tools/pii/skills/pii/SKILL.md +97 -0
  34. package/src/tools/pii/skills/pii/references/supported-entities.md +90 -0
  35. package/src/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  36. package/src/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  37. package/src/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  38. package/src/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
  39. package/src/tools/project/.claude-plugin/plugin.json +1 -1
  40. package/src/tools/project/TOOL.yaml +2 -2
  41. package/src/tools/project/skills/project/SKILL.md +4 -3
  42. package/src/tools/project/skills/project/references/changelog.md +10 -21
  43. package/src/tools/project/skills/project/references/creating.md +12 -2
  44. package/src/tools/project/skills/project/references/loading.md +2 -1
  45. package/src/tools/project/skills/project/references/templates.md +115 -71
  46. package/src/tools/project/skills/project/references/updating.md +78 -4
@@ -0,0 +1,444 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { spawnSync } from 'child_process';
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+
7
+ /**
8
+ * Integration tests for presidio scripts.
9
+ *
10
+ * These tests validate argument parsing, error paths, and JSON output shape.
11
+ * They do NOT invoke Presidio/Python directly — all Python-dependent paths
12
+ * are gated by existsSync checks in the scripts, so tests run without Python.
13
+ */
14
+
15
+ const SCRIPTS_DIR = __dirname;
16
+
17
+ interface ScriptResult {
18
+ success: boolean;
19
+ already_existed?: boolean;
20
+ initialized?: boolean;
21
+ python_path?: string;
22
+ venv_path?: string;
23
+ entities?: Array<{ type: string; start: number; end: number; score: number; line: number; text?: string }>;
24
+ dry_run?: boolean;
25
+ original_path?: string;
26
+ output_path?: string;
27
+ entities_found?: number;
28
+ entities_redacted?: number;
29
+ redacted_text?: string;
30
+ error?: string;
31
+ init_required?: boolean;
32
+ }
33
+
34
+ function runScript(scriptName: string, args: string[]): ScriptResult {
35
+ const scriptPath = join(SCRIPTS_DIR, `${scriptName}.ts`);
36
+
37
+ const result = spawnSync('bun', ['run', scriptPath, ...args], {
38
+ encoding: 'utf-8',
39
+ cwd: process.cwd(),
40
+ });
41
+
42
+ if (result.error) {
43
+ return { success: false, error: result.error.message };
44
+ }
45
+
46
+ try {
47
+ return JSON.parse(result.stdout.trim());
48
+ } catch {
49
+ return {
50
+ success: false,
51
+ error: `Failed to parse output: ${result.stdout}\nStderr: ${result.stderr}`,
52
+ };
53
+ }
54
+ }
55
+
56
+ function createFakeVenvWithPython(tempHome: string, scriptContent: string): string {
57
+ const venvPath = join(tempHome, '.droid', 'runtimes', 'presidio');
58
+ const binDir = join(venvPath, 'bin');
59
+ mkdirSync(binDir, { recursive: true });
60
+ writeFileSync(join(binDir, 'python3'), scriptContent, { mode: 0o755 });
61
+ return venvPath;
62
+ }
63
+
64
+ // ─── presidio-init ───────────────────────────────────────────────────────────
65
+
66
+ describe('presidio-init', () => {
67
+ let tempHome: string;
68
+ let originalHome: string;
69
+
70
+ beforeEach(() => {
71
+ originalHome = process.env.HOME || '';
72
+ tempHome = mkdtempSync(join(tmpdir(), 'pii-test-home-'));
73
+ process.env.HOME = tempHome;
74
+ });
75
+
76
+ afterEach(() => {
77
+ process.env.HOME = originalHome;
78
+ try {
79
+ rmSync(tempHome, { recursive: true, force: true });
80
+ } catch {
81
+ // Ignore cleanup errors
82
+ }
83
+ });
84
+
85
+ it('returns already_existed:true when marker file and venv binary both exist', () => {
86
+ // Pre-create the venv structure
87
+ const venvPath = join(tempHome, '.droid', 'runtimes', 'presidio');
88
+ const binDir = join(venvPath, 'bin');
89
+ mkdirSync(binDir, { recursive: true });
90
+ writeFileSync(join(venvPath, '.droid-initialized'), new Date().toISOString());
91
+ writeFileSync(join(binDir, 'python3'), '#!/bin/bash\necho "Python 3.9.0"', { mode: 0o755 });
92
+
93
+ const result = spawnSync(
94
+ 'bun',
95
+ ['run', join(SCRIPTS_DIR, 'presidio-init.ts')],
96
+ {
97
+ encoding: 'utf-8',
98
+ env: { ...process.env, HOME: tempHome },
99
+ }
100
+ );
101
+
102
+ let parsed: ScriptResult;
103
+ try {
104
+ parsed = JSON.parse(result.stdout.trim());
105
+ } catch {
106
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
107
+ }
108
+
109
+ expect(parsed.success).toBe(true);
110
+ expect(parsed.already_existed).toBe(true);
111
+ });
112
+
113
+ it('returns success:false with clear error when python3 is not available', () => {
114
+ // Override PATH to have no python3
115
+ const result = spawnSync(
116
+ 'bun',
117
+ ['run', join(SCRIPTS_DIR, 'presidio-init.ts')],
118
+ {
119
+ encoding: 'utf-8',
120
+ env: { ...process.env, HOME: tempHome, PATH: '/nonexistent' },
121
+ }
122
+ );
123
+
124
+ let parsed: ScriptResult;
125
+ try {
126
+ parsed = JSON.parse(result.stdout.trim());
127
+ } catch {
128
+ // If Python is truly not found, the error may be in stderr
129
+ parsed = { success: false, error: 'python3 not found' };
130
+ }
131
+
132
+ expect(parsed.success).toBe(false);
133
+ expect(parsed.error).toBeDefined();
134
+ });
135
+ });
136
+
137
+ // ─── presidio-analyze ────────────────────────────────────────────────────────
138
+
139
+ describe('presidio-analyze', () => {
140
+ let tempDir: string;
141
+
142
+ beforeEach(() => {
143
+ tempDir = mkdtempSync(join(tmpdir(), 'pii-analyze-test-'));
144
+ });
145
+
146
+ afterEach(() => {
147
+ try {
148
+ rmSync(tempDir, { recursive: true, force: true });
149
+ } catch {
150
+ // Ignore
151
+ }
152
+ });
153
+
154
+ it('returns success:false with init_required when venv does not exist', () => {
155
+ const result = spawnSync(
156
+ 'bun',
157
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--file', join(tempDir, 'test.md')],
158
+ {
159
+ encoding: 'utf-8',
160
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
161
+ }
162
+ );
163
+
164
+ let parsed: ScriptResult;
165
+ try {
166
+ parsed = JSON.parse(result.stdout.trim());
167
+ } catch {
168
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
169
+ }
170
+
171
+ expect(parsed.success).toBe(false);
172
+ expect(parsed.init_required).toBe(true);
173
+ expect(parsed.error).toContain('presidio-init');
174
+ });
175
+
176
+ it('returns success:false when neither --file nor --text is provided', () => {
177
+ // Use a fake home so venv won't be found — still validates arg parsing
178
+ const result = spawnSync(
179
+ 'bun',
180
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts')],
181
+ {
182
+ encoding: 'utf-8',
183
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
184
+ }
185
+ );
186
+
187
+ let parsed: ScriptResult;
188
+ try {
189
+ parsed = JSON.parse(result.stdout.trim());
190
+ } catch {
191
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
192
+ }
193
+
194
+ // Either init_required or missing arg error — both are valid failures
195
+ expect(parsed.success).toBe(false);
196
+ expect(parsed.error).toBeDefined();
197
+ });
198
+
199
+ it('returns success:false when --file does not exist (and venv exists)', () => {
200
+ // Pre-create a fake venv so the script gets past the venv check
201
+ const fakeHome = join(tempDir, 'fake-home');
202
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho ""');
203
+
204
+ const nonExistentFile = join(tempDir, 'does-not-exist.md');
205
+
206
+ const result = spawnSync(
207
+ 'bun',
208
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--file', nonExistentFile],
209
+ {
210
+ encoding: 'utf-8',
211
+ env: { ...process.env, HOME: fakeHome },
212
+ }
213
+ );
214
+
215
+ let parsed: ScriptResult;
216
+ try {
217
+ parsed = JSON.parse(result.stdout.trim());
218
+ } catch {
219
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
220
+ }
221
+
222
+ expect(parsed.success).toBe(false);
223
+ expect(parsed.error).toContain('not found');
224
+ });
225
+
226
+ it('rejects invalid entity names before invoking Python', () => {
227
+ const fakeHome = join(tempDir, 'fake-home');
228
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'[]\'');
229
+
230
+ const result = spawnSync(
231
+ 'bun',
232
+ [
233
+ 'run',
234
+ join(SCRIPTS_DIR, 'presidio-analyze.ts'),
235
+ '--text',
236
+ 'Call me at 555-1234',
237
+ '--entities',
238
+ 'EMAIL_ADDRESS,__import__("os").system("whoami")',
239
+ ],
240
+ {
241
+ encoding: 'utf-8',
242
+ env: { ...process.env, HOME: fakeHome },
243
+ }
244
+ );
245
+
246
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
247
+ expect(parsed.success).toBe(false);
248
+ expect(parsed.error).toContain('Invalid entity type');
249
+ });
250
+
251
+ it('does not include raw text in analyzer output entities', () => {
252
+ const fakeHome = join(tempDir, 'fake-home-no-text');
253
+ createFakeVenvWithPython(
254
+ fakeHome,
255
+ `#!/bin/bash
256
+ if grep -q "'text': text\\[r.start:r.end\\]" "$1"; then
257
+ echo '[{"type":"EMAIL_ADDRESS","start":11,"end":27,"score":0.99,"text":"jane@example.com"}]'
258
+ else
259
+ echo '[{"type":"EMAIL_ADDRESS","start":11,"end":27,"score":0.99}]'
260
+ fi`,
261
+ );
262
+
263
+ const result = spawnSync(
264
+ 'bun',
265
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--text', 'Contact me: jane@example.com'],
266
+ {
267
+ encoding: 'utf-8',
268
+ env: { ...process.env, HOME: fakeHome },
269
+ }
270
+ );
271
+
272
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
273
+ expect(parsed.success).toBe(true);
274
+ expect(parsed.entities?.[0]?.type).toBe('EMAIL_ADDRESS');
275
+ expect(parsed.entities?.[0]).not.toHaveProperty('text');
276
+ });
277
+ });
278
+
279
+ // ─── presidio-redact ─────────────────────────────────────────────────────────
280
+
281
+ describe('presidio-redact', () => {
282
+ let tempDir: string;
283
+
284
+ beforeEach(() => {
285
+ tempDir = mkdtempSync(join(tmpdir(), 'pii-redact-test-'));
286
+ });
287
+
288
+ afterEach(() => {
289
+ try {
290
+ rmSync(tempDir, { recursive: true, force: true });
291
+ } catch {
292
+ // Ignore
293
+ }
294
+ });
295
+
296
+ it('returns success:false when --file is missing', () => {
297
+ const result = spawnSync(
298
+ 'bun',
299
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts')],
300
+ {
301
+ encoding: 'utf-8',
302
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
303
+ }
304
+ );
305
+
306
+ let parsed: ScriptResult;
307
+ try {
308
+ parsed = JSON.parse(result.stdout.trim());
309
+ } catch {
310
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
311
+ }
312
+
313
+ expect(parsed.success).toBe(false);
314
+ // Either init_required or missing --file error
315
+ expect(parsed.error).toBeDefined();
316
+ });
317
+
318
+ it('returns success:false with init_required when venv does not exist', () => {
319
+ const testFile = join(tempDir, 'test.md');
320
+ writeFileSync(testFile, 'Hello, my email is test@example.com');
321
+
322
+ const result = spawnSync(
323
+ 'bun',
324
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile],
325
+ {
326
+ encoding: 'utf-8',
327
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
328
+ }
329
+ );
330
+
331
+ let parsed: ScriptResult;
332
+ try {
333
+ parsed = JSON.parse(result.stdout.trim());
334
+ } catch {
335
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
336
+ }
337
+
338
+ expect(parsed.success).toBe(false);
339
+ expect(parsed.init_required).toBe(true);
340
+ expect(parsed.error).toContain('presidio-init');
341
+ });
342
+
343
+ it('does not write output file with --dry-run (venv missing path)', () => {
344
+ const testFile = join(tempDir, 'test.md');
345
+ writeFileSync(testFile, 'Hello, my email is test@example.com');
346
+ const expectedOutput = join(tempDir, 'test-redacted.md');
347
+
348
+ const result = spawnSync(
349
+ 'bun',
350
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--dry-run'],
351
+ {
352
+ encoding: 'utf-8',
353
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
354
+ }
355
+ );
356
+
357
+ // With no venv, it will fail before writing
358
+ expect(existsSync(expectedOutput)).toBe(false);
359
+
360
+ let parsed: ScriptResult;
361
+ try {
362
+ parsed = JSON.parse(result.stdout.trim());
363
+ } catch {
364
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
365
+ }
366
+ expect(parsed.success).toBe(false);
367
+ expect(parsed.init_required).toBe(true);
368
+ });
369
+
370
+ it('correctly parses --output path argument', () => {
371
+ const testFile = join(tempDir, 'source.md');
372
+ writeFileSync(testFile, 'Contact: jane@example.com');
373
+ const customOutput = join(tempDir, 'custom-output.md');
374
+
375
+ // Without venv this will fail, but we can check the argument is parsed
376
+ const result = spawnSync(
377
+ 'bun',
378
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--output', customOutput],
379
+ {
380
+ encoding: 'utf-8',
381
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
382
+ }
383
+ );
384
+
385
+ let parsed: ScriptResult;
386
+ try {
387
+ parsed = JSON.parse(result.stdout.trim());
388
+ } catch {
389
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
390
+ }
391
+
392
+ // With no venv this will fail with init_required
393
+ expect(parsed.success).toBe(false);
394
+ expect(parsed.init_required).toBe(true);
395
+ // Confirm the output file was NOT created
396
+ expect(existsSync(customOutput)).toBe(false);
397
+ });
398
+
399
+ it('omits redacted_text in non-dry-run output', () => {
400
+ const fakeHome = join(tempDir, 'fake-home-no-redacted-text');
401
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'{"redacted_text":"masked text","entities_found":1,"entities_redacted":1}\'');
402
+
403
+ const testFile = join(tempDir, 'source.md');
404
+ const outputFile = join(tempDir, 'source-redacted.md');
405
+ writeFileSync(testFile, 'Contact: jane@example.com');
406
+
407
+ const result = spawnSync(
408
+ 'bun',
409
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile],
410
+ {
411
+ encoding: 'utf-8',
412
+ env: { ...process.env, HOME: fakeHome },
413
+ }
414
+ );
415
+
416
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
417
+ expect(parsed.success).toBe(true);
418
+ expect(parsed.dry_run).toBe(false);
419
+ expect(parsed.redacted_text).toBeUndefined();
420
+ expect(parsed.output_path).toBe(outputFile);
421
+ expect(existsSync(outputFile)).toBe(true);
422
+ });
423
+
424
+ it('rejects unsupported entity names', () => {
425
+ const fakeHome = join(tempDir, 'fake-home');
426
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'{"redacted_text":"ok","entities_found":0,"entities_redacted":0}\'');
427
+
428
+ const testFile = join(tempDir, 'source.md');
429
+ writeFileSync(testFile, 'Contact: jane@example.com');
430
+
431
+ const result = spawnSync(
432
+ 'bun',
433
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--entities', 'NOT_A_REAL_ENTITY'],
434
+ {
435
+ encoding: 'utf-8',
436
+ env: { ...process.env, HOME: fakeHome },
437
+ }
438
+ );
439
+
440
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
441
+ expect(parsed.success).toBe(false);
442
+ expect(parsed.error).toContain('Unsupported entity type');
443
+ });
444
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "droid-project",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Manage project context files for persistent AI memory across sessions. Load, update, or create project context before working on multi-session features.",
5
5
  "author": {
6
6
  "name": "Orderful",
@@ -1,6 +1,6 @@
1
1
  name: project
2
2
  description: "Manage project context files for persistent AI memory across sessions. Load, update, or create project context before working on multi-session features."
3
- version: 0.3.6
3
+ version: 0.4.0
4
4
  status: beta
5
5
 
6
6
  includes:
@@ -21,7 +21,7 @@ config_schema:
21
21
  required: true
22
22
  preset:
23
23
  type: select
24
- description: Output format for templates
24
+ description: "Link style: obsidian uses [[wikilinks]], markdown uses [standard](links)"
25
25
  default: "markdown"
26
26
  options:
27
27
  - markdown
@@ -7,7 +7,7 @@ allowed-tools: [Read, Write, Glob, Grep, Bash]
7
7
 
8
8
  # Project Skill
9
9
 
10
- Persistent project context that survives across sessions. Projects are folders containing `PROJECT.md` + `CHANGELOG.md`.
10
+ Persistent project context that survives across sessions. Projects are folders containing `PROJECT.md` + `CHANGELOG.md`, with optional companion docs (`DECISIONS.md`, `WORKLOG.md`) that emerge as the project grows.
11
11
 
12
12
  ## Why Projects?
13
13
 
@@ -31,7 +31,7 @@ Chat history disappears. Projects persist.
31
31
  | Setting | Default | Description |
32
32
  | -------------- | ------------ | -------------------------------------------------- |
33
33
  | `projects_dir` | **required** | Where projects are stored (must be configured) |
34
- | `preset` | `markdown` | Output format: `markdown` or `obsidian` |
34
+ | `preset` | `markdown` | Link style: `obsidian` uses `[[wikilinks]]`, `markdown` uses `[standard](links)` |
35
35
  | `override` | (none) | User-defined behaviour overrides |
36
36
 
37
37
  **Overrides:** This skill supports user-defined overrides. See `/droid` skill § Skill Overrides.
@@ -106,6 +106,7 @@ Projects work best in a cycle:
106
106
  3. **Document** - Capture decisions in PROJECT.md before implementing
107
107
  4. **Implement** - Build with full context of why decisions were made
108
108
  5. **Capture** - `/project update` saves new learnings
109
- 6. **Repeat** - Next session starts with accumulated knowledge
109
+ 6. **Prune** - If PROJECT.md exceeds ~300 lines, archive completed work to WORKLOG.md
110
+ 7. **Repeat** - Next session starts with accumulated knowledge
110
111
 
111
112
  Each cycle starts further ahead than the last.
@@ -5,7 +5,7 @@ Projects use a split file pattern to minimize context usage when loading.
5
5
  ## File Structure
6
6
 
7
7
  - `CHANGELOG.md` (sibling to PROJECT.md) - Complete version history
8
- - `PROJECT.md` - Only 3 most recent entries + link to full history
8
+ - `PROJECT.md` - Link to full history only (version number lives in the metadata table)
9
9
 
10
10
  ## On Update
11
11
 
@@ -15,8 +15,8 @@ Projects use a split file pattern to minimize context usage when loading.
15
15
  2. **Add new entry to CHANGELOG.md** (prepend after header)
16
16
 
17
17
  3. **Update PROJECT.md changelog section**
18
- - Keep only 3 most recent entries
19
- - Ensure link to full history exists
18
+ - Keep only a link to CHANGELOG.md — no inline entries
19
+ - The version number in the metadata table is the quick-glance indicator
20
20
 
21
21
  ## CHANGELOG.md Format
22
22
 
@@ -35,31 +35,20 @@ All notable changes to the {Project Name} project.
35
35
 
36
36
  ## PROJECT.md Changelog Section Format
37
37
 
38
- **Markdown preset:**
39
38
  ```markdown
40
39
  ## Changelog
41
40
 
42
41
  See [CHANGELOG.md](CHANGELOG.md) for full history.
43
-
44
- ### X.Y.Z - YYYY-MM-DD
45
- - [Most recent]
46
-
47
- ### X.Y.Z - YYYY-MM-DD
48
- - [Second most recent]
49
-
50
- ### X.Y.Z - YYYY-MM-DD
51
- - [Third most recent]
52
42
  ```
53
43
 
54
- **Obsidian preset:**
55
- ```markdown
56
- ## Changelog
44
+ When `preset` is `obsidian`, use `See [[CHANGELOG|full history]].` instead.
57
45
 
58
- See [[CHANGELOG|full history]].
46
+ ## Migration
59
47
 
60
- ### X.Y.Z - YYYY-MM-DD
61
- - [Most recent]
62
- ```
48
+ If an existing PROJECT.md has inline changelog entries:
49
+ 1. Ensure all entries exist in CHANGELOG.md
50
+ 2. Replace the inline entries with just the link
51
+ 3. This happens naturally during `/project update` — no need to proactively migrate
63
52
 
64
53
  ## Guidelines
65
54
 
@@ -67,4 +56,4 @@ See [[CHANGELOG|full history]].
67
56
  - Keep entries concise but descriptive
68
57
  - Group related changes in a single bullet when appropriate
69
58
  - Most recent version at the top
70
- - Use `##` headers in CHANGELOG.md, `###` in PROJECT.md
59
+ - Use `##` headers in CHANGELOG.md
@@ -7,7 +7,6 @@
7
7
  1. **Read config first**
8
8
  - Run `droid config --get tools.project` and parse the JSON output
9
9
  - Use `projects_dir` (required - if not configured, inform user to set it up)
10
- - Use `preset` if configured (markdown or obsidian)
11
10
 
12
11
  2. **Get project name**
13
12
  - Use provided name, or ask if not provided
@@ -21,7 +20,6 @@
21
20
  4. **Create files from templates** (see `templates.md`)
22
21
  - `PROJECT.md` - Main context file
23
22
  - `CHANGELOG.md` - Version history
24
- - Format varies by `preset` config (markdown vs obsidian)
25
23
 
26
24
  5. **Confirm creation**
27
25
  - Show created folder path
@@ -62,6 +60,18 @@ New projects start minimal and grow organically. Common sections added over time
62
60
 
63
61
  See `templates.md` for the full initial template.
64
62
 
63
+ ### Companion Docs
64
+
65
+ Projects may grow companion docs over time. These are **never** created at project creation — they emerge as the project grows.
66
+
67
+ | Doc | Created When | Purpose |
68
+ |-----|-------------|---------|
69
+ | `CHANGELOG.md` | At project creation (the one exception) | Version history |
70
+ | `DECISIONS.md` | 3+ decisions accumulate in PROJECT.md | Dedicated decisions log |
71
+ | `WORKLOG.md` | PROJECT.md exceeds ~300 lines with completed items | Archive of completed work |
72
+
73
+ The `/project update` procedure handles creation and migration automatically — see `updating.md` for details.
74
+
65
75
  ---
66
76
 
67
77
  ## Creating from Codex
@@ -32,7 +32,8 @@
32
32
  - Confirm which project was loaded
33
33
  - Summarize key context (2-3 sentences)
34
34
  - Use project contents for all subsequent work in the session
35
- - **Do NOT read CHANGELOG.md** it exists for on-demand lookup only. PROJECT.md already references it; that is enough context.
35
+ - **Do NOT read companion docs by default** — CHANGELOG.md, DECISIONS.md, and WORKLOG.md exist for on-demand lookup only. PROJECT.md already references them; that is enough context.
36
+ - If the user asks about decisions, past work, or history, THEN read the relevant companion doc.
36
37
 
37
38
  7. **If `-- {instruction}` provided:** Execute the follow-up instruction against the loaded project
38
39