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.
Files changed (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +15 -4
  3. package/README.zh.md +15 -3
  4. package/bin/kiro-spec-engine.js +84 -0
  5. package/docs/adoption-guide.md +3 -2
  6. package/docs/command-reference.md +30 -12
  7. package/docs/cross-tool-guide.md +2 -1
  8. package/docs/document-governance.md +2 -1
  9. package/docs/faq.md +14 -13
  10. package/docs/manual-workflows-guide.md +2 -1
  11. package/docs/quick-start-with-ai-tools.md +4 -3
  12. package/docs/quick-start.md +21 -7
  13. package/docs/spec-workflow.md +3 -2
  14. package/docs/tools/claude-guide.md +3 -2
  15. package/docs/tools/cursor-guide.md +3 -2
  16. package/docs/tools/generic-guide.md +3 -2
  17. package/docs/tools/vscode-guide.md +3 -2
  18. package/docs/tools/windsurf-guide.md +3 -2
  19. package/docs/troubleshooting.md +10 -9
  20. package/docs/zh/quick-start.md +30 -14
  21. package/docs/zh/tools/claude-guide.md +2 -1
  22. package/docs/zh/tools/cursor-guide.md +2 -1
  23. package/docs/zh/tools/generic-guide.md +8 -7
  24. package/docs/zh/tools/vscode-guide.md +2 -1
  25. package/docs/zh/tools/windsurf-guide.md +2 -1
  26. package/lib/commands/orchestrate.js +123 -95
  27. package/lib/commands/spec-bootstrap.js +147 -0
  28. package/lib/commands/spec-gate.js +157 -0
  29. package/lib/commands/spec-pipeline.js +205 -0
  30. package/lib/spec/bootstrap/context-collector.js +48 -0
  31. package/lib/spec/bootstrap/draft-generator.js +158 -0
  32. package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
  33. package/lib/spec/bootstrap/trace-emitter.js +59 -0
  34. package/lib/spec/multi-spec-orchestrate.js +93 -0
  35. package/lib/spec/pipeline/constants.js +6 -0
  36. package/lib/spec/pipeline/stage-adapters.js +118 -0
  37. package/lib/spec/pipeline/stage-runner.js +146 -0
  38. package/lib/spec/pipeline/state-store.js +119 -0
  39. package/lib/spec-gate/engine/gate-engine.js +165 -0
  40. package/lib/spec-gate/policy/default-policy.js +22 -0
  41. package/lib/spec-gate/policy/policy-loader.js +103 -0
  42. package/lib/spec-gate/result-emitter.js +81 -0
  43. package/lib/spec-gate/rules/default-rules.js +156 -0
  44. package/lib/spec-gate/rules/rule-registry.js +51 -0
  45. package/package.json +2 -1
@@ -0,0 +1,93 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Parse --spec / --specs options into a de-duplicated list.
7
+ *
8
+ * @param {object} options
9
+ * @returns {string[]}
10
+ */
11
+ function parseSpecTargets(options = {}) {
12
+ const fromSpec = (options.spec || '').trim();
13
+ const fromSpecs = `${options.specs || ''}`
14
+ .split(',')
15
+ .map(item => item.trim())
16
+ .filter(Boolean);
17
+
18
+ const merged = [];
19
+ if (fromSpec) {
20
+ merged.push(fromSpec);
21
+ }
22
+ merged.push(...fromSpecs);
23
+
24
+ return Array.from(new Set(merged));
25
+ }
26
+
27
+ /**
28
+ * Execute multi-spec work through orchestrate mode.
29
+ *
30
+ * @param {object} options
31
+ * @param {string[]} options.specTargets
32
+ * @param {string} options.projectPath
33
+ * @param {object} options.commandOptions
34
+ * @param {Function} options.runOrchestration
35
+ * @param {string} options.commandLabel
36
+ * @param {string} options.nextActionLabel
37
+ * @returns {Promise<object>}
38
+ */
39
+ async function runMultiSpecViaOrchestrate(options = {}) {
40
+ const {
41
+ specTargets,
42
+ projectPath,
43
+ commandOptions,
44
+ runOrchestration,
45
+ commandLabel,
46
+ nextActionLabel
47
+ } = options;
48
+
49
+ const orchestrationResult = await runOrchestration({
50
+ specs: specTargets.join(','),
51
+ maxParallel: commandOptions.maxParallel,
52
+ json: false,
53
+ silent: true
54
+ }, {
55
+ workspaceRoot: projectPath
56
+ });
57
+
58
+ const result = {
59
+ mode: 'orchestrate',
60
+ spec_ids: specTargets,
61
+ status: orchestrationResult.status,
62
+ orchestrate_result: orchestrationResult,
63
+ next_actions: [
64
+ nextActionLabel,
65
+ 'Use kse orchestrate status to inspect live orchestration state.'
66
+ ]
67
+ };
68
+
69
+ if (commandOptions.out) {
70
+ const outPath = path.isAbsolute(commandOptions.out)
71
+ ? commandOptions.out
72
+ : path.join(projectPath, commandOptions.out);
73
+ await fs.ensureDir(path.dirname(outPath));
74
+ await fs.writeJson(outPath, result, { spaces: 2 });
75
+ result.output_file = outPath;
76
+ }
77
+
78
+ if (commandOptions.json) {
79
+ console.log(JSON.stringify(result, null, 2));
80
+ } else {
81
+ console.log(chalk.blue('🚀') + ` ${commandLabel} defaulted to orchestrate mode for ${specTargets.length} specs.`);
82
+ console.log(chalk.gray(` Specs: ${specTargets.join(', ')}`));
83
+ console.log(chalk.gray(` Status: ${orchestrationResult.status}`));
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ module.exports = {
90
+ parseSpecTargets,
91
+ runMultiSpecViaOrchestrate
92
+ };
93
+
@@ -0,0 +1,6 @@
1
+ const STAGE_ORDER = ['requirements', 'design', 'tasks', 'gate'];
2
+
3
+ module.exports = {
4
+ STAGE_ORDER
5
+ };
6
+
@@ -0,0 +1,118 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { runSpecBootstrap } = require('../../commands/spec-bootstrap');
4
+ const { runSpecGate } = require('../../commands/spec-gate');
5
+
6
+ function createDefaultStageAdapters(projectPath = process.cwd()) {
7
+ return {
8
+ requirements: async context => {
9
+ const specDir = path.join(projectPath, '.kiro', 'specs', context.specId);
10
+ const requirementsPath = path.join(specDir, 'requirements.md');
11
+
12
+ if (!await fs.pathExists(requirementsPath)) {
13
+ await runSpecBootstrap({
14
+ name: context.specId,
15
+ nonInteractive: true,
16
+ profile: 'pipeline',
17
+ dryRun: false,
18
+ json: false
19
+ }, {
20
+ projectPath
21
+ });
22
+
23
+ return {
24
+ success: true,
25
+ details: {
26
+ action: 'bootstrap-generated'
27
+ }
28
+ };
29
+ }
30
+
31
+ return {
32
+ success: true,
33
+ details: {
34
+ action: 'requirements-existing'
35
+ }
36
+ };
37
+ },
38
+
39
+ design: async context => {
40
+ const designPath = path.join(projectPath, '.kiro', 'specs', context.specId, 'design.md');
41
+ const exists = await fs.pathExists(designPath);
42
+
43
+ if (!exists) {
44
+ await runSpecBootstrap({
45
+ name: context.specId,
46
+ nonInteractive: true,
47
+ profile: 'pipeline',
48
+ dryRun: false,
49
+ json: false
50
+ }, {
51
+ projectPath
52
+ });
53
+ }
54
+
55
+ return {
56
+ success: true,
57
+ warnings: exists ? [] : ['design.md was created from bootstrap defaults'],
58
+ details: {
59
+ action: exists ? 'design-existing' : 'design-generated'
60
+ }
61
+ };
62
+ },
63
+
64
+ tasks: async context => {
65
+ const tasksPath = path.join(projectPath, '.kiro', 'specs', context.specId, 'tasks.md');
66
+ const exists = await fs.pathExists(tasksPath);
67
+
68
+ if (!exists) {
69
+ await runSpecBootstrap({
70
+ name: context.specId,
71
+ nonInteractive: true,
72
+ profile: 'pipeline',
73
+ dryRun: false,
74
+ json: false
75
+ }, {
76
+ projectPath
77
+ });
78
+ }
79
+
80
+ return {
81
+ success: true,
82
+ warnings: exists ? [] : ['tasks.md was created from bootstrap defaults'],
83
+ details: {
84
+ action: exists ? 'tasks-existing' : 'tasks-generated'
85
+ }
86
+ };
87
+ },
88
+
89
+ gate: async context => {
90
+ const gateResult = await runSpecGate({
91
+ spec: context.specId,
92
+ json: false,
93
+ strict: !!context.strict,
94
+ out: context.gateOut,
95
+ silent: true
96
+ }, {
97
+ projectPath
98
+ });
99
+
100
+ const success = gateResult.decision === 'go' || gateResult.decision === 'conditional-go';
101
+
102
+ return {
103
+ success,
104
+ warnings: gateResult.warnings ? gateResult.warnings.map(item => item.message) : [],
105
+ details: {
106
+ decision: gateResult.decision,
107
+ score: gateResult.score,
108
+ failed_checks: gateResult.failed_checks || []
109
+ }
110
+ };
111
+ }
112
+ };
113
+ }
114
+
115
+ module.exports = {
116
+ createDefaultStageAdapters
117
+ };
118
+
@@ -0,0 +1,146 @@
1
+ const { STAGE_ORDER } = require('./constants');
2
+
3
+ class StageRunner {
4
+ constructor(options = {}) {
5
+ this.stateStore = options.stateStore;
6
+ this.adapters = options.adapters || {};
7
+ }
8
+
9
+ async run(context) {
10
+ const selectedStages = this._selectStages({
11
+ fromStage: context.fromStage,
12
+ toStage: context.toStage
13
+ });
14
+
15
+ const stageResults = [];
16
+
17
+ for (const stageName of selectedStages) {
18
+ if (context.resume && this._isCompleted(context.state, stageName)) {
19
+ stageResults.push({
20
+ name: stageName,
21
+ status: 'skipped',
22
+ reason: 'already-completed'
23
+ });
24
+ continue;
25
+ }
26
+
27
+ const adapter = this.adapters[stageName];
28
+ if (typeof adapter !== 'function') {
29
+ const error = `No adapter registered for stage: ${stageName}`;
30
+ await this.stateStore.markStageResult(context.state, stageName, {
31
+ status: 'failed',
32
+ error
33
+ });
34
+ stageResults.push({ name: stageName, status: 'failed', error });
35
+ return {
36
+ status: 'failed',
37
+ stageResults,
38
+ failure: { stage: stageName, error }
39
+ };
40
+ }
41
+
42
+ await this.stateStore.markStageStart(context.state, stageName);
43
+
44
+ let adapterResult;
45
+ try {
46
+ adapterResult = context.dryRun
47
+ ? { success: true, details: { dry_run: true }, warnings: [] }
48
+ : await adapter(context);
49
+ } catch (error) {
50
+ adapterResult = {
51
+ success: false,
52
+ error: error.message,
53
+ warnings: []
54
+ };
55
+ }
56
+
57
+ const normalized = this._normalizeResult(adapterResult);
58
+ const status = normalized.success ? (normalized.warnings.length > 0 ? 'warning' : 'completed') : 'failed';
59
+
60
+ await this.stateStore.markStageResult(context.state, stageName, {
61
+ status,
62
+ warnings: normalized.warnings,
63
+ error: normalized.error,
64
+ output: normalized.details
65
+ });
66
+
67
+ stageResults.push({
68
+ name: stageName,
69
+ status,
70
+ warnings: normalized.warnings,
71
+ error: normalized.error,
72
+ output: normalized.details
73
+ });
74
+
75
+ if (status === 'failed' && context.failFast) {
76
+ return {
77
+ status: 'failed',
78
+ stageResults,
79
+ failure: {
80
+ stage: stageName,
81
+ error: normalized.error || 'stage failed'
82
+ }
83
+ };
84
+ }
85
+
86
+ if (status === 'warning' && !context.continueOnWarning) {
87
+ return {
88
+ status: 'failed',
89
+ stageResults,
90
+ failure: {
91
+ stage: stageName,
92
+ error: 'warning encountered with continue-on-warning disabled'
93
+ }
94
+ };
95
+ }
96
+ }
97
+
98
+ return {
99
+ status: 'completed',
100
+ stageResults,
101
+ failure: null
102
+ };
103
+ }
104
+
105
+ _selectStages(range = {}) {
106
+ const fromStage = range.fromStage || STAGE_ORDER[0];
107
+ const toStage = range.toStage || STAGE_ORDER[STAGE_ORDER.length - 1];
108
+
109
+ const fromIndex = STAGE_ORDER.indexOf(fromStage);
110
+ const toIndex = STAGE_ORDER.indexOf(toStage);
111
+
112
+ if (fromIndex < 0 || toIndex < 0 || fromIndex > toIndex) {
113
+ throw new Error(`Invalid stage range: from=${fromStage}, to=${toStage}`);
114
+ }
115
+
116
+ return STAGE_ORDER.slice(fromIndex, toIndex + 1);
117
+ }
118
+
119
+ _normalizeResult(result) {
120
+ if (!result || typeof result !== 'object') {
121
+ return {
122
+ success: false,
123
+ warnings: [],
124
+ error: 'Invalid stage result',
125
+ details: null
126
+ };
127
+ }
128
+
129
+ return {
130
+ success: !!result.success,
131
+ warnings: Array.isArray(result.warnings) ? result.warnings : [],
132
+ error: result.error || null,
133
+ details: result.details || null
134
+ };
135
+ }
136
+
137
+ _isCompleted(state, stageName) {
138
+ const stage = (state.stages || []).find(item => item.name === stageName);
139
+ return !!stage && stage.status === 'completed';
140
+ }
141
+ }
142
+
143
+ module.exports = {
144
+ StageRunner
145
+ };
146
+
@@ -0,0 +1,119 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { randomUUID } = require('crypto');
4
+
5
+ class PipelineStateStore {
6
+ constructor(projectPath = process.cwd()) {
7
+ this.projectPath = projectPath;
8
+ }
9
+
10
+ getStateDir(specId) {
11
+ return path.join(this.projectPath, '.kiro', 'state', 'spec-pipeline', specId);
12
+ }
13
+
14
+ getRunPath(specId, runId) {
15
+ return path.join(this.getStateDir(specId), `${runId}.json`);
16
+ }
17
+
18
+ getLatestPath(specId) {
19
+ return path.join(this.getStateDir(specId), 'latest.json');
20
+ }
21
+
22
+ async create(specId, options = {}) {
23
+ const runId = options.runId || randomUUID();
24
+ const state = {
25
+ spec_id: specId,
26
+ run_id: runId,
27
+ status: 'running',
28
+ strategy: {
29
+ fail_fast: options.failFast !== false,
30
+ continue_on_warning: !!options.continueOnWarning
31
+ },
32
+ stages: [],
33
+ started_at: new Date().toISOString(),
34
+ ended_at: null
35
+ };
36
+
37
+ await this.save(state);
38
+ return state;
39
+ }
40
+
41
+ async save(state) {
42
+ const runPath = this.getRunPath(state.spec_id, state.run_id);
43
+ const latestPath = this.getLatestPath(state.spec_id);
44
+
45
+ await fs.ensureDir(path.dirname(runPath));
46
+ await fs.writeJson(runPath, state, { spaces: 2 });
47
+ await fs.writeJson(latestPath, {
48
+ spec_id: state.spec_id,
49
+ run_id: state.run_id,
50
+ updated_at: new Date().toISOString()
51
+ }, { spaces: 2 });
52
+
53
+ return runPath;
54
+ }
55
+
56
+ async loadLatest(specId) {
57
+ const latestPath = this.getLatestPath(specId);
58
+ if (!await fs.pathExists(latestPath)) {
59
+ return null;
60
+ }
61
+
62
+ const latest = await fs.readJson(latestPath);
63
+ const runPath = this.getRunPath(specId, latest.run_id);
64
+ if (!await fs.pathExists(runPath)) {
65
+ return null;
66
+ }
67
+
68
+ return fs.readJson(runPath);
69
+ }
70
+
71
+ async markStageStart(state, stageName) {
72
+ const stage = this._getOrCreateStage(state, stageName);
73
+ stage.status = 'running';
74
+ stage.started_at = new Date().toISOString();
75
+ stage.ended_at = null;
76
+ stage.error = null;
77
+ stage.warnings = [];
78
+
79
+ await this.save(state);
80
+ }
81
+
82
+ async markStageResult(state, stageName, result) {
83
+ const stage = this._getOrCreateStage(state, stageName);
84
+ stage.status = result.status;
85
+ stage.ended_at = new Date().toISOString();
86
+ stage.error = result.error || null;
87
+ stage.warnings = result.warnings || [];
88
+ stage.output = result.output || null;
89
+
90
+ await this.save(state);
91
+ }
92
+
93
+ async markFinished(state, status) {
94
+ state.status = status;
95
+ state.ended_at = new Date().toISOString();
96
+ await this.save(state);
97
+ }
98
+
99
+ _getOrCreateStage(state, stageName) {
100
+ let stage = state.stages.find(item => item.name === stageName);
101
+ if (!stage) {
102
+ stage = {
103
+ name: stageName,
104
+ status: 'pending',
105
+ started_at: null,
106
+ ended_at: null,
107
+ warnings: []
108
+ };
109
+ state.stages.push(stage);
110
+ }
111
+
112
+ return stage;
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ PipelineStateStore
118
+ };
119
+
@@ -0,0 +1,165 @@
1
+ const { randomUUID } = require('crypto');
2
+
3
+ class GateEngine {
4
+ constructor(options = {}) {
5
+ this.registry = options.registry;
6
+ this.policy = options.policy;
7
+ }
8
+
9
+ async evaluate(input = {}) {
10
+ const specId = input.specId;
11
+ const runId = input.runId || randomUUID();
12
+ const startedAt = new Date().toISOString();
13
+
14
+ const policyRules = (this.policy && this.policy.rules) || {};
15
+ const enabledRules = this.registry.listEnabled(policyRules);
16
+
17
+ const rules = [];
18
+ const failedChecks = [];
19
+ const warnings = [];
20
+
21
+ let weightedScore = 0;
22
+ let totalWeight = 0;
23
+ let hardFailTriggered = false;
24
+
25
+ for (const rule of enabledRules) {
26
+ const policyEntry = policyRules[rule.id] || {};
27
+ const weight = Number(policyEntry.weight || 0);
28
+ const hardFail = !!policyEntry.hard_fail;
29
+
30
+ totalWeight += weight;
31
+ const execution = await rule.execute({
32
+ specId,
33
+ runId,
34
+ policy: this.policy
35
+ });
36
+
37
+ const passed = !!execution.passed;
38
+ const ratio = this._normalizeRatio(execution.ratio);
39
+ const ruleScore = passed ? weight : Number((weight * ratio).toFixed(2));
40
+
41
+ if (!passed) {
42
+ failedChecks.push({
43
+ id: rule.id,
44
+ hard_fail: hardFail,
45
+ details: execution.details || {}
46
+ });
47
+ }
48
+
49
+ if (hardFail && !passed) {
50
+ hardFailTriggered = true;
51
+ }
52
+
53
+ if (Array.isArray(execution.warnings) && execution.warnings.length > 0) {
54
+ warnings.push(...execution.warnings.map(message => ({ id: rule.id, message })));
55
+ }
56
+
57
+ weightedScore += ruleScore;
58
+
59
+ rules.push({
60
+ id: rule.id,
61
+ passed,
62
+ score: ruleScore,
63
+ max_score: weight,
64
+ hard_fail: hardFail,
65
+ warnings: execution.warnings || [],
66
+ details: execution.details || {}
67
+ });
68
+ }
69
+
70
+ const score = totalWeight > 0
71
+ ? Math.round((weightedScore / totalWeight) * 100)
72
+ : 0;
73
+
74
+ const decision = this._decide({
75
+ score,
76
+ hardFailTriggered,
77
+ warnings
78
+ });
79
+
80
+ const nextActions = this._buildNextActions(decision, failedChecks, warnings);
81
+ const endedAt = new Date().toISOString();
82
+
83
+ return {
84
+ spec_id: specId,
85
+ run_id: runId,
86
+ decision,
87
+ score,
88
+ rules,
89
+ failed_checks: failedChecks,
90
+ warnings,
91
+ next_actions: nextActions,
92
+ policy_snapshot: {
93
+ thresholds: this.policy.thresholds,
94
+ strict_mode: this.policy.strict_mode
95
+ },
96
+ started_at: startedAt,
97
+ ended_at: endedAt
98
+ };
99
+ }
100
+
101
+ _normalizeRatio(value) {
102
+ if (typeof value !== 'number' || Number.isNaN(value)) {
103
+ return 0;
104
+ }
105
+
106
+ if (value < 0) {
107
+ return 0;
108
+ }
109
+
110
+ if (value > 1) {
111
+ return 1;
112
+ }
113
+
114
+ return value;
115
+ }
116
+
117
+ _decide(context) {
118
+ const thresholds = this.policy.thresholds || {};
119
+ const goThreshold = Number(thresholds.go || 90);
120
+ const conditionalThreshold = Number(thresholds.conditional_go || 70);
121
+ const warningAsFailure = !!(this.policy.strict_mode && this.policy.strict_mode.warning_as_failure);
122
+
123
+ if (context.hardFailTriggered) {
124
+ return 'no-go';
125
+ }
126
+
127
+ if (warningAsFailure && context.warnings.length > 0) {
128
+ return 'no-go';
129
+ }
130
+
131
+ if (context.score >= goThreshold) {
132
+ return 'go';
133
+ }
134
+
135
+ if (context.score >= conditionalThreshold) {
136
+ return 'conditional-go';
137
+ }
138
+
139
+ return 'no-go';
140
+ }
141
+
142
+ _buildNextActions(decision, failedChecks, warnings) {
143
+ if (decision === 'go') {
144
+ return ['Proceed to implementation and keep evidence attached to Spec artifacts.'];
145
+ }
146
+
147
+ const actions = [];
148
+
149
+ if (failedChecks.length > 0) {
150
+ actions.push(`Fix failed checks: ${failedChecks.map(item => item.id).join(', ')}`);
151
+ }
152
+
153
+ if (warnings.length > 0) {
154
+ actions.push('Review warnings and decide whether to re-run with relaxed strict mode.');
155
+ }
156
+
157
+ actions.push('Re-run gate after remediation and attach latest gate report.');
158
+ return actions;
159
+ }
160
+ }
161
+
162
+ module.exports = {
163
+ GateEngine
164
+ };
165
+
@@ -0,0 +1,22 @@
1
+ const DEFAULT_GATE_POLICY = {
2
+ version: 1,
3
+ thresholds: {
4
+ go: 90,
5
+ conditional_go: 70
6
+ },
7
+ strict_mode: {
8
+ warning_as_failure: true
9
+ },
10
+ rules: {
11
+ mandatory: { enabled: true, weight: 30, hard_fail: true },
12
+ tests: { enabled: true, weight: 25, hard_fail: true },
13
+ docs: { enabled: true, weight: 15, hard_fail: false },
14
+ config_consistency: { enabled: true, weight: 15, hard_fail: false },
15
+ traceability: { enabled: true, weight: 15, hard_fail: false }
16
+ }
17
+ };
18
+
19
+ module.exports = {
20
+ DEFAULT_GATE_POLICY
21
+ };
22
+