sdg-agents 1.4.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 +1 -1
- package/src/assets/instructions/commands/sdg-end.md +16 -13
- package/src/assets/instructions/core/writing-soul.md +22 -59
- package/src/assets/instructions/templates/bump.mjs +0 -1
- package/src/engine/bin/auto-bump.mjs +37 -1
- package/src/engine/bin/check-narrative.mjs +99 -0
- package/src/engine/bin/check-sync.mjs +4 -5
- package/src/engine/bin/index.mjs +34 -0
- package/src/engine/lib/fs-utils.mjs +21 -0
- package/src/engine/lib/instruction-assembler.mjs +20 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdg-agents",
|
|
3
|
-
"version": "1.4
|
|
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",
|
|
@@ -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
|
-
|
|
15
|
+
Prepare your technical narrative in the `CHANGELOG.md`.
|
|
16
16
|
|
|
17
|
-
1. **Identify
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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.
|
|
27
20
|
|
|
28
|
-
|
|
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
|
|
|
@@ -62,13 +61,17 @@ If a lint script exists (`lint`, `lint:fix`, `lint:all`, or a config file is det
|
|
|
62
61
|
- If non-auto-fixable violations remain, surface them explicitly.
|
|
63
62
|
- Block commit if errors remain.
|
|
64
63
|
|
|
65
|
-
## Step 8 — COMMIT
|
|
64
|
+
## Step 8 — COMMIT & RELEASE
|
|
66
65
|
|
|
67
66
|
Propose the commit message and **WAIT** for explicit Developer approval before committing.
|
|
68
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
|
+
|
|
69
71
|
## Step 9 — PUSH
|
|
70
72
|
|
|
71
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).
|
|
72
75
|
|
|
73
76
|
---
|
|
74
77
|
|
|
@@ -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.
|
|
@@ -63,7 +63,6 @@ function updateChangelog(newVersion) {
|
|
|
63
63
|
const today = new Date().toISOString().split('T')[0];
|
|
64
64
|
|
|
65
65
|
// Pattern to find the [Unreleased] section
|
|
66
|
-
// It handles both formats: ## [Unreleased] and ## [Unreleased] - YYYY-MM-DD
|
|
67
66
|
const unreleasedRegex = /##\s*\[Unreleased\](\s*-\s*\d{4}-\d{2}-\d{2})?/i;
|
|
68
67
|
|
|
69
68
|
if (!unreleasedRegex.test(content)) {
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 isPrePush = process.argv.includes('--pre-push');
|
|
18
|
+
const commitMsgFile = isPrePush ? null : process.argv[2];
|
|
19
|
+
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
const commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
|
|
31
|
+
const firstLine = commitMsg.split('\n')[0].trim();
|
|
32
|
+
|
|
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
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let changelog = '';
|
|
41
|
+
try {
|
|
42
|
+
// Read STAGED version of CHANGELOG.md (if possible)
|
|
43
|
+
changelog = execSync('git show :CHANGELOG.md', {
|
|
44
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
45
|
+
}).toString();
|
|
46
|
+
} catch {
|
|
47
|
+
if (fs.existsSync(CHANGELOG_PATH)) {
|
|
48
|
+
changelog = fs.readFileSync(CHANGELOG_PATH, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!changelog) {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const unreleasedMatch = changelog.match(
|
|
57
|
+
/##\s*\[Unreleased\].*?\n([\s\S]*?)(?=\n##\s|(?:\n){0,1}$)/i
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!unreleasedMatch) {
|
|
61
|
+
if (isPrePush) process.exit(0);
|
|
62
|
+
console.error('\n ❌ NARRATIVE VIOLATION: "## [Unreleased]" section missing in CHANGELOG.md.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const narrative = unreleasedMatch[1]
|
|
67
|
+
.replace(/###\s*(Added|Fixed|Changed|Removed|Security|Deprecated)/gi, '')
|
|
68
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
69
|
+
.trim();
|
|
70
|
+
|
|
71
|
+
const isNarrativeEmpty = !narrative || narrative.length < 3;
|
|
72
|
+
|
|
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
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
run().catch((err) => {
|
|
96
|
+
console.error(' ❌ Narrative Guard Exception:', err.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
// 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.');
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
+
writeJsonAtomic(pkgPath, pkg, fs.readFileSync(pkgPath, 'utf8'));
|
|
567
578
|
}
|
|
568
579
|
|
|
569
580
|
// 4. Configure Husky if .husky exists
|
|
@@ -573,21 +584,21 @@ function writeAutomationScripts(targetDir, selections) {
|
|
|
573
584
|
const nvmShim = dedent`
|
|
574
585
|
# NVM Shim (Essential for projects Staff in Linux/NVM)
|
|
575
586
|
export NVM_DIR="$HOME/.nvm"
|
|
576
|
-
[ -s "$NVM_DIR/nvm.sh" ] &&
|
|
587
|
+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
|
577
588
|
`;
|
|
578
589
|
|
|
579
|
-
const bumpCmd = '
|
|
590
|
+
const bumpCmd = '# Pre-push check (non-mutating)\nnpm test';
|
|
580
591
|
|
|
581
592
|
if (fs.existsSync(prePushPath)) {
|
|
582
593
|
const content = fs.readFileSync(prePushPath, 'utf8');
|
|
583
|
-
if (!content.includes('npm
|
|
594
|
+
if (!content.includes('npm test')) {
|
|
584
595
|
const separator = content.endsWith('\n') ? '' : '\n';
|
|
585
|
-
|
|
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`
|
|
589
601
|
#!/usr/bin/env sh
|
|
590
|
-
. "$(dirname -- "$0")/_/husky.sh"
|
|
591
602
|
|
|
592
603
|
${nvmShim}
|
|
593
604
|
|