jumpstart-mode 1.1.11 → 1.1.13

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 (188) hide show
  1. package/.github/agents/jumpstart-adversary.agent.md +2 -1
  2. package/.github/agents/jumpstart-architect.agent.md +6 -7
  3. package/.github/agents/jumpstart-challenger.agent.md +2 -1
  4. package/.github/agents/jumpstart-developer.agent.md +1 -1
  5. package/.github/agents/jumpstart-devops.agent.md +2 -2
  6. package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
  7. package/.github/agents/jumpstart-maintenance.agent.md +1 -0
  8. package/.github/agents/jumpstart-performance.agent.md +1 -0
  9. package/.github/agents/jumpstart-pm.agent.md +1 -1
  10. package/.github/agents/jumpstart-refactor.agent.md +1 -0
  11. package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
  12. package/.github/agents/jumpstart-researcher.agent.md +1 -0
  13. package/.github/agents/jumpstart-retrospective.agent.md +1 -0
  14. package/.github/agents/jumpstart-reviewer.agent.md +2 -0
  15. package/.github/agents/jumpstart-scout.agent.md +1 -1
  16. package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
  17. package/.github/agents/jumpstart-security.agent.md +2 -1
  18. package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
  19. package/.github/agents/jumpstart-uiux-designer.agent.md +66 -0
  20. package/.github/workflows/quality.yml +19 -2
  21. package/.jumpstart/agents/analyst.md +38 -0
  22. package/.jumpstart/agents/architect.md +39 -1
  23. package/.jumpstart/agents/challenger.md +38 -0
  24. package/.jumpstart/agents/developer.md +41 -0
  25. package/.jumpstart/agents/pm.md +38 -0
  26. package/.jumpstart/agents/scout.md +33 -0
  27. package/.jumpstart/agents/ux-designer.md +29 -9
  28. package/.jumpstart/commands/commands.md +6 -5
  29. package/.jumpstart/config.yaml +25 -1
  30. package/.jumpstart/roadmap.md +1 -1
  31. package/.jumpstart/schemas/timeline.schema.json +1 -0
  32. package/.jumpstart/skills/README.md +1 -0
  33. package/.jumpstart/skills/quality-gates/SKILL.md +126 -0
  34. package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
  35. package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
  36. package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
  37. package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
  38. package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
  39. package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  40. package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  41. package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
  42. package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
  43. package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  44. package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
  45. package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
  46. package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
  47. package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
  48. package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
  49. package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
  50. package/.jumpstart/skills/ui-ux-pro-max/SKILL.md +266 -0
  51. package/.jumpstart/skills/ui-ux-pro-max/data/charts.csv +26 -0
  52. package/.jumpstart/skills/ui-ux-pro-max/data/colors.csv +97 -0
  53. package/.jumpstart/skills/ui-ux-pro-max/data/icons.csv +101 -0
  54. package/.jumpstart/skills/ui-ux-pro-max/data/landing.csv +31 -0
  55. package/.jumpstart/skills/ui-ux-pro-max/data/products.csv +97 -0
  56. package/.jumpstart/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  57. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  58. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  59. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  60. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  61. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  62. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  63. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  64. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  65. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  66. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  67. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  68. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  69. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  70. package/.jumpstart/skills/ui-ux-pro-max/data/styles.csv +68 -0
  71. package/.jumpstart/skills/ui-ux-pro-max/data/typography.csv +58 -0
  72. package/.jumpstart/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  73. package/.jumpstart/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  74. package/.jumpstart/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  75. package/.jumpstart/skills/ui-ux-pro-max/scripts/core.py +253 -0
  76. package/.jumpstart/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  77. package/.jumpstart/skills/ui-ux-pro-max/scripts/search.py +114 -0
  78. package/.jumpstart/state/timeline.json +659 -0
  79. package/.jumpstart/templates/model-map.md +1 -1
  80. package/.jumpstart/templates/ux-design.md +3 -3
  81. package/.jumpstart/usage-log.json +74 -3
  82. package/AGENTS.md +1 -1
  83. package/README.md +64 -3
  84. package/bin/cli.js +3217 -1
  85. package/bin/headless-runner.js +62 -2
  86. package/bin/lib/agent-checkpoint.js +168 -0
  87. package/bin/lib/ai-evaluation.js +104 -0
  88. package/bin/lib/ai-intake.js +152 -0
  89. package/bin/lib/ambiguity-heatmap.js +152 -0
  90. package/bin/lib/artifact-comparison.js +104 -0
  91. package/bin/lib/ast-edit-engine.js +157 -0
  92. package/bin/lib/backlog-sync.js +338 -0
  93. package/bin/lib/bcdr-planning.js +158 -0
  94. package/bin/lib/bidirectional-trace.js +199 -0
  95. package/bin/lib/branch-workflow.js +266 -0
  96. package/bin/lib/cab-output.js +119 -0
  97. package/bin/lib/chat-integration.js +122 -0
  98. package/bin/lib/ci-cd-integration.js +208 -0
  99. package/bin/lib/codebase-retrieval.js +125 -0
  100. package/bin/lib/collaboration.js +168 -0
  101. package/bin/lib/compliance-packs.js +213 -0
  102. package/bin/lib/context-chunker.js +128 -0
  103. package/bin/lib/context-onboarding.js +122 -0
  104. package/bin/lib/contract-first.js +124 -0
  105. package/bin/lib/cost-router.js +148 -0
  106. package/bin/lib/credential-boundary.js +155 -0
  107. package/bin/lib/data-classification.js +180 -0
  108. package/bin/lib/data-contracts.js +129 -0
  109. package/bin/lib/db-evolution.js +158 -0
  110. package/bin/lib/decision-conflicts.js +299 -0
  111. package/bin/lib/delivery-confidence.js +361 -0
  112. package/bin/lib/dependency-upgrade.js +153 -0
  113. package/bin/lib/design-system.js +133 -0
  114. package/bin/lib/deterministic-artifacts.js +151 -0
  115. package/bin/lib/diagram-studio.js +115 -0
  116. package/bin/lib/domain-ontology.js +140 -0
  117. package/bin/lib/ea-review-packet.js +151 -0
  118. package/bin/lib/enterprise-search.js +123 -0
  119. package/bin/lib/enterprise-templates.js +140 -0
  120. package/bin/lib/environment-promotion.js +220 -0
  121. package/bin/lib/estimation-studio.js +130 -0
  122. package/bin/lib/event-modeling.js +133 -0
  123. package/bin/lib/evidence-collector.js +179 -0
  124. package/bin/lib/finops-planner.js +182 -0
  125. package/bin/lib/fitness-functions.js +279 -0
  126. package/bin/lib/focus.js +448 -0
  127. package/bin/lib/governance-dashboard.js +165 -0
  128. package/bin/lib/guided-handoff.js +120 -0
  129. package/bin/lib/impact-analysis.js +190 -0
  130. package/bin/lib/incident-feedback.js +157 -0
  131. package/bin/lib/integrate.js +1 -1
  132. package/bin/lib/knowledge-graph.js +122 -0
  133. package/bin/lib/legacy-modernizer.js +160 -0
  134. package/bin/lib/migration-planner.js +144 -0
  135. package/bin/lib/model-governance.js +185 -0
  136. package/bin/lib/model-router.js +144 -0
  137. package/bin/lib/multi-repo.js +272 -0
  138. package/bin/lib/next-phase.js +53 -8
  139. package/bin/lib/ops-ownership.js +152 -0
  140. package/bin/lib/parallel-agents.js +257 -0
  141. package/bin/lib/pattern-library.js +115 -0
  142. package/bin/lib/persona-packs.js +99 -0
  143. package/bin/lib/plan-executor.js +366 -0
  144. package/bin/lib/platform-engineering.js +119 -0
  145. package/bin/lib/playback-summaries.js +126 -0
  146. package/bin/lib/policy-engine.js +240 -0
  147. package/bin/lib/portfolio-reporting.js +357 -0
  148. package/bin/lib/pr-package.js +197 -0
  149. package/bin/lib/project-memory.js +235 -0
  150. package/bin/lib/prompt-governance.js +130 -0
  151. package/bin/lib/promptless-mode.js +128 -0
  152. package/bin/lib/quality-graph.js +193 -0
  153. package/bin/lib/raci-matrix.js +188 -0
  154. package/bin/lib/refactor-planner.js +167 -0
  155. package/bin/lib/reference-architectures.js +304 -0
  156. package/bin/lib/release-readiness.js +171 -0
  157. package/bin/lib/repo-graph.js +262 -0
  158. package/bin/lib/requirements-baseline.js +358 -0
  159. package/bin/lib/risk-register.js +211 -0
  160. package/bin/lib/role-approval.js +249 -0
  161. package/bin/lib/role-views.js +142 -0
  162. package/bin/lib/root-cause-analysis.js +132 -0
  163. package/bin/lib/runtime-debugger.js +154 -0
  164. package/bin/lib/safe-rename.js +135 -0
  165. package/bin/lib/secret-scanner.js +313 -0
  166. package/bin/lib/semantic-diff.js +335 -0
  167. package/bin/lib/sla-slo.js +210 -0
  168. package/bin/lib/smoke-tester.js +344 -0
  169. package/bin/lib/spec-comments.js +147 -0
  170. package/bin/lib/spec-maturity.js +287 -0
  171. package/bin/lib/sre-integration.js +154 -0
  172. package/bin/lib/structured-elicitation.js +174 -0
  173. package/bin/lib/telemetry-feedback.js +118 -0
  174. package/bin/lib/test-generator.js +146 -0
  175. package/bin/lib/timeline.js +2 -1
  176. package/bin/lib/tool-bridge.js +159 -0
  177. package/bin/lib/tool-guardrails.js +139 -0
  178. package/bin/lib/tool-schemas.js +281 -3
  179. package/bin/lib/transcript-ingestion.js +150 -0
  180. package/bin/lib/type-checker.js +261 -0
  181. package/bin/lib/uat-coverage.js +411 -0
  182. package/bin/lib/vendor-risk.js +173 -0
  183. package/bin/lib/waiver-workflow.js +174 -0
  184. package/bin/lib/web-dashboard.js +126 -0
  185. package/bin/lib/workshop-mode.js +165 -0
  186. package/bin/lib/workstream-ownership.js +104 -0
  187. package/package.json +1 -1
  188. package/.github/agents/jumpstart-ux-designer.agent.md +0 -45
