agentic-orchestrator 0.2.11 → 0.2.13
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/.claude/settings.local.json +3 -1
- package/apps/control-plane/src/application/services/git-reconciliation-service.ts +16 -4
- package/apps/control-plane/src/supervisor/planning-wave-executor.ts +162 -148
- package/apps/control-plane/src/supervisor/run-coordinator.ts +7 -2
- package/apps/control-plane/test/git-reconciliation-service.spec.ts +3 -1
- package/apps/control-plane/test/organizer-enrollment-scheduler.spec.ts +41 -1
- package/apps/control-plane/test/planning-wave-executor.spec.ts +64 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +43 -0
- package/apps/control-plane/test/worker-execution-policy.spec.ts +28 -0
- package/apps/control-plane/vitest.config.ts +2 -1
- package/dist/apps/control-plane/application/services/git-reconciliation-service.js +9 -4
- package/dist/apps/control-plane/application/services/git-reconciliation-service.js.map +1 -1
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js +141 -130
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.js +6 -2
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/package.json +1 -1
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
"Bash(find /home/erictaurone/workspace/Aequitas/projects/Agentic-Orchestrator/packages/web-dashboard/src/components -name *question* -o -name *intake* -o -name *needs* -o -name *input*)",
|
|
64
64
|
"Bash(tee /tmp/test-out.txt)",
|
|
65
65
|
"Bash(find /home/erictaurone/workspace/Aequitas/projects/Agentic-Orchestrator -type f -name .env*)",
|
|
66
|
-
"Bash(find /home/erictaurone/workspace/Aequitas/projects/Agentic-Orchestrator/packages/web-dashboard/src/app/api -type f -name *stream* -o -name *events* -o -name *sse*)"
|
|
66
|
+
"Bash(find /home/erictaurone/workspace/Aequitas/projects/Agentic-Orchestrator/packages/web-dashboard/src/app/api -type f -name *stream* -o -name *events* -o -name *sse*)",
|
|
67
|
+
"Bash(find /home/erictaurone/workspace/Aequitas/projects/Agentic-Orchestrator/apps/reconciler -type f -name *.ts)",
|
|
68
|
+
"Bash(sort -t'|' -k3 -n)"
|
|
67
69
|
]
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -274,7 +274,7 @@ function errorMessage(error: unknown): string {
|
|
|
274
274
|
return 'unknown reconciler dispatch failure';
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
function
|
|
277
|
+
function _isMissingReconcilerConversation(error: unknown): boolean {
|
|
278
278
|
if (error instanceof Error && error.message.includes('No conversation found with session ID')) {
|
|
279
279
|
return true;
|
|
280
280
|
}
|
|
@@ -1200,11 +1200,17 @@ export class GitReconciliationService {
|
|
|
1200
1200
|
},
|
|
1201
1201
|
): Promise<boolean> {
|
|
1202
1202
|
if (!sessionId || sessionId === 'unknown' || sessionId === 'unassigned') {
|
|
1203
|
+
console.warn(
|
|
1204
|
+
`[reconciler] dispatch skipped for ${featureId}: no valid session (sessionId=${sessionId ?? 'null'})`,
|
|
1205
|
+
);
|
|
1203
1206
|
return false;
|
|
1204
1207
|
}
|
|
1205
1208
|
|
|
1206
1209
|
const provider = this.port.getProvider();
|
|
1207
1210
|
if (!provider?.sendMessage) {
|
|
1211
|
+
console.warn(
|
|
1212
|
+
`[reconciler] dispatch skipped for ${featureId}: provider does not support sendMessage`,
|
|
1213
|
+
);
|
|
1208
1214
|
return false;
|
|
1209
1215
|
}
|
|
1210
1216
|
|
|
@@ -1227,7 +1233,8 @@ export class GitReconciliationService {
|
|
|
1227
1233
|
try {
|
|
1228
1234
|
await provider.sendMessage(sessionId, message);
|
|
1229
1235
|
} catch (error) {
|
|
1230
|
-
|
|
1236
|
+
// Any send failure triggers a session refresh attempt — not just "No conversation found"
|
|
1237
|
+
if (this.port.refreshReconcilerSession) {
|
|
1231
1238
|
const refreshedSessionId = await this.port.refreshReconcilerSession().catch(() => null);
|
|
1232
1239
|
if (
|
|
1233
1240
|
refreshedSessionId &&
|
|
@@ -1246,15 +1253,20 @@ export class GitReconciliationService {
|
|
|
1246
1253
|
} catch (retryError) {
|
|
1247
1254
|
await this.appendReconciliationLog(
|
|
1248
1255
|
featureId,
|
|
1249
|
-
`
|
|
1256
|
+
`dispatch_failed_after_refresh: ${errorMessage(retryError)} session_id=${refreshedSessionId}`,
|
|
1250
1257
|
);
|
|
1251
1258
|
return false;
|
|
1252
1259
|
}
|
|
1253
1260
|
}
|
|
1261
|
+
await this.appendReconciliationLog(
|
|
1262
|
+
featureId,
|
|
1263
|
+
`dispatch_failed: ${errorMessage(error)} session_id=${sessionId} (refresh returned ${refreshedSessionId ?? 'null'})`,
|
|
1264
|
+
);
|
|
1265
|
+
return false;
|
|
1254
1266
|
}
|
|
1255
1267
|
await this.appendReconciliationLog(
|
|
1256
1268
|
featureId,
|
|
1257
|
-
`dispatch_failed: ${errorMessage(error)} session_id=${sessionId}`,
|
|
1269
|
+
`dispatch_failed: ${errorMessage(error)} session_id=${sessionId} (no refresh available)`,
|
|
1258
1270
|
);
|
|
1259
1271
|
return false;
|
|
1260
1272
|
}
|
|
@@ -593,145 +593,177 @@ export class PlanningWaveExecutor {
|
|
|
593
593
|
async runPostQaReconciliation(featureIds: string[], iteration: number): Promise<void> {
|
|
594
594
|
await this.plannerSessionSync?.syncPlannerSessions(featureIds);
|
|
595
595
|
for (const featureId of featureIds) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
596
|
+
try {
|
|
597
|
+
const context = await this.toolCaller.callTool<FeatureContextPayload>(
|
|
598
|
+
'planner',
|
|
599
|
+
TOOLS.FEATURE_GET_CONTEXT,
|
|
600
|
+
{
|
|
601
|
+
feature_id: featureId,
|
|
602
|
+
},
|
|
603
|
+
);
|
|
603
604
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
605
|
+
const frontMatter = asRecord(asRecord(context.data.state).front_matter);
|
|
606
|
+
const status = readStatus(frontMatter.status);
|
|
607
|
+
if (!isPostQaStatus(status)) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (
|
|
611
|
+
status === STATUS.BLOCKED &&
|
|
612
|
+
resolvePlannerPhaseFromContext(context.data) === 'intake'
|
|
613
|
+
) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
612
616
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
617
|
+
const existingPlan = asRecord(context.data.plan);
|
|
618
|
+
if (Object.keys(existingPlan).length === 0) {
|
|
619
|
+
await this.appendDecisionLog(featureId, {
|
|
620
|
+
phase: 'post_qa_reconciliation',
|
|
621
|
+
iteration,
|
|
622
|
+
plan_decision: 'unchanged',
|
|
623
|
+
execution_disposition: 'blocked_other',
|
|
624
|
+
reasons: ['missing_plan'],
|
|
625
|
+
status,
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
625
629
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
630
|
+
const planVersion = readPlanVersion(existingPlan);
|
|
631
|
+
if (planVersion == null) {
|
|
632
|
+
await this.appendDecisionLog(featureId, {
|
|
633
|
+
phase: 'post_qa_reconciliation',
|
|
634
|
+
iteration,
|
|
635
|
+
plan_decision: 'unchanged',
|
|
636
|
+
execution_disposition: 'blocked_other',
|
|
637
|
+
reasons: ['existing_plan_version_invalid'],
|
|
638
|
+
status,
|
|
639
|
+
});
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const decision = await this.evaluateReconciliationDecision(
|
|
644
|
+
context.data,
|
|
645
|
+
existingPlan,
|
|
634
646
|
status,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
647
|
+
);
|
|
648
|
+
if (this.shouldInvokePlannerForPlanTraceContract(decision)) {
|
|
649
|
+
const plannerDecision = await this.workerDecisionRunner.execute({
|
|
650
|
+
role: 'planner',
|
|
651
|
+
featureId,
|
|
652
|
+
contextBundle: {
|
|
653
|
+
...asRecord(context.data),
|
|
654
|
+
plan_trace_contract: structuredClone(decision.planTraceContract),
|
|
655
|
+
},
|
|
656
|
+
instructions:
|
|
657
|
+
'The accepted plan_trace no longer matches the verified manifest contract. Review plan_trace_contract, revise the accepted plan so every verified manifest obligation is correctly traced, remove obsolete obligation mappings, and emit PLAN_SUBMISSION or REQUEST.action=amend_plan. Do not leave the stale plan_trace unchanged.',
|
|
658
|
+
});
|
|
638
659
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
660
|
+
await this.appendDecisionLog(featureId, {
|
|
661
|
+
phase: 'post_qa_reconciliation',
|
|
662
|
+
iteration,
|
|
663
|
+
plan_decision:
|
|
664
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
665
|
+
? 'update_required'
|
|
666
|
+
: 'unchanged',
|
|
667
|
+
execution_disposition:
|
|
668
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
669
|
+
? 'retry_build'
|
|
670
|
+
: decision.executionDisposition,
|
|
671
|
+
status,
|
|
672
|
+
reasons:
|
|
673
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
674
|
+
? normalizeList([
|
|
675
|
+
...decision.reasons,
|
|
676
|
+
'planner_verified_manifest_trace_revision_requested',
|
|
677
|
+
])
|
|
678
|
+
: decision.reasons,
|
|
679
|
+
qa_summary: decision.qaSummary,
|
|
680
|
+
latest_gate_overall: decision.latestGateOverall,
|
|
681
|
+
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
682
|
+
checkpoint_validity: decision.checkpointValidity,
|
|
683
|
+
gate_failure_context: decision.gateFailureContext,
|
|
684
|
+
edge_case_checklist: decision.edgeCaseChecklist,
|
|
685
|
+
plan_trace_contract: decision.planTraceContract,
|
|
686
|
+
});
|
|
655
687
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
670
|
-
? normalizeList([
|
|
671
|
-
...decision.reasons,
|
|
672
|
-
'planner_verified_manifest_trace_revision_requested',
|
|
673
|
-
])
|
|
674
|
-
: decision.reasons,
|
|
675
|
-
qa_summary: decision.qaSummary,
|
|
676
|
-
latest_gate_overall: decision.latestGateOverall,
|
|
677
|
-
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
678
|
-
checkpoint_validity: decision.checkpointValidity,
|
|
679
|
-
gate_failure_context: decision.gateFailureContext,
|
|
680
|
-
edge_case_checklist: decision.edgeCaseChecklist,
|
|
681
|
-
plan_trace_contract: decision.planTraceContract,
|
|
682
|
-
});
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (this.shouldInvokePlannerForGateFailure(decision)) {
|
|
691
|
+
const plannerDecision = await this.workerDecisionRunner.execute({
|
|
692
|
+
role: 'planner',
|
|
693
|
+
featureId,
|
|
694
|
+
contextBundle: {
|
|
695
|
+
...asRecord(context.data),
|
|
696
|
+
gate_failure_context: structuredClone(decision.gateFailureContext),
|
|
697
|
+
},
|
|
698
|
+
instructions:
|
|
699
|
+
'A required gate is currently failing with fresh evidence. Review gate_failure_context, revise the accepted plan as needed so the builder can legally investigate and fix the failure, and emit PLAN_SUBMISSION or REQUEST.action=amend_plan when a plan revision is required.',
|
|
700
|
+
});
|
|
683
701
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
702
|
+
await this.appendDecisionLog(featureId, {
|
|
703
|
+
phase: 'post_qa_reconciliation',
|
|
704
|
+
iteration,
|
|
705
|
+
plan_decision:
|
|
706
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
707
|
+
? 'update_required'
|
|
708
|
+
: 'unchanged',
|
|
709
|
+
execution_disposition:
|
|
710
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
711
|
+
? 'retry_build'
|
|
712
|
+
: decision.executionDisposition,
|
|
713
|
+
status,
|
|
714
|
+
reasons:
|
|
715
|
+
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
716
|
+
? normalizeList([...decision.reasons, 'planner_gate_failure_revision_requested'])
|
|
717
|
+
: decision.reasons,
|
|
718
|
+
qa_summary: decision.qaSummary,
|
|
719
|
+
latest_gate_overall: decision.latestGateOverall,
|
|
720
|
+
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
721
|
+
checkpoint_validity: decision.checkpointValidity,
|
|
722
|
+
gate_failure_context: decision.gateFailureContext,
|
|
723
|
+
edge_case_checklist: decision.edgeCaseChecklist,
|
|
724
|
+
plan_trace_contract: decision.planTraceContract,
|
|
725
|
+
});
|
|
697
726
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
703
|
-
? 'update_required'
|
|
704
|
-
: 'unchanged',
|
|
705
|
-
execution_disposition:
|
|
706
|
-
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
707
|
-
? 'retry_build'
|
|
708
|
-
: decision.executionDisposition,
|
|
709
|
-
status,
|
|
710
|
-
reasons:
|
|
711
|
-
plannerDecision.planSubmission || plannerDecision.requestHandled
|
|
712
|
-
? normalizeList([...decision.reasons, 'planner_gate_failure_revision_requested'])
|
|
713
|
-
: decision.reasons,
|
|
714
|
-
qa_summary: decision.qaSummary,
|
|
715
|
-
latest_gate_overall: decision.latestGateOverall,
|
|
716
|
-
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
717
|
-
checkpoint_validity: decision.checkpointValidity,
|
|
718
|
-
gate_failure_context: decision.gateFailureContext,
|
|
719
|
-
edge_case_checklist: decision.edgeCaseChecklist,
|
|
720
|
-
plan_trace_contract: decision.planTraceContract,
|
|
721
|
-
});
|
|
727
|
+
if (plannerDecision.planSubmission || plannerDecision.requestHandled) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
722
731
|
|
|
723
|
-
if (
|
|
732
|
+
if (decision.planDecision === 'unchanged') {
|
|
733
|
+
await this.appendDecisionLog(featureId, {
|
|
734
|
+
phase: 'post_qa_reconciliation',
|
|
735
|
+
iteration,
|
|
736
|
+
plan_decision: decision.planDecision,
|
|
737
|
+
execution_disposition: decision.executionDisposition,
|
|
738
|
+
status,
|
|
739
|
+
reasons: decision.reasons,
|
|
740
|
+
qa_summary: decision.qaSummary,
|
|
741
|
+
latest_gate_overall: decision.latestGateOverall,
|
|
742
|
+
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
743
|
+
checkpoint_validity: decision.checkpointValidity,
|
|
744
|
+
gate_failure_context: decision.gateFailureContext,
|
|
745
|
+
edge_case_checklist: decision.edgeCaseChecklist,
|
|
746
|
+
plan_trace_contract: decision.planTraceContract,
|
|
747
|
+
});
|
|
724
748
|
continue;
|
|
725
749
|
}
|
|
726
|
-
}
|
|
727
750
|
|
|
728
|
-
|
|
751
|
+
const nextPlan = this.buildUpdatedPlan(existingPlan, planVersion, decision);
|
|
752
|
+
await this.toolCaller.callTool('planner', TOOLS.PLAN_UPDATE, {
|
|
753
|
+
feature_id: featureId,
|
|
754
|
+
expected_plan_version: planVersion,
|
|
755
|
+
plan_json: nextPlan,
|
|
756
|
+
});
|
|
757
|
+
|
|
729
758
|
await this.appendDecisionLog(featureId, {
|
|
730
759
|
phase: 'post_qa_reconciliation',
|
|
731
760
|
iteration,
|
|
732
761
|
plan_decision: decision.planDecision,
|
|
733
762
|
execution_disposition: decision.executionDisposition,
|
|
734
763
|
status,
|
|
764
|
+
expected_plan_version: planVersion,
|
|
765
|
+
next_plan_version: nextPlan.plan_version,
|
|
766
|
+
revision_reason: nextPlan.revision_reason,
|
|
735
767
|
reasons: decision.reasons,
|
|
736
768
|
qa_summary: decision.qaSummary,
|
|
737
769
|
latest_gate_overall: decision.latestGateOverall,
|
|
@@ -741,34 +773,16 @@ export class PlanningWaveExecutor {
|
|
|
741
773
|
edge_case_checklist: decision.edgeCaseChecklist,
|
|
742
774
|
plan_trace_contract: decision.planTraceContract,
|
|
743
775
|
});
|
|
744
|
-
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (this.isFailRunPolicyError(error)) {
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
781
|
+
console.warn(
|
|
782
|
+
`[post-qa] feature ${featureId} failed, continuing with remaining features: ${msg}`,
|
|
783
|
+
);
|
|
784
|
+
await this.blockFeatureOnUnhandledError(featureId, error);
|
|
745
785
|
}
|
|
746
|
-
|
|
747
|
-
const nextPlan = this.buildUpdatedPlan(existingPlan, planVersion, decision);
|
|
748
|
-
await this.toolCaller.callTool('planner', TOOLS.PLAN_UPDATE, {
|
|
749
|
-
feature_id: featureId,
|
|
750
|
-
expected_plan_version: planVersion,
|
|
751
|
-
plan_json: nextPlan,
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
await this.appendDecisionLog(featureId, {
|
|
755
|
-
phase: 'post_qa_reconciliation',
|
|
756
|
-
iteration,
|
|
757
|
-
plan_decision: decision.planDecision,
|
|
758
|
-
execution_disposition: decision.executionDisposition,
|
|
759
|
-
status,
|
|
760
|
-
expected_plan_version: planVersion,
|
|
761
|
-
next_plan_version: nextPlan.plan_version,
|
|
762
|
-
revision_reason: nextPlan.revision_reason,
|
|
763
|
-
reasons: decision.reasons,
|
|
764
|
-
qa_summary: decision.qaSummary,
|
|
765
|
-
latest_gate_overall: decision.latestGateOverall,
|
|
766
|
-
gate_evidence_freshness: decision.gateEvidenceFreshness,
|
|
767
|
-
checkpoint_validity: decision.checkpointValidity,
|
|
768
|
-
gate_failure_context: decision.gateFailureContext,
|
|
769
|
-
edge_case_checklist: decision.edgeCaseChecklist,
|
|
770
|
-
plan_trace_contract: decision.planTraceContract,
|
|
771
|
-
});
|
|
772
786
|
}
|
|
773
787
|
}
|
|
774
788
|
|
|
@@ -717,9 +717,14 @@ export class RunCoordinator {
|
|
|
717
717
|
console.warn(
|
|
718
718
|
`[reconciliation] coordinator dispatched reconciler for blocked feature ${featureId}`,
|
|
719
719
|
);
|
|
720
|
+
} else {
|
|
721
|
+
console.warn(
|
|
722
|
+
`[reconciliation] coordinator redrive returned false for ${featureId} — reconciler session may not be available`,
|
|
723
|
+
);
|
|
720
724
|
}
|
|
721
|
-
} catch {
|
|
722
|
-
|
|
725
|
+
} catch (err) {
|
|
726
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
727
|
+
console.warn(`[reconciliation] coordinator redrive failed for ${featureId}: ${msg}`);
|
|
723
728
|
}
|
|
724
729
|
}
|
|
725
730
|
continue;
|
|
@@ -1863,7 +1863,9 @@ describe('GitReconciliationService — internal queue and state branches', () =>
|
|
|
1863
1863
|
);
|
|
1864
1864
|
expect(fsAppendFileMock).toHaveBeenCalledWith(
|
|
1865
1865
|
expect.stringContaining('reconciliation.log'),
|
|
1866
|
-
expect.stringContaining(
|
|
1866
|
+
expect.stringContaining(
|
|
1867
|
+
'dispatch_failed_after_refresh: retry dispatch failed session_id=reconciler:2',
|
|
1868
|
+
),
|
|
1867
1869
|
'utf8',
|
|
1868
1870
|
);
|
|
1869
1871
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -188,6 +188,46 @@ describe('OrganizerAgentEnrollmentScheduler', () => {
|
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
it('GIVEN_artifact_missing_twice_WHEN_schedule_called_twice_THEN_warns_only_once', async () => {
|
|
192
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
193
|
+
const scheduler = new OrganizerAgentEnrollmentScheduler({
|
|
194
|
+
orderingArtifactPath: artifactPath,
|
|
195
|
+
fallback: new CapacityAwareEnrollmentScheduler(),
|
|
196
|
+
});
|
|
197
|
+
const input = {
|
|
198
|
+
activeFeatureIds: [] as string[],
|
|
199
|
+
queuedFeatureIds: [] as string[],
|
|
200
|
+
assignedFeatureIds: [] as string[],
|
|
201
|
+
maxActiveFeatures: 3,
|
|
202
|
+
request: makeRequest(),
|
|
203
|
+
features: [{ feature_id: 'spec_a', status: null }],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await scheduler.schedule(input);
|
|
207
|
+
await scheduler.schedule(input);
|
|
208
|
+
|
|
209
|
+
const missingWarns = warnSpy.mock.calls.filter((c) =>
|
|
210
|
+
String(c[0]).includes('Artifact missing'),
|
|
211
|
+
);
|
|
212
|
+
expect(missingWarns).toHaveLength(1);
|
|
213
|
+
|
|
214
|
+
// Now write a valid artifact — the warn flag should reset
|
|
215
|
+
const artifact = makeArtifact({ ready_set: ['spec_a'] });
|
|
216
|
+
await writeFile(artifactPath, JSON.stringify(artifact), 'utf8');
|
|
217
|
+
await scheduler.schedule(input);
|
|
218
|
+
|
|
219
|
+
// Remove the artifact again — should warn once more
|
|
220
|
+
const { rm: fsRm } = await import('node:fs/promises');
|
|
221
|
+
await fsRm(artifactPath, { force: true });
|
|
222
|
+
await scheduler.schedule(input);
|
|
223
|
+
|
|
224
|
+
const allMissingWarns = warnSpy.mock.calls.filter((c) =>
|
|
225
|
+
String(c[0]).includes('Artifact missing'),
|
|
226
|
+
);
|
|
227
|
+
expect(allMissingWarns).toHaveLength(2);
|
|
228
|
+
warnSpy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
191
231
|
it('GIVEN_artifact_with_partial_status_WHEN_schedule_THEN_delegates_to_fallback', async () => {
|
|
192
232
|
const artifact = makeArtifact({
|
|
193
233
|
completion_status: 'partial',
|
|
@@ -2007,6 +2007,70 @@ describe('PlanningWaveExecutor live watchdog and semantic policies', () => {
|
|
|
2007
2007
|
warnSpy.mockRestore();
|
|
2008
2008
|
});
|
|
2009
2009
|
|
|
2010
|
+
it('isolates post-QA reconciliation errors per feature instead of failing the loop', async () => {
|
|
2011
|
+
const contextCallFeatureIds: string[] = [];
|
|
2012
|
+
const toolCaller = {
|
|
2013
|
+
callTool: vi.fn(async (_role: string, toolName: string, args: Record<string, unknown>) => {
|
|
2014
|
+
if (toolName === TOOLS.FEATURE_GET_CONTEXT) {
|
|
2015
|
+
const fid = args.feature_id as string;
|
|
2016
|
+
contextCallFeatureIds.push(fid);
|
|
2017
|
+
if (fid === 'feature-a') {
|
|
2018
|
+
throw { normalizedResponse: { error: { code: 'plan_validation_failed' } } };
|
|
2019
|
+
}
|
|
2020
|
+
// feature-b: not in post-QA status, so it will be skipped via isPostQaStatus
|
|
2021
|
+
return {
|
|
2022
|
+
data: {
|
|
2023
|
+
state: { front_matter: { status: STATUS.PLANNING, version: 1 } },
|
|
2024
|
+
plan: {},
|
|
2025
|
+
intake: {},
|
|
2026
|
+
},
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
return { ok: true, data: {} };
|
|
2030
|
+
}),
|
|
2031
|
+
};
|
|
2032
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
2033
|
+
const executor = new PlanningWaveExecutor({
|
|
2034
|
+
toolCaller: toolCaller as never,
|
|
2035
|
+
planGenerator: { generateInitialPlan: vi.fn() } as never,
|
|
2036
|
+
providerMode: 'live',
|
|
2037
|
+
workerDecisionRunner: { execute: vi.fn() },
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
await executor.runPostQaReconciliation(['feature-a', 'feature-b'], 1);
|
|
2041
|
+
|
|
2042
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
2043
|
+
expect.stringContaining('[post-qa] feature feature-a failed'),
|
|
2044
|
+
);
|
|
2045
|
+
// feature-b was still reached despite feature-a throwing
|
|
2046
|
+
expect(contextCallFeatureIds).toEqual(['feature-a', 'feature-b']);
|
|
2047
|
+
warnSpy.mockRestore();
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
it('blocks feature on tool dispatch errors with normalizedResponse instead of treating as provider failure', async () => {
|
|
2051
|
+
const toolCaller = createPlanningToolCaller([STATUS.PLANNING]);
|
|
2052
|
+
const toolDispatchError = {
|
|
2053
|
+
normalizedResponse: { error: { code: 'plan_validation_failed', message: 'schema fail' } },
|
|
2054
|
+
};
|
|
2055
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
2056
|
+
const executor = new PlanningWaveExecutor({
|
|
2057
|
+
toolCaller: toolCaller as never,
|
|
2058
|
+
planGenerator: { generateInitialPlan: vi.fn() } as never,
|
|
2059
|
+
providerMode: 'live',
|
|
2060
|
+
workerDecisionRunner: {
|
|
2061
|
+
execute: vi.fn(async () => {
|
|
2062
|
+
throw toolDispatchError;
|
|
2063
|
+
}),
|
|
2064
|
+
},
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
await executor.run(['feature-a']);
|
|
2068
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
2069
|
+
expect.stringContaining('[planning-wave] feature feature-a failed'),
|
|
2070
|
+
);
|
|
2071
|
+
warnSpy.mockRestore();
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2010
2074
|
it('throws provider spawn failure when fail-run policy is configured', async () => {
|
|
2011
2075
|
const toolCaller = createPlanningToolCaller([STATUS.PLANNING]);
|
|
2012
2076
|
const runtimeError = new Error('runtime unavailable') as Error & { code?: string };
|
|
@@ -3281,6 +3281,49 @@ describe('RunCoordinator notification and budget branches', () => {
|
|
|
3281
3281
|
},
|
|
3282
3282
|
});
|
|
3283
3283
|
});
|
|
3284
|
+
|
|
3285
|
+
it('GIVEN_blocked_feature_with_mainline_conflict_WHEN_recoverBlockedFeatures_runs_THEN_attempts_reconciler_redrive', async () => {
|
|
3286
|
+
const redriveMock = vi.fn(async () => false);
|
|
3287
|
+
const { coordinator, kernel } = makeCoordinatorDeps({
|
|
3288
|
+
toolCallerImpl: async (_role: string, toolName: string) => {
|
|
3289
|
+
if (toolName === TOOLS.FEATURE_STATE_GET) {
|
|
3290
|
+
return {
|
|
3291
|
+
ok: true,
|
|
3292
|
+
data: {
|
|
3293
|
+
front_matter: {
|
|
3294
|
+
version: 5,
|
|
3295
|
+
status: STATUS.BLOCKED,
|
|
3296
|
+
status_reason: 'mainline_divergence:blocked',
|
|
3297
|
+
gates: { plan: 'na', fast: 'na', full: 'na' },
|
|
3298
|
+
evidence: {},
|
|
3299
|
+
checkpoints: [{ checkpoint_id: 'cp-1', net_new_worktree_change: false }],
|
|
3300
|
+
conflicts: [
|
|
3301
|
+
{
|
|
3302
|
+
type: 'mainline_divergence',
|
|
3303
|
+
resolution_status: 'in_progress',
|
|
3304
|
+
conflicting_files: ['src/a.ts'],
|
|
3305
|
+
},
|
|
3306
|
+
],
|
|
3307
|
+
},
|
|
3308
|
+
},
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
return { ok: true, data: {} };
|
|
3312
|
+
},
|
|
3313
|
+
});
|
|
3314
|
+
(kernel as Record<string, unknown>).redrivePreparedReconcilerConflict = redriveMock;
|
|
3315
|
+
|
|
3316
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
3317
|
+
try {
|
|
3318
|
+
await (coordinator as any).recoverBlockedFeatures(['feature_conflict']);
|
|
3319
|
+
expect(redriveMock).toHaveBeenCalledWith('feature_conflict');
|
|
3320
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
3321
|
+
expect.stringContaining('[reconciliation] coordinator redrive returned false'),
|
|
3322
|
+
);
|
|
3323
|
+
} finally {
|
|
3324
|
+
warnSpy.mockRestore();
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3284
3327
|
});
|
|
3285
3328
|
|
|
3286
3329
|
describe('RunCoordinator checkpoint freshness helpers', () => {
|
|
@@ -2334,6 +2334,34 @@ describe('QaWaveExecutor live policy branches', () => {
|
|
|
2334
2334
|
});
|
|
2335
2335
|
});
|
|
2336
2336
|
|
|
2337
|
+
it('GIVEN_tool_dispatch_error_with_normalizedResponse_WHEN_run_THEN_blocks_feature_instead_of_failing_run', async () => {
|
|
2338
|
+
const deps = qaDependencies(qaToolCaller());
|
|
2339
|
+
const toolError = {
|
|
2340
|
+
normalizedResponse: { error: { code: 'qa_validation_failed', message: 'schema fail' } },
|
|
2341
|
+
};
|
|
2342
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
2343
|
+
const executor = new QaWaveExecutor({
|
|
2344
|
+
kernel: deps.kernel as never,
|
|
2345
|
+
provider: deps.provider as never,
|
|
2346
|
+
toolCaller: deps.toolCaller as never,
|
|
2347
|
+
promptProvider: deps.promptProvider as never,
|
|
2348
|
+
featureClusterPatcher: deps.featureClusterPatcher as never,
|
|
2349
|
+
state: deps.state as never,
|
|
2350
|
+
providerMode: 'live',
|
|
2351
|
+
workerDecisionRunner: {
|
|
2352
|
+
execute: vi.fn(async () => {
|
|
2353
|
+
throw toolError;
|
|
2354
|
+
}),
|
|
2355
|
+
},
|
|
2356
|
+
});
|
|
2357
|
+
|
|
2358
|
+
await executor.run(['feature-a'], 1);
|
|
2359
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
2360
|
+
expect.stringContaining('[qa-wave] feature feature-a failed'),
|
|
2361
|
+
);
|
|
2362
|
+
warnSpy.mockRestore();
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2337
2365
|
it('GIVEN_status_transitions_blocked_to_qa_WHEN_run_THEN_resets_stall_tracking', async () => {
|
|
2338
2366
|
const deps = qaDependencies(qaToolCaller([STATUS.BLOCKED, STATUS.QA]));
|
|
2339
2367
|
const outputLoopDetector = {
|