sinapse-ai 1.19.0 → 1.19.1

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.
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * Integrates all Epic 11 modules:
12
12
  * - ExecutorAssignment (11.1) — agent selection
13
- * - TerminalSpawner (11.2) — agent spawning
14
- * - WorkflowExecutor (11.3) — development cycle
13
+ * - WorkflowExecutor (11.3) — development cycle (honest manual hand-off)
15
14
  * - SurfaceChecker (11.4) — human decision criteria
16
15
  * - SessionState (11.5) — session persistence
17
16
  *
@@ -1252,8 +1251,7 @@ class BobOrchestrator {
1252
1251
  *
1253
1252
  * Delegates to Epic 11 modules:
1254
1253
  * - ExecutorAssignment for agent selection (AC8)
1255
- * - TerminalSpawner for agent spawning (AC9)
1256
- * - WorkflowExecutor for development cycle (AC10)
1254
+ * - WorkflowExecutor for development cycle, with honest manual hand-off (AC10)
1257
1255
  *
1258
1256
  * Story 12.5 AC5: Updates session state at each phase transition.
1259
1257
  *
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require('fs-extra');
15
15
  const path = require('path');
16
+ const yaml = require('js-yaml');
16
17
  const EpicExecutor = require('./epic-executor');
17
18
 
18
19
  /**
@@ -215,44 +216,47 @@ class Epic4Executor extends EpicExecutor {
215
216
  * @private
216
217
  */
217
218
  async _createStubPlan(planPath, storyId, specPath) {
218
- const stubPlan = `# Implementation Plan: ${storyId}
219
- # Generated: ${new Date().toISOString()}
220
-
221
- metadata:
222
- storyId: "${storyId}"
223
- specPath: "${specPath || 'N/A'}"
224
- status: draft
225
- createdAt: "${new Date().toISOString()}"
226
-
227
- phases:
228
- - phase: 1
229
- name: Setup
230
- subtasks:
231
- - id: "1.1"
232
- name: Initialize project structure
233
- status: pending
234
-
235
- - phase: 2
236
- name: Implementation
237
- subtasks:
238
- - id: "2.1"
239
- name: Implement core functionality
240
- status: pending
241
-
242
- - phase: 3
243
- name: Testing
244
- subtasks:
245
- - id: "3.1"
246
- name: Write tests
247
- status: pending
248
-
249
- - phase: 4
250
- name: Documentation
251
- subtasks:
252
- - id: "4.1"
253
- name: Update documentation
254
- status: pending
255
- `;
219
+ const now = new Date().toISOString();
220
+
221
+ // Build the plan as an object and serialize via yaml.dump. This is robust to
222
+ // any content — notably Windows absolute paths in `specPath`
223
+ // (e.g. "C:\Users\...\AppData\...") which, when hand-written into a
224
+ // double-quoted YAML scalar, contain invalid escape sequences (`\U`, `\A`)
225
+ // that make `yaml.load` throw and kill the whole build path on Windows.
226
+ // yaml.dump escapes/quotes correctly for any value (Story: FIX windows-path-yaml-plan).
227
+ const planObject = {
228
+ metadata: {
229
+ storyId,
230
+ specPath: specPath || 'N/A',
231
+ status: 'draft',
232
+ createdAt: now,
233
+ },
234
+ phases: [
235
+ {
236
+ phase: 1,
237
+ name: 'Setup',
238
+ subtasks: [{ id: '1.1', name: 'Initialize project structure', status: 'pending' }],
239
+ },
240
+ {
241
+ phase: 2,
242
+ name: 'Implementation',
243
+ subtasks: [{ id: '2.1', name: 'Implement core functionality', status: 'pending' }],
244
+ },
245
+ {
246
+ phase: 3,
247
+ name: 'Testing',
248
+ subtasks: [{ id: '3.1', name: 'Write tests', status: 'pending' }],
249
+ },
250
+ {
251
+ phase: 4,
252
+ name: 'Documentation',
253
+ subtasks: [{ id: '4.1', name: 'Update documentation', status: 'pending' }],
254
+ },
255
+ ],
256
+ };
257
+
258
+ const header = `# Implementation Plan: ${storyId}\n# Generated: ${now}\n\n`;
259
+ const stubPlan = header + yaml.dump(planObject, { lineWidth: -1, noRefs: true });
256
260
 
257
261
  await fs.ensureDir(path.dirname(planPath));
258
262
  await fs.writeFile(planPath, stubPlan);
@@ -202,6 +202,22 @@ class GateEvaluator {
202
202
  async _runGateChecks(fromEpic, toEpic, epicResult, gateConfig) {
203
203
  const checks = [];
204
204
 
205
+ // Universal honesty guard (master/gate leak fix, epic: orchestration-consolidation):
206
+ // a gate must NEVER approve a result that itself signals failure. The e2e checkpoint
207
+ // caught the gate approving (score 5.0) a QA report whose verdict was BLOCKED, because
208
+ // the existence checks (qa_report_exists / verdict_generated) never inspected the VALUE.
209
+ // This runs on EVERY gate, but ONLY adds a (failing, critical) check when a real failure
210
+ // signal is present — so it never inflates an honest zero-check gate (F5 invariant).
211
+ const failureSignal = this._detectFailureSignal(epicResult);
212
+ if (failureSignal) {
213
+ checks.push({
214
+ name: 'result_not_failed',
215
+ passed: false,
216
+ message: failureSignal,
217
+ severity: 'critical',
218
+ });
219
+ }
220
+
205
221
  // Get check list for this gate
206
222
  const checkNames = gateConfig.checks || this._getDefaultChecks(fromEpic, toEpic);
207
223
 
@@ -254,6 +270,36 @@ class GateEvaluator {
254
270
  return checks;
255
271
  }
256
272
 
273
+ /**
274
+ * Detect whether an epic result itself signals failure (master/gate leak fix).
275
+ *
276
+ * Returns a human-readable reason string when the result is NOT sound, or null when
277
+ * no failure signal is present. Used by `_runGateChecks` to add a critical, blocking
278
+ * check so a gate can never APPROVE a failed/blocked upstream result.
279
+ *
280
+ * @param {Object} epicResult - Result from the source epic
281
+ * @returns {string|null} Failure reason, or null if the result shows no failure signal
282
+ * @private
283
+ */
284
+ _detectFailureSignal(epicResult) {
285
+ if (!epicResult || typeof epicResult !== 'object') {
286
+ return null;
287
+ }
288
+ if (epicResult.success === false || epicResult.status === 'failed') {
289
+ return 'Epic result signals failure (success:false / status:failed)';
290
+ }
291
+ const norm = (v) => (typeof v === 'string' ? v.toLowerCase() : null);
292
+ const verdict = norm(epicResult.verdict);
293
+ if (verdict === 'blocked' || verdict === 'failed') {
294
+ return `Epic verdict is "${epicResult.verdict}"`;
295
+ }
296
+ const qaVerdict = epicResult.qaReport ? norm(epicResult.qaReport.verdict) : null;
297
+ if (qaVerdict === 'blocked' || qaVerdict === 'failed') {
298
+ return `QA report verdict is "${epicResult.qaReport.verdict}"`;
299
+ }
300
+ return null;
301
+ }
302
+
257
303
  /**
258
304
  * Run a single check
259
305
  * @private
@@ -624,62 +624,30 @@ class GreenfieldHandler extends EventEmitter {
624
624
  // ═══════════════════════════════════════════════════════════════════════════════════
625
625
 
626
626
  /**
627
- * Spawns an agent via TerminalSpawner (AC7) with ExecutorAssignment (AC6)
627
+ * Resolves agent execution to an honest manual hand-off (AC6 selection only).
628
+ *
629
+ * The legacy visual-terminal lineage (TerminalSpawner → pm.sh) was a verified stub that
630
+ * never invoked claude, so spawning it fabricated success in headless-with-bash. It has
631
+ * been removed (STORY-F3C); execution is now handed off honestly to the user. Routing to
632
+ * the canonical `dispatcher → claude` lineage is intentionally out of scope here.
628
633
  *
629
634
  * @param {string} agent - Agent ID (e.g., '@devops')
630
635
  * @param {string} task - Task to execute
631
636
  * @param {Object} spawnContext - Spawn context
632
- * @returns {Promise<Object>} Spawn result
637
+ * @returns {Promise<Object>} Honest manual hand-off result
633
638
  * @private
634
639
  */
635
640
  async _spawnAgent(agent, task, spawnContext = {}) {
636
- this._log(`Spawning ${agent} for ${task}`);
641
+ this._log(`Handing off ${agent} for ${task} (manual execution)`);
637
642
 
638
643
  this.emit('agentSpawn', { agent, task, context: spawnContext });
639
644
 
640
- try {
641
- // Try TerminalSpawner first (AC7)
642
- const TerminalSpawner = require('./terminal-spawner');
643
-
644
- if (TerminalSpawner.isSpawnerAvailable()) {
645
- const agentId = agent.replace('@', '');
646
- const result = await TerminalSpawner.spawnAgent(agentId, task, {
647
- context: {
648
- instructions: spawnContext.instructions || `Execute ${task}`,
649
- creates: spawnContext.creates || [],
650
- requires: spawnContext.requires || [],
651
- metadata: spawnContext.previousResults || {},
652
- },
653
- timeout: 7200000, // 2 hours
654
- debug: this.options.debug,
655
- });
656
-
657
- if (result.pid) {
658
- this.emit('terminalSpawn', { agent, pid: result.pid, task });
659
- }
660
-
661
- return {
662
- success: result.success !== false,
663
- pid: result.pid,
664
- output: result.output,
665
- outputFile: result.outputFile,
666
- };
667
- }
668
-
669
- // Fallback: Return instructions for manual execution
670
- this._log(`TerminalSpawner not available, returning manual instructions for ${agent}`);
671
- return {
672
- success: true,
673
- manual: true,
674
- instructions: `Spawn ${agent} manually and execute: *${task}`,
675
- };
676
- } catch (error) {
677
- this._log(`Failed to spawn ${agent}: ${error.message}`, 'error');
678
- return {
679
- success: false,
680
- error: error.message,
681
- };
682
- }
645
+ // Honest hand-off: return instructions for manual execution (no fabricated success).
646
+ return {
647
+ success: true,
648
+ manual: true,
649
+ instructions: `Spawn ${agent} manually and execute: *${task}`,
650
+ };
683
651
  }
684
652
 
685
653
  // ═══════════════════════════════════════════════════════════════════════════════════
@@ -41,9 +41,6 @@ const cliCommands = require('./cli-commands');
41
41
  // Story 11.1: Executor Assignment (Projeto Bob)
42
42
  const ExecutorAssignment = require('./executor-assignment');
43
43
 
44
- // Story 11.2: Terminal Spawner (Projeto Bob)
45
- const TerminalSpawner = require('./terminal-spawner');
46
-
47
44
  // Story 11.3: Workflow Executor (Projeto Bob)
48
45
  const {
49
46
  WorkflowExecutor,
@@ -216,15 +213,6 @@ module.exports = {
216
213
  validateExecutorAssignment: ExecutorAssignment.validateExecutorAssignment,
217
214
  EXECUTOR_ASSIGNMENT_TABLE: ExecutorAssignment.EXECUTOR_ASSIGNMENT_TABLE,
218
215
 
219
- // Story 11.2: Terminal Spawner (Projeto Bob)
220
- TerminalSpawner,
221
- spawnAgent: TerminalSpawner.spawnAgent,
222
- createContextFile: TerminalSpawner.createContextFile,
223
- pollForOutput: TerminalSpawner.pollForOutput,
224
- isSpawnerAvailable: TerminalSpawner.isSpawnerAvailable,
225
- getPlatform: TerminalSpawner.getPlatform,
226
- cleanupOldFiles: TerminalSpawner.cleanupOldFiles,
227
-
228
216
  // Story 11.3: Workflow Executor (Projeto Bob)
229
217
  WorkflowExecutor,
230
218
  createWorkflowExecutor,
@@ -478,10 +478,15 @@ class MasterOrchestrator extends EventEmitter {
478
478
  } else {
479
479
  pipelineResult.epicsFailed.push(epicNum);
480
480
 
481
- // In strict mode or if epic is critical, stop pipeline
481
+ // Honesty invariant (master/gate leak fix): ANY failed epic means the
482
+ // pipeline did not fully succeed — not only critical/strict ones. A
483
+ // non-critical failure (e.g. QA/Epic 6) must still flip success:false so
484
+ // finalize() never reports an overall green build over a red epic.
485
+ pipelineResult.success = false;
486
+
487
+ // In strict mode or if epic is critical, also STOP the pipeline now.
482
488
  if (this.strictGates || this._isEpicCritical(epicNum)) {
483
489
  this._log(`Critical epic ${epicNum} failed, halting pipeline`);
484
- pipelineResult.success = false;
485
490
  break;
486
491
  }
487
492
  }
@@ -604,6 +609,45 @@ class MasterOrchestrator extends EventEmitter {
604
609
  orchestrator: this,
605
610
  });
606
611
 
612
+ // Honesty invariant (epic: orchestration-consolidation — master/gate leak fix):
613
+ // an executor may signal failure by RETURNING { success:false } / { status:'failed' }
614
+ // WITHOUT throwing. The old code fell straight through to "mark COMPLETED" + logged
615
+ // "completed successfully" + returned { success:true } for any non-stub result — so a
616
+ // failed epic leaked a green pipeline (the e2e "Epic 4 failed" → "Epic 4 completed
617
+ // successfully" → exit 0 symptom). The `catch` below only handles THROWN errors, so a
618
+ // returned failure must be detected explicitly here and treated as a real failure.
619
+ const isStubResult = result && result.status === 'stub';
620
+ const isFailedResult =
621
+ result && !isStubResult && (result.success === false || result.status === 'failed');
622
+
623
+ if (isFailedResult) {
624
+ this.executionState.epics[epicNum] = {
625
+ ...this.executionState.epics[epicNum],
626
+ status: EpicStatus.FAILED,
627
+ completedAt: new Date().toISOString(),
628
+ result,
629
+ errors: [
630
+ ...(this.executionState.epics[epicNum]?.errors || []),
631
+ {
632
+ message: result.error || `Epic ${epicNum} returned a non-success result`,
633
+ timestamp: new Date().toISOString(),
634
+ },
635
+ ],
636
+ };
637
+
638
+ await this._saveState();
639
+
640
+ this.emit('epicComplete', { epicNum, result, gateResult: null });
641
+ this.onEpicComplete(epicNum, result);
642
+
643
+ this._log(`Epic ${epicNum} failed (executor returned non-success result)`, {
644
+ level: 'error',
645
+ icon: '❌',
646
+ });
647
+
648
+ return { success: false, status: 'failed', epicNum, result };
649
+ }
650
+
607
651
  // Mark as completed
608
652
  this.executionState.epics[epicNum] = {
609
653
  ...this.executionState.epics[epicNum],
@@ -614,8 +658,8 @@ class MasterOrchestrator extends EventEmitter {
614
658
 
615
659
  // Evaluate quality gate (Story 0.6) - only in full pipeline mode
616
660
  // Skip gate evaluation if result is from stub executor
661
+ // (isStubResult already computed above with the failure-detection guard).
617
662
  let gateResult = null;
618
- const isStubResult = result && result.status === 'stub';
619
663
  if (this._inFullPipeline && result && result.success !== false && !isStubResult) {
620
664
  gateResult = await this._evaluateGate(epicNum, result);
621
665
 
@@ -1486,13 +1530,25 @@ class MasterOrchestrator extends EventEmitter {
1486
1530
  const seconds = Math.floor((duration % 60000) / 1000);
1487
1531
 
1488
1532
  const hasStubs = pipelineResult.hasStubs || (pipelineResult.epicsStubbed || []).length > 0;
1533
+ // Honesty invariant (master/gate leak fix): a pipeline with ANY failed epic, or a
1534
+ // final state of BLOCKED/FAILED, can never report success:true — even if a stale
1535
+ // optimistic flag survived. `blocked` is surfaced so the CLI shows BLOCKED (exit 2)
1536
+ // instead of "ORCHESTRATION COMPLETE" / exit 0.
1537
+ const hasFailures = (pipelineResult.epicsFailed || []).length > 0;
1538
+ const blocked = this._state === OrchestratorState.BLOCKED;
1489
1539
  return {
1490
1540
  workflowId: this.executionState.workflowId,
1491
1541
  storyId: this.storyId,
1492
1542
  status: this._state,
1543
+ blocked,
1493
1544
  // Honesty invariant (epic: orchestration-consolidation, F0a): a pipeline that ran
1494
1545
  // any epic in STUB mode did NOT really build anything — it must not report success:true.
1495
- success: (pipelineResult.success ?? this._state === OrchestratorState.COMPLETE) && !hasStubs,
1546
+ // A failed epic or a non-COMPLETE terminal state is likewise never a success.
1547
+ success:
1548
+ (pipelineResult.success ?? this._state === OrchestratorState.COMPLETE) &&
1549
+ !hasStubs &&
1550
+ !hasFailures &&
1551
+ !blocked,
1496
1552
  mode: hasStubs ? 'stub' : 'real',
1497
1553
  stubbedEpics: pipelineResult.epicsStubbed || [],
1498
1554
  ...(hasStubs
@@ -23,9 +23,8 @@ const fsSync = require('fs');
23
23
  const path = require('path');
24
24
  const yaml = require('js-yaml');
25
25
 
26
- // Import dependencies from Story 11.1, 11.2, and 11.5
26
+ // Import dependencies from Story 11.1 and 11.5
27
27
  const ExecutorAssignment = require('./executor-assignment');
28
- const TerminalSpawner = require('./terminal-spawner');
29
28
  const { SessionState, ActionType } = require('./session-state');
30
29
 
31
30
  // IDS Gate Wiring: GateEvaluator wires the IDS verification gates into the
@@ -43,29 +42,6 @@ const DEFAULT_TIMEOUT_MS = 7200000; // 2 hours
43
42
  const CHECKPOINT_TIMEOUT_MS = 1800000; // 30 minutes
44
43
  const STATE_SAVE_INTERVAL_MS = 300000; // 5 minutes
45
44
 
46
- /**
47
- * Determines whether a REAL visual-terminal spawn can occur in the current environment.
48
- *
49
- * Combines the platform-support signal (`isSpawnerAvailable()`) with the stricter
50
- * real-terminal signal (`detectEnvironment().supportsVisualTerminal`). The latter returns
51
- * `false` in headless contexts (CI / SSH / Docker / VS Code integrated terminal) and `true`
52
- * only in a native terminal. Without this second check, `isSpawnerAvailable()` alone returns
53
- * `true` on CI / Windows-without-bash, causing `spawn('bash', [pm.sh])` to fail with ENOENT
54
- * (test noise) plus a slow subprocess. When this returns `false`, phases skip the spawn and
55
- * fall through to their existing "manual execution" fallback — no bash, no noise.
56
- *
57
- * Note: this intentionally does NOT change `TerminalSpawner.isSpawnerAvailable()` semantics
58
- * (Conservative Default — Article XI); the hardening lives at the call-sites only.
59
- *
60
- * @returns {boolean} True only when a native visual terminal spawn is safe to attempt.
61
- */
62
- function canSpawnVisualTerminal() {
63
- return (
64
- TerminalSpawner.isSpawnerAvailable() &&
65
- TerminalSpawner.detectEnvironment().supportsVisualTerminal
66
- );
67
- }
68
-
69
45
  /**
70
46
  * Workflow phase status
71
47
  * @enum {string}
@@ -213,25 +189,6 @@ class WorkflowExecutor {
213
189
  }
214
190
  }
215
191
 
216
- /**
217
- * Emits terminal spawn to all registered callbacks (Story 12.6)
218
- * @param {string} agent - Agent ID
219
- * @param {number} pid - Process ID
220
- * @param {string} task - Task being executed
221
- * @private
222
- */
223
- _emitTerminalSpawn(agent, pid, task) {
224
- for (const callback of this._terminalSpawnCallbacks) {
225
- try {
226
- callback(agent, pid, task);
227
- } catch (error) {
228
- if (this.options.debug) {
229
- console.log(`[WorkflowExecutor] Terminal spawn callback error: ${error.message}`);
230
- }
231
- }
232
- }
233
- }
234
-
235
192
  /**
236
193
  * Loads the workflow definition
237
194
  * @returns {Promise<Object>} Workflow definition
@@ -739,39 +696,8 @@ class WorkflowExecutor {
739
696
  }
740
697
  }
741
698
 
742
- // Use terminal spawning (Story 11.2)
743
- if (phase.spawn_in_terminal && canSpawnVisualTerminal()) {
744
- const context = {
745
- story: storyPath,
746
- files: [],
747
- instructions: `Execute *develop for story: ${storyPath}`,
748
- metadata: this.state.accumulatedContext,
749
- };
750
-
751
- const result = await TerminalSpawner.spawnAgent(agent.replace('@', ''), 'develop', {
752
- context,
753
- timeout: DEFAULT_TIMEOUT_MS,
754
- debug: this.options.debug,
755
- });
756
-
757
- // Story 12.6: Emit terminal spawn for observability (AC1)
758
- if (result.pid) {
759
- this._emitTerminalSpawn(agent, result.pid, 'development');
760
- }
761
-
762
- return {
763
- status: result.success ? PhaseStatus.COMPLETED : PhaseStatus.FAILED,
764
- implementation: {
765
- files_created: [],
766
- files_modified: [],
767
- tests_added: [],
768
- },
769
- output: result.output,
770
- outputFile: result.outputFile,
771
- };
772
- }
773
-
774
- // Fallback: Return pending for manual execution
699
+ // Honest fallback: the dev cycle does not invoke the agent in-process here;
700
+ // execution is handed off (manual) rather than fabricated by a stub.
775
701
  return {
776
702
  status: PhaseStatus.COMPLETED,
777
703
  implementation: {
@@ -779,7 +705,7 @@ class WorkflowExecutor {
779
705
  files_modified: [],
780
706
  tests_added: [],
781
707
  },
782
- note: 'Terminal spawning not available, manual execution required',
708
+ note: 'Manual execution required',
783
709
  };
784
710
  } catch (error) {
785
711
  return {
@@ -1056,42 +982,7 @@ class WorkflowExecutor {
1056
982
  // Story 12.6: Emit agent spawn for observability (AC1)
1057
983
  this._emitAgentSpawn(agent, 'quality_gate');
1058
984
 
1059
- // Use terminal spawning
1060
- if (phase.spawn_in_terminal && canSpawnVisualTerminal()) {
1061
- const context = {
1062
- story: storyPath,
1063
- files: [],
1064
- instructions: `Execute quality review for story: ${storyPath}`,
1065
- metadata: {
1066
- executor: this.state.executor,
1067
- implementation: this.state.phaseResults['2_development']?.implementation,
1068
- },
1069
- };
1070
-
1071
- const result = await TerminalSpawner.spawnAgent(agent.replace('@', ''), 'quality-review', {
1072
- context,
1073
- timeout: DEFAULT_TIMEOUT_MS / 4, // 30 minutes
1074
- debug: this.options.debug,
1075
- });
1076
-
1077
- // Story 12.6: Emit terminal spawn for observability (AC1)
1078
- if (result.pid) {
1079
- this._emitTerminalSpawn(agent, result.pid, 'quality_gate');
1080
- }
1081
-
1082
- return {
1083
- status: result.success ? PhaseStatus.COMPLETED : PhaseStatus.FAILED,
1084
- review_result: {
1085
- verdict: result.success ? 'APPROVED' : 'NEEDS_WORK',
1086
- score: result.success ? 90 : 60,
1087
- findings: [],
1088
- recommendations: [],
1089
- },
1090
- output: result.output,
1091
- };
1092
- }
1093
-
1094
- // Fallback
985
+ // Honest fallback: the review is handed off (manual) rather than fabricated by a stub.
1095
986
  return {
1096
987
  status: PhaseStatus.COMPLETED,
1097
988
  review_result: {
@@ -1100,7 +991,7 @@ class WorkflowExecutor {
1100
991
  findings: [],
1101
992
  recommendations: [],
1102
993
  },
1103
- note: 'Terminal spawning not available, manual review required',
994
+ note: 'Manual review required',
1104
995
  };
1105
996
  } catch (error) {
1106
997
  return {
@@ -1126,40 +1017,7 @@ class WorkflowExecutor {
1126
1017
  // Story 12.6: Emit agent spawn for observability (AC1)
1127
1018
  this._emitAgentSpawn(agent, 'push');
1128
1019
 
1129
- // Use terminal spawning
1130
- if (phase.spawn_in_terminal && canSpawnVisualTerminal()) {
1131
- const context = {
1132
- story: storyPath,
1133
- files: [],
1134
- instructions: `Execute *pre-push and *push for story: ${storyPath}`,
1135
- metadata: {
1136
- review_result: this.state.phaseResults['4_quality_gate']?.review_result,
1137
- },
1138
- };
1139
-
1140
- const result = await TerminalSpawner.spawnAgent(agent.replace('@', ''), 'push-and-pr', {
1141
- context,
1142
- timeout: DEFAULT_TIMEOUT_MS / 12, // 10 minutes
1143
- debug: this.options.debug,
1144
- });
1145
-
1146
- // Story 12.6: Emit terminal spawn for observability (AC1)
1147
- if (result.pid) {
1148
- this._emitTerminalSpawn(agent, result.pid, 'push');
1149
- }
1150
-
1151
- return {
1152
- status: result.success ? PhaseStatus.COMPLETED : PhaseStatus.FAILED,
1153
- push_result: {
1154
- commit_hash: '',
1155
- branch: 'main',
1156
- },
1157
- pr_url: '',
1158
- output: result.output,
1159
- };
1160
- }
1161
-
1162
- // Fallback
1020
+ // Honest fallback: the push is handed off (manual) rather than fabricated by a stub.
1163
1021
  return {
1164
1022
  status: PhaseStatus.COMPLETED,
1165
1023
  push_result: {
@@ -1167,7 +1025,7 @@ class WorkflowExecutor {
1167
1025
  branch: 'main',
1168
1026
  },
1169
1027
  pr_url: '',
1170
- note: 'Terminal spawning not available, manual push required',
1028
+ note: 'Manual push required',
1171
1029
  };
1172
1030
  } catch (error) {
1173
1031
  return {