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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* risk-register.js — Risk Register Generation & Tracking (Item 29)
|
|
3
|
+
*
|
|
4
|
+
* Create, update, and monitor business, delivery, security,
|
|
5
|
+
* and operational risks.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/risk-register.js add|update|list|report [options]
|
|
9
|
+
*
|
|
10
|
+
* State file: .jumpstart/state/risk-register.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'risk-register.json');
|
|
19
|
+
|
|
20
|
+
const RISK_CATEGORIES = ['business', 'delivery', 'security', 'operational', 'compliance', 'technical'];
|
|
21
|
+
const RISK_LIKELIHOODS = ['rare', 'unlikely', 'possible', 'likely', 'almost-certain'];
|
|
22
|
+
const RISK_IMPACTS = ['negligible', 'minor', 'moderate', 'major', 'critical'];
|
|
23
|
+
const RISK_STATUSES = ['identified', 'mitigating', 'accepted', 'resolved', 'closed'];
|
|
24
|
+
|
|
25
|
+
const RISK_SCORE_MATRIX = {
|
|
26
|
+
'rare': { negligible: 1, minor: 2, moderate: 3, major: 4, critical: 5 },
|
|
27
|
+
'unlikely': { negligible: 2, minor: 4, moderate: 6, major: 8, critical: 10 },
|
|
28
|
+
'possible': { negligible: 3, minor: 6, moderate: 9, major: 12, critical: 15 },
|
|
29
|
+
'likely': { negligible: 4, minor: 8, moderate: 12, major: 16, critical: 20 },
|
|
30
|
+
'almost-certain': { negligible: 5, minor: 10, moderate: 15, major: 20, critical: 25 }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function defaultState() {
|
|
34
|
+
return {
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
created_at: new Date().toISOString(),
|
|
37
|
+
last_updated: null,
|
|
38
|
+
risks: [],
|
|
39
|
+
mitigations: []
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadState(stateFile) {
|
|
44
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
45
|
+
if (!fs.existsSync(filePath)) return defaultState();
|
|
46
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
47
|
+
catch { return defaultState(); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveState(state, stateFile) {
|
|
51
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
52
|
+
const dir = path.dirname(filePath);
|
|
53
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
state.last_updated = new Date().toISOString();
|
|
55
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add a new risk.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} risk - { title, category, description, likelihood, impact, owner?, mitigation? }
|
|
62
|
+
* @param {object} [options]
|
|
63
|
+
* @returns {object}
|
|
64
|
+
*/
|
|
65
|
+
function addRisk(risk, options = {}) {
|
|
66
|
+
if (!risk || !risk.title || !risk.description) {
|
|
67
|
+
return { success: false, error: 'title and description are required' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const category = (risk.category || 'technical').toLowerCase();
|
|
71
|
+
if (!RISK_CATEGORIES.includes(category)) {
|
|
72
|
+
return { success: false, error: `Invalid category. Must be one of: ${RISK_CATEGORIES.join(', ')}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const likelihood = (risk.likelihood || 'possible').toLowerCase();
|
|
76
|
+
const impact = (risk.impact || 'moderate').toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (!RISK_LIKELIHOODS.includes(likelihood)) {
|
|
79
|
+
return { success: false, error: `Invalid likelihood. Must be one of: ${RISK_LIKELIHOODS.join(', ')}` };
|
|
80
|
+
}
|
|
81
|
+
if (!RISK_IMPACTS.includes(impact)) {
|
|
82
|
+
return { success: false, error: `Invalid impact. Must be one of: ${RISK_IMPACTS.join(', ')}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
86
|
+
const state = loadState(stateFile);
|
|
87
|
+
|
|
88
|
+
const score = RISK_SCORE_MATRIX[likelihood][impact];
|
|
89
|
+
|
|
90
|
+
const newRisk = {
|
|
91
|
+
id: `RISK-${(state.risks.length + 1).toString().padStart(3, '0')}`,
|
|
92
|
+
title: risk.title,
|
|
93
|
+
description: risk.description,
|
|
94
|
+
category,
|
|
95
|
+
likelihood,
|
|
96
|
+
impact,
|
|
97
|
+
score,
|
|
98
|
+
status: 'identified',
|
|
99
|
+
owner: risk.owner || null,
|
|
100
|
+
mitigation: risk.mitigation || null,
|
|
101
|
+
created_at: new Date().toISOString(),
|
|
102
|
+
updated_at: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
state.risks.push(newRisk);
|
|
106
|
+
saveState(state, stateFile);
|
|
107
|
+
|
|
108
|
+
return { success: true, risk: newRisk };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update a risk's status or details.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} riskId
|
|
115
|
+
* @param {object} updates
|
|
116
|
+
* @param {object} [options]
|
|
117
|
+
* @returns {object}
|
|
118
|
+
*/
|
|
119
|
+
function updateRisk(riskId, updates, options = {}) {
|
|
120
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
121
|
+
const state = loadState(stateFile);
|
|
122
|
+
|
|
123
|
+
const risk = state.risks.find(r => r.id === riskId);
|
|
124
|
+
if (!risk) return { success: false, error: `Risk not found: ${riskId}` };
|
|
125
|
+
|
|
126
|
+
if (updates.status && RISK_STATUSES.includes(updates.status)) risk.status = updates.status;
|
|
127
|
+
if (updates.mitigation) risk.mitigation = updates.mitigation;
|
|
128
|
+
if (updates.owner) risk.owner = updates.owner;
|
|
129
|
+
if (updates.likelihood && RISK_LIKELIHOODS.includes(updates.likelihood)) {
|
|
130
|
+
risk.likelihood = updates.likelihood;
|
|
131
|
+
risk.score = RISK_SCORE_MATRIX[risk.likelihood][risk.impact];
|
|
132
|
+
}
|
|
133
|
+
if (updates.impact && RISK_IMPACTS.includes(updates.impact)) {
|
|
134
|
+
risk.impact = updates.impact;
|
|
135
|
+
risk.score = RISK_SCORE_MATRIX[risk.likelihood][risk.impact];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
risk.updated_at = new Date().toISOString();
|
|
139
|
+
saveState(state, stateFile);
|
|
140
|
+
|
|
141
|
+
return { success: true, risk };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* List risks with optional filter.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} [filter]
|
|
148
|
+
* @param {object} [options]
|
|
149
|
+
* @returns {object}
|
|
150
|
+
*/
|
|
151
|
+
function listRisks(filter = {}, options = {}) {
|
|
152
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
153
|
+
const state = loadState(stateFile);
|
|
154
|
+
let risks = state.risks;
|
|
155
|
+
|
|
156
|
+
if (filter.category) risks = risks.filter(r => r.category === filter.category);
|
|
157
|
+
if (filter.status) risks = risks.filter(r => r.status === filter.status);
|
|
158
|
+
if (filter.minScore) risks = risks.filter(r => r.score >= filter.minScore);
|
|
159
|
+
|
|
160
|
+
return { success: true, risks, total: risks.length };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate risk report.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} [options]
|
|
167
|
+
* @returns {object}
|
|
168
|
+
*/
|
|
169
|
+
function generateReport(options = {}) {
|
|
170
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
171
|
+
const state = loadState(stateFile);
|
|
172
|
+
|
|
173
|
+
const byCategory = {};
|
|
174
|
+
const byStatus = {};
|
|
175
|
+
let totalScore = 0;
|
|
176
|
+
|
|
177
|
+
for (const risk of state.risks) {
|
|
178
|
+
byCategory[risk.category] = (byCategory[risk.category] || 0) + 1;
|
|
179
|
+
byStatus[risk.status] = (byStatus[risk.status] || 0) + 1;
|
|
180
|
+
totalScore += risk.score;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const highRisks = state.risks.filter(r => r.score >= 15);
|
|
184
|
+
const unmitigated = state.risks.filter(r => !r.mitigation && r.status === 'identified');
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
total_risks: state.risks.length,
|
|
189
|
+
by_category: byCategory,
|
|
190
|
+
by_status: byStatus,
|
|
191
|
+
average_score: state.risks.length > 0 ? Math.round(totalScore / state.risks.length) : 0,
|
|
192
|
+
high_risks: highRisks.length,
|
|
193
|
+
unmitigated: unmitigated.length,
|
|
194
|
+
top_risks: state.risks.sort((a, b) => b.score - a.score).slice(0, 5)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
defaultState,
|
|
200
|
+
loadState,
|
|
201
|
+
saveState,
|
|
202
|
+
addRisk,
|
|
203
|
+
updateRisk,
|
|
204
|
+
listRisks,
|
|
205
|
+
generateReport,
|
|
206
|
+
RISK_CATEGORIES,
|
|
207
|
+
RISK_LIKELIHOODS,
|
|
208
|
+
RISK_IMPACTS,
|
|
209
|
+
RISK_STATUSES,
|
|
210
|
+
RISK_SCORE_MATRIX
|
|
211
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* role-approval.js — Human Approval Workflows with Roles
|
|
3
|
+
*
|
|
4
|
+
* Supports named approvers by role: product, architect, security, legal,
|
|
5
|
+
* platform owner. Tracks multi-role approval chains for artifacts.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/role-approval.js assign|approve|status [options]
|
|
9
|
+
*
|
|
10
|
+
* State file: .jumpstart/state/role-approvals.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'role-approvals.json');
|
|
19
|
+
|
|
20
|
+
const APPROVER_ROLES = ['product', 'architect', 'security', 'legal', 'platform', 'qa', 'custom'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default role approval store.
|
|
24
|
+
* @returns {object}
|
|
25
|
+
*/
|
|
26
|
+
function defaultRoleApprovalStore() {
|
|
27
|
+
return {
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
created_at: new Date().toISOString(),
|
|
30
|
+
last_updated: null,
|
|
31
|
+
workflows: {} // artifact_path → workflow
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load the role approval store from disk.
|
|
37
|
+
* @param {string} [stateFile]
|
|
38
|
+
* @returns {object}
|
|
39
|
+
*/
|
|
40
|
+
function loadRoleApprovalStore(stateFile) {
|
|
41
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
return defaultRoleApprovalStore();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
47
|
+
} catch {
|
|
48
|
+
return defaultRoleApprovalStore();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save the role approval store to disk.
|
|
54
|
+
* @param {object} store
|
|
55
|
+
* @param {string} [stateFile]
|
|
56
|
+
*/
|
|
57
|
+
function saveRoleApprovalStore(store, stateFile) {
|
|
58
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
59
|
+
const dir = path.dirname(filePath);
|
|
60
|
+
if (!fs.existsSync(dir)) {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
store.last_updated = new Date().toISOString();
|
|
64
|
+
fs.writeFileSync(filePath, JSON.stringify(store, null, 2) + '\n', 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Assign approvers to an artifact.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} artifactPath - Relative path to the artifact.
|
|
71
|
+
* @param {object[]} approvers - Array of { role, name, required? }.
|
|
72
|
+
* @param {object} [options]
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
function assignApprovers(artifactPath, approvers, options = {}) {
|
|
76
|
+
if (!artifactPath) {
|
|
77
|
+
return { success: false, error: 'artifactPath is required' };
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(approvers) || approvers.length === 0) {
|
|
80
|
+
return { success: false, error: 'approvers array is required and must not be empty' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const a of approvers) {
|
|
84
|
+
const role = (a.role || '').toLowerCase();
|
|
85
|
+
if (!APPROVER_ROLES.includes(role)) {
|
|
86
|
+
return { success: false, error: `Invalid role "${a.role}". Must be one of: ${APPROVER_ROLES.join(', ')}` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
91
|
+
const store = loadRoleApprovalStore(stateFile);
|
|
92
|
+
|
|
93
|
+
const workflow = {
|
|
94
|
+
artifact: artifactPath,
|
|
95
|
+
created_at: new Date().toISOString(),
|
|
96
|
+
last_updated: new Date().toISOString(),
|
|
97
|
+
status: 'pending',
|
|
98
|
+
approvers: approvers.map(a => ({
|
|
99
|
+
role: a.role.toLowerCase(),
|
|
100
|
+
name: a.name || null,
|
|
101
|
+
required: a.required !== false,
|
|
102
|
+
status: 'pending', // pending | approved | rejected | skipped
|
|
103
|
+
approved_at: null,
|
|
104
|
+
comment: null
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
store.workflows[artifactPath] = workflow;
|
|
109
|
+
saveRoleApprovalStore(store, stateFile);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
artifact: artifactPath,
|
|
114
|
+
approvers: workflow.approvers,
|
|
115
|
+
total_required: workflow.approvers.filter(a => a.required).length
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Record an approval (or rejection) by a specific role.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} artifactPath
|
|
123
|
+
* @param {string} role
|
|
124
|
+
* @param {string} action - 'approve' | 'reject'
|
|
125
|
+
* @param {object} [options] - { approverName?, comment?, stateFile? }
|
|
126
|
+
* @returns {object}
|
|
127
|
+
*/
|
|
128
|
+
function recordRoleAction(artifactPath, role, action, options = {}) {
|
|
129
|
+
if (!artifactPath || !role || !action) {
|
|
130
|
+
return { success: false, error: 'artifactPath, role, and action are required' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!['approve', 'reject'].includes(action)) {
|
|
134
|
+
return { success: false, error: 'action must be "approve" or "reject"' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
138
|
+
const store = loadRoleApprovalStore(stateFile);
|
|
139
|
+
|
|
140
|
+
const workflow = store.workflows[artifactPath];
|
|
141
|
+
if (!workflow) {
|
|
142
|
+
return { success: false, error: `No approval workflow found for: ${artifactPath}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const normalizedRole = role.toLowerCase();
|
|
146
|
+
const approver = workflow.approvers.find(a => a.role === normalizedRole);
|
|
147
|
+
if (!approver) {
|
|
148
|
+
return { success: false, error: `Role "${role}" not assigned to this artifact` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
approver.status = action === 'approve' ? 'approved' : 'rejected';
|
|
152
|
+
approver.approved_at = new Date().toISOString();
|
|
153
|
+
if (options.approverName) approver.name = options.approverName;
|
|
154
|
+
if (options.comment) approver.comment = options.comment;
|
|
155
|
+
workflow.last_updated = new Date().toISOString();
|
|
156
|
+
|
|
157
|
+
// Update overall workflow status
|
|
158
|
+
const required = workflow.approvers.filter(a => a.required);
|
|
159
|
+
const allApproved = required.every(a => a.status === 'approved');
|
|
160
|
+
const anyRejected = required.some(a => a.status === 'rejected');
|
|
161
|
+
|
|
162
|
+
if (anyRejected) {
|
|
163
|
+
workflow.status = 'rejected';
|
|
164
|
+
} else if (allApproved) {
|
|
165
|
+
workflow.status = 'approved';
|
|
166
|
+
} else {
|
|
167
|
+
workflow.status = 'pending';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
saveRoleApprovalStore(store, stateFile);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
artifact: artifactPath,
|
|
175
|
+
role: normalizedRole,
|
|
176
|
+
action,
|
|
177
|
+
workflow_status: workflow.status,
|
|
178
|
+
pending_roles: workflow.approvers.filter(a => a.required && a.status === 'pending').map(a => a.role)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the approval status of an artifact.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} artifactPath
|
|
186
|
+
* @param {object} [options]
|
|
187
|
+
* @returns {object}
|
|
188
|
+
*/
|
|
189
|
+
function getApprovalStatus(artifactPath, options = {}) {
|
|
190
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
191
|
+
const store = loadRoleApprovalStore(stateFile);
|
|
192
|
+
|
|
193
|
+
const workflow = store.workflows[artifactPath];
|
|
194
|
+
if (!workflow) {
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
artifact: artifactPath,
|
|
198
|
+
has_workflow: false,
|
|
199
|
+
message: 'No approval workflow assigned'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pending = workflow.approvers.filter(a => a.required && a.status === 'pending').map(a => a.role);
|
|
204
|
+
const approved = workflow.approvers.filter(a => a.status === 'approved').map(a => a.role);
|
|
205
|
+
const rejected = workflow.approvers.filter(a => a.status === 'rejected').map(a => a.role);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
artifact: artifactPath,
|
|
210
|
+
has_workflow: true,
|
|
211
|
+
status: workflow.status,
|
|
212
|
+
pending_roles: pending,
|
|
213
|
+
approved_roles: approved,
|
|
214
|
+
rejected_roles: rejected,
|
|
215
|
+
approvers: workflow.approvers,
|
|
216
|
+
fully_approved: workflow.status === 'approved'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* List all approval workflows.
|
|
222
|
+
*
|
|
223
|
+
* @param {object} [filter] - { status? }
|
|
224
|
+
* @param {object} [options]
|
|
225
|
+
* @returns {object}
|
|
226
|
+
*/
|
|
227
|
+
function listApprovalWorkflows(filter = {}, options = {}) {
|
|
228
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
229
|
+
const store = loadRoleApprovalStore(stateFile);
|
|
230
|
+
|
|
231
|
+
let workflows = Object.values(store.workflows);
|
|
232
|
+
|
|
233
|
+
if (filter.status) {
|
|
234
|
+
workflows = workflows.filter(w => w.status === filter.status);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { success: true, workflows, total: workflows.length };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
APPROVER_ROLES,
|
|
242
|
+
loadRoleApprovalStore,
|
|
243
|
+
saveRoleApprovalStore,
|
|
244
|
+
defaultRoleApprovalStore,
|
|
245
|
+
assignApprovers,
|
|
246
|
+
recordRoleAction,
|
|
247
|
+
getApprovalStatus,
|
|
248
|
+
listApprovalWorkflows
|
|
249
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* role-views.js — Role-Based Project Views (Item 62)
|
|
3
|
+
*
|
|
4
|
+
* Same project data, different role-based lenses for
|
|
5
|
+
* executive, architect, product, and engineer audiences.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/role-views.js generate|list [options]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const ROLES = ['executive', 'architect', 'product', 'engineer'];
|
|
17
|
+
|
|
18
|
+
const ROLE_FOCUS = {
|
|
19
|
+
executive: {
|
|
20
|
+
label: 'Executive View',
|
|
21
|
+
focus: ['timeline', 'risks', 'budget', 'milestones', 'decisions'],
|
|
22
|
+
exclude: ['code_details', 'test_coverage', 'api_contracts']
|
|
23
|
+
},
|
|
24
|
+
architect: {
|
|
25
|
+
label: 'Architect View',
|
|
26
|
+
focus: ['components', 'data_model', 'api_contracts', 'decisions', 'tech_stack', 'nfrs'],
|
|
27
|
+
exclude: ['budget', 'stakeholder_comms']
|
|
28
|
+
},
|
|
29
|
+
product: {
|
|
30
|
+
label: 'Product View',
|
|
31
|
+
focus: ['stories', 'acceptance_criteria', 'personas', 'journeys', 'scope', 'priorities'],
|
|
32
|
+
exclude: ['api_contracts', 'data_model', 'test_coverage']
|
|
33
|
+
},
|
|
34
|
+
engineer: {
|
|
35
|
+
label: 'Engineer View',
|
|
36
|
+
focus: ['tasks', 'api_contracts', 'data_model', 'test_coverage', 'tech_stack', 'code_details'],
|
|
37
|
+
exclude: ['budget', 'stakeholder_comms', 'personas']
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a role-specific view of the project.
|
|
43
|
+
*/
|
|
44
|
+
function generateView(root, role, options = {}) {
|
|
45
|
+
if (!ROLES.includes(role)) {
|
|
46
|
+
return { success: false, error: `Unknown role: ${role}. Valid roles: ${ROLES.join(', ')}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const config = ROLE_FOCUS[role];
|
|
50
|
+
const view = {
|
|
51
|
+
role,
|
|
52
|
+
label: config.label,
|
|
53
|
+
generated_at: new Date().toISOString(),
|
|
54
|
+
focus_areas: config.focus,
|
|
55
|
+
excluded_areas: config.exclude,
|
|
56
|
+
sections: {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Gather available specs
|
|
60
|
+
const specsDir = path.join(root, 'specs');
|
|
61
|
+
const availableSpecs = [];
|
|
62
|
+
if (fs.existsSync(specsDir)) {
|
|
63
|
+
for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.md'))) {
|
|
64
|
+
availableSpecs.push(f);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
view.sections.available_specs = availableSpecs;
|
|
68
|
+
|
|
69
|
+
// Phase status
|
|
70
|
+
const stateFile = path.join(root, '.jumpstart', 'state', 'state.json');
|
|
71
|
+
if (fs.existsSync(stateFile)) {
|
|
72
|
+
try {
|
|
73
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
74
|
+
view.sections.phase_status = {
|
|
75
|
+
current_phase: state.current_phase || 0,
|
|
76
|
+
current_agent: state.current_agent || null
|
|
77
|
+
};
|
|
78
|
+
} catch { view.sections.phase_status = { current_phase: 0 }; }
|
|
79
|
+
} else {
|
|
80
|
+
view.sections.phase_status = { current_phase: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Risk summary (if relevant to role)
|
|
84
|
+
if (config.focus.includes('risks')) {
|
|
85
|
+
const riskFile = path.join(root, '.jumpstart', 'state', 'risk-register.json');
|
|
86
|
+
if (fs.existsSync(riskFile)) {
|
|
87
|
+
try {
|
|
88
|
+
const risks = JSON.parse(fs.readFileSync(riskFile, 'utf8'));
|
|
89
|
+
view.sections.risks = {
|
|
90
|
+
total: (risks.risks || []).length,
|
|
91
|
+
high: (risks.risks || []).filter(r => r.score >= 15).length
|
|
92
|
+
};
|
|
93
|
+
} catch { view.sections.risks = { total: 0, high: 0 }; }
|
|
94
|
+
} else {
|
|
95
|
+
view.sections.risks = { total: 0, high: 0 };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { success: true, view };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List all available roles and their focus areas.
|
|
104
|
+
*/
|
|
105
|
+
function listRoles() {
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
roles: ROLES.map(r => ({
|
|
109
|
+
id: r,
|
|
110
|
+
label: ROLE_FOCUS[r].label,
|
|
111
|
+
focus: ROLE_FOCUS[r].focus,
|
|
112
|
+
exclude: ROLE_FOCUS[r].exclude
|
|
113
|
+
}))
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate summary for a specific role.
|
|
119
|
+
*/
|
|
120
|
+
function generateRoleSummary(root, role) {
|
|
121
|
+
const viewResult = generateView(root, role);
|
|
122
|
+
if (!viewResult.success) return viewResult;
|
|
123
|
+
|
|
124
|
+
const view = viewResult.view;
|
|
125
|
+
const summary = {
|
|
126
|
+
role,
|
|
127
|
+
label: view.label,
|
|
128
|
+
current_phase: view.sections.phase_status ? view.sections.phase_status.current_phase : 0,
|
|
129
|
+
specs_count: view.sections.available_specs ? view.sections.available_specs.length : 0,
|
|
130
|
+
generated_at: view.generated_at
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return { success: true, summary };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
generateView,
|
|
138
|
+
listRoles,
|
|
139
|
+
generateRoleSummary,
|
|
140
|
+
ROLES,
|
|
141
|
+
ROLE_FOCUS
|
|
142
|
+
};
|