@pennyfarthing/core 7.7.0 → 7.8.1

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 (58) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/packages/core/dist/cli/commands/doctor.d.ts +3 -0
  4. package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
  5. package/packages/core/dist/cli/commands/doctor.js +134 -9
  6. package/packages/core/dist/cli/commands/doctor.js.map +1 -1
  7. package/pennyfarthing-dist/agents/sm-setup.md +37 -2
  8. package/pennyfarthing-dist/agents/sm.md +68 -22
  9. package/pennyfarthing-dist/agents/workflow-status-check.md +11 -1
  10. package/pennyfarthing-dist/commands/git-cleanup.md +43 -308
  11. package/pennyfarthing-dist/commands/solo.md +31 -0
  12. package/pennyfarthing-dist/guides/patterns/approval-gates-pattern.md +1 -1
  13. package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +83 -83
  14. package/pennyfarthing-dist/personas/themes/the-expanse.yaml +11 -11
  15. package/pennyfarthing-dist/scripts/core/agent-session.sh +2 -2
  16. package/pennyfarthing-dist/scripts/core/check-context.sh +3 -0
  17. package/pennyfarthing-dist/scripts/core/handoff-marker.sh +13 -2
  18. package/pennyfarthing-dist/scripts/core/prime.sh +3 -157
  19. package/pennyfarthing-dist/scripts/core/run.sh +9 -0
  20. package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
  21. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +117 -20
  22. package/pennyfarthing-dist/scripts/jira/README.md +10 -7
  23. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +13 -0
  24. package/pennyfarthing-dist/scripts/misc/add_short_names.py +226 -0
  25. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +6 -5
  26. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +319 -0
  27. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +6 -5
  28. package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +270 -0
  29. package/pennyfarthing-dist/scripts/test/ensure-swebench-data.sh +59 -0
  30. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -6
  31. package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +402 -0
  32. package/pennyfarthing-dist/scripts/workflow/check.sh +3 -476
  33. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +61 -0
  34. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +13 -0
  35. package/pennyfarthing-dist/skills/judge/SKILL.md +57 -0
  36. package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +4 -22
  37. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +83 -0
  38. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-02-categorize.md +116 -0
  39. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-03-execute.md +210 -0
  40. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +88 -0
  41. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +71 -0
  42. package/pennyfarthing-dist/workflows/git-cleanup.yaml +59 -0
  43. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.mjs +0 -393
  44. package/pennyfarthing-dist/scripts/hooks/tests/question-reflector.test.mjs +0 -545
  45. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.mjs +0 -327
  46. package/pennyfarthing-dist/scripts/jira/jira-bidirectional-sync.test.mjs +0 -503
  47. package/pennyfarthing-dist/scripts/jira/jira-lib.mjs +0 -443
  48. package/pennyfarthing-dist/scripts/jira/jira-sync-story.mjs +0 -208
  49. package/pennyfarthing-dist/scripts/jira/jira-sync.mjs +0 -198
  50. package/pennyfarthing-dist/scripts/misc/add-short-names.mjs +0 -264
  51. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.mjs +0 -474
  52. package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +0 -377
  53. package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +0 -492
  54. /package/pennyfarthing-dist/guides/{AGENT-COORDINATION.md → agent-coordination.md} +0 -0
  55. /package/pennyfarthing-dist/guides/{HOOKS.md → hooks.md} +0 -0
  56. /package/pennyfarthing-dist/guides/{PROMPT-PATTERNS.md → prompt-patterns.md} +0 -0
  57. /package/pennyfarthing-dist/guides/{SESSION-ARTIFACTS.md → session-artifacts.md} +0 -0
  58. /package/pennyfarthing-dist/guides/{XML-TAGS.md → xml-tags.md} +0 -0