@@ -0,0 +1,240 @@
1
+ /**
2
+ * policy-engine.js — Enterprise Policy Engine
3
+ *
4
+ * Enforces org-specific rules for architecture, naming, security,
5
+ * legal, AI usage, and deployment standards.
6
+ *
7
+ * Policy file: .jumpstart/policies.json
8
+ *
9
+ * Usage:
10
+ * node bin/lib/policy-engine.js check|list|add [options]
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const DEFAULT_POLICY_FILE = path.join('.jumpstart', 'policies.json');
19
+
20
+ const POLICY_CATEGORIES = ['architecture', 'naming', 'security', 'legal', 'ai', 'deployment', 'other'];
21
+ const SEVERITY_LEVELS = ['error', 'warning', 'info'];
22
+
23
+ /**
24
+ * Default empty policy registry.
25
+ * @returns {object}
26
+ */
27
+ function defaultPolicies() {
28
+ return {
29
+ version: '1.0.0',
30
+ created_at: new Date().toISOString(),
31
+ last_updated: null,
32
+ policies: []
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Load policies from disk.
38
+ * @param {string} [policyFile]
39
+ * @returns {object}
40
+ */
41
+ function loadPolicies(policyFile) {
42
+ const filePath = policyFile || DEFAULT_POLICY_FILE;
43
+ if (!fs.existsSync(filePath)) {
44
+ return defaultPolicies();
45
+ }
46
+ try {
47
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
48
+ } catch {
49
+ return defaultPolicies();
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Save policies to disk.
55
+ * @param {object} policies
56
+ * @param {string} [policyFile]
57
+ */
58
+ function savePolicies(policies, policyFile) {
59
+ const filePath = policyFile || DEFAULT_POLICY_FILE;
60
+ const dir = path.dirname(filePath);
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+ policies.last_updated = new Date().toISOString();
65
+ fs.writeFileSync(filePath, JSON.stringify(policies, null, 2) + '\n', 'utf8');
66
+ }
67
+
68
+ /**
69
+ * Add a new policy rule.
70
+ *
71
+ * @param {object} rule - { id, category, name, description, pattern?, severity, applies_to? }
72
+ * @param {object} [options]
73
+ * @returns {object}
74
+ */
75
+ function addPolicy(rule, options = {}) {
76
+ if (!rule || !rule.name || !rule.description) {
77
+ return { success: false, error: 'rule.name and rule.description are required' };
78
+ }
79
+
80
+ const category = (rule.category || 'other').toLowerCase();
81
+ if (!POLICY_CATEGORIES.includes(category)) {
82
+ return { success: false, error: `category must be one of: ${POLICY_CATEGORIES.join(', ')}` };
83
+ }
84
+
85
+ const severity = (rule.severity || 'warning').toLowerCase();
86
+ if (!SEVERITY_LEVELS.includes(severity)) {
87
+ return { success: false, error: `severity must be one of: ${SEVERITY_LEVELS.join(', ')}` };
88
+ }
89
+
90
+ const policyFile = options.policyFile || DEFAULT_POLICY_FILE;
91
+ const policies = loadPolicies(policyFile);
92
+
93
+ const id = rule.id || `policy-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
94
+ const existing = policies.policies.find(p => p.id === id);
95
+ if (existing) {
96
+ return { success: false, error: `Policy with id "${id}" already exists` };
97
+ }
98
+
99
+ const newRule = {
100
+ id,
101
+ category,
102
+ name: rule.name.trim(),
103
+ description: rule.description.trim(),
104
+ pattern: rule.pattern || null,
105
+ severity,
106
+ applies_to: rule.applies_to || ['specs', 'src'],
107
+ enabled: rule.enabled !== false,
108
+ created_at: new Date().toISOString()
109
+ };
110
+
111
+ policies.policies.push(newRule);
112
+ savePolicies(policies, policyFile);
113
+
114
+ return { success: true, policy: newRule, total: policies.policies.length };
115
+ }
116
+
117
+ /**
118
+ * Run policy checks against the project.
119
+ *
120
+ * @param {string} root - Project root.
121
+ * @param {object} [options]
122
+ * @returns {object} Policy check result with violations and warnings.
123
+ */
124
+ function checkPolicies(root, options = {}) {
125
+ const policyFile = options.policyFile || path.join(root, DEFAULT_POLICY_FILE);
126
+ const policies = loadPolicies(policyFile);
127
+
128
+ const violations = [];
129
+ const warnings = [];
130
+ const infos = [];
131
+
132
+ const enabledPolicies = policies.policies.filter(p => p.enabled !== false);
133
+
134
+ for (const policy of enabledPolicies) {
135
+ if (!policy.pattern) continue;
136
+
137
+ let pattern;
138
+ try {
139
+ pattern = new RegExp(policy.pattern, 'gi');
140
+ } catch {
141
+ continue; // skip invalid regex patterns
142
+ }
143
+
144
+ const dirsToCheck = (policy.applies_to || []);
145
+ for (const dir of dirsToCheck) {
146
+ const absDir = path.join(root, dir);
147
+ if (!fs.existsSync(absDir)) continue;
148
+
149
+ const walk = (d) => {
150
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
151
+ const full = path.join(d, entry.name);
152
+ if (entry.isDirectory()) {
153
+ walk(full);
154
+ } else if (entry.isFile()) {
155
+ try {
156
+ const content = fs.readFileSync(full, 'utf8');
157
+ const rel = path.relative(root, full).replace(/\\/g, '/');
158
+ let match;
159
+ while ((match = pattern.exec(content)) !== null) {
160
+ const violation = {
161
+ policy_id: policy.id,
162
+ policy_name: policy.name,
163
+ category: policy.category,
164
+ severity: policy.severity,
165
+ file: rel,
166
+ matched: match[0],
167
+ description: policy.description
168
+ };
169
+ if (policy.severity === 'error') {
170
+ violations.push(violation);
171
+ } else if (policy.severity === 'warning') {
172
+ warnings.push(violation);
173
+ } else {
174
+ infos.push(violation);
175
+ }
176
+ }
177
+ } catch {
178
+ // skip unreadable files
179
+ }
180
+ }
181
+ }
182
+ };
183
+ walk(absDir);
184
+ }
185
+ }
186
+
187
+ const passed = violations.length === 0;
188
+
189
+ return {
190
+ success: true,
191
+ passed,
192
+ violations,
193
+ warnings,
194
+ infos,
195
+ summary: {
196
+ total_policies_checked: enabledPolicies.length,
197
+ violations: violations.length,
198
+ warnings: warnings.length,
199
+ infos: infos.length,
200
+ passed
201
+ }
202
+ };
203
+ }
204
+
205
+ /**
206
+ * List all registered policies.
207
+ *
208
+ * @param {object} [filter] - { category?, severity?, enabled? }
209
+ * @param {object} [options]
210
+ * @returns {object}
211
+ */
212
+ function listPolicies(filter = {}, options = {}) {
213
+ const policyFile = options.policyFile || DEFAULT_POLICY_FILE;
214
+ const policies = loadPolicies(policyFile);
215
+
216
+ let entries = policies.policies;
217
+
218
+ if (filter.category) {
219
+ entries = entries.filter(p => p.category === filter.category);
220
+ }
221
+ if (filter.severity) {
222
+ entries = entries.filter(p => p.severity === filter.severity);
223
+ }
224
+ if (filter.enabled !== undefined) {
225
+ entries = entries.filter(p => (p.enabled !== false) === filter.enabled);
226
+ }
227
+
228
+ return { success: true, policies: entries, total: entries.length };
229
+ }
230
+
231
+ module.exports = {
232
+ loadPolicies,
233
+ savePolicies,
234
+ defaultPolicies,
235
+ addPolicy,
236
+ checkPolicies,
237
+ listPolicies,
238
+ POLICY_CATEGORIES,
239
+ SEVERITY_LEVELS
240
+ };
@@ -0,0 +1,357 @@
1
+ /**
2
+ * portfolio-reporting.js — Portfolio Reporting Layer
3
+ *
4
+ * Show leadership status across many JumpStart initiatives: phase,
5
+ * risk, spend, readiness, blockers.
6
+ *
7
+ * Registry: .jumpstart/state/portfolio.json
8
+ *
9
+ * Usage:
10
+ * node bin/lib/portfolio-reporting.js register|status|report|remove [options]
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const DEFAULT_PORTFOLIO_FILE = path.join('.jumpstart', 'state', 'portfolio.json');
19
+
20
+ const PORTFOLIO_STATUSES = ['on-track', 'at-risk', 'blocked', 'completed', 'paused', 'cancelled'];
21
+
22
+ const PHASES = [
23
+ { id: 'scout', name: 'Scout', order: -1 },
24
+ { id: 'phase-0', name: 'Challenge', order: 0 },
25
+ { id: 'phase-1', name: 'Analyze', order: 1 },
26
+ { id: 'phase-2', name: 'Plan', order: 2 },
27
+ { id: 'phase-3', name: 'Architect', order: 3 },
28
+ { id: 'phase-4', name: 'Build', order: 4 }
29
+ ];
30
+
31
+ /**
32
+ * Default portfolio state.
33
+ * @returns {object}
34
+ */
35
+ function defaultPortfolio() {
36
+ return {
37
+ version: '1.0.0',
38
+ created_at: new Date().toISOString(),
39
+ last_updated: null,
40
+ initiatives: [],
41
+ snapshots: []
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Load portfolio from disk.
47
+ * @param {string} [portfolioFile]
48
+ * @returns {object}
49
+ */
50
+ function loadPortfolio(portfolioFile) {
51
+ const filePath = portfolioFile || DEFAULT_PORTFOLIO_FILE;
52
+ if (!fs.existsSync(filePath)) {
53
+ return defaultPortfolio();
54
+ }
55
+ try {
56
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
57
+ } catch {
58
+ return defaultPortfolio();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Save portfolio to disk.
64
+ * @param {object} portfolio
65
+ * @param {string} [portfolioFile]
66
+ */
67
+ function savePortfolio(portfolio, portfolioFile) {
68
+ const filePath = portfolioFile || DEFAULT_PORTFOLIO_FILE;
69
+ const dir = path.dirname(filePath);
70
+ if (!fs.existsSync(dir)) {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
73
+ portfolio.last_updated = new Date().toISOString();
74
+ fs.writeFileSync(filePath, JSON.stringify(portfolio, null, 2) + '\n', 'utf8');
75
+ }
76
+
77
+ /**
78
+ * Analyze a project directory to determine its status.
79
+ * @param {string} projectRoot
80
+ * @returns {object}
81
+ */
82
+ function analyzeProject(projectRoot) {
83
+ const result = {
84
+ current_phase: null,
85
+ phase_progress: 0,
86
+ artifacts_completed: 0,
87
+ total_artifacts: 5,
88
+ blockers: [],
89
+ risks: [],
90
+ readiness: 'unknown'
91
+ };
92
+
93
+ const stateFile = path.join(projectRoot, '.jumpstart', 'state', 'state.json');
94
+ if (fs.existsSync(stateFile)) {
95
+ try {
96
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
97
+ if (state.current_phase) result.current_phase = state.current_phase;
98
+ } catch {
99
+ // ignore parse errors
100
+ }
101
+ }
102
+
103
+ // Check which artifacts exist and are approved
104
+ const artifactMap = {
105
+ 'specs/challenger-brief.md': 'phase-0',
106
+ 'specs/product-brief.md': 'phase-1',
107
+ 'specs/prd.md': 'phase-2',
108
+ 'specs/architecture.md': 'phase-3',
109
+ 'specs/implementation-plan.md': 'phase-3'
110
+ };
111
+
112
+ let completed = 0;
113
+ let latestPhase = null;
114
+
115
+ for (const [relPath, phase] of Object.entries(artifactMap)) {
116
+ const fullPath = path.join(projectRoot, relPath);
117
+ if (fs.existsSync(fullPath)) {
118
+ const content = fs.readFileSync(fullPath, 'utf8');
119
+ const isApproved = /- \[x\]/i.test(content) && /Approved by[:\s]+(?!Pending)/i.test(content);
120
+ if (isApproved) {
121
+ completed++;
122
+ latestPhase = phase;
123
+ }
124
+
125
+ // Check for blockers
126
+ const blockerMatches = content.match(/\[BLOCKER[:\s]*([^\]]*)\]/gi);
127
+ if (blockerMatches) {
128
+ result.blockers.push(...blockerMatches.map(b => b.replace(/\[BLOCKER[:\s]*/i, '').replace(/\]/g, '')));
129
+ }
130
+
131
+ // Check for risks
132
+ const clarificationMatches = content.match(/\[NEEDS CLARIFICATION[:\s]*([^\]]*)\]/gi);
133
+ if (clarificationMatches) {
134
+ result.risks.push(...clarificationMatches.map(c => `Unresolved: ${c.replace(/\[NEEDS CLARIFICATION[:\s]*/i, '').replace(/\]/g, '')}`));
135
+ }
136
+ }
137
+ }
138
+
139
+ result.artifacts_completed = completed;
140
+ result.phase_progress = Math.round((completed / result.total_artifacts) * 100);
141
+
142
+ if (!result.current_phase && latestPhase) {
143
+ const phaseObj = PHASES.find(p => p.id === latestPhase);
144
+ const nextPhase = PHASES.find(p => p.order === (phaseObj ? phaseObj.order + 1 : 0));
145
+ result.current_phase = nextPhase ? nextPhase.id : latestPhase;
146
+ }
147
+
148
+ // Determine readiness
149
+ if (completed >= 5) result.readiness = 'production-ready';
150
+ else if (completed >= 3) result.readiness = 'implementation-ready';
151
+ else if (completed >= 1) result.readiness = 'in-progress';
152
+ else result.readiness = 'not-started';
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Register a project initiative.
159
+ *
160
+ * @param {object} initiative - { name, path, owner?, budget?, target_date? }
161
+ * @param {object} [options]
162
+ * @returns {object}
163
+ */
164
+ function registerInitiative(initiative, options = {}) {
165
+ if (!initiative || !initiative.name) {
166
+ return { success: false, error: 'initiative.name is required' };
167
+ }
168
+
169
+ const portfolioFile = options.portfolioFile || DEFAULT_PORTFOLIO_FILE;
170
+ const portfolio = loadPortfolio(portfolioFile);
171
+
172
+ const id = initiative.id || initiative.name.toLowerCase().replace(/\s+/g, '-');
173
+ if (portfolio.initiatives.find(i => i.id === id)) {
174
+ return { success: false, error: `Initiative "${id}" already exists` };
175
+ }
176
+
177
+ const newInitiative = {
178
+ id,
179
+ name: initiative.name.trim(),
180
+ path: initiative.path || null,
181
+ owner: initiative.owner || null,
182
+ budget: initiative.budget || null,
183
+ target_date: initiative.target_date || null,
184
+ status: 'on-track',
185
+ registered_at: new Date().toISOString(),
186
+ last_checked: null,
187
+ current_phase: null,
188
+ phase_progress: 0,
189
+ readiness: 'not-started',
190
+ blockers: [],
191
+ risks: [],
192
+ spend: 0,
193
+ notes: []
194
+ };
195
+
196
+ portfolio.initiatives.push(newInitiative);
197
+ savePortfolio(portfolio, portfolioFile);
198
+
199
+ return { success: true, initiative: newInitiative };
200
+ }
201
+
202
+ /**
203
+ * Update initiative status from project analysis.
204
+ *
205
+ * @param {string} initiativeId
206
+ * @param {object} [options]
207
+ * @returns {object}
208
+ */
209
+ function refreshInitiative(initiativeId, options = {}) {
210
+ const portfolioFile = options.portfolioFile || DEFAULT_PORTFOLIO_FILE;
211
+ const portfolio = loadPortfolio(portfolioFile);
212
+
213
+ const initiative = portfolio.initiatives.find(i => i.id === initiativeId);
214
+ if (!initiative) {
215
+ return { success: false, error: `Initiative not found: ${initiativeId}` };
216
+ }
217
+
218
+ if (initiative.path && fs.existsSync(initiative.path)) {
219
+ const analysis = analyzeProject(initiative.path);
220
+ initiative.current_phase = analysis.current_phase;
221
+ initiative.phase_progress = analysis.phase_progress;
222
+ initiative.readiness = analysis.readiness;
223
+ initiative.blockers = analysis.blockers;
224
+ initiative.risks = analysis.risks;
225
+ initiative.artifacts_completed = analysis.artifacts_completed;
226
+
227
+ if (analysis.blockers.length > 0) {
228
+ initiative.status = 'blocked';
229
+ } else if (analysis.risks.length > 3) {
230
+ initiative.status = 'at-risk';
231
+ } else if (analysis.phase_progress >= 100) {
232
+ initiative.status = 'completed';
233
+ }
234
+ }
235
+
236
+ initiative.last_checked = new Date().toISOString();
237
+ savePortfolio(portfolio, portfolioFile);
238
+
239
+ return { success: true, initiative };
240
+ }
241
+
242
+ /**
243
+ * Get portfolio status report.
244
+ *
245
+ * @param {object} [options]
246
+ * @returns {object}
247
+ */
248
+ function getPortfolioStatus(options = {}) {
249
+ const portfolioFile = options.portfolioFile || DEFAULT_PORTFOLIO_FILE;
250
+ const portfolio = loadPortfolio(portfolioFile);
251
+
252
+ const statusCounts = {};
253
+ for (const status of PORTFOLIO_STATUSES) {
254
+ statusCounts[status] = portfolio.initiatives.filter(i => i.status === status).length;
255
+ }
256
+
257
+ const totalBudget = portfolio.initiatives.reduce((sum, i) => sum + (i.budget || 0), 0);
258
+ const totalSpend = portfolio.initiatives.reduce((sum, i) => sum + (i.spend || 0), 0);
259
+
260
+ const allBlockers = portfolio.initiatives.flatMap(i =>
261
+ (i.blockers || []).map(b => ({ initiative: i.name, blocker: b }))
262
+ );
263
+
264
+ const avgProgress = portfolio.initiatives.length > 0
265
+ ? Math.round(portfolio.initiatives.reduce((sum, i) => sum + (i.phase_progress || 0), 0) / portfolio.initiatives.length)
266
+ : 0;
267
+
268
+ return {
269
+ success: true,
270
+ total_initiatives: portfolio.initiatives.length,
271
+ status_counts: statusCounts,
272
+ average_progress: avgProgress,
273
+ budget: { total: totalBudget, spent: totalSpend, remaining: totalBudget - totalSpend },
274
+ blockers: allBlockers,
275
+ initiatives: portfolio.initiatives.map(i => ({
276
+ id: i.id,
277
+ name: i.name,
278
+ status: i.status,
279
+ phase: i.current_phase,
280
+ progress: i.phase_progress,
281
+ readiness: i.readiness,
282
+ owner: i.owner,
283
+ blockers: (i.blockers || []).length,
284
+ risks: (i.risks || []).length
285
+ }))
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Remove an initiative.
291
+ *
292
+ * @param {string} initiativeId
293
+ * @param {object} [options]
294
+ * @returns {object}
295
+ */
296
+ function removeInitiative(initiativeId, options = {}) {
297
+ const portfolioFile = options.portfolioFile || DEFAULT_PORTFOLIO_FILE;
298
+ const portfolio = loadPortfolio(portfolioFile);
299
+
300
+ const index = portfolio.initiatives.findIndex(i => i.id === initiativeId);
301
+ if (index === -1) {
302
+ return { success: false, error: `Initiative not found: ${initiativeId}` };
303
+ }
304
+
305
+ const removed = portfolio.initiatives.splice(index, 1)[0];
306
+ savePortfolio(portfolio, portfolioFile);
307
+
308
+ return { success: true, removed: removed.name };
309
+ }
310
+
311
+ /**
312
+ * Take a snapshot of portfolio status for historical tracking.
313
+ *
314
+ * @param {object} [options]
315
+ * @returns {object}
316
+ */
317
+ function takeSnapshot(options = {}) {
318
+ const portfolioFile = options.portfolioFile || DEFAULT_PORTFOLIO_FILE;
319
+ const portfolio = loadPortfolio(portfolioFile);
320
+
321
+ const snapshot = {
322
+ taken_at: new Date().toISOString(),
323
+ total_initiatives: portfolio.initiatives.length,
324
+ status_summary: {},
325
+ avg_progress: 0
326
+ };
327
+
328
+ for (const status of PORTFOLIO_STATUSES) {
329
+ snapshot.status_summary[status] = portfolio.initiatives.filter(i => i.status === status).length;
330
+ }
331
+
332
+ snapshot.avg_progress = portfolio.initiatives.length > 0
333
+ ? Math.round(portfolio.initiatives.reduce((sum, i) => sum + (i.phase_progress || 0), 0) / portfolio.initiatives.length)
334
+ : 0;
335
+
336
+ portfolio.snapshots.push(snapshot);
337
+ if (portfolio.snapshots.length > 100) {
338
+ portfolio.snapshots = portfolio.snapshots.slice(-100);
339
+ }
340
+ savePortfolio(portfolio, portfolioFile);
341
+
342
+ return { success: true, snapshot };
343
+ }
344
+
345
+ module.exports = {
346
+ defaultPortfolio,
347
+ loadPortfolio,
348
+ savePortfolio,
349
+ analyzeProject,
350
+ registerInitiative,
351
+ refreshInitiative,
352
+ getPortfolioStatus,
353
+ removeInitiative,
354
+ takeSnapshot,
355
+ PORTFOLIO_STATUSES,
356
+ PHASES
357
+ };