create-sdd-project 0.16.10 → 0.17.1
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/lib/adapt-agents.js +121 -29
- package/lib/diff-generator.js +7 -1
- package/lib/doctor.js +166 -0
- package/lib/generator.js +8 -0
- package/lib/init-generator.js +67 -160
- package/lib/meta.js +344 -0
- package/lib/scanner.js +127 -6
- package/lib/stack-adaptations.js +335 -0
- package/lib/upgrade-generator.js +552 -157
- package/package.json +1 -1
- package/template/gitignore +3 -0
package/lib/upgrade-generator.js
CHANGED
|
@@ -12,6 +12,8 @@ const {
|
|
|
12
12
|
const {
|
|
13
13
|
adaptAgentContentForProjectType,
|
|
14
14
|
adaptAgentContentString,
|
|
15
|
+
adaptWorkflowCoreContentForProjectType,
|
|
16
|
+
adaptBaseStandardsContentForProjectType,
|
|
15
17
|
} = require('./adapt-agents');
|
|
16
18
|
const {
|
|
17
19
|
adaptBaseStandards,
|
|
@@ -24,6 +26,23 @@ const {
|
|
|
24
26
|
updateAutonomy,
|
|
25
27
|
regexReplaceInFile,
|
|
26
28
|
} = require('./init-generator');
|
|
29
|
+
// v0.17.0: hash-based smart-diff + shared stack adaptations
|
|
30
|
+
// v0.17.1: + normalizedContentEquals (replaces isStandardModified)
|
|
31
|
+
const {
|
|
32
|
+
readMeta,
|
|
33
|
+
writeMeta,
|
|
34
|
+
computeHash,
|
|
35
|
+
hashFileOnDisk,
|
|
36
|
+
toPosix,
|
|
37
|
+
pruneExpectedAbsent,
|
|
38
|
+
expectedSmartDiffTrackedPaths,
|
|
39
|
+
normalizeForCompare: metaNormalizeForCompare,
|
|
40
|
+
normalizedContentEquals,
|
|
41
|
+
} = require('./meta');
|
|
42
|
+
const {
|
|
43
|
+
applyStackAdaptations,
|
|
44
|
+
applyStackAdaptationsToContent,
|
|
45
|
+
} = require('./stack-adaptations');
|
|
27
46
|
|
|
28
47
|
// --- v0.16.10: backup-before-replace helpers ---
|
|
29
48
|
//
|
|
@@ -57,22 +76,16 @@ function buildBackupTimestamp() {
|
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
/**
|
|
60
|
-
* Normalize text for smart-diff comparison.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
79
|
+
* Normalize text for smart-diff comparison.
|
|
80
|
+
*
|
|
81
|
+
* v0.17.0: delegates to `lib/meta.js` which only strips CR/CRLF (Windows
|
|
82
|
+
* git core.autocrlf compatibility). Trailing whitespace is NO LONGER
|
|
83
|
+
* stripped — that would destroy markdown hard-breaks (two trailing
|
|
84
|
+
* spaces = <br>) and silently wipe legitimate customizations (Gemini M2
|
|
85
|
+
* fix from plan v1.0 review). A local re-export here keeps the old
|
|
86
|
+
* symbol available for any pre-v0.17.0 code paths that still call it.
|
|
66
87
|
*/
|
|
67
|
-
|
|
68
|
-
return text
|
|
69
|
-
.replace(/\r\n/g, '\n')
|
|
70
|
-
.replace(/\r/g, '\n')
|
|
71
|
-
.split('\n')
|
|
72
|
-
.map((l) => l.replace(/[ \t]+$/, ''))
|
|
73
|
-
.join('\n')
|
|
74
|
-
.trim();
|
|
75
|
-
}
|
|
88
|
+
const normalizeForCompare = metaNormalizeForCompare;
|
|
76
89
|
|
|
77
90
|
/**
|
|
78
91
|
* Copy a user file to .sdd-backup/<timestamp>/<relativePath> before it is
|
|
@@ -214,14 +227,6 @@ function collectCustomCommands(dest) {
|
|
|
214
227
|
return customs;
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
/**
|
|
218
|
-
* Check if a standard file has been modified by the user.
|
|
219
|
-
* Compares existing file against freshly generated version.
|
|
220
|
-
*/
|
|
221
|
-
function isStandardModified(existingContent, freshContent) {
|
|
222
|
-
return existingContent.trim() !== freshContent.trim();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
230
|
/**
|
|
226
231
|
* Build the upgrade summary for display.
|
|
227
232
|
*/
|
|
@@ -289,6 +294,48 @@ function generateUpgrade(config) {
|
|
|
289
294
|
// so we can surface the list in the upgrade result summary.
|
|
290
295
|
const modifiedAgentsResults = [];
|
|
291
296
|
|
|
297
|
+
// v0.17.0: provenance tracking. Read existing hashes at the start; track
|
|
298
|
+
// new/updated hashes as we go. Preserved files leave their entry untouched
|
|
299
|
+
// (Codex M1 invariant: only write canonical hashes for tool-written content).
|
|
300
|
+
// `filesToAdapt` collects POSIX paths of files that were replaced or newly
|
|
301
|
+
// written in this run; applyStackAdaptations will be called with this
|
|
302
|
+
// allowlist after the write loop so only these files get re-adapted.
|
|
303
|
+
const meta = readMeta(dest);
|
|
304
|
+
const newHashes = { ...(meta?.hashes ?? {}) };
|
|
305
|
+
const filesToAdapt = new Set();
|
|
306
|
+
|
|
307
|
+
// v0.17.1: before the skills/ wholesale delete-and-copy, save the content
|
|
308
|
+
// of the 6 workflow-core files so we can restore them if their hash tells
|
|
309
|
+
// us they were customized. Map keyed by absolute path, value = string or
|
|
310
|
+
// null (null = file didn't exist before upgrade).
|
|
311
|
+
const workflowCoreBackup = new Map();
|
|
312
|
+
const workflowCorePosixPaths = [];
|
|
313
|
+
{
|
|
314
|
+
const dirsForBackup = [];
|
|
315
|
+
if (aiTools !== 'gemini') dirsForBackup.push('.claude');
|
|
316
|
+
if (aiTools !== 'claude') dirsForBackup.push('.gemini');
|
|
317
|
+
for (const dir of dirsForBackup) {
|
|
318
|
+
for (const relSub of [
|
|
319
|
+
'skills/development-workflow/SKILL.md',
|
|
320
|
+
'skills/development-workflow/references/ticket-template.md',
|
|
321
|
+
'skills/development-workflow/references/merge-checklist.md',
|
|
322
|
+
]) {
|
|
323
|
+
const posix = `${dir}/${relSub}`;
|
|
324
|
+
const abs = path.join(dest, ...posix.split('/'));
|
|
325
|
+
workflowCorePosixPaths.push({ posix, abs });
|
|
326
|
+
if (fs.existsSync(abs)) {
|
|
327
|
+
try {
|
|
328
|
+
workflowCoreBackup.set(abs, fs.readFileSync(abs, 'utf8'));
|
|
329
|
+
} catch {
|
|
330
|
+
workflowCoreBackup.set(abs, null);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
workflowCoreBackup.set(abs, null);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
292
339
|
console.log(`\nUpgrading SDD DevFlow in ${config.projectName}...\n`);
|
|
293
340
|
console.log(` Backup directory: .sdd-backup/${backupTimestamp}/\n`);
|
|
294
341
|
|
|
@@ -359,43 +406,118 @@ function generateUpgrade(config) {
|
|
|
359
406
|
const templateAgentPath = path.join(srcSub, file);
|
|
360
407
|
const existingAgentPath = path.join(destSub, file);
|
|
361
408
|
const relativePath = path.relative(dest, existingAgentPath);
|
|
409
|
+
const posixPath = toPosix(relativePath);
|
|
362
410
|
|
|
363
411
|
const rawTemplate = fs.readFileSync(templateAgentPath, 'utf8');
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
412
|
+
const adaptedCoreTarget = adaptAgentContentString(rawTemplate, file, projectType);
|
|
413
|
+
|
|
414
|
+
// --- v0.17.0 decision tree ---
|
|
415
|
+
//
|
|
416
|
+
// Case 1: file missing or --force-template → unconditional write.
|
|
417
|
+
// Case 2: meta has a hash for this path → hash-based path.
|
|
418
|
+
// 2a. hash matches → pristine, replace with adaptedCoreTarget.
|
|
419
|
+
// 2b. hash mismatches → customized, preserve + .new backup.
|
|
420
|
+
// IMPORTANT (Codex M1): do NOT update newHashes here.
|
|
421
|
+
// Case 3: no meta or no hash for this path → fallback path.
|
|
422
|
+
// 3a. Compute adaptedFullTarget by applying stack adaptations
|
|
423
|
+
// in-memory so init-adapted files don't false-positive
|
|
424
|
+
// (Gemini M1 fix).
|
|
425
|
+
// 3b. Content match → replace.
|
|
426
|
+
// 3c. Content mismatch → preserve + .new backup. Same Codex M1
|
|
427
|
+
// rule: preserved files do NOT get a new hash.
|
|
428
|
+
|
|
429
|
+
if (!fs.existsSync(existingAgentPath)) {
|
|
430
|
+
// Missing — write fresh and track for stack adaptations.
|
|
431
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
432
|
+
filesToAdapt.add(posixPath);
|
|
433
|
+
replaced++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (config.forceTemplate) {
|
|
438
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
439
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
440
|
+
filesToAdapt.add(posixPath);
|
|
441
|
+
replaced++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const existingContent = fs.readFileSync(existingAgentPath, 'utf8');
|
|
446
|
+
const storedHash = meta && meta.hashes[posixPath];
|
|
447
|
+
|
|
448
|
+
const preserveFile = (target) => {
|
|
449
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
450
|
+
const newBackupPath = path.join(
|
|
451
|
+
dest,
|
|
452
|
+
'.sdd-backup',
|
|
453
|
+
backupTimestamp,
|
|
454
|
+
`${relativePath}.new`
|
|
455
|
+
);
|
|
456
|
+
try {
|
|
457
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
458
|
+
fs.writeFileSync(newBackupPath, target, 'utf8');
|
|
459
|
+
} catch (e) {
|
|
460
|
+
console.warn(
|
|
461
|
+
` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`
|
|
378
462
|
);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
463
|
+
}
|
|
464
|
+
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
465
|
+
preserved++;
|
|
466
|
+
// Codex M1 invariant: do NOT update newHashes[posixPath]
|
|
467
|
+
// for preserved files. The existing hash (if any) persists.
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (storedHash) {
|
|
471
|
+
// Case 2: primary hash path.
|
|
472
|
+
const currentHash = computeHash(existingContent);
|
|
473
|
+
if (currentHash === storedHash) {
|
|
474
|
+
// Pristine — replace with core-adapted target. Stack
|
|
475
|
+
// adaptations will be applied via filesToAdapt after the
|
|
476
|
+
// smart-diff loop.
|
|
477
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
478
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
479
|
+
filesToAdapt.add(posixPath);
|
|
480
|
+
replaced++;
|
|
389
481
|
continue;
|
|
390
482
|
}
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
483
|
+
// Hash mismatch → preserve. The .new backup target is the
|
|
484
|
+
// FULL adapted target (core + stack) so the user can diff
|
|
485
|
+
// apples to apples against their customized file.
|
|
486
|
+
const adaptedFullTarget = applyStackAdaptationsToContent(
|
|
487
|
+
adaptedCoreTarget,
|
|
488
|
+
posixPath,
|
|
489
|
+
scan,
|
|
490
|
+
config
|
|
491
|
+
);
|
|
492
|
+
preserveFile(adaptedFullTarget);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Case 3: fallback path — no hash available. Compare against
|
|
497
|
+
// the FULL adapted target (core + stack) so init-adapted files
|
|
498
|
+
// from pre-v0.17.0 projects don't false-positive (Gemini M1).
|
|
499
|
+
const adaptedFullTargetFallback = applyStackAdaptationsToContent(
|
|
500
|
+
adaptedCoreTarget,
|
|
501
|
+
posixPath,
|
|
502
|
+
scan,
|
|
503
|
+
config
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
normalizeForCompare(existingContent) ===
|
|
508
|
+
normalizeForCompare(adaptedFullTargetFallback)
|
|
509
|
+
) {
|
|
510
|
+
// Pristine per content compare — replace with core target.
|
|
511
|
+
// Stack adaptations run after the loop to finalize the file.
|
|
395
512
|
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
513
|
+
fs.writeFileSync(existingAgentPath, adaptedCoreTarget, 'utf8');
|
|
514
|
+
filesToAdapt.add(posixPath);
|
|
515
|
+
replaced++;
|
|
516
|
+
continue;
|
|
396
517
|
}
|
|
397
|
-
|
|
398
|
-
|
|
518
|
+
|
|
519
|
+
// Content mismatch → preserve. Same rule: no hash update.
|
|
520
|
+
preserveFile(adaptedFullTargetFallback);
|
|
399
521
|
}
|
|
400
522
|
continue;
|
|
401
523
|
}
|
|
@@ -473,67 +595,266 @@ function generateUpgrade(config) {
|
|
|
473
595
|
preserved++;
|
|
474
596
|
}
|
|
475
597
|
|
|
476
|
-
// ---
|
|
477
|
-
|
|
598
|
+
// --- c2) v0.17.1: workflow-core smart-diff protection ---
|
|
599
|
+
//
|
|
600
|
+
// The 6 development-workflow files (SKILL.md + ticket-template.md +
|
|
601
|
+
// merge-checklist.md, × 2 tools) were just wholesale-copied by the
|
|
602
|
+
// skills/ delete-and-replace at step (b). Without this block, user
|
|
603
|
+
// customizations to the core workflow files (ticket templates, merge
|
|
604
|
+
// checklists, SKILL.md definitions) would be silently lost every
|
|
605
|
+
// upgrade. Check each against its pre-upgrade backup (captured before
|
|
606
|
+
// step (b)) via the hash decision tree; restore the backup if the user
|
|
607
|
+
// had customized it, otherwise leave the template version in place.
|
|
608
|
+
for (const { posix, abs } of workflowCorePosixPaths) {
|
|
609
|
+
const backup = workflowCoreBackup.get(abs);
|
|
610
|
+
const relativePath = path.relative(dest, abs);
|
|
611
|
+
|
|
612
|
+
if (backup === null || backup === undefined) {
|
|
613
|
+
// File did not exist pre-upgrade (first install or user deleted it)
|
|
614
|
+
// → leave the freshly-copied template version in place.
|
|
615
|
+
if (fs.existsSync(abs)) {
|
|
616
|
+
filesToAdapt.add(posix);
|
|
617
|
+
replaced++;
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
478
621
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'base-standards.mdc'), 'utf8');
|
|
484
|
-
const fresh = adaptBaseStandards(template, scan, config);
|
|
485
|
-
if (isStandardModified(existing, fresh)) {
|
|
486
|
-
standardsResults.push({ name: 'ai-specs/specs/base-standards.mdc', modified: true });
|
|
487
|
-
preserved++;
|
|
488
|
-
} else {
|
|
489
|
-
fs.writeFileSync(baseStdPath, fresh, 'utf8');
|
|
490
|
-
standardsResults.push({ name: 'ai-specs/specs/base-standards.mdc', modified: false });
|
|
622
|
+
if (config.forceTemplate) {
|
|
623
|
+
// --force-template: keep the fresh template, back up the old content.
|
|
624
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
625
|
+
filesToAdapt.add(posix);
|
|
491
626
|
replaced++;
|
|
627
|
+
continue;
|
|
492
628
|
}
|
|
493
|
-
}
|
|
494
629
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
630
|
+
const freshContent = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : null;
|
|
631
|
+
if (freshContent === null) {
|
|
632
|
+
// Template no longer ships this file (should not happen in v0.17.1)
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const storedHash = meta && meta.hashes[posix];
|
|
637
|
+
const backupHash = computeHash(backup);
|
|
638
|
+
|
|
639
|
+
if (storedHash) {
|
|
640
|
+
// Case 2: hash-based path.
|
|
641
|
+
if (backupHash === storedHash) {
|
|
642
|
+
// Pristine → keep the fresh copy, back up the pre-upgrade version.
|
|
643
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
644
|
+
filesToAdapt.add(posix);
|
|
508
645
|
replaced++;
|
|
646
|
+
continue;
|
|
509
647
|
}
|
|
648
|
+
// Hash mismatch → customized → restore backup + write .new with
|
|
649
|
+
// adapted target so user can diff against canonical v0.17.1 output.
|
|
650
|
+
// v0.17.1 round-3: the .new backup must mirror what init would have
|
|
651
|
+
// produced (stack rules + project-type rules), so the user can diff
|
|
652
|
+
// apples-to-apples against their customized file.
|
|
653
|
+
const stackTarget = applyStackAdaptationsToContent(freshContent, posix, scan, config);
|
|
654
|
+
const adaptedTarget = adaptWorkflowCoreContentForProjectType(stackTarget, posix, projectType);
|
|
655
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
656
|
+
fs.writeFileSync(abs, backup, 'utf8');
|
|
657
|
+
const newBackupPath = path.join(
|
|
658
|
+
dest,
|
|
659
|
+
'.sdd-backup',
|
|
660
|
+
backupTimestamp,
|
|
661
|
+
`${relativePath}.new`
|
|
662
|
+
);
|
|
663
|
+
try {
|
|
664
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
665
|
+
fs.writeFileSync(newBackupPath, adaptedTarget, 'utf8');
|
|
666
|
+
} catch (e) {
|
|
667
|
+
console.warn(` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`);
|
|
668
|
+
}
|
|
669
|
+
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
670
|
+
preserved++;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Case 3: fallback path — no stored hash (pre-v0.17.1 project). Compare
|
|
675
|
+
// backup against the FULLY adapted template target (stack rules +
|
|
676
|
+
// project-type rules) OR the raw template content. Both are valid
|
|
677
|
+
// "pristine" states for a pre-v0.17.1 project:
|
|
678
|
+
// (a) adapted: --init ran applyStackAdaptations AND
|
|
679
|
+
// adaptAgentContentForProjectType at install time (full adapted)
|
|
680
|
+
// (b) raw: generator.js scaffold copied template without running adapters
|
|
681
|
+
// v0.17.1 round-3: BOTH stack rules and project-type rules must be
|
|
682
|
+
// applied to the comparison target. Previously only stack rules were
|
|
683
|
+
// applied, which caused false-positive preserve on single-stack projects
|
|
684
|
+
// (scenario 55 regression guard).
|
|
685
|
+
const stackTarget = applyStackAdaptationsToContent(freshContent, posix, scan, config);
|
|
686
|
+
const adaptedTarget = adaptWorkflowCoreContentForProjectType(stackTarget, posix, projectType);
|
|
687
|
+
if (
|
|
688
|
+
normalizedContentEquals(backup, adaptedTarget) ||
|
|
689
|
+
normalizedContentEquals(backup, freshContent)
|
|
690
|
+
) {
|
|
691
|
+
// Pristine per content compare → keep the fresh copy.
|
|
692
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
693
|
+
filesToAdapt.add(posix);
|
|
694
|
+
replaced++;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
// Customized → restore backup + write .new with adapted target.
|
|
698
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
699
|
+
fs.writeFileSync(abs, backup, 'utf8');
|
|
700
|
+
const newBackupPath = path.join(
|
|
701
|
+
dest,
|
|
702
|
+
'.sdd-backup',
|
|
703
|
+
backupTimestamp,
|
|
704
|
+
`${relativePath}.new`
|
|
705
|
+
);
|
|
706
|
+
try {
|
|
707
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
708
|
+
fs.writeFileSync(newBackupPath, adaptedTarget, 'utf8');
|
|
709
|
+
} catch (e) {
|
|
710
|
+
console.warn(` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`);
|
|
510
711
|
}
|
|
712
|
+
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
713
|
+
preserved++;
|
|
511
714
|
}
|
|
512
715
|
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
716
|
+
// --- d) Handle standards (smart diff) ---
|
|
717
|
+
const standardsResults = [];
|
|
718
|
+
|
|
719
|
+
// v0.17.1: hash decision tree for all 4 standards.
|
|
720
|
+
//
|
|
721
|
+
// Each standard uses its own adapter (adaptBaseStandards, adaptBackendStandards,
|
|
722
|
+
// adaptFrontendStandards, or — for documentation-standards — the imperative
|
|
723
|
+
// branch in applyStackAdaptationsToContent). The decision tree is uniform:
|
|
724
|
+
// 1. Missing or --force-template → unconditional write + hash update
|
|
725
|
+
// 2. Stored hash match → pristine, replace + hash update
|
|
726
|
+
// 3. Stored hash mismatch → customized, preserve + .new backup, NO hash update
|
|
727
|
+
// 4. No stored hash → fallback compare against adapted target (normalizedContentEquals)
|
|
728
|
+
//
|
|
729
|
+
// Replaces v0.17.0's isStandardModified main-path compare (deleted in this commit).
|
|
730
|
+
// Standards are added to filesToAdapt when replaced, so their hashes get
|
|
731
|
+
// computed at the end of the upgrade via the filesToAdapt loop.
|
|
732
|
+
const standardsSpecs = [
|
|
733
|
+
{
|
|
734
|
+
posix: 'ai-specs/specs/base-standards.mdc',
|
|
735
|
+
relTemplate: ['ai-specs', 'specs', 'base-standards.mdc'],
|
|
736
|
+
// v0.17.1 round-3: init applies project-type rules via
|
|
737
|
+
// adaptAgentContentForProjectType AFTER adaptBaseStandards, so the
|
|
738
|
+
// comparison target must include both layers to avoid false-positive
|
|
739
|
+
// preserve on single-stack upgrades (scenario 55 regression guard).
|
|
740
|
+
adapter: (tpl) => adaptBaseStandardsContentForProjectType(
|
|
741
|
+
adaptBaseStandards(tpl, scan, config),
|
|
742
|
+
projectType
|
|
743
|
+
),
|
|
744
|
+
include: true,
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
posix: 'ai-specs/specs/backend-standards.mdc',
|
|
748
|
+
relTemplate: ['ai-specs', 'specs', 'backend-standards.mdc'],
|
|
749
|
+
adapter: (tpl) => adaptBackendStandards(tpl, scan),
|
|
750
|
+
include: projectType !== 'frontend',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
posix: 'ai-specs/specs/frontend-standards.mdc',
|
|
754
|
+
relTemplate: ['ai-specs', 'specs', 'frontend-standards.mdc'],
|
|
755
|
+
adapter: (tpl) => adaptFrontendStandards(tpl, scan),
|
|
756
|
+
include: projectType !== 'backend',
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
posix: 'ai-specs/specs/documentation-standards.mdc',
|
|
760
|
+
relTemplate: ['ai-specs', 'specs', 'documentation-standards.mdc'],
|
|
761
|
+
// documentation-standards has no dedicated adapter; stack-adaptations.js
|
|
762
|
+
// handles project-type pruning via its imperative branch at the end of
|
|
763
|
+
// applyStackAdaptations. For the adapted target used in fallback
|
|
764
|
+
// content-compare here, we apply it in-memory via
|
|
765
|
+
// applyStackAdaptationsToContent (same helper, different invocation).
|
|
766
|
+
adapter: (tpl) => applyStackAdaptationsToContent(tpl, 'ai-specs/specs/documentation-standards.mdc', scan, config),
|
|
767
|
+
include: true,
|
|
768
|
+
},
|
|
769
|
+
];
|
|
770
|
+
|
|
771
|
+
for (const spec of standardsSpecs) {
|
|
772
|
+
if (!spec.include) continue;
|
|
773
|
+
const absPath = path.join(dest, ...spec.posix.split('/'));
|
|
774
|
+
const templatePath = path.join(templateDir, ...spec.relTemplate);
|
|
775
|
+
if (!fs.existsSync(templatePath)) continue;
|
|
776
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
777
|
+
const freshAdapted = spec.adapter(template);
|
|
778
|
+
const relativePath = path.relative(dest, absPath);
|
|
779
|
+
|
|
780
|
+
if (!fs.existsSync(absPath)) {
|
|
781
|
+
// Missing → unconditional write
|
|
782
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
783
|
+
fs.writeFileSync(absPath, freshAdapted, 'utf8');
|
|
784
|
+
filesToAdapt.add(spec.posix);
|
|
785
|
+
standardsResults.push({ name: spec.posix, modified: false });
|
|
786
|
+
replaced++;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (config.forceTemplate) {
|
|
791
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
792
|
+
fs.writeFileSync(absPath, freshAdapted, 'utf8');
|
|
793
|
+
filesToAdapt.add(spec.posix);
|
|
794
|
+
standardsResults.push({ name: spec.posix, modified: false });
|
|
795
|
+
replaced++;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const existing = fs.readFileSync(absPath, 'utf8');
|
|
800
|
+
const storedHash = meta && meta.hashes[spec.posix];
|
|
801
|
+
|
|
802
|
+
const preserveStandard = () => {
|
|
803
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
804
|
+
const newBackupPath = path.join(dest, '.sdd-backup', backupTimestamp, `${relativePath}.new`);
|
|
805
|
+
try {
|
|
806
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
807
|
+
fs.writeFileSync(newBackupPath, freshAdapted, 'utf8');
|
|
808
|
+
} catch (e) {
|
|
809
|
+
console.warn(` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`);
|
|
810
|
+
}
|
|
811
|
+
standardsResults.push({ name: spec.posix, modified: true });
|
|
812
|
+
preserved++;
|
|
813
|
+
// Codex M1 invariant: do NOT update newHashes[spec.posix] — the
|
|
814
|
+
// inherited hash (if any) persists untouched for preserved files.
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
if (storedHash) {
|
|
818
|
+
// Case 2: hash-based path.
|
|
819
|
+
const currentHash = computeHash(existing);
|
|
820
|
+
if (currentHash === storedHash) {
|
|
821
|
+
// Pristine → replace with adapted target.
|
|
822
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
823
|
+
fs.writeFileSync(absPath, freshAdapted, 'utf8');
|
|
824
|
+
filesToAdapt.add(spec.posix);
|
|
825
|
+
standardsResults.push({ name: spec.posix, modified: false });
|
|
526
826
|
replaced++;
|
|
827
|
+
continue;
|
|
527
828
|
}
|
|
829
|
+
// Hash mismatch → preserve.
|
|
830
|
+
preserveStandard();
|
|
831
|
+
continue;
|
|
528
832
|
}
|
|
529
|
-
}
|
|
530
833
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
834
|
+
// Case 3: fallback content-compare (no stored hash, pre-v0.17.1 project).
|
|
835
|
+
//
|
|
836
|
+
// Two acceptable "pristine" states for a pre-v0.17.1 project:
|
|
837
|
+
// (a) existing matches the adapted target — the --init path ran the
|
|
838
|
+
// adapter at install time (existing is adapted content)
|
|
839
|
+
// (b) existing matches the RAW template content — the generator.js
|
|
840
|
+
// scaffold path copied the template without running the adapter
|
|
841
|
+
// (existing is raw content)
|
|
842
|
+
//
|
|
843
|
+
// Without case (b), every fresh-scaffolded v0.17.0 project upgrading to
|
|
844
|
+
// v0.17.1 would false-positive-preserve all 4 standards on first upgrade
|
|
845
|
+
// (the template content on disk would not match the adapter output).
|
|
846
|
+
if (
|
|
847
|
+
normalizedContentEquals(existing, freshAdapted) ||
|
|
848
|
+
normalizedContentEquals(existing, template)
|
|
849
|
+
) {
|
|
850
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
851
|
+
fs.writeFileSync(absPath, freshAdapted, 'utf8');
|
|
852
|
+
filesToAdapt.add(spec.posix);
|
|
853
|
+
standardsResults.push({ name: spec.posix, modified: false });
|
|
854
|
+
replaced++;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
preserveStandard();
|
|
537
858
|
}
|
|
538
859
|
|
|
539
860
|
step('Updated standards files');
|
|
@@ -545,42 +866,72 @@ function generateUpgrade(config) {
|
|
|
545
866
|
}
|
|
546
867
|
|
|
547
868
|
// --- e) Replace top-level configs ---
|
|
548
|
-
// AGENTS.md — v0.16.10
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
869
|
+
// AGENTS.md — hash-based smart-diff (v0.17.0 upgrade of v0.16.10 Change #3).
|
|
870
|
+
//
|
|
871
|
+
// Decision tree identical to the template-agent loop above:
|
|
872
|
+
// 1. Missing or --force-template → unconditional write.
|
|
873
|
+
// 2. meta has a hash for AGENTS.md → hash-based path:
|
|
874
|
+
// 2a. hash match → pristine, replace.
|
|
875
|
+
// 2b. hash mismatch → preserve + .new backup. Codex M1 invariant:
|
|
876
|
+
// do NOT update newHashes['AGENTS.md'].
|
|
877
|
+
// 3. No hash → fallback content compare against the full adapted
|
|
878
|
+
// target. AGENTS.md has no stack adaptations (adaptAgentsMd already
|
|
879
|
+
// includes project-type pruning), so the comparison target is the
|
|
880
|
+
// adaptAgentsMd output itself.
|
|
553
881
|
const agentsMdTemplate = fs.readFileSync(path.join(templateDir, 'AGENTS.md'), 'utf8');
|
|
554
882
|
const adaptedAgentsMd = adaptAgentsMd(agentsMdTemplate, config, scan);
|
|
555
883
|
const agentsMdDestPath = path.join(dest, 'AGENTS.md');
|
|
884
|
+
const AGENTS_MD_POSIX = 'AGENTS.md';
|
|
885
|
+
|
|
886
|
+
const preserveAgentsMd = () => {
|
|
887
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
888
|
+
const newBackupPath = path.join(dest, '.sdd-backup', backupTimestamp, 'AGENTS.md.new');
|
|
889
|
+
try {
|
|
890
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
891
|
+
fs.writeFileSync(newBackupPath, adaptedAgentsMd, 'utf8');
|
|
892
|
+
} catch (e) {
|
|
893
|
+
console.warn(` ⚠ Failed to write .new backup for AGENTS.md: ${e.code || e.message}`);
|
|
894
|
+
}
|
|
895
|
+
modifiedAgentsResults.push({ name: 'AGENTS.md', modified: true });
|
|
896
|
+
preserved++;
|
|
897
|
+
// Codex M1 invariant: do NOT update newHashes[AGENTS_MD_POSIX].
|
|
898
|
+
};
|
|
556
899
|
|
|
557
|
-
if (fs.existsSync(agentsMdDestPath)
|
|
900
|
+
if (!fs.existsSync(agentsMdDestPath)) {
|
|
901
|
+
// Missing — write and hash fresh.
|
|
902
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
903
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
904
|
+
replaced++;
|
|
905
|
+
} else if (config.forceTemplate) {
|
|
906
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
907
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
908
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
909
|
+
replaced++;
|
|
910
|
+
} else {
|
|
558
911
|
const existingAgentsMd = fs.readFileSync(agentsMdDestPath, 'utf8');
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
fs.writeFileSync(
|
|
566
|
-
|
|
567
|
-
|
|
912
|
+
const storedAgentsMdHash = meta && meta.hashes[AGENTS_MD_POSIX];
|
|
913
|
+
|
|
914
|
+
if (storedAgentsMdHash) {
|
|
915
|
+
const currentHash = computeHash(existingAgentsMd);
|
|
916
|
+
if (currentHash === storedAgentsMdHash) {
|
|
917
|
+
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
918
|
+
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
919
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
920
|
+
replaced++;
|
|
921
|
+
} else {
|
|
922
|
+
preserveAgentsMd();
|
|
568
923
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
//
|
|
924
|
+
} else if (
|
|
925
|
+
normalizeForCompare(existingAgentsMd) === normalizeForCompare(adaptedAgentsMd)
|
|
926
|
+
) {
|
|
927
|
+
// Fallback content-compare.
|
|
573
928
|
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
574
929
|
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
930
|
+
newHashes[AGENTS_MD_POSIX] = computeHash(adaptedAgentsMd);
|
|
575
931
|
replaced++;
|
|
932
|
+
} else {
|
|
933
|
+
preserveAgentsMd();
|
|
576
934
|
}
|
|
577
|
-
} else {
|
|
578
|
-
// Missing file, or --force-template: always back up (if exists) and overwrite
|
|
579
|
-
if (fs.existsSync(agentsMdDestPath)) {
|
|
580
|
-
backupBeforeReplace(dest, 'AGENTS.md', backupTimestamp);
|
|
581
|
-
}
|
|
582
|
-
fs.writeFileSync(agentsMdDestPath, adaptedAgentsMd, 'utf8');
|
|
583
|
-
replaced++;
|
|
584
935
|
}
|
|
585
936
|
|
|
586
937
|
// CLAUDE.md / GEMINI.md (back up before replace, not smart-diff'd)
|
|
@@ -640,17 +991,33 @@ function generateUpgrade(config) {
|
|
|
640
991
|
replaced++;
|
|
641
992
|
}
|
|
642
993
|
|
|
643
|
-
// --- e3) .gitignore — idempotent append of .sdd-backup/ (v0.16.10)
|
|
644
|
-
//
|
|
645
|
-
//
|
|
994
|
+
// --- e3) .gitignore — idempotent append of .sdd-backup/ (v0.16.10)
|
|
995
|
+
// and .sdd-meta.json (v0.17.0) ---
|
|
996
|
+
// Existing projects created before these versions don't have the
|
|
997
|
+
// entries in their .gitignore. Append them once so the files aren't
|
|
998
|
+
// accidentally committed.
|
|
646
999
|
const userGitignorePath = path.join(dest, '.gitignore');
|
|
647
1000
|
if (fs.existsSync(userGitignorePath)) {
|
|
648
|
-
|
|
1001
|
+
let existingGitignore = fs.readFileSync(userGitignorePath, 'utf8');
|
|
1002
|
+
let updatedGitignore = false;
|
|
1003
|
+
|
|
649
1004
|
if (!/^\s*\/?\.sdd-backup\/?\s*$/m.test(existingGitignore)) {
|
|
650
1005
|
const appendBlock = '\n\n# sdd-devflow upgrade backups (ignored — kept locally for recovery only)\n.sdd-backup/\n';
|
|
651
|
-
|
|
1006
|
+
existingGitignore = existingGitignore.trimEnd() + appendBlock;
|
|
1007
|
+
updatedGitignore = true;
|
|
652
1008
|
step('Updated .gitignore with .sdd-backup/ entry');
|
|
653
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
if (!/^\s*\/?\.sdd-meta\.json\s*$/m.test(existingGitignore)) {
|
|
1012
|
+
const appendBlock = '\n\n# sdd-devflow provenance tracking (local-only, content-addressable hashes)\n.sdd-meta.json\n';
|
|
1013
|
+
existingGitignore = existingGitignore.trimEnd() + appendBlock;
|
|
1014
|
+
updatedGitignore = true;
|
|
1015
|
+
step('Updated .gitignore with .sdd-meta.json entry');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (updatedGitignore) {
|
|
1019
|
+
fs.writeFileSync(userGitignorePath, existingGitignore, 'utf8');
|
|
1020
|
+
}
|
|
654
1021
|
}
|
|
655
1022
|
|
|
656
1023
|
// --- f) Adapt for project type ---
|
|
@@ -673,30 +1040,30 @@ function generateUpgrade(config) {
|
|
|
673
1040
|
}
|
|
674
1041
|
}
|
|
675
1042
|
|
|
676
|
-
// Adapt agent/skill content for project type
|
|
1043
|
+
// Adapt agent/skill content for project type (single-stack pruning —
|
|
1044
|
+
// removes frontend/backend refs). Separate from stack substitutions
|
|
1045
|
+
// (Zod/ORM/DDD). Safe to run on all files because the pruning rules
|
|
1046
|
+
// use literal template strings that only appear in raw template.
|
|
677
1047
|
if (projectType !== 'fullstack') {
|
|
678
1048
|
adaptAgentContentForProjectType(dest, config, regexReplaceInFile);
|
|
679
1049
|
}
|
|
680
1050
|
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
]);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
1051
|
+
// v0.17.0: Stack adaptations run ONLY on files that were replaced or
|
|
1052
|
+
// newly written in this run. Preserved (customized) files MUST NOT be
|
|
1053
|
+
// touched by stack adaptations, otherwise their user edits could be
|
|
1054
|
+
// mangled by the rule replacements (Codex M1 + plan v1.1 § Allowlist
|
|
1055
|
+
// semantics).
|
|
1056
|
+
//
|
|
1057
|
+
// v0.17.1: SKILL.md, ticket-template.md, and documentation-standards.mdc
|
|
1058
|
+
// are now smart-diff-protected (c2 block and standards block respectively)
|
|
1059
|
+
// and ARE added to filesToAdapt conditionally on being replaced (NOT
|
|
1060
|
+
// preserved). The pre-v0.17.1 unconditional adds that used to live here
|
|
1061
|
+
// were removed because they violated the Codex M1 invariant when the
|
|
1062
|
+
// new smart-diff blocks preserved a customized file (Gemini round-3
|
|
1063
|
+
// finding 1): the unconditional add would re-apply stack adaptations
|
|
1064
|
+
// to the restored user content, mangling it, and then the hash loop
|
|
1065
|
+
// would overwrite the preserved hash.
|
|
1066
|
+
applyStackAdaptations(dest, scan, config, filesToAdapt);
|
|
700
1067
|
|
|
701
1068
|
step('Adapted files for project type and stack');
|
|
702
1069
|
|
|
@@ -732,6 +1099,32 @@ function generateUpgrade(config) {
|
|
|
732
1099
|
fs.writeFileSync(path.join(dest, '.sdd-version'), newVersion + '\n', 'utf8');
|
|
733
1100
|
step(`Updated .sdd-version to ${newVersion}`);
|
|
734
1101
|
|
|
1102
|
+
// --- g1) v0.17.0: update .sdd-meta.json ---
|
|
1103
|
+
//
|
|
1104
|
+
// For every smart-diff-tracked file that was replaced or newly written
|
|
1105
|
+
// in this run (i.e. in filesToAdapt AND in the expected tracked set),
|
|
1106
|
+
// recompute its hash from the post-adaptation on-disk content and merge
|
|
1107
|
+
// into newHashes. Preserved files are NOT in filesToAdapt, so their old
|
|
1108
|
+
// hash (if any) is left alone — Codex M1 invariant.
|
|
1109
|
+
//
|
|
1110
|
+
// Then prune hashes for paths that are no longer expected for this
|
|
1111
|
+
// (aiTools, projectType) combination (e.g. single-stack removed a
|
|
1112
|
+
// frontend agent). User-deleted files that ARE expected keep their
|
|
1113
|
+
// hash, since the next upgrade will recreate the file from template.
|
|
1114
|
+
{
|
|
1115
|
+
const trackedSet = expectedSmartDiffTrackedPaths(aiTools, projectType);
|
|
1116
|
+
for (const posixPath of filesToAdapt) {
|
|
1117
|
+
if (!trackedSet.has(posixPath)) continue;
|
|
1118
|
+
const absPath = path.join(dest, ...posixPath.split('/'));
|
|
1119
|
+
const h = hashFileOnDisk(absPath);
|
|
1120
|
+
if (h !== null) {
|
|
1121
|
+
newHashes[posixPath] = h;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const prunedHashes = pruneExpectedAbsent(newHashes, aiTools, projectType);
|
|
1125
|
+
writeMeta(dest, prunedHashes);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
735
1128
|
// --- Show result ---
|
|
736
1129
|
const updatedCount = standardsResults.filter((s) => !s.modified).length;
|
|
737
1130
|
const preservedCount = modifiedStandards.length;
|
|
@@ -766,16 +1159,19 @@ function generateUpgrade(config) {
|
|
|
766
1159
|
);
|
|
767
1160
|
}
|
|
768
1161
|
console.log(
|
|
769
|
-
`\n Note: this is
|
|
1162
|
+
`\n Note: this is expected on the first v0.17.0+ upgrade from a pre-v0.17.0 project`
|
|
1163
|
+
);
|
|
1164
|
+
console.log(
|
|
1165
|
+
` for files the user had not touched — the fallback content-compare path is`
|
|
770
1166
|
);
|
|
771
1167
|
console.log(
|
|
772
|
-
`
|
|
1168
|
+
` conservative by design. After this upgrade, .sdd-meta.json records the hash of`
|
|
773
1169
|
);
|
|
774
1170
|
console.log(
|
|
775
|
-
`
|
|
1171
|
+
` every SDD-managed file. Subsequent upgrades will use hash-based precision and`
|
|
776
1172
|
);
|
|
777
1173
|
console.log(
|
|
778
|
-
`
|
|
1174
|
+
` will only warn on files the user actually edited.`
|
|
779
1175
|
);
|
|
780
1176
|
console.log(`\n If you have NOT customized these files:`);
|
|
781
1177
|
console.log(
|
|
@@ -803,6 +1199,5 @@ module.exports = {
|
|
|
803
1199
|
readAutonomyLevel,
|
|
804
1200
|
collectCustomAgents,
|
|
805
1201
|
collectCustomCommands,
|
|
806
|
-
isStandardModified,
|
|
807
1202
|
buildSummary,
|
|
808
1203
|
};
|