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.
- package/.sinapse-ai/core/orchestration/bob-orchestrator.js +2 -4
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +42 -38
- package/.sinapse-ai/core/orchestration/gate-evaluator.js +46 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +14 -46
- package/.sinapse-ai/core/orchestration/index.js +0 -12
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +60 -4
- package/.sinapse-ai/core/orchestration/workflow-executor.js +8 -150
- package/.sinapse-ai/data/entity-registry.yaml +315 -527
- package/.sinapse-ai/development/agents/project-lead/MEMORY.md +1 -1
- package/.sinapse-ai/development/agents/project-lead.md +9 -13
- package/.sinapse-ai/development/tasks/push-and-pr.md +1 -1
- package/.sinapse-ai/development/workflows/development-cycle.yaml +1 -12
- package/.sinapse-ai/install-manifest.yaml +24 -32
- package/CHANGELOG.md +29 -26
- package/package.json +1 -1
- package/.sinapse-ai/core/orchestration/terminal-spawner.js +0 -1067
- package/.sinapse-ai/scripts/pm.sh +0 -466
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Integrates all Epic 11 modules:
|
|
12
12
|
* - ExecutorAssignment (11.1) — agent selection
|
|
13
|
-
* -
|
|
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
|
-
* -
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
*
|
|
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>}
|
|
637
|
+
* @returns {Promise<Object>} Honest manual hand-off result
|
|
633
638
|
* @private
|
|
634
639
|
*/
|
|
635
640
|
async _spawnAgent(agent, task, spawnContext = {}) {
|
|
636
|
-
this._log(`
|
|
641
|
+
this._log(`Handing off ${agent} for ${task} (manual execution)`);
|
|
637
642
|
|
|
638
643
|
this.emit('agentSpawn', { agent, task, context: spawnContext });
|
|
639
644
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
743
|
-
|
|
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: '
|
|
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
|
-
//
|
|
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: '
|
|
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
|
-
//
|
|
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: '
|
|
1028
|
+
note: 'Manual push required',
|
|
1171
1029
|
};
|
|
1172
1030
|
} catch (error) {
|
|
1173
1031
|
return {
|