jumpstart-mode 1.1.12 → 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.
- package/.github/agents/jumpstart-adversary.agent.md +2 -1
- package/.github/agents/jumpstart-architect.agent.md +5 -6
- package/.github/agents/jumpstart-challenger.agent.md +2 -1
- package/.github/agents/jumpstart-devops.agent.md +2 -2
- package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
- package/.github/agents/jumpstart-maintenance.agent.md +1 -0
- package/.github/agents/jumpstart-performance.agent.md +1 -0
- package/.github/agents/jumpstart-pm.agent.md +1 -1
- package/.github/agents/jumpstart-refactor.agent.md +1 -0
- package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
- package/.github/agents/jumpstart-researcher.agent.md +1 -0
- package/.github/agents/jumpstart-retrospective.agent.md +1 -0
- package/.github/agents/jumpstart-reviewer.agent.md +2 -0
- package/.github/agents/jumpstart-scout.agent.md +1 -1
- package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
- package/.github/agents/jumpstart-security.agent.md +2 -1
- package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
- package/.github/workflows/quality.yml +19 -2
- package/.jumpstart/agents/analyst.md +38 -0
- package/.jumpstart/agents/architect.md +38 -0
- package/.jumpstart/agents/challenger.md +38 -0
- package/.jumpstart/agents/developer.md +41 -0
- package/.jumpstart/agents/pm.md +38 -0
- package/.jumpstart/agents/scout.md +33 -0
- package/.jumpstart/agents/ux-designer.md +4 -0
- package/.jumpstart/config.yaml +24 -0
- package/.jumpstart/schemas/timeline.schema.json +1 -0
- package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
- package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
- package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
- package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
- package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
- package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
- package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
- package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
- package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
- package/.jumpstart/state/timeline.json +659 -0
- package/.jumpstart/usage-log.json +74 -3
- package/README.md +62 -1
- package/bin/cli.js +3217 -1
- package/bin/headless-runner.js +62 -2
- package/bin/lib/agent-checkpoint.js +168 -0
- package/bin/lib/ai-evaluation.js +104 -0
- package/bin/lib/ai-intake.js +152 -0
- package/bin/lib/ambiguity-heatmap.js +152 -0
- package/bin/lib/artifact-comparison.js +104 -0
- package/bin/lib/ast-edit-engine.js +157 -0
- package/bin/lib/backlog-sync.js +338 -0
- package/bin/lib/bcdr-planning.js +158 -0
- package/bin/lib/bidirectional-trace.js +199 -0
- package/bin/lib/branch-workflow.js +266 -0
- package/bin/lib/cab-output.js +119 -0
- package/bin/lib/chat-integration.js +122 -0
- package/bin/lib/ci-cd-integration.js +208 -0
- package/bin/lib/codebase-retrieval.js +125 -0
- package/bin/lib/collaboration.js +168 -0
- package/bin/lib/compliance-packs.js +213 -0
- package/bin/lib/context-chunker.js +128 -0
- package/bin/lib/context-onboarding.js +122 -0
- package/bin/lib/contract-first.js +124 -0
- package/bin/lib/cost-router.js +148 -0
- package/bin/lib/credential-boundary.js +155 -0
- package/bin/lib/data-classification.js +180 -0
- package/bin/lib/data-contracts.js +129 -0
- package/bin/lib/db-evolution.js +158 -0
- package/bin/lib/decision-conflicts.js +299 -0
- package/bin/lib/delivery-confidence.js +361 -0
- package/bin/lib/dependency-upgrade.js +153 -0
- package/bin/lib/design-system.js +133 -0
- package/bin/lib/deterministic-artifacts.js +151 -0
- package/bin/lib/diagram-studio.js +115 -0
- package/bin/lib/domain-ontology.js +140 -0
- package/bin/lib/ea-review-packet.js +151 -0
- package/bin/lib/enterprise-search.js +123 -0
- package/bin/lib/enterprise-templates.js +140 -0
- package/bin/lib/environment-promotion.js +220 -0
- package/bin/lib/estimation-studio.js +130 -0
- package/bin/lib/event-modeling.js +133 -0
- package/bin/lib/evidence-collector.js +179 -0
- package/bin/lib/finops-planner.js +182 -0
- package/bin/lib/fitness-functions.js +279 -0
- package/bin/lib/focus.js +448 -0
- package/bin/lib/governance-dashboard.js +165 -0
- package/bin/lib/guided-handoff.js +120 -0
- package/bin/lib/impact-analysis.js +190 -0
- package/bin/lib/incident-feedback.js +157 -0
- package/bin/lib/integrate.js +1 -1
- package/bin/lib/knowledge-graph.js +122 -0
- package/bin/lib/legacy-modernizer.js +160 -0
- package/bin/lib/migration-planner.js +144 -0
- package/bin/lib/model-governance.js +185 -0
- package/bin/lib/model-router.js +144 -0
- package/bin/lib/multi-repo.js +272 -0
- package/bin/lib/next-phase.js +53 -8
- package/bin/lib/ops-ownership.js +152 -0
- package/bin/lib/parallel-agents.js +257 -0
- package/bin/lib/pattern-library.js +115 -0
- package/bin/lib/persona-packs.js +99 -0
- package/bin/lib/plan-executor.js +366 -0
- package/bin/lib/platform-engineering.js +119 -0
- package/bin/lib/playback-summaries.js +126 -0
- package/bin/lib/policy-engine.js +240 -0
- package/bin/lib/portfolio-reporting.js +357 -0
- package/bin/lib/pr-package.js +197 -0
- package/bin/lib/project-memory.js +235 -0
- package/bin/lib/prompt-governance.js +130 -0
- package/bin/lib/promptless-mode.js +128 -0
- package/bin/lib/quality-graph.js +193 -0
- package/bin/lib/raci-matrix.js +188 -0
- package/bin/lib/refactor-planner.js +167 -0
- package/bin/lib/reference-architectures.js +304 -0
- package/bin/lib/release-readiness.js +171 -0
- package/bin/lib/repo-graph.js +262 -0
- package/bin/lib/requirements-baseline.js +358 -0
- package/bin/lib/risk-register.js +211 -0
- package/bin/lib/role-approval.js +249 -0
- package/bin/lib/role-views.js +142 -0
- package/bin/lib/root-cause-analysis.js +132 -0
- package/bin/lib/runtime-debugger.js +154 -0
- package/bin/lib/safe-rename.js +135 -0
- package/bin/lib/semantic-diff.js +335 -0
- package/bin/lib/sla-slo.js +210 -0
- package/bin/lib/spec-comments.js +147 -0
- package/bin/lib/spec-maturity.js +287 -0
- package/bin/lib/sre-integration.js +154 -0
- package/bin/lib/structured-elicitation.js +174 -0
- package/bin/lib/telemetry-feedback.js +118 -0
- package/bin/lib/test-generator.js +146 -0
- package/bin/lib/timeline.js +2 -1
- package/bin/lib/tool-bridge.js +107 -0
- package/bin/lib/tool-guardrails.js +139 -0
- package/bin/lib/tool-schemas.js +172 -3
- package/bin/lib/transcript-ingestion.js +150 -0
- package/bin/lib/vendor-risk.js +173 -0
- package/bin/lib/waiver-workflow.js +174 -0
- package/bin/lib/web-dashboard.js +126 -0
- package/bin/lib/workshop-mode.js +165 -0
- package/bin/lib/workstream-ownership.js +104 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|