sdg-agents 1.10.0 → 1.10.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdg-agents",
3
- "version": "1.10.0",
3
+ "version": "1.10.4",
4
4
  "description": "Structured working protocol and engineering rules for AI agents. Works with Claude Code, Antigravity, Codex, and others.",
5
5
  "type": "module",
6
6
  "main": "./src/engine/bin/index.mjs",
@@ -61,13 +61,17 @@ If a lint script exists (`lint`, `lint:fix`, `lint:all`, or a config file is det
61
61
  - If non-auto-fixable violations remain, surface them explicitly.
62
62
  - Block commit if errors remain.
63
63
 
64
- ## Step 8 — COMMIT
64
+ ## Step 8 — COMMIT & RELEASE
65
65
 
66
66
  Propose the commit message and **WAIT** for explicit Developer approval before committing.
67
67
 
68
+ - **Option A (Manual)**: If you just want to save progress without a version bump, commit with `feat:` or `fix:`. Remember you **MUST** have content in `[Unreleased]`.
69
+ - **Option B (End Cycle)**: If the cycle is complete, run `npm run bump <fix|feat|major>` to consolidate the narrative and bump the version.
70
+
68
71
  ## Step 9 — PUSH
69
72
 
70
73
  **ASK** for explicit permission before pushing to remote.
74
+ The `pre-push` hook will **BLOCK** the push if any `[Unreleased]` narrative remains (preventing unversioned leaks to main).
71
75
 
72
76
  ---
73
77
 
@@ -14,71 +14,82 @@ const PROJECT_ROOT = process.cwd();
14
14
  const CHANGELOG_PATH = path.join(PROJECT_ROOT, 'CHANGELOG.md');
15
15
 
16
16
  async function run() {
17
- const commitMsgFile = process.argv[2];
18
- if (!commitMsgFile) {
19
- console.error(' ❌ Error: No commit message file provided.');
20
- process.exit(1);
21
- }
17
+ const isPrePush = process.argv.includes('--pre-push');
18
+ const commitMsgFile = isPrePush ? null : process.argv[2];
22
19
 
23
- if (!fs.existsSync(commitMsgFile)) {
24
- console.error(` ❌ Error: Commit message file not found at ${commitMsgFile}`);
25
- process.exit(1);
26
- }
20
+ if (!isPrePush) {
21
+ if (!commitMsgFile) {
22
+ console.error(' ❌ Error: No commit message file provided.');
23
+ process.exit(1);
24
+ }
25
+ if (!fs.existsSync(commitMsgFile)) {
26
+ console.error(` ❌ Error: Commit message file not found at ${commitMsgFile}`);
27
+ process.exit(1);
28
+ }
27
29
 
28
- const commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
29
- const firstLine = commitMsg.split('\n')[0].trim();
30
+ const commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
31
+ const firstLine = commitMsg.split('\n')[0].trim();
30
32
 
31
- // ONLY target feat: and fix: (SDG Cycle Triggers) as per maintainer instruction
32
- const isSdgTrigger = /^feat:/.test(firstLine) || /^fix:/.test(firstLine);
33
- if (!isSdgTrigger) {
34
- process.exit(0);
33
+ // ONLY target feat: and fix: (SDG Cycle Triggers) as per maintainer instruction
34
+ const isSdgTrigger = /^feat:/.test(firstLine) || /^fix:/.test(firstLine);
35
+ if (!isSdgTrigger) {
36
+ process.exit(0);
37
+ }
35
38
  }
36
39
 
37
40
  let changelog = '';
38
41
  try {
39
- // Read STAGED version of CHANGELOG.md to prevent issues with lint-staged or stashing
42
+ // Read STAGED version of CHANGELOG.md (if possible)
40
43
  changelog = execSync('git show :CHANGELOG.md', {
41
44
  stdio: ['pipe', 'pipe', 'ignore'],
42
45
  }).toString();
43
46
  } catch {
44
- // Fallback to disk if not in index (shouldn't happen in a commit hook for a tracked file)
45
47
  if (fs.existsSync(CHANGELOG_PATH)) {
46
48
  changelog = fs.readFileSync(CHANGELOG_PATH, 'utf8');
47
49
  }
48
50
  }
49
51
 
50
52
  if (!changelog) {
51
- console.warn(' ⚠️ Warning: CHANGELOG.md not found or empty. Skipping narrative check.');
52
53
  process.exit(0);
53
54
  }
54
55
 
55
- // Extract content between [Unreleased] and the next version header (## [...) or end of file
56
- // We use (?=\n##\s) to match the next level-2 header specifically, avoiding matches on ### sub-headers.
57
56
  const unreleasedMatch = changelog.match(
58
57
  /##\s*\[Unreleased\].*?\n([\s\S]*?)(?=\n##\s|(?:\n){0,1}$)/i
59
58
  );
60
59
 
61
60
  if (!unreleasedMatch) {
61
+ if (isPrePush) process.exit(0);
62
62
  console.error('\n ❌ NARRATIVE VIOLATION: "## [Unreleased]" section missing in CHANGELOG.md.');
63
- console.error(' SDG cycles (feat/fix) MUST have a technical narrative before committing.\n');
64
63
  process.exit(1);
65
64
  }
66
65
 
67
66
  const narrative = unreleasedMatch[1]
68
67
  .replace(/###\s*(Added|Fixed|Changed|Removed|Security|Deprecated)/gi, '')
69
- .replace(/<!--[\s\S]*?-->/g, '') // Remove markdown comments
68
+ .replace(/<!--[\s\S]*?-->/g, '')
70
69
  .trim();
71
70
 
72
- // If the narrative is just whitespace or empty after removing headers/comments
73
- if (!narrative || narrative.length < 3) {
74
- console.error('\n ❌ NARRATIVE VIOLATION: The [Unreleased] section is empty.');
75
- console.error(' You are committing a "feat:" or "fix:" which triggers a version bump.');
76
- console.error(' Please document your changes in CHANGELOG.md under [Unreleased] first.\n');
77
- process.exit(1);
78
- }
71
+ const isNarrativeEmpty = !narrative || narrative.length < 3;
79
72
 
80
- console.log(' ✅ Narrative Guard: CHANGELOG.md validated.');
81
- process.exit(0);
73
+ if (isPrePush) {
74
+ if (!isNarrativeEmpty) {
75
+ console.error('\n ❌ DELIVERY BLOCKED: Unreleased narrative detected.');
76
+ console.error(' You have documented changes in CHANGELOG.md that have not been versioned.');
77
+ console.error(
78
+ ' Action: Run "npm run bump <fix|feat>" to finalize the cycle before pushing.\n'
79
+ );
80
+ process.exit(1);
81
+ }
82
+ process.exit(0);
83
+ } else {
84
+ // commit-msg mode
85
+ if (isNarrativeEmpty) {
86
+ console.error('\n ❌ NARRATIVE VIOLATION: The [Unreleased] section is empty.');
87
+ console.error(' SDG cycles (feat/fix) MUST have a technical narrative before committing.\n');
88
+ process.exit(1);
89
+ }
90
+ console.log(' ✅ Narrative Guard: CHANGELOG.md validated.');
91
+ process.exit(0);
92
+ }
82
93
  }
83
94
 
84
95
  run().catch((err) => {
@@ -88,6 +88,25 @@ function runIfDirect(importMetaUrl, fn) {
88
88
  }
89
89
  }
90
90
 
91
+ function detectIndentation(content) {
92
+ const lines = content.split('\n');
93
+ for (const line of lines) {
94
+ const match = line.match(/^(\s+)/);
95
+ if (match) return match[1];
96
+ }
97
+ return ' ';
98
+ }
99
+
100
+ function writeJsonAtomic(filePath, data, originalContent = null) {
101
+ const indent = originalContent ? detectIndentation(originalContent) : ' ';
102
+ const newContent = JSON.stringify(data, null, indent) + '\n';
103
+
104
+ if (originalContent === newContent) return false;
105
+
106
+ fs.writeFileSync(filePath, newContent);
107
+ return true;
108
+ }
109
+
91
110
  const FsUtils = {
92
111
  getDirectories,
93
112
  copyRecursiveSync,
@@ -96,6 +115,8 @@ const FsUtils = {
96
115
  evaluateVersionCondition,
97
116
  getDirname,
98
117
  runIfDirect,
118
+ detectIndentation,
119
+ writeJsonAtomic,
99
120
  };
100
121
 
101
122
  export { FsUtils };
@@ -14,7 +14,7 @@ import { FsUtils } from './fs-utils.mjs';
14
14
 
15
15
  const { displayName } = DisplayUtils;
16
16
  const { computeHashes } = ManifestUtils;
17
- const { getDirname } = FsUtils;
17
+ const { getDirname, writeJsonAtomic } = FsUtils;
18
18
 
19
19
  const __dirname = getDirname(import.meta.url);
20
20
  const SOURCE_INSTRUCTIONS = path.join(__dirname, '..', '..', 'assets', 'instructions');
@@ -463,6 +463,9 @@ function writeAgentConfig(targetDir, content, requestedAgents = []) {
463
463
  const fullDir = path.join(targetDir, target.dir);
464
464
  if (!fs.existsSync(fullDir)) fs.mkdirSync(fullDir, { recursive: true });
465
465
 
466
+ const targetFile = path.join(fullDir, target.file);
467
+ const originalContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, 'utf8') : null;
468
+
466
469
  let finalContent = content;
467
470
  if (agent === 'cursor') {
468
471
  finalContent = `---\ndescription: Project Governance and Architectural Rules\nglob: *\n---\n\n${content}`;
@@ -470,7 +473,9 @@ function writeAgentConfig(targetDir, content, requestedAgents = []) {
470
473
  finalContent = buildClaudeContent();
471
474
  }
472
475
 
473
- fs.writeFileSync(path.join(fullDir, target.file), finalContent);
476
+ if (originalContent !== finalContent) {
477
+ fs.writeFileSync(targetFile, finalContent);
478
+ }
474
479
  }
475
480
  }
476
481
 
@@ -525,7 +530,13 @@ function writeManifest(targetDir, selections, pkgVersion) {
525
530
 
526
531
  const aiDir = path.join(targetDir, '.ai');
527
532
  if (!fs.existsSync(aiDir)) fs.mkdirSync(aiDir, { recursive: true });
528
- fs.writeFileSync(path.join(aiDir, '.sdg-manifest.json'), JSON.stringify(manifest, null, 2));
533
+
534
+ const manifestPath = path.join(aiDir, '.sdg-manifest.json');
535
+ const originalContent = fs.existsSync(manifestPath)
536
+ ? fs.readFileSync(manifestPath, 'utf8')
537
+ : null;
538
+
539
+ writeJsonAtomic(manifestPath, manifest, originalContent);
529
540
  }
530
541
 
531
542
  /**
@@ -563,7 +574,7 @@ function writeAutomationScripts(targetDir, selections) {
563
574
  if (!pkg.scripts) pkg.scripts = {};
564
575
  if (!pkg.scripts.bump) {
565
576
  pkg.scripts.bump = 'node scripts/bump.mjs';
566
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
577
+ writeJsonAtomic(pkgPath, pkg, fs.readFileSync(pkgPath, 'utf8'));
567
578
  }
568
579
 
569
580
  // 4. Configure Husky if .husky exists
@@ -580,9 +591,10 @@ function writeAutomationScripts(targetDir, selections) {
580
591
 
581
592
  if (fs.existsSync(prePushPath)) {
582
593
  const content = fs.readFileSync(prePushPath, 'utf8');
583
- if (!content.includes('npm run bump')) {
594
+ if (!content.includes('npm test')) {
584
595
  const separator = content.endsWith('\n') ? '' : '\n';
585
- fs.appendFileSync(prePushPath, `${separator}\n${nvmShim}\n${bumpCmd}\n`);
596
+ const newPrePushContent = `${content}${separator}\n${nvmShim}\n${bumpCmd}\n`;
597
+ fs.writeFileSync(prePushPath, newPrePushContent);
586
598
  }
587
599
  } else {
588
600
  const prePushContent = dedent`