create-sdd-project 0.16.9 → 0.17.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/bin/cli.js +2 -0
- package/lib/adapt-agents.js +117 -63
- package/lib/doctor.js +221 -0
- package/lib/generator.js +8 -0
- package/lib/init-generator.js +121 -198
- package/lib/meta.js +291 -0
- package/lib/stack-adaptations.js +335 -0
- package/lib/upgrade-generator.js +441 -36
- package/package.json +1 -1
- package/template/.claude/agents/backend-planner.md +3 -3
- package/template/.claude/agents/frontend-planner.md +3 -3
- package/template/.gemini/agents/backend-planner.md +2 -2
- package/template/.gemini/agents/frontend-planner.md +3 -3
- package/template/gitignore +6 -0
package/lib/upgrade-generator.js
CHANGED
|
@@ -9,7 +9,10 @@ const {
|
|
|
9
9
|
TEMPLATE_AGENTS,
|
|
10
10
|
TEMPLATE_COMMANDS,
|
|
11
11
|
} = require('./config');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
adaptAgentContentForProjectType,
|
|
14
|
+
adaptAgentContentString,
|
|
15
|
+
} = require('./adapt-agents');
|
|
13
16
|
const {
|
|
14
17
|
adaptBaseStandards,
|
|
15
18
|
adaptBackendStandards,
|
|
@@ -21,6 +24,96 @@ const {
|
|
|
21
24
|
updateAutonomy,
|
|
22
25
|
regexReplaceInFile,
|
|
23
26
|
} = require('./init-generator');
|
|
27
|
+
// v0.17.0: hash-based smart-diff + shared stack adaptations
|
|
28
|
+
const {
|
|
29
|
+
readMeta,
|
|
30
|
+
writeMeta,
|
|
31
|
+
computeHash,
|
|
32
|
+
hashFileOnDisk,
|
|
33
|
+
toPosix,
|
|
34
|
+
pruneExpectedAbsent,
|
|
35
|
+
expectedSmartDiffTrackedPaths,
|
|
36
|
+
normalizeForCompare: metaNormalizeForCompare,
|
|
37
|
+
} = require('./meta');
|
|
38
|
+
const {
|
|
39
|
+
applyStackAdaptations,
|
|
40
|
+
applyStackAdaptationsToContent,
|
|
41
|
+
} = require('./stack-adaptations');
|
|
42
|
+
|
|
43
|
+
// --- v0.16.10: backup-before-replace helpers ---
|
|
44
|
+
//
|
|
45
|
+
// Nuclear safety net for smart-diff protection (Changes #1 + #2 + #3).
|
|
46
|
+
// Ensures no user file is overwritten during upgrade without a recoverable
|
|
47
|
+
// backup. Idempotent per run — each path is backed up at most once even if
|
|
48
|
+
// touched by multiple stages of the upgrade pipeline.
|
|
49
|
+
|
|
50
|
+
const backedUpPaths = new Set(); // reset at the start of every generateUpgrade call
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a collision-safe backup directory name.
|
|
54
|
+
*
|
|
55
|
+
* Format: YYYYMMDD-HHMMSS-NNNN where NNNN is the last 4 digits of the
|
|
56
|
+
* current epoch millisecond count. Two upgrades within the same second get
|
|
57
|
+
* distinct directory names because the millisecond suffix differs.
|
|
58
|
+
*
|
|
59
|
+
* Examples:
|
|
60
|
+
* 20260413-150000-1234
|
|
61
|
+
* 20260413-150000-5678 (one millisecond later)
|
|
62
|
+
*/
|
|
63
|
+
function buildBackupTimestamp() {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const iso = now
|
|
66
|
+
.toISOString()
|
|
67
|
+
.replace(/[-:]/g, '')
|
|
68
|
+
.replace(/\..+$/, '')
|
|
69
|
+
.replace('T', '-');
|
|
70
|
+
const msSuffix = Date.now().toString().slice(-4);
|
|
71
|
+
return `${iso}-${msSuffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalize text for smart-diff comparison.
|
|
76
|
+
*
|
|
77
|
+
* v0.17.0: delegates to `lib/meta.js` which only strips CR/CRLF (Windows
|
|
78
|
+
* git core.autocrlf compatibility). Trailing whitespace is NO LONGER
|
|
79
|
+
* stripped — that would destroy markdown hard-breaks (two trailing
|
|
80
|
+
* spaces = <br>) and silently wipe legitimate customizations (Gemini M2
|
|
81
|
+
* fix from plan v1.0 review). A local re-export here keeps the old
|
|
82
|
+
* symbol available for any pre-v0.17.0 code paths that still call it.
|
|
83
|
+
*/
|
|
84
|
+
const normalizeForCompare = metaNormalizeForCompare;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Copy a user file to .sdd-backup/<timestamp>/<relativePath> before it is
|
|
88
|
+
* modified or replaced. Returns the absolute backup path, or null if the
|
|
89
|
+
* source doesn't exist, was already backed up this run, or the copy failed
|
|
90
|
+
* (non-fatal — warning printed).
|
|
91
|
+
*
|
|
92
|
+
* Contract:
|
|
93
|
+
* - Idempotent per run: calling twice for the same path is a no-op
|
|
94
|
+
* - Non-fatal on failure: upgrade continues even if backup can't be written
|
|
95
|
+
* - Does NOT mkdir the backup parent directory until we know the source
|
|
96
|
+
* file exists, to avoid leaving empty directories on failure
|
|
97
|
+
*/
|
|
98
|
+
function backupBeforeReplace(dest, relativePath, backupTimestamp) {
|
|
99
|
+
const key = `${backupTimestamp}::${relativePath}`;
|
|
100
|
+
if (backedUpPaths.has(key)) return null;
|
|
101
|
+
|
|
102
|
+
const sourcePath = path.join(dest, relativePath);
|
|
103
|
+
if (!fs.existsSync(sourcePath)) return null;
|
|
104
|
+
|
|
105
|
+
const backupRoot = path.join(dest, '.sdd-backup', backupTimestamp);
|
|
106
|
+
const backupPath = path.join(backupRoot, relativePath);
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
109
|
+
fs.copyFileSync(sourcePath, backupPath);
|
|
110
|
+
backedUpPaths.add(key);
|
|
111
|
+
return backupPath;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.warn(` ⚠ Backup of ${relativePath} failed: ${e.code || e.message}`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
24
117
|
|
|
25
118
|
/**
|
|
26
119
|
* Read the installed SDD version from .sdd-version file.
|
|
@@ -195,7 +288,28 @@ function generateUpgrade(config) {
|
|
|
195
288
|
const aiTools = config.aiTools;
|
|
196
289
|
const projectType = config.projectType;
|
|
197
290
|
|
|
291
|
+
// v0.16.10: Reset per-run backup tracking and build a collision-safe timestamp.
|
|
292
|
+
// Every file replaced by this upgrade will be backed up to .sdd-backup/<ts>/
|
|
293
|
+
// before modification, so the user can always recover.
|
|
294
|
+
backedUpPaths.clear();
|
|
295
|
+
const backupTimestamp = buildBackupTimestamp();
|
|
296
|
+
|
|
297
|
+
// Track which template agents and AGENTS.md were preserved due to customization,
|
|
298
|
+
// so we can surface the list in the upgrade result summary.
|
|
299
|
+
const modifiedAgentsResults = [];
|
|
300
|
+
|
|
301
|
+
// v0.17.0: provenance tracking. Read existing hashes at the start; track
|
|
302
|
+
// new/updated hashes as we go. Preserved files leave their entry untouched
|
|
303
|
+
// (Codex M1 invariant: only write canonical hashes for tool-written content).
|
|
304
|
+
// `filesToAdapt` collects POSIX paths of files that were replaced or newly
|
|
305
|
+
// written in this run; applyStackAdaptations will be called with this
|
|
306
|
+
// allowlist after the write loop so only these files get re-adapted.
|
|
307
|
+
const meta = readMeta(dest);
|
|
308
|
+
const newHashes = { ...(meta?.hashes ?? {}) };
|
|
309
|
+
const filesToAdapt = new Set();
|
|
310
|
+
|
|
198
311
|
console.log(`\nUpgrading SDD DevFlow in ${config.projectName}...\n`);
|
|
312
|
+
console.log(` Backup directory: .sdd-backup/${backupTimestamp}/\n`);
|
|
199
313
|
|
|
200
314
|
// --- a) Preserve user items ---
|
|
201
315
|
const autonomy = readAutonomyLevel(dest);
|
|
@@ -219,8 +333,10 @@ function generateUpgrade(config) {
|
|
|
219
333
|
|
|
220
334
|
for (const dir of toolDirs) {
|
|
221
335
|
const base = path.join(dest, dir);
|
|
222
|
-
//
|
|
223
|
-
|
|
336
|
+
// v0.16.10: we NO LONGER wholesale-delete agents/. The smart-diff loop below
|
|
337
|
+
// iterates TEMPLATE_AGENTS and preserves customized files individually.
|
|
338
|
+
// skills/hooks/styles remain SDD-owned with wholesale delete-and-replace.
|
|
339
|
+
for (const sub of ['skills', 'hooks', 'styles']) {
|
|
224
340
|
const subDir = path.join(base, sub);
|
|
225
341
|
if (fs.existsSync(subDir)) {
|
|
226
342
|
fs.rmSync(subDir, { recursive: true, force: true });
|
|
@@ -240,19 +356,155 @@ function generateUpgrade(config) {
|
|
|
240
356
|
for (const sub of ['agents', 'skills', 'hooks', 'styles', 'commands']) {
|
|
241
357
|
const srcSub = path.join(templateToolDir, sub);
|
|
242
358
|
const destSub = path.join(base, sub);
|
|
243
|
-
if (fs.existsSync(srcSub))
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
359
|
+
if (!fs.existsSync(srcSub)) continue;
|
|
360
|
+
|
|
361
|
+
// v0.16.10: smart-diff per template agent file (see Change #2 in
|
|
362
|
+
// /Users/pb/.claude/plans/validated-wobbling-blum.md).
|
|
363
|
+
//
|
|
364
|
+
// Invariant: this loop only processes files listed in TEMPLATE_AGENTS.
|
|
365
|
+
// Custom agents (files NOT in TEMPLATE_AGENTS) are left untouched on
|
|
366
|
+
// disk — they're also captured by collectCustomAgents earlier, so the
|
|
367
|
+
// existing restore loop at step (c) below will rewrite them from memory
|
|
368
|
+
// (redundant but harmless, same content).
|
|
369
|
+
if (sub === 'agents') {
|
|
370
|
+
fs.mkdirSync(destSub, { recursive: true });
|
|
371
|
+
|
|
372
|
+
const templateAgentFiles = fs
|
|
373
|
+
.readdirSync(srcSub, { withFileTypes: true })
|
|
374
|
+
.filter((e) => e.isFile() && TEMPLATE_AGENTS.includes(e.name))
|
|
375
|
+
.map((e) => e.name);
|
|
376
|
+
|
|
377
|
+
for (const file of templateAgentFiles) {
|
|
378
|
+
const templateAgentPath = path.join(srcSub, file);
|
|
379
|
+
const existingAgentPath = path.join(destSub, file);
|
|
380
|
+
const relativePath = path.relative(dest, existingAgentPath);
|
|
381
|
+
const posixPath = toPosix(relativePath);
|
|
382
|
+
|
|
383
|
+
const rawTemplate = fs.readFileSync(templateAgentPath, 'utf8');
|
|
384
|
+
const adaptedCoreTarget = adaptAgentContentString(rawTemplate, file, projectType);
|
|
385
|
+
|
|
386
|
+
// --- v0.17.0 decision tree ---
|
|
387
|
+
//
|
|
388
|
+
// Case 1: file missing or --force-template → unconditional write.
|
|
389
|
+
// Case 2: meta has a hash for this path → hash-based path.
|
|
390
|
+
// 2a. hash matches → pristine, replace with adaptedCoreTarget.
|
|
391
|
+
// 2b. hash mismatches → customized, preserve + .new backup.
|
|
392
|
+
// IMPORTANT (Codex M1): do NOT update newHashes here.
|
|
393
|
+
// Case 3: no meta or no hash for this path → fallback path.
|
|
394
|
+
// 3a. Compute adaptedFullTarget by applying stack adaptations
|
|
395
|
+
// in-memory so init-adapted files don't false-positive
|
|
396
|
+
// (Gemini M1 fix).
|
|
397
|
+
// 3b. Content match → replace.
|
|
398
|
+
// 3c. Content mismatch → preserve + .new backup. Same Codex M1
|
|
399
|
+
// rule: preserved files do NOT get a new hash.
|
|
400
|
+
|
|
401
|
+
if (!fs.existsSync(existingAgentPath)) {
|
|
402
|
+
// Missing — write fresh and track for stack adaptations.
|
|
403
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
404
|
+
filesToAdapt.add(posixPath);
|
|
405
|
+
replaced++;
|
|
406
|
+
continue;
|
|
250
407
|
}
|
|
251
|
-
|
|
252
|
-
|
|
408
|
+
|
|
409
|
+
if (config.forceTemplate) {
|
|
410
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
411
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
412
|
+
filesToAdapt.add(posixPath);
|
|
413
|
+
replaced++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const existingContent = fs.readFileSync(existingAgentPath, 'utf8');
|
|
418
|
+
const storedHash = meta && meta.hashes[posixPath];
|
|
419
|
+
|
|
420
|
+
const preserveFile = (target) => {
|
|
421
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
422
|
+
const newBackupPath = path.join(
|
|
423
|
+
dest,
|
|
424
|
+
'.sdd-backup',
|
|
425
|
+
backupTimestamp,
|
|
426
|
+
`${relativePath}.new`
|
|
427
|
+
);
|
|
428
|
+
try {
|
|
429
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
430
|
+
fs.writeFileSync(newBackupPath, target, 'utf8');
|
|
431
|
+
} catch (e) {
|
|
432
|
+
console.warn(
|
|
433
|
+
` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
437
|
+
preserved++;
|
|
438
|
+
// Codex M1 invariant: do NOT update newHashes[posixPath]
|
|
439
|
+
// for preserved files. The existing hash (if any) persists.
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (storedHash) {
|
|
443
|
+
// Case 2: primary hash path.
|
|
444
|
+
const currentHash = computeHash(existingContent);
|
|
445
|
+
if (currentHash === storedHash) {
|
|
446
|
+
// Pristine — replace with core-adapted target. Stack
|
|
447
|
+
// adaptations will be applied via filesToAdapt after the
|
|
448
|
+
// smart-diff loop.
|
|
449
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
450
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
451
|
+
filesToAdapt.add(posixPath);
|
|
452
|
+
replaced++;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
// Hash mismatch → preserve. The .new backup target is the
|
|
456
|
+
// FULL adapted target (core + stack) so the user can diff
|
|
457
|
+
// apples to apples against their customized file.
|
|
458
|
+
const adaptedFullTarget = applyStackAdaptationsToContent(
|
|
459
|
+
adaptedCoreTarget,
|
|
460
|
+
posixPath,
|
|
461
|
+
scan,
|
|
462
|
+
config
|
|
463
|
+
);
|
|
464
|
+
preserveFile(adaptedFullTarget);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Case 3: fallback path — no hash available. Compare against
|
|
469
|
+
// the FULL adapted target (core + stack) so init-adapted files
|
|
470
|
+
// from pre-v0.17.0 projects don't false-positive (Gemini M1).
|
|
471
|
+
const adaptedFullTargetFallback = applyStackAdaptationsToContent(
|
|
472
|
+
adaptedCoreTarget,
|
|
473
|
+
posixPath,
|
|
474
|
+
scan,
|
|
475
|
+
config
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
normalizeForCompare(existingContent) ===
|
|
480
|
+
normalizeForCompare(adaptedFullTargetFallback)
|
|
481
|
+
) {
|
|
482
|
+
// Pristine per content compare — replace with core target.
|
|
483
|
+
// Stack adaptations run after the loop to finalize the file.
|
|
484
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
485
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
486
|
+
filesToAdapt.add(posixPath);
|
|
487
|
+
replaced++;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Content mismatch → preserve. Same rule: no hash update.
|
|
492
|
+
preserveFile(adaptedFullTargetFallback);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// For .claude/commands, merge: overwrite SDD template commands, preserve user's custom commands
|
|
498
|
+
if (dir === '.claude' && sub === 'commands') {
|
|
499
|
+
fs.mkdirSync(destSub, { recursive: true });
|
|
500
|
+
for (const file of fs.readdirSync(srcSub)) {
|
|
501
|
+
// Always overwrite template-owned files (they may have been updated)
|
|
502
|
+
fs.cpSync(path.join(srcSub, file), path.join(destSub, file));
|
|
253
503
|
}
|
|
254
|
-
|
|
504
|
+
} else {
|
|
505
|
+
fs.cpSync(srcSub, destSub, { recursive: true });
|
|
255
506
|
}
|
|
507
|
+
replaced++;
|
|
256
508
|
}
|
|
257
509
|
|
|
258
510
|
// Merge settings.json — strategy depends on the tool:
|
|
@@ -387,18 +639,82 @@ function generateUpgrade(config) {
|
|
|
387
639
|
}
|
|
388
640
|
|
|
389
641
|
// --- e) Replace top-level configs ---
|
|
390
|
-
// AGENTS.md
|
|
642
|
+
// AGENTS.md — hash-based smart-diff (v0.17.0 upgrade of v0.16.10 Change #3).
|
|
643
|
+
//
|
|
644
|
+
// Decision tree identical to the template-agent loop above:
|
|
645
|
+
// 1. Missing or --force-template → unconditional write.
|
|
646
|
+
// 2. meta has a hash for AGENTS.md → hash-based path:
|
|
647
|
+
// 2a. hash match → pristine, replace.
|
|
648
|
+
// 2b. hash mismatch → preserve + .new backup. Codex M1 invariant:
|
|
649
|
+
// do NOT update newHashes['AGENTS.md'].
|
|
650
|
+
// 3. No hash → fallback content compare against the full adapted
|
|
651
|
+
// target. AGENTS.md has no stack adaptations (adaptAgentsMd already
|
|
652
|
+
// includes project-type pruning), so the comparison target is the
|
|
653
|
+
// adaptAgentsMd output itself.
|
|
391
654
|
const agentsMdTemplate = fs.readFileSync(path.join(templateDir, 'AGENTS.md'), 'utf8');
|
|
392
655
|
const adaptedAgentsMd = adaptAgentsMd(agentsMdTemplate, config, scan);
|
|
393
|
-
|
|
394
|
-
|
|
656
|
+
const agentsMdDestPath = path.join(dest, 'AGENTS.md');
|
|
657
|
+
const AGENTS_MD_POSIX = 'AGENTS.md';
|
|
658
|
+
|
|
659
|
+
const preserveAgentsMd = () => {
|
|
660
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
661
|
+
const newBackupPath = path.join(dest, '.sdd-backup', backupTimestamp, 'AGENTS.md.new');
|
|
662
|
+
try {
|
|
663
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
664
|
+
fs.writeFileSync(newBackupPath, adaptedAgentsMd, 'utf8');
|
|
665
|
+
} catch (e) {
|
|
666
|
+
console.warn(` ⚠ Failed to write .new backup for AGENTS.md: ${e.code || e.message}`);
|
|
667
|
+
}
|
|
668
|
+
modifiedAgentsResults.push({ name: 'AGENTS.md', modified: true });
|
|
669
|
+
preserved++;
|
|
670
|
+
// Codex M1 invariant: do NOT update newHashes[AGENTS_MD_POSIX].
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
if (!fs.existsSync(agentsMdDestPath)) {
|
|
674
|
+
// Missing — write and hash fresh.
|
|
675
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
676
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
677
|
+
replaced++;
|
|
678
|
+
} else if (config.forceTemplate) {
|
|
679
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
680
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
681
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
682
|
+
replaced++;
|
|
683
|
+
} else {
|
|
684
|
+
const existingAgentsMd = fs.readFileSync(agentsMdDestPath, 'utf8');
|
|
685
|
+
const storedAgentsMdHash = meta && meta.hashes[AGENTS_MD_POSIX];
|
|
686
|
+
|
|
687
|
+
if (storedAgentsMdHash) {
|
|
688
|
+
const currentHash = computeHash(existingAgentsMd);
|
|
689
|
+
if (currentHash === storedAgentsMdHash) {
|
|
690
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
691
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
692
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
693
|
+
replaced++;
|
|
694
|
+
} else {
|
|
695
|
+
preserveAgentsMd();
|
|
696
|
+
}
|
|
697
|
+
} else if (
|
|
698
|
+
normalizeForCompare(existingAgentsMd) === normalizeForCompare(adaptedAgentsMd)
|
|
699
|
+
) {
|
|
700
|
+
// Fallback content-compare.
|
|
701
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
702
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
703
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
704
|
+
replaced++;
|
|
705
|
+
} else {
|
|
706
|
+
preserveAgentsMd();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
395
709
|
|
|
396
|
-
// CLAUDE.md / GEMINI.md
|
|
710
|
+
// CLAUDE.md / GEMINI.md (back up before replace, not smart-diff'd)
|
|
397
711
|
if (aiTools !== 'gemini') {
|
|
712
|
+
backupBeforeReplace(dest, 'CLAUDE.md', backupTimestamp);
|
|
398
713
|
fs.copyFileSync(path.join(templateDir, 'CLAUDE.md'), path.join(dest, 'CLAUDE.md'));
|
|
399
714
|
replaced++;
|
|
400
715
|
}
|
|
401
716
|
if (aiTools !== 'claude') {
|
|
717
|
+
backupBeforeReplace(dest, 'GEMINI.md', backupTimestamp);
|
|
402
718
|
fs.copyFileSync(path.join(templateDir, 'GEMINI.md'), path.join(dest, 'GEMINI.md'));
|
|
403
719
|
replaced++;
|
|
404
720
|
}
|
|
@@ -448,6 +764,35 @@ function generateUpgrade(config) {
|
|
|
448
764
|
replaced++;
|
|
449
765
|
}
|
|
450
766
|
|
|
767
|
+
// --- e3) .gitignore — idempotent append of .sdd-backup/ (v0.16.10)
|
|
768
|
+
// and .sdd-meta.json (v0.17.0) ---
|
|
769
|
+
// Existing projects created before these versions don't have the
|
|
770
|
+
// entries in their .gitignore. Append them once so the files aren't
|
|
771
|
+
// accidentally committed.
|
|
772
|
+
const userGitignorePath = path.join(dest, '.gitignore');
|
|
773
|
+
if (fs.existsSync(userGitignorePath)) {
|
|
774
|
+
let existingGitignore = fs.readFileSync(userGitignorePath, 'utf8');
|
|
775
|
+
let updatedGitignore = false;
|
|
776
|
+
|
|
777
|
+
if (!/^\s*\/?\.sdd-backup\/?\s*$/m.test(existingGitignore)) {
|
|
778
|
+
const appendBlock = '\n\n# sdd-devflow upgrade backups (ignored — kept locally for recovery only)\n.sdd-backup/\n';
|
|
779
|
+
existingGitignore = existingGitignore.trimEnd() + appendBlock;
|
|
780
|
+
updatedGitignore = true;
|
|
781
|
+
step('Updated .gitignore with .sdd-backup/ entry');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (!/^\s*\/?\.sdd-meta\.json\s*$/m.test(existingGitignore)) {
|
|
785
|
+
const appendBlock = '\n\n# sdd-devflow provenance tracking (local-only, content-addressable hashes)\n.sdd-meta.json\n';
|
|
786
|
+
existingGitignore = existingGitignore.trimEnd() + appendBlock;
|
|
787
|
+
updatedGitignore = true;
|
|
788
|
+
step('Updated .gitignore with .sdd-meta.json entry');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (updatedGitignore) {
|
|
792
|
+
fs.writeFileSync(userGitignorePath, existingGitignore, 'utf8');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
451
796
|
// --- f) Adapt for project type ---
|
|
452
797
|
// Remove agents for single-stack projects
|
|
453
798
|
if (projectType === 'backend') {
|
|
@@ -468,30 +813,33 @@ function generateUpgrade(config) {
|
|
|
468
813
|
}
|
|
469
814
|
}
|
|
470
815
|
|
|
471
|
-
// Adapt agent/skill content for project type
|
|
816
|
+
// Adapt agent/skill content for project type (single-stack pruning —
|
|
817
|
+
// removes frontend/backend refs). Separate from stack substitutions
|
|
818
|
+
// (Zod/ORM/DDD). Safe to run on all files because the pruning rules
|
|
819
|
+
// use literal template strings that only appear in raw template.
|
|
472
820
|
if (projectType !== 'fullstack') {
|
|
473
821
|
adaptAgentContentForProjectType(dest, config, regexReplaceInFile);
|
|
474
822
|
}
|
|
475
823
|
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
[/\| `docs\/specs\/api-spec\.yaml` \|[^\n]*\n/, ''],
|
|
492
|
-
]);
|
|
493
|
-
}
|
|
824
|
+
// v0.17.0: Stack adaptations run ONLY on files that were replaced or
|
|
825
|
+
// newly written in this run. Preserved (customized) files MUST NOT be
|
|
826
|
+
// touched by stack adaptations, otherwise their user edits could be
|
|
827
|
+
// mangled by the rule replacements (Codex M1 + plan v1.1 § Allowlist
|
|
828
|
+
// semantics).
|
|
829
|
+
//
|
|
830
|
+
// SKILL.md, ticket-template.md, and documentation-standards.mdc were
|
|
831
|
+
// wholesale-recopied earlier in the upgrade (via fs.cpSync and the
|
|
832
|
+
// standards pipeline), so they are always in the "replaced" state and
|
|
833
|
+
// must be in the allowlist.
|
|
834
|
+
for (const dir of toolDirs) {
|
|
835
|
+
filesToAdapt.add(toPosix(`${dir}/skills/development-workflow/SKILL.md`));
|
|
836
|
+
filesToAdapt.add(
|
|
837
|
+
toPosix(`${dir}/skills/development-workflow/references/ticket-template.md`)
|
|
838
|
+
);
|
|
494
839
|
}
|
|
840
|
+
filesToAdapt.add(toPosix('ai-specs/specs/documentation-standards.mdc'));
|
|
841
|
+
|
|
842
|
+
applyStackAdaptations(dest, scan, config, filesToAdapt);
|
|
495
843
|
|
|
496
844
|
step('Adapted files for project type and stack');
|
|
497
845
|
|
|
@@ -527,6 +875,32 @@ function generateUpgrade(config) {
|
|
|
527
875
|
fs.writeFileSync(path.join(dest, '.sdd-version'), newVersion + '\n', 'utf8');
|
|
528
876
|
step(`Updated .sdd-version to ${newVersion}`);
|
|
529
877
|
|
|
878
|
+
// --- g1) v0.17.0: update .sdd-meta.json ---
|
|
879
|
+
//
|
|
880
|
+
// For every smart-diff-tracked file that was replaced or newly written
|
|
881
|
+
// in this run (i.e. in filesToAdapt AND in the expected tracked set),
|
|
882
|
+
// recompute its hash from the post-adaptation on-disk content and merge
|
|
883
|
+
// into newHashes. Preserved files are NOT in filesToAdapt, so their old
|
|
884
|
+
// hash (if any) is left alone — Codex M1 invariant.
|
|
885
|
+
//
|
|
886
|
+
// Then prune hashes for paths that are no longer expected for this
|
|
887
|
+
// (aiTools, projectType) combination (e.g. single-stack removed a
|
|
888
|
+
// frontend agent). User-deleted files that ARE expected keep their
|
|
889
|
+
// hash, since the next upgrade will recreate the file from template.
|
|
890
|
+
{
|
|
891
|
+
const trackedSet = expectedSmartDiffTrackedPaths(aiTools, projectType);
|
|
892
|
+
for (const posixPath of filesToAdapt) {
|
|
893
|
+
if (!trackedSet.has(posixPath)) continue;
|
|
894
|
+
const absPath = path.join(dest, ...posixPath.split('/'));
|
|
895
|
+
const h = hashFileOnDisk(absPath);
|
|
896
|
+
if (h !== null) {
|
|
897
|
+
newHashes[posixPath] = h;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const prunedHashes = pruneExpectedAbsent(newHashes, aiTools, projectType);
|
|
901
|
+
writeMeta(dest, prunedHashes);
|
|
902
|
+
}
|
|
903
|
+
|
|
530
904
|
// --- Show result ---
|
|
531
905
|
const updatedCount = standardsResults.filter((s) => !s.modified).length;
|
|
532
906
|
const preservedCount = modifiedStandards.length;
|
|
@@ -551,6 +925,37 @@ function generateUpgrade(config) {
|
|
|
551
925
|
}
|
|
552
926
|
}
|
|
553
927
|
|
|
928
|
+
if (modifiedAgentsResults.length > 0) {
|
|
929
|
+
console.log(
|
|
930
|
+
`\n ⚠ Review preserved customizations (backups in .sdd-backup/${backupTimestamp}/):`
|
|
931
|
+
);
|
|
932
|
+
for (const r of modifiedAgentsResults) {
|
|
933
|
+
console.log(
|
|
934
|
+
` - ${r.name} (not updated; new adapted version saved as ${r.name}.new)`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
console.log(
|
|
938
|
+
`\n Note: this is EXPECTED on cross-version upgrades (e.g. ${config.installedVersion} → ${newVersion}).`
|
|
939
|
+
);
|
|
940
|
+
console.log(
|
|
941
|
+
` v0.16.10 uses conservative preserve semantics — any file that does not exactly`
|
|
942
|
+
);
|
|
943
|
+
console.log(
|
|
944
|
+
` match the new template's adapted output is preserved, even if you never edited it.`
|
|
945
|
+
);
|
|
946
|
+
console.log(
|
|
947
|
+
` Provenance tracking (v0.17.0) will eliminate these false positives.`
|
|
948
|
+
);
|
|
949
|
+
console.log(`\n If you have NOT customized these files:`);
|
|
950
|
+
console.log(
|
|
951
|
+
` → re-run with --force-template to accept the new template content in bulk`
|
|
952
|
+
);
|
|
953
|
+
console.log(`\n If you HAVE customized these files:`);
|
|
954
|
+
console.log(
|
|
955
|
+
` → diff .sdd-backup/${backupTimestamp}/<path>.new against your file and merge manually`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
554
959
|
console.log(`\nNext: git add -A && git commit -m "chore: upgrade SDD DevFlow to ${newVersion}"\n`);
|
|
555
960
|
}
|
|
556
961
|
|
package/package.json
CHANGED
|
@@ -78,9 +78,9 @@ List every empirical check you executed using this format: `<command> → <obser
|
|
|
78
78
|
|
|
79
79
|
Example format:
|
|
80
80
|
|
|
81
|
-
- `Grep: "
|
|
82
|
-
- `Read:
|
|
83
|
-
- `Grep: "
|
|
81
|
+
- `Grep: "Status" in src/` → 2 hits: `src/domain/order.ts:14`, `src/schemas/enums.ts:8` → both must be updated in the same commit as the migration, listed under "Files to Modify"
|
|
82
|
+
- `Read: prisma/schema.prisma:45-60` → confirmed `id String @id @default(cuid())` → validator uses `z.string().cuid()`, NOT `z.string().uuid()` or `z.number().int()`
|
|
83
|
+
- `Grep: "formatStatusLabel" in src/` → helper does not yet exist → list under "Files to Create" before commits that depend on it
|
|
84
84
|
- (continue with every empirical check)
|
|
85
85
|
|
|
86
86
|
**If this subsection is empty or missing**, prepend the plan with a warning: `⚠ This plan is text-only and has not been empirically verified against the code. Cross-model reviewers MUST run empirical checks before approving.`
|
|
@@ -78,9 +78,9 @@ List every empirical check using this format: `<command> → <observed fact> →
|
|
|
78
78
|
|
|
79
79
|
Example format:
|
|
80
80
|
|
|
81
|
-
- `Grep: "
|
|
82
|
-
- `Read:
|
|
83
|
-
- `Grep: "aria-labelledby" in
|
|
81
|
+
- `Grep: "formatStatusLabel" in src/` → helper exists in `src/shared/format.ts:32` → do not duplicate inline, import from shared, list under "Existing Code to Reuse"
|
|
82
|
+
- `Read: src/schemas/order.ts:40-60` → confirmed `status` field is `z.enum([...])` with 4 variants → UI must handle all branches, listed under "Key Patterns"
|
|
83
|
+
- `Grep: "aria-labelledby" in src/components/` → existing pattern uses `useId()` for hook-generated IDs → reuse same pattern in new component, not hardcoded strings
|
|
84
84
|
- (continue with every empirical check)
|
|
85
85
|
|
|
86
86
|
**If this subsection is empty or missing**, prepend the plan with a warning: `⚠ This plan is text-only and has not been empirically verified against the code. Cross-model reviewers MUST run empirical checks before approving.`
|
|
@@ -45,8 +45,8 @@ Required checks:
|
|
|
45
45
|
|
|
46
46
|
Append to the ticket a final subsection `### Verification commands run`. Use this exact 3-field format per entry: `<command> → <observed fact> → <impact on plan>`. Every entry must have all three fields — a bare command without an observed fact is not verification. Example:
|
|
47
47
|
|
|
48
|
-
- `Grep: "
|
|
49
|
-
- `Read:
|
|
48
|
+
- `Grep: "Status" in src/` → 2 hits (`src/domain/order.ts:14`, `src/schemas/enums.ts:8`) → both must be updated in the same commit as the migration
|
|
49
|
+
- `Read: prisma/schema.prisma:45-60` → `id String @id @default(cuid())` → validator uses `z.string().cuid()`, NOT `z.string().uuid()` or `z.number().int()`
|
|
50
50
|
|
|
51
51
|
If the subsection is empty or missing, prepend the plan with `⚠ This plan is text-only and has not been empirically verified. Cross-model reviewers MUST run empirical checks.`
|
|
52
52
|
|
|
@@ -40,15 +40,15 @@ Before emitting the final plan, verify every structural claim empirically agains
|
|
|
40
40
|
Required checks:
|
|
41
41
|
|
|
42
42
|
1. Grep or read every file you cite — confirm path exists
|
|
43
|
-
2. Before proposing an inline helper, grep
|
|
43
|
+
2. Before proposing an inline helper, grep shared/utility directories for an existing equivalent. Helpers used across features MUST live in a shared location and be imported; do NOT duplicate inline
|
|
44
44
|
3. Read the shared validation schema for any API response the frontend renders. Frontend MUST match the backend contract, not invent fields
|
|
45
45
|
4. Verify CSS tokens and component primitives exist before proposing new classes. Design tokens live in `tailwind.config.ts` or `globals.css`, not in component files
|
|
46
46
|
5. Verify accessibility semantics (`aria-*`, role, labelled-by) against existing accessible components in the codebase
|
|
47
47
|
|
|
48
48
|
Append to the ticket a final subsection `### Verification commands run`. Use this exact 3-field format per entry: `<command> → <observed fact> → <impact on plan>`. Every entry must have all three fields. Example:
|
|
49
49
|
|
|
50
|
-
- `Grep: "
|
|
51
|
-
- `Read:
|
|
50
|
+
- `Grep: "formatStatusLabel" in src/` → helper exists in `src/shared/format.ts:32` → import from shared, do not duplicate
|
|
51
|
+
- `Read: src/schemas/order.ts:40-60` → `status` field is `z.enum([...])` with 4 variants → component handles all branches
|
|
52
52
|
|
|
53
53
|
If empty or missing, prepend plan with `⚠ This plan is text-only and has not been empirically verified. Cross-model reviewers MUST run empirical checks.`
|
|
54
54
|
|
package/template/gitignore
CHANGED
|
@@ -50,3 +50,9 @@ npm-debug.log*
|
|
|
50
50
|
# SDD PM Orchestrator (ephemeral session control files)
|
|
51
51
|
docs/project_notes/pm-session.lock
|
|
52
52
|
docs/project_notes/pm-stop.md
|
|
53
|
+
|
|
54
|
+
# sdd-devflow upgrade backups (ignored — kept locally for recovery only)
|
|
55
|
+
.sdd-backup/
|
|
56
|
+
|
|
57
|
+
# sdd-devflow provenance tracking (local-only, content-addressable hashes)
|
|
58
|
+
.sdd-meta.json
|