@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.
- package/README.md +109 -201
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +205 -0
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.js +31 -0
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.js +31 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/pennyfarthing-dist/agents/architect.md +48 -53
- package/pennyfarthing-dist/agents/dev.md +74 -164
- package/pennyfarthing-dist/agents/devops.md +44 -39
- package/pennyfarthing-dist/agents/handoff.md +46 -23
- package/pennyfarthing-dist/agents/orchestrator.md +84 -255
- package/pennyfarthing-dist/agents/pm.md +40 -50
- package/pennyfarthing-dist/agents/reviewer-preflight.md +58 -26
- package/pennyfarthing-dist/agents/reviewer.md +107 -298
- package/pennyfarthing-dist/agents/sm-file-summary.md +51 -30
- package/pennyfarthing-dist/agents/sm-finish.md +59 -38
- package/pennyfarthing-dist/agents/sm-handoff.md +40 -33
- package/pennyfarthing-dist/agents/sm-setup.md +122 -45
- package/pennyfarthing-dist/agents/sm.md +204 -545
- package/pennyfarthing-dist/agents/tea.md +77 -146
- package/pennyfarthing-dist/agents/tech-writer.md +43 -24
- package/pennyfarthing-dist/agents/testing-runner.md +73 -30
- package/pennyfarthing-dist/agents/ux-designer.md +39 -25
- package/pennyfarthing-dist/agents/workflow-status-check.md +45 -17
- package/pennyfarthing-dist/commands/benchmark.md +19 -1
- package/pennyfarthing-dist/commands/continue-session.md +1 -1
- package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
- package/pennyfarthing-dist/commands/solo.md +36 -0
- package/pennyfarthing-dist/commands/theme-maker.md +5 -5
- package/pennyfarthing-dist/commands/work.md +1 -1
- package/pennyfarthing-dist/guides/agent-behavior.md +22 -9
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +432 -0
- package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +27 -7
- package/pennyfarthing-dist/guides/scale-levels.md +114 -0
- package/pennyfarthing-dist/guides/xml-tags.md +335 -0
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
- package/pennyfarthing-dist/scripts/core/agent-session.sh +13 -7
- package/pennyfarthing-dist/scripts/core/check-context.sh +9 -1
- package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +3 -132
- package/pennyfarthing-dist/scripts/core/run.sh +9 -0
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +45 -4
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +32 -7
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +30 -11
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +80 -23
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +4 -4
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +499 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +7 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +94 -0
- package/pennyfarthing-dist/scripts/jira/README.md +10 -7
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +10 -152
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +14 -4
- package/pennyfarthing-dist/scripts/jira/jira-sync.sh +12 -4
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +11 -99
- package/pennyfarthing-dist/scripts/lib/common.sh +55 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +97 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +27 -22
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +14 -154
- package/pennyfarthing-dist/scripts/story/size-story.sh +12 -192
- package/pennyfarthing-dist/scripts/story/story-template.sh +12 -156
- package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
- package/pennyfarthing-dist/scripts/test/ground-truth-judge.py +24 -93
- package/pennyfarthing-dist/scripts/test/swebench-judge.py +33 -59
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +575 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +502 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
- package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
- package/pennyfarthing-dist/skills/skill-registry.yaml +52 -16
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
- package/pennyfarthing-dist/skills/sprint/skill.md +1 -1
- package/pennyfarthing-dist/templates/settings.local.json.template +11 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
- package/pennyfarthing-dist/guides/XML-TAGS.md +0 -156
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -380
- package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
- package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
- package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
- package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
- package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
- /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md ā agent-coordination.md} +0 -0
- /package/pennyfarthing-dist/guides/{HOOKS.md ā hooks.md} +0 -0
- /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md ā prompt-patterns.md} +0 -0
- /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
|
-
}
|