sinapse-ai 1.6.1 → 1.8.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 (131) hide show
  1. package/.claude/CLAUDE.md +5 -11
  2. package/.claude/hooks/README.md +14 -1
  3. package/.claude/hooks/code-intel-pretool.cjs +115 -0
  4. package/.claude/hooks/enforce-delegation.cjs +31 -3
  5. package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
  6. package/.claude/hooks/enforce-permission-mode.cjs +249 -0
  7. package/.claude/hooks/secret-scanning.cjs +34 -43
  8. package/.claude/hooks/synapse-engine.cjs +23 -23
  9. package/.claude/hooks/telemetry-post-tool.cjs +128 -0
  10. package/.claude/hooks/telemetry-stop.cjs +132 -0
  11. package/.claude/hooks/verify-packages.cjs +9 -2
  12. package/.claude/rules/documentation-first.md +1 -1
  13. package/.claude/rules/hook-governance.md +2 -0
  14. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  15. package/.sinapse-ai/core/README.md +11 -0
  16. package/.sinapse-ai/core/config/config-loader.js +19 -0
  17. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  18. package/.sinapse-ai/core/errors/constants.js +147 -0
  19. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  20. package/.sinapse-ai/core/errors/index.js +50 -0
  21. package/.sinapse-ai/core/errors/serializer.js +147 -0
  22. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  23. package/.sinapse-ai/core/errors/utils.js +187 -0
  24. package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
  25. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  26. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  27. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  28. package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
  29. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  30. package/.sinapse-ai/core/grounding/README.md +71 -11
  31. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  32. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  33. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  34. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  35. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  36. package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
  37. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  38. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  39. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  40. package/.sinapse-ai/core/ids/index.js +30 -0
  41. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  42. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  43. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  44. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  45. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  46. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  47. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  48. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  49. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  50. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  51. package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
  52. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  53. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  54. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  55. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  56. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  57. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  58. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  59. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  60. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  61. package/.sinapse-ai/core/synapse/engine.js +43 -3
  62. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  63. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  64. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  65. package/.sinapse-ai/core-config.yaml +68 -1
  66. package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
  67. package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
  68. package/.sinapse-ai/development/agents/developer.md +2 -0
  69. package/.sinapse-ai/development/agents/devops.md +9 -0
  70. package/.sinapse-ai/development/external-executors/README.md +18 -0
  71. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  72. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  73. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
  74. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  75. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  76. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  77. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  78. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
  79. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
  80. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  81. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  82. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  83. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  84. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  85. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  86. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  87. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  88. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  89. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  90. package/.sinapse-ai/install-manifest.yaml +218 -114
  91. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  92. package/.sinapse-ai/scripts/pm.sh +18 -6
  93. package/bin/cli.js +17 -0
  94. package/bin/commands/agents.js +96 -0
  95. package/bin/commands/doctor.js +15 -0
  96. package/bin/commands/ideate.js +129 -0
  97. package/bin/commands/uninstall.js +40 -0
  98. package/bin/postinstall.js +50 -4
  99. package/bin/sinapse.js +146 -2
  100. package/bin/utils/secret-scanner-core.js +253 -0
  101. package/bin/utils/staged-secret-scan.js +106 -40
  102. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  103. package/docs/guides/parallel-workflow.md +6 -6
  104. package/package.json +22 -5
  105. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  106. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  107. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  108. package/packages/installer/src/wizard/validators.js +38 -1
  109. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  110. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  111. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  112. package/scripts/eval-runner.js +422 -0
  113. package/scripts/generate-install-manifest.js +13 -9
  114. package/scripts/generate-synapse-runtime.js +51 -0
  115. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  116. package/scripts/validate-all.js +1 -0
  117. package/scripts/validate-evals.js +466 -0
  118. package/scripts/validate-schemas.js +539 -0
  119. package/scripts/validate-squad-orqx.js +9 -2
  120. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  121. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  122. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  123. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  124. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  125. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  126. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
  127. package/docs/chrome-brain-upgrade-plan.md +0 -624
  128. package/docs/constitution-compliance.md +0 -87
  129. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  130. package/docs/research-synthesis-for-upgrade.md +0 -511
  131. package/docs/security-audit-report.md +0 -306
@@ -85,6 +85,18 @@ const { G2StoryCreationGate } = require('./gates/g2-story-creation');
85
85
  const { G3StoryValidationGate } = require('./gates/g3-story-validation');
