scene-capability-engine 3.3.14 → 3.3.16

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.
@@ -24,6 +24,10 @@ const DEFAULT_RATE_LIMIT_PARALLEL_FLOOR = 1;
24
24
  const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 45000;
25
25
  const DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE = 8;
26
26
  const DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS = 60000;
27
+ const DEFAULT_RATE_LIMIT_SIGNAL_WINDOW_MS = 30000;
28
+ const DEFAULT_RATE_LIMIT_SIGNAL_THRESHOLD = 3;
29
+ const DEFAULT_RATE_LIMIT_SIGNAL_EXTRA_HOLD_MS = 3000;
30
+ const DEFAULT_RATE_LIMIT_DYNAMIC_BUDGET_FLOOR = 1;
27
31
  const DEFAULT_AGENT_WAIT_TIMEOUT_SECONDS = 600;
28
32
  const AGENT_WAIT_TIMEOUT_GRACE_MS = 30000;
29
33
  const RATE_LIMIT_BACKOFF_JITTER_RATIO = 0.5;
@@ -120,6 +124,14 @@ class OrchestrationEngine extends EventEmitter {
120
124
  this._rateLimitLaunchBudgetPerMinute = DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE;
121
125
  /** @type {number} rolling window size for launch-budget throttling */
122
126
  this._rateLimitLaunchBudgetWindowMs = DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS;
127
+ /** @type {number|null} dynamic launch budget per minute derived from recent rate-limit pressure */
128
+ this._dynamicLaunchBudgetPerMinute = null;
129
+ /** @type {number[]} timestamps (ms) of recent rate-limit signals for spike detection */
130
+ this._rateLimitSignalTimestamps = [];
131
+ /** @type {number} rolling window for rate-limit spike detection */
132
+ this._rateLimitSignalWindowMs = DEFAULT_RATE_LIMIT_SIGNAL_WINDOW_MS;
133
+ /** @type {number} number of rate-limit signals inside window that triggers escalation */
134
+ this._rateLimitSignalThreshold = DEFAULT_RATE_LIMIT_SIGNAL_THRESHOLD;
123
135
  /** @type {number[]} timestamps (ms) of recent spec launches for rolling budget accounting */
124
136
  this._rateLimitLaunchTimestamps = [];
125
137
  /** @type {number} last launch-budget hold telemetry emission timestamp (ms) */
@@ -1081,6 +1093,8 @@ class OrchestrationEngine extends EventEmitter {
1081
1093
  this._rateLimitCooldownUntil = 0;
1082
1094
  this._rateLimitLaunchHoldUntil = 0;
1083
1095
  this._rateLimitLaunchTimestamps = [];
1096
+ this._rateLimitSignalTimestamps = [];
1097
+ this._dynamicLaunchBudgetPerMinute = null;
1084
1098
  this._launchBudgetLastHoldSignalAt = 0;
1085
1099
  this._launchBudgetLastHoldMs = 0;
1086
1100
  this._updateStatusMonitorParallelTelemetry({
@@ -1147,11 +1161,14 @@ class OrchestrationEngine extends EventEmitter {
1147
1161
  _onRateLimitSignal(retryDelayMs = 0) {
1148
1162
  const now = this._getNow();
1149
1163
  const launchHoldMs = this._toNonNegativeInteger(retryDelayMs, 0);
1164
+ this._recordRateLimitSignal(now);
1150
1165
  if (launchHoldMs > 0) {
1151
1166
  const currentHoldUntil = this._toNonNegativeInteger(this._rateLimitLaunchHoldUntil, 0);
1152
1167
  this._rateLimitLaunchHoldUntil = Math.max(currentHoldUntil, now + launchHoldMs);
1153
1168
  }
1154
1169
 
1170
+ this._applyRateLimitEscalation(now);
1171
+
1155
1172
  if (!this._isAdaptiveParallelEnabled()) {
1156
1173
  return;
1157
1174
  }
@@ -1192,6 +1209,8 @@ class OrchestrationEngine extends EventEmitter {
1192
1209
  * @private
1193
1210
  */
1194
1211
  _maybeRecoverParallelLimit(maxParallel) {
1212
+ this._maybeRecoverLaunchBudget();
1213
+
1195
1214
  if (!this._isAdaptiveParallelEnabled()) {
1196
1215
  return;
1197
1216
  }
@@ -1261,11 +1280,16 @@ class OrchestrationEngine extends EventEmitter {
1261
1280
  * @private
1262
1281
  */
1263
1282
  _getLaunchBudgetConfig() {
1283
+ const configuredBudget = this._toNonNegativeInteger(
1284
+ this._rateLimitLaunchBudgetPerMinute,
1285
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
1286
+ );
1287
+ const dynamicBudget = this._dynamicLaunchBudgetPerMinute === null
1288
+ || this._dynamicLaunchBudgetPerMinute === undefined
1289
+ ? configuredBudget
1290
+ : this._toNonNegativeInteger(this._dynamicLaunchBudgetPerMinute, configuredBudget);
1264
1291
  return {
1265
- budgetPerMinute: this._toNonNegativeInteger(
1266
- this._rateLimitLaunchBudgetPerMinute,
1267
- DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
1268
- ),
1292
+ budgetPerMinute: Math.min(configuredBudget, dynamicBudget),
1269
1293
  windowMs: this._toPositiveInteger(
1270
1294
  this._rateLimitLaunchBudgetWindowMs,
1271
1295
  DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_WINDOW_MS
@@ -1487,6 +1511,168 @@ class OrchestrationEngine extends EventEmitter {
1487
1511
  return Math.max(1, Math.round(cappedBaseDelay * jitterFactor));
1488
1512
  }
1489
1513
 
1514
+ /**
1515
+ * @param {number} now
1516
+ * @private
1517
+ */
1518
+ _recordRateLimitSignal(now) {
1519
+ const signalAt = Number.isFinite(now) ? now : this._getNow();
1520
+ const windowMs = this._toPositiveInteger(
1521
+ this._rateLimitSignalWindowMs,
1522
+ DEFAULT_RATE_LIMIT_SIGNAL_WINDOW_MS
1523
+ );
1524
+ if (!Array.isArray(this._rateLimitSignalTimestamps)) {
1525
+ this._rateLimitSignalTimestamps = [];
1526
+ }
1527
+ this._rateLimitSignalTimestamps = this._rateLimitSignalTimestamps
1528
+ .filter((timestamp) => Number.isFinite(timestamp) && timestamp > (signalAt - windowMs));
1529
+ this._rateLimitSignalTimestamps.push(signalAt);
1530
+ }
1531
+
1532
+ /**
1533
+ * @returns {number}
1534
+ * @private
1535
+ */
1536
+ _getRecentRateLimitSignalCount() {
1537
+ const now = this._getNow();
1538
+ const windowMs = this._toPositiveInteger(
1539
+ this._rateLimitSignalWindowMs,
1540
+ DEFAULT_RATE_LIMIT_SIGNAL_WINDOW_MS
1541
+ );
1542
+ if (!Array.isArray(this._rateLimitSignalTimestamps)) {
1543
+ this._rateLimitSignalTimestamps = [];
1544
+ return 0;
1545
+ }
1546
+ this._rateLimitSignalTimestamps = this._rateLimitSignalTimestamps
1547
+ .filter((timestamp) => Number.isFinite(timestamp) && timestamp > (now - windowMs));
1548
+ return this._rateLimitSignalTimestamps.length;
1549
+ }
1550
+
1551
+ /**
1552
+ * @param {number} now
1553
+ * @private
1554
+ */
1555
+ _applyRateLimitEscalation(now) {
1556
+ const signalCount = this._getRecentRateLimitSignalCount();
1557
+ const threshold = this._toPositiveInteger(
1558
+ this._rateLimitSignalThreshold,
1559
+ DEFAULT_RATE_LIMIT_SIGNAL_THRESHOLD
1560
+ );
1561
+
1562
+ if (signalCount < threshold) {
1563
+ return;
1564
+ }
1565
+
1566
+ const maxHoldMs = this._toPositiveInteger(
1567
+ this._rateLimitBackoffMaxMs,
1568
+ DEFAULT_RATE_LIMIT_BACKOFF_MAX_MS
1569
+ );
1570
+ const escalationUnits = signalCount - threshold + 1;
1571
+ const extraHoldMs = Math.min(
1572
+ maxHoldMs,
1573
+ escalationUnits * DEFAULT_RATE_LIMIT_SIGNAL_EXTRA_HOLD_MS
1574
+ );
1575
+ if (extraHoldMs > 0) {
1576
+ const currentHoldUntil = this._toNonNegativeInteger(this._rateLimitLaunchHoldUntil, 0);
1577
+ this._rateLimitLaunchHoldUntil = Math.max(currentHoldUntil, now + extraHoldMs);
1578
+ }
1579
+
1580
+ const configuredBudget = this._toNonNegativeInteger(
1581
+ this._rateLimitLaunchBudgetPerMinute,
1582
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
1583
+ );
1584
+ if (configuredBudget <= 0) {
1585
+ return;
1586
+ }
1587
+
1588
+ const currentBudget = this._toPositiveInteger(
1589
+ this._dynamicLaunchBudgetPerMinute == null
1590
+ ? configuredBudget
1591
+ : this._dynamicLaunchBudgetPerMinute,
1592
+ configuredBudget
1593
+ );
1594
+ const budgetFloor = Math.max(
1595
+ 1,
1596
+ Math.min(configuredBudget, DEFAULT_RATE_LIMIT_DYNAMIC_BUDGET_FLOOR)
1597
+ );
1598
+ const nextBudget = Math.max(budgetFloor, Math.floor(currentBudget / 2));
1599
+ if (nextBudget >= currentBudget) {
1600
+ return;
1601
+ }
1602
+
1603
+ this._dynamicLaunchBudgetPerMinute = nextBudget;
1604
+ const launchBudgetConfig = this._getLaunchBudgetConfig();
1605
+ const holdMs = this._getLaunchBudgetHoldRemainingMs();
1606
+ this._updateStatusMonitorLaunchBudget({
1607
+ event: 'throttled',
1608
+ budgetPerMinute: launchBudgetConfig.budgetPerMinute,
1609
+ windowMs: launchBudgetConfig.windowMs,
1610
+ used: Array.isArray(this._rateLimitLaunchTimestamps) ? this._rateLimitLaunchTimestamps.length : 0,
1611
+ holdMs,
1612
+ });
1613
+ this.emit('launch:budget-throttled', {
1614
+ reason: 'rate-limit-spike',
1615
+ signalCount,
1616
+ budgetPerMinute: launchBudgetConfig.budgetPerMinute,
1617
+ windowMs: launchBudgetConfig.windowMs,
1618
+ holdMs,
1619
+ });
1620
+ }
1621
+
1622
+ /**
1623
+ * @private
1624
+ */
1625
+ _maybeRecoverLaunchBudget() {
1626
+ const configuredBudget = this._toNonNegativeInteger(
1627
+ this._rateLimitLaunchBudgetPerMinute,
1628
+ DEFAULT_RATE_LIMIT_LAUNCH_BUDGET_PER_MINUTE
1629
+ );
1630
+ if (configuredBudget <= 0) {
1631
+ this._dynamicLaunchBudgetPerMinute = null;
1632
+ return;
1633
+ }
1634
+
1635
+ const currentBudget = this._toPositiveInteger(
1636
+ this._dynamicLaunchBudgetPerMinute == null
1637
+ ? configuredBudget
1638
+ : this._dynamicLaunchBudgetPerMinute,
1639
+ configuredBudget
1640
+ );
1641
+ if (currentBudget >= configuredBudget) {
1642
+ this._dynamicLaunchBudgetPerMinute = null;
1643
+ return;
1644
+ }
1645
+
1646
+ if (this._getNow() < this._rateLimitCooldownUntil) {
1647
+ return;
1648
+ }
1649
+
1650
+ if (this._getRecentRateLimitSignalCount() > 0) {
1651
+ return;
1652
+ }
1653
+
1654
+ const nextBudget = Math.min(configuredBudget, currentBudget + 1);
1655
+ this._dynamicLaunchBudgetPerMinute = nextBudget >= configuredBudget
1656
+ ? null
1657
+ : nextBudget;
1658
+
1659
+ const launchBudgetConfig = this._getLaunchBudgetConfig();
1660
+ const holdMs = this._getLaunchBudgetHoldRemainingMs();
1661
+ this._updateStatusMonitorLaunchBudget({
1662
+ event: 'recovered',
1663
+ budgetPerMinute: launchBudgetConfig.budgetPerMinute,
1664
+ windowMs: launchBudgetConfig.windowMs,
1665
+ used: Array.isArray(this._rateLimitLaunchTimestamps) ? this._rateLimitLaunchTimestamps.length : 0,
1666
+ holdMs,
1667
+ });
1668
+ this.emit('launch:budget-recovered', {
1669
+ reason: 'rate-limit-cooldown',
1670
+ budgetPerMinute: launchBudgetConfig.budgetPerMinute,
1671
+ windowMs: launchBudgetConfig.windowMs,
1672
+ holdMs,
1673
+ });
1674
+ }
1675
+
1490
1676
  /**
1491
1677
  * Resolve final retry delay for rate-limit failures.
1492
1678
  * Uses larger of computed backoff and retry-after hint, then clamps to configured max.
@@ -1714,6 +1900,8 @@ class OrchestrationEngine extends EventEmitter {
1714
1900
  this._rateLimitCooldownUntil = 0;
1715
1901
  this._rateLimitLaunchHoldUntil = 0;
1716
1902
  this._rateLimitLaunchTimestamps = [];
1903
+ this._rateLimitSignalTimestamps = [];
1904
+ this._dynamicLaunchBudgetPerMinute = null;
1717
1905
  this._launchBudgetLastHoldSignalAt = 0;
1718
1906
  this._launchBudgetLastHoldMs = 0;
1719
1907
  }
@@ -155,7 +155,8 @@ class RegistryParser {
155
155
  // Required fields
156
156
  const requiredFields = [
157
157
  'id', 'name', 'category', 'description',
158
- 'difficulty', 'tags', 'files'
158
+ 'difficulty', 'tags', 'applicable_scenarios', 'files',
159
+ 'template_type', 'min_sce_version', 'risk_level', 'rollback_contract'
159
160
  ];
160
161
 
161
162
  for (const field of requiredFields) {
@@ -213,23 +214,31 @@ class RegistryParser {
213
214
  }
214
215
  }
215
216
 
216
- if (template.risk_level !== undefined && template.risk_level !== null) {
217
- if (!this.validRiskLevels.includes(template.risk_level)) {
218
- errors.push(`${prefix}: Invalid risk_level "${template.risk_level}". Must be one of: ${this.validRiskLevels.join(', ')}`);
217
+ if (!this.validRiskLevels.includes(template.risk_level)) {
218
+ errors.push(`${prefix}: Invalid risk_level "${template.risk_level}". Must be one of: ${this.validRiskLevels.join(', ')}`);
219
+ }
220
+
221
+ if (!this._isPlainObject(template.rollback_contract)) {
222
+ errors.push(`${prefix}: Field "rollback_contract" must be an object`);
223
+ } else {
224
+ if (typeof template.rollback_contract.supported !== 'boolean') {
225
+ errors.push(`${prefix}: Field "rollback_contract.supported" must be boolean`);
226
+ }
227
+ if (typeof template.rollback_contract.strategy !== 'string' ||
228
+ template.rollback_contract.strategy.trim().length === 0) {
229
+ errors.push(`${prefix}: Field "rollback_contract.strategy" must be a non-empty string`);
219
230
  }
220
231
  }
221
232
 
222
- if (template.rollback_contract !== undefined && template.rollback_contract !== null) {
223
- if (!this._isPlainObject(template.rollback_contract)) {
224
- errors.push(`${prefix}: Field "rollback_contract" must be an object`);
233
+ if (templateType === 'capability-template') {
234
+ const ontologyScope = template.ontology_scope;
235
+ if (!this._isPlainObject(ontologyScope)) {
236
+ errors.push(`${prefix}: capability-template requires "ontology_scope" object`);
225
237
  } else {
226
- if (typeof template.rollback_contract.supported !== 'boolean') {
227
- errors.push(`${prefix}: Field "rollback_contract.supported" must be boolean`);
228
- }
229
- if (template.rollback_contract.strategy !== undefined &&
230
- template.rollback_contract.strategy !== null &&
231
- typeof template.rollback_contract.strategy !== 'string') {
232
- errors.push(`${prefix}: Field "rollback_contract.strategy" must be a string when present`);
238
+ const hasAnyScope = ['domains', 'entities', 'relations', 'business_rules', 'decisions']
239
+ .some((fieldName) => Array.isArray(ontologyScope[fieldName]) && ontologyScope[fieldName].length > 0);
240
+ if (!hasAnyScope) {
241
+ errors.push(`${prefix}: capability-template ontology_scope must include at least one non-empty scope array`);
233
242
  }
234
243
  }
235
244
  }
@@ -42,20 +42,20 @@ class VersionChecker {
42
42
 
43
43
  // Get current sce version
44
44
  const packageJson = require('../../package.json');
45
- const kseVersion = packageJson.version;
45
+ const sceVersion = packageJson.version;
46
46
 
47
47
  // Check if upgrade is needed
48
- const needsUpgrade = this.versionManager.needsUpgrade(projectVersion, kseVersion);
48
+ const needsUpgrade = this.versionManager.needsUpgrade(projectVersion, sceVersion);
49
49
 
50
50
  if (needsUpgrade) {
51
- this.displayWarning(projectVersion, kseVersion);
51
+ this.displayWarning(projectVersion, sceVersion);
52
52
  }
53
53
 
54
54
  return {
55
55
  mismatch: needsUpgrade,
56
56
  shouldUpgrade: needsUpgrade,
57
57
  projectVersion,
58
- kseVersion
58
+ sceVersion
59
59
  };
60
60
  } catch (error) {
61
61
  // Silently fail - don't block commands if version check fails
@@ -67,13 +67,13 @@ class VersionChecker {
67
67
  * Displays version mismatch warning
68
68
  *
69
69
  * @param {string} projectVersion - Project version
70
- * @param {string} kseVersion - Current sce version
70
+ * @param {string} sceVersion - Current sce version
71
71
  */
72
- displayWarning(projectVersion, kseVersion) {
72
+ displayWarning(projectVersion, sceVersion) {
73
73
  console.log();
74
74
  console.log(chalk.yellow('⚠️ Version Mismatch Detected'));
75
75
  console.log(chalk.gray(' Project initialized with sce'), chalk.cyan(`v${projectVersion}`));
76
- console.log(chalk.gray(' Current sce version:'), chalk.cyan(`v${kseVersion}`));
76
+ console.log(chalk.gray(' Current sce version:'), chalk.cyan(`v${sceVersion}`));
77
77
  console.log();
78
78
  console.log(chalk.blue('💡 Tip:'), chalk.gray('Run'), chalk.cyan('sce upgrade'), chalk.gray('to update project templates'));
79
79
  console.log(chalk.gray(' Or use'), chalk.cyan('--no-version-check'), chalk.gray('to suppress this warning'));
@@ -98,7 +98,7 @@ class VersionChecker {
98
98
  }
99
99
 
100
100
  const packageJson = require('../../package.json');
101
- const kseVersion = packageJson.version;
101
+ const sceVersion = packageJson.version;
102
102
 
103
103
  console.log(chalk.blue('📦 Version Information'));
104
104
  console.log();
@@ -110,7 +110,7 @@ class VersionChecker {
110
110
 
111
111
  console.log();
112
112
  console.log(chalk.gray('Installed:'));
113
- console.log(` sce version: ${chalk.cyan(kseVersion)}`);
113
+ console.log(` sce version: ${chalk.cyan(sceVersion)}`);
114
114
 
115
115
  if (projectVersionInfo['upgrade-history'].length > 0) {
116
116
  console.log();
@@ -129,12 +129,12 @@ class VersionChecker {
129
129
 
130
130
  const needsUpgrade = this.versionManager.needsUpgrade(
131
131
  projectVersionInfo['sce-version'],
132
- kseVersion
132
+ sceVersion
133
133
  );
134
134
 
135
135
  if (needsUpgrade) {
136
136
  console.log(chalk.yellow('⚠️ Upgrade available'));
137
- console.log(chalk.gray(' Run'), chalk.cyan('sce upgrade'), chalk.gray('to update to'), chalk.cyan(`v${kseVersion}`));
137
+ console.log(chalk.gray(' Run'), chalk.cyan('sce upgrade'), chalk.gray('to update to'), chalk.cyan(`v${sceVersion}`));
138
138
  } else {
139
139
  console.log(chalk.green('✅ Project is up to date'));
140
140
  }
@@ -88,16 +88,16 @@ class VersionManager {
88
88
  /**
89
89
  * Creates initial version info for a new project
90
90
  *
91
- * @param {string} kseVersion - Current sce version
91
+ * @param {string} sceVersion - Current sce version
92
92
  * @param {string} templateVersion - Template version (default: same as sce)
93
93
  * @returns {VersionInfo}
94
94
  */
95
- createVersionInfo(kseVersion, templateVersion = null) {
95
+ createVersionInfo(sceVersion, templateVersion = null) {
96
96
  const now = new Date().toISOString();
97
97
 
98
98
  return {
99
- 'sce-version': kseVersion,
100
- 'template-version': templateVersion || kseVersion,
99
+ 'sce-version': sceVersion,
100
+ 'template-version': templateVersion || sceVersion,
101
101
  'created': now,
102
102
  'last-upgraded': now,
103
103
  'upgrade-history': []
@@ -141,20 +141,20 @@ class VersionManager {
141
141
  * Checks if upgrade is needed
142
142
  *
143
143
  * @param {string} projectVersion - Current project version
144
- * @param {string} kseVersion - Installed sce version
144
+ * @param {string} sceVersion - Installed sce version
145
145
  * @returns {boolean}
146
146
  */
147
- needsUpgrade(projectVersion, kseVersion) {
148
- if (!projectVersion || !kseVersion) {
147
+ needsUpgrade(projectVersion, sceVersion) {
148
+ if (!projectVersion || !sceVersion) {
149
149
  return false;
150
150
  }
151
151
 
152
152
  try {
153
153
  // Use semver for comparison
154
- return semver.lt(projectVersion, kseVersion);
154
+ return semver.lt(projectVersion, sceVersion);
155
155
  } catch (error) {
156
156
  // If semver comparison fails, do string comparison
157
- return projectVersion !== kseVersion;
157
+ return projectVersion !== sceVersion;
158
158
  }
159
159
  }
160
160
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.14",
3
+ "version": "3.3.16",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,9 @@
1
+ {
2
+ "enabled": false,
3
+ "require_auth_for": [
4
+ "apply",
5
+ "release",
6
+ "rollback"
7
+ ],
8
+ "password_env": "SCE_STUDIO_AUTH_PASSWORD"
9
+ }
@@ -11,6 +11,6 @@
11
11
  },
12
12
  "then": {
13
13
  "type": "askAgent",
14
- "prompt": "The tasks.md file has been updated. Please review the changes and update the workspace status if needed. Run `kse workspace sync` to synchronize."
14
+ "prompt": "The tasks.md file has been updated. Please review the changes and update workspace status if needed. Run `sce workspace sync` to synchronize."
15
15
  }
16
16
  }