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.
@@ -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.44.0",
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": {
@@ -19,7 +19,7 @@ This project uses **Spec-driven development** - a structured approach where:
19
19
 
20
20
  ---
21
21
 
22
- ## 🚀 kse Capabilities (v1.44.x)
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