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 +2 -2
- package/src/assets/instructions/commands/sdg-end.md +13 -14
- package/src/assets/instructions/core/writing-soul.md +22 -59
- package/src/assets/instructions/templates/bump.mjs +89 -0
- package/src/assets/instructions/templates/workflow.md +2 -1
- package/src/engine/bin/auto-bump.mjs +37 -1
- package/src/engine/bin/build-bundle.mjs +3 -0
- package/src/engine/bin/check-narrative.mjs +88 -0
- package/src/engine/bin/check-sync.mjs +4 -5
- package/src/engine/bin/index.mjs +34 -0
- package/src/engine/lib/cli-parser.mjs +1 -0
- package/src/engine/lib/instruction-assembler.mjs +70 -0
- package/src/engine/lib/wizard.mjs +23 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdg-agents",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
1. **Identify
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
26
|
-
|
|
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
|
|
1
|
+
# The Writing Soul
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Cultivating Personality
|
|
9
6
|
|
|
10
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
###
|
|
15
|
+
### Natural Expression
|
|
27
16
|
|
|
28
|
-
|
|
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
|
-
###
|
|
19
|
+
### Active Clarity
|
|
32
20
|
|
|
33
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
## Language and Style Practices
|
|
47
28
|
|
|
48
|
-
|
|
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
|
-
###
|
|
31
|
+
### Visual Serenity
|
|
52
32
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
## The Refinement Process
|
|
72
38
|
|
|
73
|
-
|
|
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
|
|
43
|
+
## Reference and Credits
|
|
81
44
|
|
|
82
|
-
This
|
|
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
|
-
- [ ] **
|
|
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 {
|
|
16
|
+
const { runIfDirect } = FsUtils;
|
|
17
17
|
const { success, fail } = ResultUtils;
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
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'];
|
package/src/engine/bin/index.mjs
CHANGED
|
@@ -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.');
|
|
@@ -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 :
|
|
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:
|
|
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';
|