@pennyfarthing/core 7.4.1 → 7.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts +13 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.d.ts.map +1 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +207 -0
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -0
- package/packages/core/dist/cli/commands/doctor.d.ts +16 -0
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +130 -2
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +17 -27
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +21 -52
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +15 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +148 -2
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/cli/utils/themes.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/themes.js +9 -0
- package/packages/core/dist/cli/utils/themes.js.map +1 -1
- package/pennyfarthing-dist/agents/dev.md +29 -24
- package/pennyfarthing-dist/agents/handoff.md +42 -119
- package/pennyfarthing-dist/agents/reviewer.md +32 -37
- package/pennyfarthing-dist/agents/sm-handoff.md +43 -66
- package/pennyfarthing-dist/agents/sm.md +52 -35
- package/pennyfarthing-dist/agents/tea.md +25 -8
- package/pennyfarthing-dist/agents/testing-runner.md +4 -4
- package/pennyfarthing-dist/commands/architect.md +0 -55
- package/pennyfarthing-dist/commands/dev.md +1 -54
- package/pennyfarthing-dist/commands/devops.md +0 -52
- package/pennyfarthing-dist/commands/health-check.md +33 -0
- package/pennyfarthing-dist/commands/orchestrator.md +0 -49
- package/pennyfarthing-dist/commands/pm.md +0 -53
- package/pennyfarthing-dist/commands/reviewer.md +1 -58
- package/pennyfarthing-dist/commands/sm.md +1 -64
- package/pennyfarthing-dist/commands/sprint.md +133 -0
- package/pennyfarthing-dist/commands/standalone.md +194 -0
- package/pennyfarthing-dist/commands/tea.md +1 -57
- package/pennyfarthing-dist/commands/tech-writer.md +0 -46
- package/pennyfarthing-dist/commands/theme-maker.md +10 -5
- package/pennyfarthing-dist/commands/ux-designer.md +0 -55
- package/pennyfarthing-dist/guides/XML-TAGS.md +156 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +64 -38
- package/pennyfarthing-dist/guides/measurement-framework.md +210 -0
- package/pennyfarthing-dist/personas/themes/a-team.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/alice-in-wonderland.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/ancient-strategists.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/arcane.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/better-call-saul.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/big-lebowski.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/black-sails.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/blade-runner.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/bobiverse.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/breaking-bad.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/count-of-monte-cristo.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/cowboy-bebop.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/deadwood.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/dickens.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/discworld.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/doctor-who.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/don-quixote.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/dune.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/enlightenment-thinkers.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/expeditionary-force.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/futurama.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/game-of-thrones.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/gilligans-island.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/gothic-literature.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/great-gatsby.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/hannibal.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/harry-potter.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/his-dark-materials.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/inspector-morse.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/jane-austen.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/legion-of-doom.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/mad-max.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/moby-dick.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/neuromancer.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/parks-and-rec.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/princess-bride.yaml +130 -0
- package/pennyfarthing-dist/personas/themes/renaissance-masters.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/russian-masters.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/sandman.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/scientific-revolutionaries.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/shakespeare.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/star-trek-tng.yaml +139 -3
- package/pennyfarthing-dist/personas/themes/star-trek-tos.yaml +124 -0
- package/pennyfarthing-dist/personas/themes/star-wars.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/succession.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/superfriends.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/ted-lasso.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/the-americans.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-expanse.yaml +131 -1
- package/pennyfarthing-dist/personas/themes/the-good-place.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-matrix.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/the-sopranos.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/west-wing.yaml +6 -6
- package/pennyfarthing-dist/personas/themes/world-explorers.yaml +1 -1
- package/pennyfarthing-dist/personas/themes/wwii-leaders.yaml +1 -1
- package/pennyfarthing-dist/scripts/core/check-context.sh +23 -6
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +95 -0
- package/pennyfarthing-dist/scripts/git/release.sh +3 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +162 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +87 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/deploy.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +25 -32
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.mjs +377 -0
- package/pennyfarthing-dist/scripts/sprint/import-epic-to-future.sh +9 -0
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.js +492 -0
- package/pennyfarthing-dist/scripts/theme/compute-theme-tiers.sh +8 -200
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +38 -5
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +40 -0
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +12 -7
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-04-final-validation.md +11 -3
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +122 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/workflow.yaml +3 -2
- package/packages/core/dist/workflow/generic-handoff.d.ts +0 -281
- package/packages/core/dist/workflow/generic-handoff.d.ts.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.js +0 -411
- package/packages/core/dist/workflow/generic-handoff.js.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.test.d.ts +0 -21
- package/packages/core/dist/workflow/generic-handoff.test.d.ts.map +0 -1
- package/packages/core/dist/workflow/generic-handoff.test.js +0 -499
- package/packages/core/dist/workflow/generic-handoff.test.js.map +0 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* import-epic-to-future.mjs - Import epics-and-stories workflow output to future.yaml
|
|
4
|
+
*
|
|
5
|
+
* Transforms the markdown output from the epics-and-stories workflow into
|
|
6
|
+
* the YAML format used by sprint/future.yaml.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node import-epic-to-future.mjs [--dry-run] <epics-md-file> [initiative-name]
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* node import-epic-to-future.mjs docs/planning/reflector-epics-and-stories.md "Reflector Consolidation"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Colors
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
const colors = {
|
|
26
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
27
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
28
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
29
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const log = {
|
|
33
|
+
info: (msg) => console.log(`${colors.blue('INFO')}: ${msg}`),
|
|
34
|
+
success: (msg) => console.log(`${colors.green('SUCCESS')}: ${msg}`),
|
|
35
|
+
warn: (msg) => console.log(`${colors.yellow('WARN')}: ${msg}`),
|
|
36
|
+
error: (msg) => console.error(`${colors.red('ERROR')}: ${msg}`),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Find project root
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
function findProjectRoot() {
|
|
44
|
+
let dir = process.cwd();
|
|
45
|
+
while (dir !== '/') {
|
|
46
|
+
if (fs.existsSync(path.join(dir, '.claude'))) {
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
dir = path.dirname(dir);
|
|
50
|
+
}
|
|
51
|
+
return process.cwd();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// Parse epics markdown file
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
function parseEpicsMarkdown(content) {
|
|
59
|
+
const result = {
|
|
60
|
+
title: '',
|
|
61
|
+
description: '',
|
|
62
|
+
totalPoints: 0,
|
|
63
|
+
epicTitle: '',
|
|
64
|
+
epicDescription: '',
|
|
65
|
+
stories: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const lines = content.split('\n');
|
|
69
|
+
|
|
70
|
+
// Extract title from first # heading
|
|
71
|
+
const titleMatch = content.match(/^# (.+?)( - Epic Breakdown)?$/m);
|
|
72
|
+
if (titleMatch) {
|
|
73
|
+
result.title = titleMatch[1].replace(' - Epics and Stories', '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract description from Overview section
|
|
77
|
+
const overviewMatch = content.match(/## Overview\s*\n\s*\n(.+?)(?=\n\n##|\n##)/s);
|
|
78
|
+
if (overviewMatch) {
|
|
79
|
+
result.description = overviewMatch[1].trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extract total points
|
|
83
|
+
const pointsMatch = content.match(/\*\*Points:\*\*\s*(\d+)/);
|
|
84
|
+
if (pointsMatch) {
|
|
85
|
+
result.totalPoints = parseInt(pointsMatch[1], 10);
|
|
86
|
+
} else {
|
|
87
|
+
const effortMatch = content.match(/Total Effort.*?(\d+)\s*story points/i);
|
|
88
|
+
if (effortMatch) {
|
|
89
|
+
result.totalPoints = parseInt(effortMatch[1], 10);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Extract epic title
|
|
94
|
+
const epicTitleMatch = content.match(/^## Epic \d+:\s*(.+)$/m);
|
|
95
|
+
if (epicTitleMatch) {
|
|
96
|
+
result.epicTitle = epicTitleMatch[1];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract epic description (User Outcome)
|
|
100
|
+
const userOutcomeMatch = content.match(/\*\*User Outcome:\*\*\s*(.+)/);
|
|
101
|
+
if (userOutcomeMatch) {
|
|
102
|
+
result.epicDescription = userOutcomeMatch[1];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse stories
|
|
106
|
+
const storyRegex = /^### Story \d+\.(\d+):\s*(.+)$/gm;
|
|
107
|
+
const iWantRegex = /^I want \*\*(.+?)\*\*,/m;
|
|
108
|
+
|
|
109
|
+
let match;
|
|
110
|
+
let storyPositions = [];
|
|
111
|
+
|
|
112
|
+
// Find all story positions
|
|
113
|
+
while ((match = storyRegex.exec(content)) !== null) {
|
|
114
|
+
storyPositions.push({
|
|
115
|
+
num: parseInt(match[1], 10),
|
|
116
|
+
title: match[2],
|
|
117
|
+
index: match.index,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract description for each story
|
|
122
|
+
for (let i = 0; i < storyPositions.length; i++) {
|
|
123
|
+
const story = storyPositions[i];
|
|
124
|
+
const startIndex = story.index;
|
|
125
|
+
const endIndex = i < storyPositions.length - 1
|
|
126
|
+
? storyPositions[i + 1].index
|
|
127
|
+
: content.length;
|
|
128
|
+
|
|
129
|
+
const storyContent = content.slice(startIndex, endIndex);
|
|
130
|
+
|
|
131
|
+
// Extract "I want" description
|
|
132
|
+
const iWantMatch = storyContent.match(iWantRegex);
|
|
133
|
+
let description = '';
|
|
134
|
+
if (iWantMatch) {
|
|
135
|
+
description = iWantMatch[1];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
result.stories.push({
|
|
139
|
+
num: story.num,
|
|
140
|
+
title: story.title,
|
|
141
|
+
description: description,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// Get next epic number from future.yaml
|
|
150
|
+
// =============================================================================
|
|
151
|
+
|
|
152
|
+
function getNextEpicNumber(futureYamlPath) {
|
|
153
|
+
if (!fs.existsSync(futureYamlPath)) {
|
|
154
|
+
return 60;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const content = fs.readFileSync(futureYamlPath, 'utf-8');
|
|
158
|
+
|
|
159
|
+
// Look for "Next Available Epic Number: XX"
|
|
160
|
+
const nextMatch = content.match(/Next Available Epic Number:\s*(\d+)/i);
|
|
161
|
+
if (nextMatch) {
|
|
162
|
+
return parseInt(nextMatch[1], 10);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback: find highest epic-XX number
|
|
166
|
+
const epicMatches = content.matchAll(/epic-(\d+)/g);
|
|
167
|
+
let maxNum = 59;
|
|
168
|
+
for (const m of epicMatches) {
|
|
169
|
+
const num = parseInt(m[1], 10);
|
|
170
|
+
if (num > maxNum) {
|
|
171
|
+
maxNum = num;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return maxNum + 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Generate YAML for initiative
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
function generateYaml(parsed, epicNum, initiativeName, epicsFile) {
|
|
183
|
+
const date = new Date().toISOString().split('T')[0];
|
|
184
|
+
|
|
185
|
+
// Indent helper
|
|
186
|
+
const indent = (text, spaces) => {
|
|
187
|
+
return text.split('\n').map(line => ' '.repeat(spaces) + line).join('\n');
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
let yaml = `
|
|
191
|
+
# ==========================================================================
|
|
192
|
+
# ${initiativeName}
|
|
193
|
+
# Imported from: ${epicsFile}
|
|
194
|
+
# Date: ${date}
|
|
195
|
+
# ==========================================================================
|
|
196
|
+
- name: "${initiativeName}"
|
|
197
|
+
description: |
|
|
198
|
+
${parsed.description}
|
|
199
|
+
status: ready
|
|
200
|
+
blocked_by: null
|
|
201
|
+
total_points: ${parsed.totalPoints}
|
|
202
|
+
prd: docs/planning/reflector-prd.md
|
|
203
|
+
epics_doc: ${epicsFile}
|
|
204
|
+
epics:
|
|
205
|
+
- id: epic-${epicNum}
|
|
206
|
+
title: "${parsed.epicTitle || initiativeName}"
|
|
207
|
+
description: |
|
|
208
|
+
${parsed.epicDescription || parsed.description}
|
|
209
|
+
points: ${parsed.totalPoints}
|
|
210
|
+
priority: P1
|
|
211
|
+
marker: "reflector"
|
|
212
|
+
repos: pennyfarthing
|
|
213
|
+
status: planning
|
|
214
|
+
stories:
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
// Add stories
|
|
218
|
+
for (const story of parsed.stories) {
|
|
219
|
+
yaml += ` - id: "${epicNum}-${story.num}"
|
|
220
|
+
title: "${story.title}"
|
|
221
|
+
description: |
|
|
222
|
+
${story.description || story.title}
|
|
223
|
+
points: 1
|
|
224
|
+
priority: P0
|
|
225
|
+
status: planning
|
|
226
|
+
repos: pennyfarthing
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return yaml;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Update future.yaml
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
function updateFutureYaml(futureYamlPath, newContent, nextEpicNum) {
|
|
238
|
+
let content = fs.readFileSync(futureYamlPath, 'utf-8');
|
|
239
|
+
|
|
240
|
+
// Update next epic number comment
|
|
241
|
+
content = content.replace(
|
|
242
|
+
/Next Available Epic Number:\s*\d+/i,
|
|
243
|
+
`Next Available Epic Number: ${nextEpicNum + 1}`
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Find insertion point (before SUMMARY section)
|
|
247
|
+
const summaryIndex = content.indexOf('# =============================================================================\n# SUMMARY');
|
|
248
|
+
|
|
249
|
+
if (summaryIndex === -1) {
|
|
250
|
+
// No summary section, append at end
|
|
251
|
+
content += newContent;
|
|
252
|
+
} else {
|
|
253
|
+
// Insert before summary
|
|
254
|
+
content = content.slice(0, summaryIndex) + newContent + '\n' + content.slice(summaryIndex);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update epic numbering comment (append new epic number to the range)
|
|
258
|
+
const epicNumMatch = content.match(/# Epic Numbering Used: ([\d\-,\s]+)\n/);
|
|
259
|
+
if (epicNumMatch) {
|
|
260
|
+
const current = epicNumMatch[1].trim();
|
|
261
|
+
if (!current.includes(nextEpicNum.toString())) {
|
|
262
|
+
// Parse the last range and extend it, or add new number
|
|
263
|
+
const lastRange = current.match(/(\d+)-(\d+)$/);
|
|
264
|
+
let newNumbering;
|
|
265
|
+
if (lastRange && parseInt(lastRange[2], 10) === nextEpicNum - 1) {
|
|
266
|
+
// Extend the range (e.g., 55-59 becomes 55-61)
|
|
267
|
+
newNumbering = current.replace(/(\d+)-(\d+)$/, `$1-${nextEpicNum}`);
|
|
268
|
+
} else {
|
|
269
|
+
// Add new number (e.g., 55-59, 61)
|
|
270
|
+
newNumbering = `${current}, ${nextEpicNum}`;
|
|
271
|
+
}
|
|
272
|
+
content = content.replace(
|
|
273
|
+
/# Epic Numbering Used: [\d\-,\s]+\n/,
|
|
274
|
+
`# Epic Numbering Used: ${newNumbering}\n`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update summary table
|
|
280
|
+
const summaryTableMatch = content.match(/# TOTAL\s+\|\s+(\d+)\s+\|\s+(\d+)/);
|
|
281
|
+
if (summaryTableMatch) {
|
|
282
|
+
const oldEpics = parseInt(summaryTableMatch[1], 10);
|
|
283
|
+
const oldPoints = parseInt(summaryTableMatch[2], 10);
|
|
284
|
+
// We'd need to parse the new points, but for simplicity just note it needs manual update
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return content;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// =============================================================================
|
|
291
|
+
// Main
|
|
292
|
+
// =============================================================================
|
|
293
|
+
|
|
294
|
+
function main() {
|
|
295
|
+
const args = process.argv.slice(2);
|
|
296
|
+
|
|
297
|
+
let dryRun = false;
|
|
298
|
+
let epicsFile = '';
|
|
299
|
+
let initiativeName = '';
|
|
300
|
+
|
|
301
|
+
// Parse arguments
|
|
302
|
+
for (const arg of args) {
|
|
303
|
+
if (arg === '--dry-run') {
|
|
304
|
+
dryRun = true;
|
|
305
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
306
|
+
console.log(`Usage: node import-epic-to-future.mjs [--dry-run] <epics-md-file> [initiative-name]
|
|
307
|
+
|
|
308
|
+
Arguments:
|
|
309
|
+
epics-md-file Path to the markdown file from epics-and-stories workflow
|
|
310
|
+
initiative-name Name for the initiative (optional, extracted from file if not provided)
|
|
311
|
+
|
|
312
|
+
Options:
|
|
313
|
+
--dry-run Print YAML to stdout instead of updating future.yaml
|
|
314
|
+
--help, -h Show this help message`);
|
|
315
|
+
process.exit(0);
|
|
316
|
+
} else if (!epicsFile) {
|
|
317
|
+
epicsFile = arg;
|
|
318
|
+
} else if (!initiativeName) {
|
|
319
|
+
initiativeName = arg;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!epicsFile) {
|
|
324
|
+
log.error('No epics markdown file specified');
|
|
325
|
+
console.log('Usage: node import-epic-to-future.mjs [--dry-run] <epics-md-file> [initiative-name]');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const projectRoot = findProjectRoot();
|
|
330
|
+
const fullEpicsPath = path.isAbsolute(epicsFile)
|
|
331
|
+
? epicsFile
|
|
332
|
+
: path.join(projectRoot, epicsFile);
|
|
333
|
+
|
|
334
|
+
if (!fs.existsSync(fullEpicsPath)) {
|
|
335
|
+
log.error(`File not found: ${fullEpicsPath}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const futureYamlPath = path.join(projectRoot, 'sprint', 'future.yaml');
|
|
340
|
+
|
|
341
|
+
// Read and parse epics file
|
|
342
|
+
const content = fs.readFileSync(fullEpicsPath, 'utf-8');
|
|
343
|
+
const parsed = parseEpicsMarkdown(content);
|
|
344
|
+
|
|
345
|
+
// Use provided name or extract from file
|
|
346
|
+
if (!initiativeName) {
|
|
347
|
+
initiativeName = parsed.title || 'Imported Initiative';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Get next epic number
|
|
351
|
+
const nextEpicNum = getNextEpicNumber(futureYamlPath);
|
|
352
|
+
|
|
353
|
+
log.info(`Next epic number: ${nextEpicNum}`);
|
|
354
|
+
log.info(`Initiative name: ${initiativeName}`);
|
|
355
|
+
log.info(`Total points: ${parsed.totalPoints}`);
|
|
356
|
+
log.info(`Epic title: ${parsed.epicTitle}`);
|
|
357
|
+
log.info(`Stories found: ${parsed.stories.length}`);
|
|
358
|
+
|
|
359
|
+
// Generate YAML
|
|
360
|
+
const newYaml = generateYaml(parsed, nextEpicNum, initiativeName, epicsFile);
|
|
361
|
+
|
|
362
|
+
if (dryRun) {
|
|
363
|
+
console.log(`\n${colors.yellow('=== DRY RUN: Would append to future.yaml ===')}`);
|
|
364
|
+
console.log(newYaml);
|
|
365
|
+
console.log(`${colors.yellow('=== End of YAML ===')}\n`);
|
|
366
|
+
console.log(`${colors.green('To apply, run without --dry-run')}`);
|
|
367
|
+
} else {
|
|
368
|
+
// Update future.yaml
|
|
369
|
+
const updatedContent = updateFutureYaml(futureYamlPath, newYaml, nextEpicNum);
|
|
370
|
+
fs.writeFileSync(futureYamlPath, updatedContent);
|
|
371
|
+
|
|
372
|
+
log.success(`Added epic-${nextEpicNum} to ${futureYamlPath}`);
|
|
373
|
+
log.success(`Next available epic number is now: ${nextEpicNum + 1}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
main();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# import-epic-to-future.sh - Thin wrapper for import-epic-to-future.mjs
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./scripts/sprint/import-epic-to-future.sh [--dry-run] <epics-md-file> [initiative-name]
|
|
5
|
+
#
|
|
6
|
+
# Delegates to Node script for cleaner markdown parsing and YAML generation.
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
exec node "${SCRIPT_DIR}/import-epic-to-future.mjs" "$@"
|