sdg-agents 1.2.6 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdg-agents",
3
- "version": "1.2.6",
3
+ "version": "1.10.0",
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",
@@ -14,7 +14,7 @@
14
14
  "dev": "node src/engine/bin/index.mjs",
15
15
  "build": "node src/engine/bin/build-bundle.mjs",
16
16
  "review": "node src/engine/bin/review-bundle.mjs",
17
- "bump": "node src/engine/bin/auto-bump.mjs",
17
+ "bump": "node scripts/bump.mjs",
18
18
  "add-idiom": "node src/engine/bin/add-idiom.mjs",
19
19
  "clear": "node src/engine/bin/clear-bundle.mjs",
20
20
  "test": "node --test src/engine/lib/*.test.mjs",
@@ -12,20 +12,19 @@ Write one sentence per completed PLAN task. If no PLAN existed (e.g. `[S]` tasks
12
12
 
13
13
  ## Step 2 — CHANGELOG
14
14
 
15
- - Append an entry under `## [Unreleased]` in `CHANGELOG.md`:
16
-
17
- 1. **Identify Version**: Check `package.json` (or equivalent) for the current version.
18
- 2. **Calculate Next**: Determine the next version (patch/minor) based on the cycle type (`feat` minor/patch, `fix` patch).
19
- 3. **Update Header**:
20
- - If the project uses auto-bump or a release is intended: Create/Update the header to `## [NEXT_VERSION] - YYYY-MM-DD`.
21
- - Otherwise: Append under `## [Unreleased]`.
22
- 4. **Append Entry**:
23
- - `feat:` cycle → `### Added`
24
- - `fix:` cycle → `### Fixed`
25
- - `docs:` cycle → skip this step
26
- - `land:` cycle → skip this step
27
-
28
- If `## [Unreleased]` (or the appropriate version header) does not exist, create it above the most recent entry.
15
+ Prepare your technical narrative in the `CHANGELOG.md`.
16
+
17
+ 1. **Identify Strategy**:
18
+ - **Automated**: If `auto-bump.mjs` exists, you **MUST** first manually populate the `## [Unreleased]` section with your technical narrative. The automated pipeline will promote this content to the new version header during the commit. **DO NOT commit with an empty [Unreleased] section.**
19
+ - **Manual**: If no automation is present, you must promote the header manually (e.g., `## [1.2.0] - 2026-04-11`) or run the designated `bump` script.
20
+
21
+ 2. **Append Entry**:
22
+ - `feat:` cycle → Add under `### Added`
23
+ - `fix:` cycle → Add under `### Fixed`
24
+ - `docs:` cycle → Optional (for major architectural docs)
25
+ - `land:` cycle → Skip
26
+
27
+ 3. **Verify Header**: Ensure `## [Unreleased]` exists at the top. If missing, create it above the most recent version entry.
29
28
 
30
29
  ## Step 3 — BACKLOG: tasks.md
31
30
 
@@ -1,82 +1,45 @@
1
- # Writing Soul — Professional Authority Standards
1
+ # The Writing Soul
2
2
 
3
- > [!IMPORTANT]
4
- > **GOVERNANCE MANDATE**
5
- > This specification applies to all **non-code writing tasks**, including READMEs, Guides, UI Content, CHANGELOGs, and Commit Messages.
6
- > The goal is to eliminate "AI Slop" and restore **Developer intentionality**, density, and soul to every project artifact.
3
+ Good technical writing starts with the realization that there is a person on both sides of the screen. We believe that software documentation is a shared conversation, and maintaining a sense of mind behind the words is essential for effective communication. Sterile or voiceless text often feels like a missed opportunity to connect and share real engineering wisdom. These standards apply to all non-code writing tasks, including READMEs, guides, UI content, and commit messages.
7
4
 
8
- ## 1. Personality and Soul
5
+ ## Cultivating Personality
9
6
 
10
- Good technical writing has a **mind** behind it. Sterile, voiceless writing is as obvious as low-density slop.
7
+ Effective writing reflects the natural rhythms of how we think and solve problems. You can bring a sense of purpose to your text by offering a clear perspective on the facts you present. Mixing brief observations with longer, more detailed explanations helps keep the reader engaged.
11
8
 
12
- - **Have opinions**: Don't just report facts react to them.
13
- - **Vary your rhythm**: Mix short, punchy sentences with longer, flowing ones.
14
- - **Acknowledge complexity**: Real professionals have mixed feelings and uncertainty.
15
- - **Let some mess in**: Perfectly algorithmic structure feels fake. Tangents and asides add pulse.
9
+ Authenticity also comes from acknowledging that engineering is complex. Professionals often navigate uncertainty or hold nuanced views on a topic, and reflecting those feelings makes your writing more reliable. Allowing space for a well-placed aside or a brief tangent can create the pulse that makes a technical guide feel alive and uniquely human.
16
10
 
17
- ---
18
-
19
- ## 2. Content Patterns to Eliminate
20
-
21
- ### I. Significance Inflation
11
+ ## Seeking Authenticity
22
12
 
23
- **Banned phrases**: "testament/reminder to", "pivotal moment", "is a testament/reminder", "evolving landscape", "underscores its significance".
24
- **Fix**: Remove the "puffery". Just state the facts and their direct consequences.
13
+ Maintaining a professional and grounded tone involves recognizing patterns that sometimes cloud the clarity of our message. We can improve our writing by focusing on direct observations rather than relying on stylistic crutches.
25
14
 
26
- ### II. Superficial -ing Endings
15
+ ### Natural Expression
27
16
 
28
- **Patterns**: "...highlighting X", "...ensuring Y", "...reflecting Z".
29
- **Fix**: Break into clear, active clauses. Avoid tacking on "fake depth" via present participle phrases.
17
+ True authority often speaks for itself. We can avoid significance inflation by stating facts and their direct consequences, allowing the quality of the work to be the primary focus. Using phrases like "testament to" or "pivotal moment" often adds unnecessary weight where a simple description of the impact would be more effective.
30
18
 
31
- ### III. Promotional & Sycophantic Tone
19
+ ### Active Clarity
32
20
 
33
- **Banned adjectives**: "vibrant", "rich", "groundbreaking", "breathtaking", "seamless".
34
- **Banned artifacts**: "Great question!", "I hope this helps!", "Certainly!".
35
- **Fix**: Maintain a professional, peer-to-level tone. No sales pitch, no servility.
21
+ Direct communication is usually the most helpful for the reader. We can sharpen our message by breaking complex ideas into clear, active clauses. While present participle phrases ending in "-ing" are common, they sometimes obscure the relationship between actions. Choosing active verbs helps ensure the reader understands the exact flow of information or logic.
36
22
 
37
- ### IV. Generic Positive Conclusions
38
-
39
- **Patterns**: "The future looks bright", "Exciting times lie ahead".
40
- **Fix**: End with a concrete next step or a specific fact about future plans.
41
-
42
- ---
23
+ ### Professional Peerage
43
24
 
44
- ## 3. Language & Style Hardening
25
+ We value a tone that respects the expertise of the reader. A calm and peer-to-level approach builds trust more effectively than using promotional adjectives like "vibrant" or "groundbreaking." This same principle applies to our interactions, where a professional and direct response carries more weight than conversational fillers or overly decorative enthusiasm.
45
26
 
46
- ### I. Copula Avoidance
27
+ ## Language and Style Practices
47
28
 
48
- **Stop**: "serves as", "stands as", "represents [a]".
49
- **Use**: "is", "are", "has". Simple is more direct.
29
+ The way we structure our language influences how easily a reader can follow our logic. We prefer simple and direct verbs to keep the focus on the content. Words like "is," "are," and "has" are often more effective than complex alternatives that can slow down the reading experience.
50
30
 
51
- ### II. Em Dash & Boldface Overuse
31
+ ### Visual Serenity
52
32
 
53
- **Rule**: Em dashes (—) are banned. No exceptions. AI uses them constantly to fake rhetorical punch.
54
- **Fix**: Rewrite with a comma, a period, or a new sentence. If it needs a pause, earn it with structure.
55
- Bold is for true technical emphasis only: a term, a command, a key constraint. Not decoration.
56
-
57
- ### III. Title Case & Emojis
58
-
59
- **Rule**: No Title Case for everything. Emojis are reserved for **pattern signaling only** — marking BAD/GOOD examples, callouts, or explicit visual cues in documentation.
60
- **Banned**: Decorative emojis in section headers, link anchors, or inline prose (e.g. `## 🚀 Quick Start`, `✨ Try the app`).
61
- **Allowed**: Signal emojis that carry semantic meaning in structured examples:
62
-
63
- - `BAD`: `❌ if (data == null)` / `GOOD`: `✅ if (!data)`
64
- - `[!WARNING]`, `[!TIP]`, `[!NOTE]` callouts (GitHub-native, not emoji)
65
- **Fix**: Use Sentence case for headings. Strip all decorative emoji. If a header needs visual weight, use formatting — not icons.
66
-
67
- ---
33
+ Structure and rhythm should guide the reader through the text naturally. We find that avoiding em dashes encourages a more thoughtful sentence structure, as it requires earning each pause through careful composition. Similarly, we use bold formatting exclusively for technical emphasis, such as specific terms, commands, or key constraints.
68
34
 
69
- ## 4. The Authority Loop
35
+ Our approach to formatting also prioritizes clarity. Headings follow sentence case to maintain a serene and professional appearance. We use emojis only when they carry semantic meaning for pattern signaling, such as marking successful or unsuccessful examples in a technical guide. This keeps the visual environment focused on the information being shared.
70
36
 
71
- Before finalizing any content, perform a self-audit:
37
+ ## The Refinement Process
72
38
 
73
- 1. **Identify Patterns**: Scan for the AI-isms listed above.
74
- 2. **Rewrite**: Replace slop with direct, active voice.
75
- 3. **Add Soul**: Inject a specific observation or a varied rhythm.
76
- 4. **Final Check**: Ask: _"What makes this so obviously AI-generated?"_ Fix the remaining tells.
39
+ Before sharing your work, a brief moment of reflection can help ensure the content reflects your intention. You might find it helpful to look for familiar patterns that feel more like general summaries than specific observations. Replacing these with active, direct language often makes the writing feel more genuine. Adding a unique insight or varying the length of your sentences can provide the final touch that makes the text feel truly yours.
77
40
 
78
41
  ---
79
42
 
80
- ## Reference & Credits
43
+ ## Reference and Credits
81
44
 
82
- This specification is based on the **Wikipedia:Signs of AI writing** project, maintained by **WikiProject AI Cleanup**. It serves as the foundational standard for eliminating statistical slop and restoring **Developer intentionality** to generated text.
45
+ This standard is inspired by the collective efforts of the project to eliminate statistical slop and restore intentionality to generated text. It serves as a foundation for building clear, purposeful, and human-centered documentation across the entire ecosystem.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * SDG-Agents: Bump & Changelog Automation
3
+ * Automates semantic versioning and promotes Unreleased changes in CHANGELOG.md.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { execSync } from 'node:child_process';
9
+
10
+ const ROOT_DIR = process.cwd();
11
+ const PACKAGE_JSON_PATH = path.join(ROOT_DIR, 'package.json');
12
+ const CHANGELOG_PATH = path.join(ROOT_DIR, 'CHANGELOG.md');
13
+
14
+ function run() {
15
+ const args = process.argv.slice(2);
16
+ const bumpType = args[0]; // feat, fix, major
17
+
18
+ if (!['feat', 'fix', 'major'].includes(bumpType)) {
19
+ console.error('❌ Error: Please specify bump type (feat, fix, or major).');
20
+ console.log('Usage: npm run bump <feat|fix|major>');
21
+ process.exit(1);
22
+ }
23
+
24
+ const typeMap = {
25
+ feat: 'minor',
26
+ fix: 'patch',
27
+ major: 'major',
28
+ };
29
+
30
+ const npmType = typeMap[bumpType];
31
+
32
+ try {
33
+ // 1. Get current version
34
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
35
+ const oldVersion = pkg.version;
36
+
37
+ // 2. Bump version in package.json (no git tag/commit yet)
38
+ console.log(`🚀 Bumping version (${npmType})...`);
39
+ execSync(`npm version ${npmType} --no-git-tag-version`, { stdio: 'inherit' });
40
+
41
+ // 3. Get new version
42
+ const newPkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
43
+ const newVersion = newPkg.version;
44
+
45
+ // 4. Update CHANGELOG.md
46
+ updateChangelog(newVersion);
47
+
48
+ console.log(`✅ Success: ${oldVersion} → ${newVersion}`);
49
+ console.log('🔗 CHANGELOG.md updated and promoted to current date.');
50
+ } catch (error) {
51
+ console.error('❌ Error during bump strategy:', error.message);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ function updateChangelog(newVersion) {
57
+ if (!fs.existsSync(CHANGELOG_PATH)) {
58
+ console.warn('⚠️ CHANGELOG.md not found. Skipping changelog update.');
59
+ return;
60
+ }
61
+
62
+ const content = fs.readFileSync(CHANGELOG_PATH, 'utf8');
63
+ const today = new Date().toISOString().split('T')[0];
64
+
65
+ // Pattern to find the [Unreleased] section
66
+ const unreleasedRegex = /##\s*\[Unreleased\](\s*-\s*\d{4}-\d{2}-\d{2})?/i;
67
+
68
+ if (!unreleasedRegex.test(content)) {
69
+ console.warn('⚠️ Could not find "## [Unreleased]" header in CHANGELOG.md.');
70
+ console.log('Skipping content promotion.');
71
+ return;
72
+ }
73
+
74
+ const newHeader = `## [${newVersion}] - ${today}`;
75
+
76
+ // 1. Promote Unreleased to New Version
77
+ let updatedContent = content.replace(unreleasedRegex, newHeader);
78
+
79
+ // 2. Inject new [Unreleased] block at the top
80
+ const insertIndex = updatedContent.indexOf(newHeader);
81
+ const nextBlock = `## [Unreleased]\n\n### Added\n\n### Fixed\n\n`;
82
+
83
+ updatedContent =
84
+ updatedContent.slice(0, insertIndex) + nextBlock + updatedContent.slice(insertIndex);
85
+
86
+ fs.writeFileSync(CHANGELOG_PATH, updatedContent);
87
+ }
88
+
89
+ run();
@@ -140,7 +140,8 @@ On every request, classify intent before acting:
140
140
  ### END Checklist (mandatory — execute in order, mark each before proceeding)
141
141
 
142
142
  - [ ] **SUMMARIZE** — one sentence per completed PLAN task written in response
143
- - [ ] **CHANGELOG** — Append entry. **Identify next version** (check \`package.json\`) to determine the header (\`## [NEXT_VERSION] - YYYY-MM-DD\` for releases/auto-bump, or \`## [Unreleased]\`). Categories: \`### Added\` (feat), \`### Fixed\` (fix).
143
+ - [ ] **BUMP** — run \`npm run bump <feat|fix>\` to promote CHANGELOG and package.json version. Skip if not applicable.
144
+ - [ ] **CHANGELOG** — Verify [Unreleased] content was promoted. Append any manual notes if needed.
144
145
  - [ ] **BACKLOG: tasks.md** — all completed tasks moved to `## Done` with `[DONE]` status
145
146
  - [ ] **BACKLOG: context.md** — \`## Now\` updated with next objective or cleared
146
147
  - [ ] **KNOWLEDGE** — Log any patterns, findings, or rework discovered during this cycle. Update \`.ai-backlog/learned.md\` (for successful feats) or \`.ai-backlog/troubleshoot.md\` (for fixed incidents). Curate stale or irrelevant items.
@@ -12,6 +12,7 @@ const { runIfDirect } = FsUtils;
12
12
  // bin/ → engine/ → src/ → root
13
13
  const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../');
14
14
  const PACKAGE_PATHS = [path.join(ROOT_DIR, 'package.json')];
15
+ const CHANGELOG_PATH = path.join(ROOT_DIR, 'CHANGELOG.md');
15
16
 
16
17
  // --- Orchestrator ---
17
18
 
@@ -27,7 +28,13 @@ async function run() {
27
28
  const rootPkg = readPackageJson(resolveRootPackagePath());
28
29
  const nextVersion = bumpVersion(rootPkg.version, bumpType);
29
30
 
31
+ // 1. Promote Changelog
32
+ updateChangelog(nextVersion);
33
+
34
+ // 2. Sync Package.json files
30
35
  syncAllPackages(nextVersion);
36
+
37
+ // 3. Commit the bump
31
38
  stageAndCommit(nextVersion);
32
39
 
33
40
  console.log(` auto-bump: ${rootPkg.version} → ${nextVersion} (${bumpType})`);
@@ -57,9 +64,37 @@ function bumpVersion(current, bumpType) {
57
64
  return `${major}.${minor + 1}.0`;
58
65
  case 'patch':
59
66
  return `${major}.${minor}.${patch + 1}`;
67
+ default:
68
+ return current;
60
69
  }
61
70
  }
62
71
 
72
+ function updateChangelog(newVersion) {
73
+ if (!fs.existsSync(CHANGELOG_PATH)) return;
74
+
75
+ const content = fs.readFileSync(CHANGELOG_PATH, 'utf8');
76
+ const today = new Date().toISOString().split('T')[0];
77
+
78
+ // Pattern to find the [Unreleased] section
79
+ const unreleasedRegex = /##\s*\[Unreleased\](\s*-\s*\d{4}-\d{2}-\d{2})?/i;
80
+
81
+ if (!unreleasedRegex.test(content)) return;
82
+
83
+ const newHeader = `## [${newVersion}] - ${today}`;
84
+
85
+ // 1. Promote Unreleased to New Version
86
+ let updatedContent = content.replace(unreleasedRegex, newHeader);
87
+
88
+ // 2. Inject new [Unreleased] block at the top if it was promoted
89
+ const insertIndex = updatedContent.indexOf(newHeader);
90
+ const nextBlock = `## [Unreleased]\n\n### Added\n\n### Fixed\n\n`;
91
+
92
+ updatedContent =
93
+ updatedContent.slice(0, insertIndex) + nextBlock + updatedContent.slice(insertIndex);
94
+
95
+ fs.writeFileSync(CHANGELOG_PATH, updatedContent);
96
+ }
97
+
63
98
  // --- Sync & Commit ---
64
99
 
65
100
  function syncAllPackages(nextVersion) {
@@ -71,7 +106,8 @@ function syncAllPackages(nextVersion) {
71
106
 
72
107
  function stageAndCommit(nextVersion) {
73
108
  const paths = resolvePackagePaths();
74
- const files = paths.filter((p) => fs.existsSync(p)).join(' ');
109
+ const files = [...paths, CHANGELOG_PATH].filter((p) => fs.existsSync(p)).join(' ');
110
+
75
111
  execSync(`git add ${files}`, { stdio: 'inherit' });
76
112
  execSync(`git commit -m "chore: bump version to ${nextVersion}"`, { stdio: 'inherit' });
77
113
  }
@@ -21,6 +21,7 @@ const {
21
21
  writeBacklogFiles,
22
22
  writeGitignore,
23
23
  writeManifest,
24
+ writeAutomationScripts,
24
25
  } = InstructionAssembler;
25
26
  const { success } = ResultUtils;
26
27
  const { runIfDirect } = FsUtils;
@@ -207,6 +208,7 @@ function executeQuickPipeline(targetDir, selections, { noDevGuides = false } = {
207
208
 
208
209
  printStep(5, 5, 'Injecting spec templates...');
209
210
  injectPrompts(targetDir, selections.track);
211
+ writeAutomationScripts(targetDir, selections);
210
212
  writeManifest(targetDir, selections, packageJson.version);
211
213
  }
212
214
 
@@ -239,6 +241,7 @@ function executeAgentsPipeline(targetDir, selections, { noDevGuides = false } =
239
241
  writeGitignore(targetDir);
240
242
 
241
243
  printStep(5, 5, 'Finalizing manifest...');
244
+ writeAutomationScripts(targetDir, selections);
242
245
  writeManifest(targetDir, selections, packageJson.version);
243
246
  }
244
247
 
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Narrative Guard — Blocks feat: and fix: commits if CHANGELOG.md [Unreleased] is empty.
5
+ * Ensures the auto-bump pipeline always has content to promote.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import { execSync } from 'node:child_process';
12
+
13
+ const PROJECT_ROOT = process.cwd();
14
+ const CHANGELOG_PATH = path.join(PROJECT_ROOT, 'CHANGELOG.md');
15
+
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
+ }
22
+
23
+ if (!fs.existsSync(commitMsgFile)) {
24
+ console.error(` ❌ Error: Commit message file not found at ${commitMsgFile}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
29
+ const firstLine = commitMsg.split('\n')[0].trim();
30
+
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);
35
+ }
36
+
37
+ let changelog = '';
38
+ try {
39
+ // Read STAGED version of CHANGELOG.md to prevent issues with lint-staged or stashing
40
+ changelog = execSync('git show :CHANGELOG.md', {
41
+ stdio: ['pipe', 'pipe', 'ignore'],
42
+ }).toString();
43
+ } catch {
44
+ // Fallback to disk if not in index (shouldn't happen in a commit hook for a tracked file)
45
+ if (fs.existsSync(CHANGELOG_PATH)) {
46
+ changelog = fs.readFileSync(CHANGELOG_PATH, 'utf8');
47
+ }
48
+ }
49
+
50
+ if (!changelog) {
51
+ console.warn(' ⚠️ Warning: CHANGELOG.md not found or empty. Skipping narrative check.');
52
+ process.exit(0);
53
+ }
54
+
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
+ const unreleasedMatch = changelog.match(
58
+ /##\s*\[Unreleased\].*?\n([\s\S]*?)(?=\n##\s|(?:\n){0,1}$)/i
59
+ );
60
+
61
+ if (!unreleasedMatch) {
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
+ process.exit(1);
65
+ }
66
+
67
+ const narrative = unreleasedMatch[1]
68
+ .replace(/###\s*(Added|Fixed|Changed|Removed|Security|Deprecated)/gi, '')
69
+ .replace(/<!--[\s\S]*?-->/g, '') // Remove markdown comments
70
+ .trim();
71
+
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
+ }
79
+
80
+ console.log(' ✅ Narrative Guard: CHANGELOG.md validated.');
81
+ process.exit(0);
82
+ }
83
+
84
+ run().catch((err) => {
85
+ console.error(' ❌ Narrative Guard Exception:', err.message);
86
+ process.exit(1);
87
+ });
88
+ // test
@@ -13,13 +13,12 @@ import crypto from 'node:crypto';
13
13
  import { FsUtils } from '../lib/fs-utils.mjs';
14
14
  import { ResultUtils } from '../lib/result-utils.mjs';
15
15
 
16
- const { getDirname, runIfDirect } = FsUtils;
16
+ const { runIfDirect } = FsUtils;
17
17
  const { success, fail } = ResultUtils;
18
18
 
19
- const __dirname = getDirname(import.meta.url);
20
- const MONOREPO_ROOT = path.join(__dirname, '..', '..', '..', '..', '..');
21
- const ASSETS_DIR = path.join(MONOREPO_ROOT, 'packages', 'cli', 'src', 'assets', 'instructions');
22
- const AI_DIR = path.join(MONOREPO_ROOT, 'packages', 'cli', '.ai', 'instructions');
19
+ const PROJECT_ROOT = process.cwd();
20
+ const ASSETS_DIR = path.join(PROJECT_ROOT, 'src', 'assets', 'instructions');
21
+ const AI_DIR = path.join(PROJECT_ROOT, '.ai', 'instructions');
23
22
 
24
23
  // why: flavor/ in .ai/ is populated by renaming flavors/{id}/ — no direct mirror exists in src/assets/flavor/
25
24
  const MIRRORED_DIRS = ['core', 'idioms', 'templates', 'competencies'];
@@ -39,6 +39,11 @@ async function run() {
39
39
  // Resolve target directory early for the entire cycle
40
40
  args.targetDir = path.resolve(args.targetDir || process.cwd());
41
41
 
42
+ // Maintainer Protocol: ensuring core instructions are synced to .ai/ for the agent
43
+ if (!args.subcommand && !args.help && !args.version) {
44
+ await ensureMaintainerSync(args.targetDir);
45
+ }
46
+
42
47
  if (args.subcommand) {
43
48
  await executeSubcommand(args);
44
49
  } else {
@@ -210,6 +215,35 @@ async function runSettingsMenu(targetDir) {
210
215
 
211
216
  // --- System ---
212
217
 
218
+ async function ensureMaintainerSync(targetDir) {
219
+ const { isMaintainerMode } = PromptUtils;
220
+ if (!isMaintainerMode()) return;
221
+
222
+ const { SyncChecker } = await import('./check-sync.mjs');
223
+ const syncResult = SyncChecker.run();
224
+
225
+ if (syncResult.isFailure) {
226
+ console.log('\n 🛠️ MAINTAINER MODE: Drift detected in core instructions.');
227
+ console.log(' 🔄 Automatic sync in progress...\n');
228
+
229
+ const { ManifestUtils } = await import('../lib/manifest-utils.mjs');
230
+ const { SDG } = await import('./build-bundle.mjs');
231
+ const manifest = ManifestUtils.loadManifest(targetDir);
232
+
233
+ if (manifest) {
234
+ try {
235
+ await SDG.run(targetDir, { selections: manifest.selections });
236
+ console.log('\n ✅ Core instructions synchronized. Agent rules are up-to-date.\n');
237
+ console.log('─'.repeat(50) + '\n');
238
+ } catch (error) {
239
+ console.log(`\n ⚠️ Automatic sync failed: ${error.message}\n`);
240
+ }
241
+ } else {
242
+ console.log(' ⚠️ Cannot auto-sync: No manifest found. Run "init" once.\n');
243
+ }
244
+ }
245
+ }
246
+
213
247
  function handleExitError(error) {
214
248
  if (error.message?.includes('force closed') || error.name === 'ExitPromptError') {
215
249
  console.log('\n\n 👋 Goodbye! See you soon engineer.');
@@ -17,6 +17,7 @@ function parseCliArgs(argv) {
17
17
  mode: getArgValue(argv, '--mode'),
18
18
  track: getArgValue(argv, '--track'),
19
19
  scope: getArgValue(argv, '--scope'),
20
+ bump: !argv.includes('--no-bump'),
20
21
  };
21
22
  }
22
23
 
@@ -528,6 +528,75 @@ function writeManifest(targetDir, selections, pkgVersion) {
528
528
  fs.writeFileSync(path.join(aiDir, '.sdg-manifest.json'), JSON.stringify(manifest, null, 2));
529
529
  }
530
530
 
531
+ /**
532
+ * Injects automation scripts and configurations (Bump, Husky) if enabled.
533
+ * Idempotent: skips if scripts/bump.mjs exists or if selections.bump is false.
534
+ */
535
+ function writeAutomationScripts(targetDir, selections) {
536
+ if (selections.bump === false) return;
537
+
538
+ const scriptsDir = path.join(targetDir, 'scripts');
539
+ const bumpScriptPath = path.join(scriptsDir, 'bump.mjs');
540
+
541
+ // 1. Check for existing bump script in package.json to avoid collision
542
+ const pkgPath = path.join(targetDir, 'package.json');
543
+ if (!fs.existsSync(pkgPath)) return;
544
+
545
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
546
+ const hasExistingBump =
547
+ pkg.scripts && (pkg.scripts.bump || pkg.scripts.release || pkg.scripts.version);
548
+
549
+ if (hasExistingBump && !fs.existsSync(bumpScriptPath)) {
550
+ // If they have a script but not our file, we respect their script
551
+ return;
552
+ }
553
+
554
+ // 2. Write bump.mjs template
555
+ if (!fs.existsSync(bumpScriptPath)) {
556
+ if (!fs.existsSync(scriptsDir)) fs.mkdirSync(scriptsDir, { recursive: true });
557
+ const templatePath = path.join(SOURCE_INSTRUCTIONS, 'templates', 'bump.mjs');
558
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
559
+ fs.writeFileSync(bumpScriptPath, templateContent);
560
+ }
561
+
562
+ // 3. Update package.json scripts
563
+ if (!pkg.scripts) pkg.scripts = {};
564
+ if (!pkg.scripts.bump) {
565
+ pkg.scripts.bump = 'node scripts/bump.mjs';
566
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
567
+ }
568
+
569
+ // 4. Configure Husky if .husky exists
570
+ const huskyDir = path.join(targetDir, '.husky');
571
+ if (fs.existsSync(huskyDir)) {
572
+ const prePushPath = path.join(huskyDir, 'pre-push');
573
+ const nvmShim = dedent`
574
+ # NVM Shim (Essential for projects Staff in Linux/NVM)
575
+ export NVM_DIR="$HOME/.nvm"
576
+ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
577
+ `;
578
+
579
+ const bumpCmd = '# Pre-push check (non-mutating)\nnpm test';
580
+
581
+ if (fs.existsSync(prePushPath)) {
582
+ const content = fs.readFileSync(prePushPath, 'utf8');
583
+ if (!content.includes('npm run bump')) {
584
+ const separator = content.endsWith('\n') ? '' : '\n';
585
+ fs.appendFileSync(prePushPath, `${separator}\n${nvmShim}\n${bumpCmd}\n`);
586
+ }
587
+ } else {
588
+ const prePushContent = dedent`
589
+ #!/usr/bin/env sh
590
+
591
+ ${nvmShim}
592
+
593
+ ${bumpCmd}
594
+ `;
595
+ fs.writeFileSync(prePushPath, prePushContent, { mode: 0o755 });
596
+ }
597
+ }
598
+ }
599
+
531
600
  const InstructionAssembler = {
532
601
  buildMasterInstructions,
533
602
  buildClaudeContent,
@@ -536,6 +605,7 @@ const InstructionAssembler = {
536
605
  writeBacklogFiles,
537
606
  writeGitignore,
538
607
  writeManifest,
608
+ writeAutomationScripts,
539
609
  };
540
610
 
541
611
  export { InstructionAssembler };
@@ -33,7 +33,7 @@ async function gatherUserSelections(targetDir = process.cwd()) {
33
33
  let scope = 'fullstack';
34
34
  let step = 0;
35
35
 
36
- const finalStep = () => (selections.mode === 'prompts' ? 2 : 8);
36
+ const finalStep = () => (selections.mode === 'prompts' ? 2 : 9);
37
37
 
38
38
  while (step < finalStep()) {
39
39
  const context = {
@@ -74,6 +74,7 @@ async function gatherUserSelections(targetDir = process.cwd()) {
74
74
  if (stepValue.ide) currentSelections.ide = stepValue.ide;
75
75
  if (stepValue.undoLastIdiom) currentSelections.idioms.pop();
76
76
  if (stepValue.resetIdioms) currentSelections.idioms = [];
77
+ if (stepValue.bump !== undefined) currentSelections.bump = stepValue.bump;
77
78
  return nextScope;
78
79
  }
79
80
  }
@@ -98,8 +99,10 @@ async function executeWizardStep(step, context) {
98
99
  return promptDesignPreset(context);
99
100
  case 7:
100
101
  return promptIdeSelection();
102
+ case 8:
103
+ return promptBumpAutomation(context);
101
104
  default: {
102
- const defaultResult = success({ nextStep: 8 });
105
+ const defaultResult = success({ nextStep: 9 });
103
106
  return defaultResult;
104
107
  }
105
108
  }
@@ -393,6 +396,24 @@ async function promptIdeSelection() {
393
396
  return ideResult;
394
397
  }
395
398
 
399
+ async function promptBumpAutomation(context) {
400
+ const { selections } = context;
401
+
402
+ const hasJsTs = selections.idioms.some((id) => id === 'javascript' || id === 'typescript');
403
+
404
+ if (!hasJsTs) {
405
+ return success({ nextStep: 9, bump: false });
406
+ }
407
+
408
+ const result = await safeConfirm({
409
+ message: 'Enable automated versioning (Bump & Changelog)?',
410
+ default: true,
411
+ });
412
+
413
+ const bumpResult = success({ nextStep: 9, bump: result });
414
+ return bumpResult;
415
+ }
416
+
396
417
  function validateSelections(selections) {
397
418
  if (selections.mode === 'quick') {
398
419
  selections.flavor = selections.flavor || 'lite';