86
86
  const { G4DevContextGate, G4_DEFAULT_TIMEOUT_MS } = require('./gates/g4-dev-context');
87
87
 
88
+ // IDS-5b: Gate G5 (Blocking — Semantic Handshake)
89
+ const {
90
+ G5SemanticHandshakeGate,
91
+ G5_DEFAULT_TIMEOUT_MS,
92
+ } = require('./gates/g5-semantic-handshake');
93
+
94
+ // IDS Gate Wiring: Gate G6 (Blocking — CI/CD Registry Integrity)
95
+ const {
96
+ G6CiIntegrityGate,
97
+ G6_DEFAULT_TIMEOUT_MS,
98
+ } = require('./gates/g6-ci-integrity');
99
+
88
100
  // IDS-7: Framework Governor (sinapse-orqx integration)
89
101
  const {
90
102
  FrameworkGovernor,
@@ -92,6 +104,12 @@ const {
92
104
  RISK_THRESHOLDS,
93
105
  } = require('./framework-governor');
94
106
 
107
+ // IDS Gate Wiring: Gate Evaluator (wires G1-G5 into the SDC workflow)
108
+ const {
109
+ GateEvaluator,
110
+ DEFAULT_PHASE_GATES,
111
+ } = require('./gate-evaluator');
112
+
95
113
  module.exports = {
96
114
  // IDS-1: Registry Foundation
97
115
  RegistryLoader,
@@ -149,9 +167,21 @@ module.exports = {
149
167
  G4DevContextGate,
150
168
  G4_DEFAULT_TIMEOUT_MS,
151
169
 
170
+ // IDS-5b: Gate G5 (Blocking)
171
+ G5SemanticHandshakeGate,
172
+ G5_DEFAULT_TIMEOUT_MS,
173
+
174
+ // IDS Gate Wiring: Gate G6 (Blocking — CI/CD Registry Integrity)
175
+ G6CiIntegrityGate,
176
+ G6_DEFAULT_TIMEOUT_MS,
177
+
152
178
  // IDS-7: Framework Governor
153
179
  FrameworkGovernor,
154
180
  TIMEOUT_MS,
155
181
  RISK_THRESHOLDS,
182
+
183
+ // IDS Gate Wiring: Gate Evaluator
184
+ GateEvaluator,
185
+ DEFAULT_PHASE_GATES,
156
186
  };
157
187
 
@@ -7,6 +7,17 @@
7
7
  *
8
8
  * @created 2026-01-29
9
9
  * @updated 2026-02-09 - Removed orphan modules tests (Story MIS-2)
10
+ * @updated 2026-06-15 - Status note added (see below)
11
+ *
12
+ * STATUS — IMPORTANT (do not mistake this for a passing CI gate):
13
+ * This is a `.verify.js` standalone script (run manually via `node`), NOT a
14
+ * Jest spec — jest.config.js only matches `*.test.js`/`*.spec.js`, so it never
15
+ * runs in CI. The "Feedback Loop" section below asserts the Story 9.4 surface
16
+ * (FeedbackType enum + trackUserFeedback / getAccuracyMetrics / getSuggestedRules)
17
+ * which is NOT yet implemented in gotchas-memory.js — those assertions are a
18
+ * forward spec for pending work, not a regression check. The Custom Rules
19
+ * section reflects shipped behavior. Do not "fix" this by inventing the feedback
20
+ * feature without its story (Constitution Art. IV — No Invention).
10
21
  */
11
22
 
12
23
  const path = require('path');
@@ -253,15 +253,29 @@ class GotchasMemory extends EventEmitter {
253
253
  count: 0,
254
254
  firstSeen: now,
255
255
  lastSeen: now,
256
+ timestamps: [],
256
257
  samples: [],
257
258
  errorPattern: errorData.message,
258
259
  category: this._detectCategory(errorData.message + ' ' + (errorData.stack || '')),
259
260
  };
260
261
  }
261
262
 
262
- // Update tracking
263
- tracking.count++;
263
+ // Keep only occurrences inside the configured rolling error window.
264
+ const windowStart = now - this.options.errorWindowMs;
265
+ const timestamps = this._normalizeErrorTimestamps(tracking).filter(
266
+ (timestamp) => timestamp >= windowStart,
267
+ );
268
+ timestamps.push(now);
269
+
270
+ tracking.timestamps = timestamps;
271
+ tracking.count = timestamps.length;
272
+ tracking.firstSeen = timestamps[0];
264
273
  tracking.lastSeen = now;
274
+ tracking.samples = (tracking.samples || []).filter((sample) => {
275
+ const timestamp = Date.parse(sample.timestamp);
276
+ return Number.isFinite(timestamp) && timestamp >= windowStart;
277
+ });
278
+
265
279
  if (tracking.samples.length < 5) {
266
280
  tracking.samples.push({
267
281
  timestamp: new Date(now).toISOString(),
@@ -759,6 +773,27 @@ class GotchasMemory extends EventEmitter {
759
773
  return Math.abs(hash).toString(16);
760
774
  }
761
775
 
776
+ /**
777
+ * Normalize persisted error occurrence timestamps.
778
+ * Backward-compat: if tracking only has lastSeen (legacy format), fall back to [lastSeen].
779
+ * @private
780
+ * @param {Object} tracking - Error tracking entry
781
+ * @returns {number[]} Timestamp list in milliseconds
782
+ */
783
+ _normalizeErrorTimestamps(tracking) {
784
+ const timestamps = Array.isArray(tracking.timestamps) ? tracking.timestamps : [tracking.lastSeen];
785
+
786
+ return timestamps
787
+ .map((timestamp) => {
788
+ if (typeof timestamp === 'number') {
789
+ return timestamp;
790
+ }
791
+ const parsed = Date.parse(timestamp);
792
+ return Number.isFinite(parsed) ? parsed : null;
793
+ })
794
+ .filter((timestamp) => timestamp !== null);
795
+ }
796
+
762
797
  /**
763
798
  * Detect category from text
764
799
  * @private
@@ -116,6 +116,12 @@ class AgentInvoker extends EventEmitter {
116
116
  // Audit log (AC7)
117
117
  this.invocations = [];
118
118
  this.logs = [];
119
+
120
+ // Memoization caches: agent/task definitions are read from disk on every
121
+ // invocation but never change during a process run. Caching avoids the
122
+ // repeated fs.readFile hops the deep-dive flagged as a redundant I/O hop.
123
+ this._agentCache = new Map();
124
+ this._taskCache = new Map();
119
125
  }
120
126
 
121
127
  /**
@@ -218,6 +224,10 @@ class AgentInvoker extends EventEmitter {
218
224
  // Normalize agent name (remove @ prefix if present)
219
225
  const name = agentName.replace(/^@/, '').toLowerCase();
220
226
 
227
+ if (this._agentCache.has(name)) {
228
+ return this._agentCache.get(name);
229
+ }
230
+
221
231
  // Check if supported
222
232
  const agentConfig = SUPPORTED_AGENTS[name];
223
233
  if (!agentConfig) {
@@ -239,12 +249,14 @@ class AgentInvoker extends EventEmitter {
239
249
 
240
250
  const content = await fs.readFile(agentPath, 'utf8');
241
251
 
242
- return {
252
+ const result = {
243
253
  ...agentConfig,
244
254
  loaded: true,
245
255
  content,
246
256
  path: agentPath,
247
257
  };
258
+ this._agentCache.set(name, result);
259
+ return result;
248
260
  }
249
261
 
250
262
  /**
@@ -252,6 +264,10 @@ class AgentInvoker extends EventEmitter {
252
264
  * @private
253
265
  */
254
266
  async _loadTask(taskPath) {
267
+ if (this._taskCache.has(taskPath)) {
268
+ return this._taskCache.get(taskPath);
269
+ }
270
+
255
271
  // Handle both full path and task name
256
272
  let fullPath = taskPath;
257
273
  if (!path.isAbsolute(taskPath)) {
@@ -275,12 +291,14 @@ class AgentInvoker extends EventEmitter {
275
291
  // Parse task metadata from frontmatter
276
292
  const metadata = this._parseTaskMetadata(content);
277
293
 
278
- return {
294
+ const result = {
279
295
  path: fullPath,
280
296
  name: path.basename(fullPath, '.md'),
281
297
  content,
282
298
  ...metadata,
283
299
  };
300
+ this._taskCache.set(taskPath, result);
301
+ return result;
284
302
  }
285
303
 
286
304
  /**
@@ -401,11 +419,16 @@ class AgentInvoker extends EventEmitter {
401
419
  );
402
420
  }
403
421
 
404
- // Default: return simulated result
405
- // In production, this would interface with Claude/LLM
422
+ // Honesty invariant (epic: orchestration-consolidation, F1):
423
+ // No executor wired do NOT fabricate a 'simulated' success. Report a stub so
424
+ // callers never mistake "nothing ran" for real work. The MasterOrchestrator
425
+ // supplies a real SubagentDispatcher executor by default (_createDispatchExecutor).
406
426
  return {
407
- status: 'simulated',
408
- message: `Task ${task.name} executed by @${agent.name}`,
427
+ status: 'stub',
428
+ stub: true,
429
+ stubReason:
430
+ 'No executor wired into AgentInvoker — real dispatch is supplied by MasterOrchestrator.',
431
+ message: `Task ${task.name} not executed (no executor) for @${agent.name}`,
409
432
  context: {
410
433
  agentName: agent.name,
411
434
  taskName: task.name,
@@ -67,6 +67,35 @@ const PhaseFailureAction = {
67
67
  ABORT: 'abort',
68
68
  };
69
69
 
70
+ /**
71
+ * Maps a classified project_type to its brownfield workflow variant. Variants
72
+ * stay separate files (distinct contracts — fusion refuted in the deep-dive);
73
+ * the handler dispatches to the right one. Unknown/absent type falls back to
74
+ * brownfield-discovery (the generic 10-phase tech-debt assessment default).
75
+ * @see project-intelligence.md classification matrix
76
+ */
77
+ const BROWNFIELD_WORKFLOW_BY_TYPE = Object.freeze({
78
+ site: 'brownfield-ui',
79
+ lp: 'brownfield-ui',
80
+ app: 'brownfield-ui',
81
+ platform: 'brownfield-fullstack',
82
+ saas: 'brownfield-fullstack',
83
+ api: 'brownfield-service',
84
+ service: 'brownfield-service',
85
+ });
86
+
87
+ const DEFAULT_BROWNFIELD_WORKFLOW = 'brownfield-discovery';
88
+
89
+ /**
90
+ * Resolve the brownfield workflow basename for a project_type.
91
+ * @param {string} [projectType]
92
+ * @returns {string} workflow name (without extension)
93
+ */
94
+ function resolveBrownfieldWorkflow(projectType) {
95
+ const key = String(projectType || '').toLowerCase();
96
+ return BROWNFIELD_WORKFLOW_BY_TYPE[key] || DEFAULT_BROWNFIELD_WORKFLOW;
97
+ }
98
+
70
99
  // ═══════════════════════════════════════════════════════════════════════════════════
71
100
  // BROWNFIELD HANDLER CLASS
72
101
  // ═══════════════════════════════════════════════════════════════════════════════════
@@ -101,10 +130,12 @@ class BrownfieldHandler extends EventEmitter {
101
130
  this._surfaceChecker = options.surfaceChecker || null;
102
131
  this._sessionState = options.sessionState || null;
103
132
 
104
- // Workflow path
133
+ // Workflow variant dispatched by project_type (site/lp/app → ui,
134
+ // platform/saas → fullstack, api/service → service). Default discovery.
135
+ this.workflowName = resolveBrownfieldWorkflow(options.projectType);
105
136
  this.workflowPath = path.join(
106
137
  projectRoot,
107
- '.sinapse-ai/development/workflows/brownfield-discovery.yaml',
138
+ `.sinapse-ai/development/workflows/${this.workflowName}.yaml`,
108
139
  );
109
140
 
110
141
  // Phase progress tracking
@@ -723,7 +754,7 @@ Quer que eu comece?`;
723
754
  const exists = await sessionState.exists();
724
755
  if (exists) {
725
756
  await sessionState.loadSessionState();
726
- await sessionState.recordPhaseChange(`brownfield_${phase}`, 'brownfield-discovery', '@architect');
757
+ await sessionState.recordPhaseChange(`brownfield_${phase}`, this.workflowName, '@architect');
727
758
  this._log(`Phase recorded in session state: ${phase}`);
728
759
  }
729
760
  } catch (error) {
@@ -758,5 +789,7 @@ module.exports = {
758
789
  BrownfieldPhase,
759
790
  PostDiscoveryChoice,
760
791
  PhaseFailureAction,
792
+ BROWNFIELD_WORKFLOW_BY_TYPE,
793
+ resolveBrownfieldWorkflow,
761
794
  };
762
795
 
@@ -23,6 +23,28 @@
23
23
  * @property {string} reason - Reason for the decision
24
24
  */
25
25
 
26
+ /**
27
+ * Runtime-resolved condition defaults.
28
+ * These values are used when a condition is not a built-in evaluator and
29
+ * no runtime override has been set. Frozen so callers cannot mutate them.
30
+ */
31
+ const RUNTIME_CONDITION_DEFAULTS = Object.freeze({
32
+ after_prd_creation: true,
33
+ architecture_changes_needed: true,
34
+ architecture_suggests_prd_changes: true,
35
+ based_on_classification: true,
36
+ documentation_inadequate: true,
37
+ epic_complete: true,
38
+ gate_approved: true,
39
+ major_enhancement_path: true,
40
+ po_checklist_issues: true,
41
+ qa_left_unchecked_items: true,
42
+ stories_remaining: true,
43
+ user_has_generated_ui: true,
44
+ user_wants_ai_generation: true,
45
+ user_wants_story_review: true,
46
+ });
47
+
26
48
  /**
27
49
  * Evaluates workflow conditions based on detected tech stack
28
50
  */
@@ -36,6 +58,30 @@ class ConditionEvaluator {
36
58
  // Context for QA approval tracking (updated externally)
37
59
  this._qaApproved = false;
38
60
  this._phaseOutputs = {};
61
+
62
+ // Runtime condition overrides — take precedence over defaults and built-ins
63
+ this._runtimeOverrides = new Map();
64
+ }
65
+
66
+ /**
67
+ * Set a single runtime-resolved condition value.
68
+ * Runtime overrides take precedence over RUNTIME_CONDITION_DEFAULTS and
69
+ * over built-in tech-stack evaluators.
70
+ * @param {string} name - Condition name
71
+ * @param {boolean} value - Runtime condition value
72
+ */
73
+ setRuntimeCondition(name, value) {
74
+ this._runtimeOverrides.set(name, Boolean(value));
75
+ }
76
+
77
+ /**
78
+ * Set multiple runtime-resolved condition values at once.
79
+ * @param {Object} overrides - Map of condition names to boolean values
80
+ */
81
+ setRuntimeConditions(overrides = {}) {
82
+ for (const [name, value] of Object.entries(overrides)) {
83
+ this.setRuntimeCondition(name, value);
84
+ }
39
85
  }
40
86
 
41
87
  /**
@@ -65,6 +111,11 @@ class ConditionEvaluator {
65
111
  return true;
66
112
  }
67
113
 
114
+ // Runtime overrides take highest precedence
115
+ if (this._runtimeOverrides.has(condition)) {
116
+ return this._runtimeOverrides.get(condition);
117
+ }
118
+
68
119
  // Built-in condition evaluators
69
120
  const evaluators = {
70
121
  // Tech stack conditions
@@ -105,6 +156,11 @@ class ConditionEvaluator {
105
156
  return evaluator();
106
157
  }
107
158
 
159
+ // Fall back to frozen defaults for known runtime conditions
160
+ if (Object.prototype.hasOwnProperty.call(RUNTIME_CONDITION_DEFAULTS, condition)) {
161
+ return RUNTIME_CONDITION_DEFAULTS[condition];
162
+ }
163
+
108
164
  // Handle negation first
109
165
  if (condition.startsWith('!')) {
110
166
  return !this.evaluate(condition.substring(1).trim());
@@ -377,4 +433,5 @@ class ConditionEvaluator {
377
433
  }
378
434
 
379
435
  module.exports = ConditionEvaluator;
436
+ module.exports.RUNTIME_CONDITION_DEFAULTS = RUNTIME_CONDITION_DEFAULTS;
380
437
 
@@ -77,6 +77,7 @@ class Epic3Executor extends EpicExecutor {
77
77
  // Execute spec pipeline phases
78
78
  const phaseResults = {};
79
79
  let specPath = null;
80
+ let specWasStubbed = false;
80
81
 
81
82
  for (const phase of SPEC_PHASES) {
82
83
  this._log(`Running phase: ${phase}`);
@@ -113,9 +114,18 @@ class Epic3Executor extends EpicExecutor {
113
114
 
114
115
  // Check if spec file exists
115
116
  if (!(await fs.pathExists(specPath))) {
116
- // Create stub spec for pipeline to continue
117
- await this._createStubSpec(specPath, storyId, context);
118
- this._log('Created stub spec (real spec generation requires agent invocation)');
117
+ // F1: try to generate a REAL spec via a real agent through the orchestrator.
118
+ const generated = await this._generateSpecViaAgent(storyId, source, techStack, context);
119
+ if (generated) {
120
+ await fs.ensureDir(path.dirname(specPath));
121
+ await fs.writeFile(specPath, generated);
122
+ this._log('Generated spec via real agent invocation');
123
+ } else {
124
+ // No executor wired (or agent produced nothing) → honest stub.
125
+ await this._createStubSpec(specPath, storyId, context);
126
+ this._log('Created stub spec (no real agent available)');
127
+ specWasStubbed = true;
128
+ }
119
129
  }
120
130
 
121
131
  this._addArtifact('spec', specPath);
@@ -124,12 +134,20 @@ class Epic3Executor extends EpicExecutor {
124
134
  const complexity = phaseResults['assess-complexity']?.complexity || 'STANDARD';
125
135
  const requirements = phaseResults['gather-requirements']?.requirements || [];
126
136
 
127
- return this._completeExecution({
137
+ // Honesty invariant (epic: orchestration-consolidation, F0a):
138
+ // if the spec was auto-stubbed (no real agent ran), report STUB, not success.
139
+ const specResult = {
128
140
  specPath,
129
141
  complexity,
130
142
  requirements,
131
143
  phases: Object.keys(phaseResults),
132
- });
144
+ };
145
+ return specWasStubbed
146
+ ? this._stubExecution(
147
+ 'Spec auto-stubbed — real spec generation requires agent invocation',
148
+ specResult,
149
+ )
150
+ : this._completeExecution(specResult);
133
151
  } catch (error) {
134
152
  return this._failExecution(error);
135
153
  }
@@ -181,6 +199,59 @@ class Epic3Executor extends EpicExecutor {
181
199
  };
182
200
  }
183
201
 
202
+ /**
203
+ * Generate the spec by invoking a real agent through the orchestrator.
204
+ *
205
+ * epic: orchestration-consolidation, F1 — connects Epic 3 to the real executor
206
+ * (orchestrator.invokeAgent → SubagentDispatcher → claude). Returns the generated
207
+ * spec markdown, or null when no executor is wired or the agent produced nothing
208
+ * (the caller then falls back to an honest stub).
209
+ *
210
+ * @returns {Promise<string|null>} Generated spec markdown, or null
211
+ * @private
212
+ */
213
+ async _generateSpecViaAgent(storyId, source, techStack, context = {}) {
214
+ const invokeAgent = this.orchestrator && this.orchestrator.invokeAgent;
215
+ if (typeof invokeAgent !== 'function') {
216
+ return null; // No real executor wired → caller falls back to stub
217
+ }
218
+
219
+ const description =
220
+ `Generate a complete, implementation-ready specification (spec.md) for story "${storyId}". ` +
221
+ `Source: ${source || 'story'}. ` +
222
+ (techStack ? `Tech stack: ${JSON.stringify(techStack)}. ` : '') +
223
+ 'Output ONLY the spec markdown: overview, scope (in/out), acceptance criteria, ' +
224
+ 'dependencies and complexity estimate. No commentary outside the spec.';
225
+
226
+ try {
227
+ // Pass only serializable, prompt-relevant context — never the execution
228
+ // context wholesale (it carries `orchestrator: this` → circular reference).
229
+ const result = await invokeAgent(
230
+ { name: 'analyst' },
231
+ { id: `spec-${storyId}`, type: 'analysis', description },
232
+ { storyId, source, techStack, prdPath: context.prdPath },
233
+ );
234
+ const content = result && (result.output || result.content);
235
+ // Honesty: an agent "success" whose output is a CLI error banner or a
236
+ // few stray lines is NOT a spec. Require minimum substance + markdown
237
+ // structure before accepting, otherwise fall back to the honest stub.
238
+ const looksLikeSpec =
239
+ typeof content === 'string' &&
240
+ content.trim().length >= 200 &&
241
+ /^#{1,3}\s/m.test(content) &&
242
+ !/issue with the selected model|may not exist or you may not have access/i.test(content);
243
+ if (result && result.success !== false && looksLikeSpec) {
244
+ return content;
245
+ }
246
+ if (content && !looksLikeSpec) {
247
+ this._log('Agent output rejected (not a plausible spec) — falling back to stub', 'warn');
248
+ }
249
+ } catch (err) {
250
+ this._log(`Spec agent invocation failed: ${err.message}`, 'warn');
251
+ }
252
+ return null;
253
+ }
254
+
184
255
  /**
185
256
  * Create stub spec file
186
257
  * @private
@@ -105,37 +105,83 @@ class Epic4Executor extends EpicExecutor {
105
105
  }
106
106
  }
107
107
 
108
- // Execute subtasks
109
- const subtaskResults = await this._executeSubtasks(storyId, tracker, context);
108
+ // F1/convergência (epic: orchestration-consolidation): Epic 4 no longer
109
+ // re-implements execution. It DELEGATES to the BuildOrchestrator — the single
110
+ // real build path (worktree → plan → execute via claude → QA → merge). This
111
+ // kills the duplication the audit flagged (two competing execution engines).
112
+ if (this._realExecutionAllowed()) {
113
+ const buildResult = await this._executeViaBuildOrchestrator(storyId, context);
114
+ if (buildResult && buildResult.success) {
115
+ return this._completeExecution({
116
+ implementationPath: planPath,
117
+ planPath,
118
+ build: buildResult,
119
+ reportPath: buildResult.reportPath,
120
+ phases: buildResult.phases,
121
+ });
122
+ }
123
+ // Build actually ran and failed → honest failure (not a stub, not fake success).
124
+ return this._failExecution(
125
+ (buildResult && buildResult.error) || 'BuildOrchestrator failed',
126
+ );
127
+ }
110
128
 
111
- // Collect code changes
129
+ // Fallback (test runner without SINAPSE_REAL_DISPATCH): honest stub — never
130
+ // fabricate success. Preserves the F0a invariant and keeps unit tests fast.
131
+ const subtaskResults = await this._executeSubtasks(storyId, tracker, context);
112
132
  const codeChanges = this._collectCodeChanges(subtaskResults);
113
-
114
- // Run tests if available
115
133
  const testResults = await this._runTests(context);
116
-
117
- // Calculate final progress
118
134
  const progress = {
119
135
  total: subtaskResults.length,
120
136
  completed: subtaskResults.filter((r) => r.success).length,
121
137
  failed: subtaskResults.filter((r) => !r.success).length,
122
138
  };
123
-
124
139
  this._addArtifact('progress', JSON.stringify(progress));
125
-
126
- return this._completeExecution({
127
- implementationPath: planPath,
128
- planPath,
129
- progress,
130
- subtaskResults,
131
- codeChanges,
132
- testResults,
133
- });
140
+ return this._stubExecution(
141
+ 'Epic 4 stub mode — real build is delegated to BuildOrchestrator outside the test runner',
142
+ {
143
+ implementationPath: planPath,
144
+ planPath,
145
+ progress,
146
+ subtaskResults,
147
+ codeChanges,
148
+ testResults,
149
+ },
150
+ );
134
151
  } catch (error) {
135
152
  return this._failExecution(error);
136
153
  }
137
154
  }
138
155
 
156
+ /**
157
+ * Delegate execution to the BuildOrchestrator — the single real build path.
158
+ *
159
+ * epic: orchestration-consolidation, F1/convergence — instead of re-implementing
160
+ * the build (and duplicating it), Epic 4 hands off to the BuildOrchestrator, which
161
+ * runs plan → execute (claude) → QA → merge. Conservative defaults inside the
162
+ * pipeline (no worktree/merge side-effects, QA is Epic 6's job); tune via
163
+ * context.buildOptions.
164
+ *
165
+ * @returns {Promise<Object>} BuildOrchestrator result ({ success, ... })
166
+ * @private
167
+ */
168
+ async _executeViaBuildOrchestrator(storyId, context) {
169
+ try {
170
+ const { BuildOrchestrator } = require('../../execution/build-orchestrator');
171
+ const builder = new BuildOrchestrator({
172
+ rootPath: this.projectRoot,
173
+ useWorktree: false,
174
+ autoMerge: false,
175
+ runQA: false,
176
+ ...(context.buildOptions || {}),
177
+ });
178
+ return await builder.build(storyId, context.buildOptions || {});
179
+ } catch (err) {
180
+ this._log(`BuildOrchestrator delegation failed: ${err.message}`, 'error');
181
+ return { success: false, error: err.message };
182
+ }
183
+ }
184
+
139
185
  /**
140
186
  * Find existing plan or create new one
141
187
  * @private