scene-capability-engine 3.3.3 → 3.3.5

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.
@@ -39,6 +39,25 @@ const RATE_LIMIT_ERROR_PATTERNS = [
39
39
  /requests per minute/i,
40
40
  /tokens per minute/i,
41
41
  ];
42
+ const DEFAULT_COORDINATION_POLICY_RELATIVE_PATH = path.join(
43
+ 'docs',
44
+ 'agent-runtime',
45
+ 'multi-agent-coordination-policy-baseline.json'
46
+ );
47
+ const DEFAULT_RESULT_SUMMARY_REQUIRED_FIELDS = [
48
+ 'spec_id',
49
+ 'changed_files',
50
+ 'tests_run',
51
+ 'tests_passed',
52
+ 'risk_level',
53
+ 'open_issues'
54
+ ];
55
+ const DEFAULT_COORDINATION_RULES = {
56
+ require_result_summary: false,
57
+ block_merge_on_failed_tests: true,
58
+ block_merge_on_unresolved_conflicts: true
59
+ };
60
+ const VALID_RESULT_RISK_LEVELS = new Set(['low', 'medium', 'high', 'unknown']);
42
61
 
43
62
  class OrchestrationEngine extends EventEmitter {
44
63
  /**
@@ -115,6 +134,14 @@ class OrchestrationEngine extends EventEmitter {
115
134
  this._random = typeof options.random === 'function' ? options.random : Math.random;
116
135
  /** @type {() => number} */
117
136
  this._now = typeof options.now === 'function' ? options.now : Date.now;
137
+ /** @type {{ require_result_summary: boolean, block_merge_on_failed_tests: boolean, block_merge_on_unresolved_conflicts: boolean }} */
138
+ this._coordinationRules = { ...DEFAULT_COORDINATION_RULES };
139
+ /** @type {string[]} */
140
+ this._resultSummaryRequiredFields = [...DEFAULT_RESULT_SUMMARY_REQUIRED_FIELDS];
141
+ /** @type {Map<string, object>} */
142
+ this._resultSummaries = new Map();
143
+ /** @type {string} */
144
+ this._coordinationPolicyPath = DEFAULT_COORDINATION_POLICY_RELATIVE_PATH;
118
145
  }
119
146
 
120
147
  // ---------------------------------------------------------------------------
