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.
- package/.github/agents/jumpstart-adversary.agent.md +2 -1
- package/.github/agents/jumpstart-architect.agent.md +6 -7
- package/.github/agents/jumpstart-challenger.agent.md +2 -1
- package/.github/agents/jumpstart-developer.agent.md +1 -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/agents/jumpstart-uiux-designer.agent.md +66 -0
- package/.github/workflows/quality.yml +19 -2
- package/.jumpstart/agents/analyst.md +38 -0
- package/.jumpstart/agents/architect.md +39 -1
- 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 +29 -9
- package/.jumpstart/commands/commands.md +6 -5
- package/.jumpstart/config.yaml +25 -1
- package/.jumpstart/roadmap.md +1 -1
- package/.jumpstart/schemas/timeline.schema.json +1 -0
- package/.jumpstart/skills/README.md +1 -0
- package/.jumpstart/skills/quality-gates/SKILL.md +126 -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/skills/ui-ux-pro-max/SKILL.md +266 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.jumpstart/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.jumpstart/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.jumpstart/state/timeline.json +659 -0
- package/.jumpstart/templates/model-map.md +1 -1
- package/.jumpstart/templates/ux-design.md +3 -3
- package/.jumpstart/usage-log.json +74 -3
- package/AGENTS.md +1 -1
- package/README.md +64 -3
- 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/secret-scanner.js +313 -0
- package/bin/lib/semantic-diff.js +335 -0
- package/bin/lib/sla-slo.js +210 -0
- package/bin/lib/smoke-tester.js +344 -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 +159 -0
- package/bin/lib/tool-guardrails.js +139 -0
- package/bin/lib/tool-schemas.js +281 -3
- package/bin/lib/transcript-ingestion.js +150 -0
- package/bin/lib/type-checker.js +261 -0
- package/bin/lib/uat-coverage.js +411 -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
- package/.github/agents/jumpstart-ux-designer.agent.md +0 -45
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sla-slo.js — SLA & SLO Specification Support (Item 28)
|
|
3
|
+
*
|
|
4
|
+
* Make operational expectations first-class citizens in the PRD
|
|
5
|
+
* and architecture.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node bin/lib/sla-slo.js define|check|report [options]
|
|
9
|
+
*
|
|
10
|
+
* State file: .jumpstart/state/sla-slo.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', 'sla-slo.json');
|
|
19
|
+
|
|
20
|
+
const SLO_TYPES = ['availability', 'latency', 'throughput', 'error-rate', 'durability', 'freshness'];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SLO_TEMPLATES = {
|
|
23
|
+
'web-api': [
|
|
24
|
+
{ type: 'availability', target: 99.9, unit: 'percent', window: '30d' },
|
|
25
|
+
{ type: 'latency', target: 200, unit: 'ms', percentile: 'p99', window: '30d' },
|
|
26
|
+
{ type: 'error-rate', target: 0.1, unit: 'percent', window: '30d' }
|
|
27
|
+
],
|
|
28
|
+
'batch-processing': [
|
|
29
|
+
{ type: 'availability', target: 99.5, unit: 'percent', window: '30d' },
|
|
30
|
+
{ type: 'throughput', target: 1000, unit: 'records/sec', window: '1h' },
|
|
31
|
+
{ type: 'freshness', target: 60, unit: 'minutes', window: '24h' }
|
|
32
|
+
],
|
|
33
|
+
'data-pipeline': [
|
|
34
|
+
{ type: 'availability', target: 99.0, unit: 'percent', window: '30d' },
|
|
35
|
+
{ type: 'freshness', target: 15, unit: 'minutes', window: '24h' },
|
|
36
|
+
{ type: 'durability', target: 99.999, unit: 'percent', window: '30d' }
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function defaultState() {
|
|
41
|
+
return {
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
created_at: new Date().toISOString(),
|
|
44
|
+
last_updated: null,
|
|
45
|
+
slos: [],
|
|
46
|
+
slas: [],
|
|
47
|
+
error_budgets: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadState(stateFile) {
|
|
52
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
53
|
+
if (!fs.existsSync(filePath)) return defaultState();
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
55
|
+
catch { return defaultState(); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveState(state, stateFile) {
|
|
59
|
+
const filePath = stateFile || DEFAULT_STATE_FILE;
|
|
60
|
+
const dir = path.dirname(filePath);
|
|
61
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
state.last_updated = new Date().toISOString();
|
|
63
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Define an SLO.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} slo - { name, service, type, target, unit, window, description? }
|
|
70
|
+
* @param {object} [options]
|
|
71
|
+
* @returns {object}
|
|
72
|
+
*/
|
|
73
|
+
function defineSLO(slo, options = {}) {
|
|
74
|
+
if (!slo || !slo.name || !slo.service || !slo.target) {
|
|
75
|
+
return { success: false, error: 'name, service, and target are required' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const type = (slo.type || 'availability').toLowerCase();
|
|
79
|
+
if (!SLO_TYPES.includes(type)) {
|
|
80
|
+
return { success: false, error: `Invalid type. Must be one of: ${SLO_TYPES.join(', ')}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
84
|
+
const state = loadState(stateFile);
|
|
85
|
+
|
|
86
|
+
const newSLO = {
|
|
87
|
+
id: `SLO-${Date.now().toString(36).toUpperCase()}`,
|
|
88
|
+
name: slo.name,
|
|
89
|
+
service: slo.service,
|
|
90
|
+
type,
|
|
91
|
+
target: slo.target,
|
|
92
|
+
unit: slo.unit || 'percent',
|
|
93
|
+
window: slo.window || '30d',
|
|
94
|
+
description: slo.description || '',
|
|
95
|
+
created_at: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
state.slos.push(newSLO);
|
|
99
|
+
saveState(state, stateFile);
|
|
100
|
+
|
|
101
|
+
return { success: true, slo: newSLO };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply an SLO template by service type.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} serviceName
|
|
108
|
+
* @param {string} templateType - 'web-api' | 'batch-processing' | 'data-pipeline'
|
|
109
|
+
* @param {object} [options]
|
|
110
|
+
* @returns {object}
|
|
111
|
+
*/
|
|
112
|
+
function applyTemplate(serviceName, templateType, options = {}) {
|
|
113
|
+
const template = DEFAULT_SLO_TEMPLATES[templateType];
|
|
114
|
+
if (!template) {
|
|
115
|
+
return { success: false, error: `Unknown template: ${templateType}. Available: ${Object.keys(DEFAULT_SLO_TEMPLATES).join(', ')}` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const t of template) {
|
|
120
|
+
const result = defineSLO({
|
|
121
|
+
name: `${serviceName} ${t.type}`,
|
|
122
|
+
service: serviceName,
|
|
123
|
+
...t
|
|
124
|
+
}, options);
|
|
125
|
+
results.push(result);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { success: true, service: serviceName, template: templateType, slos_created: results.length };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check SLO coverage in specs.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} root - Project root.
|
|
135
|
+
* @param {object} [options]
|
|
136
|
+
* @returns {object}
|
|
137
|
+
*/
|
|
138
|
+
function checkSLOCoverage(root, options = {}) {
|
|
139
|
+
const stateFile = options.stateFile || path.join(root, DEFAULT_STATE_FILE);
|
|
140
|
+
const state = loadState(stateFile);
|
|
141
|
+
|
|
142
|
+
const archFile = path.join(root, 'specs', 'architecture.md');
|
|
143
|
+
const prdFile = path.join(root, 'specs', 'prd.md');
|
|
144
|
+
|
|
145
|
+
let archHasSLO = false;
|
|
146
|
+
let prdHasSLO = false;
|
|
147
|
+
|
|
148
|
+
if (fs.existsSync(archFile)) {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(archFile, 'utf8');
|
|
151
|
+
archHasSLO = /\bSL[OA]\b|service.level|availability|latency.target/i.test(content);
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(prdFile)) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(prdFile, 'utf8');
|
|
158
|
+
prdHasSLO = /\bSL[OA]\b|service.level|availability|uptime/i.test(content);
|
|
159
|
+
} catch { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
defined_slos: state.slos.length,
|
|
165
|
+
architecture_mentions_slo: archHasSLO,
|
|
166
|
+
prd_mentions_slo: prdHasSLO,
|
|
167
|
+
coverage: state.slos.length > 0 ? 'defined' : 'missing',
|
|
168
|
+
recommendations: state.slos.length === 0 ? ['Define SLOs using `jumpstart-mode sla-slo define`'] : []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate SLO report.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} [options]
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
function generateReport(options = {}) {
|
|
179
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
180
|
+
const state = loadState(stateFile);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
slos: state.slos,
|
|
185
|
+
slas: state.slas,
|
|
186
|
+
total_slos: state.slos.length,
|
|
187
|
+
total_slas: state.slas.length,
|
|
188
|
+
by_service: state.slos.reduce((acc, s) => {
|
|
189
|
+
acc[s.service] = acc[s.service] || [];
|
|
190
|
+
acc[s.service].push(s);
|
|
191
|
+
return acc;
|
|
192
|
+
}, {}),
|
|
193
|
+
by_type: state.slos.reduce((acc, s) => {
|
|
194
|
+
acc[s.type] = (acc[s.type] || 0) + 1;
|
|
195
|
+
return acc;
|
|
196
|
+
}, {})
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
defaultState,
|
|
202
|
+
loadState,
|
|
203
|
+
saveState,
|
|
204
|
+
defineSLO,
|
|
205
|
+
applyTemplate,
|
|
206
|
+
checkSLOCoverage,
|
|
207
|
+
generateReport,
|
|
208
|
+
SLO_TYPES,
|
|
209
|
+
DEFAULT_SLO_TEMPLATES
|
|
210
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smoke-tester.js — Automated Smoke Testing
|
|
3
|
+
*
|
|
4
|
+
* Performs a "vitality" check on the application: attempts a build
|
|
5
|
+
* and optionally starts the app to verify it responds (e.g., 200 OK
|
|
6
|
+
* on a health route). Provides immediate feedback to the Developer
|
|
7
|
+
* agent during Phase 4 implementation.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* echo '{"root":"."}' | node bin/lib/smoke-tester.js
|
|
11
|
+
*
|
|
12
|
+
* Input (stdin JSON):
|
|
13
|
+
* {
|
|
14
|
+
* "root": ".",
|
|
15
|
+
* "config": {
|
|
16
|
+
* "build_command": "npm run build",
|
|
17
|
+
* "start_command": "npm start",
|
|
18
|
+
* "health_url": "http://localhost:3000/health",
|
|
19
|
+
* "health_timeout": 10000,
|
|
20
|
+
* "skip_health_check": false
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Output (stdout JSON):
|
|
25
|
+
* {
|
|
26
|
+
* "build": { "pass": true, "command": "npm run build", "duration_ms": 3200 },
|
|
27
|
+
* "health": { "pass": true, "url": "http://localhost:3000/health", "status": 200 },
|
|
28
|
+
* "pass": true
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createRequire } from 'module';
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const { execSync, spawn } = require('child_process');
|
|
37
|
+
const http = require('http');
|
|
38
|
+
const https = require('https');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect the project's build command from package.json or common configs.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} root - Project root path.
|
|
44
|
+
* @returns {{ build: string|null, start: string|null, type: string }}
|
|
45
|
+
*/
|
|
46
|
+
function detectProjectCommands(root) {
|
|
47
|
+
const result = { build: null, start: null, type: 'unknown' };
|
|
48
|
+
|
|
49
|
+
// Check package.json
|
|
50
|
+
const pkgPath = path.join(root, 'package.json');
|
|
51
|
+
if (fs.existsSync(pkgPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
54
|
+
result.type = 'node';
|
|
55
|
+
if (pkg.scripts) {
|
|
56
|
+
if (pkg.scripts.build) result.build = 'npm run build';
|
|
57
|
+
if (pkg.scripts.start) result.start = 'npm start';
|
|
58
|
+
if (pkg.scripts.dev) result.start = result.start || 'npm run dev';
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for Python projects
|
|
67
|
+
if (fs.existsSync(path.join(root, 'pyproject.toml')) ||
|
|
68
|
+
fs.existsSync(path.join(root, 'setup.py'))) {
|
|
69
|
+
result.type = 'python';
|
|
70
|
+
if (fs.existsSync(path.join(root, 'Makefile'))) {
|
|
71
|
+
result.build = 'make build';
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for Go projects
|
|
77
|
+
if (fs.existsSync(path.join(root, 'go.mod'))) {
|
|
78
|
+
result.type = 'go';
|
|
79
|
+
result.build = 'go build ./...';
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check Makefile as fallback
|
|
84
|
+
if (fs.existsSync(path.join(root, 'Makefile'))) {
|
|
85
|
+
result.build = 'make';
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run a build command and report results.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} command - Build command to run.
|
|
96
|
+
* @param {string} root - Project root path.
|
|
97
|
+
* @returns {{ pass: boolean, command: string, duration_ms: number, output: string, exit_code: number }}
|
|
98
|
+
*/
|
|
99
|
+
function runBuild(command, root) {
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
let output = '';
|
|
102
|
+
let exitCode = 0;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
output = execSync(command, {
|
|
106
|
+
cwd: root,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
109
|
+
timeout: 120000
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
output = (err.stdout || '') + (err.stderr || '');
|
|
113
|
+
exitCode = err.status || 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const duration = Date.now() - start;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
pass: exitCode === 0,
|
|
120
|
+
command,
|
|
121
|
+
duration_ms: duration,
|
|
122
|
+
output: output.substring(0, 2000),
|
|
123
|
+
exit_code: exitCode
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Perform a health check by making an HTTP(S) GET request.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} url - URL to check.
|
|
131
|
+
* @param {number} timeout - Timeout in milliseconds.
|
|
132
|
+
* @returns {Promise<{ pass: boolean, url: string, status: number|null, error: string|null }>}
|
|
133
|
+
*/
|
|
134
|
+
function checkHealth(url, timeout = 10000) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const client = url.startsWith('https') ? https : http;
|
|
137
|
+
let settled = false;
|
|
138
|
+
const settle = (result) => {
|
|
139
|
+
if (!settled) {
|
|
140
|
+
settled = true;
|
|
141
|
+
resolve(result);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let req;
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
if (req) { try { req.destroy(); } catch { /* ignore */ } }
|
|
148
|
+
settle({ pass: false, url, status: null, error: 'Timeout' });
|
|
149
|
+
}, timeout);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
req = client.get(url, (res) => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
settle({
|
|
155
|
+
pass: res.statusCode >= 200 && res.statusCode < 400,
|
|
156
|
+
url,
|
|
157
|
+
status: res.statusCode,
|
|
158
|
+
error: null
|
|
159
|
+
});
|
|
160
|
+
res.resume();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
req.on('error', (err) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
settle({ pass: false, url, status: null, error: err.message || err.code || 'Connection failed' });
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
settle({ pass: false, url, status: null, error: err.message });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start a server process and wait for it to be ready.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} command - Start command.
|
|
178
|
+
* @param {string} root - Project root path.
|
|
179
|
+
* @param {string} healthUrl - URL to check for readiness.
|
|
180
|
+
* @param {number} timeout - Total timeout in milliseconds.
|
|
181
|
+
* @returns {Promise<{ process: object|null, ready: boolean, error: string|null }>}
|
|
182
|
+
*/
|
|
183
|
+
function startAndWait(command, root, healthUrl, timeout = 15000) {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const parts = command.split(' ');
|
|
186
|
+
const proc = spawn(parts[0], parts.slice(1), {
|
|
187
|
+
cwd: root,
|
|
188
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
189
|
+
detached: false
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
let stderr = '';
|
|
193
|
+
proc.stderr.on('data', d => { stderr += d.toString(); });
|
|
194
|
+
|
|
195
|
+
const deadline = Date.now() + timeout;
|
|
196
|
+
const pollInterval = 1000;
|
|
197
|
+
|
|
198
|
+
const poll = async () => {
|
|
199
|
+
if (Date.now() > deadline) {
|
|
200
|
+
try { proc.kill(); } catch { /* ignore */ }
|
|
201
|
+
resolve({ process: null, ready: false, error: 'Startup timeout exceeded' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const health = await checkHealth(healthUrl, 2000);
|
|
206
|
+
if (health.pass) {
|
|
207
|
+
resolve({ process: proc, ready: true, error: null });
|
|
208
|
+
} else {
|
|
209
|
+
setTimeout(poll, pollInterval);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
proc.on('error', (err) => {
|
|
214
|
+
resolve({ process: null, ready: false, error: err.message });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
proc.on('exit', (code) => {
|
|
218
|
+
if (code !== 0 && code !== null) {
|
|
219
|
+
resolve({ process: null, ready: false, error: `Process exited with code ${code}: ${stderr.substring(0, 500)}` });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Start polling after a brief delay
|
|
224
|
+
setTimeout(poll, 1500);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Run the full smoke test suite.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} input - Smoke test options.
|
|
232
|
+
* @param {string} [input.root] - Project root.
|
|
233
|
+
* @param {object} [input.config] - Override config.
|
|
234
|
+
* @returns {Promise<object>} Smoke test results.
|
|
235
|
+
*/
|
|
236
|
+
async function runSmokeTest(input) {
|
|
237
|
+
const { root = '.', config = {} } = input;
|
|
238
|
+
const resolvedRoot = path.resolve(root);
|
|
239
|
+
|
|
240
|
+
// Detect or use provided commands
|
|
241
|
+
const detected = detectProjectCommands(resolvedRoot);
|
|
242
|
+
const buildCommand = config.build_command || detected.build;
|
|
243
|
+
const startCommand = config.start_command || detected.start;
|
|
244
|
+
const healthUrl = config.health_url || 'http://localhost:3000/health';
|
|
245
|
+
const healthTimeout = config.health_timeout || 10000;
|
|
246
|
+
const skipHealthCheck = config.skip_health_check || false;
|
|
247
|
+
|
|
248
|
+
const result = {
|
|
249
|
+
project_type: detected.type,
|
|
250
|
+
build: null,
|
|
251
|
+
health: null,
|
|
252
|
+
pass: true
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Step 1: Build
|
|
256
|
+
if (buildCommand) {
|
|
257
|
+
result.build = runBuild(buildCommand, resolvedRoot);
|
|
258
|
+
if (!result.build.pass) {
|
|
259
|
+
result.pass = false;
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
result.build = {
|
|
264
|
+
pass: true,
|
|
265
|
+
command: null,
|
|
266
|
+
duration_ms: 0,
|
|
267
|
+
output: 'No build command detected.',
|
|
268
|
+
exit_code: 0
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Step 2: Health check (optional)
|
|
273
|
+
if (!skipHealthCheck && startCommand) {
|
|
274
|
+
const startup = await startAndWait(startCommand, resolvedRoot, healthUrl, healthTimeout);
|
|
275
|
+
|
|
276
|
+
if (startup.ready) {
|
|
277
|
+
const health = await checkHealth(healthUrl, 5000);
|
|
278
|
+
result.health = health;
|
|
279
|
+
result.pass = result.pass && health.pass;
|
|
280
|
+
} else {
|
|
281
|
+
result.health = {
|
|
282
|
+
pass: false,
|
|
283
|
+
url: healthUrl,
|
|
284
|
+
status: null,
|
|
285
|
+
error: startup.error || 'Could not start server'
|
|
286
|
+
};
|
|
287
|
+
result.pass = false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Clean up server process
|
|
291
|
+
if (startup.process) {
|
|
292
|
+
try { startup.process.kill(); } catch { /* ignore */ }
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
result.health = {
|
|
296
|
+
pass: true,
|
|
297
|
+
url: null,
|
|
298
|
+
status: null,
|
|
299
|
+
error: skipHealthCheck ? 'Health check skipped' : 'No start command detected'
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── CLI Entry Point ──────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
if (process.argv[1] && (
|
|
309
|
+
process.argv[1].endsWith('smoke-tester.js') ||
|
|
310
|
+
process.argv[1].endsWith('smoke-tester')
|
|
311
|
+
)) {
|
|
312
|
+
let input = '';
|
|
313
|
+
process.stdin.setEncoding('utf8');
|
|
314
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
315
|
+
process.stdin.on('end', () => {
|
|
316
|
+
(async () => {
|
|
317
|
+
try {
|
|
318
|
+
const parsed = input.trim() ? JSON.parse(input) : {};
|
|
319
|
+
const result = await runSmokeTest(parsed);
|
|
320
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
321
|
+
process.exit(result.pass ? 0 : 1);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
|
|
324
|
+
process.exit(2);
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (process.stdin.isTTY) {
|
|
330
|
+
(async () => {
|
|
331
|
+
const result = await runSmokeTest({});
|
|
332
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
333
|
+
process.exit(result.pass ? 0 : 1);
|
|
334
|
+
})();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export {
|
|
339
|
+
runSmokeTest,
|
|
340
|
+
detectProjectCommands,
|
|
341
|
+
runBuild,
|
|
342
|
+
checkHealth,
|
|
343
|
+
startAndWait
|
|
344
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec-comments.js — Inline Spec Review Comments (Item 63)
|
|
3
|
+
*
|
|
4
|
+
* Comment, resolve, assign, and approve within artifact sections.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/lib/spec-comments.js add|resolve|list|assign [options]
|
|
8
|
+
*
|
|
9
|
+
* State file: .jumpstart/state/spec-comments.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STATE_FILE = path.join('.jumpstart', 'state', 'spec-comments.json');
|
|
18
|
+
|
|
19
|
+
const COMMENT_STATUSES = ['open', 'resolved', 'wontfix', 'deferred'];
|
|
20
|
+
|
|
21
|
+
function defaultState() {
|
|
22
|
+
return {
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
created_at: new Date().toISOString(),
|
|
25
|
+
last_updated: null,
|
|
26
|
+
comments: []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadState(stateFile) {
|
|
31
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
32
|
+
if (!fs.existsSync(fp)) return defaultState();
|
|
33
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
34
|
+
catch { return defaultState(); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveState(state, stateFile) {
|
|
38
|
+
const fp = stateFile || DEFAULT_STATE_FILE;
|
|
39
|
+
const dir = path.dirname(fp);
|
|
40
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
state.last_updated = new Date().toISOString();
|
|
42
|
+
fs.writeFileSync(fp, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add a review comment to a spec artifact.
|
|
47
|
+
*/
|
|
48
|
+
function addComment(artifact, section, text, options = {}) {
|
|
49
|
+
if (!artifact || !text) return { success: false, error: 'artifact and text are required' };
|
|
50
|
+
|
|
51
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
52
|
+
const state = loadState(stateFile);
|
|
53
|
+
|
|
54
|
+
const comment = {
|
|
55
|
+
id: `C-${Date.now()}`,
|
|
56
|
+
artifact,
|
|
57
|
+
section: section || null,
|
|
58
|
+
text,
|
|
59
|
+
author: options.author || 'anonymous',
|
|
60
|
+
assignee: options.assignee || null,
|
|
61
|
+
status: 'open',
|
|
62
|
+
created_at: new Date().toISOString(),
|
|
63
|
+
resolved_at: null,
|
|
64
|
+
replies: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
state.comments.push(comment);
|
|
68
|
+
saveState(state, stateFile);
|
|
69
|
+
|
|
70
|
+
return { success: true, comment };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a comment.
|
|
75
|
+
*/
|
|
76
|
+
function resolveComment(commentId, resolution, options = {}) {
|
|
77
|
+
if (!commentId) return { success: false, error: 'commentId is required' };
|
|
78
|
+
|
|
79
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
80
|
+
const state = loadState(stateFile);
|
|
81
|
+
|
|
82
|
+
const comment = state.comments.find(c => c.id === commentId);
|
|
83
|
+
if (!comment) return { success: false, error: `Comment ${commentId} not found` };
|
|
84
|
+
|
|
85
|
+
comment.status = 'resolved';
|
|
86
|
+
comment.resolution = resolution || 'Resolved';
|
|
87
|
+
comment.resolved_at = new Date().toISOString();
|
|
88
|
+
comment.resolved_by = options.author || 'anonymous';
|
|
89
|
+
saveState(state, stateFile);
|
|
90
|
+
|
|
91
|
+
return { success: true, comment };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List comments, optionally filtered.
|
|
96
|
+
*/
|
|
97
|
+
function listComments(options = {}) {
|
|
98
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
99
|
+
const state = loadState(stateFile);
|
|
100
|
+
|
|
101
|
+
let comments = state.comments;
|
|
102
|
+
|
|
103
|
+
if (options.artifact) {
|
|
104
|
+
comments = comments.filter(c => c.artifact === options.artifact);
|
|
105
|
+
}
|
|
106
|
+
if (options.status) {
|
|
107
|
+
comments = comments.filter(c => c.status === options.status);
|
|
108
|
+
}
|
|
109
|
+
if (options.assignee) {
|
|
110
|
+
comments = comments.filter(c => c.assignee === options.assignee);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
total: comments.length,
|
|
116
|
+
comments
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Assign a comment to a reviewer.
|
|
122
|
+
*/
|
|
123
|
+
function assignComment(commentId, assignee, options = {}) {
|
|
124
|
+
if (!commentId || !assignee) return { success: false, error: 'commentId and assignee are required' };
|
|
125
|
+
|
|
126
|
+
const stateFile = options.stateFile || DEFAULT_STATE_FILE;
|
|
127
|
+
const state = loadState(stateFile);
|
|
128
|
+
|
|
129
|
+
const comment = state.comments.find(c => c.id === commentId);
|
|
130
|
+
if (!comment) return { success: false, error: `Comment ${commentId} not found` };
|
|
131
|
+
|
|
132
|
+
comment.assignee = assignee;
|
|
133
|
+
saveState(state, stateFile);
|
|
134
|
+
|
|
135
|
+
return { success: true, comment };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
addComment,
|
|
140
|
+
resolveComment,
|
|
141
|
+
listComments,
|
|
142
|
+
assignComment,
|
|
143
|
+
loadState,
|
|
144
|
+
saveState,
|
|
145
|
+
defaultState,
|
|
146
|
+
COMMENT_STATUSES
|
|
147
|
+
};
|