@pennyfarthing/core 7.6.1 → 7.8.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.
Files changed (109) hide show
  1. package/README.md +109 -201
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/packages/core/dist/cli/commands/doctor.js +205 -0
  5. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  6. package/packages/core/dist/cli/commands/init.js +31 -0
  7. package/packages/core/dist/cli/commands/init.js.map +1 -1
  8. package/packages/core/dist/cli/commands/update.js +31 -0
  9. package/packages/core/dist/cli/commands/update.js.map +1 -1
  10. package/pennyfarthing-dist/agents/architect.md +48 -53
  11. package/pennyfarthing-dist/agents/dev.md +74 -164
  12. package/pennyfarthing-dist/agents/devops.md +44 -39
  13. package/pennyfarthing-dist/agents/handoff.md +46 -23
  14. package/pennyfarthing-dist/agents/orchestrator.md +84 -255
  15. package/pennyfarthing-dist/agents/pm.md +40 -50
  16. package/pennyfarthing-dist/agents/reviewer-preflight.md +58 -26
  17. package/pennyfarthing-dist/agents/reviewer.md +107 -298
  18. package/pennyfarthing-dist/agents/sm-file-summary.md +51 -30
  19. package/pennyfarthing-dist/agents/sm-finish.md +59 -38
  20. package/pennyfarthing-dist/agents/sm-handoff.md +40 -33
  21. package/pennyfarthing-dist/agents/sm-setup.md +122 -45
  22. package/pennyfarthing-dist/agents/sm.md +204 -545
  23. package/pennyfarthing-dist/agents/tea.md +77 -146
  24. package/pennyfarthing-dist/agents/tech-writer.md +43 -24
  25. package/pennyfarthing-dist/agents/testing-runner.md +73 -30
  26. package/pennyfarthing-dist/agents/ux-designer.md +39 -25
  27. package/pennyfarthing-dist/agents/workflow-status-check.md +45 -17
  28. package/pennyfarthing-dist/commands/benchmark.md +19 -1
  29. package/pennyfarthing-dist/commands/continue-session.md +1 -1
  30. package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
  31. package/pennyfarthing-dist/commands/solo.md +36 -0
  32. package/pennyfarthing-dist/commands/theme-maker.md +5 -5
  33. package/pennyfarthing-dist/commands/work.md +1 -1
  34. package/pennyfarthing-dist/guides/agent-behavior.md +22 -9
  35. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
  36. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
  37. package/pennyfarthing-dist/guides/scale-levels.md +114 -0
  38. package/pennyfarthing-dist/guides/xml-tags.md +335 -0
  39. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
  40. package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
  41. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
  42. package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
  43. package/pennyfarthing-dist/scripts/core/check-context.sh +9 -1
  44. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
  45. package/pennyfarthing-dist/scripts/core/prime.sh +3 -132
  46. package/pennyfarthing-dist/scripts/core/run.sh +9 -0
  47. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
  48. package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
  49. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  50. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
  51. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
  52. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
  53. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +499 -0
  54. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
  55. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
  56. package/pennyfarthing-dist/scripts/jira/README.md +10 -7
  57. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
  58. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
  59. package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
  60. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
  61. package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
  62. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
  63. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
  64. package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
  65. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
  66. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
  67. package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
  68. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
  69. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
  70. package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
  71. package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
  72. package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
  73. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
  74. package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
  75. package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
  76. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
  77. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
  78. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
  79. package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
  80. package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
  81. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
  82. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
  83. package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
  84. package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
  85. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
  86. package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
  87. package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
  88. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
  89. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
  90. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
  91. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
  92. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
  93. package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
  94. package/pennyfarthing-dist/guides/XML-TAGS.md +0 -156
  95. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -380
  96. package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
  97. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
  98. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
  99. package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
  100. package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
  101. package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
  102. package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
  103. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
  104. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
  105. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
  106. /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md → agent-coordination.md} +0 -0
  107. /package/pennyfarthing-dist/guides/{HOOKS.md → hooks.md} +0 -0
  108. /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md → prompt-patterns.md} +0 -0
  109. /package/pennyfarthing-dist/guides/{SESSION-ARTIFACTS.md → session-artifacts.md} +0 -0