@@ -197,6 +224,7 @@ class OrchestrationEngine extends EventEmitter {
197
224
  // Get config for maxParallel and maxRetries
198
225
  const config = await this._orchestratorConfig.getConfig();
199
226
  this._applyRetryPolicyConfig(config);
227
+ await this._applyCoordinationPolicyConfig(config);
200
228
  this._agentWaitTimeoutMs = this._resolveAgentWaitTimeoutMs(config);
201
229
  const maxParallel = options.maxParallel || config.maxParallel || 3;
202
230
  const maxRetries = config.maxRetries || 2;
@@ -486,6 +514,33 @@ class OrchestrationEngine extends EventEmitter {
486
514
  * @private
487
515
  */
488
516
  async _handleSpecCompleted(specName, agentId) {
517
+ const summaryValidation = this._resolveAndValidateResultSummary(specName, agentId);
518
+ if (!summaryValidation.valid) {
519
+ await this._handleSummaryContractViolation(
520
+ specName,
521
+ agentId,
522
+ summaryValidation.message,
523
+ summaryValidation
524
+ );
525
+ return;
526
+ }
527
+
528
+ const mergeDecision = this._evaluateMergeDecision(summaryValidation.summary);
529
+ if (!mergeDecision.allowed) {
530
+ await this._handleSummaryContractViolation(
531
+ specName,
532
+ agentId,
533
+ `merge blocked by result summary policy: ${mergeDecision.reasons.join('; ')}`,
534
+ {
535
+ valid: true,
536
+ summary: summaryValidation.summary,
537
+ issues: mergeDecision.reasons
538
+ }
539
+ );
540
+ return;
541
+ }
542
+
543
+ this._resultSummaries.set(specName, { ...summaryValidation.summary });
489
544
  this._completedSpecs.add(specName);
490
545
  this._statusMonitor.updateSpecStatus(specName, 'completed', agentId);
491
546
 
@@ -495,7 +550,12 @@ class OrchestrationEngine extends EventEmitter {
495
550
  // Sync external status (Req 8.5)
496
551
  await this._syncExternalSafe(specName, 'completed');
497
552
 
498
- this.emit('spec:complete', { specName, agentId });
553
+ this.emit('spec:complete', {
554
+ specName,
555
+ agentId,
556
+ result_summary: summaryValidation.summary,
557
+ merge_decision: 'accepted'
558
+ });
499
559
  }
500
560
 
501
561
  /**
@@ -673,6 +733,277 @@ class OrchestrationEngine extends EventEmitter {
673
733
  }
674
734
  }
675
735
 
736
+ /**
737
+ * Resolve coordination policy from baseline file and runtime config.
738
+ *
739
+ * @param {object} config
740
+ * @returns {Promise<void>}
741
+ * @private
742
+ */
743
+ async _applyCoordinationPolicyConfig(config) {
744
+ const baseline = await this._loadCoordinationPolicyBaseline(config);
745
+ const baselineRules = baseline && baseline.coordination_rules
746
+ ? baseline.coordination_rules
747
+ : {};
748
+ const runtimeRules = config && config.coordinationRules && typeof config.coordinationRules === 'object'
749
+ ? config.coordinationRules
750
+ : {};
751
+ const requireFields = baseline
752
+ && baseline.result_summary_contract
753
+ && Array.isArray(baseline.result_summary_contract.required_fields)
754
+ ? baseline.result_summary_contract.required_fields
755
+ : DEFAULT_RESULT_SUMMARY_REQUIRED_FIELDS;
756
+ const overrideFields = Array.isArray(config && config.resultSummaryRequiredFields)
757
+ ? config.resultSummaryRequiredFields
758
+ : null;
759
+
760
+ this._coordinationRules = {
761
+ require_result_summary: this._toBoolean(
762
+ runtimeRules.require_result_summary,
763
+ this._toBoolean(baselineRules.require_result_summary, DEFAULT_COORDINATION_RULES.require_result_summary)
764
+ ),
765
+ block_merge_on_failed_tests: this._toBoolean(
766
+ runtimeRules.block_merge_on_failed_tests,
767
+ this._toBoolean(
768
+ baselineRules.block_merge_on_failed_tests,
769
+ DEFAULT_COORDINATION_RULES.block_merge_on_failed_tests
770
+ )
771
+ ),
772
+ block_merge_on_unresolved_conflicts: this._toBoolean(
773
+ runtimeRules.block_merge_on_unresolved_conflicts,
774
+ this._toBoolean(
775
+ baselineRules.block_merge_on_unresolved_conflicts,
776
+ DEFAULT_COORDINATION_RULES.block_merge_on_unresolved_conflicts
777
+ )
778
+ )
779
+ };
780
+
781
+ const selectedFields = overrideFields || requireFields;
782
+ const normalizedFields = selectedFields
783
+ .map((field) => `${field || ''}`.trim())
784
+ .filter(Boolean);
785
+ this._resultSummaryRequiredFields = normalizedFields.length > 0
786
+ ? normalizedFields
787
+ : [...DEFAULT_RESULT_SUMMARY_REQUIRED_FIELDS];
788
+ }
789
+
790
+ /**
791
+ * @param {object} config
792
+ * @returns {Promise<object>}
793
+ * @private
794
+ */
795
+ async _loadCoordinationPolicyBaseline(config) {
796
+ const relativePath = (
797
+ config
798
+ && typeof config.coordinationPolicyFile === 'string'
799
+ && config.coordinationPolicyFile.trim()
800
+ )
801
+ ? config.coordinationPolicyFile.trim()
802
+ : this._coordinationPolicyPath;
803
+ const policyPath = path.resolve(this._workspaceRoot, relativePath);
804
+ const exists = await fsUtils.pathExists(policyPath);
805
+ if (!exists) {
806
+ return {};
807
+ }
808
+ try {
809
+ return await fsUtils.readJSON(policyPath);
810
+ } catch (err) {
811
+ console.warn(`[OrchestrationEngine] Failed to parse coordination policy: ${err.message}`);
812
+ return {};
813
+ }
814
+ }
815
+
816
+ /**
817
+ * @param {string} specName
818
+ * @param {string} agentId
819
+ * @returns {{ valid: boolean, summary: object|null, issues: string[], message: string }}
820
+ * @private
821
+ */
822
+ _resolveAndValidateResultSummary(specName, agentId) {
823
+ const requireSummary = this._coordinationRules
824
+ && this._coordinationRules.require_result_summary === true;
825
+ if (!requireSummary) {
826
+ return {
827
+ valid: true,
828
+ summary: {
829
+ spec_id: specName,
830
+ changed_files: [],
831
+ tests_run: 0,
832
+ tests_passed: 0,
833
+ risk_level: 'unknown',
834
+ open_issues: []
835
+ },
836
+ issues: [],
837
+ message: ''
838
+ };
839
+ }
840
+
841
+ const summary = this._readResultSummaryFromSpawner(agentId);
842
+ if (!summary) {
843
+ return {
844
+ valid: false,
845
+ summary: null,
846
+ issues: ['missing result summary payload'],
847
+ message: 'result summary contract missing'
848
+ };
849
+ }
850
+
851
+ const normalizedSummary = this._normalizeResultSummary(summary, specName);
852
+ const issues = this._validateResultSummary(normalizedSummary);
853
+ return {
854
+ valid: issues.length === 0,
855
+ summary: normalizedSummary,
856
+ issues,
857
+ message: issues.length === 0 ? '' : `result summary contract invalid: ${issues.join('; ')}`
858
+ };
859
+ }
860
+
861
+ /**
862
+ * @param {string} agentId
863
+ * @returns {object|null}
864
+ * @private
865
+ */
866
+ _readResultSummaryFromSpawner(agentId) {
867
+ if (!this._agentSpawner || typeof this._agentSpawner.getResultSummary !== 'function') {
868
+ return null;
869
+ }
870
+ try {
871
+ return this._agentSpawner.getResultSummary(agentId);
872
+ } catch (_err) {
873
+ return null;
874
+ }
875
+ }
876
+
877
+ /**
878
+ * @param {object} summary
879
+ * @param {string} fallbackSpecName
880
+ * @returns {object}
881
+ * @private
882
+ */
883
+ _normalizeResultSummary(summary, fallbackSpecName) {
884
+ const changedFiles = Array.isArray(summary.changed_files)
885
+ ? summary.changed_files.map((item) => `${item || ''}`.trim()).filter(Boolean)
886
+ : [];
887
+ const openIssues = Array.isArray(summary.open_issues)
888
+ ? summary.open_issues.map((item) => `${item || ''}`.trim()).filter(Boolean)
889
+ : [];
890
+ const testsRun = Number(summary.tests_run);
891
+ const testsPassed = Number(summary.tests_passed);
892
+ const normalizedRisk = `${summary.risk_level || 'unknown'}`.trim().toLowerCase();
893
+
894
+ return {
895
+ spec_id: `${summary.spec_id || fallbackSpecName || ''}`.trim(),
896
+ changed_files: changedFiles,
897
+ tests_run: Number.isFinite(testsRun) ? Math.max(0, Math.floor(testsRun)) : NaN,
898
+ tests_passed: Number.isFinite(testsPassed) ? Math.max(0, Math.floor(testsPassed)) : NaN,
899
+ risk_level: normalizedRisk || 'unknown',
900
+ open_issues: openIssues
901
+ };
902
+ }
903
+
904
+ /**
905
+ * @param {object} summary
906
+ * @returns {string[]}
907
+ * @private
908
+ */
909
+ _validateResultSummary(summary) {
910
+ const issues = [];
911
+ const requiredFields = Array.isArray(this._resultSummaryRequiredFields)
912
+ ? this._resultSummaryRequiredFields
913
+ : [];
914
+ for (const field of requiredFields) {
915
+ if (!Object.prototype.hasOwnProperty.call(summary, field)) {
916
+ issues.push(`missing field '${field}'`);
917
+ }
918
+ }
919
+
920
+ if (!summary.spec_id) {
921
+ issues.push('spec_id must be non-empty');
922
+ }
923
+ if (!Array.isArray(summary.changed_files)) {
924
+ issues.push('changed_files must be an array');
925
+ }
926
+ if (!Number.isFinite(summary.tests_run)) {
927
+ issues.push('tests_run must be a non-negative integer');
928
+ }
929
+ if (!Number.isFinite(summary.tests_passed)) {
930
+ issues.push('tests_passed must be a non-negative integer');
931
+ }
932
+ if (
933
+ Number.isFinite(summary.tests_run)
934
+ && Number.isFinite(summary.tests_passed)
935
+ && summary.tests_passed > summary.tests_run
936
+ ) {
937
+ issues.push('tests_passed cannot exceed tests_run');
938
+ }
939
+ if (!VALID_RESULT_RISK_LEVELS.has(summary.risk_level)) {
940
+ issues.push(`risk_level must be one of: ${[...VALID_RESULT_RISK_LEVELS].join(', ')}`);
941
+ }
942
+ if (!Array.isArray(summary.open_issues)) {
943
+ issues.push('open_issues must be an array');
944
+ }
945
+
946
+ return issues;
947
+ }
948
+
949
+ /**
950
+ * @param {object} summary
951
+ * @returns {{ allowed: boolean, reasons: string[] }}
952
+ * @private
953
+ */
954
+ _evaluateMergeDecision(summary) {
955
+ const reasons = [];
956
+ const coordinationRules = this._coordinationRules || DEFAULT_COORDINATION_RULES;
957
+ if (
958
+ coordinationRules.block_merge_on_failed_tests
959
+ && Number.isFinite(summary.tests_run)
960
+ && Number.isFinite(summary.tests_passed)
961
+ && summary.tests_run > summary.tests_passed
962
+ ) {
963
+ reasons.push(
964
+ `tests failed (${summary.tests_passed}/${summary.tests_run} passed)`
965
+ );
966
+ }
967
+
968
+ if (coordinationRules.block_merge_on_unresolved_conflicts) {
969
+ const hasConflictIssue = Array.isArray(summary.open_issues)
970
+ && summary.open_issues.some((issue) => /conflict|unresolved/i.test(`${issue}`));
971
+ if (hasConflictIssue) {
972
+ reasons.push('open issues contain unresolved conflict');
973
+ }
974
+ }
975
+
976
+ return {
977
+ allowed: reasons.length === 0,
978
+ reasons
979
+ };
980
+ }
981
+
982
+ /**
983
+ * @param {string} specName
984
+ * @param {string} agentId
985
+ * @param {string} errorMessage
986
+ * @param {object} validation
987
+ * @returns {Promise<void>}
988
+ * @private
989
+ */
990
+ async _handleSummaryContractViolation(specName, agentId, errorMessage, validation = {}) {
991
+ this._completedSpecs.delete(specName);
992
+ this._failedSpecs.add(specName);
993
+ this._statusMonitor.updateSpecStatus(specName, 'failed', agentId, errorMessage);
994
+ await this._syncExternalSafe(specName, 'failed');
995
+
996
+ this.emit('spec:failed', {
997
+ specName,
998
+ agentId,
999
+ error: errorMessage,
1000
+ summary_contract_violation: true,
1001
+ summary_validation: validation
1002
+ });
1003
+
1004
+ this._propagateFailure(specName);
1005
+ }
1006
+
676
1007
  // ---------------------------------------------------------------------------
677
1008
  // Validation & Helpers
678
1009
  // ---------------------------------------------------------------------------
@@ -1342,12 +1673,20 @@ class OrchestrationEngine extends EventEmitter {
1342
1673
  * @private
1343
1674
  */
1344
1675
  _buildResult(status, error = null) {
1676
+ const resultSummaries = {};
1677
+ if (this._resultSummaries && typeof this._resultSummaries.entries === 'function') {
1678
+ for (const [specName, summary] of this._resultSummaries.entries()) {
1679
+ resultSummaries[specName] = { ...summary };
1680
+ }
1681
+ }
1345
1682
  return {
1346
1683
  status,
1347
1684
  plan: this._executionPlan,
1348
1685
  completed: [...this._completedSpecs],
1349
1686
  failed: [...this._failedSpecs],
1350
1687
  skipped: [...this._skippedSpecs],
1688
+ result_summaries: resultSummaries,
1689
+ coordination_rules: { ...(this._coordinationRules || DEFAULT_COORDINATION_RULES) },
1351
1690
  error,
1352
1691
  };
1353
1692
  }
@@ -1363,6 +1702,11 @@ class OrchestrationEngine extends EventEmitter {
1363
1702
  this._failedSpecs.clear();
1364
1703
  this._skippedSpecs.clear();
1365
1704
  this._completedSpecs.clear();
1705
+ if (this._resultSummaries && typeof this._resultSummaries.clear === 'function') {
1706
+ this._resultSummaries.clear();
1707
+ } else {
1708
+ this._resultSummaries = new Map();
1709
+ }
1366
1710
  this._executionPlan = null;
1367
1711
  this._stopped = false;
1368
1712
  this._baseMaxParallel = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.3.3",
3
+ "version": "3.3.5",
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": {
@@ -43,6 +43,9 @@
43
43
  "gate:matrix-regression": "node scripts/matrix-regression-gate.js --json",
44
44
  "report:moqui-baseline": "node scripts/moqui-template-baseline-report.js --json",
45
45
  "report:moqui-summary": "node scripts/moqui-release-summary.js --json",
46
+ "report:symbol-evidence": "node scripts/symbol-evidence-locate.js --query \"approve order\" --json",
47
+ "report:failure-attribution": "node scripts/failure-attribution-repair.js --error \"Cannot find module\" --json",
48
+ "report:capability-mapping": "node scripts/capability-mapping-report.js --input '{\"changes\":[],\"templates\":[],\"ontology\":{}}' --json",
46
49
  "report:matrix-remediation-queue": "node scripts/moqui-matrix-remediation-queue.js --json",
47
50
  "run:matrix-remediation-phased": "node scripts/moqui-matrix-remediation-phased-runner.js",
48
51
  "run:matrix-remediation-from-baseline": "node scripts/moqui-matrix-remediation-phased-runner.js --baseline .kiro/reports/release-evidence/moqui-template-baseline.json",