kiro-spec-engine 1.45.13 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -4
- package/README.zh.md +15 -3
- package/bin/kiro-spec-engine.js +84 -0
- package/docs/adoption-guide.md +3 -2
- package/docs/command-reference.md +30 -12
- package/docs/cross-tool-guide.md +2 -1
- package/docs/document-governance.md +2 -1
- package/docs/faq.md +14 -13
- package/docs/manual-workflows-guide.md +2 -1
- package/docs/quick-start-with-ai-tools.md +4 -3
- package/docs/quick-start.md +21 -7
- package/docs/spec-workflow.md +3 -2
- package/docs/tools/claude-guide.md +3 -2
- package/docs/tools/cursor-guide.md +3 -2
- package/docs/tools/generic-guide.md +3 -2
- package/docs/tools/vscode-guide.md +3 -2
- package/docs/tools/windsurf-guide.md +3 -2
- package/docs/troubleshooting.md +10 -9
- package/docs/zh/quick-start.md +30 -14
- package/docs/zh/tools/claude-guide.md +2 -1
- package/docs/zh/tools/cursor-guide.md +2 -1
- package/docs/zh/tools/generic-guide.md +8 -7
- package/docs/zh/tools/vscode-guide.md +2 -1
- package/docs/zh/tools/windsurf-guide.md +2 -1
- package/lib/commands/orchestrate.js +123 -95
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/package.json +2 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { DEFAULT_GATE_POLICY } = require('./default-policy');
|
|
4
|
+
|
|
5
|
+
class PolicyLoader {
|
|
6
|
+
constructor(projectPath = process.cwd()) {
|
|
7
|
+
this.projectPath = projectPath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async load(options = {}) {
|
|
11
|
+
const policyPath = this._resolvePolicyPath(options.policy);
|
|
12
|
+
if (!policyPath) {
|
|
13
|
+
return this._applyStrictMode(this._clone(DEFAULT_GATE_POLICY), options.strict);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const userPolicy = await fs.readJson(policyPath);
|
|
17
|
+
this._validatePolicy(userPolicy, policyPath);
|
|
18
|
+
|
|
19
|
+
const merged = this._deepMerge(this._clone(DEFAULT_GATE_POLICY), userPolicy);
|
|
20
|
+
return this._applyStrictMode(merged, options.strict);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getTemplate() {
|
|
24
|
+
return this._clone(DEFAULT_GATE_POLICY);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_resolvePolicyPath(explicitPath) {
|
|
28
|
+
if (explicitPath) {
|
|
29
|
+
return path.isAbsolute(explicitPath)
|
|
30
|
+
? explicitPath
|
|
31
|
+
: path.join(this.projectPath, explicitPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectPolicy = path.join(this.projectPath, '.kiro', 'config', 'spec-gate-policy.json');
|
|
35
|
+
if (fs.existsSync(projectPolicy)) {
|
|
36
|
+
return projectPolicy;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_applyStrictMode(policy, strict) {
|
|
43
|
+
if (!strict) {
|
|
44
|
+
return policy;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...policy,
|
|
49
|
+
strict_mode: {
|
|
50
|
+
...policy.strict_mode,
|
|
51
|
+
warning_as_failure: true
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_validatePolicy(policy, sourcePath) {
|
|
57
|
+
if (!policy || typeof policy !== 'object') {
|
|
58
|
+
throw new Error(`Invalid gate policy at ${sourcePath}: expected JSON object`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!policy.rules || typeof policy.rules !== 'object') {
|
|
62
|
+
throw new Error(`Invalid gate policy at ${sourcePath}: missing rules object`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (policy.thresholds) {
|
|
66
|
+
const { go, conditional_go: conditionalGo } = policy.thresholds;
|
|
67
|
+
if ((go !== undefined && typeof go !== 'number') ||
|
|
68
|
+
(conditionalGo !== undefined && typeof conditionalGo !== 'number')) {
|
|
69
|
+
throw new Error(`Invalid gate policy at ${sourcePath}: thresholds must be numeric`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_clone(value) {
|
|
75
|
+
return JSON.parse(JSON.stringify(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_deepMerge(base, override) {
|
|
79
|
+
const result = { ...base };
|
|
80
|
+
|
|
81
|
+
Object.keys(override || {}).forEach(key => {
|
|
82
|
+
const baseValue = result[key];
|
|
83
|
+
const overrideValue = override[key];
|
|
84
|
+
|
|
85
|
+
if (this._isPlainObject(baseValue) && this._isPlainObject(overrideValue)) {
|
|
86
|
+
result[key] = this._deepMerge(baseValue, overrideValue);
|
|
87
|
+
} else {
|
|
88
|
+
result[key] = overrideValue;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_isPlainObject(value) {
|
|
96
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
PolicyLoader
|
|
102
|
+
};
|
|
103
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
class ResultEmitter {
|
|
6
|
+
constructor(projectPath = process.cwd()) {
|
|
7
|
+
this.projectPath = projectPath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async emit(result, options = {}) {
|
|
11
|
+
if (!options.silent) {
|
|
12
|
+
if (options.json) {
|
|
13
|
+
console.log(JSON.stringify(result, null, 2));
|
|
14
|
+
} else {
|
|
15
|
+
this._printHumanReadable(result);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const outputPath = options.out
|
|
20
|
+
? this._resolvePath(options.out)
|
|
21
|
+
: null;
|
|
22
|
+
|
|
23
|
+
if (outputPath) {
|
|
24
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
25
|
+
await fs.writeJson(outputPath, result, { spaces: 2 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
outputPath
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_printHumanReadable(result) {
|
|
34
|
+
const decisionColor = result.decision === 'go'
|
|
35
|
+
? chalk.green
|
|
36
|
+
: result.decision === 'conditional-go'
|
|
37
|
+
? chalk.yellow
|
|
38
|
+
: chalk.red;
|
|
39
|
+
|
|
40
|
+
console.log(chalk.red('🔥') + ' Spec Gate');
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(`${chalk.gray('Spec:')} ${result.spec_id}`);
|
|
43
|
+
console.log(`${chalk.gray('Run:')} ${result.run_id}`);
|
|
44
|
+
console.log(`${chalk.gray('Decision:')} ${decisionColor(result.decision)}`);
|
|
45
|
+
console.log(`${chalk.gray('Score:')} ${result.score}`);
|
|
46
|
+
console.log();
|
|
47
|
+
|
|
48
|
+
console.log(chalk.bold('Rule Results'));
|
|
49
|
+
result.rules.forEach(rule => {
|
|
50
|
+
const icon = rule.passed ? chalk.green('✓') : chalk.red('✗');
|
|
51
|
+
console.log(` ${icon} ${rule.id} (${rule.score}/${rule.max_score})`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (result.failed_checks.length > 0) {
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.bold('Failed Checks'));
|
|
57
|
+
result.failed_checks.forEach(check => {
|
|
58
|
+
console.log(` - ${check.id} (hard_fail: ${check.hard_fail})`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.next_actions.length > 0) {
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(chalk.bold('Next Actions'));
|
|
65
|
+
result.next_actions.forEach(action => console.log(` - ${action}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_resolvePath(filePath) {
|
|
70
|
+
if (path.isAbsolute(filePath)) {
|
|
71
|
+
return filePath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return path.join(this.projectPath, filePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
ResultEmitter
|
|
80
|
+
};
|
|
81
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const docsCommand = require('../../commands/docs');
|
|
4
|
+
|
|
5
|
+
function createDefaultRules(projectPath = process.cwd()) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
id: 'mandatory',
|
|
9
|
+
description: 'Verify mandatory Spec files exist',
|
|
10
|
+
async execute(context) {
|
|
11
|
+
const specPath = path.join(projectPath, '.kiro', 'specs', context.specId);
|
|
12
|
+
const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
|
|
13
|
+
const checks = await Promise.all(requiredFiles.map(async fileName => {
|
|
14
|
+
const filePath = path.join(specPath, fileName);
|
|
15
|
+
const exists = await fs.pathExists(filePath);
|
|
16
|
+
return { fileName, exists };
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const missing = checks.filter(item => !item.exists).map(item => item.fileName);
|
|
20
|
+
const passRatio = checks.filter(item => item.exists).length / requiredFiles.length;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
passed: missing.length === 0,
|
|
24
|
+
ratio: passRatio,
|
|
25
|
+
details: {
|
|
26
|
+
requiredFiles,
|
|
27
|
+
missing
|
|
28
|
+
},
|
|
29
|
+
warnings: missing.length > 0 ? [`Missing files: ${missing.join(', ')}`] : []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'tests',
|
|
35
|
+
description: 'Verify tasks include explicit validation intent',
|
|
36
|
+
async execute(context) {
|
|
37
|
+
const tasksPath = path.join(projectPath, '.kiro', 'specs', context.specId, 'tasks.md');
|
|
38
|
+
if (!await fs.pathExists(tasksPath)) {
|
|
39
|
+
return {
|
|
40
|
+
passed: false,
|
|
41
|
+
ratio: 0,
|
|
42
|
+
details: { reason: 'tasks.md missing' },
|
|
43
|
+
warnings: ['tasks.md missing']
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = await fs.readFile(tasksPath, 'utf8');
|
|
48
|
+
const hasValidationHints = /验证|Validation|Acceptance/i.test(content);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
passed: hasValidationHints,
|
|
52
|
+
ratio: hasValidationHints ? 1 : 0,
|
|
53
|
+
details: {
|
|
54
|
+
hasValidationHints
|
|
55
|
+
},
|
|
56
|
+
warnings: hasValidationHints ? [] : ['No validation markers found in tasks.md']
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'docs',
|
|
62
|
+
description: 'Validate document structure compatibility',
|
|
63
|
+
async execute(context) {
|
|
64
|
+
const exitCode = await docsCommand('validate', {
|
|
65
|
+
spec: context.specId
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
passed: exitCode === 0,
|
|
70
|
+
ratio: exitCode === 0 ? 1 : 0,
|
|
71
|
+
details: {
|
|
72
|
+
exitCode
|
|
73
|
+
},
|
|
74
|
+
warnings: exitCode === 0 ? [] : [`docs validate returned exit code ${exitCode}`]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'config_consistency',
|
|
80
|
+
description: 'Verify project-level KSE config baseline exists',
|
|
81
|
+
async execute() {
|
|
82
|
+
const kiroDir = path.join(projectPath, '.kiro');
|
|
83
|
+
const configDir = path.join(kiroDir, 'config');
|
|
84
|
+
const hasKiro = await fs.pathExists(kiroDir);
|
|
85
|
+
const hasConfig = await fs.pathExists(configDir);
|
|
86
|
+
|
|
87
|
+
const ratio = hasKiro && hasConfig ? 1 : hasKiro ? 0.5 : 0;
|
|
88
|
+
const warnings = [];
|
|
89
|
+
if (!hasKiro) {
|
|
90
|
+
warnings.push('.kiro directory missing');
|
|
91
|
+
}
|
|
92
|
+
if (hasKiro && !hasConfig) {
|
|
93
|
+
warnings.push('.kiro/config directory missing');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
passed: hasKiro,
|
|
98
|
+
ratio,
|
|
99
|
+
details: {
|
|
100
|
+
hasKiro,
|
|
101
|
+
hasConfig
|
|
102
|
+
},
|
|
103
|
+
warnings
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'traceability',
|
|
109
|
+
description: 'Verify requirement-design-task traceability hints',
|
|
110
|
+
async execute(context) {
|
|
111
|
+
const specPath = path.join(projectPath, '.kiro', 'specs', context.specId);
|
|
112
|
+
const designPath = path.join(specPath, 'design.md');
|
|
113
|
+
const tasksPath = path.join(specPath, 'tasks.md');
|
|
114
|
+
|
|
115
|
+
const hasDesign = await fs.pathExists(designPath);
|
|
116
|
+
const hasTasks = await fs.pathExists(tasksPath);
|
|
117
|
+
|
|
118
|
+
if (!hasDesign || !hasTasks) {
|
|
119
|
+
return {
|
|
120
|
+
passed: false,
|
|
121
|
+
ratio: 0,
|
|
122
|
+
details: {
|
|
123
|
+
hasDesign,
|
|
124
|
+
hasTasks
|
|
125
|
+
},
|
|
126
|
+
warnings: ['design.md or tasks.md missing']
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const [designContent, tasksContent] = await Promise.all([
|
|
131
|
+
fs.readFile(designPath, 'utf8'),
|
|
132
|
+
fs.readFile(tasksPath, 'utf8')
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const hasMapping = /Mapping|映射|Requirement/i.test(designContent);
|
|
136
|
+
const hasTaskReferences = /Requirement|Design|需求|设计/i.test(tasksContent);
|
|
137
|
+
const scoreParts = [hasMapping, hasTaskReferences].filter(Boolean).length;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
passed: hasMapping && hasTaskReferences,
|
|
141
|
+
ratio: scoreParts / 2,
|
|
142
|
+
details: {
|
|
143
|
+
hasMapping,
|
|
144
|
+
hasTaskReferences
|
|
145
|
+
},
|
|
146
|
+
warnings: hasMapping && hasTaskReferences ? [] : ['Traceability links are incomplete']
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
createDefaultRules
|
|
155
|
+
};
|
|
156
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class RuleRegistry {
|
|
2
|
+
constructor(initialRules = []) {
|
|
3
|
+
this.rules = new Map();
|
|
4
|
+
initialRules.forEach(rule => this.register(rule));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
register(rule) {
|
|
8
|
+
if (!rule || !rule.id || typeof rule.execute !== 'function') {
|
|
9
|
+
throw new Error('Invalid gate rule: expected id and execute()');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
this.rules.set(rule.id, rule);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setEnabled(ruleId, enabled) {
|
|
16
|
+
const rule = this.rules.get(ruleId);
|
|
17
|
+
if (!rule) {
|
|
18
|
+
throw new Error(`Rule not found: ${ruleId}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
rule.enabled = !!enabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get(ruleId) {
|
|
25
|
+
return this.rules.get(ruleId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
list() {
|
|
29
|
+
return Array.from(this.rules.values());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listEnabled(policyRules = {}) {
|
|
33
|
+
return this.list().filter(rule => {
|
|
34
|
+
const policy = policyRules[rule.id];
|
|
35
|
+
if (!policy) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof rule.enabled === 'boolean') {
|
|
40
|
+
return rule.enabled;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return policy.enabled !== false;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
RuleRegistry
|
|
50
|
+
};
|
|
51
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiro-spec-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "kiro-spec-engine (kse) - A CLI tool and npm package for spec-driven development with AI coding assistants. NOT the Kiro IDE desktop application.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -84,3 +84,4 @@
|
|
|
84
84
|
"jest": "^27.5.1"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
|