@@ -1,443 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * jira-lib.mjs - Shared Jira functions for Pennyfarthing
4
- *
5
- * Provides utilities for interacting with Jira using the jira CLI
6
- * and REST API for operations the CLI doesn't support well.
7
- */
8
-
9
- import { execSync, spawnSync } from 'child_process';
10
- import { readFileSync, existsSync } from 'fs';
11
- import { dirname, join } from 'path';
12
- import { fileURLToPath } from 'url';
13
-
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = dirname(__filename);
16
-
17
- // Configuration
18
- export const JIRA_PROJECT = process.env.JIRA_PROJECT || 'MSSCI';
19
- export const JIRA_URL = process.env.JIRA_URL || 'https://1898andco.atlassian.net';
20
-
21
- // ANSI colors
22
- const colors = {
23
- red: '\x1b[31m',
24
- green: '\x1b[32m',
25
- yellow: '\x1b[33m',
26
- blue: '\x1b[34m',
27
- reset: '\x1b[0m'
28
- };
29
-
30
- /**
31
- * Output helpers
32
- */
33
- export function success(msg) {
34
- console.error(`${colors.green}[OK]${colors.reset} ${msg}`);
35
- }
36
-
37
- export function info(msg) {
38
- console.error(`${colors.blue}[INFO]${colors.reset} ${msg}`);
39
- }
40
-
41
- export function warn(msg) {
42
- console.error(`${colors.yellow}[WARN]${colors.reset} ${msg}`);
43
- }
44
-
45
- export function error(msg) {
46
- console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`);
47
- }
48
-
49
- /**
50
- * Find project root by looking for .claude directory
51
- */
52
- export function findProjectRoot(startDir = process.cwd()) {
53
- let dir = startDir;
54
- while (dir !== '/') {
55
- if (existsSync(join(dir, '.claude'))) {
56
- return dir;
57
- }
58
- dir = dirname(dir);
59
- }
60
- throw new Error('Could not find project root (no .claude/ directory found)');
61
- }
62
-
63
- /**
64
- * Check if jira CLI and dependencies are available
65
- */
66
- export function checkDependencies(options = {}) {
67
- const { quiet = false } = options;
68
- const missing = [];
69
-
70
- // Check for jira CLI
71
- try {
72
- execSync('which jira', { stdio: 'pipe' });
73
- if (!quiet) success('jira installed');
74
- } catch {
75
- missing.push('jira');
76
- error('jira not found');
77
- console.error(' Install with: brew install ankitpokhrel/jira-cli/jira-cli');
78
- console.error(' Then run: jira init');
79
- }
80
-
81
- // Check for JIRA_API_TOKEN
82
- if (!process.env.JIRA_API_TOKEN) {
83
- missing.push('JIRA_API_TOKEN');
84
- error('JIRA_API_TOKEN not set');
85
- console.error(' Create token at: https://id.atlassian.com/manage-profile/security/api-tokens');
86
- console.error(' Then: export JIRA_API_TOKEN="your-token"');
87
- } else if (!quiet) {
88
- success('JIRA_API_TOKEN set');
89
- }
90
-
91
- // Check jira config
92
- const configPath = join(process.env.HOME, '.config/.jira/.config.yml');
93
- if (existsSync(configPath)) {
94
- if (!quiet) success('jira configured');
95
- } else if (missing.indexOf('jira') === -1) {
96
- missing.push('jira-config');
97
- error('jira not configured');
98
- console.error(' Run: jira init');
99
- }
100
-
101
- if (missing.length > 0) {
102
- console.error('');
103
- error(`Missing: ${missing.join(', ')}`);
104
- return false;
105
- }
106
-
107
- console.error('');
108
- return true;
109
- }
110
-
111
- /**
112
- * Execute jira CLI command and return output
113
- */
114
- export function jiraExec(args, options = {}) {
115
- const { silent = false, allowFailure = false } = options;
116
-
117
- try {
118
- const result = spawnSync('jira', args, {
119
- encoding: 'utf-8',
120
- stdio: silent ? 'pipe' : ['pipe', 'pipe', 'pipe']
121
- });
122
-
123
- if (result.status !== 0 && !allowFailure) {
124
- throw new Error(result.stderr || `jira command failed with exit code ${result.status}`);
125
- }
126
-
127
- return {
128
- stdout: result.stdout || '',
129
- stderr: result.stderr || '',
130
- status: result.status
131
- };
132
- } catch (err) {
133
- if (allowFailure) {
134
- return { stdout: '', stderr: err.message, status: 1 };
135
- }
136
- throw err;
137
- }
138
- }
139
-
140
- /**
141
- * Get issue as JSON from Jira
142
- */
143
- export function getIssueJson(issueKey) {
144
- try {
145
- const result = jiraExec(['issue', 'view', issueKey, '--raw'], { silent: true, allowFailure: true });
146
- if (result.status !== 0 || !result.stdout) {
147
- return null;
148
- }
149
- return JSON.parse(result.stdout);
150
- } catch {
151
- return null;
152
- }
153
- }
154
-
155
- /**
156
- * Extract field from Jira issue JSON
157
- */
158
- export function getJiraField(issueJson, fieldPath, defaultValue = null) {
159
- if (!issueJson) return defaultValue;
160
-
161
- const parts = fieldPath.replace(/^\./, '').split('.');
162
- let value = issueJson;
163
-
164
- for (const part of parts) {
165
- if (value === null || value === undefined) return defaultValue;
166
- value = value[part];
167
- }
168
-
169
- return value ?? defaultValue;
170
- }
171
-
172
- /**
173
- * Status mapping: Pennyfarthing -> Jira
174
- */
175
- export function mapStatusToJira(PennyfarthingStatus) {
176
- const mapping = {
177
- 'backlog': 'To Do',
178
- 'todo': 'To Do',
179
- 'in-progress': 'In Progress',
180
- 'in_progress': 'In Progress',
181
- 'active': 'In Progress',
182
- 'review': 'In Review',
183
- 'in-review': 'In Review',
184
- 'in_review': 'In Review',
185
- 'done': 'Done',
186
- 'completed': 'Done',
187
- 'closed': 'Done',
188
- 'blocked': 'Blocked'
189
- };
190
-
191
- return mapping[PennyfarthingStatus?.toLowerCase()] || 'To Do';
192
- }
193
-
194
- /**
195
- * Status mapping: Jira -> Pennyfarthing
196
- */
197
- export function mapJiraToStatus(jiraStatus) {
198
- const mapping = {
199
- 'To Do': 'backlog',
200
- 'Open': 'backlog',
201
- 'Backlog': 'backlog',
202
- 'In Progress': 'in-progress',
203
- 'Active': 'in-progress',
204
- 'In Review': 'review',
205
- 'Review': 'review',
206
- 'Done': 'done',
207
- 'Closed': 'done',
208
- 'Resolved': 'done',
209
- 'Blocked': 'blocked'
210
- };
211
-
212
- return mapping[jiraStatus] || 'backlog';
213
- }
214
-
215
- /**
216
- * Extract Jira key from URL or return as-is
217
- */
218
- export function extractJiraKey(input) {
219
- if (!input) return null;
220
-
221
- // Already in key format
222
- const keyPattern = new RegExp(`^${JIRA_PROJECT}-\\d+$`);
223
- if (keyPattern.test(input)) {
224
- return input;
225
- }
226
-
227
- // Extract from URL
228
- const urlPattern = new RegExp(`(${JIRA_PROJECT}-\\d+)`);
229
- const match = input.match(urlPattern);
230
- return match ? match[1] : input;
231
- }
232
-
233
- /**
234
- * Load sprint YAML file using yq CLI
235
- */
236
- export function loadSprintFile(projectRoot) {
237
- const sprintPath = join(projectRoot, 'sprint', 'current-sprint.yaml');
238
- if (!existsSync(sprintPath)) {
239
- throw new Error(`Sprint file not found: ${sprintPath}`);
240
- }
241
-
242
- // Use yq to convert YAML to JSON, then parse
243
- try {
244
- const json = execSync(`yq -o json '.' "${sprintPath}"`, { encoding: 'utf-8' });
245
- return JSON.parse(json);
246
- } catch (err) {
247
- throw new Error(`Failed to parse sprint file: ${err.message}`);
248
- }
249
- }
250
-
251
- /**
252
- * Find epic in sprint data (handles "24" and "epic-24" formats)
253
- */
254
- export function findEpic(sprintData, epicNum) {
255
- if (!sprintData?.epics) return null;
256
-
257
- return sprintData.epics.find(e =>
258
- e.id === epicNum ||
259
- e.id === `epic-${epicNum}` ||
260
- e.id === epicNum.replace(/^epic-/, '')
261
- );
262
- }
263
-
264
- /**
265
- * Find story in epic
266
- */
267
- export function findStory(epic, storyId) {
268
- if (!epic?.stories) return null;
269
- return epic.stories.find(s => s.id === storyId);
270
- }
271
-
272
- /**
273
- * Get story field from sprint YAML
274
- */
275
- export function getStoryField(sprintData, storyKey, fieldName) {
276
- // Extract epic number from story key (e.g., "24-1" -> "24")
277
- const epicNum = storyKey.split('-')[0];
278
-
279
- const epic = findEpic(sprintData, epicNum);
280
- if (!epic) return null;
281
-
282
- const story = findStory(epic, storyKey);
283
- if (!story) return null;
284
-
285
- return story[fieldName] ?? null;
286
- }
287
-
288
- /**
289
- * Transition issue to new status
290
- */
291
- export function moveIssue(issueKey, targetStatus, options = {}) {
292
- const { dryRun = false } = options;
293
-
294
- if (dryRun) {
295
- warn(`[DRY-RUN] Would move ${issueKey} to: ${targetStatus}`);
296
- return { success: true, dryRun: true };
297
- }
298
-
299
- // Check current status first
300
- const issueJson = getIssueJson(issueKey);
301
- if (issueJson) {
302
- const currentStatus = getJiraField(issueJson, 'fields.status.name');
303
- if (currentStatus?.toLowerCase() === targetStatus.toLowerCase()) {
304
- return { success: true, alreadyAtStatus: true };
305
- }
306
- }
307
-
308
- const result = jiraExec(['issue', 'move', issueKey, targetStatus], { allowFailure: true });
309
- return {
310
- success: result.status === 0,
311
- output: result.stdout + result.stderr
312
- };
313
- }
314
-
315
- /**
316
- * Get story points from issue
317
- */
318
- export function getStoryPoints(issueKey, issueJson = null) {
319
- if (!issueJson) {
320
- issueJson = getIssueJson(issueKey);
321
- }
322
- if (!issueJson) return null;
323
-
324
- // customfield_10031 is Story Points for 1898andco Jira
325
- return getJiraField(issueJson, 'fields.customfield_10031', null);
326
- }
327
-
328
- /**
329
- * Sync story points via REST API (jira CLI doesn't handle custom fields well)
330
- */
331
- export async function syncStoryPoints(issueKey, PennyfarthingPoints, options = {}) {
332
- const { dryRun = false, currentJiraPoints = null } = options;
333
-
334
- if (!PennyfarthingPoints || PennyfarthingPoints === 'null') {
335
- return { success: false, reason: 'No points in Pennyfarthing' };
336
- }
337
-
338
- // Compare current points
339
- const jiraPoints = currentJiraPoints ?? getStoryPoints(issueKey);
340
- const jiraPointsInt = jiraPoints ? Math.floor(Number(jiraPoints)) : null;
341
-
342
- if (jiraPointsInt === Number(PennyfarthingPoints)) {
343
- return { success: true, alreadySynced: true };
344
- }
345
-
346
- if (dryRun) {
347
- warn(`[DRY-RUN] Would sync points for ${issueKey}: ${jiraPoints} -> ${PennyfarthingPoints}`);
348
- return { success: true, dryRun: true };
349
- }
350
-
351
- // Use REST API to update custom field
352
- const jiraUser = process.env.JIRA_USER || 'keith.avery@1898andco.io';
353
- const apiUrl = `${JIRA_URL}/rest/api/3/issue/${issueKey}`;
354
-
355
- try {
356
- const response = await fetch(apiUrl, {
357
- method: 'PUT',
358
- headers: {
359
- 'Authorization': `Basic ${Buffer.from(`${jiraUser}:${process.env.JIRA_API_TOKEN}`).toString('base64')}`,
360
- 'Content-Type': 'application/json'
361
- },
362
- body: JSON.stringify({
363
- fields: {
364
- customfield_10031: Number(PennyfarthingPoints)
365
- }
366
- })
367
- });
368
-
369
- if (response.ok || response.status === 204) {
370
- return { success: true };
371
- } else {
372
- return { success: false, reason: `HTTP ${response.status}` };
373
- }
374
- } catch (err) {
375
- return { success: false, reason: err.message };
376
- }
377
- }
378
-
379
- /**
380
- * Add comment to issue
381
- */
382
- export function addComment(issueKey, comment, options = {}) {
383
- const { dryRun = false } = options;
384
-
385
- if (dryRun) {
386
- warn(`[DRY-RUN] Would add comment to ${issueKey}`);
387
- return { success: true, dryRun: true };
388
- }
389
-
390
- const result = jiraExec(['issue', 'comment', 'add', issueKey, comment], { allowFailure: true });
391
- return {
392
- success: result.status === 0,
393
- output: result.stdout + result.stderr
394
- };
395
- }
396
-
397
- /**
398
- * GitHub username to Jira email mapping
399
- */
400
- export function mapGithubToJira(githubUser) {
401
- const mapping = {
402
- 'slabgorb': 'keith.avery@1898andco.io',
403
- 'arcaven': 'michael.pursifull@1898andco.io',
404
- 'RoseSecurity': 'michael.rosenfeld@1898andco.io',
405
- 'Zious11': 'jared.richards@1898andco.io',
406
- 'drbothen': 'joshua.magady@1898andco.io'
407
- };
408
-
409
- return mapping[githubUser] || `${githubUser}@1898andco.io`;
410
- }
411
-
412
- /**
413
- * Parse command line arguments
414
- */
415
- export function parseArgs(argv, spec) {
416
- const args = { _positional: [] };
417
-
418
- for (let i = 2; i < argv.length; i++) {
419
- const arg = argv[i];
420
-
421
- if (arg.startsWith('--')) {
422
- const key = arg.slice(2);
423
- const specEntry = spec[key];
424
-
425
- if (specEntry?.type === 'boolean') {
426
- args[key] = true;
427
- } else if (specEntry?.type === 'string') {
428
- args[key] = argv[++i];
429
- } else {
430
- // Unknown flag, treat as boolean
431
- args[key] = true;
432
- }
433
- } else if (arg.startsWith('-')) {
434
- // Short flags
435
- const key = arg.slice(1);
436
- args[key] = true;
437
- } else {
438
- args._positional.push(arg);
439
- }
440
- }
441
-
442
- return args;
443
- }
@@ -1,208 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * jira-sync-story.mjs - Sync a single story to Jira
4
- *
5
- * Usage: node jira-sync-story.mjs <story_key> [--transition] [--points] [--comment "message"]
6
- *
7
- * Options:
8
- * --transition Transition story to match Pennyfarthing status
9
- * --points Sync story points from Pennyfarthing to Jira
10
- * --comment Add a comment to the story
11
- */
12
-
13
- import {
14
- checkDependencies,
15
- findProjectRoot,
16
- loadSprintFile,
17
- findEpic,
18
- findStory,
19
- extractJiraKey,
20
- mapStatusToJira,
21
- getIssueJson,
22
- getJiraField,
23
- moveIssue,
24
- getStoryPoints,
25
- syncStoryPoints,
26
- addComment,
27
- parseArgs,
28
- success,
29
- info,
30
- warn,
31
- error,
32
- JIRA_URL
33
- } from './jira-lib.mjs';
34
-
35
- // Parse arguments
36
- const args = parseArgs(process.argv, {
37
- 'transition': { type: 'boolean' },
38
- 'points': { type: 'boolean' },
39
- 'comment': { type: 'string' }
40
- });
41
-
42
- const storyKey = args._positional[0];
43
- const doTransition = args.transition || false;
44
- const doSyncPoints = args.points || false;
45
- const commentText = args.comment || null;
46
-
47
- // Show usage if no story key provided
48
- if (!storyKey) {
49
- error('Story key required');
50
- console.log(`
51
- Usage: jira-sync-story.mjs <story_key> [--transition] [--points] [--comment "message"]
52
-
53
- Examples:
54
- jira-sync-story.mjs 24-1 # Show story status
55
- jira-sync-story.mjs 24-1 --transition # Sync status to Jira
56
- jira-sync-story.mjs 24-1 --points # Sync story points
57
- jira-sync-story.mjs 24-1 --transition --points
58
- jira-sync-story.mjs 24-1 --comment "Started development"
59
- `);
60
- process.exit(2);
61
- }
62
-
63
- // Check dependencies (quiet mode - we'll show story info instead)
64
- if (!checkDependencies({ quiet: false })) {
65
- process.exit(2);
66
- }
67
-
68
- // Find project root and load sprint data
69
- let projectRoot;
70
- let sprintData;
71
-
72
- try {
73
- projectRoot = findProjectRoot();
74
- sprintData = loadSprintFile(projectRoot);
75
- } catch (err) {
76
- error(err.message);
77
- process.exit(1);
78
- }
79
-
80
- // Extract epic number from story key (e.g., "24-1" -> "24")
81
- const epicNum = storyKey.split('-')[0];
82
-
83
- // Find epic and story
84
- const epic = findEpic(sprintData, epicNum);
85
- if (!epic) {
86
- error(`Epic ${epicNum} not found in sprint file`);
87
- process.exit(1);
88
- }
89
-
90
- const story = findStory(epic, storyKey);
91
- if (!story) {
92
- error(`Story ${storyKey} not found in epic ${epicNum}`);
93
- process.exit(1);
94
- }
95
-
96
- // Get story details from Pennyfarthing
97
- const PennyfarthingStatus = story.status || 'backlog';
98
- const storyJira = story.jira;
99
- const storyBranch = story.branch;
100
- const storyPr = story.pr;
101
- const storyPoints = story.points;
102
-
103
- // Check if synced to Jira
104
- if (!storyJira || storyJira === 'null') {
105
- warn(`Story ${storyKey} not synced to Jira yet`);
106
- console.log('');
107
- console.log('To create this story in Jira, use:');
108
- console.log(` jira issue create -tStory -s"Story ${storyKey}" -yHigh`);
109
- process.exit(1);
110
- }
111
-
112
- const jiraKey = extractJiraKey(storyJira);
113
-
114
- info(`Story: ${storyKey}`);
115
- info(`Jira: ${jiraKey}`);
116
- console.log('');
117
-
118
- // Fetch current Jira state
119
- const issueJson = getIssueJson(jiraKey);
120
- if (!issueJson) {
121
- error(`Could not fetch issue ${jiraKey}`);
122
- process.exit(2);
123
- }
124
-
125
- const jiraStatus = getJiraField(issueJson, 'fields.status.name', 'Unknown');
126
- const jiraAssignee = getJiraField(issueJson, 'fields.assignee.displayName', 'Unassigned');
127
- const jiraSummary = getJiraField(issueJson, 'fields.summary', 'No summary');
128
- const jiraPoints = getStoryPoints(jiraKey, issueJson);
129
-
130
- // Display current state
131
- console.log(` Summary: ${jiraSummary}`);
132
- console.log(` Jira Status: ${jiraStatus}`);
133
- console.log(` Assignee: ${jiraAssignee}`);
134
- console.log(` Pennyfarthing Status: ${PennyfarthingStatus}`);
135
- if (storyPoints) console.log(` Pennyfarthing Points: ${storyPoints}`);
136
- if (jiraPoints) console.log(` Jira Points: ${jiraPoints}`);
137
- if (storyBranch) console.log(` Branch: ${storyBranch}`);
138
- if (storyPr) console.log(` PR: ${storyPr}`);
139
- console.log('');
140
-
141
- // Map Pennyfarthing status to target Jira status
142
- const targetJiraStatus = mapStatusToJira(PennyfarthingStatus);
143
-
144
- // Transition if requested
145
- if (doTransition) {
146
- if (jiraStatus === targetJiraStatus) {
147
- success(`Already at correct status: ${jiraStatus}`);
148
- } else {
149
- info(`Transitioning: ${jiraStatus} -> ${targetJiraStatus}`);
150
- const result = moveIssue(jiraKey, targetJiraStatus);
151
- if (result.success) {
152
- success(`Transitioned to ${targetJiraStatus}`);
153
- } else {
154
- warn('Could not transition (status may not be available from current state)');
155
- }
156
- }
157
- }
158
-
159
- // Sync story points if requested
160
- if (doSyncPoints) {
161
- info(`Syncing story points: ${jiraPoints || 'unset'} -> ${storyPoints || 'unset'}`);
162
- const result = await syncStoryPoints(jiraKey, storyPoints, { currentJiraPoints: jiraPoints });
163
- if (result.success) {
164
- if (result.alreadySynced) {
165
- success(`Story points already synced: ${storyPoints}`);
166
- } else {
167
- success(`Story points synced: ${storyPoints}`);
168
- }
169
- } else {
170
- warn(`Could not sync story points: ${result.reason}`);
171
- }
172
- }
173
-
174
- // Add comment if provided
175
- if (commentText) {
176
- info('Adding comment...');
177
- const result = addComment(jiraKey, commentText);
178
- if (result.success) {
179
- success('Comment added');
180
- } else {
181
- warn('Could not add comment');
182
- }
183
- }
184
-
185
- // Auto-comment for transitions (if no manual comment provided)
186
- if (doTransition && !commentText) {
187
- let autoComment = null;
188
-
189
- if (PennyfarthingStatus === 'in-progress' && storyBranch) {
190
- autoComment = `**Development Started**\n\nBranch: \`${storyBranch}\`\n\nSynced from Pennyfarthing`;
191
- } else if (PennyfarthingStatus === 'review' && storyPr) {
192
- autoComment = `**Ready for Review**\n\nPR: ${storyPr}\n\nSynced from Pennyfarthing`;
193
- } else if (PennyfarthingStatus === 'done') {
194
- const today = new Date().toISOString().split('T')[0];
195
- autoComment = `**Completed**\n\nSynced from Pennyfarthing on ${today}`;
196
- }
197
-
198
- if (autoComment) {
199
- info('Adding sync comment...');
200
- const result = addComment(jiraKey, autoComment);
201
- if (result.success) {
202
- success('Sync comment added');
203
- }
204
- }
205
- }
206
-
207
- console.log('');
208
- success(`${JIRA_URL}/browse/${jiraKey}`);