kiro-spec-engine 1.44.0 → 1.45.2
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 +32 -0
- package/README.md +29 -2
- package/README.zh.md +29 -2
- package/bin/kiro-spec-engine.js +4 -0
- package/lib/commands/orchestrate.js +315 -0
- package/lib/orchestrator/agent-spawner.js +432 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +631 -0
- package/lib/orchestrator/orchestrator-config.js +157 -0
- package/lib/orchestrator/status-monitor.js +438 -0
- package/package.json +1 -1
- package/template/.kiro/README.md +14 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages `.kiro/config/orchestrator.json` for the Agent Orchestrator.
|
|
5
|
+
* When the config file does not exist or contains invalid JSON,
|
|
6
|
+
* returns a default configuration so that orchestration can proceed
|
|
7
|
+
* with sensible defaults.
|
|
8
|
+
*
|
|
9
|
+
* Requirements: 7.1 (read from orchestrator.json), 7.2 (defaults when missing),
|
|
10
|
+
* 7.3 (supported config fields), 7.4 (invalid JSON fallback),
|
|
11
|
+
* 7.5 (unknown fields ignored)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs-extra');
|
|
16
|
+
const fsUtils = require('../utils/fs-utils');
|
|
17
|
+
|
|
18
|
+
const CONFIG_FILENAME = 'orchestrator.json';
|
|
19
|
+
const CONFIG_DIR = '.kiro/config';
|
|
20
|
+
|
|
21
|
+
/** Known configuration keys — anything else is silently ignored. */
|
|
22
|
+
const KNOWN_KEYS = new Set([
|
|
23
|
+
'agentBackend',
|
|
24
|
+
'maxParallel',
|
|
25
|
+
'timeoutSeconds',
|
|
26
|
+
'maxRetries',
|
|
27
|
+
'apiKeyEnvVar',
|
|
28
|
+
'bootstrapTemplate',
|
|
29
|
+
'codexArgs',
|
|
30
|
+
'codexCommand',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** @type {import('./orchestrator-config').OrchestratorConfigData} */
|
|
34
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
35
|
+
agentBackend: 'codex',
|
|
36
|
+
maxParallel: 3,
|
|
37
|
+
timeoutSeconds: 600,
|
|
38
|
+
maxRetries: 2,
|
|
39
|
+
apiKeyEnvVar: 'CODEX_API_KEY',
|
|
40
|
+
bootstrapTemplate: null,
|
|
41
|
+
codexArgs: [],
|
|
42
|
+
codexCommand: null,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
class OrchestratorConfig {
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} workspaceRoot - Absolute path to the project root
|
|
48
|
+
*/
|
|
49
|
+
constructor(workspaceRoot) {
|
|
50
|
+
this._workspaceRoot = workspaceRoot;
|
|
51
|
+
this._configPath = path.join(workspaceRoot, CONFIG_DIR, CONFIG_FILENAME);
|
|
52
|
+
this._configDir = path.join(workspaceRoot, CONFIG_DIR);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read the current configuration.
|
|
57
|
+
* Returns the default config when the file is missing or contains invalid JSON.
|
|
58
|
+
* Unknown fields in the file are silently ignored (Requirement 7.5).
|
|
59
|
+
*
|
|
60
|
+
* @returns {Promise<object>} Resolved configuration
|
|
61
|
+
*/
|
|
62
|
+
async getConfig() {
|
|
63
|
+
const exists = await fsUtils.pathExists(this._configPath);
|
|
64
|
+
if (!exists) {
|
|
65
|
+
return { ...DEFAULT_CONFIG };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const data = await fsUtils.readJSON(this._configPath);
|
|
70
|
+
return this._mergeWithDefaults(data);
|
|
71
|
+
} catch (_err) {
|
|
72
|
+
// Invalid JSON — fall back to defaults (Requirement 7.4)
|
|
73
|
+
console.warn(
|
|
74
|
+
`[OrchestratorConfig] Failed to parse ${this._configPath}, using default config`
|
|
75
|
+
);
|
|
76
|
+
return { ...DEFAULT_CONFIG };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Persist a (partial) configuration update.
|
|
82
|
+
* Merges the provided values with the current config and writes atomically.
|
|
83
|
+
* Auto-initialises the config directory on first write.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} updates - Partial config values to merge
|
|
86
|
+
* @returns {Promise<object>} The full config after the update
|
|
87
|
+
*/
|
|
88
|
+
async updateConfig(updates) {
|
|
89
|
+
await fsUtils.ensureDirectory(this._configDir);
|
|
90
|
+
const current = await this.getConfig();
|
|
91
|
+
const filtered = this._filterKnownKeys(updates);
|
|
92
|
+
const merged = { ...current, ...filtered };
|
|
93
|
+
await fsUtils.writeJSON(this._configPath, merged);
|
|
94
|
+
return merged;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the bootstrap prompt template.
|
|
99
|
+
* If a custom template path is configured, reads and returns its content.
|
|
100
|
+
* Otherwise returns null (callers should use the built-in default template).
|
|
101
|
+
*
|
|
102
|
+
* @returns {Promise<string|null>} Template content or null
|
|
103
|
+
*/
|
|
104
|
+
async getBootstrapTemplate() {
|
|
105
|
+
const config = await this.getConfig();
|
|
106
|
+
if (!config.bootstrapTemplate) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const templatePath = path.resolve(this._workspaceRoot, config.bootstrapTemplate);
|
|
111
|
+
try {
|
|
112
|
+
return await fs.readFile(templatePath, 'utf8');
|
|
113
|
+
} catch (_err) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[OrchestratorConfig] Failed to read bootstrap template at ${templatePath}, using default`
|
|
116
|
+
);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Merge raw data with defaults, keeping only known keys.
|
|
123
|
+
* @param {object} data - Raw config data from file
|
|
124
|
+
* @returns {object} Merged config with only known keys
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
_mergeWithDefaults(data) {
|
|
128
|
+
const filtered = this._filterKnownKeys(data);
|
|
129
|
+
return { ...DEFAULT_CONFIG, ...filtered };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Filter an object to only include known configuration keys.
|
|
134
|
+
* @param {object} obj
|
|
135
|
+
* @returns {object}
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
_filterKnownKeys(obj) {
|
|
139
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
const result = {};
|
|
143
|
+
for (const key of Object.keys(obj)) {
|
|
144
|
+
if (KNOWN_KEYS.has(key)) {
|
|
145
|
+
result[key] = obj[key];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Absolute path to the config file (useful for tests / diagnostics). */
|
|
152
|
+
get configPath() {
|
|
153
|
+
return this._configPath;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { OrchestratorConfig, DEFAULT_CONFIG, KNOWN_KEYS };
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Monitor — Orchestration State Tracker
|
|
3
|
+
*
|
|
4
|
+
* Parses Codex JSON Lines events, tracks per-Spec execution status,
|
|
5
|
+
* computes aggregate orchestration state, and synchronises progress
|
|
6
|
+
* to SpecLifecycleManager and ContextSyncManager.
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 4.1 (maintain per-agent status), 4.2 (parse JSON Lines events),
|
|
9
|
+
* 4.3 (update SpecLifecycleManager on completion),
|
|
10
|
+
* 4.4 (update ContextSyncManager on completion),
|
|
11
|
+
* 4.5 (return summary report with statuses, progress, batch info)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const VALID_SPEC_STATUSES = new Set([
|
|
15
|
+
'pending', 'running', 'completed', 'failed', 'timeout', 'skipped',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const VALID_ORCHESTRATION_STATUSES = new Set([
|
|
19
|
+
'idle', 'running', 'completed', 'failed', 'stopped',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maps Codex JSON Lines event types to internal handling.
|
|
24
|
+
* item.* events are matched by prefix.
|
|
25
|
+
* @type {Set<string>}
|
|
26
|
+
*/
|
|
27
|
+
const KNOWN_EVENT_TYPES = new Set([
|
|
28
|
+
'thread.started',
|
|
29
|
+
'turn.started',
|
|
30
|
+
'turn.completed',
|
|
31
|
+
'error',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
class StatusMonitor {
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('../collab/spec-lifecycle-manager').SpecLifecycleManager} specLifecycleManager
|
|
37
|
+
* @param {import('../steering/context-sync-manager').ContextSyncManager} contextSyncManager
|
|
38
|
+
*/
|
|
39
|
+
constructor(specLifecycleManager, contextSyncManager) {
|
|
40
|
+
this._specLifecycleManager = specLifecycleManager;
|
|
41
|
+
this._contextSyncManager = contextSyncManager;
|
|
42
|
+
|
|
43
|
+
/** @type {'idle'|'running'|'completed'|'failed'|'stopped'} */
|
|
44
|
+
this._orchestrationStatus = 'idle';
|
|
45
|
+
/** @type {string|null} */
|
|
46
|
+
this._startedAt = null;
|
|
47
|
+
/** @type {string|null} */
|
|
48
|
+
this._completedAt = null;
|
|
49
|
+
/** @type {number} */
|
|
50
|
+
this._currentBatch = 0;
|
|
51
|
+
/** @type {number} */
|
|
52
|
+
this._totalBatches = 0;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Per-Spec execution status.
|
|
56
|
+
* @type {Map<string, {status: string, batch: number, agentId: string|null, retryCount: number, error: string|null, turnCount: number}>}
|
|
57
|
+
*/
|
|
58
|
+
this._specs = new Map();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public API
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a Spec for tracking before execution begins.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} specName
|
|
69
|
+
* @param {number} batch - Batch number this Spec belongs to
|
|
70
|
+
*/
|
|
71
|
+
initSpec(specName, batch) {
|
|
72
|
+
this._specs.set(specName, {
|
|
73
|
+
status: 'pending',
|
|
74
|
+
batch: batch || 0,
|
|
75
|
+
agentId: null,
|
|
76
|
+
retryCount: 0,
|
|
77
|
+
error: null,
|
|
78
|
+
turnCount: 0,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update the execution status of a specific Spec.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} specName
|
|
86
|
+
* @param {string} status - One of VALID_SPEC_STATUSES
|
|
87
|
+
* @param {string|null} [agentId=null]
|
|
88
|
+
* @param {string|null} [error=null]
|
|
89
|
+
*/
|
|
90
|
+
updateSpecStatus(specName, status, agentId = null, error = null) {
|
|
91
|
+
const entry = this._specs.get(specName);
|
|
92
|
+
if (!entry) {
|
|
93
|
+
// Unknown spec — initialise on the fly
|
|
94
|
+
this._specs.set(specName, {
|
|
95
|
+
status: VALID_SPEC_STATUSES.has(status) ? status : 'pending',
|
|
96
|
+
batch: 0,
|
|
97
|
+
agentId,
|
|
98
|
+
retryCount: 0,
|
|
99
|
+
error,
|
|
100
|
+
turnCount: 0,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (VALID_SPEC_STATUSES.has(status)) {
|
|
106
|
+
entry.status = status;
|
|
107
|
+
}
|
|
108
|
+
if (agentId !== null && agentId !== undefined) {
|
|
109
|
+
entry.agentId = agentId;
|
|
110
|
+
}
|
|
111
|
+
if (error !== null && error !== undefined) {
|
|
112
|
+
entry.error = error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Increment the retry count for a Spec.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} specName
|
|
120
|
+
*/
|
|
121
|
+
incrementRetry(specName) {
|
|
122
|
+
const entry = this._specs.get(specName);
|
|
123
|
+
if (entry) {
|
|
124
|
+
entry.retryCount++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set the overall orchestration state.
|
|
130
|
+
*
|
|
131
|
+
* @param {'idle'|'running'|'completed'|'failed'|'stopped'} state
|
|
132
|
+
*/
|
|
133
|
+
setOrchestrationState(state) {
|
|
134
|
+
if (!VALID_ORCHESTRATION_STATUSES.has(state)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this._orchestrationStatus = state;
|
|
138
|
+
|
|
139
|
+
if (state === 'running' && !this._startedAt) {
|
|
140
|
+
this._startedAt = new Date().toISOString();
|
|
141
|
+
}
|
|
142
|
+
if (state === 'completed' || state === 'failed' || state === 'stopped') {
|
|
143
|
+
this._completedAt = new Date().toISOString();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set batch progress information.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} current - Current batch index (1-based)
|
|
151
|
+
* @param {number} total - Total number of batches
|
|
152
|
+
*/
|
|
153
|
+
setBatchInfo(current, total) {
|
|
154
|
+
this._currentBatch = typeof current === 'number' ? current : 0;
|
|
155
|
+
this._totalBatches = typeof total === 'number' ? total : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle a Codex JSON Lines event for a specific agent.
|
|
160
|
+
* Gracefully handles invalid/malformed events — never throws.
|
|
161
|
+
*
|
|
162
|
+
* Supported event types:
|
|
163
|
+
* - thread.started: marks the agent's Spec as running
|
|
164
|
+
* - turn.started / turn.completed: tracks turn progress
|
|
165
|
+
* - item.*: generic item events (logged)
|
|
166
|
+
* - error: records error information
|
|
167
|
+
*
|
|
168
|
+
* @param {string} agentId
|
|
169
|
+
* @param {*} event - Parsed or raw event (string or object)
|
|
170
|
+
*/
|
|
171
|
+
handleEvent(agentId, event) {
|
|
172
|
+
try {
|
|
173
|
+
const parsed = this._parseEvent(event);
|
|
174
|
+
if (!parsed) return;
|
|
175
|
+
|
|
176
|
+
// Find the Spec associated with this agentId
|
|
177
|
+
const specName = this._findSpecByAgentId(agentId);
|
|
178
|
+
|
|
179
|
+
this._processEvent(specName, agentId, parsed);
|
|
180
|
+
} catch (_err) {
|
|
181
|
+
// Graceful handling — never throw (Req 4.2)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Return the full orchestration status report.
|
|
187
|
+
* Computes aggregate stats from the per-Spec map.
|
|
188
|
+
*
|
|
189
|
+
* @returns {object} OrchestrationStatus
|
|
190
|
+
*/
|
|
191
|
+
getOrchestrationStatus() {
|
|
192
|
+
const specs = Object.create(null);
|
|
193
|
+
let completedSpecs = 0;
|
|
194
|
+
let failedSpecs = 0;
|
|
195
|
+
let runningSpecs = 0;
|
|
196
|
+
|
|
197
|
+
for (const [specName, entry] of this._specs) {
|
|
198
|
+
specs[specName] = {
|
|
199
|
+
status: entry.status,
|
|
200
|
+
batch: entry.batch,
|
|
201
|
+
agentId: entry.agentId,
|
|
202
|
+
retryCount: entry.retryCount,
|
|
203
|
+
error: entry.error,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (entry.status === 'completed') completedSpecs++;
|
|
207
|
+
else if (entry.status === 'failed' || entry.status === 'timeout') failedSpecs++;
|
|
208
|
+
else if (entry.status === 'running') runningSpecs++;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
status: this._orchestrationStatus,
|
|
213
|
+
startedAt: this._startedAt,
|
|
214
|
+
completedAt: this._completedAt,
|
|
215
|
+
totalSpecs: this._specs.size,
|
|
216
|
+
completedSpecs,
|
|
217
|
+
failedSpecs,
|
|
218
|
+
runningSpecs,
|
|
219
|
+
currentBatch: this._currentBatch,
|
|
220
|
+
totalBatches: this._totalBatches,
|
|
221
|
+
specs,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Return the execution status of a specific Spec.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} specName
|
|
229
|
+
* @returns {object|null} SpecExecutionStatus or null if not tracked
|
|
230
|
+
*/
|
|
231
|
+
getSpecStatus(specName) {
|
|
232
|
+
const entry = this._specs.get(specName);
|
|
233
|
+
if (!entry) return null;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
status: entry.status,
|
|
237
|
+
batch: entry.batch,
|
|
238
|
+
agentId: entry.agentId,
|
|
239
|
+
retryCount: entry.retryCount,
|
|
240
|
+
error: entry.error,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Synchronise a Spec's completion status to external systems:
|
|
246
|
+
* - SpecLifecycleManager: transition Spec status
|
|
247
|
+
* - ContextSyncManager: update progress entry
|
|
248
|
+
*
|
|
249
|
+
* Failures are logged but do not propagate (non-fatal).
|
|
250
|
+
*
|
|
251
|
+
* @param {string} specName
|
|
252
|
+
* @param {string} status - 'completed' | 'failed' | 'timeout' etc.
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
*/
|
|
255
|
+
async syncExternalStatus(specName, status) {
|
|
256
|
+
// --- SpecLifecycleManager (Req 4.3) ---
|
|
257
|
+
if (this._specLifecycleManager) {
|
|
258
|
+
try {
|
|
259
|
+
const lifecycleStatus = this._mapToLifecycleStatus(status);
|
|
260
|
+
if (lifecycleStatus) {
|
|
261
|
+
await this._specLifecycleManager.transition(specName, lifecycleStatus);
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn(
|
|
265
|
+
`[StatusMonitor] Failed to update SpecLifecycleManager for ${specName}: ${err.message}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- ContextSyncManager (Req 4.4) ---
|
|
271
|
+
if (this._contextSyncManager) {
|
|
272
|
+
try {
|
|
273
|
+
const progress = status === 'completed' ? 100 : 0;
|
|
274
|
+
const summary = this._buildProgressSummary(specName, status);
|
|
275
|
+
await this._contextSyncManager.updateSpecProgress(specName, {
|
|
276
|
+
status,
|
|
277
|
+
progress,
|
|
278
|
+
summary,
|
|
279
|
+
});
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.warn(
|
|
282
|
+
`[StatusMonitor] Failed to update ContextSyncManager for ${specName}: ${err.message}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Private helpers
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Parse a raw event into a normalised object.
|
|
294
|
+
* Accepts both string (JSON) and pre-parsed objects.
|
|
295
|
+
* Returns null for invalid/unparseable input.
|
|
296
|
+
*
|
|
297
|
+
* @param {*} event
|
|
298
|
+
* @returns {object|null}
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
_parseEvent(event) {
|
|
302
|
+
if (!event) return null;
|
|
303
|
+
|
|
304
|
+
// Already an object
|
|
305
|
+
if (typeof event === 'object' && event !== null) {
|
|
306
|
+
return event.type ? event : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// String — attempt JSON parse
|
|
310
|
+
if (typeof event === 'string') {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(event);
|
|
313
|
+
return parsed && typeof parsed === 'object' && parsed.type ? parsed : null;
|
|
314
|
+
} catch (_err) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Find the Spec name associated with a given agentId.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} agentId
|
|
326
|
+
* @returns {string|null}
|
|
327
|
+
* @private
|
|
328
|
+
*/
|
|
329
|
+
_findSpecByAgentId(agentId) {
|
|
330
|
+
for (const [specName, entry] of this._specs) {
|
|
331
|
+
if (entry.agentId === agentId) {
|
|
332
|
+
return specName;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Process a parsed event and update internal state.
|
|
340
|
+
*
|
|
341
|
+
* @param {string|null} specName
|
|
342
|
+
* @param {string} agentId
|
|
343
|
+
* @param {object} event
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
_processEvent(specName, agentId, event) {
|
|
347
|
+
const type = event.type;
|
|
348
|
+
if (!type || typeof type !== 'string') return;
|
|
349
|
+
|
|
350
|
+
if (type === 'thread.started') {
|
|
351
|
+
if (specName) {
|
|
352
|
+
this._updateEntryStatus(specName, 'running');
|
|
353
|
+
}
|
|
354
|
+
} else if (type === 'turn.started') {
|
|
355
|
+
// Track turn activity
|
|
356
|
+
if (specName) {
|
|
357
|
+
const entry = this._specs.get(specName);
|
|
358
|
+
if (entry) {
|
|
359
|
+
entry.turnCount++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if (type === 'turn.completed') {
|
|
363
|
+
// Turn completed — no status change, just tracking
|
|
364
|
+
} else if (type === 'error') {
|
|
365
|
+
if (specName) {
|
|
366
|
+
const errorMsg = event.message || event.error || 'Unknown error';
|
|
367
|
+
const entry = this._specs.get(specName);
|
|
368
|
+
if (entry) {
|
|
369
|
+
entry.error = errorMsg;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} else if (type.startsWith('item.')) {
|
|
373
|
+
// item.* events — generic progress tracking, no status change
|
|
374
|
+
}
|
|
375
|
+
// Unknown event types are silently ignored
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Update a Spec entry's status if the Spec is tracked.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} specName
|
|
382
|
+
* @param {string} status
|
|
383
|
+
* @private
|
|
384
|
+
*/
|
|
385
|
+
_updateEntryStatus(specName, status) {
|
|
386
|
+
const entry = this._specs.get(specName);
|
|
387
|
+
if (entry && VALID_SPEC_STATUSES.has(status)) {
|
|
388
|
+
entry.status = status;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Map an orchestrator status to a SpecLifecycleManager status.
|
|
394
|
+
* Returns null if no mapping exists.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} status
|
|
397
|
+
* @returns {string|null}
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
_mapToLifecycleStatus(status) {
|
|
401
|
+
switch (status) {
|
|
402
|
+
case 'running':
|
|
403
|
+
return 'in-progress';
|
|
404
|
+
case 'completed':
|
|
405
|
+
return 'completed';
|
|
406
|
+
// failed/timeout/skipped have no direct lifecycle mapping
|
|
407
|
+
default:
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Build a human-readable progress summary for ContextSyncManager.
|
|
414
|
+
*
|
|
415
|
+
* @param {string} specName
|
|
416
|
+
* @param {string} status
|
|
417
|
+
* @returns {string}
|
|
418
|
+
* @private
|
|
419
|
+
*/
|
|
420
|
+
_buildProgressSummary(specName, status) {
|
|
421
|
+
switch (status) {
|
|
422
|
+
case 'completed':
|
|
423
|
+
return `Spec ${specName} completed successfully`;
|
|
424
|
+
case 'failed':
|
|
425
|
+
return `Spec ${specName} failed`;
|
|
426
|
+
case 'timeout':
|
|
427
|
+
return `Spec ${specName} timed out`;
|
|
428
|
+
case 'skipped':
|
|
429
|
+
return `Spec ${specName} skipped (dependency failed)`;
|
|
430
|
+
case 'running':
|
|
431
|
+
return `Spec ${specName} in progress`;
|
|
432
|
+
default:
|
|
433
|
+
return `Spec ${specName}: ${status}`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = { StatusMonitor, VALID_SPEC_STATUSES, VALID_ORCHESTRATION_STATUSES };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiro-spec-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.2",
|
|
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": {
|
package/template/.kiro/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This project uses **Spec-driven development** - a structured approach where:
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
## 🚀 kse Capabilities (v1.
|
|
22
|
+
## 🚀 kse Capabilities (v1.45.x)
|
|
23
23
|
|
|
24
24
|
**IMPORTANT**: After installing or updating kse, read this section to understand all available capabilities. Using the right tool for the job ensures efficient, high-quality development.
|
|
25
25
|
|
|
@@ -91,6 +91,19 @@ Fourth steering layer (L4) and Spec lifecycle coordination for multi-agent scena
|
|
|
91
91
|
- `kse auto status/resume/stop/config` — Manage autonomous execution
|
|
92
92
|
- Intelligent error recovery, checkpoint system, learning from history
|
|
93
93
|
|
|
94
|
+
### Agent Orchestrator — Multi-Agent Spec Execution (v1.45.0)
|
|
95
|
+
Automate parallel Spec execution via Codex CLI sub-agents (replaces manual multi-terminal workflow):
|
|
96
|
+
- `kse orchestrate run --specs "spec-a,spec-b,spec-c" --max-parallel 3` — Start multi-agent orchestration
|
|
97
|
+
- `kse orchestrate status` — View orchestration progress (per-Spec status, overall state)
|
|
98
|
+
- `kse orchestrate stop` — Gracefully stop all sub-agents
|
|
99
|
+
- **OrchestratorConfig** (`lib/orchestrator`) — Configuration management (agent backend, parallelism, timeout, retries) via `.kiro/config/orchestrator.json`
|
|
100
|
+
- **BootstrapPromptBuilder** (`lib/orchestrator`) — Builds bootstrap prompts with Spec path, steering context, execution instructions
|
|
101
|
+
- **AgentSpawner** (`lib/orchestrator`) — Process manager for Codex CLI sub-agents with timeout detection, graceful termination (SIGTERM → SIGKILL)
|
|
102
|
+
- **StatusMonitor** (`lib/orchestrator`) — Codex JSON Lines event parsing, per-Spec status tracking, orchestration-level aggregation
|
|
103
|
+
- **OrchestrationEngine** (`lib/orchestrator`) — DAG-based dependency analysis, batch scheduling, parallel execution (≤ maxParallel), failure propagation, retry mechanism
|
|
104
|
+
- Prerequisites: Codex CLI installed, `CODEX_API_KEY` environment variable set
|
|
105
|
+
- 11 correctness properties verified via property-based testing
|
|
106
|
+
|
|
94
107
|
### Scene Runtime (Template Engine + Quality + ERP)
|
|
95
108
|
- **Template Engine**: `kse scene template-validate/resolve/render` — Variable schema, multi-file rendering, 3-layer inheritance
|
|
96
109
|
- **Package Registry**: `kse scene publish/unpublish/install/list/search/info/diff/version` — Local package management
|