@@ -1,198 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * jira-sync.mjs - Sync Pennyfarthing Epic to Jira
4
- *
5
- * Usage: node jira-sync.mjs <epic_number> [--dry-run] [--transition] [--points]
6
- *
7
- * Options:
8
- * --dry-run Show what would be done without making changes
9
- * --transition Transition Jira issues to match Pennyfarthing status
10
- * --points Sync story points from Pennyfarthing to Jira
11
- */
12
-
13
- import {
14
- checkDependencies,
15
- findProjectRoot,
16
- loadSprintFile,
17
- findEpic,
18
- extractJiraKey,
19
- mapStatusToJira,
20
- getIssueJson,
21
- getJiraField,
22
- moveIssue,
23
- getStoryPoints,
24
- syncStoryPoints,
25
- parseArgs,
26
- success,
27
- info,
28
- warn,
29
- error,
30
- JIRA_URL
31
- } from './jira-lib.mjs';
32
-
33
- // Parse arguments
34
- const args = parseArgs(process.argv, {
35
- 'dry-run': { type: 'boolean' },
36
- 'transition': { type: 'boolean' },
37
- 'points': { type: 'boolean' },
38
- 'with-comments': { type: 'boolean' } // Deprecated, ignored
39
- });
40
-
41
- const epicNum = args._positional[0];
42
- const dryRun = args['dry-run'] || false;
43
- const doTransition = args.transition || false;
44
- const syncPoints = args.points || false;
45
-
46
- // Show usage if no epic provided
47
- if (!epicNum) {
48
- error('Epic number required');
49
- console.log(`
50
- Usage: jira-sync.mjs <epic_number> [--dry-run] [--transition] [--points]
51
-
52
- Examples:
53
- jira-sync.mjs 35 # Show sync status for epic 35
54
- jira-sync.mjs 35 --dry-run # Show what would be done
55
- jira-sync.mjs 35 --transition # Sync status to Jira
56
- jira-sync.mjs 35 --transition --points # Sync status and story points
57
- `);
58
- process.exit(2);
59
- }
60
-
61
- // Check dependencies
62
- if (!checkDependencies()) {
63
- process.exit(2);
64
- }
65
-
66
- // Find project root and load sprint data
67
- let projectRoot;
68
- let sprintData;
69
-
70
- try {
71
- projectRoot = findProjectRoot();
72
- sprintData = loadSprintFile(projectRoot);
73
- } catch (err) {
74
- error(err.message);
75
- process.exit(1);
76
- }
77
-
78
- // Find epic
79
- const epic = findEpic(sprintData, epicNum);
80
- if (!epic) {
81
- error(`Epic ${epicNum} not found in sprint file`);
82
- process.exit(1);
83
- }
84
-
85
- // Display header
86
- console.log('');
87
- info('==========================================');
88
- info(`Epic ${epicNum}: ${epic.title}`);
89
- if (epic.jira) info(`Jira: ${epic.jira}`);
90
- info('==========================================');
91
- console.log('');
92
-
93
- if (dryRun) {
94
- warn('[DRY-RUN MODE] No changes will be made');
95
- console.log('');
96
- }
97
-
98
- // Get stories
99
- const stories = epic.stories || [];
100
- if (stories.length === 0) {
101
- warn(`No stories found in epic ${epicNum}`);
102
- process.exit(0);
103
- }
104
-
105
- info(`Found ${stories.length} stories to process`);
106
- console.log('');
107
-
108
- // Process each story
109
- let synced = 0;
110
- let skipped = 0;
111
- let errors = 0;
112
-
113
- for (const story of stories) {
114
- const storyKey = story.id;
115
- const storyTitle = story.title || 'Untitled';
116
- const storyStatus = story.status || 'backlog';
117
- const storyJira = story.jira;
118
- const storyPoints = story.points;
119
-
120
- console.log('---');
121
- info(`Story ${storyKey}: ${storyTitle}`);
122
- console.log(` Status: ${storyStatus}`);
123
-
124
- // Check if synced to Jira
125
- if (!storyJira || storyJira === 'null') {
126
- warn(' Not synced to Jira - skipping');
127
- skipped++;
128
- continue;
129
- }
130
-
131
- // Extract Jira key
132
- const jiraKey = extractJiraKey(storyJira);
133
- console.log(` Jira: ${jiraKey}`);
134
-
135
- if (dryRun) {
136
- const actions = [];
137
- if (doTransition) actions.push('transition');
138
- if (syncPoints) actions.push('sync points');
139
- warn(` [DRY-RUN] Would sync: ${actions.length > 0 ? actions.join(', ') : 'view only'}`);
140
- synced++;
141
- continue;
142
- }
143
-
144
- // Fetch current Jira state
145
- const issueJson = getIssueJson(jiraKey);
146
- if (!issueJson) {
147
- warn(` Could not fetch Jira issue ${jiraKey}`);
148
- errors++;
149
- continue;
150
- }
151
-
152
- const jiraStatus = getJiraField(issueJson, 'fields.status.name', 'Unknown');
153
- const jiraPoints = getStoryPoints(jiraKey, issueJson);
154
- const targetStatus = mapStatusToJira(storyStatus);
155
-
156
- console.log(` Jira Status: ${jiraStatus} (target: ${targetStatus})`);
157
- if (storyPoints) console.log(` Points: Pennyfarthing=${storyPoints}, Jira=${jiraPoints || 'unset'}`);
158
-
159
- // Transition if requested
160
- if (doTransition) {
161
- if (jiraStatus === targetStatus) {
162
- success(` Already at correct status: ${jiraStatus}`);
163
- } else {
164
- info(` Transitioning: ${jiraStatus} -> ${targetStatus}`);
165
- const result = moveIssue(jiraKey, targetStatus);
166
- if (result.success) {
167
- success(` Transitioned to ${targetStatus}`);
168
- } else {
169
- warn(` Could not transition (may not be available from current state)`);
170
- }
171
- }
172
- }
173
-
174
- // Sync points if requested
175
- if (syncPoints && storyPoints) {
176
- info(` Syncing story points: ${jiraPoints || 'unset'} -> ${storyPoints}`);
177
- const result = await syncStoryPoints(jiraKey, storyPoints, { currentJiraPoints: jiraPoints });
178
- if (result.success) {
179
- if (result.alreadySynced) {
180
- success(` Story points already synced: ${storyPoints}`);
181
- } else {
182
- success(` Story points synced: ${storyPoints}`);
183
- }
184
- } else {
185
- warn(` Could not sync story points: ${result.reason}`);
186
- }
187
- }
188
-
189
- synced++;
190
- }
191
-
192
- // Summary
193
- console.log('');
194
- console.log('==========================================');
195
- success(`Summary: ${synced} synced, ${skipped} skipped, ${errors} errors`);
196
- console.log('==========================================');
197
-
198
- process.exit(errors > 0 ? 1 : 0);
@@ -1,264 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * add-short-names.mjs
4
- *
5
- * Pre-generates shortName field for all characters in theme YAML files.
6
- * Finds the shortest unique identifier that distinguishes each character.
7
- *
8
- * Usage:
9
- * node add-short-names.mjs # Dry run - show what would change
10
- * node add-short-names.mjs --write # Actually write changes
11
- * node add-short-names.mjs --theme discworld # Only process one theme
12
- */
13
-
14
- import { readFileSync, writeFileSync, readdirSync } from 'fs';
15
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
16
- import { join, dirname } from 'path';
17
- import { fileURLToPath } from 'url';
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
- const themesDir = join(__dirname, '..', 'personas', 'themes');
22
-
23
- // Common titles/prefixes to strip for comparison
24
- const SKIP_PREFIXES = new Set([
25
- 'the', 'dr.', 'dr', 'captain', 'admiral', 'colonel', 'lieutenant', 'commander',
26
- 'president', 'lord', 'lady', 'sir', 'professor', 'inspector', 'sergeant',
27
- 'mr.', 'mr', 'mrs.', 'mrs', 'miss', 'ms.', 'ms', 'chief', 'major', 'general',
28
- 'king', 'queen', 'prince', 'princess', 'duke', 'earl', 'count', 'baron',
29
- 'first', 'grand', 'arch', 'high',
30
- // Family/religious titles
31
- 'uncle', 'aunt', 'brother', 'sister', 'father', 'mother', 'friar',
32
- // Role titles
33
- 'avatar', 'agent', 'detective', 'officer', 'private', 'corporal',
34
- 'chancellor', 'ambassador', 'senator', 'governor', 'minister',
35
- // Honorifics
36
- 'master', 'young', 'old', 'elder', 'reverend', 'bishop', 'cardinal'
37
- ]);
38
-
39
- // Words that make poor short names on their own (contextless or too generic)
40
- const POOR_SHORT_NAMES = new Set([
41
- 'big', 'little', 'old', 'young', 'true', 'false', 'good', 'bad',
42
- 'thought', 'ministry', 'situation', 'room', 'place', 'house',
43
- 'superintendent', 'commander', 'speaker', 'council',
44
- 'mode', 'narrator', 'chronicler',
45
- // Avoid single/double initials
46
- 'h.m.', 'j.f.', 'a.w.', 'e.b.', 'l.'
47
- ]);
48
-
49
- // Names that should use the full form (iconic two-word names)
50
- const USE_FULL_NAME = new Set([
51
- 'big brother',
52
- 'sun tzu'
53
- ]);
54
-
55
- /**
56
- * Extract quoted nickname from character name if present
57
- * e.g., 'Colonel John "Hannibal" Smith' -> 'Hannibal'
58
- * Returns null if no nickname or if nickname is multi-word (like "Howling Mad")
59
- */
60
- function extractNickname(name) {
61
- const match = name.match(/["']([^"']+)["']/);
62
- if (match) {
63
- const nickname = match[1].trim();
64
- // Only use single-word nicknames (skip descriptive like "Howling Mad")
65
- if (nickname && !nickname.includes(' ')) {
66
- return nickname;
67
- }
68
- }
69
- return null;
70
- }
71
-
72
- /**
73
- * Clean character name by removing parenthetical annotations and slash alternatives
74
- */
75
- function cleanName(name) {
76
- // Remove parenthetical annotations like '(Season 1)', '(Architect)', '(DevOps)'
77
- let cleaned = name.replace(/\s*\([^)]+\)\s*/g, ' ').trim();
78
- // For slash alternatives like 'Commander/Captain John', take the last part
79
- // e.g., 'Commander/Captain' becomes just 'Captain', 'John Sheridan' stays
80
- cleaned = cleaned.replace(/\b\w+\/(\w+)\s/g, '$1 ');
81
- // Remove quoted nicknames like "Apollo" or 'Bones'
82
- cleaned = cleaned.replace(/\s*["'][^"']+["']\s*/g, ' ').trim();
83
- // Collapse multiple spaces
84
- cleaned = cleaned.replace(/\s+/g, ' ').trim();
85
- return cleaned;
86
- }
87
-
88
- /**
89
- * Tokenize a name into meaningful parts
90
- */
91
- function tokenize(name) {
92
- const cleaned = cleanName(name);
93
- const words = cleaned.split(/\s+/).filter(w => w.length > 0);
94
-
95
- // Filter out prefixes and single-letter initials (like R. or L.)
96
- const filtered = words.filter(w => {
97
- const lower = w.toLowerCase();
98
- if (SKIP_PREFIXES.has(lower)) return false;
99
- if (/^[A-Z]\.$/.test(w)) return false; // Single initial like R.
100
- if (/^[IVXLCDM]+$/.test(w)) return false; // Roman numerals
101
- return true;
102
- });
103
-
104
- return filtered.length > 0 ? filtered : words;
105
- }
106
-
107
- /**
108
- * Compute display name map for all characters in a theme
109
- */
110
- function computeShortNames(agents) {
111
- const shortNames = new Map();
112
- const characters = Object.values(agents)
113
- .filter(a => a?.character)
114
- .map(a => a.character);
115
-
116
- /**
117
- * Check if a candidate is unique among all characters
118
- */
119
- function isUnique(candidate, exceptFor) {
120
- const candidateLower = candidate.toLowerCase();
121
- for (const char of characters) {
122
- if (char === exceptFor) continue;
123
- const tokens = tokenize(char);
124
- if (tokens.some(t => t.toLowerCase() === candidateLower)) return false;
125
- }
126
- return true;
127
- }
128
-
129
- /**
130
- * Check if a candidate would make a good short name
131
- */
132
- function isGoodShortName(candidate) {
133
- const lower = candidate.toLowerCase();
134
- return !POOR_SHORT_NAMES.has(lower) && candidate.length > 1;
135
- }
136
-
137
- /**
138
- * Find the best short name for a character
139
- */
140
- function findShortName(fullName) {
141
- const cleaned = cleanName(fullName);
142
-
143
- // Check if this is an iconic name that should stay full
144
- if (USE_FULL_NAME.has(cleaned.toLowerCase())) {
145
- return cleaned;
146
- }
147
-
148
- // Strategy 0: Prefer quoted nickname if present (e.g., "Hannibal", "Starbuck")
149
- const nickname = extractNickname(fullName);
150
- if (nickname && isGoodShortName(nickname)) {
151
- return nickname;
152
- }
153
-
154
- const tokens = tokenize(fullName);
155
-
156
- if (tokens.length === 0) {
157
- return cleaned;
158
- }
159
-
160
- if (tokens.length === 1) {
161
- return tokens[0];
162
- }
163
-
164
- // Strategy 1: First token (if good and unique)
165
- if (isGoodShortName(tokens[0]) && isUnique(tokens[0], fullName)) {
166
- return tokens[0];
167
- }
168
-
169
- // Strategy 2: Last token (surname, if good and unique)
170
- const lastToken = tokens[tokens.length - 1];
171
- if (isGoodShortName(lastToken) && isUnique(lastToken, fullName)) {
172
- return lastToken;
173
- }
174
-
175
- // Strategy 3: First + Last
176
- if (tokens.length >= 2) {
177
- const firstLast = `${tokens[0]} ${lastToken}`;
178
- if (isUnique(firstLast, fullName)) {
179
- return firstLast;
180
- }
181
- }
182
-
183
- // Fallback: cleaned full name
184
- return cleanName(fullName);
185
- }
186
-
187
- // Compute short name for each character
188
- for (const char of characters) {
189
- shortNames.set(char, findShortName(char));
190
- }
191
-
192
- return shortNames;
193
- }
194
-
195
- /**
196
- * Process a single theme file
197
- */
198
- function processTheme(filename, dryRun = true) {
199
- const filepath = join(themesDir, filename);
200
- const content = readFileSync(filepath, 'utf-8');
201
- const theme = parseYaml(content);
202
-
203
- if (!theme?.agents) {
204
- console.log(` Skipping ${filename} - no agents found`);
205
- return { changes: 0, filename };
206
- }
207
-
208
- const shortNames = computeShortNames(theme.agents);
209
- let changes = 0;
210
-
211
- for (const [role, agent] of Object.entries(theme.agents)) {
212
- if (!agent?.character) continue;
213
-
214
- const shortName = shortNames.get(agent.character);
215
- const existing = agent.shortName;
216
-
217
- if (shortName && shortName !== existing) {
218
- if (dryRun) {
219
- const existingNote = existing ? ` (was: "${existing}")` : '';
220
- console.log(` ${role}: "${agent.character}" -> "${shortName}"${existingNote}`);
221
- }
222
- agent.shortName = shortName;
223
- changes++;
224
- }
225
- }
226
-
227
- if (!dryRun && changes > 0) {
228
- // Write back with yaml stringify
229
- const newContent = stringifyYaml(theme, { lineWidth: 0 });
230
- writeFileSync(filepath, newContent, 'utf-8');
231
- console.log(` Wrote ${changes} changes to ${filename}`);
232
- }
233
-
234
- return { changes, filename };
235
- }
236
-
237
- // Main execution
238
- const args = process.argv.slice(2);
239
- const dryRun = !args.includes('--write');
240
- const themeArg = args.find(a => a.startsWith('--theme='))?.split('=')[1];
241
- const singleTheme = args.includes('--theme') ? args[args.indexOf('--theme') + 1] : themeArg;
242
-
243
- console.log(dryRun ? 'šŸ” DRY RUN - No files will be modified\n' : 'āœļø WRITING CHANGES\n');
244
-
245
- let files = readdirSync(themesDir).filter(f => f.endsWith('.yaml'));
246
- if (singleTheme) {
247
- files = files.filter(f => f === `${singleTheme}.yaml` || f === singleTheme);
248
- }
249
-
250
- let totalChanges = 0;
251
- for (const file of files.sort()) {
252
- console.log(`\nšŸ“ ${file}:`);
253
- const { changes } = processTheme(file, dryRun);
254
- totalChanges += changes;
255
- if (changes === 0) {
256
- console.log(' (no changes needed)');
257
- }
258
- }
259
-
260
- console.log(`\n${'='.repeat(50)}`);
261
- console.log(`Total: ${totalChanges} changes across ${files.length} themes`);
262
- if (dryRun && totalChanges > 0) {
263
- console.log('\nRun with --write to apply changes');
264
- }